Example Dysmalpy 2D fitting

In this example, we use dysmalpy to measure the kinematics of galaxy GS4_43501 at \(z=1.613\). This notebook shows how to find the best fit models for the two-dimensional velocity and velocity dispersion profiles using \(\texttt{MPFIT}\) as well as \(\texttt{MCMC}\). These fits allow us to measure quantities such as the total mass (disk+bulge), the effective radius \(r_\mathrm{eff}\), dark matter fraction \(f_\mathrm{DM}\) and velocity dispersion \(\sigma_0\).

The fitting includes the following components:

  • Disk + Bulge

  • NFW halo

  • Constant velocity dispersion

The structure of the notebook is the following:

  1. Setup steps

  2. Initialize galaxy, model set, instrument

  3. MPFIT fitting

  4. MCMC fitting

  5. Visualize a real MCMC example

  6. Nested sampling fitting

  7. Visualize a real Nested Sampling example

1) Setup steps

Import modules

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import dysmalpy
from dysmalpy import galaxy
from dysmalpy import models
from dysmalpy import fitting
from dysmalpy import instrument
from dysmalpy import data_classes
from dysmalpy import parameters
from dysmalpy import plotting
from dysmalpy import aperture_classes
from dysmalpy import observation
from dysmalpy import config

import os
import copy

import numpy as np
import astropy.units as u
import astropy.io.fits as fits
INFO:numexpr.utils:Note: NumExpr detected 10 cores but "NUMEXPR_MAX_THREADS" not set, so enforcing safe limit of 8.
INFO:numexpr.utils:NumExpr defaulting to 8 threads.
# A check for compatibility:
import emcee
if int(emcee.__version__[0]) >= 3:
    ftype_sampler = 'h5'
else:
    ftype_sampler = 'pickle' 

Setup notebook

# Setup plotting
import matplotlib as mpl
import matplotlib.pyplot as plt
%matplotlib inline

mpl.rcParams['figure.dpi']= 100
mpl.rc("savefig", dpi=300)

from IPython.core.display import Image
import logging
logger = logging.getLogger('DysmalPy')
logger.setLevel(logging.INFO)

Set data, output paths

# Data directory
dir_path = os.path.abspath(fitting.__file__)
data_dir = os.sep.join(os.path.dirname(dir_path).split(os.sep)[:-1]+["tests", "test_data", ""])
#'/YOUR/DATA/PATH/'
print(data_dir)

# Where to save output files (see examples below)
#outdir = '/Users/sedona/data/dysmalpy_test_examples/JUPYTER_OUTPUT_2D/'
outdir = '/Users/jespejo/Dropbox/Postdoc/Data/dysmalpy_test_examples/JUPYTER_OUTPUT_2D/'
/Users/jespejo/anaconda3/envs/test_dysmalpy/lib/python3.11/site-packages/dysmalpy/tests/test_data/

Load the function to tie the scale height to the disk effective radius

from dysmalpy.fitting_wrappers.tied_functions import tie_sigz_reff

Load the function to tie Mvirial to \(f_{DM}(R_e)\)

from dysmalpy.fitting_wrappers.tied_functions import tie_lmvirial_NFW

Info

Also see fitting_wrappers.tied_functions for more tied functions


2) Initialize galaxy, model set, instrument

gal = galaxy.Galaxy(z=1.613, name='GS4_43501')
mod_set = models.ModelSet()

Baryonic component: Combined Disk+Bulge

total_mass = 11.0    # M_sun
bt = 0.3             # Bulge-Total ratio
r_eff_disk = 5.0     # kpc
n_disk = 1.0
invq_disk = 5.0
r_eff_bulge = 1.0    # kpc
n_bulge = 4.0
invq_bulge = 1.0
noord_flat = True    # Switch for applying Noordermeer flattening

# Fix components
bary_fixed = {'total_mass': False,
              'r_eff_disk': False, #True,
              'n_disk': True,
              'r_eff_bulge': True,
              'n_bulge': True,
              'bt': True}

# Set bounds
bary_bounds = {'total_mass': (10, 13),
               'r_eff_disk': (1.0, 30.0),
               'n_disk': (1, 8),
               'r_eff_bulge': (1, 5),
               'n_bulge': (1, 8),
               'bt': (0, 1)}

bary = models.DiskBulge(total_mass=total_mass, bt=bt,
                        r_eff_disk=r_eff_disk, n_disk=n_disk,
                        invq_disk=invq_disk,
                        r_eff_bulge=r_eff_bulge, n_bulge=n_bulge,
                        invq_bulge=invq_bulge,
                        noord_flat=noord_flat,
                        name='disk+bulge',
                        fixed=bary_fixed, bounds=bary_bounds)

bary.r_eff_disk.prior = parameters.BoundedGaussianPrior(center=5.0, stddev=1.0)

Halo component

mvirial = 12.0
conc = 5.0
fdm = 0.5

halo_fixed = {'mvirial': False,
              'conc': True, 
              'fdm':  False}
# Mvirial will be tied -- so must set 'fixed=False' for Mvirial...

halo_bounds = {'mvirial': (10, 13),
               'conc': (1, 20),
               'fdm': (0, 1)}

halo = models.NFW(mvirial=mvirial, conc=conc, fdm=fdm, z=gal.z,
                  fixed=halo_fixed, bounds=halo_bounds, name='halo')


halo.mvirial.tied = tie_lmvirial_NFW

Dispersion profile

sigma0 = 39.   # km/s
disp_fixed = {'sigma0': False}
disp_bounds = {'sigma0': (5, 300)}

disp_prof = models.DispersionConst(sigma0=sigma0, fixed=disp_fixed,
                                          bounds=disp_bounds, name='dispprof', tracer='halpha')

z-height profile

sigmaz = 0.9   # kpc
zheight_fixed = {'sigmaz': False}

zheight_prof = models.ZHeightGauss(sigmaz=sigmaz, name='zheightgaus',
                                   fixed=zheight_fixed)
zheight_prof.sigmaz.tied = tie_sigz_reff

Geometry

inc = 62.     # degrees
pa = 142.     # degrees, blue-shifted side CCW from north
xshift = 0    # pixels from center
yshift = 0    # pixels from center
vel_shift = 0 # velocity shift at center ; km/s
geom_fixed = {'inc': False,
              'pa': False,
              'xshift': False,
              'yshift': False,
              'vel_shift': False}

geom_bounds = {'inc': (52, 72),
               'pa': (132, 152),
               'xshift': (-2.5, 2.5),
               'yshift': (-2.5, 2.5),
               'vel_shift': (-100, 100)}

geom = models.Geometry(inc=inc, pa=pa, xshift=xshift, yshift=yshift, vel_shift=vel_shift, 
                       fixed=geom_fixed, bounds=geom_bounds, 
                       name='geom', obs_name='halpha_2D')

geom.inc.prior = parameters.BoundedSineGaussianPrior(center=62, stddev=0.1)

Add all model components to ModelSet

# Add all of the model components to the ModelSet
mod_set.add_component(bary, light=True)
mod_set.add_component(halo)
mod_set.add_component(disp_prof)
mod_set.add_component(zheight_prof)
mod_set.add_component(geom)

Set kinematic options for calculating velocity profile

mod_set.kinematic_options.adiabatic_contract = False
mod_set.kinematic_options.pressure_support = True

Set up the observation and instrument

obs = observation.Observation(name='halpha_2D', tracer='halpha')
inst = instrument.Instrument()

beamsize = 0.55*u.arcsec                 # FWHM of beam
sig_inst = 45*u.km/u.s                   # Instrumental spectral resolution

beam = instrument.GaussianBeam(major=beamsize)
lsf = instrument.LSF(sig_inst)

inst.beam = beam
inst.lsf = lsf
inst.pixscale = 0.125*u.arcsec           # arcsec/pixel
inst.fov = [27, 27]                      # (nx, ny) pixels
inst.spec_type = 'velocity'              # 'velocity' or 'wavelength'
inst.spec_step = 10*u.km/u.s             # Spectral step
inst.spec_start = -1000*u.km/u.s         # Starting value of spectrum
inst.nspec = 201                         # Number of spectral pixels

# Set the beam kernel so it doesn't have to be calculated every step
inst.set_beam_kernel()
inst.set_lsf_kernel()

# Extraction information
inst.ndim = 2                            # Dimensionality of data
inst.moment = False                      # For 1D/2D data, if True then velocities and dispersion calculated from moments
                                         # Default is False, meaning Gaussian extraction used
    
# Add instrument to observation
obs.instrument = inst

Load data

  • Load the data from file:

    • 2D velocity, dispersion maps and error

    • A mask can be loaded / created as well

  • Put data in Data2D class

  • Add data to Galaxy object

gal_vel = fits.getdata(data_dir+'GS4_43501_Ha_vm.fits')
gal_disp = fits.getdata(data_dir+'GS4_43501_Ha_dm.fits')

err_vel = fits.getdata(data_dir+'GS4_43501_Ha_vm_err.fits')
err_disp = fits.getdata(data_dir+'GS4_43501_Ha_dm_err.fits')

mask = fits.getdata(data_dir+'GS4_43501_Ha_m.fits')

#gal_disp[(gal_disp > 1000.) | (~np.isfinite(gal_disp))] = -1e6
#mask[(gs4_disp < 0)] = 0

inst_corr = True                  # Flag for if the measured dispersion has been
                                  # corrected for instrumental resolution

# Mask NaNs:
mask[~np.isfinite(gal_vel)] = 0
gal_vel[~np.isfinite(gal_vel)] = 0.

mask[~np.isfinite(err_vel)] = 0
err_vel[~np.isfinite(err_vel)] = 0.

mask[~np.isfinite(gal_disp)] = 0
gal_disp[~np.isfinite(gal_disp)] = 0.

mask[~np.isfinite(err_disp)] = 0
err_disp[~np.isfinite(err_disp)] = 0.
    

# Put data in Data2D data class: 
#    ** specifies data pixscale as well **
data2d = data_classes.Data2D(pixscale=inst.pixscale.value, velocity=gal_vel,
                                  vel_disp=gal_disp, vel_err=err_vel,
                                  vel_disp_err=err_disp, mask=mask, 
                                  inst_corr=inst_corr)

# Use moment for extraction for example: 
#     Not how data was extracted, but it's a speedup for this example. 
#     
#     The best practic option, if the 2D maps are extracted with gaussians, 
#     is to compile the C++ extensions and pass the option 'gauss_extract_with_c=True'
#     when running fitting.fit_mpfit()
data2d.moment = True

# Add data to Observation:
obs.data = copy.deepcopy(data2d)

Define model, fit options:

obs.mod_options.oversample = 1  
# Factor by which to oversample model (eg, subpixels)
# For increased precision, oversample=3 is suggested. 

obs.fit_options.fit = True             # Include this observation in the fit (T/F)
obs.fit_options.fit_velocity = True    # 1D/2D: Fit velocity of observation (T/F)
obs.fit_options.fit_dispersion = True  # 1D/2D: Fit dispersion of observation (T/F)
obs.fit_options.fit_flux = False       # 1D/2D: Fit flux of observation (T/F)

Add the model set, observation to the Galaxy

gal.model = mod_set
gal.add_observation(obs)

3) MPFIT Fitting

Set up MPFITFitter fitter with fitting parameters

# Options passed to MPFIT:
maxiter = 200

fitter = fitting.MPFITFitter(maxiter=maxiter)

Set up fit/plot output options

# Output directory
outdir_mpfit = outdir+'MPFIT/'

# Output options: 
do_plotting = True  
plot_type = 'png'
overwrite = True

output_options = config.OutputOptions(outdir=outdir_mpfit, 
                                      do_plotting=do_plotting, 
                                      plot_type=plot_type,
                                      overwrite=overwrite)

Run Dysmalpy fitting: MPFIT

mpfit_results = fitter.fit(gal, output_options)
INFO:DysmalPy:*************************************
INFO:DysmalPy: Fitting: GS4_43501 using MPFIT
INFO:DysmalPy:    obs: halpha_2D
INFO:DysmalPy:        nSubpixels: 1
INFO:DysmalPy:    mvirial_tied: <function tie_lmvirial_NFW at 0x176a55da0>
INFO:DysmalPy:
MPFIT Fitting:
Start: 2024-05-02 10:47:55.313966

INFO:DysmalPy:Iter 1  CHI-SQUARE = 27260.87785  DOF = 403
   disk+bulge:total_mass = 11  
   disk+bulge:r_eff_disk = 5  
   halo:fdm = 0.5  
   dispprof:sigma0 = 39  
   geom:inc = 62  
   geom:pa = 142  
   geom:xshift = 0  
   geom:yshift = 0  
   geom:vel_shift = 0  

INFO:DysmalPy:Iter 2  CHI-SQUARE = 7174.918633  DOF = 403
   disk+bulge:total_mass = 11.01621875  
   disk+bulge:r_eff_disk = 5.697081099  
   halo:fdm = 0.290454085  
   dispprof:sigma0 = 41.78914475  
   geom:inc = 72  
   geom:pa = 142.4487865  
   geom:xshift = 0.01555573571  
   geom:yshift = -0.01483259239  
   geom:vel_shift = 0.688311908  

INFO:DysmalPy:Iter 3  CHI-SQUARE = 4845.547462  DOF = 403
   disk+bulge:total_mass = 11.00485748  
   disk+bulge:r_eff_disk = 7.299527889  
   halo:fdm = 0.3316062539  
   dispprof:sigma0 = 35.80898768  
   geom:inc = 72  
   geom:pa = 142.6165451  
   geom:xshift = 0.00498880543  
   geom:yshift = -0.01907966512  
   geom:vel_shift = 1.26046722  

INFO:DysmalPy:Iter 4  CHI-SQUARE = 4135.278964  DOF = 403
   disk+bulge:total_mass = 10.97290392  
   disk+bulge:r_eff_disk = 5.906465959  
   halo:fdm = 0.2268716489  
   dispprof:sigma0 = 39.19735991  
   geom:inc = 72  
   geom:pa = 142.9235737  
   geom:xshift = 0.006769462096  
   geom:yshift = -0.04087333416  
   geom:vel_shift = 3.910223933  

INFO:DysmalPy:Iter 5  CHI-SQUARE = 2681.1139  DOF = 403
   disk+bulge:total_mass = 10.99187836  
   disk+bulge:r_eff_disk = 5.260076359  
   halo:fdm = 0.06304476313  
   dispprof:sigma0 = 35.0014785  
   geom:inc = 72  
   geom:pa = 144.7019383  
   geom:xshift = 0.005638298412  
   geom:yshift = -0.08458184636  
   geom:vel_shift = 13.29443606  

INFO:DysmalPy:Iter 6  CHI-SQUARE = 1975.039779  DOF = 403
   disk+bulge:total_mass = 11.01752643  
   disk+bulge:r_eff_disk = 5.972062778  
   halo:fdm = 0.07041719829  
   dispprof:sigma0 = 33.79762307  
   geom:inc = 72  
   geom:pa = 144.82705  
   geom:xshift = 0.003838933716  
   geom:yshift = -0.08965685046  
   geom:vel_shift = 22.53154176  

INFO:DysmalPy:Iter 7  CHI-SQUARE = 1911.611202  DOF = 403
   disk+bulge:total_mass = 11.02762255  
   disk+bulge:r_eff_disk = 6.236754678  
   halo:fdm = 0.07706825794  
   dispprof:sigma0 = 35.78878353  
   geom:inc = 72  
   geom:pa = 145.0908239  
   geom:xshift = 0.002765540566  
   geom:yshift = -0.08522907649  
   geom:vel_shift = 24.69604616  

INFO:DysmalPy:Iter 8  CHI-SQUARE = 1874.721763  DOF = 403
   disk+bulge:total_mass = 11.05717868  
   disk+bulge:r_eff_disk = 6.756909482  
   halo:fdm = 0.08265623498  
   dispprof:sigma0 = 36.07195236  
   geom:inc = 72  
   geom:pa = 145.1249566  
   geom:xshift = 0.002925563871  
   geom:yshift = -0.09434522976  
   geom:vel_shift = 24.74136223  

INFO:DysmalPy:Iter 9  CHI-SQUARE = 1868.205783  DOF = 403
   disk+bulge:total_mass = 11.07330048  
   disk+bulge:r_eff_disk = 6.976880571  
   halo:fdm = 0.08303211692  
   dispprof:sigma0 = 39.11660957  
   geom:inc = 72  
   geom:pa = 145.0231882  
   geom:xshift = 0.002306094135  
   geom:yshift = -0.1027435952  
   geom:vel_shift = 24.51481928  

INFO:DysmalPy:Iter 10  CHI-SQUARE = 1834.891502  DOF = 403
   disk+bulge:total_mass = 11.12364485  
   disk+bulge:r_eff_disk = 7.783313281  
   halo:fdm = 0.05379420088  
   dispprof:sigma0 = 35.87322102  
   geom:inc = 72  
   geom:pa = 145.0823943  
   geom:xshift = 0.002328402216  
   geom:yshift = -0.1072879964  
   geom:vel_shift = 24.3734785  

INFO:DysmalPy:Iter 11  CHI-SQUARE = 1804.57498  DOF = 403
   disk+bulge:total_mass = 11.15233343  
   disk+bulge:r_eff_disk = 8.49550137  
   halo:fdm = 0.05314505098  
   dispprof:sigma0 = 35.22862485  
   geom:inc = 72  
   geom:pa = 145.2209654  
   geom:xshift = 0.00213117077  
   geom:yshift = -0.1118302835  
   geom:vel_shift = 25.7821227  

INFO:DysmalPy:Iter 12  CHI-SQUARE = 1765.875956  DOF = 403
   disk+bulge:total_mass = 11.1995498  
   disk+bulge:r_eff_disk = 9.671163543  
   halo:fdm = 0.0567955085  
   dispprof:sigma0 = 33.6639676  
   geom:inc = 72  
   geom:pa = 145.1054693  
   geom:xshift = 0.001771156208  
   geom:yshift = -0.1214819783  
   geom:vel_shift = 25.15294892  

INFO:DysmalPy:Iter 13  CHI-SQUARE = 1750.678931  DOF = 403
   disk+bulge:total_mass = 11.23072663  
   disk+bulge:r_eff_disk = 10.71370417  
   halo:fdm = 0.06951073943  
   dispprof:sigma0 = 33.20391113  
   geom:inc = 72  
   geom:pa = 145.5238662  
   geom:xshift = 0.001667999645  
   geom:yshift = -0.1229693221  
   geom:vel_shift = 25.23407271  

INFO:DysmalPy:Iter 14  CHI-SQUARE = 1749.269119  DOF = 403
   disk+bulge:total_mass = 11.20953224  
   disk+bulge:r_eff_disk = 10.26453885  
   halo:fdm = 0.07783456653  
   dispprof:sigma0 = 33.20999322  
   geom:inc = 72  
   geom:pa = 145.2975363  
   geom:xshift = 0.00150466758  
   geom:yshift = -0.1259508646  
   geom:vel_shift = 25.64745219  

INFO:DysmalPy:Iter 15  CHI-SQUARE = 1742.263826  DOF = 403
   disk+bulge:total_mass = 11.26350202  
   disk+bulge:r_eff_disk = 11.69794866  
   halo:fdm = 0.06604308184  
   dispprof:sigma0 = 30.21624009  
   geom:inc = 72  
   geom:pa = 145.2839822  
   geom:xshift = 0.001305084891  
   geom:yshift = -0.1274298711  
   geom:vel_shift = 24.36968127  

INFO:DysmalPy:Iter 16  CHI-SQUARE = 1739.337454  DOF = 403
   disk+bulge:total_mass = 11.27240108  
   disk+bulge:r_eff_disk = 12.14000166  
   halo:fdm = 0.06225635304  
   dispprof:sigma0 = 29.57297226  
   geom:inc = 72  
   geom:pa = 145.3862099  
   geom:xshift = 0.001253914203  
   geom:yshift = -0.1281740116  
   geom:vel_shift = 25.17378889  

INFO:DysmalPy:Iter 17  CHI-SQUARE = 1739.259112  DOF = 403
   disk+bulge:total_mass = 11.27160988  
   disk+bulge:r_eff_disk = 12.16727606  
   halo:fdm = 0.06791349056  
   dispprof:sigma0 = 29.6275998  
   geom:inc = 72  
   geom:pa = 145.5798563  
   geom:xshift = 0.00125420715  
   geom:yshift = -0.1281740091  
   geom:vel_shift = 25.16057188  

INFO:DysmalPy:Iter 18  CHI-SQUARE = 1739.231006  DOF = 403
   disk+bulge:total_mass = 11.27124049  
   disk+bulge:r_eff_disk = 12.16817228  
   halo:fdm = 0.06921169832  
   dispprof:sigma0 = 29.36534085  
   geom:inc = 72  
   geom:pa = 145.5474635  
   geom:xshift = 0.0012542433  
   geom:yshift = -0.1281740091  
   geom:vel_shift = 25.16393383  

INFO:DysmalPy:Iter 19  CHI-SQUARE = 1739.16602  DOF = 403
   disk+bulge:total_mass = 11.27116728  
   disk+bulge:r_eff_disk = 12.16748897  
   halo:fdm = 0.06849834081  
   dispprof:sigma0 = 29.39382619  
   geom:inc = 72  
   geom:pa = 145.5310079  
   geom:xshift = 0.001254262134  
   geom:yshift = -0.1281740093  
   geom:vel_shift = 25.16345192  

INFO:DysmalPy:Iter 20  CHI-SQUARE = 1739.088768  DOF = 403
   disk+bulge:total_mass = 11.27103747  
   disk+bulge:r_eff_disk = 12.16351352  
   halo:fdm = 0.06834328557  
   dispprof:sigma0 = 29.48910938  
   geom:inc = 72  
   geom:pa = 145.5112834  
   geom:xshift = 0.001254247263  
   geom:yshift = -0.12817401  
   geom:vel_shift = 25.16981092  

INFO:DysmalPy:Iter 21  CHI-SQUARE = 1739.068692  DOF = 403
   disk+bulge:total_mass = 11.27103113  
   disk+bulge:r_eff_disk = 12.1637523  
   halo:fdm = 0.06841855337  
   dispprof:sigma0 = 29.56525146  
   geom:inc = 72  
   geom:pa = 145.5131879  
   geom:xshift = 0.001254248468  
   geom:yshift = -0.12817401  
   geom:vel_shift = 25.17058334  

INFO:DysmalPy:Iter 22  CHI-SQUARE = 1739.068121  DOF = 403
   disk+bulge:total_mass = 11.27101952  
   disk+bulge:r_eff_disk = 12.16317579  
   halo:fdm = 0.06840690692  
   dispprof:sigma0 = 29.52972761  
   geom:inc = 72  
   geom:pa = 145.5064353  
   geom:xshift = 0.001254248505  
   geom:yshift = -0.12817401  
   geom:vel_shift = 25.17074722  

INFO:DysmalPy:
End: 2024-05-02 10:48:07.465394

******************
Time= 12.15 (sec),   0:12.15 (m:s)
MPFIT Status = 2
MPFIT Error/Warning Message = None
******************

Examine MPFIT results

Result plots

# Look at best-fit:
filepath = outdir_mpfit+"GS4_43501_mpfit_bestfit_halpha_2D.{}".format(plot_type)
Image(filename=filepath, width=600)
../../_images/9095812db6ba6f163d12255d140951801bac56cd9a16413c341d93b57eae5326.png

Directly generating result plots

Reload the galaxy, results files:

f_galmodel =       outdir_mpfit + 'GS4_43501_model.pickle'
f_mpfit_results =  outdir_mpfit + 'GS4_43501_mpfit_results.pickle'

gal, mpfit_results = fitting.reload_all_fitting(filename_galmodel=f_galmodel, 
                            filename_results=f_mpfit_results, fit_method='mpfit')

Plot the best-fit results:

mpfit_results.plot_results(gal)
../../_images/02dc815cc48dffba74005003bc6a5ec6bfddbc6aecd1e8eaff1256fc93c76c90.png

Result reports

We now look at the results reports, which include the best-fit values and uncertainties (as well as other fitting settings and output).

# Print report
print(mpfit_results.results_report(gal=gal))
###############################
 Fitting for GS4_43501

Date: 2024-05-02 10:48:08.695418

    obs: halpha_2D
         Datafiles:
         fit_velocity:           True
         fit_dispersion:         True
         fit_flux:               False
         moment:           False
         n_wholepix_z_min:      3
         oversample:            1
         oversize:              1


Fitting method: MPFIT
    fit status: 2

pressure_support:      True
pressure_support_type: 1

###############################
 Fitting results
-----------
 disk+bulge
    total_mass         11.2710  +/-   0.0105
    r_eff_disk         12.1632  +/-   0.3596

    n_disk              1.0000  [FIXED]
    r_eff_bulge         1.0000  [FIXED]
    n_bulge             4.0000  [FIXED]
    bt                  0.3000  [FIXED]
    mass_to_light       1.0000  [FIXED]

    noord_flat          True
-----------
 halo
    fdm                 0.0684  +/-   0.0017

    mvirial            10.3144  [TIED]
    conc                5.0000  [FIXED]
-----------
 dispprof
    sigma0             29.5297  +/-   1.0188
-----------
 zheightgaus
    sigmaz              2.0661  [TIED]
-----------
 geom
    inc                72.0000  +/-   0.0000
    pa                145.5064  +/-   0.3302
    xshift              0.0013  +/-   0.0001
    yshift             -0.1282  +/-   0.0056
    vel_shift          25.1707  +/-   0.4656

-----------
Adiabatic contraction: False

-----------
Red. chisq: 4.3153

-----------
obs halpha_2D: Rout,max,2D: 10.3258

To directly save the results report to a file, we can use the following:

(Note: by default, report files are saved as part of the fitting process)

# Save report to file:
f_mpfit_report = outdir_mpfit + 'mpfit_fit_report.txt'
mpfit_results.results_report(gal=gal, filename=f_mpfit_report)

4) MCMC Fitting

Get a clean copy of model, obs

gal = galaxy.Galaxy(z=1.613, name='GS4_43501')

obscopy = observation.Observation(name='halpha_2D', tracer='halpha')
obscopy.data = copy.deepcopy(data2d)
obscopy.instrument = copy.deepcopy(inst)
obscopy.mod_options = copy.deepcopy(obs.mod_options)
obscopy.fit_options = copy.deepcopy(obs.fit_options)

gal.add_observation(obscopy)

gal.model = copy.deepcopy(mod_set)

Set up MCMCFitter fitter with fitting parameters

# Options passed to emcee
## SHORT TEST:
nWalkers = 20
nCPUs = 4
scale_param_a = 5 #3   
# The walkers were not exploring the parameter space well with scale_param_a = 3, 
#     but were getting 'stuck'. So to improve the walker movement with larger 
#     'stretch' in the steps, we increased scale_param_a.
nBurn = 2
nSteps = 5
minAF = None
maxAF = None
nEff = 10

blob_name = 'mvirial'    # Also save 'blob' values of Mvirial, calculated at every chain step


fitter = fitting.MCMCFitter(nWalkers=nWalkers, nCPUs=nCPUs, 
                            scale_param_a=scale_param_a, nBurn=nBurn, nSteps=nSteps, 
                            minAF=minAF, maxAF=maxAF, nEff=nEff, blob_name=blob_name)

Set up fit/plot output options

# Output directory
outdir_mcmc = outdir + 'MCMC/'

# Output options: 
do_plotting = True  
plot_type = 'png'
overwrite = True

output_options = config.OutputOptions(outdir=outdir_mcmc, 
                                      do_plotting=do_plotting, 
                                      plot_type=plot_type,
                                      overwrite=overwrite)

Run Dysmalpy fitting: MCMC

mcmc_results = fitter.fit(gal, output_options)
INFO:DysmalPy:*************************************
INFO:DysmalPy: Fitting: GS4_43501 with MCMC
INFO:DysmalPy:    obs: halpha_2D
INFO:DysmalPy:        nSubpixels: 1
INFO:DysmalPy:
nCPUs: 4
INFO:DysmalPy:nWalkers: 20
INFO:DysmalPy:lnlike: oversampled_chisq=True
INFO:DysmalPy:
blobs: mvirial
INFO:DysmalPy:
mvirial_tied: <function tie_lmvirial_NFW at 0x176a55da0>
INFO:DysmalPy:
Burn-in:
Start: 2024-05-02 10:48:08.778789

INFO:DysmalPy: k=0, time.time=2024-05-02 10:48:08.779474, a_frac=nan
INFO:DysmalPy: k=1, time.time=2024-05-02 10:48:09.729780, a_frac=0.25
INFO:DysmalPy:
End: 2024-05-02 10:48:10.247523

******************
nCPU, nParam, nWalker, nBurn = 4, 9, 20, 2
Scale param a= 5
Time= 1.47 (sec),   0:1.47 (m:s)
Mean acceptance fraction: 0.225
Ideal acceptance frac: 0.2 - 0.5
Autocorr est: [nan nan nan nan nan nan nan nan nan]
******************
INFO:DysmalPy:
Ensemble sampling:
Start: 2024-05-02 10:48:10.973361

INFO:DysmalPy:ii=0, a_frac=0.2 time.time()=2024-05-02 10:48:11.308325
INFO:DysmalPy:0: acor_time =[nan nan nan nan nan nan nan nan nan]
INFO:DysmalPy:ii=1, a_frac=0.175 time.time()=2024-05-02 10:48:11.514939
INFO:DysmalPy:1: acor_time =[nan nan nan nan nan nan nan nan nan]
INFO:DysmalPy:ii=2, a_frac=0.18333333333333335 time.time()=2024-05-02 10:48:12.045867
INFO:DysmalPy:2: acor_time =[nan nan nan nan nan nan nan nan nan]
INFO:DysmalPy:ii=3, a_frac=0.2125 time.time()=2024-05-02 10:48:12.293471
INFO:DysmalPy:3: acor_time =[nan nan nan nan nan nan nan nan nan]
INFO:DysmalPy:ii=4, a_frac=0.22000000000000006 time.time()=2024-05-02 10:48:12.597950
INFO:DysmalPy:4: acor_time =[nan nan nan nan nan nan nan nan nan]
INFO:DysmalPy:Finished 5 steps

INFO:DysmalPy:
End: 2024-05-02 10:48:12.608378

******************
nCPU, nParam, nWalker, nSteps = 4, 9, 20, 5
Scale param a= 5
Time= 1.63 (sec),   0:1.63 (m:s)
Mean acceptance fraction: 0.220
Ideal acceptance frac: 0.2 - 0.5
Autocorr est: [nan nan nan nan nan nan nan nan nan]
******************
WARNING:root:Too few points to create valid contours
WARNING:root:Too few points to create valid contours
WARNING:root:Too few points to create valid contours
WARNING:root:Too few points to create valid contours

Examine MCMC results

Of course this (very short!) example looks terrible, but it’s instructive to see what’s happening even if you only did a very short / few walker MCMC test:

Trace

  • The individual walkers should move around in the parameter space over the chain iterations (not necessarily for every step; but there should be some exploration of the space)

# Look at trace:
filepath = outdir_mcmc+"GS4_43501_mcmc_trace.{}".format(plot_type)
Image(filepath, width=600)
../../_images/acf1027b180225ace716780ae4fc324fb70dad157e51cc904ff84a9cefb3b84e.png

Best-fit

  • This is a good opportunity to check that the model PA and slit PA are correct, or else the data and model curves will have opposite shapes!

  • Also, it’s helpful to check that your data masking is reasonable.

  • Finally, this is a worthwhile chance to see if your “nuisance” geometry and spectral parameters (especially xshift, yshift, vel_shift have reasonable values, and if appropriate, reasonable bounds and priors.)

# Look at best-fit:
filepath = outdir_mcmc+"GS4_43501_mcmc_bestfit_halpha_2D.{}".format(plot_type)
Image(filepath, width=600)
../../_images/839d8130c3a616ce71ddad7f4b0c2413ecc899177377996e46e2d581180ad5db.png

Sampler “corner” plot

  • The “best-fit” MAP (by default taken to be the peak of each marginalized parameter posterior, independent of the other parameters) is marked with the solid blue line.

    • However, the MAP can also be found by jointly analyzing two or more parameters’ posterior space.

      (→ see the example in the :ref:1D example fit <dysmalpy_example_fit_1D.ipynb> tutorial)

  • Check to see that your Gaussian prior centers are marked in orange in the appropriate rows/columns (if any Gaussian priors are used).

  • The vertical dashed black lines show the 2.275%, 15.865%, 84.135%, 97.725% percentile intervals for the marginalized posterior for each parameter.

  • The vertical dashed purple lines show the shortest \(1\sigma\) interval, determined from the marginalized posterior for each parameter independently.

# Look at corner:
filepath = outdir_mcmc+"GS4_43501_mcmc_param_corner.{}".format(plot_type)
Image(filepath, height=620)
../../_images/cec6cf9b36f1ef602559e9afdfc6bcd85ba38abc6118867908ca481ded175d57.png

5) Visualize a real MCMC example

In the interest of time, let’s look at some results calculated previously, using 1000 walkers, 175 burn-in steps, and 200 steps.

Using 190 threads, it took about 21 minutes to run the MCMC fit.

First, you need to download the sampler from:

https://www.mpe.mpg.de/resources/IR/DYSMALPY/dysmalpy_optional_extra_files/JUPYTER_OUTPUT_2D.tar.gz

Decompress it in your preferred path (e.g.: /path/to/your/directory) and then set an environment variable to access that directory from this notebook. You can do that from the terminal running:

export DATADIR=/path/to/your/directory

Or, you can simply run the bash command from within the notebook as:

%env FULL_MCMC_RUNS_DATADIR=/path/to/your/directory

# A working example using the path of one of the maintainers:
%env FULL_MCMC_RUNS_DATADIR=/Users/jespejo/Dropbox/dysmalpy_example_files/JUPYTER_OUTPUT_2D/
env: FULL_MCMC_RUNS_DATADIR=/Users/jespejo/Dropbox/dysmalpy_example_files/JUPYTER_OUTPUT_2D/
# Now, add the environment variable to retrieve the data 
_full_mcmc_runs = os.getenv('FULL_MCMC_RUNS_DATADIR')

Note

We used moment extraction for the model 2D profile extraction, as this does not seem to change the result much compared to gaussian extraction (as the observed maps were derived). This is likely because the extraction is over single pixels, so there is less complex line shape mixing as found in 1D extractions.

If this fit is done using a gaussian extraction, exactly following the data extraction method, it is generally slower.

When performing fits using moment extraction on data that was gaussian extracted, it is worth considering whether this is a reasonable thing to be doing. (Or at least check the final results with a gaussian extraction to see if there are systematic differences between the moment and gaussian maps.)

outdir_mcmc_full = _full_mcmc_runs + '/MCMC_full_run_nw1000_ns200_a5/'

Examine results:

Helpful for:

  • replotting

  • reanalyzing chain (eg, jointly constraining some posteriors)

Reload the galaxy, results files:

f_galmodel = outdir_mcmc_full + 'galaxy_model.pickle'
f_mcmc_results =  outdir_mcmc_full + 'mcmc_results.pickle'

#----------------------------------------
## Fix module import
import sys
from dysmalpy import fitting_wrappers
sys.modules['fitting_wrappers'] = fitting_wrappers
#----------------------------------------


if os.path.isfile(f_galmodel) & os.path.isfile(f_mcmc_results):
    gal_full, mcmc_results = fitting.reload_all_fitting(filename_galmodel=f_galmodel, 
                                filename_results=f_mcmc_results, fit_method='mcmc')
else:
    gal_full = mcmc_results = None

If necessary, also reload the sampler chain:

f_sampler =  outdir_mcmc_full + 'mcmc_sampler.{}'.format(ftype_sampler)
if os.path.isfile(f_sampler) & (mcmc_results is not None):
    mcmc_results.reload_sampler_results(filename=f_sampler)
WARNING:emcee.autocorr:The chain is shorter than 10 times the integrated autocorrelation time for 1 parameter(s). Use this estimate with caution and run a longer chain!
N/10 = 20;
tau: [19.6537326  19.9891195  19.55235249 19.98314067 19.7486466  19.41789169
 19.19654616 20.31377879 19.68237055]

Plot the best-fit results:

if (gal_full is not None) & (mcmc_results is not None):
    mcmc_results.plot_results(gal_full)
../../_images/6e7c6833a86a6a606138a63c00508f12cfa16bbd05d01df298d28d2331c1fd9a.png ../../_images/2022e74102db984e82f0e67a6267a4f8eda338b6e1c8f6c879e4e0430a006f43.png ../../_images/8063aa1eddeeb485bbb7ac5d7accd308ee161dc7f2e3ae84a71fb50ae3eb355d.png

Results report:

# Print report
if (gal_full is not None) & (mcmc_results is not None):
    print(mcmc_results.results_report(gal=gal_full))
###############################
 Fitting for GS4_43501

Date: 2024-05-02 10:48:21.959682

    obs: OBS
         Datafiles:
         fit_velocity:           True
         fit_dispersion:         True
         fit_flux:               False
         moment:           True
         n_wholepix_z_min:      3
         oversample:            1
         oversize:              1


Fitting method: MCMC

pressure_support:      True
pressure_support_type: 1

###############################
 Fitting results
-----------
 disk+bulge
    total_mass         10.8755  -   0.0278 +   0.0312
    r_eff_disk          4.0151  -   0.2608 +   0.2707

    n_disk              1.0000  [FIXED]
    r_eff_bulge         1.0000  [FIXED]
    n_bulge             4.0000  [FIXED]
    bt                  0.3000  [FIXED]

    noord_flat          True
-----------
 halo
    fdm                 0.1433  -   0.0150 +   0.0242

    mvirial            11.4323  [TIED]
    conc                5.0000  [FIXED]
-----------
 dispprof
    sigma0             33.2030  -   1.1670 +   1.6011
-----------
 zheightgaus
    sigmaz              0.6820  [TIED]
-----------
 geom
    inc                69.8972  -   1.6953 +   1.2540
    pa                146.6866  -   0.3726 +   0.3369
    xshift              0.2318  -   0.0688 +   0.0688
    yshift             -0.9261  -   0.0410 +   0.0396
    vel_shift          15.0223  -   0.9314 +   0.5174

-----------
    mvirial            11.4323  -   0.1467 +   0.1957

-----------
Adiabatic contraction: False

-----------
Red. chisq: 2.3761

-----------
obs OBS: Rout,max,2D: 10.8553

Or save results report to file:

# Save report to file:
f_mcmc_report = outdir_mcmc + 'mcmc_fit_report.txt'
if (gal_full is not None) & (mcmc_results is not None):
    mcmc_results.results_report(gal=gal_full, filename=f_mcmc_report)