Spaxel-by-Spaxel fitting
Now we are going to perform the spaxel-by-spaxel fitting. The whole process is split across 3 seperate steps and we will slowly explore all of them:
“Unwrapping the cube”: Extracting all of the spaxel spectra, binning them if required, and estimating the uncertainties of the flux
“Spaxel fitting”: fitting each of the spectra extracted above
“Map creation”: Post processing of the fits to each of the specta:
1) Unwrapping the cube
- Cube.unwrap_cube(rad=0.4, mask_manual=0, sp_binning='Nearest', add='', binning_pix=1, err_range=[0], boundary=2.4, instrument='NIRSPEC05')
Unwrapping the cube to prep it for spaxel-by-spaxel fitting. Saves the output as a pickle .txt object.
- Parameters:
rad (float) – radius of the circular aperture to select spaxel to fit (only used if mask_manual is not supplied)
mask_manual (2d - array) – 2D array - with True value for spaxel you want ot fit. Ideal to select with QFitsView and load with QubeSpec.sp.Qfitsview_mask function
sp_binning (str) – spatial binning - ‘Single’ - no binning, ‘Nearest’ - bins within 0.1 arcsec radius
binning_pix (int -) – If sp_biiing = ‘Nearest’, how many pixels to bin over 1,2,3
add (str - optional) – add additional string to the saved file name for version/variations/names of companions.
err_range (list - optional) – same as for extracting 1D spectra and for the errors.
boundary (float - optinal) – same as for extracting 1D spectra and for the errors.
instrument (str) – not used anymore. Doesnt do anything. Will be remove soon.
#importing modules
import numpy as np
import matplotlib.pyplot as plt; plt.ioff()
c= 3e8
h= 6.62*10**-34
k= 1.38*10**-23
%load_ext autoreload
%autoreload 2
import QubeSpec as IFU
import QubeSpec.Plotting as emplot
import QubeSpec.Fitting as emfit
Cube = IFU.Cube()
Cube.load('/Users/jansen/Test.txt')
mask_spaxel = IFU.sp.QFitsview_mask(QubeSpec_setup['Spaxel_mask'])
plt.figure()
plt.imshow(mask_spaxel, cmap='gray', origin='lower')
plt.show()
Unwrapping = False
if Unwrapping==True:
Cube.unwrap_cube(instrument='NIRSPEC05',mask_manual=mask_spaxel, \
err_range=QubeSpec_setup['err_range'],\
boundary=QubeSpec_setup['err_boundary'],\
add='',\
sp_binning= QubeSpec_setup['Spaxel_Binning'])
plt.show()
2) Fitting spaxel-by-spaxel
In order to fit all of the spaxels we need to use the QubeSpec.Spaxel module. Similar to the fitting the 1D collapsed spectra,
there are pre written functions/classes to allow fit the basic Halpha, [OIII] and Halpha+[OIII] and of course full custom functions.
The four classes classes are:
QubeSpec.Spaxel.Halpha- class to fit Halpha complexQubeSpec.Spaxel.OIII- class to fit [OIII] complexQubeSpec.Spaxel.Halpha_OIII- class to fit Halpha+[OIII] complexQubeSpec.Spaxel.general- class to fit any custom function.
Within each class there are couple of standarized models and combination of models to fit (models='Single'):
Single- single Gaussian per emission lineBLR- BLR + outflowBLR_simple- BLR (no outflow)outflow_both- Fits single model and outflow models and then we decide later which fit to useBLR_both- BLR and BLR_simple and then we decide later which fit to use
Below is the full description of the fitting function:
- Halpha_OIII.Spaxel_fitting(Cube, add='', Ncores=0, models='Single', priors={'BLR_Hal_peak': [0, 'loguniform', -4, 1], 'BLR_Hbeta_peak': [0, 'loguniform', -3, 1], 'BLR_fwhm': [4000, 'uniform', 2000, 9000], 'Hal_out_peak': [0, 'loguniform', -4, 1], 'Hal_peak': [0, 'loguniform', -4, 1], 'Hbeta_out_peak': [0, 'loguniform', -4, 1], 'Hbeta_peak': [0, 'loguniform', -4, 1], 'NII_out_peak': [0, 'loguniform', -4, 1], 'NII_peak': [0, 'loguniform', -4, 1], 'Nar_fwhm': [300, 'uniform', 100, 900], 'OIII_out_peak': [0, 'loguniform', -4, 1], 'OIII_peak': [0, 'loguniform', -4, 1], 'SIIb_peak': [0, 'loguniform', -3, 1], 'SIIr_peak': [0, 'loguniform', -3, 1], 'cont': [0, 'loguniform', -4, 1], 'cont_grad': [0, 'normal', 0, 0.3], 'outflow_fwhm': [600, 'uniform', 300, 1500], 'outflow_vel': [-50, 'normal', 0, 300], 'z': [0, 'normal', 0, 0.003], 'zBLR': [0, 'normal', 0, 0.003]}, **kwargs)
Function to use to fit Spaxels.
- Parameters:
Cube (QubeSpec.Cube class instance) – Cube class from the main part of the QubeSpec.
models (str) – option - Single, BLR, BLR_simple, outflow_both, BLR_both
add (str - optional) – add string to the name of the file to load and
Ncores (int - optional) – number of cpus to use to fit - default number of available cpu -1
priors (dict - optional) – dictionary with all of the priors to update
And below is an example of how to trigger it:
Please not couple of things. First in the priors, we have set the low boundary of the _peak and cont to quite low (-6 or 1e-6).
This allows for pretty low values when fitting spaxel spectrum with very low fluxes in them. Secondly, please make sure that you run the Spaxel
fitting in the if __name__ == '__main__': or the multiprocess code will freak out.
In order to fit a custom function, we need to define similar things as for the general fitting in 1D spectrum fitting. Actually, I highly recommend that we use the exact function, labels, priors, etc. This way will make sure that things work on a single spectrum before we fit all of the spaxel. See example below:
def gauss(x, k, mu,FWHM):
sig = FWHM/3e5*mu/2.35482
expo= -((x-mu)**2)/(2*sig*sig)
y= k* e**expo
return y
from astropy.modeling.powerlaws import PowerLaw1D
def Full_optical(x, z, cont,cont_grad, Hal_peak, NII_peak, OIIIn_peak, Hbeta_peak, Hgamma_peak, Hdelta_peak, NeIII_peak, OII_peak, OII_rat,OIIIc_peak, HeI_peak,HeII_peak, Nar_fwhm):
# Halpha side of things
Hal_wv = 6564.52*(1+z)/1e4
NII_r = 6585.27*(1+z)/1e4
NII_b = 6549.86*(1+z)/1e4
OIIIr = 5008.24*(1+z)/1e4
OIIIb = 4960.3*(1+z)/1e4
Hbeta = 4862.6*(1+z)/1e4
Hal_nar = gauss(x, Hal_peak, Hal_wv, Nar_fwhm)
NII_nar_r = gauss(x, NII_peak, NII_r, Nar_fwhm)
NII_nar_b = gauss(x, NII_peak/3, NII_b, Nar_fwhm)
Hgamma_wv = 4341.647191*(1+z)/1e4
Hdelta_wv = 4102.859855*(1+z)/1e4
Hgamma_nar = gauss(x, Hgamma_peak, Hgamma_wv, Nar_fwhm)
Hdelta_nar = gauss(x, Hdelta_peak, Hdelta_wv, Nar_fwhm)
# [OIII] side of things
OIIIr = 5008.24*(1+z)/1e4
OIIIb = 4960.3*(1+z)/1e4
Hbeta = 4862.6*(1+z)/1e4
OIII_nar = gauss(x, OIIIn_peak, OIIIr, Nar_fwhm) + gauss(x, OIIIn_peak/3, OIIIb, Nar_fwhm)
Hbeta_nar = gauss(x, Hbeta_peak, Hbeta, Nar_fwhm)
NeIII = gauss(x, NeIII_peak, 3869.68*(1+z)/1e4, Nar_fwhm ) + gauss(x, 0.322*NeIII_peak, 3968.68*(1+z)/1e4, Nar_fwhm)
OII = gauss(x, OII_peak, 3727.1*(1+z)/1e4, Nar_fwhm ) + gauss(x, OII_rat*OII_peak, 3729.875*(1+z)/1e4, Nar_fwhm)
OIIIc = gauss(x, OIIIc_peak, 4364.436*(1+z)/1e4, Nar_fwhm )
HeI = gauss(x, HeI_peak, 3889.73*(1+z)/1e4, Nar_fwhm )
HeII = gauss(x, HeII_peak, 4686.0*(1+z)/1e4, Nar_fwhm )
contm = PowerLaw1D.evaluate(x, cont,Hal_wv, alpha=cont_grad)
return contm+Hal_nar+NII_nar_r+NII_nar_b + OIII_nar + Hbeta_nar + Hgamma_nar + Hdelta_nar + NeIII+ OII + OIIIc+ HeI+HeII
labels= ['z', 'cont','cont_grad', 'Hal_peak', 'NII_peak', 'OIII_peak', 'Hbeta_peak','Hgamma_peak', 'Hdelta_peak','NeIII_peak','OII_peak','OII_rat','OIIIaur_peak', 'HeI_peak','HeII_peak', 'Nar_fwhm']
dvmax = 1000/3e5*(1+Cube.z)
dvstd = 200/3e5*(1+Cube.z)
priors={'z':[Cube.z,'normal_hat', Cube.z, dvstd, Cube.z-dvmax, Cube.z+dvmax]}
priors['cont']=[0.001,'loguniform', -4,1]
priors['cont_grad']=[0.1,'normal', 0,0.2]
priors['Hal_peak']=[0.1,'loguniform', -4,1]
priors['NII_peak']=[0.4,'loguniform', -4,1]
priors['Nar_fwhm']=[300,'uniform', 200,900]
priors['OIII_peak']=[0.1,'loguniform', -4,1]
priors['OI_peak']=[0.01,'loguniform', -4,1]
priors['HeI_peak']=[0.01,'loguniform', -4,1]
priors['Hbeta_peak']=[0.02,'loguniform', -4,1]
priors['Hgamma_peak'] = [0.02,'loguniform',-4,1]
priors['Hdelta_peak'] = [0.01,'loguniform',-4,1]
priors['NeIII_peak'] = [0.01,'loguniform',-4,1]
priors['OII_peak'] = [0.01,'loguniform',-4,1]
priors['OII_rat']=[1,'uniform', 0.2,4]
priors['OIIIaur_peak']=[0.01,'loguniform', -4,1]
Please notice above in the priors that we have intenationally put the initial conditions for the _peak to be ~5-10 smaller than in the 1D spectra case.
Secondly, the lower boundaries for the _peak are also smaller.
Below is the full description of the Spaxel_fitting function.
- general.Spaxel_fitting(Cube, fitted_model, labels, priors, logprior, nwalkers=64, use=array([], dtype=float64), N=10000, add='', Ncores=0, **kwargs)
Function to use to fit Spaxels.
- Parameters:
Cube (QubeSpec.Cube class instance) – Cube class from the main part of the QubeSpec.
fitted_model (callable) – Function to fit
labels (list) – list of the name of the paramters in the same order as in the fitted_function
priors (dict - optional) – dictionary with all of the priors to update
logprior (callable function) – logprior evaluation function - use emfit.logprior_general or emfit.logprior_general_scipy
nwalkers (int - optional) – default 64 walkers for the MCMC
add (str - optional) – add string to the name of the file to load and
Ncores (int - optional) – number of cpus to use to fit - default number of available cpu -1
And here is the example to run it. Ass you can see we have supplied all of the same info as for fitting a 1D spectrum and the same variabls as for pre written models.
Spaxel = False
if Spaxel==True:
if __name__ == '__main__':
spx = IFU.Spaxel.general()
spx.Spaxel_fitting_general_MCMC_mp(Cube, Full_optical,labels, priors, emfit.logprior_general_scipy, add='', Ncores=QubeSpec_setup['ncpu'])
3) Map creation
During the Spaxel-by-Spaxel fitting above, we only create QubeSpec.Fitting.Fitting class instance for every spaxel and save it into a text document (pickling it).
However, we dont actually extract any useful information (such as fluxes, velocities, velocity widths, etc). As such, we need to post process all of the fitting results.
To post process the results, we will usethe QubeSpec.Maps module. As usual there are pre written function to post process the results for the usual emission line combination
and general functions.
- Maps.Map_creation_Halpha_OIII(SNR_cut=3, fwhmrange=[100, 500], velrange=[-100, 100], dbic=10, flux_max=0, width_upper=300, add='')
Function to post process fits. The function will load the fits results and determine which model is more likely, based on BIC. It will then calculate the W80 of the emission lines, V50 etc and create flux maps, velocity maps etc., Afterwards it saves all of it as .fits file.
- Parameters:
Cube (QubeSpec.Cube class instance) – Cube class from the main part of the QubeSpec.
SNR_cut (float) – SNR cutoff to detect emission lines
fwhmrange (list) – list of the two values to use as vmin and vmax in imshow of FWHM range
velrange (list) – list of the two values to use as vmin and vmax in imshow of velocity range
width_upper (float) – FWHM value used in the flux upper limit calculation.
dbic (float) – delta bic to decide which model to use.
add (str) – additional string to use to load the results and save maps/pdf
This is the same for QubeSpec.Maps.Map_creation_Halpha and QubeSpec.Maps.Map_creation_OIII
For the general fit, you need supply it additional information to extract all of the emission lines.
- Maps.Map_creation_general(info, SNR_cut=3, width_upper=300, add='', brokenaxes_xlims=((2.82, 3.45), (3.75, 4.05), (5, 5.3)))
Function to post process fits. The function will load the fits results and determine which model is more likely, based on BIC. It will then calculate the W80 of the emission lines, V50 etc and create flux maps, velocity maps eyc., Afterwards it saves all of it as .fits file.
- Parameters:
Cube (QubeSpec.Cube class instance) – Cube class from the main part of the QubeSpec.
info (dict) – dictionary containing information on what to extract.
SNR_cut (float) – SNR cutoff to detect emission lines
add (str) – additional string to use to load the results and save maps/pdf
brokenaxes_xlims (list) – list of wavelength ranges to use for broken axes when plotting
most importantly you need to supply the info dictionary containing the information needed to extract the emission lines.
The shape of the info dictionary should be as below:
OIII_kins = {'fwhms':['Nar_fwhm','outflow_fwhm'], 'vels':['outflow_vel'], 'peaks':['OIII_peak', 'OIII_out_peak']}
Hal_kins = {'fwhms':['Nar_fwhm'], 'vels':[], 'peaks':['Hal_peak']}
info = {'Hal': {'wv':6563,'fwhm':'Nar_fwhm','kin':Hal_kins}}
info['NII'] = {'wv':6583, 'fwhm':'Nar_fwhm',}
info['OIII'] = {'wv':5008, 'fwhm':'Nar_fwhm', 'kin': OIII_kins}
info['OIII_out'] = {'wv':5008, 'fwhm':'outflow_fwhm',}
info['Hbeta'] = {'wv':4861, 'fwhm':'Nar_fwhm',}
info['Hgamma'] = {'wv':4341.647, 'fwhm':'Nar_fwhm',}
info['Hdelta'] = {'wv':4102.859, 'fwhm':'Nar_fwhm',}
info['NeIII'] = {'wv':3869.68, 'fwhm':'Nar_fwhm',}
info['OII'] = {'wv':3727.1, 'fwhm':'Nar_fwhm',}
info['OIIIaur'] = {'wv':4363, 'fwhm':'Nar_fwhm',}
info['HeI'] = {'wv':3889, 'fwhm':'Nar_fwhm',}
info['params'] = ['z','outflow_vel', 'outflow_fwhm']
fmaps = IFU.Maps.Map_creation_general(Cube, info, SNR_cut=4., add='_test' )
Each entry contains another dictionary with:
'wv'- rest-frame wavelength of the emission line'fwhm'- name of the FWHM variable associated with that particular emission line component'kin'- If you want to recover the kinematics of the line or multiple components of the same line. The'kin'should contain a dictionary with the name of the peaks, FWHMs and velocities to get the v10,w80,v90 and peak velocity'params'- please put with a list of variables you would like to directly extract from the chains.
We can then run the post processing suc this:
Visually inspecting the fits
- Once we fit all of the spaxels, we can visually inspect the maps and each of the fits. We need to use the
QubeSpec.Visulizationsmodule which initialize a UI to fit. To initialize it we need to supply the path to the fits file containing the Spaxel maps and list of three fits extentions we want to show.
Something didnt fit right? lets refit it.
There is a decent chat that not all (800?) fits are going to be perfect on the first try. Actually, it is quite likely.
Therefore, I have written few “topup” functions. These function have the same syntax as the e.g. QubeSpec.Spaxel.Halpha_OIII.Spaxel_fitting
but they also include variable to_fit with should contain a list of pair of coordinates to refit:
this will replace the fits with new ones. However, please remember to regenerate all of the maps.