def fit_spectra(bin_sci_j, ppxf_bestfit_j, plot=False): """ """ with fits.open(bin_sci_j) as hdu: odata = hdu[0].data ohdr = hdu[0].header bestfit = fits.getdata(ppxf_bestfit_j) lamRange = ohdr['CRVAL1'] + np.array([1. - ohdr['CRPIX1'], ohdr['NAXIS1'] - ohdr['CRPIX1']]) * ohdr['CD1_1'] x = np.linspace(lamRange[0], lamRange[1], ohdr['NAXIS1']) # Log bin the spectra to match the best fit absorption template galaxy, logLam1, velscale = util.log_rebin(lamRange, odata) log_bins = np.exp(logLam1) emlns, lnames, lwave = util.emission_lines(logLam1, lamRange, 2.3) # Hgamma 4340.47, Hbeta 4861.33, OIII [4958.92, 5006.84] # lwave = [4340.47, 4861.33, 4958.92, 5006.84] # find the index of the emission lines iHg = (np.abs(log_bins - lwave[0])).argmin() iHb = (np.abs(log_bins - lwave[1])).argmin() iOIII = (np.abs(log_bins - lwave[2])).argmin() iOIIIb = (np.abs(log_bins - (lwave[2] - 47.92))).argmin() # There are BOTH absorption and emission features about the wavelength of Hgamma and Hbeta, so we need # to use a specialized fitting function (convolved Guassian and Lorentzian -> pVoight) to remove the # emission lines popt_Hg, pcov_Hg = fit_ems_pvoightcont(log_bins, galaxy, x, odata, iHg, bestfit) popt_Hb, pcov_Hb = fit_ems_pvoightcont(log_bins, galaxy, x, odata, iHb, bestfit) # There are only emission features about the OIII doublet so we only fit the emission line with a Gaussian popt_OIII, pcov_OIII = fit_ems_lincont(x, odata, iOIII, bestfit, x[954], [x[952], x[956]]) popt_OIIIb, pcov_OIIIb = fit_ems_lincont(x, odata, iOIIIb, bestfit) em_fit = gauss_lorentz(x, popt_Hg[0], popt_Hg[1], popt_Hg[2], popt_Hg[3], popt_Hg[4], popt_Hg[5]) + \ gauss_lorentz(x, popt_Hb[0], popt_Hb[1], popt_Hb[2], popt_Hb[3], popt_Hb[4], popt_Hb[5]) + \ gauss_lorentz(x, popt_OIII[0], popt_OIII[1], popt_OIII[2], popt_OIII[3], popt_OIII[4], popt_OIII[5]) + \ gauss_lorentz(x, popt_OIIIb[0], popt_OIIIb[1], popt_OIIIb[2], popt_OIIIb[3], popt_OIIIb[4], popt_OIIIb[5]) abs_fit = odata - em_fit if plot: plt.plot(x, odata, '-k', label="spectra") # plt.plot(x, bestfit, '--r', label="bestfit absorption line") plt.plot(x, abs_fit, '-b', label="absorption spectra - gauss") plt.legend() plt.show() return abs_fit, em_fit
def ppxf_population_gas_example_sdss(): # Read SDSS DR8 galaxy spectrum taken from here http://www.sdss3.org/dr8/ # The spectrum is *already* log rebinned by the SDSS DR8 # pipeline and log_rebin should not be used in this case. # file = 'spectra/NGC3522_SDSS_DR8.fits' hdu = pyfits.open(file) t = hdu[1].data z = float(hdu[1].header["Z"]) # SDSS redshift estimate # Only use the wavelength range in common between galaxy and stellar library. # mask = (t['wavelength'] > 3540) & (t['wavelength'] < 7409) flux = t['flux'][mask] galaxy = flux/np.median(flux) # Normalize spectrum to avoid numerical issues wave = t['wavelength'][mask] # The noise level is chosen to give Chi^2/DOF=1 without regularization (REGUL=0). # A constant error is not a bad approximation in the fitted wavelength # range and reduces the noise in the fit. # noise = galaxy*0 + 0.01528 # Assume constant noise per pixel here # The velocity step was already chosen by the SDSS pipeline # and we convert it below to km/s # c = 299792.458 # speed of light in km/s velscale = np.log(wave[1]/wave[0])*c FWHM_gal = 2.76 # SDSS has an instrumental resolution FWHM of 2.76A. #------------------- Setup templates ----------------------- stars_templates, lamRange_temp, logLam_temp = \ setup_spectral_library(velscale, FWHM_gal) # The stellar templates are reshaped into a 2-dim array with each spectrum # as a column, however we save the original array dimensions, which are # needed to specify the regularization dimensions # reg_dim = stars_templates.shape[1:] stars_templates = stars_templates.reshape(stars_templates.shape[0], -1) # See the pPXF documentation for the keyword REGUL, # for an explanation of the following two lines # stars_templates /= np.median(stars_templates) # Normalizes stellar templates by a scalar regul_err = 0.004 # Desired regularization error # Construct a set of Gaussian emission line templates. # Estimate the wavelength fitted range in the rest frame. # lamRange_gal = np.array([np.min(wave), np.max(wave)])/(1 + z) gas_templates, line_names, line_wave = \ util.emission_lines(logLam_temp, lamRange_gal, FWHM_gal) # Combines the stellar and gaseous templates into a single array # during the PPXF fit they will be assigned a different kinematic # COMPONENT value # templates = np.hstack([stars_templates, gas_templates]) #----------------------------------------------------------- # The galaxy and the template spectra do not have the same starting wavelength. # For this reason an extra velocity shift DV has to be applied to the template # to fit the galaxy spectrum. We remove this artificial shift by using the # keyword VSYST in the call to PPXF below, so that all velocities are # measured with respect to DV. This assume the redshift is negligible. # In the case of a high-redshift galaxy one should de-redshift its # wavelength to the rest frame before using the line below as described # in PPXF_KINEMATICS_EXAMPLE_SAURON. # c = 299792.458 dv = (np.log(lamRange_temp[0])-np.log(wave[0]))*c # km/s vel = c*z # Initial estimate of the galaxy velocity in km/s # Here the actual fit starts. The best fit is plotted on the screen. # # IMPORTANT: Ideally one would like not to use any polynomial in the fit # as the continuum shape contains important information on the population. # Unfortunately this is often not feasible, due to small calibration # uncertainties in the spectral shape. To avoid affecting the line strength of # the spectral features, we exclude additive polynomials (DEGREE=-1) and only use # multiplicative ones (MDEGREE=10). This is only recommended for population, not # for kinematic extraction, where additive polynomials are always recommended. # start = [vel, 180.] # (km/s), starting guess for [V,sigma] t = clock() # Assign component=0 to the stellar templates and # component=1 to the gas emission lines templates. # One can easily assign different kinematic components to different gas species # e.g. component=1 for the Balmer series, component=2 for the [OIII] doublet, ...) # Input a negative MOMENTS value to keep fixed the LOSVD of a component. # nTemps = stars_templates.shape[1] nLines = gas_templates.shape[1] component = [0]*nTemps + [1]*nLines moments = [4, 2] # fit (V,sig,h3,h4) for the stars and (V,sig) for the gas start = [start, start] # adopt the same starting value for both gas and stars pp = ppxf(templates, galaxy, noise, velscale, start, plot=False, moments=moments, degree=-1, mdegree=10, vsyst=dv, clean=False, regul=1./regul_err, reg_dim=reg_dim, component=component) # Plot fit results for stars and gas plt.clf() plt.subplot(211) plt.plot(wave, pp.galaxy, 'k') plt.plot(wave, pp.bestfit, 'b', linewidth=2) plt.xlabel("Observed Wavelength ($\AA$)") plt.ylabel("Relative Flux") plt.ylim([-0.1,1.3]) plt.xlim([np.min(wave), np.max(wave)]) plt.plot(wave, pp.galaxy-pp.bestfit, 'd', ms=4, color='LimeGreen', mec='LimeGreen') # fit residuals plt.axhline(y=-0, linestyle='--', color='k', linewidth=2) stars = pp.matrix[:,:nTemps].dot(pp.weights[:nTemps]) plt.plot(wave, stars, 'r', linewidth=2) # overplot stellar templates alone gas = pp.matrix[:,-nLines:].dot(pp.weights[-nLines:]) plt.plot(wave, gas+0.15, 'b', linewidth=2) # overplot emission lines alone # When the two Delta Chi^2 below are the same, the solution is the smoothest # consistent with the observed spectrum. # print('Desired Delta Chi^2: %.4g' % np.sqrt(2*galaxy.size)) print('Current Delta Chi^2: %.4g' % ((pp.chi2 - 1)*galaxy.size)) print('Elapsed time in PPXF: %.2f s' % (clock() - t)) w = np.where(np.array(component) == 1)[0] # Extract weights of gas emissions print('++++++++++++++++++++++++++++++') print('Gas V=%.4g and sigma=%.2g km/s' % (pp.sol[1][0], pp.sol[1][1])) print('Emission lines peak intensity:') for name, weight, line in zip(line_names, pp.weights[w], pp.matrix[:,w].T): print('%12s: %.3g' % (name, weight*np.max(line))) print('------------------------------') # Plot stellar population mass distribution plt.subplot(212) weights = pp.weights[:np.prod(reg_dim)].reshape(reg_dim)/pp.weights.sum() plt.imshow(np.rot90(weights), interpolation='nearest', cmap='gist_heat', aspect='auto', origin='upper', extent=(np.log10(1.0), np.log10(17.7828), -1.9, 0.45)) plt.colorbar() plt.title("Mass Fraction") plt.xlabel("log$_{10}$ Age (Gyr)") plt.ylabel("[M/H]") plt.tight_layout() plt.show()
def remove_emission_lines(bin_sci, ppxf_bestfit, abs_bin_sci, plot=True, bad_lines=[]): """ Fit the absorption spectra with a pVoight function by parameterizing the bestfit template as output from pPXF, fit the emission lines over the absorption features with a sum of a gaussian and lorentz and subtract from the original spectra. Where the emission lines are isolated, just interpolate the continuum values from the best fit template and replace. This requires a very good fit between the galaxy spectra and the bestfit spectra. I recommend running this with plot=True to check for any residuals. """ for j in range(len(glob.glob(bin_sci.format('*')))): bin_sci_j = bin_sci.format(j) ppxf_bestfit_j = ppxf_bestfit.format(j) if not os.path.exists(abs_bin_sci.format(j)): if plot: print('>>>>> Removing emission lines from spectra {}'.format(j)) with fits.open(bin_sci_j) as hdu: odata = hdu[0].data ohdr = hdu[0].header bestfit = fits.getdata(ppxf_bestfit_j) lamRange = ohdr['CRVAL1'] + np.array([1. - ohdr['CRPIX1'], ohdr['NAXIS1'] - ohdr['CRPIX1']]) * ohdr['CD1_1'] lin_bins = np.linspace(lamRange[0], lamRange[1], ohdr['NAXIS1']) # Log bin the spectra to match the best fit absorption template galaxy, logLam1, velscale = util.log_rebin(lamRange, odata) log_bins = np.exp(logLam1) emlns, lnames, lwave = util.emission_lines(logLam1, lamRange, 2.3) # Hgamma 4340.47, Hbeta 4861.33, OIII [4958.92, 5006.84] # lwave = [4340.47, 4861.33, 4958.92, 5006.84] # find the index of the emission lines iHg = (np.abs(log_bins - lwave[0])).argmin() iHb = (np.abs(log_bins - lwave[1])).argmin() iOIII = (np.abs(log_bins - lwave[2])).argmin() iOIIIb = (np.abs(log_bins - (lwave[2] - 47.92))).argmin() # There are BOTH absorption and emission features about the wavelength of Hgamma and Hbeta, so we need # to use a specialized fitting function (convolved Guassian and Lorentzian -> pVoight) to remove the # emission lines popt_Hg, pcov_Hg = remove_lines.fit_ems_pvoightcont(log_bins, galaxy, lin_bins, odata, iHg, bestfit) popt_Hb, pcov_Hb = remove_lines.fit_ems_pvoightcont(log_bins, galaxy, lin_bins, odata, iHb, bestfit) em_fit = remove_lines.gauss_lorentz(lin_bins, popt_Hg[0], popt_Hg[1], popt_Hg[2], popt_Hg[3], popt_Hg[4], popt_Hg[5]) + \ remove_lines.gauss_lorentz(lin_bins, popt_Hb[0], popt_Hb[1], popt_Hb[2], popt_Hb[3], popt_Hb[4], popt_Hb[5]) abs_feat_fit = odata - em_fit abs_fit = remove_lines.em_chop(lin_bins, abs_feat_fit, iOIII, log_bins, bestfit) abs_fit = remove_lines.em_chop(lin_bins, abs_fit, iOIIIb, log_bins, bestfit) for lin in bad_lines: ibad = (np.abs(lin_bins - lin)).argmin() abs_fit = remove_lines.em_chop(lin_bins, abs_fit, ibad, log_bins, bestfit) if plot: plt.plot(lin_bins, odata, '-k', label="spectra") # plt.plot(lin_bins, bestfit, '--r', label="bestfit absorption line") plt.plot(lin_bins, abs_fit, '-b', label="absorption spectra") plt.legend() plt.show() abs_hdu = fits.PrimaryHDU() abs_hdu.data = abs_fit abs_hdu.header = fits.getheader(bin_sci.format(j), 0) abs_hdu.writeto(abs_bin_sci.format(j))
def ppxf_population_gas_example_sdss(quiet=True): galaxy='ic1459' z = 0.005 wav_range='4200-' out_dir = '%s/Data/vimos/analysis' % (cc.base_dir) output = "%s/%s/results/%s" % (out_dir, galaxy, wav_range) out_plots = "%splots" % (output) out_pickle = '%s/pickled' % (output) pickleFile = open("%s/dataObj_%s_pop.pkl" % (out_pickle, wav_range), 'rb') #pickleFile = open("%s/dataObj_%s.pkl" % (cc.home_dir, wav_range), 'rb') D = pickle.load(pickleFile) pickleFile.close() #------------------- Setup templates ----------------------- c = 299792.458 # speed of light in km/s velscale = np.log(D.bin[100].lam[1]/D.bin[100].lam[0])*c FWHM_gal = 4*0.71 # SDSS has an instrumental resolution FWHM of 2.76A. stars_templates, lamRange_temp, logLam_temp = \ setup_spectral_library(velscale, FWHM_gal) # The stellar templates are reshaped into a 2-dim array with each spectrum # as a column, however we save the original array dimensions, which are # needed to specify the regularization dimensions # reg_dim = stars_templates.shape[1:] stars_templates = stars_templates.reshape(stars_templates.shape[0],-1) # See the pPXF documentation for the keyword REGUL, # for an explanation of the following two lines # stars_templates /= np.median(stars_templates) # Normalizes stellar templates by a scalar regul_err = 0.004 # Desired regularization error w=[] for bin_number in range(D.number_of_bins): bin_number=32 print(bin_number,'/',D.number_of_bins) # Read SDSS DR8 galaxy spectrum taken from here http://www.sdss3.org/dr8/ # The spectrum is *already* log rebinned by the SDSS DR8 # pipeline and log_rebin should not be used in this case. # b=D.bin[bin_number] # file = 'spectra/NGC3522_SDSS.fits' # hdu = pyfits.open(file) # t = hdu[1].data # z = float(hdu[1].header["Z"]) # SDSS redshift estimate # # Only use the wavelength range in common between galaxy and stellar library. # # # mask = (t.field('wavelength') > 3540) & (t.field('wavelength') < 7409) # galaxy = t[mask].field('flux')/np.median(t[mask].field('flux')) # Normalize spectrum to avoid numerical issues # wave = t[mask].field('wavelength') mask = (b.lam > 3540) & (b.lam < 7409) galaxy = b.spectrum/np.median(b.spectrum) # Normalize spectrum to avoid numerical issues wave = b.lam # The noise level is chosen to give Chi^2/DOF=1 without regularization (REGUL=0). # A constant error is not a bad approximation in the fitted wavelength # range and reduces the noise in the fit. # noise = galaxy*0 + 0.01528 # Assume constant noise per pixel here # Construct a set of Gaussian emission line templates. # Estimate the wavelength fitted range in the rest frame. # lamRange_gal = np.array([np.min(wave), np.max(wave)])/(1 + z) gas_templates, line_names, line_wave = \ util.emission_lines(logLam_temp, lamRange_gal, FWHM_gal, quiet=quiet) # Combines the stellar and gaseous templates into a single array # during the PPXF fit they will be assigned a different kinematic # COMPONENT value # templates = np.hstack([stars_templates, gas_templates]) #----------------------------------------------------------- # The galaxy and the template spectra do not have the same starting wavelength. # For this reason an extra velocity shift DV has to be applied to the template # to fit the galaxy spectrum. We remove this artificial shift by using the # keyword VSYST in the call to PPXF below, so that all velocities are # measured with respect to DV. This assume the redshift is negligible. # In the case of a high-redshift galaxy one should de-redshift its # wavelength to the rest frame before using the line below as described # in PPXF_KINEMATICS_EXAMPLE_SAURON. # dv = (np.log(lamRange_temp[0])-np.log(wave[0]))*c # km/s vel = c*z # Initial estimate of the galaxy velocity in km/s # Here the actual fit starts. The best fit is plotted on the screen. # # IMPORTANT: Ideally one would like not to use any polynomial in the fit # as the continuum shape contains important information on the population. # Unfortunately this is often not feasible, due to small calibration # uncertainties in the spectral shape. To avoid affecting the line strength of # the spectral features, we exclude additive polynomials (DEGREE=-1) and only use # multiplicative ones (MDEGREE=10). This is only recommended for population, not # for kinematic extraction, where additive polynomials are always recommended. # start = [vel, 180.] # (km/s), starting guess for [V,sigma] # Assign component=0 to the stellar templates and # component=1 to the gas emission lines templates. # One can easily assign different kinematic components to different gas species # e.g. component=1 for the Balmer series, component=2 for the [OIII] doublet, ...) # Input a negative MOMENTS value to keep fixed the LOSVD of a component. # nTemps = stars_templates.shape[1] nLines = gas_templates.shape[1] component = [0]*nTemps + [1]*nLines moments = [4, 2] # fit (V,sig,h3,h4) for the stars and (V,sig) for the gas start = [start, start] # adopt the same starting value for both gas and stars pp = ppxf(templates, galaxy, noise, velscale, start, plot=True, moments=moments, degree=-1, mdegree=10, vsyst=dv, clean=False, regul=1./regul_err, reg_dim=reg_dim, component=component, quiet=quiet) plt.subplot(212) weights = pp.weights[:np.prod(reg_dim)].reshape(reg_dim)/pp.weights.sum() plt.imshow(np.rot90(weights), interpolation='nearest', cmap='gist_heat', aspect='auto', origin='upper', extent=[np.log10(1), np.log10(17.7828), -1.9, 0.45]) plt.colorbar() plt.title("Mass Fraction") plt.xlabel("log$_{10}$ Age (Gyr)") plt.ylabel("[M/H]") plt.tight_layout() plt.show() # Plot fit results for stars and gas weights = pp.weights[:np.prod(reg_dim)].reshape(reg_dim)/pp.weights.sum() w.append(weights) w = np.array(w) pickleFile = open("pop.pkl", 'wb') pickle.dump(w,pickleFile) pickleFile.close()
def ppxf_example_population_gas_sdss(): file_dir = path.dirname(path.realpath(__file__)) # path of this procedure # Read SDSS DR8 galaxy spectrum taken from here http://www.sdss3.org/dr8/ # The spectrum is *already* log rebinned by the SDSS DR8 # pipeline and log_rebin should not be used in this case. # file = file_dir + '/spectra/NGC3522_SDSS_DR8.fits' hdu = fits.open(file) t = hdu[1].data z = float(hdu[1].header["Z"]) # SDSS redshift estimate # Only use the wavelength range in common between galaxy and stellar library. # mask = (t['wavelength'] > 3540) & (t['wavelength'] < 7409) flux = t['flux'][mask] galaxy = flux/np.median(flux) # Normalize spectrum to avoid numerical issues wave = t['wavelength'][mask] # The SDSS wavelengths are in vacuum, while the MILES ones are in air. # For a rigorous treatment, the SDSS vacuum wavelengths should be # converted into air wavelengths and the spectra should be resampled. # To avoid resampling, given that the wavelength dependence of the # correction is very weak, I approximate it with a constant factor. # wave *= np.median(util.vac_to_air(wave)/wave) # The noise level is chosen to give Chi^2/DOF=1 without regularization (REGUL=0). # A constant noise is not a bad approximation in the fitted wavelength # range and reduces the noise in the fit. # noise = np.full_like(galaxy, 0.01635) # Assume constant noise per pixel here # The velocity step was already chosen by the SDSS pipeline # and we convert it below to km/s # c = 299792.458 # speed of light in km/s velscale = c*np.log(wave[1]/wave[0]) # eq.(8) of Cappellari (2017) FWHM_gal = 2.76 # SDSS has an approximate instrumental resolution FWHM of 2.76A. #------------------- Setup templates ----------------------- pathname = file_dir + '/miles_models/Mun1.30*.fits' miles = lib.miles(pathname, velscale, FWHM_gal) # The stellar templates are reshaped into a 2-dim array with each spectrum # as a column, however we save the original array dimensions, which are # needed to specify the regularization dimensions # reg_dim = miles.templates.shape[1:] stars_templates = miles.templates.reshape(miles.templates.shape[0], -1) # See the pPXF documentation for the keyword REGUL, regul_err = 0.013 # Desired regularization error # Construct a set of Gaussian emission line templates. # Estimate the wavelength fitted range in the rest frame. # lam_range_gal = np.array([np.min(wave), np.max(wave)])/(1 + z) gas_templates, gas_names, line_wave = \ util.emission_lines(miles.log_lam_temp, lam_range_gal, FWHM_gal) # Combines the stellar and gaseous templates into a single array # during the PPXF fit they will be assigned a different kinematic # COMPONENT value # templates = np.column_stack([stars_templates, gas_templates]) #----------------------------------------------------------- # The galaxy and the template spectra do not have the same starting wavelength. # For this reason an extra velocity shift DV has to be applied to the template # to fit the galaxy spectrum. We remove this artificial shift by using the # keyword VSYST in the call to PPXF below, so that all velocities are # measured with respect to DV. This assume the redshift is negligible. # In the case of a high-redshift galaxy one should de-redshift its # wavelength to the rest frame before using the line below as described # in PPXF_EXAMPLE_KINEMATICS_SAURON. # c = 299792.458 dv = c*(miles.log_lam_temp[0] - np.log(wave[0])) # km/s # Here the actual fit starts. The best fit is plotted on the screen. # # IMPORTANT: Ideally one would like not to use any polynomial in the fit # as the continuum shape contains important information on the population. # Unfortunately this is often not feasible, due to small calibration # uncertainties in the spectral shape. To avoid affecting the line strength of # the spectral features, we exclude additive polynomials (DEGREE=-1) and only use # multiplicative ones (MDEGREE=10). This is only recommended for population, not # for kinematic extraction, where additive polynomials are always recommended. # vel = c*np.log(1 + z) # eq.(8) of Cappellari (2017) start = [vel, 180.] # (km/s), starting guess for [V, sigma] t = clock() n_temps = stars_templates.shape[1] n_balmer = 4 # Number of Balmer lines included in the fit n_forbidden = 5 # Assign component=0 to the stellar templates, component=1 to the Balmer # gas emission lines templates and component=2 to the forbidden lines. component = [0]*n_temps + [1]*n_balmer + [2]*n_forbidden gas_component = np.array(component) > 0 # gas_component=True for gas templates # Fit (V, sig, h3, h4) moments=4 for the stars # and (V, sig) moments=2 for the two gas components moments = [4, 2, 2] # Adopt the same starting value for the stars and the two gas components start = [start, start, start] pp = ppxf(templates, galaxy, noise, velscale, start, plot=False, moments=moments, degree=-1, mdegree=10, vsyst=dv, lam=wave, clean=False, regul=1./regul_err, reg_dim=reg_dim, component=component, gas_component=gas_component, gas_names=gas_names) # When the two Delta Chi^2 below are the same, the solution # is the smoothest consistent with the observed spectrum. # print('Desired Delta Chi^2: %.4g' % np.sqrt(2*galaxy.size)) print('Current Delta Chi^2: %.4g' % ((pp.chi2 - 1)*galaxy.size)) print('Elapsed time in PPXF: %.2f s' % (clock() - t)) weights = pp.weights[~gas_component] # Exclude weights of the gas templates weights = weights.reshape(reg_dim)/weights.sum() # Normalized miles.mean_age_metal(weights) miles.mass_to_light(weights, band="r") # Plot fit results for stars and gas. # For every kinematic component, specify whether they are # gas emission, using the boolean `gas_component` keyword. plt.clf() plt.subplot(211) pp.plot() # Plot stellar population mass fraction distribution plt.subplot(212) miles.plot(weights) plt.tight_layout() plt.pause(1)
def __init__(self, gas, lamRange, logLam_template, FWHM_gal, quiet=True, lines='all', narrow_broad=False, goodWav=None):#, NaD=False): if narrow_broad: raise ValueError('Two components (narrow and broad line componets' + ' are not currently set up on the version (See git' + ' errors2_muse.py)') if lines == 'all' or lines =='All' or lines =='ALL': lines = ['Hdelta', 'Hgamma', 'Hbeta', 'Halpha', '[OII]3726', '[OII]3729', '[SII]6716', '[SII]6731', '[OIII]5007d', '[NI]d', '[OI]6300d', '[NII]6583d']#, 'NaD'] self.element = [] self.component = [] self.templatesToUse = [] self.templates = [] # This is not particularly robust code if goodWav is not None: w = np.where(np.diff(goodWav) > goodWav[-1]-goodWav[-2])[0] mask = np.array(zip(goodWav[w], goodWav[w+1])) else: mask = np.array([[lamRange[0],lamRange[0]]]) if len(mask) == 0: mask = np.array([[lamRange[0],lamRange[0]]]) ## ----------============ All lines together ===============--------- if gas == 1: emission_lines, line_name, line_wav = util.emission_lines( logLam_template, lamRange, FWHM_gal, quiet=quiet) nTemp = 0 for i in range(len(line_name)): if np.sum(mask[:,0] - line_wav[i] > 0) == np.sum( mask[:,1] - line_wav[i] > 0) and line_name[i] in lines: self.templatesToUse = np.append(self.templatesToUse, line_name[i]) self.templates.append(emission_lines[:,i]) nTemp += 1 # if NaD and np.sum(mask[:,0] - 588.995 > 0) == np.sum( # mask[:,1] -589.5924 > 0) and 'NaD' in lines: # if NaD and 'NaD' in lines: # print 'here' # self.templatesToUse = np.append(self.templatesToUse, 'NaD') # # taken from ppxf_util.py # lam = np.exp(logLam_template) # doublet = np.exp(-0.5*((lam - 588.995)/(FWHM_gal/2.355))**2) + \ # 0.5*np.exp(-0.5*((lam - 589.5924)/(FWHM_gal/2.355))**2) # self.templates.append(-doublet) # -ve to give absorption # nTemp += 1 self.component = self.component + [1]*nTemp self.element.append('gas') ## ----------=============== SF and shocks lines ==============--------- if gas == 2: emission_lines, line_name, line_wav = util.emission_lines( logLam_template, lamRange, FWHM_gal, quiet=quiet) for i in range(len(line_name)): if np.sum(mask[:,0] - line_wav[i] > 0) == np.sum( mask[:,1] - line_wav[i] > 0) and line_name[i] in lines: if 'H' in line_name[i]: self.templatesToUse = np.append(self.templatesToUse, line_name[i]) self.templates.append(emission_lines[:,i]) self.component = self.component + [1] self.element.append['SF'] else: self.templatesToUse = np.append(self.templatesToUse, line_name[i]) self.templates.append(emission_lines[:,i]) self.component = self.component + [2] self.element.append['Shocks'] ## ----------=========== All lines inderpendantly ==============--------- if gas == 3: emission_lines, line_name, line_wav = util.emission_lines( logLam_template, lamRange, FWHM_gal, quiet=quiet) aph_lin = np.sort(line_name) for i in range(len(line_name)): if np.sum(mask[:,0] - line_wav[i] > 0) == np.sum( mask[:,1] - line_wav[i] > 0) and line_name[i] in lines: self.templatesToUse = np.append(self.templatesToUse, line_name[i]) # line listed alphabetically self.component = self.component + \ [np.where(line_name[i] == aph_lin)[0][0]+1] self.templates.append(emission_lines[:,i]) self.element.append(aph_lin[i]) self.templates = np.array(self.templates).T if gas: self.ntemp = self.templates.shape[1] else: self.ntemp = 0
def ppxf_population_gas_example_sdss(): # Read SDSS DR8 galaxy spectrum taken from here http://www.sdss3.org/dr8/ # The spectrum is *already* log rebinned by the SDSS DR8 # pipeline and log_rebin should not be used in this case. # file = 'spectra/NGC3522_SDSS_DR8.fits' hdu = fits.open(file) t = hdu[1].data z = float(hdu[1].header["Z"]) # SDSS redshift estimate # Only use the wavelength range in common between galaxy and stellar library. # mask = (t['wavelength'] > 3540) & (t['wavelength'] < 7409) flux = t['flux'][mask] galaxy = flux / np.median( flux) # Normalize spectrum to avoid numerical issues wave = t['wavelength'][mask] # The noise level is chosen to give Chi^2/DOF=1 without regularization (REGUL=0). # A constant error is not a bad approximation in the fitted wavelength # range and reduces the noise in the fit. # noise = galaxy * 0 + 0.01528 # Assume constant noise per pixel here # The velocity step was already chosen by the SDSS pipeline # and we convert it below to km/s # c = 299792.458 # speed of light in km/s velscale = c * np.log(wave[1] / wave[0]) FWHM_gal = 2.76 # SDSS has an approximate instrumental resolution FWHM of 2.76A. #------------------- Setup templates ----------------------- stars_templates, lamRange_temp, logLam_temp = \ setup_spectral_library(velscale, FWHM_gal) # The stellar templates are reshaped into a 2-dim array with each spectrum # as a column, however we save the original array dimensions, which are # needed to specify the regularization dimensions # reg_dim = stars_templates.shape[1:] stars_templates = stars_templates.reshape(stars_templates.shape[0], -1) # See the pPXF documentation for the keyword REGUL, # for an explanation of the following two lines # stars_templates /= np.median( stars_templates) # Normalizes stellar templates by a scalar regul_err = 0.004 # Desired regularization error # Construct a set of Gaussian emission line templates. # Estimate the wavelength fitted range in the rest frame. # lamRange_gal = np.array([np.min(wave), np.max(wave)]) / (1 + z) gas_templates, line_names, line_wave = \ util.emission_lines(logLam_temp, lamRange_gal, FWHM_gal) # Combines the stellar and gaseous templates into a single array # during the PPXF fit they will be assigned a different kinematic # COMPONENT value # templates = np.column_stack([stars_templates, gas_templates]) #----------------------------------------------------------- # The galaxy and the template spectra do not have the same starting wavelength. # For this reason an extra velocity shift DV has to be applied to the template # to fit the galaxy spectrum. We remove this artificial shift by using the # keyword VSYST in the call to PPXF below, so that all velocities are # measured with respect to DV. This assume the redshift is negligible. # In the case of a high-redshift galaxy one should de-redshift its # wavelength to the rest frame before using the line below as described # in PPXF_KINEMATICS_EXAMPLE_SAURON. # c = 299792.458 dv = c * np.log(lamRange_temp[0] / wave[0]) # km/s vel = c * np.log(1 + z) # Relation between redshift and velocity in pPXF # Here the actual fit starts. The best fit is plotted on the screen. # # IMPORTANT: Ideally one would like not to use any polynomial in the fit # as the continuum shape contains important information on the population. # Unfortunately this is often not feasible, due to small calibration # uncertainties in the spectral shape. To avoid affecting the line strength of # the spectral features, we exclude additive polynomials (DEGREE=-1) and only use # multiplicative ones (MDEGREE=10). This is only recommended for population, not # for kinematic extraction, where additive polynomials are always recommended. # start = [vel, 180., 0, 0] # (km/s), starting guess for [V,sigma] t = clock() # Assign component=0 to the stellar templates and # component=1 to the gas emission lines templates. # One can easily assign different kinematic components to different gas species # e.g. component=1 for the Balmer series, component=2 for the [OIII] doublet, ...) # Input a negative MOMENTS value to keep fixed the LOSVD of a component. # nTemps = stars_templates.shape[1] nLines = gas_templates.shape[1] component = [0] * nTemps + [1] * nLines moments = [4, 2] # fit (V,sig,h3,h4) for the stars and (V,sig) for the gas start = [start, start] # adopt the same starting value for both gas and stars pp = ppxf(templates, galaxy, noise, velscale, start, plot=False, moments=moments, degree=-1, mdegree=10, vsyst=dv, clean=False, regul=1. / regul_err, reg_dim=reg_dim, component=component) # Plot fit results for stars and gas plt.clf() plt.subplot(211) plt.plot(wave, pp.galaxy, 'k') plt.plot(wave, pp.bestfit, 'b', linewidth=2) plt.xlabel("Observed Wavelength ($\AA$)") plt.ylabel("Relative Flux") plt.ylim([-0.1, 1.3]) plt.xlim([np.min(wave), np.max(wave)]) plt.plot(wave, pp.galaxy - pp.bestfit, 'd', ms=4, color='LimeGreen', mec='LimeGreen') # fit residuals plt.axhline(y=-0, linestyle='--', color='k', linewidth=2) stars = pp.matrix[:, :nTemps].dot(pp.weights[:nTemps]) plt.plot(wave, stars, 'r', linewidth=2) # overplot stellar templates alone gas = pp.matrix[:, -nLines:].dot(pp.weights[-nLines:]) plt.plot(wave, gas + 0.15, 'b', linewidth=2) # overplot emission lines alone # When the two Delta Chi^2 below are the same, the solution is the smoothest # consistent with the observed spectrum. # print('Desired Delta Chi^2: %.4g' % np.sqrt(2 * galaxy.size)) print('Current Delta Chi^2: %.4g' % ((pp.chi2 - 1) * galaxy.size)) print('Elapsed time in PPXF: %.2f s' % (clock() - t)) w = np.array(component) == 1 # Extract weights of gas emissions only print('++++++++++++++++++++++++++++++') print('Gas V=%.4g and sigma=%.2g km/s' % (pp.sol[1][0], pp.sol[1][1])) # component=1 print('Emission lines peak intensity:') for name, weight, line in zip(line_names, pp.weights[w], pp.matrix[:, w].T): print('%12s: %.3g' % (name, weight * np.max(line))) print('------------------------------') # Plot stellar population mass distribution plt.subplot(212) weights = pp.weights[:np.prod(reg_dim)].reshape(reg_dim) / pp.weights.sum() plt.imshow(np.rot90(weights), interpolation='nearest', cmap='gist_heat', aspect='auto', origin='upper', extent=(np.log10(1.0), np.log10(17.7828), -1.9, 0.45)) plt.colorbar() plt.title("Mass Fraction") plt.xlabel("log$_{10}$ Age (Gyr)") plt.ylabel("[M/H]") plt.tight_layout() plt.show()
def ppxf_kinematics_gas_parallel(bin_sci, ppxf_file, ppxf_bestfit, template_fits, template_res, vel_init=1500., sig_init=100., bias=0.6, plot=False, quiet=True): """ Follow the pPXF usage example by Michile Cappellari INPUT: DIR_SCI_COMB (comb_fits_sci_{S/N}/bin_sci_{S/N}.fits), TEMPLATE_* (spectra/Mun1.30z*.fits) OUTPUT: PPXF_FILE (ppxf_output_sn30.txt), OUT_BESTFIT_FITS (ppxf_fits/ppxf_bestfit_sn30.fits) """ if os.path.exists(ppxf_file): print('File {} already exists'.format(ppxf_file)) return # Read in the galaxy spectrum, logarithmically rebin # assert len(glob.glob(bin_sci.format('*'))) > 0, 'Binned spectra {} not found'.format(glob.glob(bin_sci.format('*'))) with fits.open(bin_sci.format(0)) as gal_hdu: gal_data = gal_hdu[0].data gal_hdr = gal_hdu[0].header lamRange1 = gal_hdr['CRVAL1'] + np.array([1. - gal_hdr['CRPIX1'], gal_hdr['NAXIS1'] - gal_hdr['CRPIX1']]) \ * gal_hdr['CD1_1'] galaxy, logLam1, velscale = util.log_rebin(lamRange1, gal_data) galaxy = galaxy / np.median(galaxy) # Normalize spectrum to avoid numerical issues wave = np.exp(logLam1) # The noise level is chosen to give Chi^2/DOF=1 without regularization (REGUL=0). # A constant error is not a bad approximation in the fitted wavelength # range and reduces the noise in the fit. # noise = galaxy * 0 + 0.01528 # Assume constant noise per pixel here c = 299792.458 # speed of light in km/s FWHM_gal = 2.3 # GMOS IFU has an instrumental resolution FWHM of 2.3 A # stars_templates, lamRange_temp, logLam_temp = setup_spectral_library(velscale, FWHM_gal) # Read in template galaxy spectra # template_spectra = glob.glob(template_fits) assert len(template_spectra) > 0, 'Template spectra not found: {}'.format(template_fits) with fits.open(template_spectra[0]) as temp_hdu: ssp = temp_hdu[0].data h2 = temp_hdu[0].header lamRange2 = h2['CRVAL1'] + np.array([0., h2['CDELT1'] * (h2['NAXIS1'] - 1)]) sspNew, logLam2, velscale = util.log_rebin(lamRange2, ssp, velscale=velscale) stars_templates = np.empty((sspNew.size, len(template_spectra))) FWHM_tem = template_res # FWHM_dif = np.sqrt(FWHM_gal ** 2 - template_res ** 2) FWHM_dif = np.sqrt(FWHM_tem ** 2 - FWHM_gal ** 2) sigma = FWHM_dif / 2.355 / h2['CDELT1'] # SIGMA DIFFERENCE IN PIXELS, 1.078435697220085 for j in range(len(template_spectra)): with fits.open(template_spectra[j]) as temp_hdu_j: ssp_j = temp_hdu_j[0].data ssp_j = ndimage.gaussian_filter1d(ssp_j, sigma) sspNew, logLam2, velscale = util.log_rebin(lamRange2, ssp_j, velscale=velscale) stars_templates[:, j] = sspNew / np.median(sspNew) # Normalizes templates # we save the original array dimensions, which are needed to specify the regularization dimensions # reg_dim = stars_templates.shape[1:] # See the pPXF documentation for the keyword REGUL, # for an explanation of the following two lines # regul_err = 0.004 # Desired regularization error # Construct a set of Gaussian emission line templates. # Estimate the wavelength fitted range in the rest frame. # z = np.exp(vel_init / c) - 1 # Relation between velocity and redshift in pPXF lamRange_gal = np.array([lamRange1[0], lamRange1[-1]]) / (1 + z) gas_templates, line_names, line_wave = util.emission_lines(logLam2, lamRange_gal, FWHM_gal) # Combines the stellar and gaseous templates into a single array # during the PPXF fit they will be assigned a different kinematic COMPONENT value # templates = np.hstack([stars_templates, gas_templates]) # Here the actual fit starts. The best fit is plotted on the screen. # # IMPORTANT: Ideally one would like not to use any polynomial in the fit # as the continuum shape contains important information on the population. # Unfortunately this is often not feasible, due to small calibration # uncertainties in the spectral shape. To avoid affecting the line strength of # the spectral features, we exclude additive polynomials (DEGREE=-1) and only use # multiplicative ones (MDEGREE=10). This is only recommended for population, not # for kinematic extraction, where additive polynomials are always recommended. # # start = [vel, 180.] # (km/s), starting guess for [V,sigma] start = [vel_init, sig_init] # (km/s), starting guess for [V,sigma] # Assign component=0 to the stellar templates and # component=1 to the gas emission lines templates. # One can easily assign different kinematic components to different gas species # e.g. component=1 for the Balmer series, component=2 for the [OIII] doublet, ...) # Input a negative MOMENTS value to keep fixed the LOSVD of a component. # nTemps = stars_templates.shape[1] nLines = gas_templates.shape[1] component = [0] * nTemps + [1] * nLines moments = [4, 2] # fit (V,sig,h3,h4) for the stars and (V,sig) for the gas start = [start, start] # adopt the same starting value for both gas and stars bin_sci_list = glob.glob(bin_sci.format('*')) pp_sol = np.zeros([moments[0] + moments[1], len(bin_sci_list)]) pp_error = np.zeros([moments[0] + moments[1], len(bin_sci_list)]) pp = Parallel(n_jobs=len(bin_sci_list))( delayed(ppxf_gas_kinematics_parallel_loop)(bin_sci.format(j), ppxf_bestfit.format(j), gal_hdr, templates, velscale, sigma, lamRange1, logLam2, start, plot, moments, regul_err, reg_dim, component, bias, quiet) for j in range(len(bin_sci_list))) with open(ppxf_file, 'a') as outfile: outfile.write(pp) '''
def ppxf_kinematics_gas(bin_sci, ppxf_file, ppxf_bestfit, template_fits, template_res, vel_init=1500., sig_init=100., bias=0.6, plot=False, quiet=True): """ Follow the pPXF usage example by Michile Cappellari INPUT: DIR_SCI_COMB (comb_fits_sci_{S/N}/bin_sci_{S/N}.fits), TEMPLATE_* (spectra/Mun1.30z*.fits) OUTPUT: PPXF_FILE (ppxf_output_sn30.txt), OUT_BESTFIT_FITS (ppxf_fits/ppxf_bestfit_sn30.fits) """ if os.path.exists(ppxf_file): print('File {} already exists'.format(ppxf_file)) return # Read in the galaxy spectrum, logarithmically rebin # assert len(glob.glob(bin_sci.format('*'))) > 0, 'Binned spectra {} not found'.format(glob.glob(bin_sci.format('*'))) with fits.open(bin_sci.format(0)) as gal_hdu: gal_data = gal_hdu[0].data gal_hdr = gal_hdu[0].header lamRange1 = gal_hdr['CRVAL1'] + np.array([1. - gal_hdr['CRPIX1'], gal_hdr['NAXIS1'] - gal_hdr['CRPIX1']]) \ * gal_hdr['CD1_1'] galaxy, logLam1, velscale = util.log_rebin(lamRange1, gal_data) galaxy = galaxy / np.median(galaxy) # Normalize spectrum to avoid numerical issues wave = np.exp(logLam1) # The noise level is chosen to give Chi^2/DOF=1 without regularization (REGUL=0). # A constant error is not a bad approximation in the fitted wavelength # range and reduces the noise in the fit. # noise = galaxy * 0 + 0.01528 # Assume constant noise per pixel here c = 299792.458 # speed of light in km/s FWHM_gal = 2.3 # GMOS IFU has an instrumental resolution FWHM of 2.3 A # stars_templates, lamRange_temp, logLam_temp = setup_spectral_library(velscale, FWHM_gal) # Read in template galaxy spectra # template_spectra = glob.glob(template_fits) assert len(template_spectra) > 0, 'Template spectra not found: {}'.format(template_fits) with fits.open(template_spectra[0]) as temp_hdu: ssp = temp_hdu[0].data h2 = temp_hdu[0].header lamRange2 = h2['CRVAL1'] + np.array([0., h2['CDELT1'] * (h2['NAXIS1'] - 1)]) sspNew, logLam2, velscale = util.log_rebin(lamRange2, ssp, velscale=velscale) stars_templates = np.empty((sspNew.size, len(template_spectra))) FWHM_tem = template_res # FWHM_dif = np.sqrt(FWHM_gal ** 2 - template_res ** 2) FWHM_dif = np.sqrt(FWHM_tem ** 2 - FWHM_gal ** 2) sigma = FWHM_dif / 2.355 / h2['CDELT1'] # SIGMA DIFFERENCE IN PIXELS, 1.078435697220085 for j in range(len(template_spectra)): with fits.open(template_spectra[j]) as temp_hdu_j: ssp_j = temp_hdu_j[0].data ssp_j = ndimage.gaussian_filter1d(ssp_j, sigma) sspNew, logLam2, velscale = util.log_rebin(lamRange2, ssp_j, velscale=velscale) stars_templates[:, j] = sspNew / np.median(sspNew) # Normalizes templates # we save the original array dimensions, which are needed to specify the regularization dimensions # reg_dim = stars_templates.shape[1:] # See the pPXF documentation for the keyword REGUL, # for an explanation of the following two lines # regul_err = 0.004 # Desired regularization error # Construct a set of Gaussian emission line templates. # Estimate the wavelength fitted range in the rest frame. # z = np.exp(vel_init / c) - 1 # Relation between velocity and redshift in pPXF lamRange_gal = np.array([lamRange1[0], lamRange1[-1]]) / (1 + z) gas_templates, line_names, line_wave = util.emission_lines(logLam2, lamRange_gal, FWHM_gal) # Combines the stellar and gaseous templates into a single array # during the PPXF fit they will be assigned a different kinematic COMPONENT value # templates = np.hstack([stars_templates, gas_templates]) # Here the actual fit starts. The best fit is plotted on the screen. # # IMPORTANT: Ideally one would like not to use any polynomial in the fit # as the continuum shape contains important information on the population. # Unfortunately this is often not feasible, due to small calibration # uncertainties in the spectral shape. To avoid affecting the line strength of # the spectral features, we exclude additive polynomials (DEGREE=-1) and only use # multiplicative ones (MDEGREE=10). This is only recommended for population, not # for kinematic extraction, where additive polynomials are always recommended. # # start = [vel, 180.] # (km/s), starting guess for [V,sigma] start = [vel_init, sig_init] # (km/s), starting guess for [V,sigma] # Assign component=0 to the stellar templates and # component=1 to the gas emission lines templates. # One can easily assign different kinematic components to different gas species # e.g. component=1 for the Balmer series, component=2 for the [OIII] doublet, ...) # Input a negative MOMENTS value to keep fixed the LOSVD of a component. # nTemps = stars_templates.shape[1] nLines = gas_templates.shape[1] component = [0] * nTemps + [1] * nLines moments = [4, 2] # fit (V,sig,h3,h4) for the stars and (V,sig) for the gas start = [start, start] # adopt the same starting value for both gas and stars bin_sci_list = glob.glob(bin_sci.format('*')) pp_sol = np.zeros([moments[0] + moments[1], len(bin_sci_list)]) pp_error = np.zeros([moments[0] + moments[1], len(bin_sci_list)]) for j in range(len(bin_sci_list)): gal_data = fits.getdata(bin_sci.format(j), 0) gal_data_new = ndimage.gaussian_filter1d(gal_data, sigma) galaxy, logLam1, velscale = util.log_rebin(lamRange1, gal_data_new, velscale=velscale) noise = galaxy * 0 + 1 # Assume constant noise per pixel here # The galaxy and the template spectra do not have the same starting wavelength. # For this reason an extra velocity shift DV has to be applied to the template # to fit the galaxy spectrum. We remove this artificial shift by using the # keyword VSYST in the call to PPXF below, so that all velocities are # measured with respect to DV. This assume the redshift is negligible. # In the case of a high-redshift galaxy one should de-redshift its # wavelength to the rest frame before using the line below as described # in PPXF_KINEMATICS_EXAMPLE_SAURON. # c = 299792.458 dv = (logLam2[0] - logLam1[0]) * c # km/s t = clock() pp = ppxf(templates, galaxy, noise, velscale, start, plot=plot, moments=moments, degree=-1, mdegree=10, vsyst=dv, clean=False, regul=1. / regul_err, reg_dim=reg_dim, component=component, bias=bias, quiet=quiet) # Save the velocity, sigma, h3, h4 information for both stellar and gas to a table for k, sol in enumerate(pp.sol[0]): pp_sol[k, j] = pp.sol[0][k] pp_error[k, j] = pp.error[0][k] for k, sol in enumerate(pp.sol[1]): pp_sol[k + len(pp.sol[0]), j] = pp.sol[1][k] pp_error[k + len(pp.error[0]), j] = pp.error[1][k] # Plot fit results for stars and gas # if plot: plt.clf() # plt.subplot(211) plt.plot(wave, pp.galaxy, 'k') plt.plot(wave, pp.bestfit, 'b', linewidth=2) plt.xlabel("Observed Wavelength ($\AA$)") plt.ylabel("Relative Flux") plt.ylim([-0.1, 1.3]) plt.xlim([np.min(wave), np.max(wave)]) plt.plot(wave, pp.galaxy - pp.bestfit, 'd', ms=4, color='LimeGreen', mec='LimeGreen') # fit residuals plt.axhline(y=-0, linestyle='--', color='k', linewidth=2) stars = pp.matrix[:, :nTemps].dot(pp.weights[:nTemps]) plt.plot(wave, stars, 'r', linewidth=2) # overplot stellar templates alone gas = pp.matrix[:, -nLines:].dot(pp.weights[-nLines:]) plt.plot(wave, gas + 0.15, 'b', linewidth=2) # overplot emission lines alone plt.legend() plt.show() # When the two Delta Chi^2 below are the same, the solution is the smoothest # consistent with the observed spectrum. # print('Desired Delta Chi^2: %.4g' % np.sqrt(2 * galaxy.size)) print('Current Delta Chi^2: %.4g' % ((pp.chi2 - 1) * galaxy.size)) print('Elapsed time in PPXF: %.2f s' % (clock() - t)) w = np.where(np.array(component) == 1)[0] # Extract weights of gas emissions print('++++++++++++++++++++++++++++++') print('Gas V=%.4g and sigma=%.2g km/s' % (pp.sol[1][0], pp.sol[1][1])) print('Emission lines peak intensity:') for name, weight, line in zip(line_names, pp.weights[w], pp.matrix[:, w].T): print('%12s: %.3g' % (name, weight * np.max(line))) print('------------------------------') # Plot stellar population mass distribution # plt.subplot(212) # weights = pp.weights[:np.prod(reg_dim)].reshape(reg_dim) / pp.weights.sum() # plt.imshow(np.rot90(weights), interpolation='nearest', cmap='gist_heat', aspect='auto', origin='upper', # extent=(np.log10(1.0), np.log10(17.7828), -1.9, 0.45)) # plt.colorbar() # plt.title("Mass Fraction") # plt.xlabel("log$_{10}$ Age (Gyr)") # plt.ylabel("[M/H]") # plt.tight_layout() plt.legend() plt.show() if not quiet: print("Formal errors:") print(" dV dsigma dh3 dh4") print("".join("%8.2g" % f for f in pp.error * np.sqrt(pp.chi2))) hdu_best = fits.PrimaryHDU() hdu_best.data = pp.bestfit hdu_best.header['CD1_1'] = gal_hdr['CD1_1'] hdu_best.header['CDELT1'] = gal_hdr['CD1_1'] hdu_best.header['CRPIX1'] = gal_hdr['CRPIX1'] hdu_best.header['CRVAL1'] = gal_hdr['CRVAL1'] hdu_best.header['NAXIS1'] = pp.bestfit.size hdu_best.header['CTYPE1'] = 'LINEAR' # corresponds to sampling of values above hdu_best.header['DC-FLAG'] = '1' # 0 = linear, 1= log-linear sampling hdu_best.writeto(ppxf_bestfit.format(j), clobber=True) np.savetxt(ppxf_file, np.column_stack( [pp_sol[0], pp_error[0], pp_sol[1], pp_error[1], pp_sol[2], pp_error[2], pp_sol[3], pp_error[3], pp_sol[4], pp_error[4], pp_sol[5], pp_error[5]]), fmt=b'%10.4f %10.4f %10.4f %10.4f %10.4f %10.4f %10.4f %10.4f %10.4f %10.4f %10.4f %10.4f', header='Stellar pop: Gas: \n' 'vel d_vel sigma d_sigma h3 d_h3 h4 d_h4 vel d_vel sigma d_sigma')
def ppxf_population_gas_sdss(file, z, name): # Read SDSS DR8 galaxy spectrum taken from here http://www.sdss3.org/dr8/ # The spectrum is *already* log rebinned by the SDSS DR8 # pipeline and log_rebin should not be used in this case. hdulist = pyfits.open(file) VAC = (10**hdulist[1].data.loglam) wave = [] for i in range(0, len(VAC)): wave.append(VAC[i] / (1.0 + 2.735182E-4 + 131.4182 / VAC[i]**2 + 2.76249E8 / VAC[i]**4) / (1+z)) flux = hdulist[1].data.flux*10**-17 err = hdulist[1].data.ivar*10**-17 #bunit = hdulist[0].header['bunit'] #c0 = hdulist[0].header['coeff0'] #c1 = hdulist[0].header['coeff1'] #units = 'erg/s/cm^2/Ang' xarr = pyspeckit.units.SpectroscopicAxis(wave, units='angstroms') spec = pyspeckit.OpticalSpectrum(header=hdulist[0].header, xarr=xarr, data=flux*1e17, error=err) #spec.units = 'erg s^{-1} cm^{-2} \\AA^{-1}' #spec.xarr.units='angstroms' #Galactic extinction correction #Take the ebv of the galaxy from IrsaDust table = IrsaDust.get_query_table(name, section='ebv') ebv = table['ext SFD mean'][0] spec.deredden(ebv=ebv) # deredden in place t = hdulist[1].data #z = float(hdu[1].header["Z"]) # SDSS redshift estimate # Create the mask # Only use the wavelength range in common between galaxy and stellar library. mask = [True]*(len(wave)) for i in range(0, len(wave)): #mask[i]=(wave[i] > 3540) & (wave[i] < 7409) mask[i] = (wave[i] > 3750) & (wave[i] < 7400) # take a smaller the wavelength range #mask for the galaxy galaxy = t.field('flux')/np.median(t.field('flux')) # Normalize spectrum to avoid numerical issues galaxymask = [] for i in range(0, len(mask)): if mask[i]: galaxymask.append(galaxy[i]) galaxy = np.array(galaxymask) #mask for the wavelength #create an array with only the allowed values of the wavelenght wavemask = [] for i in range(0, len(mask)): if mask[i]: wavemask.append(wave[i]) wave = np.array(wavemask) #create a mask for the emission lines NeIIIa = 3869.9 NeIIIb = 3971.1 Heps = 3890.2 Hdelta = 4102.9 Hgamma = 4341.7 OIIIc = 4364.4 HeIIa = 4687.0 HeIIb = 5413.0 SIII = 6313.8 OIa = 5578.9 OIb = 6365.5 Hbeta = 4861.33 OIIIa = 4958.92 OIIIb = 5006.84 OI = 6300.30 NIIa = 6549.86 NIIb = 6585.27 Halpha = 6564.614 SIIa = 6718.2 SIIb = 6732.68 ArIII = 7137.8 delta = 10 delta2 = 20 maskHa = [True]*(len(wave)) for i in range(0, len(wave)): maskHa[i] = (((wave[i] < (Halpha - delta2)) or (wave[i] > (Halpha + delta2))) & ((wave[i] < (Hbeta - delta2)) or (wave[i] > (Hbeta + delta2))) & ((wave[i] < (OIIIa - delta)) or (wave[i] > (OIIIa + delta))) & ((wave[i] < (OIIIb - delta)) or (wave[i] > (OIIIb + delta))) & ((wave[i] < (OI - delta)) or (wave[i] > (OI + delta))) & ((wave[i] < (NIIa - delta)) or (wave[i] > (NIIa + delta))) & ((wave[i] < (NIIb - delta)) or (wave[i] > (NIIb + delta))) & ((wave[i] < (SIIa - delta)) or (wave[i] > (SIIa + delta))) & ((wave[i] < (SIIb - delta)) or (wave[i] > (SIIb + delta))) & ((wave[i] < (NeIIIa - delta)) or (wave[i] > (NeIIIa + delta))) & ((wave[i] < (NeIIIb - delta)) or (wave[i] > (NeIIIb + delta))) & ((wave[i] < (Heps - delta)) or (wave[i] > (Heps + delta))) & ((wave[i] < (Hdelta - delta)) or (wave[i] > (Hdelta + delta))) & ((wave[i] < (Hgamma - delta)) or (wave[i] > (Hgamma + delta))) & ((wave[i] < (OIIIc - delta)) or (wave[i] > (OIIIc + delta))) & ((wave[i] < (HeIIa - delta)) or (wave[i] > (HeIIa + delta))) & ((wave[i] < (HeIIb - delta)) or (wave[i] > (HeIIb + delta))) & ((wave[i] < (SIII - delta)) or (wave[i] > (SIII + delta))) & ((wave[i] < (OIa - delta)) or (wave[i] > (OIa + delta))) & ((wave[i] < (OIb - delta)) or (wave[i] > (OIb + delta))) & ((wave[i] < (ArIII - delta)) or (wave[i] > (ArIII + delta)))) # mask for the wavelength for the emission lines # create an array with only the allowed values of the wavelenght wavemask = [] for i in range(0, len(maskHa)): if maskHa[i]: wavemask.append(wave[i]) wave = np.array(wavemask) #Use this mask for the galaxy galaxymask = [] for i in range(0, len(maskHa)): if maskHa[i]: galaxymask.append(galaxy[i]) galaxy = np.array(galaxymask) # The noise level is chosen to give Chi^2/DOF=1 without regularization (REGUL=0) # # noise = galaxy*0 + 0.01528 # Assume constant noise per pixel here # The velocity step was already chosen by the SDSS pipeline # and we convert it below to km/s # c = 299792.458 # speed of light in km/s velscale = np.log(wave[1]/wave[0])*c FWHM_gal = 2.76 # SDSS has an instrumental resolution FWHM of 2.76A. stars_templates, lamRange_temp, logLam_temp = \ setup_spectral_library(velscale, FWHM_gal) # The stellar templates are reshaped into a 2-dim array with each spectrum # as a column, however we save the original array dimensions, which are # needed to specify the regularization dimensions # reg_dim = stars_templates.shape[1:] stars_templates = stars_templates.reshape(stars_templates.shape[0], -1) # See the pPXF documentation for the keyword REGUL, # for an explanation of the following two lines # stars_templates /= np.median(stars_templates) # Normalizes stellar templates by a scalar regul_err = 0.004 # Desired regularization error # Construct a set of Gaussian emission line templates # gas_templates = util.emission_lines(logLam_temp, FWHM_gal) # Combines the stellar and gaseous templates into a single array # during the PPXF fit they will be assigned a different kinematic # COMPONENT value # templates = np.hstack([stars_templates, gas_templates]) # The galaxy and the template spectra do not have the same starting wavelength. # For this reason an extra velocity shift DV has to be applied to the template # to fit the galaxy spectrum. We remove this artificial shift by using the # keyword VSYST in the call to PPXF below, so that all velocities are # measured with respect to DV. This assume the redshift is negligible. # In the case of a high-redshift galaxy one should de-redshift its # wavelength to the rest frame before using the line below as described # in PPXF_KINEMATICS_EXAMPLE_SAURON. # z = 0 # redshift already corrected c = 299792.458 dv = (np.log(lamRange_temp[0])-np.log(wave[0]))*c # km/s vel = c*z # Initial estimate of the galaxy velocity in km/s # Here the actual fit starts. The best fit is plotted on the screen. # # IMPORTANT: Ideally one would like not to use any polynomial in the fit # as the continuum shape contains important information on the population. # Unfortunately this is often not feasible, due to small calibration # uncertainties in the spectral shape. To avoid affecting the line strength of # the spectral features, we exclude additive polynomials (DEGREE=-1) and only use # multiplicative ones (MDEGREE=10). This is only recommended for population, not # for kinematic extraction, where additive polynomials are always recommended. # start = [vel, 180.] # (km/s), starting guess for [V,sigma] t = clock() plt.clf() plt.subplot(211) # Assign component=0 to the stellar templates and # component=1 to the gas emission lines templates. # One can easily assign different components to different gas species # e.g. component=1 for the Balmer series, component=2 for the [OIII] doublet, ...) # Input a negative MOMENTS value to keep fixed the LOSVD of a component. # component = [0]*stars_templates.shape[1] + [1]*gas_templates.shape[1] moments = [4, 4] # fit (V,sig,h3,h4) for both the stars and the gas start = [start, start] # adopt the same starting value for both gas and stars pp = ppxf(file, templates, wave, galaxy, noise, velscale, start, plot=True, moments=moments, degree=-1, mdegree=10, vsyst=dv, clean=False, regul=1./regul_err, reg_dim=reg_dim, component=component) # When the two numbers below are the same, the solution is the smoothest # consistent with the observed spectrum. # print 'Desired Delta Chi^2:', np.sqrt(2*galaxy.size) print 'Current Delta Chi^2:', (pp.chi2 - 1)*galaxy.size print 'elapsed time in PPXF (s):', clock() - t plt.subplot(212) #plt.set_cmap('gist_heat') # = IDL's loadct, 3 plt.imshow(np.rot90(pp.weights[:np.prod(reg_dim)].reshape(reg_dim)/pp.weights.sum()), interpolation='nearest', aspect='auto', extent=(np.log(1.0), np.log(17.7828), -1.9, 0.45)) plt.set_cmap('gist_heat') # = IDL's loadct, 3 plt.colorbar() plt.title("Mass Fraction") plt.xlabel("log Age (Gyr)") plt.ylabel("[M/H]") plt.tight_layout() # Save the figure name = splitext(basename(file))[0] plt.savefig(name) return
def ppxf_population_gas(bin_sci, FWHM_gal, template_fits, FWHM_tem, ppxf_file, ppxf_bestfit, vel_init=1500, bias=0.6, plot=False): """ """ if os.path.exists(ppxf_file): print('File {} already exists'.format(ppxf_file)) return # Read a galaxy spectrum and define the wavelength range bin_sci_list = glob.glob(bin_sci.format('*')) assert len(bin_sci_list) > 0, 'Binned spectra {} not found'.format(glob.glob(bin_sci.format('*'))) # Read SDSS DR8 galaxy spectrum taken from here http://www.sdss3.org/dr8/ # The spectrum is *already* log rebinned by the SDSS DR8 # pipeline and log_rebin should not be used in this case. # # file = 'spectra/NGC3522_SDSS.fits' # with fits.open(file) as hdu: # t = hdu[1].data # z = float(hdu[1].header["Z"]) # SDSS redshift estimate # with fits.open(bin_sci.format(0)) as hdu: gal_lin = hdu[0].data hdr = hdu[0].header # Only use the wavelength range in common between galaxy and stellar library. # # mask = (t.field('wavelength') > 3540) & (t.field('wavelength') < 7409) # galaxy = t[mask].field('flux') / np.median(t[mask].field('flux')) # Normalize spectrum to avoid numerical issues # wave = t[mask].field('wavelength') # lam_range = hdr['CRVAL1'] + np.array([1. - hdr['CRPIX1'], hdr['NAXIS1'] - hdr['CRPIX1']]) * hdr['CD1_1'] # log rebin the bins, not done here because SDSS alrady log binned, which is what this example uses galaxy, wave, velscale = util.log_rebin(lam_range, gal_lin) galaxy = galaxy/np.median(galaxy) # our data set does not exceed the range of the template so dont mask # The noise level is chosen to give Chi^2/DOF=1 without regularization (REGUL=0). # A constant error is not a bad approximation in the fitted wavelength # range and reduces the noise in the fit. # noise = galaxy * 0 + 0.01528 # Assume constant noise per pixel here # The velocity step was already chosen by the SDSS pipeline # and we convert it below to km/s # c = 299792.458 # speed of light in km/s # velscale = np.log(wave[1] / wave[0]) * c z = np.exp(vel_init / c) - 1 # FWHM_gal = 2.76 # SDSS has an instrumental resolution FWHM of 2.76A. #------------------- Setup templates ----------------------- stars_templates, lamRange_temp, logLam_temp, logAge_grid, metal_grid = setup_spectral_library(velscale, FWHM_gal, template_fits, FWHM_tem) # The stellar templates are reshaped into a 2-dim array with each spectrum # as a column, however we save the original array dimensions, which are # needed to specify the regularization dimensions # reg_dim = stars_templates.shape[1:] stars_templates = stars_templates.reshape(stars_templates.shape[0], -1) # See the pPXF documentation for the keyword REGUL, # for an explanation of the following two lines # stars_templates /= np.median(stars_templates) # Normalizes stellar templates by a scalar regul_err = 0.004 # Desired regularization error # Construct a set of Gaussian emission line templates. # Estimate the wavelength fitted range in the rest frame. # # lamRange_gal = np.array([np.min(wave), np.max(wave)]) / (1 + z) # gas_templates, line_names, line_wave = util.emission_lines(logLam_temp, lamRange_gal, FWHM_gal) gas_templates, line_names, line_wave = util.emission_lines(logLam_temp, lam_range, FWHM_gal) # Combines the stellar and gaseous templates into a single array # during the PPXF fit they will be assigned a different kinematic # COMPONENT value # templates = np.hstack([stars_templates, gas_templates]) #----------------------------------------------------------- # The galaxy and the template spectra do not have the same starting wavelength. # For this reason an extra velocity shift DV has to be applied to the template # to fit the galaxy spectrum. We remove this artificial shift by using the # keyword VSYST in the call to PPXF below, so that all velocities are # measured with respect to DV. This assume the redshift is negligible. # In the case of a high-redshift galaxy one should de-redshift its # wavelength to the rest frame before using the line below as described # in PPXF_KINEMATICS_EXAMPLE_SAURON. # c = 299792.458 # dv = (np.log(lamRange_temp[0]) - np.log(wave[0])) * c # km/s dv = (np.log(lamRange_temp[0]) - wave[0]) * c # km/s # vel = c * z # Initial estimate of the galaxy velocity in km/s vel = vel_init # Here the actual fit starts. The best fit is plotted on the screen. # # IMPORTANT: Ideally one would like not to use any polynomial in the fit # as the continuum shape contains important information on the population. # Unfortunately this is often not feasible, due to small calibration # uncertainties in the spectral shape. To avoid affecting the line strength of # the spectral features, we exclude additive polynomials (DEGREE=-1) and only use # multiplicative ones (MDEGREE=10). This is only recommended for population, not # for kinematic extraction, where additive polynomials are always recommended. # start = [vel, 180.] # (km/s), starting guess for [V,sigma] t = clock() # Assign component=0 to the stellar templates and # component=1 to the gas emission lines templates. # One can easily assign different kinematic components to different gas species # e.g. component=1 for the Balmer series, component=2 for the [OIII] doublet, ...) # Input a negative MOMENTS value to keep fixed the LOSVD of a component. # nTemps = stars_templates.shape[1] nLines = gas_templates.shape[1] component = [0] * nTemps + [1] * nLines moments = [4, 2] # fit (V,sig,h3,h4) for the stars and (V,sig) for the gas start = [start, start] # adopt the same starting value for both gas and stars pp_sol = np.zeros([moments[0] + moments[1], len(bin_sci_list)]) pp_error = np.zeros([moments[0] + moments[1], len(bin_sci_list)]) for j in range(len(bin_sci_list)): gal_data = fits.getdata(bin_sci.format(j), 0) galaxy, logLam1, velscale = util.log_rebin(lam_range, gal_data, velscale=velscale) pp = ppxf(templates, galaxy, noise, velscale, start, plot=plot, moments=moments, degree=-1, mdegree=10, vsyst=dv, clean=False, regul=1. / regul_err, reg_dim=reg_dim, component=component, bias=bias) # Save the velocity, sigma, h3, h4 information for both stellar and gas to a table for k, sol in enumerate(pp.sol[0]): pp_sol[k, j] = pp.sol[0][k] pp_error[k, j] = pp.error[0][k] for k, sol in enumerate(pp.sol[1]): pp_sol[k + len(pp.sol[0]), j] = pp.sol[1][k] pp_error[k + len(pp.error[0]), j] = pp.error[1][k] # Plot fit results for stars and gas if plot: plt.clf() plt.subplot(211) plt.plot(wave, pp.galaxy, 'k') plt.plot(wave, pp.bestfit, 'b', linewidth=2) plt.xlabel("Observed Wavelength ($\AA$)") plt.ylabel("Relative Flux") #plt.ylim([-0.1, 5]) plt.xlim([np.min(wave), np.max(wave)]) plt.plot(wave, pp.galaxy - pp.bestfit, 'd', ms=4, color='LimeGreen', mec='LimeGreen') # fit residuals plt.axhline(y=-0, linestyle='--', color='k', linewidth=2) stars = pp.matrix[:, :nTemps].dot(pp.weights[:nTemps]) plt.plot(wave, stars, 'r', linewidth=2) # overplot stellar templates alone gas = pp.matrix[:, -nLines:].dot(pp.weights[-nLines:]) plt.plot(wave, gas + 0.15, 'b', linewidth=2) # overplot emission lines alone # When the two Delta Chi^2 below are the same, the solution is the smoothest # consistent with the observed spectrum. # print('Desired Delta Chi^2: %.4g' % np.sqrt(2 * galaxy.size)) print('Current Delta Chi^2: %.4g' % ((pp.chi2 - 1) * galaxy.size)) print('Elapsed time in PPXF: %.2f s' % (clock() - t)) w = np.where(np.array(component) == 1)[0] # Extract weights of gas emissions print('++++++++++++++++++++++++++++++') print('Gas V=%.4g and sigma=%.2g km/s' % (pp.sol[1][0], pp.sol[1][1])) print('Emission lines peak intensity:') for name, weight, line in zip(line_names, pp.weights[w], pp.matrix[:, w].T): print('%12s: %.3g' % (name, weight * np.max(line))) print('------------------------------') # Plot stellar population mass distribution weight_arr = pp.weights[:stars_templates.shape[1]] print('Mass-weighted <logAge> [Gyr]: %.3g' % (np.sum(weight_arr*logAge_grid.ravel())/np.sum(weight_arr))) print('Mass-weighted <[M/H]>: %.3g' % (np.sum(weight_arr*metal_grid.ravel())/np.sum(weight_arr))) plt.subplot(212) weights = pp.weights[:np.prod(reg_dim)].reshape(reg_dim) / pp.weights.sum() plt.imshow(np.rot90(weights), interpolation='nearest', cmap='gist_heat', aspect='auto', origin='upper', extent=(np.log10(1.0), np.log10(17.7828), -1.9, 0.45)) plt.colorbar() plt.title("Mass Fraction") plt.xlabel("log$_{10}$ Age (Gyr)") plt.ylabel("[M/H]") plt.tight_layout() plt.show() np.savetxt(ppxf_bestfit.format(j), pp.bestfit) np.savetxt(ppxf_file, np.column_stack( [pp_sol[0], pp_error[0], pp_sol[1], pp_error[1], pp_sol[2], pp_error[2], pp_sol[3], pp_error[3], pp_sol[4], pp_error[4], pp_sol[5], pp_error[5]]), fmt=b'%10.4f %10.4f %10.4f %10.4f %10.4f %10.4f %10.4f %10.4f %10.4f %10.4f %10.4f %10.4f', header='Stellar pop: Gas: \n' 'vel d_vel sigma d_sigma h3 d_h3 h4 d_h4 vel d_vel sigma d_sigma')