def emission(dirfile, w1, f1, redshift, plateifu, tie_balmer, limit_doublets): ppxf_dir = path.dirname(path.realpath(ppxf_package.__file__)) z = redshift flux = f1 galaxy = flux wave = w1 wave *= np.median(util.vac_to_air(wave) / wave) noise = np.full_like(galaxy, 0.01635) # Assume constant noise per pixel here c = 299792.458 # speed of light in km/s velscale = c * np.log(wave[1] / wave[0]) # eq.(8) of Cappellari (2017) # SDSS has an approximate instrumental resolution FWHM of 2.76A. FWHM_gal = 2.76 # ------------------- Setup templates ----------------------- pathname = ppxf_dir + '/miles_models/Mun1.30*.fits' # The templates are normalized to mean=1 within the FWHM of the V-band. # In this way the weights and mean values are light-weighted quantities miles = lib.miles(pathname, velscale, FWHM_gal) reg_dim = miles.templates.shape[1:] regul_err = 0.013 # Desired regularization error 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, tie_balmer=tie_balmer, limit_doublets=limit_doublets) templates = gas_templates c = 299792.458 dv = c * (miles.log_lam_temp[0] - np.log(wave[0]) ) # eq.(8) of Cappellari (2017) vel = c * np.log(1 + z) # eq.(8) of Cappellari (2017) start = [vel, 180.] # (km/s), starting guess for [V, sigma] # n_temps = stars_templates.shape[1] n_forbidden = np.sum(["[" in a for a in gas_names]) # forbidden lines contain "[*]" n_balmer = len(gas_names) - n_forbidden component = [0] * n_balmer + [1] * n_forbidden gas_component = np.array( component) >= 0 # gas_component=True for gas templates moments = [2, 2] start = [start, start] gas_reddening = 0 if tie_balmer else None t = clock() pp = gas.ppxf(dirfile, templates, galaxy, noise, velscale, start, z, plot=True, moments=moments, degree=-1, mdegree=10, vsyst=dv, lam=wave, clean=False, component=component, gas_component=gas_component, gas_names=gas_names, gas_reddening=gas_reddening) pp.plot() return pp.bestfit, pp.lam
def ppxf_example_population_gas_sdss(tie_balmer, limit_doublets): ppxf_dir = path.dirname(path.realpath(ppxf_package.__file__)) cube_id = 468 # reading cube_data cube_file = "data/cubes/cube_" + str(cube_id) + ".fits" hdu = fits.open(cube_file) t = hdu[0].data # using our redshift estimate from lmfit cube_result_file = ("results/cube_" + str(cube_id) + "/cube_" + str(cube_id) + "_lmfit.txt") cube_result_file = open(cube_result_file) line_count = 0 for crf_line in cube_result_file: if (line_count == 20): curr_line = crf_line.split() z = float(curr_line[1]) line_count += 1 cube_x_data = np.load("results/cube_" + str(int(cube_id)) + "/cube_" + str(int(cube_id)) + "_cbd_x.npy") cube_y_data = np.load("results/cube_" + str(int(cube_id)) + "/cube_" + str(int(cube_id)) + "_cbs_y.npy") # Only use the wavelength range in common between galaxy and stellar library. # mask = (cube_x_data > 3540) & (cube_x_data < 7409) flux = cube_y_data[mask] galaxy = flux / np.median( flux) # Normalize spectrum to avoid numerical issues wave = cube_x_data[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 = ppxf_dir + '/miles_models/Mun1.30*.fits' miles = lib.miles(pathname, velscale, FWHM_gal) # The stellar templates are reshaped below 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, tie_balmer=tie_balmer, limit_doublets=limit_doublets) # 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 and Sec.2.4 of Cappellari (2017) # c = 299792.458 dv = c * (miles.log_lam_temp[0] - np.log(wave[0]) ) # eq.(8) of Cappellari (2017) vel = c * np.log(1 + z) # eq.(8) of Cappellari (2017) start = [vel, 180.] # (km/s), starting guess for [V, sigma] n_temps = stars_templates.shape[1] n_forbidden = np.sum(["[" in a for a in gas_names]) # forbidden lines contain "[*]" n_balmer = len(gas_names) - n_forbidden # 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 kinematic components moments = [4, 2, 2] # Adopt the same starting value for the stars and the two gas components start = [start, start, start] # If the Balmer lines are tied one should allow for gas reddeining. # The gas_reddening can be different from the stellar one, if both are fitted. gas_reddening = 0 if tie_balmer else None # Here the actual fit starts. # # 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. # t = clock() 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, gas_reddening=gas_reddening) # 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. 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) plt.show()
def fit_spectrum(spec, zgal, specresolution, tie_balmer=False, miles_dir=None, rebin=True, limit_doublets=False, degree_add=None, degree_mult=5, **kwargs): """This is a wrapper for pPXF to fit stellar population models as well as emission lines to galaxy spectra. Although pPXF allows otherwise, the emission lines are kinematically fixed to one another as are the stellar models, and the stars and gas independently vary with one another. Please see the pPXF documentation for more details on the vast array of parameters and options afforded by the software. The pPXF software may be downloaded at http://www-astro.physics.ox.ac.uk/~mxc/software/ Parameters ---------- spec : XSpectrum1D Spectrum to be fitted zgal : float Redshift of galaxy specresolution : float Spectral resolution (R) of the data tie_balmer : bool, optional Assume intrinsic Balmer decrement. See documentation in ppxf_util.py, as this has implications for the derived reddening. limit_doublets : bool, optional Limit the ratios of [OII] and [SII] lines to ranges allowed by atomic physics. See documentation in ppxf_util.py, as this has implications for getting the true flux values from those reported. degree_add : int, optional Degree of the additive Legendre polynomial used to modify the template continuum in the fit. degree_mult : int,optional miles_dir: str, optional Location of MILES models Returns ------- ppfit : ppxf Object returned by pPXF; attributes are data pertaining to the fit miles : miles Contains information about the stellar templates used in the fit. See the documentation in miles_util.py for full details weights : 1d numpy vector Weights of the *stellar* template components. Equivalent to the first N elements of ppfit.weights where N is the number of stellar templates used in the fit. """ if spec is None: print('Galaxy has no spectrum associated') return None ### Spectra must be rebinned to a log scale (in wavelength). ### pPXF provides a routine to do this, but the input spectra ### must be sampled uniformly linear. linetools to the rescue! if rebin: meddiff = np.median(spec.wavelength.value[1:] - spec.wavelength.value[0:-1]) newwave = np.arange(spec.wavelength.value[0], spec.wavelength.value[-1], meddiff) spec = spec.rebin(newwave * units.AA, do_sig=True, grow_bad_sig=True) # get data and transform wavelength to rest frame for template fitting wave = spec.wavelength.to(units.Angstrom).value flux = spec.flux.value flux = flux * (1. + zgal) noise = spec.sig.value noise = noise * (1. + zgal) wave = wave / (1. + zgal) # transform to air wavelengths for MILES templates and get approx. FWHM wave *= np.median(util.vac_to_air(wave) / wave) # use only wavelength range covered by templates mask = (wave > 3540) & (wave < 7409) #mask = (wave > 1682) & (wave < 10000.) maskidx = np.where(mask)[0] # also deal with declared good regions of the spectrum if 'goodpixels' in kwargs: goodpix = kwargs['goodpixels'] pixmask = np.in1d(maskidx, goodpix) newgoodpix = np.where(pixmask)[0] kwargs['goodpixels'] = newgoodpix wave = wave[mask] flux = flux[mask] noise = noise[mask] # Nonnegative noise values are not allowed noise[noise <= 0] = np.max(noise) # pPXF requires the spectra to be log rebinned, so do it flux, logLam, velscale = util.log_rebin(np.array([wave[0], wave[-1]]), flux) noise, junk1, junk2 = util.log_rebin(np.array([wave[0], wave[-1]]), noise) ### The following lines unnecessary for DEIMOS/Hecto spectra due to their ### units but rescaling may be necessary for some # galaxy = flux / np.median(flux) # Normalize spectrum to avoid numerical issues # print 'Scale flux by', round(np.median(flux),2) galaxy = flux # use the native units # pPXF wants the spectral resolution in units of wavelength FWHM_gal = wave / specresolution ### Set up stellar templates #miles_dir = resource_filename('ppxf', '/miles_models/') #miles_dir = resource_filename('ppxf', '/emiles_padova_chabrier/') if miles_dir is None: miles_dir = resource_filename('ppxf', '/miles_padova_chabrier/') #path4libcall = miles_dir + 'Mun1.30*.fits' #path4libcall = miles_dir + 'Ech1.30*.fits' path4libcall = miles_dir + 'Mch1.30*.fits' miles = lib.miles(path4libcall, velscale, FWHM_gal, wave_gal=wave) ### Stuff for 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.01 # Desired regularization error ### Now the emission lines! Only include lines in fit region. if 'goodpixels' in kwargs: gal_lam = wave[newgoodpix] # Also, log rebinning the spectrum change which pixels are 'goodpixels' newnewgoodpix = np.searchsorted(np.exp(logLam), gal_lam, side='left') uqnewnewgoodpix = np.unique(newnewgoodpix) if uqnewnewgoodpix[-1] == len(wave): uqnewnewgoodpix = uqnewnewgoodpix[:-1] kwargs['goodpixels'] = uqnewnewgoodpix else: gal_lam = wave def FWHM_func(wave): # passed to generate emission line templates return wave / specresolution gas_templates, gas_names, line_wave = util.emission_lines_mask( miles.log_lam_temp, gal_lam, FWHM_func, tie_balmer=tie_balmer, limit_doublets=limit_doublets) # How many gas components do we have? balmerlines = [ll for ll in gas_names if ll[0] == 'H'] numbalm = len(balmerlines) numforbid = len(gas_names) - numbalm # Stack all templates templates = np.column_stack([stars_templates, gas_templates]) # other needed quantities dv = c * (miles.log_lam_temp[0] - logLam[0]) ### use the following line if not transforming to z=0 first # vel = c * np.log(1 + zgal) # eq.(8) of Cappellari (2017) vel = 0. # We already transformed to the restframe! start = [vel, 25.] # (km/s), starting guess for [V, sigma] ### Set up combination of templates n_temps = stars_templates.shape[1] n_balmer = 1 if tie_balmer else numbalm # Number of Balmer lines included in the fit n_forbidden = numforbid # Number of other lines included in the fit # Assign component=0 to the stellar templates, component=1 to the Balmer # emission lines templates, and component=2 to the forbidden lines. # component = [0]*n_temps + [1]*n_balmer + [2]*n_forbidden component = [0] * n_temps + [1] * (n_balmer + n_forbidden ) # tie the gas lines together 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 kinematic components if len(gas_names) > 0: moments = [2, 2] # fix the gas kinematic components to one another start = [[vel, 50.], start] # Adopt different gas/stars starting values else: moments = [2] # only stars to be fit start = [vel, 50.] # If the Balmer lines are tied one should allow for gas reddening. # The gas_reddening can be different from the stellar one, if both are fitted. gas_reddening = 0 if tie_balmer else None if degree_add is None: degree_add = -1 t = time.time() ppfit = ppxf.ppxf(templates, galaxy, noise, velscale, start, plot=False, moments=moments, degree=degree_add, vsyst=dv, lam=np.exp(logLam), clean=False, regul=1. / regul_err, reg_dim=reg_dim, component=component, gas_component=gas_component, gas_names=gas_names, gas_reddening=gas_reddening, mdegree=degree_mult, **kwargs) print('Desired Delta Chi^2: %.4g' % np.sqrt(2 * galaxy.size)) print('Current Delta Chi^2: %.4g' % ((ppfit.chi2 - 1) * galaxy.size)) print('Elapsed time in PPXF: %.2f s' % (time.time() - t)) weights = ppfit.weights[ ~gas_component] # Exclude weights of the gas templates weights = weights.reshape(reg_dim) # Normalized return ppfit, miles, weights