def mc_errors(self, nsim=200): """ Calculate the errors using MC simulations""" errs = np.zeros((nsim, len(self.error))) for i in range(nsim): y = self.bestfit + np.random.normal(0, self.noise, len( self.galaxy)) noise = np.ones_like(self.galaxy) * self.noise sim = ppxf(self.bestfit_unbroad, y, noise, velscale, [0, self.sol[1]], goodpixels=self.goodpixels, plot=False, moments=4, degree=-1, mdegree=-1, vsyst=self.vsyst, lam=self.lam, quiet=True, bias=0.) errs[i] = sim.sol median = np.ma.median(errs, axis=0) error = 1.4826 * np.ma.median(np.ma.abs(errs - median), axis=0) # Here I am using always the maximum error between the simulated # and the values given by pPXF. self.error = np.maximum(error, self.error) return
def run(self): c = 299792.458 # speed of light in km/s meps = np.finfo('float64').eps dv = self.dv velscale = self.velscale templates = self.templates lam_gal = self.lam_gal mask = self.mask wave_mask = self.wave_mask use_mask = mask[wave_mask] flux = ((self.flux)[wave_mask]) noise = ((self.ivar**(-0.5))[wave_mask]) nonzero_finite_bool = use_mask & (noise > 0) & (np.isfinite(noise)) igoodpixels = np.zeros(len(lam_gal)).astype(int) igoodpixels[self.goodpixels] = 1 maskpixels = (igoodpixels & nonzero_finite_bool) > 0 noise[(noise <= 0) | ~np.isfinite(noise)] = meps galaxy = flux / np.median( flux[maskpixels]) # Normalize spectrum to avoid numerical issues noise = noise / np.median(flux[maskpixels]) medsn = np.median(galaxy[maskpixels] / noise[maskpixels]) # Gas emission lines are excluded from the pPXF fit using the GOODPIXELS keyword. # vel = c*np.log(1 + z) # eq.(8) of Cappellari (2017) start = [0, 200.] # (km/s), starting guess for [V, sigma] adegree = 0 mdegree = 0 pp = ppxf(templates, galaxy, noise, velscale, start, mask=maskpixels, plot=False, quiet=True, moments=2, degree=adegree, mdegree=mdegree, vsyst=dv, clean=False, lam=lam_gal) return pp, medsn
def ppxf_fit(self, spectrum, noise, verbose=False): """Run pPXF on one spaxel. Note: must run prepstellarfit() first! Arguments: spectrum, noise (1D array): Observed spectrum and variance array verbose (bool): If 'True', make diagnostic plots Returns: params (float 1D array): Best fit velocity, velocity dispersion, velocity error, velocity dispersion error """ # Make sure spectrum and noise arrays are the right length if spectrum.size != self.wvl_cropped.size: raise ValueError('Size of spectrum array %s does not match size of wvl array %s' % (spectrum.size, self.wvl_cropped.size)) if noise.size != self.wvl_cropped.size: raise ValueError('Size of noise array %s does not match size of wvl array %s' % (noise.size, self.wvl_cropped.size)) # Prep the observed spectrum galspec = ndimage.gaussian_filter1d(spectrum, self.sigma) galaxy, logLam1, velscale = util.log_rebin(self.lamRange1, galspec) galaxy = galaxy/np.median(galaxy) # Shift the template to fit the starting wavelength of the galaxy spectrum c = 299792.458 dv = (self.logLam2[0] - logLam1[0])*c # km/s goodPixels = util.determine_goodpixels(logLam1, self.lamRange2, 0) # Here the actual fit starts. The best fit is plotted on the screen start = [0., 200.] # (km/s), starting guess for [V, sigma] if verbose: plot=True else: plot=False pp = ppxf(self.templates, galaxy, np.sqrt(noise), velscale, start, goodpixels=goodPixels, plot=plot, moments=2, degree=6, vsyst=dv, clean=False, quiet=True) if verbose: plt.show() print("Formal errors:") print(" dV dsigma") print("".join("%8.2g" % f for f in pp.error*np.sqrt(pp.chi2))) print('Best-fitting redshift z:', (self.z + 1)*(1 + pp.sol[0]/c) - 1) return np.asarray([pp.sol[0], pp.sol[1], pp.error[0]*np.sqrt(pp.chi2), pp.error[1]*np.sqrt(pp.chi2), pp.chi2]), np.exp(logLam1), pp.bestfit, galaxy, noise
def mc_errors(self, nsim=200): """ Calculate the errors using MC simulations""" errs = np.zeros((nsim, len(self.error))) for i in range(nsim): y = self.bestfit + np.random.normal(0, self.noise, len(self.galaxy)) noise = np.ones_like(self.galaxy) * self.noise sim = ppxf(self.bestfit_unbroad, y, noise, velscale, [0, self.sol[1]], goodpixels=self.goodpixels, plot=False, moments=4, degree=-1, mdegree=-1, vsyst=self.vsyst, lam=self.lam, quiet=True, bias=0.) errs[i] = sim.sol median = np.ma.median(errs, axis=0) error = 1.4826 * np.ma.median(np.ma.abs(errs - median), axis=0) # Here I am using always the maximum error between the simulated # and the values given by pPXF. self.error = np.maximum(error, self.error) return
def spectra_distance(star, template, mask, degree): # Make ppxf fit without any velocity shift and with no sigma broadening. # Only the polynomials are fitted. velscale = 1. # Arbitrary number: we work in pixels units start = [0, 1.] # Negligible sigma: OK with >2016 Fourier pPXF code noise = np.ones_like(star) pp = ppxf(template, star, noise, velscale, start, moments=2, degree=degree, linear=True, mask=mask) resid = star - pp.bestfit dist = np.percentile(np.abs(resid[mask]), 95.45) / np.mean( star[mask]) * 50 # 2sigma/2 in % return dist, pp
def fit_single_spectrum(lam_gal, flux_gal, templates, velscale, start, velscale_ratio=1, noise=None, goodpixels=None, dv=0, add_poly_deg=4, smooth=False, smooth_sigma_pix=None, clean=False, plot=False): """ Fit a single spectrum with PPXF :param lam_gal: :param flux_gal: :param templates: :param velscale: :param start: :param velscale_ratio: :param noise: :param goodpixels: :param dv: :param add_poly_deg: :return: """ # Smooth the spectrum to the resolution of the EMILES templates if requested # The smoothing length should be provided by the user if smooth: flux_gal = ppxf_util.gaussian_filter1d(flux_gal, smooth_sigma_pix) # Log rebin the spectrum to the given velocity scale flux_rebin_gal, log_lam_gal, velscale = ppxf_util.log_rebin( [lam_gal[0], lam_gal[-1]], flux_gal, velscale=velscale) # Generate a default goodpixels vector if goodpixels is None: goodpixels = np.arange(len(flux_rebin_gal)) # Normalize the spectrum to avoid rounding error norm = np.nanmedian(flux_rebin_gal) flux_rebin_gal /= np.nanmedian(flux_rebin_gal) # Generate a noise spectrum if noise is None. Use 15% of the flux if noise is None: noise = 0.15 * np.abs(flux_rebin_gal) noise[np.isnan(noise) | (noise == 0)] = 1.0 # Remove NaNs and infs from goodpixels and change them to 0 in the spectrum nonfinite_pix = ~np.isfinite(flux_rebin_gal) flux_rebin_gal[nonfinite_pix] = 0.0 for i in range(len(nonfinite_pix)): if nonfinite_pix[i]: test = np.where(goodpixels == i)[0] if len(test) > 0: goodpixels = np.delete(goodpixels, test[0]) # Require at least 50% of the original pixels to do a fit if np.float(len(goodpixels)) / np.float(len(log_lam_gal)) > 0.25: # Run ppxf pp = ppxf.ppxf(templates, flux_rebin_gal, noise, velscale, start, plot=False, moments=2, degree=add_poly_deg, vsyst=dv, goodpixels=goodpixels, velscale_ratio=velscale_ratio, clean=clean) vel = pp.sol[0] disp = pp.sol[1] chi2 = pp.chi2 temp_weights = pp.weights gal = pp.galaxy bestfit = pp.bestfit stellar = pp.bestfit - pp.apoly apoly = pp.apoly residual = pp.galaxy - pp.bestfit if plot: pp.plot() fig = plt.gcf() return vel, disp, chi2, temp_weights, gal, bestfit, stellar, apoly, residual, norm, fig else: return vel, disp, chi2, temp_weights, gal, bestfit, stellar, apoly, residual, norm else: return 'Not enough pixels to fit.'
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
def ppxf_example_kinematics_sauron(): ppxf_dir = path.dirname(path.realpath(ppxf_package.__file__)) # Read a galaxy spectrum and define the wavelength range # cube_id = 468 cube_file = "data/cubes/cube_" + str(cube_id) + ".fits" hdu = fits.open(cube_file) gal_lin = hdu[0].data h1 = hdu[0].header 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") mask = (cube_x_data > 5000) & (cube_x_data < 6000) cube_y_data = cube_y_data[mask] lamRange1 = h1['CRVAL1'] + np.array( [0., np.abs(h1['CD1_1']) * (h1['NAXIS1'] - 1)]) FWHM_gal = 2.51 # SAURON has an instrumental resolution FWHM of 4.2A. # If the galaxy is at significant redshift, one should bring the galaxy # spectrum roughly to the rest-frame wavelength, before calling pPXF # (See Sec2.4 of Cappellari 2017). In practice there is no # need to modify the spectrum in any way, given that a red shift # corresponds to a linear shift of the log-rebinned spectrum. # One just needs to compute the wavelength range in the rest-frame # and adjust the instrumental resolution of the galaxy observations. # This is done with the following three commented lines: # # z = 1.23 # Initial estimate of the galaxy redshift # lamRange1 = lamRange1/(1+z) # Compute approximate restframe wavelength range # FWHM_gal = FWHM_gal/(1+z) # Adjust resolution in Angstrom galaxy, logLam1, velscale = util.log_rebin(lamRange1, cube_y_data) galaxy = galaxy / np.median( galaxy) # Normalize spectrum to avoid numerical issues noise = np.full_like(galaxy, 0.0047) # Assume constant noise per pixel here print(galaxy) # Read the list of filenames from the Single Stellar Population library # by Vazdekis (2010, MNRAS, 404, 1639) http://miles.iac.es/. A subset # of the library is included for this example with permission vazdekis = glob.glob(ppxf_dir + '/miles_models/Mun1.30Z*.fits') FWHM_tem = 2.51 # Vazdekis+10 spectra have a constant resolution FWHM of 2.51A. velscale_ratio = 2 # adopts 2x higher spectral sampling for templates than for galaxy # Extract the wavelength range and logarithmically rebin one spectrum # to a velocity scale 2x smaller than the SAURON galaxy spectrum, to determine # the size needed for the array which will contain the template spectra. # hdu = fits.open(vazdekis[0]) ssp = hdu[0].data h2 = hdu[0].header lamRange2 = h2['CRVAL1'] + np.array( [0., h2['CDELT1'] * (h2['NAXIS1'] - 1)]) sspNew, logLam2, velscale_temp = util.log_rebin(lamRange2, ssp, velscale=velscale / velscale_ratio) templates = np.empty((sspNew.size, len(vazdekis))) # Convolve the whole Vazdekis library of spectral templates # with the quadratic difference between the SAURON and the # Vazdekis instrumental resolution. Logarithmically rebin # and store each template as a column in the array TEMPLATES. # Quadratic sigma difference in pixels Vazdekis --> SAURON # The formula below is rigorously valid if the shapes of the # instrumental spectral profiles are well approximated by Gaussians. # FWHM_dif = np.sqrt(FWHM_gal**2 - FWHM_tem**2) sigma = FWHM_dif / 2.355 / h2['CDELT1'] # Sigma difference in pixels for j, file in enumerate(vazdekis): hdu = fits.open(file) ssp = hdu[0].data ssp = ndimage.gaussian_filter1d(ssp, sigma) sspNew, logLam2, velscale_temp = util.log_rebin(lamRange2, ssp, velscale=velscale / velscale_ratio) templates[:, j] = sspNew / np.median(sspNew) # Normalizes 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 (see above). # c = 299792.458 if velscale_ratio > 1: dv = (np.mean(logLam2[:velscale_ratio]) - logLam1[0]) * c # km/s else: dv = (logLam2[0] - logLam1[0]) * c # km/s z = 0.0015 # Initial redshift estimate of the galaxy goodPixels = util.determine_goodpixels(logLam1, lamRange2, z) # Here the actual fit starts. The best fit is plotted on the screen. # Gas emission lines are excluded from the pPXF fit using the GOODPIXELS keyword. # vel = c * np.log(1 + z) # eq.(8) of Cappellari (2017) start = [vel, 200.] # (km/s), starting guess for [V, sigma] t = clock() pp = ppxf(templates, galaxy, noise, velscale, start, goodpixels=goodPixels, plot=True, moments=4, degree=4, vsyst=dv, velscale_ratio=velscale_ratio) print("Formal errors:") print(" dV dsigma dh3 dh4") print("".join("%8.2g" % f for f in pp.error * np.sqrt(pp.chi2))) print('Elapsed time in pPXF: %.2f s' % (clock() - t))
def apply_pPXF(spectra_file, fwhm_instrument, redshift, noise, vel_ratio=1, template_dir=None, fwhm_templates=None, templates=None, logLam2=None): """ Function to fit galaxy spectra using pPXF. If provided with the template directory and resolution, this function will run the degrade_templates() function in order to match the resolution of the observation and the templates. Else, if the degraded templates are already provided, this function will just use the templates provided to fit the observed galaxy spectra. :param spectra_file: A string describing the path to the SimSpin spectra file. :param fwhm_instrument: A float describing the resolution of the observed spectra. :param redshift: A float describing the redshift, z, of the observed galaxy. :param noise: A float describing the level of noise within the observed spectra. :param vel_ratio: A float describing the sampling rate of the templates relative to the observed spectra. Default value is 1. If wishing to degrade templates to the appropriate resolution, :param template_dir: A string describing the path to the directory in which the template files are located. Default is None. :param fwhm_templates: A float describing the resolution of the template spectra. Default is None. If you have already degraded the templates to the appropriate resolution for a previous fit, you can provide these variables directly to the function to avoid recalculating the comupationally expensive degradation and rebinning: :param templates: A matrix containing the rebinned and degraded templates. Default is None. :param logLam2: An array describing the wavelength labels of the templates. Default is None. """ hdu = fits.open(spectra_file) dim = hdu[0].data.shape mid = np.array([round(dim[1] / 2), round(dim[2] / 2)]) gal_lin = hdu[0].data # pulling in the spectra for each pixel h1 = hdu[0].header lamRange1 = h1['CRVAL3'] + (np.array([-h1['CRPIX3'], h1['CRPIX3']]) * h1['CDELT3']) # wavelength range FWHM_gal = fwhm_instrument # SAMI has an instrumental resolution FWHM of 2.65A. z = redshift # Initial estimate of the galaxy redshift lamRange1 = lamRange1 / ( 1 + z) # Compute approximate restframe wavelength range FWHM_gal = FWHM_gal / (1 + z) # Adjust resolution in Angstrom galaxy, logLam1, velscale = util.log_rebin(lamRange1, gal_lin[:, mid[0], mid[1]]) if not templates or not logLam2: assert isinstance(template_dir, str) and isinstance( fwhm_templates, float ), 'Please provide the path to the template directory and template resolution' templates, logLam2 = degrade_templates(template_dir, fwhm_templates, FWHM_observation=FWHM_gal, velscale_obs=velscale, vel_ratio=vel_ratio) velocity = np.empty([dim[1], dim[2]]) dispersion = np.empty([dim[1], dim[2]]) for x in range(0, dim[1]): for y in range(0, dim[2]): if all(np.isnan(gal_lin[:, x, y])): velocity[x, y] = None dispersion[x, y] = None else: galaxy, logLam1, velscale = util.log_rebin( lamRange1, gal_lin[:, x, y]) galaxy = galaxy / np.median( galaxy) # Normalize spectrum to avoid numerical issues noise = np.full_like( galaxy, noise) # Assume constant noise per pixel here if velscale_ratio > 1: dv = (np.mean(logLam2[:velscale_ratio]) - logLam1[0]) * c # km/s else: dv = (logLam2[0] - logLam1[0]) * c # km/s start = [100, 200.] # (km/s), starting guess for [V, sigma] pp = ppxf.ppxf(templates, galaxy, noise, velscale, start, plot=False, moments=2, quiet=True, degree=4, vsyst=dv, velscale_ratio=vel_ratio) velocity[x, y] = pp.sol[0] dispersion[x, y] = pp.sol[1] return velocity, dispersion
def ppxf_example_sky_and_symmetric_losvd(): ppxf_dir = path.dirname(path.realpath(ppxf_package.__file__)) # Solar metallicity, Age=12.59 Gyr hdu = fits.open( ppxf_dir + '/miles_models/Mun1.30Zp0.00T12.5893_iPp0.00_baseFe_linear_FWHM_2.51.fits' ) ssp = hdu[0].data h = hdu[0].header lamRange = h['CRVAL1'] + np.array([0., h['CDELT1'] * (h['NAXIS1'] - 1)]) velscale = 70. # km/s star, logLam, velscale = util.log_rebin(lamRange, ssp, velscale=velscale) star /= np.mean(star) # Adopted input parameters ================================================= vel = 200. / velscale # Velocity of 1st spectrum in pixels (2nd has -vel) sigma = 300. / velscale # Dispersion of both spectra in pixels h3 = 0.1 # h3 of 1st spectrum (2nd has -h3) h4 = 0.1 sn = 40. moments = 4 deg = 4 vshift = 10 # Adopted systemic velocity in pixels vsyst = vshift * velscale # Adopted systemic velocity in km/s # Generate input Sky ======================================================= # For illustration, the sky is modelled as two Gaussian emission lines n = star.size x = np.arange(n) sky1 = np.exp(-0.5 * (x - 1000)**2 / 100) sky2 = np.exp(-0.5 * (x - 2000)**2 / 100) # Generate input LOSVD ===================================================== dx = int(abs(vel) + 5 * sigma) v = np.linspace(-dx, dx, 2 * dx + 1) w = (v - vel) / sigma w2 = w**2 gauss = np.exp(-0.5 * w2) gauss /= np.sum(gauss) h3poly = w * (2 * w2 - 3) / np.sqrt(3) h4poly = (w2 * (4 * w2 - 12) + 3) / np.sqrt(24) losvd = gauss * (1 + h3 * h3poly + h4 * h4poly) # Generate first synthetic spectrum ======================================== # The template is convolved with the LOSVD x = np.linspace(-1, 1, n) galaxy1 = signal.fftconvolve(star, losvd, mode="same") galaxy1 = np.roll(galaxy1, vshift) # Mimic nonzero systemic velocity galaxy1 *= legendre.legval( x, np.append(1, np.random.uniform(-0.1, 0.1, deg - 1))) # Multiplicative polynomials galaxy1 += legendre.legval(x, np.random.uniform(-0.1, 0.1, deg)) # Additive polynomials galaxy1 += sky1 + 2 * sky2 # Add two sky lines galaxy1 = np.random.normal(galaxy1, 1 / sn) # Add noise # Generate symmetric synthetic spectrum ==================================== # The same template is convolved with a reversed LOSVD # and different polynomials and sky lines are included galaxy2 = signal.fftconvolve(star, np.flip(losvd, 0), mode="same") galaxy2 = np.roll(galaxy2, vshift) # Mimic nonzero systemic velocity galaxy2 *= legendre.legval( x, np.append(1, np.random.uniform(-0.1, 0.1, deg - 1))) # Multiplicative polynomials galaxy2 += legendre.legval(x, np.random.uniform(-0.1, 0.1, deg)) # Additive polynomials galaxy2 += 2 * sky1 + sky2 # Add two sky lines galaxy2 = np.random.normal(galaxy2, 1 / sn) # Add noise # Load spectral templates ================================================== vazdekis = glob.glob(ppxf_dir + '/miles_models/Mun1.30Z*.fits') templates = np.empty((n, len(vazdekis))) for j, file in enumerate(vazdekis): hdu = fits.open(file) ssp = hdu[0].data sspNew, logLam2, velscale = util.log_rebin(lamRange, ssp, velscale=velscale) templates[:, j] = sspNew / np.median(sspNew) # Normalize templates # Do the fit =============================================================== # Input both galaxy spectra simultaneously to pPXF galaxy = np.column_stack([galaxy1, galaxy2]) # Use two sky templates for each galaxy spectrum sky = np.column_stack([sky1, sky2]) # Randomized starting guess vel0 = vel + np.random.uniform(-1, 1) sigma0 = sigma * np.random.uniform(0.8, 1.2) start = np.array([vel0, sigma0]) * velscale # Convert to km/s goodpixels = np.arange(50, n - 50) print( "\nThe input values are: Vel=%0.0f, sigma=%0.0f, h3=%0.1f, h4=%0.1f\n" % (vel * velscale, sigma * velscale, h3, h4)) t = clock() pp = ppxf(templates, galaxy, np.full_like(galaxy, 1 / sn), velscale, start, goodpixels=goodpixels, plot=1, moments=moments, vsyst=vsyst, mdegree=deg, degree=deg, sky=sky) print('Elapsed time in pPXF: %.2f s' % (clock() - t)) plt.pause(1)
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()
#Randomize numpy seeds. Otherwise all processes will give exact same values. #This seed depends on exact system time np.random.seed(seed=microsecond) normal = np.random.normal(size=noise.shape) * noise galaxy = gal + normal goodPixels = np.arange(len(galaxy)) em_lam = np.array([4363, 5007, 4861, 6548, 6562, 6584, 6717, 6731]) goodPixels = [x for x in range(len(lam)) if ~((lam[x] < (em_lam + 10)) & (lam[x] > (em_lam - 10))).all()] goodPixels = np.array(goodPixels) degrees = [10,10] degrees = minimize_leg(galname, templates, galaxy, noise, velscale, start, goodPixels, dv, velscale_ratio, iterations, RunMC, find_min = True) ppxf_obj = ppxf.ppxf(templates, galaxy, noise, velscale, start, goodpixels=goodPixels, plot=False, moments=4, degree=degrees[0],vsyst=dv, velscale_ratio = velscale_ratio, mdegree=degrees[1], clean=False) redshift = ppxf_obj.sol[0] vel_disp = ppxf_obj.sol[1] vel_disp_err = ppxf_obj.error[1]*np.sqrt(ppxf_obj.chi2) print("Minimization took %s minutes to complete" %int(round((time.time() - stime) / 60.))) ppxf_obj.plot()
def cal_veldis(self, temp_spec=None, lib_path=None, temp_array=None, informat='text', temp_num=None, sig_ins=None, rand_temp=False, fwhm_temp=None, doplot=True, verbose=True, moments=4, plot=True, degree=None, mask_reg=None, quiet=False, show_weight=False, clean=False): """ This function calculates velocity dispersion using 'ppxf' method. """ """First Setup some parameters """ if temp_spec is None: self.temp_spec = self.gen_rebinned_temp(lib_path=lib_path, temp_array=temp_array, informat=informat, temp_num=temp_num, sig_ins=sig_ins, rand_temp=rand_temp, fwhm_temp=fwhm_temp, doplot=doplot, verbose=verbose) else: self.temp_spec = temp_spec if mask_reg is not None: self.mask_region = self.masking(pixel_range=mask_reg) if degree is None: deg = np.arange(4, 6) else: deg = np.arange(degree[0], degree[1]) """Setup the containers to store velocity dispersion and error values """ vel_dis = np.zeros(len(deg)) error = np.zeros(len(deg)) best_fit = [] """good pixels are the pixels which have been used in the fit""" good_pixels = [] """Do the velocity dispersion calculation """ for i, d in enumerate(deg): print('\ndegree : %d' % d) if mask_reg is None: pp = ppxf(self.temp_spec, self.flux_rebinned, self.noise_rebinned, self.v, self.start, moments=moments, plot=plot, vsyst=self.vsyst, degree=d, quiet=quiet, clean=clean, lam=np.exp(self.wav_rebinned)) else: pp = ppxf(self.temp_spec, self.flux_rebinned, self.noise_rebinned, self.v, self.start, moments=moments, plot=plot, vsyst=self.vsyst, degree=d, mask=self.mask_region, quiet=quiet, lam=np.exp(self.wav_rebinned), clean=clean) vel_dis[i] = pp.sol[1] error[i] = pp.error[1] best_fit.append(pp.bestfit) good_pixels.append(pp.goodpixels) if plot: plt.figure() if show_weight: [print('%d, %f'%(i,w)) for i,w in enumerate(pp.weights)\ if w>10] self.vel_dis = vel_dis self.error = error self.deg = deg self.best_fit = best_fit self.goodpixels = good_pixels
def stellarfit(self, plot=True): """ Fit stellar continuum of integrated spectra, return stellar kinematics, and subtract continuum/absorption features. Args: plot (bool): if 'True', make plots Returns: kinematics (array): [vel, veldisp, vel_err, veldisp_err, wvlarray, fitspectrum, obsspectrum, obsspectrum_err] """ print('Preparing templates for stellar kinematics fit...') # Define path to pPXF directory ppxf_dir = path.dirname(path.realpath(ppxf_package.__file__)) print(ppxf_dir) # Define spectrum spectrum = self.totalspec[self.goodwvl] noise = self.totalvar[self.goodwvl] wvl = self.wvl_zcorr[self.goodwvl] print(spectrum.shape, noise.shape, wvl.shape) # Define wavelength range lamRange1 = [wvl[0], wvl[-1]] fwhm_gal = 2.4 / (1 + self.z) # KCWI instrumental FWHM of ~2.4A # Rebin spectrum into log scale to get initial velocity scale galaxy, logLam1, velscale = util.log_rebin(lamRange1, spectrum) # Read the list of filenames from template library vazdekis = glob.glob( ppxf_dir + '/miles_models/Mun1.30*.fits') # From E-MILES SSP library #vazdekis = glob.glob(ppxf_dir + '/miles_stellar/s*.fits') # From MILES stellar library fwhm_tem = 2.51 # Vazdekis+10 spectra have a constant resolution FWHM of 2.51A. # Open template spectrum in order to get the size of the template array hdu = fits.open(vazdekis[0]) ssp = hdu[0].data h2 = hdu[0].header lamRange2 = h2['CRVAL1'] + np.array( [0., h2['CDELT1'] * (h2['NAXIS1'] - 1)]) sspNew, logLam2, velscale_temp = util.log_rebin(lamRange2, ssp, velscale=velscale) templates = np.empty((sspNew.size, len(vazdekis))) # Convolve observed spectrum with quadratic difference between observed and template resolution. # (This is valid if shapes of instrumental spectral profiles are well approximated by Gaussians.) fwhm_dif = np.sqrt(np.abs(fwhm_gal**2 - fwhm_tem**2)) sigma = fwhm_dif / 2.355 / h2['CDELT1'] # Sigma difference in pixels galspec = ndimage.gaussian_filter1d(spectrum, sigma) # Now logarithmically rebin this new observed spectrum galaxy, logLam1, velscale = util.log_rebin(lamRange1, galspec, velscale=velscale) # TEST: log-rebin the error spectrum too? noise, _, _ = util.log_rebin(lamRange1, noise, velscale=velscale) # Open and normalize all the templates for j, file in enumerate(vazdekis): hdu = fits.open(file) ssp = hdu[0].data sspNew, logLam2, velscale_temp = util.log_rebin(lamRange2, ssp, velscale=velscale) templates[:, j] = sspNew / np.median(sspNew) # Normalizes templates # Prep the observed spectrum galaxy = galaxy / np.median(galaxy) print('Doing stellar kinematics fit...') print(galaxy.shape, noise.shape) # Shift the template to fit the starting wavelength of the galaxy spectrum c = 299792.458 dv = (logLam2[0] - logLam1[0]) * c # km/s goodPixels = util.determine_goodpixels(logLam1, lamRange2, 0) # Here the actual fit starts. The best fit is plotted on the screen start = [0., 200.] # (km/s), starting guess for [V, sigma] pp = ppxf(templates, galaxy, np.sqrt(noise), velscale, start, goodpixels=goodPixels, plot=plot, moments=2, degree=6, vsyst=dv, clean=False, quiet=True) if plot: plt.show() print('Chi2:', pp.chi2) print('Best-fitting redshift z:', (self.z + 1) * (1 + pp.sol[0] / c) - 1) print('Final solution:', pp.sol) print("Errors:", pp.error * np.sqrt(pp.chi2)) if plot: plt.figure(figsize=(9, 3)) lines = np.array([ 3726.03, 3728.82, 3970.08, 4101.76, 4340.47, 4363.21, 4861.33, 4958.92, 5006.84, 6300.30, 6548.03, 6583.41, 6562.80, 6716.47, 6730.85 ]) for line in lines: if line < np.exp(logLam1)[-1]: plt.axvspan(line - 10., line + 10., color='gray', alpha=0.25) plt.fill_between(np.exp(logLam1), galaxy - np.sqrt(noise), galaxy + np.sqrt(noise), color='r', alpha=0.8) plt.plot(np.exp(logLam1), pp.bestfit, 'k-') plt.xlabel(r'$\lambda (\AA)$', fontsize=14) plt.ylabel(r'Normalized flux', fontsize=14) plt.ylim(-0.05, 2.0) plt.savefig('figures/' + self.galaxyname + '/' + 'intspec.png', bbox_inches='tight') plt.show() # Subtract stellar contribution from spectrum print('Normalizing data by best-fit stellar template...') self.spectrum_norm = galaxy - pp.bestfit * np.median(galaxy) self.kinematics_wvl = np.exp(logLam1) np.save('output/' + self.galaxyname + '/intspec_norm', self.spectrum_norm) np.save('output/' + self.galaxyname + '/intspec_wvl', self.kinematics_wvl) # Plot image for testing if plot: # Plot example spectrum plt.figure(figsize=(9, 3)) lines = np.array([ 3726.03, 3728.82, 3970.08, 4101.76, 4340.47, 4363.21, 4861.33, 4958.92, 5006.84, 6300.30, 6548.03, 6583.41, 6562.80, 6716.47, 6730.85 ]) for line in lines: if line < np.exp(logLam1)[-1]: plt.axvspan(line - 10., line + 10., color='gray', alpha=0.25) plt.fill_between(np.exp(logLam1), self.spectrum_norm - np.sqrt(noise), self.spectrum_norm + np.sqrt(noise), color='r', alpha=0.8) plt.plot(np.exp(logLam1), self.spectrum_norm, 'k-') plt.xlabel(r'$\lambda (\AA)$', fontsize=14) plt.ylabel(r'Normalized flux', fontsize=14) plt.xlim(3500, 5100) plt.savefig('figures/' + self.galaxyname + '/' + 'intspec_norm.png', bbox_inches='tight') plt.show() return np.asarray([ pp.sol[0], pp.sol[1], pp.error[0] * np.sqrt(pp.chi2), pp.error[1] * np.sqrt(pp.chi2) ]), np.exp(logLam1), pp.bestfit, galaxy, noise
def ppxf_single(object_id, z_init, lambda_spec, galaxy_lin, error_lin, cars_model, emsub_specfile=None, plotfile=None, outfile=None, outfile_spec=None, mpoly=None, apoly=None, reflines=None): # Speed of light c = 299792.458 #h_spec = spec_hdu[0].header #Ang_air = h_spec['CRVAL3'] + np.arange(0,h_spec['CDELT3']*(h_spec['NAXIS3']),h_spec['CDELT3']) #s = 10**4/Ang_air #n = 1 + 0.00008336624212083 + 0.02408926869968 / (130.1065924522 - s**2) + 0.000159740894897 / (38.92568793293 - s**2) #lambda_spec = Ang_air*n #wave = h_spec['CRVAL3'] + np.arange(0,h_spec['CDELT3']*(h_spec['NAXIS3']),h_spec['CDELT3']) #lambda_spec = vactoair(wave) #lambda_spec = h_spec['CRVAL3'] + np.arange(0,h_spec['CDELT3']*(h_spec['NAXIS3']),h_spec['CDELT3']) # Crop to finite use = (np.isfinite(galaxy_lin) & (galaxy_lin > 0.0)) # Making a "use" vector use_indices = np.arange(galaxy_lin.shape[0])[use] galaxy_lin = (galaxy_lin[use_indices.min():(use_indices.max() + 1)])[2:-3] error_lin = (error_lin[use_indices.min():(use_indices.max() + 1)])[2:-3] lambda_spec = (lambda_spec[use_indices.min():(use_indices.max() + 1)])[2:-3] lamRange_gal = np.array([np.min(lambda_spec), np.max(lambda_spec)]) # New resolution estimate = 0.9 \AA (sigma), converting from sigma to FWHM FWHM_gal = 2.355 * (np.max(lambda_spec) - np.min(lambda_spec)) / len(lambda_spec) print('FWHM', FWHM_gal) lamRange_gal = lamRange_gal / ( 1 + float(z_init)) # Compute approximate restframe wavelength range FWHM_gal = FWHM_gal / (1 + float(z_init)) # Adjust resolution in Angstrom sigma_gal = FWHM_gal / (2.3548 * 4500.0) * c # at ~4500 \AA # log rebinning for the fits galaxy, logLam_gal, velscale = util.log_rebin(np.around(lamRange_gal, decimals=3), galaxy_lin, flux=True) noise, logLam_noise, velscale = util.log_rebin(np.around(lamRange_gal,decimals=3), error_lin, \ velscale=velscale, flux=True) # correcting for infinite or zero noise noise[np.logical_or((noise == 0.0), np.isnan(noise))] = 1.0 galaxy[np.logical_or((galaxy < 0.0), np.isnan(galaxy))] = 0.0 if galaxy.shape != noise.shape: galaxy = galaxy[:-1] logLam_gal = logLam_gal[:-1] # Define lamRange_temp and logLam_temp #lamRange_temp, logLam_temp = setup_spectral_library_conroy(velscale[0], FWHM_gal) # Construct a set of Gaussian emission line templates. # Estimate the wavelength fitted range in the rest frame. # gas_templates, line_names, line_wave = util.emission_lines( logLam_gal, lamRange_gal, FWHM_gal) goodpixels = np.arange(galaxy.shape[0]) wave = np.exp(logLam_gal) # crop very red end (only affects a very small subsample) # goodpixels = goodpixels[wave <= 5300] # exclude lines at the edges (where things can go very very wrong :)) include_lines = np.where((line_wave > (wave.min() + 10.0)) & (line_wave < (wave.max() - 10.0))) if line_wave[include_lines[0]].shape[0] < line_wave.shape[0]: line_wave = line_wave[include_lines[0]] line_names = line_names[include_lines[0]] gas_templates = gas_templates[:, include_lines[0]] #reg_dim = stars_templates.shape[1:] reg_dim = gas_templates.shape[1:] templates = gas_templates #np.hstack([gas_templates, gas_templates]) dv = 0 #(logLam_temp[0]-logLam_gal[0])*c # km/s vel = 0 #np.log(z_init + 1)*c # Initial estimate of the galaxy velocity in km/s #z = np.exp(vel/c) - 1 # Relation between velocity and redshift in pPXF # Here the actual fit starts. The best fit is plotted on the screen. # Gas emission lines are excluded from the pPXF fit using the GOODPIXELS keyword. # t = clock() nNLines = gas_templates.shape[1] component = [0] * nNLines start_gas = [vel, 100.] start = start_gas # adopt the same starting value for both gas (BLs and NLs) and stars moments = [ 2 ] # fit (V,sig,h3,h4) for the stars and (V,sig) for the gas' broad and narrow components fixed = None ## Additive polynomial degree if apoly is None: degree = int( np.ceil((lamRange_gal[1] - lamRange_gal[0]) / (100.0 * (1. + float(z_init))))) else: degree = int(apoly) if mpoly is None: mdegree = 3 else: mdegree = int(mpoly) # Trying: sigmas must be kinematically decoupled # #Trying: velocities must be kinematically decoupled #A_ineq = [[0,0,2,0,-1,0]] #b_ineq = [0] bounds_gas = [[-1000, 1000], [0, 1000]] bounds = bounds_gas pp = ppxf.ppxf(templates, galaxy, noise, velscale, start, fixed=fixed, plot=False, moments=moments, mdegree=mdegree, degree=degree, vsyst=dv, reg_dim=reg_dim, goodpixels=goodpixels, bounds=bounds) #component=component, # redshift_to_newtonian: # return (v-astropy.constants.c.to('km/s').value*redshift)/(1.0+redshift) # "v" from above is actually the converted velocity which is # (np.exp(v_out/c) - 1)*c v_gas = pp.sol[0] ev_gas = pp.error[0] conv_vel_gas = (np.exp(pp.sol[0] / c) - 1) * c vel_gas = (1 + z_init) * (conv_vel_gas - c * z_init) sigma_gas = pp.sol[1] #first # is template, second # is the moment esigma_gas = pp.error[1] #*np.sqrt(pp.chi2) zfit_gas = (z_init + 1) * (1 + pp.sol[0] / c) - 1 zfit_stars = z_init ezfit_gas = (z_init + 1) * pp.error[0] * np.sqrt(pp.chi2) / c if plotfile is not None: # ### All of the rest of this plots and outputs the results of the fit ### Feel free to comment anything out at will # maskedgalaxy = np.copy(galaxy) lowSN = np.where(noise > (0.9 * np.max(noise))) maskedgalaxy[lowSN] = np.nan wave = np.exp(logLam_gal) * (1. + z_init) / (1. + zfit_stars) fig = plt.figure(figsize=(12, 7)) ax1 = fig.add_subplot(211) # plotting smoothed spectrum smoothing_fact = 3 ax1.plot(wave, convolve(maskedgalaxy, Box1DKernel(smoothing_fact)), color='Gray', linewidth=0.5) ax1.plot(wave[goodpixels], convolve(maskedgalaxy[goodpixels], Box1DKernel(smoothing_fact)), 'k', linewidth=1.) label = "Best fit template from high res Conroy SSPs + emission lines at z={0:.3f}".format( zfit_stars) # overplot stellar templates alone ax1.plot(wave, pp.bestfit, 'r', linewidth=1.0, alpha=0.75, label=label) ax1.set_ylabel('Flux') ax1.legend(loc='upper right', fontsize=10) ax1.set_title(object_id) xmin, xmax = ax1.get_xlim() ax2 = fig.add_subplot(413, sharex=ax1, sharey=ax1) # plotting emission lines if included in the fit gas = pp.matrix[:, -nNLines:].dot(pp.weights[-nNLines:]) ax2.plot(wave, gas, 'b', linewidth=2,\ label = '$\sigma_{gas}$'+'={0:.0f}$\pm${1:.0f} km/s'.format(sigma_gas, esigma_gas)+', $V_{gas}$'+'={0:.0f}$\pm${1:.0f} km/s'.format(v_gas, ev_gas)) # overplot emission lines alone cars_model = (cars_model[use_indices.min():(use_indices.max() + 1)])[2:-3] ax2.plot(np.array(lambda_spec)/(1+z_init), cars_model, color='orange', linewidth=1,\ label = 'CARS Model') #(lambda_spec[use_indices.min():(use_indices.max()+1)])[2:-3] stars = pp.bestfit - gas #if (ymax > 3.0*np.median(stars)): ymax = 3.0*np.median(stars) #if (ymin < -0.5): ymin = -0.5 ax2.set_ylabel('Best Fits') ax2.legend(loc='upper left', fontsize=10) # Plotting the residuals ax3 = fig.add_subplot(817, sharex=ax1) ax3.plot(wave[goodpixels], (convolve(maskedgalaxy, Box1DKernel(smoothing_fact)) - pp.bestfit)[goodpixels], 'k', label='Fit Residuals') #ax3.set_yticks([-0.5,0,0.5]) ax3.set_ylabel('Residuals') ax4 = fig.add_subplot(818, sharex=ax1) ax4.plot(wave, noise, 'k', label='Flux Error') ax4.set_ylabel('Noise') ax4.set_xlabel('Rest Frame Wavelength [$\AA$]') #ax4.set_yticks(np.arange(0,0.5,0.1)) '''if reflines is not None: for i,w,label in zip(range(len(reflines)),reflines['wave'],reflines['label']): if ((w > xmin) and (w < xmax)): # ax1.text(w,ymin+(ymax-ymin)*(0.03+0.08*(i % 2)),'$\mathrm{'+label+'}$',fontsize=10,\ # horizontalalignment='center',\ # bbox=dict(boxstyle='round', facecolor='white', alpha=0.5)) print(label.decode("utf-8")) ax1.text(w,ymin+(ymax-ymin)*(0.03+0.08*(i % 2)),'$\mathrm{'+label.decode("utf-8")+'}$',fontsize=10,\ horizontalalignment='center',\ bbox=dict(boxstyle='round', facecolor='white', alpha=0.5)) ax1.plot([w,w],[ymin,ymax],':k',alpha=0.5)''' fig.subplots_adjust(hspace=0.05) plt.setp([a.get_xticklabels() for a in fig.axes[:-1]], visible=False) #print('Saving figure to {0}'.format(plotfile)) ax1.set_xlim([6450, 6750]) ax2.set_xlim([6450, 6750]) #ymin, ymax = ax1.get_ylim([]) plt.savefig(plotfile, dpi=150) plt.close() return v_gas, vel_gas, sigma_gas, pp.chi2 # print('# id z_stars ez_stars sigma_stars esigma_stars z_gas ez_gas sigma_gas esigma_gas SN_median SN_rf_4000 SN_obs_8030 chi2dof') print('{0:d} {1:.6f} {2:.6f} {3:.1f} {4:.1f} {5:.6f} {6:.6f} {7:.1f} {8:.1f} {9:.1f} {10:.1f} {11:.1f} {12:.2f}\n'.format(\ object_id,zfit_stars,ezfit_stars, sigma_stars,esigma_stars,\ zfit_gas, ezfit_gas, sigma_gas, esigma_gas, SN_median, SN_rf_4000, SN_obs_8030, pp.chi2)) ## This prints the fit parameters to an open file object called outfile if outfile is not None: outfile.write('{0:d} {1:d} {2:.6f} {3:.6f} {4:.1f} {5:.1f} {6:.6f} {7:.6f} {8:.1f} {9:.1f} {10:.1f} {11:.1f} {12:.1f} {13:.2f}\n'.format(\ object_id, row2D, zfit_stars, ezfit_stars, sigma_stars,esigma_stars,\ zfit_gas, ezfit_gas, sigma_gas, esigma_gas, SN_median, SN_rf_4000, SN_obs_8030, pp.chi2)) ## This outputs the spectrum and best-fit model to an open file object called outfile_spec if outfile_spec is not None: outfile_spec.write( '# l f ef f_stars f_gas f_model_tot used_in_fit add_poly mult_poly\n' ) for i in np.arange(wave.shape[0]): isgood = 0 if goodpixels[goodpixels == i].shape[0] == 1: isgood = 1 outfile_spec.write( '{0:0.4f} {1:0.4f} {2:0.4f} {3:0.4f} {4:0.4f} {5:0.4f} {6} {7:0.4f} {8:0.4f}\n' .format(wave[i], galaxy[i], noise[i], stars[i], gas[i], pp.bestfit[i], isgood, add_polynomial[i], mult_polynomial[i])) # ## This outputs the best-fit emission-subtracted spectrum to a fits file # ## but I've commented it out since you are unlikely to need this! # if emsub_specfile is not None: # wave = np.exp(logLam_gal)*(1.+z_init) # if include_gas: # emsub = galaxy - gas # else: # emsub = galaxy # col1 = fits.Column(name='wavelength', format='E', array=wave) # col2 = fits.Column(name='flux',format='E',array=galaxy) # col3 = fits.Column(name='error',format='E',array=noise) # col4 = fits.Column(name='flux_emsub',format='E',array=emsub) # cols = fits.ColDefs([col1,col2,col3,col4]) # tbhdu = fits.BinTableHDU.from_columns(cols) # # delete old file if it exists # if os.path.isfile(emsub_specfile): os.remove(emsub_specfile) # tbhdu.writeto(emsub_specfile) # return zfit_stars, ezfit_stars, sigma_stars, esigma_stars, zfit_gas, ezfit_gas, \ # sigma_gas, esigma_gas, SN_median, SN_rf_4000, SN_obs_8030, pp.chi2 return zfit_stars, sigma_stars, sigma_blr, wave, pp.bestfit
def rv_fit(self, guesses, niter=10000, line_sigma=3,\ n_CPU=-1, line_significants=5, RV_guess_var=0.): """ The module runs the radial velocity fit using `ppxf`_ and the :ref:`Monte Carlo`. Args: guesses : :func:`numpy.array` The initial guesses for the the radial velocity fit guesses in the form [RV,sepctral_dispersion] Kwargs: niter : :obj:`int` (optional, default: 10000) number of iterations to bootstrap the spectrum line_sigma: :obj:`int` (optional, default: 3): sigma for the RV clipping for the individual lines n_CPU : :obj:`float` (optional, default: -1) Setting the number of CPUs used for the parallelization. If set to -1 all available system resources are used. Maximum number of CPUs is the number of spectral lines the fit is performed to. line_significants: :obj:`int` (optional, default: 5) The sigma-level for the spectral line to be above the continuum in order to be considered *valid* RV_guess_var : :obj:`float` (optional, default: 0) The maximum variation the RV guess will be varied using a uniform distribution. """ self.logger.info('Starting the RV fitting') self.logger.info('Settings: niter=' + str(niter)\ + '; sigma RV for excluding lines: ' + str(line_sigma)) start_time = time.time() if self.loglevel == "DEBUG": n_CPU = 1 if n_CPU == -1: n_CPU = cpu_count() self.logger.info('Max number of cores: '\ + str(n_CPU)) ### transfer the spectrum to log-space log_spec_f, logspec_lambda, velscale_spec\ = ppxf_util.log_rebin([self.spec_lambda[0],\ self.spec_lambda[-1]], self.spec_f) log_template_f, log_template_lambda, velscale_template\ = ppxf_util.log_rebin([self.spec_lambda[0],\ self.spec_lambda[-1]], self.template_f) log_spec_err, log_spec_err_lambda, velscale_spec_err\ = ppxf_util.log_rebin([self.spec_lambda[0],\ self.spec_lambda[-1]], self.spec_err) log_spec_err[~np.isfinite(log_spec_err)] = 1. # This happens if AO was used and there is a gap in the spec exponent = int(np.log10(np.nanmedian(log_spec_f)) - 4.) # Obtain larger flux numbers to make it numerically stable factor = float(10**(exponent * (-1))) log_spec_f = log_spec_f * factor log_template_f = log_template_f * factor log_spec_err = log_spec_err * factor l_fit = self.cat.loc[:, 'l_fit'].values.astype(np.float64) l_lab = self.cat.loc[:, 'l_lab'].values.astype(np.float64) line_name = self.cat.index fwhm_g, fwhm_l, fwhm_v\ = voigt_FWHM(self.cat.loc[:, 'sg_fit'].values.astype(np.float64),\ self.cat.loc[:, 'sl_fit'].values.astype(np.float64)) used = np.where(self.cat.loc[:, 'used'] == 'f') l_fit = np.delete(l_fit, used) l_lab = np.delete(l_lab, used) line_name = np.delete(line_name, used) fwhm_g = np.delete(fwhm_g, used) fwhm_l = np.delete(fwhm_l, used) fwhm_v = np.delete(fwhm_v, used) v = np.zeros_like(l_lab) ev = np.zeros_like(l_lab) for i, line in enumerate(l_lab): self.logger.info('Started line ' + line_name[i]) if self.cat.loc[line_name[i], 'significance'] > line_significants: mask = np.zeros(len(log_template_f), dtype=bool) ind_min = np.argmin(np.abs(log_template_lambda\ - np.log(line - 0.5 * fwhm_v[i]))) - 1 ind_max = np.argmin(np.abs(log_template_lambda\ - np.log(line + 0.5 * fwhm_v[i]))) + 1 ind_center = np.argmin(np.abs(log_template_lambda\ - np.log(line))) mask[ind_min:ind_max + 1] = True log_spec_f[~np.isfinite(log_spec_f)] = 0. pp_outliers_init = ppxf.ppxf(log_template_f, log_spec_f,\ log_spec_err, velscale_spec, guesses,\ mask=mask, degree=-1, clean=False, quiet=True,\ plot=False, fixed=[0, 1]) self.logger.info('RV guess variation line ' + line_name[i]\ + ': ' + str('{:4.2f}'.format(RV_guess_var)) + 'km/s') v[i], ev[i] = ppxf_MC(log_template_f, log_spec_f,\ log_spec_err, velscale_spec, guesses, nrand=0.5 * niter,\ goodpixels=pp_outliers_init.goodpixels, degree=-1,\ moments=2, RV_guess_var=RV_guess_var, n_CPU=n_CPU) self.logger.info('Finished line ' + line_name[i]\ + ': RV=(' + str('{:4.2f}'.format(v[i])) + '+-'\ + str('{:4.3f}'.format(ev[i])) + ')km/s') else: self.logger.info(line_name[i]\ + ': Line not significant [level ='\ + str(line_significants) + ']') ev[i] = np.nan v[i] = np.nan self.cat.loc[line_name[i], 'RV'] = v[i] self.cat.loc[line_name[i], 'eRV'] = ev[i] remaining_lines = line_clipping(self, v, line_significants,\ sigma=line_sigma) if remaining_lines.mask.all(): self.logger.error('NO USABLE LINE FOUND WITH SET PARAMETER !!') else: self.cat.loc[line_name[~remaining_lines.mask], 'used'] = 'x' l_fit = l_fit[~remaining_lines.mask] fwhm_v = fwhm_v[~remaining_lines.mask] rv_var_lines = MAD(self.cat.loc[line_name[~remaining_lines.mask],\ 'RV']) RV_guess_var_min = RV_guess_var if rv_var_lines > RV_guess_var: RV_guess_var = rv_var_lines self.logger.info('RV guess variation: min = '\ + str(RV_guess_var_min) + 'km/s; used = '\ + str('{:4.2f}'.format(RV_guess_var)) + 'km/s') mask = np.zeros(len(self.spec_lambda), dtype=bool) for i, line in enumerate(l_fit): ind_min = np.argmin(np.abs(logspec_lambda\ - np.log(line - 0.5 * fwhm_v[i]))) - 1 ind_max = np.argmin(np.abs(logspec_lambda\ - np.log(line + 0.5 * fwhm_v[i]))) + 1 ind_center = np.argmin(np.abs(logspec_lambda - np.log(line))) mask[ind_min:ind_max + 1] = True # pp_final_plot = plt.figure(str(self.spec_id)\ # + '_ppxf_fit_final', figsize=(10, 3)) if len(l_lab) > 1: pp_final_init = ppxf.ppxf(log_template_f, log_spec_f,\ log_spec_err, velscale_spec, guesses, degree=-1, clean=False,\ mask=mask, quiet=True, fixed=[0, 1]) if len(l_lab) == 1: pp_final_init = pp_outliers_init # pp_final_init.plot() # # plt.tight_layout() # plt.savefig(self.spec_id + '_ppxf_fit_final.png', dpi=600) # plt.close() self.logger.info('Started final RV fit') self.rv, self.erv = ppxf_MC(log_template_f, log_spec_f,\ log_spec_err, velscale_spec, guesses,\ nrand=niter, goodpixels=pp_final_init.goodpixels, degree=-1,\ moments=2, spec_id=self.spec_id, RV_guess_var=RV_guess_var,\ n_CPU=n_CPU) elapsed_time = time.time() - start_time self.logger.info('Used lines for RV fit: '\ + ', '.join(line_name[~remaining_lines.mask])) self.logger.info('Finished RV fitting: RV=('\ + str('{:4.2f}'.format(self.rv)) + '+-'\ + str('{:4.3f}'.format(self.erv)) + ')km/s based on '\ + str(len(l_fit)) + '/' + str(len(line_name)) + ' lines') self.logger.info('Elapsed time: '\ + time.strftime("%H:%M:%S", time.gmtime(elapsed_time)))
def ppxf_L_tot(int_spec, header, redshift, vel, dist_mod, dM_err=[0.1, 0.1]): """Take in collapsed galaxy spectra, with vel and distance modulus, to calculate L_bol based on various filter bands. \ Also returns Vega mangitudes for g,r and Johnson V bands, along with sigma (LOSVD). Parameters ---------- int_spec : float, array Collapsed spectra of the galaxy. header : Fits header object galaxy's Fits file header, containing information for the function. redshift : float Redshift of the galaxy, from recession velocity vel : float recession velocity of the galaxy, in km/s dist_mod : float Distance modulus for use in the calculation of Lbol and magnitudes dM_err : list, optional Error in distance modulus, by default [0.1,0.1] Returns ------- dict lum_bol_g, lum_bol_g_u, lum_bol_g_l, mag_g, mag_r, mag_v, fit_sigma """ # Read a galaxy spectrum and define the wavelength range lamRange1 = header['CRVAL3'] + np.array([ 0., header['CD3_3'] * (header['NAXIS3'] - 1) ]) #IMPORTANTE: EL RANGO DE LAMBDAS ESTA EN ESCALA LOGARITMICA #Transformamos los pixeles en lambdas: #lam=np.linspace(lamRange1[0],lamRange[1],len(gal_lin[0,:])) FWHM_gal = 2.81 # SAURON has an instrumental resolution FWHM of 4.2A. # If the galaxy is at a significant redshift (z > 0.03), one would need to apply # a large velocity shift in PPXF to match the template to the galaxy spectrum. # This would require a large initial value for the velocity (V > 1e4 km/s) # in the input parameter START = [V,sig]. This can cause PPXF to stop! # The solution consists of bringing the galaxy spectrum roughly to the # rest-frame wavelength, before calling PPXF. In practice there is no # need to modify the spectrum before the usual LOG_REBIN, given that a # red shift corresponds to a linear shift of the log-rebinned spectrum. # One just needs to compute the wavelength range in the rest-frame # and adjust the instrumental resolution of the galaxy observations. # This is done with the following three commented lines: # # z = 1.23 # Initial estimate of the galaxy redshift # lamRange1 = lamRange1/(1+z) # Compute approximate restframe wavelength range # FWHM_gal = FWHM_gal/(1+z) # Adjust resolution in Angstrom galaxy, logLam1, velscale = util.log_rebin(lamRange1, int_spec) cond = np.exp(logLam1) <= 6900 # Getting the apparent magnitude of the galaxy in the g, r and V bands mag_g, Flux_g = library(np.exp(logLam1[cond]), galaxy[cond] * 1.0e-20, filt="SDSS", band="g") mag_r, Flux_r = library(np.exp(logLam1[cond]), galaxy[cond] * 1.0e-20, filt="SDSS", band="r") mag_v, Flux_v = library(np.exp(logLam1[cond]), galaxy[cond] * 1.0e-20, filt="GROUND_JOHNSON", band="V") # Converting to absolute magnitude M_g = mag_g - dist_mod # M_g_u = mag_g - (dist_mod + dM_err[0]) # upper distance M_g M_g_l = mag_g - (dist_mod - dM_err[1]) # lower distance M_g # M_r = mag_r - dist_mod # M_r_u = mag_r - (dist_mod + dM_err[0]) # upper distance M_g # M_r_l = mag_r - (dist_mod - dM_err[1]) # M_v = mag_v - dist_mod # M_v_u = mag_v - (dist_mod + dM_err[0]) # upper distance M_g # M_v_l = mag_v - (dist_mod - dM_err[1]) galaxy = galaxy / np.median( galaxy) # Normalize spectrum to avoid numerical issues noise = np.full_like(galaxy, redshift) # Assume constant noise per pixel here # Read the list of filenames from the Single Stellar Population library # by Vazdekis (2010, MNRAS, 404, 1639) http://miles.iac.es/. A subset # of the library is included for this example with permission vazdekis = glob.glob( 'emiles/Ekb1.30*') # PUT HERE THE DIRECTORY TO EMILES_STARS FWHM_tem = 2.51 # Vazdekis+10 spectra have a constant resolution FWHM of 2.51A. velscale_ratio = 2 # adopts 2x higher spectral sampling for templates than for galaxy miles_path = path.expanduser("emiles/Ekb1.30Z*.fits") miles = lib.miles(miles_path, velscale, FWHM_tem) stars_templates = miles.templates.reshape(miles.templates.shape[0], -1) reg_dim = miles.templates.shape[1:] # Extract the wavelength range and logarithmically rebin one spectrum # to a velocity scale 2x smaller than the SAURON galaxy spectrum, to determine # the size needed for the array which will contain the template spectra. # hdu = fits.open(vazdekis[0]) ssp = hdu[0].data h2 = hdu[0].header lamRange2 = h2['CRVAL1'] + np.array( [0., h2['CDELT1'] * (h2['NAXIS1'] - 1)]) sspNew, logLam2, velscale_temp = util.log_rebin(lamRange2, ssp, velscale=velscale / velscale_ratio) templates = np.empty( (sspNew.size, len(vazdekis))) # PUT HERE THE DIRECTORY TO MILES_STARS # Extract the wavelength range and logarithmically rebin one spectrum # to a velocity scale 2x smaller than the SAURON galaxy spectrum, to determine # the size needed for the array which will contain the template spectra. # hdu = fits.open(vazdekis[0]) ssp = hdu[0].data h2 = hdu[0].header lamRange2 = h2['CRVAL1'] + np.array( [0., h2['CDELT1'] * (h2['NAXIS1'] - 1)]) sspNew, logLam2, velscale_temp = util.log_rebin(lamRange2, ssp, velscale=velscale / velscale_ratio) templates = np.empty((sspNew.size, len(vazdekis))) # Convolve the whole Vazdekis library of spectral templates # with the quadratic difference between the SAURON and the # Vazdekis instrumental resolution. Logarithmically rebin # and store each template as a column in the array TEMPLATES. # Quadratic sigma difference in pixels Vazdekis --> SAURON # The formula below is rigorously valid if the shapes of the # instrumental spectral profiles are well approximated by Gaussians. # FWHM_dif = np.sqrt(FWHM_gal**2 - FWHM_tem**2) sigma = FWHM_dif / 2.355 / h2['CDELT1'] # Sigma difference in pixels for j, vazd in enumerate(vazdekis): hdu = fits.open(vazd) ssp = hdu[0].data ssp = ndimage.gaussian_filter1d(ssp, sigma) sspNew, logLam2, velscale_temp = util.log_rebin(lamRange2, ssp, velscale=velscale / velscale_ratio) templates[:, j] = sspNew / np.median(sspNew) # Normalizes 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 (see above). # c = 299792.458 # in km/s #c = 299792458.0 # speed of light dv = (logLam2[0] - logLam1[0]) * c # km/s z = np.exp(vel / c) - 1 # Relation between velocity and redshift in pPXF cond = np.exp(logLam1) <= 7810 #6900 logLam1 = logLam1[cond] galaxy = galaxy[cond] noise = noise[cond] goodPixels = util.determine_goodpixels(logLam1, lamRange2, z) # Here the actual fit starts. The best fit is plotted on the screen. # Gas emission lines are excluded from the pPXF fit using the GOODPIXELS keyword. # start = [vel, 200.] # (km/s), starting guess for [V, sigma] t = clock() galaxy[np.where(np.isfinite(galaxy) == False)] = 0.0 noise[np.where(np.isfinite(noise) == False)] = 0.0 templates[np.where(np.isfinite(templates) == False)] = 0.0 pp = ppxf(templates, galaxy, noise * 0.0 + 1.0, velscale, start, goodpixels=goodPixels, plot=True, moments=4, mdegree=15, vsyst=dv, velscale_ratio=velscale_ratio) weights = pp.weights normalized_weights = weights / np.sum(weights) # Use miles_utils to get metallicity weights = pp.weights weights = weights.reshape(reg_dim) / weights.sum() # Normalized miles.mean_age_metal(weights) fit_sigma = pp.sol optimal_template = np.zeros(templates.shape[0]) for j in range(0, templates.shape[1]): optimal_template = optimal_template + templates[:, j] * normalized_weights[ j] print("Formal errors:") print(" dV dsigma dh3 dh4") print("".join("%8.2g" % f for f in pp.error * np.sqrt(pp.chi2))) print('Elapsed time in PPXF: %.2f s' % (clock() - t)) # Pass the optimal template to get the bolometric correction for the g-band BC_g = transmission(np.exp(logLam2), optimal_template, band="g") # BC_r = transmission(np.exp(logLam2), optimal_template, band="r") # BC_v = transmission(np.exp(logLam2), optimal_template, band="V") # Obtaining the bolometric correction of the Sun BC_sun_g, M_sun_g = library(0, 0, filt="SDSS", band="g", get_sun='Y') # BC_sun_r, M_sun_r = library(0,0,filt="SDSS", band="r",get_sun='Y') # BC_sun_v, M_sun_v = library(0,0,filt="GROUND_JOHNSON", band="V",get_sun='Y') # Getting the bolometric luminosity (in solar luminosity) for the g-band lum_bol_g = 10.0**(-0.4 * (M_g - M_sun_g)) * 10.0**(-0.4 * (BC_g - BC_sun_g)) lum_bol_g_u = 10.0**(-0.4 * (M_g_u - M_sun_g)) * 10.0**( -0.4 * (BC_g - BC_sun_g)) # upper dM L lum_bol_g_l = 10.0**(-0.4 * (M_g_l - M_sun_g)) * 10.0**( -0.4 * (BC_g - BC_sun_g)) # lower dM L # lum_bol_r = 10.0**(-0.4*(M_r-M_sun_r)) * 10.0**(-0.4*(BC_r-BC_sun_r)) # lum_bol_r_u = 10.0**(-0.4*(M_r_u-M_sun_r)) * 10.0**(-0.4*(BC_r-BC_sun_r)) # upper dM L # lum_bol_r_l = 10.0**(-0.4*(M_r_l-M_sun_r)) * 10.0**(-0.4*(BC_r-BC_sun_r)) # lower dM L # lum_bol_v = 10.0**(-0.4*(M_v-M_sun_v)) * 10.0**(-0.4*(BC_v-BC_sun_v)) # lum_bol_v_u = 10.0**(-0.4*(M_v_u-M_sun_v)) * 10.0**(-0.4*(BC_v-BC_sun_v)) # upper dM L # lum_bol_v_l = 10.0**(-0.4*(M_v_l-M_sun_v)) * 10.0**(-0.4*(BC_v-BC_sun_v)) # lower dM L #lum_bol_r, [lum_bol_r_u, lum_bol_r_l], lum_bol_v, [lum_bol_v_u, lum_bol_v_l], result_dict = { "Lbol": lum_bol_g, "Lbol_err_up": lum_bol_g_u, "Lbol_err_lo": lum_bol_g_l, "mag_g": mag_g, "mag_r": mag_r, "mag_v": mag_v, "sigma": fit_sigma[1] } return result_dict
def ppxf_example_simulation(): ppxf_dir = path.dirname(path.realpath(ppxf_package.__file__)) hdu = fits.open( ppxf_dir + '/miles_models/Mun1.30Zp0.00T12.5893_iPp0.00_baseFe_linear_FWHM_2.51.fits' ) # Solar metallicitly, Age=12.59 Gyr ssp = hdu[0].data h = hdu[0].header lamRange = h['CRVAL1'] + np.array([0., h['CDELT1'] * (h['NAXIS1'] - 1)]) c = 299792.458 # speed of light in km/s velscale = c * h['CDELT1'] / max( lamRange) # Do not degrade original velocity sampling star, logLam, velscale = util.log_rebin(lamRange, ssp, velscale=velscale) # The finite sampling of the observed spectrum is modeled in detail: # the galaxy spectrum is obtained by oversampling the actual observed spectrum # to a high resolution. This represent the true spectrum, which is later resampled # to lower resolution to simulate the observations on the CCD. Similarly, the # convolution with a well-sampled LOSVD is done on the high-resolution spectrum, # and later resampled to the observed resolution before fitting with PPXF. factor = 10 # Oversampling integer factor for an accurate convolution starNew = ndimage.interpolation.zoom( star, factor, order=3) # This is the underlying spectrum, known at high resolution star = rebin( starNew, factor ) # Make sure that the observed spectrum is the integral over the pixels h3 = 0.1 # Adopted G-H parameters of the LOSVD h4 = 0.1 sn = 30. # Adopted S/N of the Monte Carlo simulation m = 300 # Number of realizations of the simulation moments = 4 velV = np.random.rand(m) # velocity in *pixels* [=V(km/s)/velScale] sigmaV = np.linspace( 0.5, 4, m) # Range of sigma in *pixels* [=sigma(km/s)/velScale] result = np.zeros((m, moments)) # This will store the results t = clock() for j, (vel, sigma) in enumerate(zip(velV, sigmaV)): dx = int( abs(vel) + 5 * sigma) # Sample the Gaussian and GH at least to vel+5*sigma x = np.linspace( -dx, dx, 2 * dx * factor + 1) # Evaluate the Gaussian using steps of 1/factor pixels. w = (x - vel) / sigma w2 = w**2 gauss = np.exp(-0.5 * w2) gauss /= np.sum(gauss) # Normalized total(gauss)=1 h3poly = w * (2. * w2 - 3.) / np.sqrt(3.) # H3(y) h4poly = (w2 * (4. * w2 - 12.) + 3.) / np.sqrt(24.) # H4(y) losvd = gauss * (1. + h3 * h3poly + h4 * h4poly) galaxy = signal.fftconvolve( starNew, losvd, mode="same") # Convolve the oversampled spectrum galaxy = rebin( galaxy, factor) # Integrate spectrum into original spectral pixels noise = galaxy / sn # 1sigma error spectrum galaxy = np.random.normal(galaxy, noise) # Add noise to the galaxy spectrum start = np.array([ vel + np.random.uniform(-1, 1), sigma * np.random.uniform(0.8, 1.2) ]) * velscale # Convert to km/s pp = ppxf(star, galaxy, noise, velscale, start, goodpixels=np.arange(dx, galaxy.size - dx), plot=False, moments=moments, bias=0.2) result[j, :] = pp.sol print('Calculation time: %.2f s' % (clock() - t)) plt.clf() plt.subplot(221) plt.plot(sigmaV * velscale, result[:, 0] - velV * velscale, '+k') plt.axhline(0, color='r') plt.axvline(velscale, linestyle='dashed') plt.axvline(2 * velscale, linestyle='dashed') plt.ylim(-20, 20) plt.xlabel(r'$\sigma_{\rm in}\ (km\ s^{-1})$') plt.ylabel(r'$V - V_{\rm in}\ (km\ s^{-1})$') plt.text(2.05 * velscale, -15, r'2$\times$velscale') plt.subplot(222) plt.plot(sigmaV * velscale, result[:, 1] - sigmaV * velscale, '+k') plt.axhline(0, color='r') plt.axvline(velscale, linestyle='dashed') plt.axvline(2 * velscale, linestyle='dashed') plt.ylim(-20, 20) plt.xlabel(r'$\sigma_{in}\ (km\ s^{-1})$') plt.ylabel(r'$\sigma - \sigma_{\rm in}\ (km\ s^{-1})$') plt.text(2.05 * velscale, -15, r'2$\times$velscale') plt.subplot(223) plt.plot(sigmaV * velscale, result[:, 2], '+k') plt.axhline(h3, color='r') plt.axhline(0, linestyle='dotted', color='limegreen') plt.axvline(velscale, linestyle='dashed') plt.axvline(2 * velscale, linestyle='dashed') plt.ylim(-0.2 + h3, 0.2 + h3) plt.xlabel(r'$\sigma_{\rm in}\ (km\ s^{-1})$') plt.ylabel('$h_3$') plt.text(2.05 * velscale, h3 - 0.15, r'2$\times$velscale') plt.subplot(224) plt.plot(sigmaV * velscale, result[:, 3], '+k') plt.axhline(h4, color='r') plt.axhline(0, linestyle='dotted', color='limegreen') plt.axvline(velscale, linestyle='dashed') plt.axvline(2 * velscale, linestyle='dashed') plt.ylim(-0.2 + h4, 0.2 + h4) plt.xlabel(r'$\sigma_{\rm in}\ (km\ s^{-1})$') plt.ylabel('$h_4$') plt.text(2.05 * velscale, h4 - 0.15, r'2$\times$velscale') plt.tight_layout() plt.pause(1)
def ppxf_example_kinematics_sdss(): ppxf_dir = path.dirname(path.realpath(ppxf_package.__file__)) # Read SDSS DR12 galaxy spectrum taken from here http://dr12.sdss3.org/ # The spectrum is *already* log rebinned by the SDSS DR12 # pipeline and log_rebin should not be used in this case. file = ppxf_dir + '/spectra/NGC4636_SDSS_DR12.fits' hdu = fits.open(file) t = hdu['COADD'].data z = 0.003129 # SDSS redshift estimate # Only use the wavelength range in common between galaxy and stellar library. mask = (t['loglam'] > np.log10(3540)) & (t['loglam'] < np.log10(7409)) flux = t['flux'][mask] galaxy = flux / np.median( flux) # Normalize spectrum to avoid numerical issues loglam_gal = t['loglam'][mask] lam_gal = 10**loglam_gal noise = np.full_like(galaxy, 0.0166) # Assume constant noise per pixel here c = 299792.458 # speed of light in km/s frac = lam_gal[1] / lam_gal[0] # Constant lambda fraction per pixel dlam_gal = (frac - 1) * lam_gal # Size of every pixel in Angstrom wdisp = t['wdisp'][ mask] # Intrinsic dispersion of every pixel, in pixels units fwhm_gal = 2.355 * wdisp * dlam_gal # Resolution FWHM of every pixel, in Angstroms velscale = np.log( frac) * c # Velocity scale in km/s per pixel (eq.8 of Cappellari 2017) # If the galaxy is at significant redshift, one should bring the galaxy # spectrum roughly to the rest-frame wavelength, before calling pPXF # (See Sec.2.4 of Cappellari 2017). In practice there is no # need to modify the spectrum in any way, given that a red shift # corresponds to a linear shift of the log-rebinned spectrum. # One just needs to compute the wavelength range in the rest-frame # and adjust the instrumental resolution of the galaxy observations. # This is done with the following three commented lines: # # lam_gal = lam_gal/(1+z) # Compute approximate restframe wavelength # fwhm_gal = fwhm_gal/(1+z) # Adjust resolution in Angstrom # Read the list of filenames from the Single Stellar Population library # by Vazdekis (2010, MNRAS, 404, 1639) http://miles.iac.es/. A subset # of the library is included for this example with permission vazdekis = glob.glob(ppxf_dir + '/miles_models/Mun1.30Z*.fits') fwhm_tem = 2.51 # Vazdekis+10 spectra have a constant resolution FWHM of 2.51A. # Extract the wavelength range and logarithmically rebin one spectrum # to the same velocity scale of the SDSS galaxy spectrum, to determine # the size needed for the array which will contain the template spectra. # hdu = fits.open(vazdekis[0]) ssp = hdu[0].data h2 = hdu[0].header lam_temp = h2['CRVAL1'] + h2['CDELT1'] * np.arange(h2['NAXIS1']) lamRange_temp = [np.min(lam_temp), np.max(lam_temp)] sspNew = util.log_rebin(lamRange_temp, ssp, velscale=velscale)[0] templates = np.empty((sspNew.size, len(vazdekis))) # Interpolates the galaxy spectral resolution at the location of every pixel # of the templates. Outside the range of the galaxy spectrum the resolution # will be extrapolated, but this is irrelevant as those pixels cannot be # used in the fit anyway. fwhm_gal = np.interp(lam_temp, lam_gal, fwhm_gal) # Convolve the whole Vazdekis library of spectral templates # with the quadratic difference between the SDSS and the # Vazdekis instrumental resolution. Logarithmically rebin # and store each template as a column in the array TEMPLATES. # Quadratic sigma difference in pixels Vazdekis --> SDSS # The formula below is rigorously valid if the shapes of the # instrumental spectral profiles are well approximated by Gaussians. # # In the line below, the fwhm_dif is set to zero when fwhm_gal < fwhm_tem. # In principle it should never happen and a higher resolution template should be used. # fwhm_dif = np.sqrt((fwhm_gal**2 - fwhm_tem**2).clip(0)) sigma = fwhm_dif / 2.355 / h2['CDELT1'] # Sigma difference in pixels for j, fname in enumerate(vazdekis): hdu = fits.open(fname) ssp = hdu[0].data ssp = util.gaussian_filter1d( ssp, sigma) # perform convolution with variable sigma sspNew = util.log_rebin(lamRange_temp, ssp, velscale=velscale)[0] templates[:, j] = sspNew / np.median(sspNew) # Normalizes 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 (see above). # c = 299792.458 dv = np.log(lam_temp[0] / lam_gal[0]) * c # km/s goodpixels = util.determine_goodpixels(np.log(lam_gal), lamRange_temp, z) # Here the actual fit starts. The best fit is plotted on the screen. # Gas emission lines are excluded from the pPXF fit using the GOODPIXELS keyword. # vel = c * np.log(1 + z) # eq.(8) of Cappellari (2017) start = [vel, 200.] # (km/s), starting guess for [V, sigma] t = clock() pp = ppxf(templates, galaxy, noise, velscale, start, goodpixels=goodpixels, plot=True, moments=4, degree=12, vsyst=dv, clean=False, lam=lam_gal) print("Formal errors:") print(" dV dsigma dh3 dh4") print("".join("%8.2g" % f for f in pp.error * np.sqrt(pp.chi2))) print('Elapsed time in PPXF: %.2f s' % (clock() - t))
def apply_ppxf(self, galnoise, FWHM_gal, FWHM_tem, z, moments, plot=False, directory_plots=''): """This is the method that uses the PPxF package and applies it to the trimmed data. It saves the results in the class variable 'results_ppxf'. It also has the possibility of saving the plots that it produces. Parameters ---------- galnoise : float Noise in the galaxy's data. FWHM_gal : float Full width-half maximum of the galaxy. FWHM_tem : float Full width-half maximum of the templates. z : float The galaxy's redshift. moments : int The number of moments that should be used with PPxF. plot : boolean, optional Boolean to determine if the plots should be saved. directory_plots : String String that represents the absolute path where the plots are to be saved at. """ # make sure that the list of results is empty self.results_ppxf = [] temp_header, ssp = self.templates[self.templates_names[0]] CRVAL1h3 = temp_header["CRVAL1"] CRPIX1h3 = temp_header["CRPIX1"] CDELT1h3 = temp_header["CDELT1"] NAXIS1h3 = temp_header["NAXIS1"] # calculate the wavelenght range from the templates lamRange2 = (CRVAL1h3 - CRPIX1h3 * CDELT1h3) + np.array( [0., (CDELT1h3 * NAXIS1h3 - 1.0)]) # speed of light c = 299792.458 # Initial estimate of the galaxy's velocity in km/s vel = c * z # I NEED TO UNDERSTAND WHAT THE FOLLOWING TWO LINES MEAN goodPixels = np.arange( 1150, 1820) #(1150,1820) #[1150,1820] or perhaps an np.arange(1150,1820) start = [vel, 120.] # The current pixel being worked on number_of_pixel = 1 ### Start nested loop for i in np.arange(self.cube.trimmed_data.shape[1]): for j in np.arange(self.cube.trimmed_data.shape[2]): # the spectrum corresponding to these pixel coordinates spec = self.cube.trimmed_data[:, i, j] galaxy, logLam1, velscale = util.log_rebin( self.cube.wavelength_range, spec) galaxy = galaxy / np.median(galaxy) # Normalizing the spectrum noise = galaxy * 0 + galnoise # Assuming constant noise per pixel here sspNew, logLam2, velscale = util.log_rebin(lamRange2, ssp, velscale=velscale) templates = np.zeros((sspNew.shape[0], self.ntemplates)) FWHM_dif = np.sqrt((FWHM_gal**2 - FWHM_tem**2)) sigma = FWHM_dif / 2.355 / CDELT1h3 #list_of_templates_names = list(templates_dictionary.keys()) k = 0 while k < self.ntemplates: h3, ssp = self.templates[self.templates_names[k]] ssp = util.gaussian_filter1d( ssp, sigma) # perform convolution with variable sspNew = util.log_rebin(lamRange2, ssp, velscale=velscale)[0] templates[:, k] = sspNew / np.median(sspNew) k += 1 dv = (logLam2[0] - logLam1[0]) * c # km/s pp = ppxf(templates, galaxy, noise, velscale, start, goodpixels=goodPixels, plot=False, moments=moments, degree=4, mdegree=0, vsyst=dv) self.results_ppxf.append(pp) if plot: plt.rcParams['figure.figsize'] = (32, 20) myplt = pp.plot() plt.savefig(directory_plots + str(number_of_pixel) + ".pdf") plt.clf() number_of_pixel += 1
def fit(self, wavelength, data, mask=None, initial_velocity=0.0, initial_sigma=150.0, fwhm_gal=2, fwhm_model=1.8, noise=0.05, plot_fit=False, quiet=False, deg=4, moments=4, **kwargs): """ Performs the pPXF fit. Parameters ---------- wavelength : numpy.ndarray Wavelength coordinates of the data. data : numpy.ndarray Input spectrum flux vector. mask : list List of masked regions, as pairs of wavelength coordinates. initial_velocity : float Initial guess for radial velocity. initial_sigma : float Initial guess for the velocity dispersion. fwhm_gal : float Full width at half maximum of a resolution element in the observed spectrum in units of pixels. fwhm_model : float The same as the above for the models. noise : float or numpy.ndarray If float it as assumed as the signal to noise ratio, and will be horizontally applied to the whole spectrum. If it is an array, it will be interpreted as individual noise values for each pixel. plot_fit : bool Plots the resulting fit. quiet : bool Prints information on the fit. deg : int Degree of polynomial function to be fit in addition to the stellar population spectrum. moments : int Number of moments in the Gauss-Hermite polynomial. A simple Gaussian would be 2. kwargs Additional keyword arguments passed directly to ppxf. Returns ------- pp pPXF output object. See Also -------- ppxf, ppxf_util """ self.mask = mask fw = (wavelength >= self.fitting_window[0]) & (wavelength < self.fitting_window[1]) lam_range1 = wavelength[fw][[0, -1]] gal_lin = copy.deepcopy(data[fw]) self.obs_flux = gal_lin galaxy, log_lam1, velscale = ppxf_util.log_rebin(lam_range1, gal_lin) # Here we use the goodpixels as the fitting window gp = np.arange(len(log_lam1)) lam1 = np.exp(log_lam1) self.obs_wavelength = lam1 if self.mask is not None: if len(self.mask) == 1: gp = gp[(lam1 < self.mask[0][0]) | (lam1 > self.mask[0][1])] else: m = np.array([(lam1 < i[0]) | (lam1 > i[1]) for i in self.mask]) gp = gp[np.sum(m, 0) == m.shape[0]] self.good_pixels = gp lam_range2 = self.base_wavelength[[0, -1]] ssp = self.base[0] ssp_new, log_lam2, velscale = ppxf_util.log_rebin(lam_range2, ssp, velscale=velscale) templates = np.empty((ssp_new.size, len(self.base))) fwhm_dif = np.sqrt(fwhm_gal ** 2 - fwhm_model ** 2) # Sigma difference in pixels sigma = fwhm_dif / 2.355 / self.base_delta for j in range(len(self.base)): ssp = self.base[j] ssp = gaussian_filter(ssp, sigma) ssp_new, log_lam2, velscale = ppxf_util.log_rebin(lam_range2, ssp, velscale=velscale) # Normalizes templates templates[:, j] = ssp_new / np.median(ssp_new) c = constants.c.value * 1.e-3 dv = (log_lam2[0] - log_lam1[0]) * c # km/s # z = np.exp(vel/c) - 1 # Here the actual fit starts. start = [initial_velocity, initial_sigma] # (km/s), starting guess for [V,sigma] # Assumes uniform noise accross the spectrum def make_noise(galaxy, noise): noise = galaxy * noise noise_mask = (~np.isfinite(noise)) | (noise <= 0.0) mean_noise = np.mean(noise[~noise_mask]) noise[noise_mask] = mean_noise return noise if isinstance(noise, float): noise = make_noise(galaxy, noise) elif isinstance(noise, np.ndarray): noise, log_lam1, velscale = ppxf_util.log_rebin(lam_range1, copy.deepcopy(noise)[fw]) self.noise = noise self.normalization_factor = np.nanmean(galaxy) galaxy = copy.deepcopy(ma.getdata(galaxy / self.normalization_factor)) noise = copy.deepcopy(ma.getdata(np.abs(noise / self.normalization_factor))) assert np.all((noise > 0) & np.isfinite(noise)), 'Invalid values encountered in noise spectrum.' if len(gp.shape) > 1: gp = gp[0] pp = ppxf.ppxf(templates, galaxy, noise, velscale, start, goodpixels=gp, moments=moments, degree=deg, vsyst=dv, quiet=quiet, **kwargs) self.solution = pp if plot_fit: self.plot_fit() return pp
def ppxf_wifis(spectrumfl, z_guess=0.004, sigma_guess=170.): ppxf_dir = path.dirname(path.realpath(ppxf_package.__file__)) # Read a galaxy spectrum and define the wavelength range # hdu = fits.open(spectrumfl) gal_lin = hdu[0].data wl_lin = hdu[1].data noise_lin = hdu[2].data #wh_fit = np.where((wl_lin >= 11600) & (wl_lin <= 13000))[0] wh_fit = np.where((wl_lin >= 11600) & (wl_lin <= 13000))[0] gal_lin = gal_lin[wh_fit] wl_lin = wl_lin[wh_fit] noise_lin = noise_lin[wh_fit] #lamRange1 = h1['CRVAL1'] + np.array([0., h1['CDELT1']*(h1['NAXIS1'] - 1)]) lamRange1 = [wl_lin[0], wl_lin[-1]] #FWHM_gal = 6.06 # SAURON has an instrumental resolution FWHM of 4.2A. #FWHM_gal = 5.08 # SAURON has an instrumental resolution FWHM of 4.2A. FWHM_gal = 4.68 # SAURON has an instrumental resolution FWHM of 4.2A. # If the galaxy is at significant redshift, one should bring the galaxy # spectrum roughly to the rest-frame wavelength, before calling pPXF # (See Sec2.4 of Cappellari 2017). In practice there is no # need to modify the spectrum in any way, given that a red shift # corresponds to a linear shift of the log-rebinned spectrum. # One just needs to compute the wavelength range in the rest-frame # and adjust the instrumental resolution of the galaxy observations. # This is done with the following three commented lines: # # z = 1.23 # Initial estimate of the galaxy redshift # lamRange1 = lamRange1/(1+z) # Compute approximate restframe wavelength range # FWHM_gal = FWHM_gal/(1+z) # Adjust resolution in Angstrom galaxy, logLam1, velscale = util.log_rebin(lamRange1, gal_lin) noise, logLam1, velscale = util.log_rebin(lamRange1, noise_lin) galaxy = galaxy / np.median( galaxy) # Normalize spectrum to avoid numerical issues noise = noise / np.median(noise) #noise = np.full_like(galaxy, 0.0047) # Assume constant noise per pixel here # Read the list of filenames from the Single Stellar Population library # by Vazdekis (2010, MNRAS, 404, 1639) http://miles.iac.es/. A subset # of the library is included for this example with permission #vazdekis = glob.glob('/home/elliot/mcmcgemini/spec/vcj_ssp/*t05.0*Zp0.0*.s100') vazdekis = glob.glob('/home/elliot/mcmcgemini/spec/vcj_ssp/*.s100') FWHM_tem = 1.63 # Vazdekis+10 spectra have a constant resolution FWHM of 2.51A. #FWHM_tem = 3 # Vazdekis+10 spectra have a constant resolution FWHM of 2.51A. velscale_ratio = 3 # adopts 2x higher spectral sampling for templates than for galaxy # Extract the wavelength range and logarithmically rebin one spectrum # to a velocity scale 2x smaller than the SAURON galaxy spectrum, to determine # the size needed for the array which will contain the template spectra. # hdu = np.loadtxt(vazdekis[0]) ssp = hdu[:, 74] modelwl = hdu[:, 0] wh_wifis = np.where((modelwl >= 8000) & (modelwl <= 13500))[0] modelwl_wifis = modelwl[wh_wifis] wl_rebin = np.linspace(modelwl_wifis[0], modelwl_wifis[-1], num=len(modelwl_wifis)) ssp_rebin = np.interp(wl_rebin, modelwl_wifis, ssp[wh_wifis]) lamRange2 = [wl_rebin[0], wl_rebin[-1]] sspNew, logLam2, velscale_temp = util.log_rebin(lamRange2, ssp_rebin, velscale=velscale / velscale_ratio) #h2 = hdu[0].header #lamRange2 = h2['CRVAL1'] + np.array([0., h2['CDELT1']*(h2['NAXIS1'] - 1)]) fullage = np.array([1.0, 3.0, 5.0, 7.0, 9.0, 11.0, 13.5]) #fullZ = np.array([-1.5, -1.0, -0.5, 0.0, 0.2]) fullZ = np.array([-0.5, 0.0, 0.2]) #templates = np.empty((sspNew.size, len(vazdekis))) templates = np.empty((sspNew.size, len(fullage), len(fullZ))) conroydict = {} for fl in vazdekis: flspl = fl.split('/')[-1] mnamespl = flspl.split('_') age = float(mnamespl[3][1:]) Zsign = mnamespl[4][1] Zval = float(mnamespl[4][2:5]) if Zsign == "m": Zval = -1.0 * Zval conroydict[(age, Zval)] = fl # Convolve the whole Vazdekis library of spectral templates # with the quadratic difference between the SAURON and the # Vazdekis instrumental resolution. Logarithmically rebin # and store each template as a column in the array TEMPLATES. # Quadratic sigma difference in pixels Vazdekis --> SAURON # The formula below is rigorously valid if the shapes of the # instrumental spectral profiles are well approximated by Gaussians. # FWHM_dif = np.sqrt(FWHM_gal**2 - FWHM_tem**2) #sigma = FWHM_dif/2.355/h2['CDELT1'] # Sigma difference in pixels sigma = FWHM_dif / 2.355 / (wl_rebin[1] - wl_rebin[0] ) # Sigma difference in pixels for j in range(len(fullage)): for k in range(len(fullZ)): #for j, file in enumerate(vazdekis): x = pd.read_csv(conroydict[(fullage[j], fullZ[k])], delim_whitespace=True, header=None) hdu = np.array(x) #hdu = np.loadtxt(conroydict[(fullage[j],fullZ[j])]) ssp = hdu[:, 73] ssp_rebin = np.interp(wl_rebin, modelwl_wifis, ssp[wh_wifis]) ssp = ndimage.gaussian_filter1d(ssp_rebin, sigma) sspNew, logLam2, velscale_temp = util.log_rebin(lamRange2, ssp, velscale=velscale / velscale_ratio) templates[:, j, k] = sspNew / np.median(sspNew) # Normalizes 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 (see above). # c = 299792.458 dv = (np.mean(logLam2[:velscale_ratio]) - logLam1[0]) * c # km/s z = z_guess # Initial redshift estimate of the galaxy goodPixels = util.determine_goodpixels(logLam1, lamRange2, z) # Here the actual fit starts. The best fit is plotted on the screen. # Gas emission lines are excluded from the pPXF fit using the GOODPIXELS keyword. # vel = c * np.log(1 + z) # eq.(8) of Cappellari (2017) start = [vel, sigma_guess] # (km/s), starting guess for [V, sigma] t = clock() pp = ppxf(templates, galaxy, noise, velscale, start, goodpixels=goodPixels, plot=True, moments=2, degree=4, vsyst=dv, velscale_ratio=velscale_ratio) print("Formal errors:") print(" dV dsigma dh3 dh4") print("".join("%8.2g" % f for f in pp.error * np.sqrt(pp.chi2))) print('Elapsed time in pPXF: %.2f s' % (clock() - t)) # If the galaxy is at significant redshift z and the wavelength has been # de-redshifted with the three lines "z = 1.23..." near the beginning of # this procedure, the best-fitting redshift is now given by the following # commented line (equation 2 of Cappellari et al. 2009, ApJ, 704, L34; # http://adsabs.harvard.edu/abs/2009ApJ...704L..34C) # # print('Best-fitting redshift z:', (z + 1)*(1 + pp.sol[0]/c) - 1) return pp
def run_ppxf(fields, w1, w2, targetSN, tempfile, logdir, redo=False, ncomp=2, **kwargs): """ New function to run pPXF. """ global velscale stars = pf.getdata(tempfile, 0) emission = pf.getdata(tempfile, 1) logLam_temp = wavelength_array(tempfile, axis=1, extension=0) ngas = len(emission) nstars = len(stars) templates = np.column_stack((stars.T, emission.T)) ########################################################################## # Set components if ncomp == 1: components = np.zeros(nstars + ngas) kwargs["component"] = components elif ncomp == 2: components = np.hstack((np.zeros(nstars), np.ones(ngas))).astype(int) kwargs["component"] = components ########################################################################## for f in fields: print "Working on Field {0}".format(f[-1]) os.chdir(os.path.join(data_dir, "combined_{0}".format(f))) outdir = os.path.join(os.getcwd(), logdir) if not os.path.exists(outdir): os.mkdir(outdir) fits = "binned_sn{0}_res2.95.fits".format(targetSN) data = pf.getdata(fits, 0) w = wavelength_array(fits, axis=1, extension=0) bins = wavelength_array(fits, axis=2, extension=0) ###################################################################### # Slice array before fitting idx = np.where(np.logical_and(w >= w1, w <= w2))[0] data = data[:,idx] w = w[idx] ###################################################################### for i,bin in enumerate(bins): output = os.path.join(outdir, "{1}_bin{2:04d}.pkl".format(targetSN, f, bin)) outroot = output.replace(".pkl", "") if os.path.exists(output) and not redo: continue print "PPXF run {0}/{1}".format(i+1, len(bins)) spec = data[i,:] signal, noise, sn = snr(spec) galaxy, logLam, vtemp = util.log_rebin([w[0], w[-1]], \ spec, velscale=velscale) dv = (logLam_temp[0]-logLam[0])*c lam = np.exp(logLam) name = "{0}_bin{1:04d}".format(f, bin) noise = np.ones_like(galaxy) * noise kwargs["lam"] = lam ################################################################### # Masking bad pixels skylines = np.array([5577,5889, 6300, 6863]) goodpixels = np.arange(len(lam)) for line in skylines: sky = np.argwhere((lam < line - 10) | (lam > line + 10)).ravel() goodpixels = np.intersect1d(goodpixels, sky) kwargs["goodpixels"] = goodpixels ################################################################### kwargs["vsyst"] = dv pp = ppxf(templates, galaxy, noise, velscale, **kwargs) title = "Field {0} Bin {1}".format(f[-1], bin) pp.name = name # Adding other things to the pp object pp.has_emission = True pp.dv = dv pp.w = np.exp(logLam) pp.velscale = velscale pp.ngas = ngas pp.ntemplates = nstars pp.templates = 0 pp.id = id pp.name = name pp.title = title ppsave(pp, outroot=outroot) ppf = ppload(outroot) ppf = pPXF(ppf, velscale) ppf.plot("{1}/{0}.png".format(name, outdir)) return
def minimize_leg(galname, templates, galaxy, noise, velscale, start, goodPixels, dv, velscale_ratio, iterations, useMC, find_min=True): #Minimizes ppxf uncertainty using legendre polynomials #find_min=True will force the program to find the minimum again value_range = range(5,23) if useMC == False: #so that you don't double up on the multiprocessing and kill the server #iterations feeds = [] for ad in value_range: for md in value_range: feeds.append([(ad, md), i, iterations, templates, galaxy, noise, velscale, start, goodPixels, dv, velscale_ratio]) P = mp.Pool(iterations) outputs = P.map(min_worker, feeds) outputs = np.array(outputs) degrees = outputs[:,0] errors = outputs[:,1] deg = degrees[np.argmin(errors)] print(' ') print('Deg is:') print(deg) print(' ') return deg else: #serial version stime = time.time() deg_used = [] errors = [] if find_min or np.any(np.isnan(degrees)): #Sample parameter space within $value_range to find minimum uncertainty minimum = 1e10 degrees = [0,0] #ad, md for ad in value_range: #degree of additive legendre polynomial for md in value_range: #degree of multiplicative legendre polynomial print('degrees of %i additive, %i multiplicative'%(ad, md)) #print((ad, md)) ppxf_obj = ppxf.ppxf(templates, galaxy, noise, velscale, start, goodpixels=goodPixels, plot=False, moments=4, degree=ad, vsyst=dv, velscale_ratio = velscale_ratio, mdegree=md, clean=False) error = ppxf_obj.error[1]*np.sqrt(ppxf_obj.chi2) deg_used.append((ad,md)) errors.append(error) #error = ppxf_obj.chi2 print('Uncertainty is ' + str(round(float(error), 2)) + ' km/s') if error < minimum: minimum = error degrees[0] = ad degrees[1] = md print('') print('Degree of Linear Legendre Polynomial is: ' + str(degrees[0])) print('') print('Degree of Multiplicative Legendre Polynomial is: ' + str(degrees[1])) print('') print('Took %s seconds to complete'%int(round(time.time() - stime))) print('') return degrees
def ppxf_example_two_components(): ppxf_dir = path.dirname(path.realpath(ppxf_package.__file__)) hdu = fits.open( ppxf_dir + '/miles_models/Mun1.30Zp0.00T12.5893_iPp0.00_baseFe_linear_FWHM_2.51.fits' ) # Solar metallicitly, Age=12.59 Gyr gal_lin = hdu[0].data h1 = hdu[0].header lamRange1 = h1['CRVAL1'] + np.array( [0., h1['CDELT1'] * (h1['NAXIS1'] - 1)]) c = 299792.458 # speed of light in km/s velscale = c * h1['CDELT1'] / max( lamRange1) # Do not degrade original velocity sampling model1, logLam1, velscale = util.log_rebin(lamRange1, gal_lin, velscale=velscale) model1 /= np.median(model1) hdu = fits.open( ppxf_dir + '/miles_models/Mun1.30Zp0.00T01.0000_iPp0.00_baseFe_linear_FWHM_2.51.fits' ) # Solar metallicitly, Age=1.00 Gyr gal_lin = hdu[0].data model2, logLam1, velscale = util.log_rebin(lamRange1, gal_lin, velscale=velscale) model2 /= np.median(model2) model = np.column_stack([model1, model2]) galaxy = np.empty_like(model) # These are the input values in spectral pixels # for the (V,sigma) of the two kinematic components # vel = np.array([0., 300.]) / velscale sigma = np.array([200., 100.]) / velscale # The synthetic galaxy model consists of the sum of two # SSP spectra with age of 1Gyr and 13Gyr respectively # with different velocity and dispersion # for j in range(len(vel)): dx = int(abs(vel[j]) + 4. * sigma[j]) # Sample the Gaussian at least to vel+4*sigma v = np.linspace(-dx, dx, 2 * dx + 1) losvd = np.exp(-0.5 * ((v - vel[j]) / sigma[j])**2) # Gaussian LOSVD losvd /= np.sum(losvd) # normalize LOSVD galaxy[:, j] = signal.fftconvolve(model[:, j], losvd, mode="same") galaxy[:, j] /= np.median(model[:, j]) galaxy = np.sum(galaxy, axis=1) sn = 100. noise = galaxy / sn galaxy = np.random.normal(galaxy, noise) # add noise to galaxy # Adopts two templates per kinematic component # templates = np.column_stack([model1, model2, model1, model2]) # With multiple stellar kinematic components a good starting velocity is essential. # Starting too far from the solution pPXF may *not* converge to the global minimum. # Giving the same starting point for both components should be generally avoided and # and is used below just to illustrate how one can swap the components using constr_kinem. # In general one should explore a grid of starting velocities as illustrated # e.g. in Sec.3.3 of Mitzkus et al. (2017 https://ui.adsabs.harvard.edu/abs/2017MNRAS.464.4789M) start = [[100, 100], [100, 100]] goodPixels = np.arange(20, 6000) t = clock() plt.clf() print("\n++++++++++++++++++++++++++++++++++++++++++++++\n" " No constraints on the kinematics\n" "----------------------------------------------") plt.subplot(211) plt.title("Two components pPXF fit") pp = ppxf(templates, galaxy, noise, velscale, start, goodpixels=goodPixels, plot=True, degree=4, moments=[2, 2], component=[0, 0, 1, 1]) print("\n++++++++++++++++++++++++++++++++++++++++++++++\n" "Force sigma(component=0) >= sigma(component=1)\n" "----------------------------------------------") # Note the swapping of the two components in the solution # with respect to the previous uncostrained fit A_ineq = [[0, -1, 0, 1]] # -sigma0 + sigma1 <= 0 b_ineq = [0] constr_kinem = {"A_ineq": A_ineq, "b_ineq": b_ineq} pp = ppxf(templates, galaxy, noise, velscale, start, goodpixels=goodPixels, degree=4, moments=[2, 2], component=[0, 0, 1, 1], constr_kinem=constr_kinem) print("\n++++++++++++++++++++++++++++++++++++++++++++++\n" " Single component pPXF fit\n" "----------------------------------------------") plt.subplot(212) plt.title("Single component pPXF fit") start = start[0] pp = ppxf(templates, galaxy, noise, velscale, start, goodpixels=goodPixels, plot=True, degree=4, moments=2) print("==============================================") print("Total elapsed time %.2f s" % (clock() - t)) plt.tight_layout() plt.pause(1)
def ppxf_fit_spec(galaxy, noise, templates, fit_range, lamRange2, logLam1, logLam2, velscale, start, sky=None, plot=True, moments=4, degree=-1, mdegree=4, goodpixels=None): """Use pPXF to fit kinematics to a spectrum. This function masks the templates so they're the appropriate lengths (a bit longer than the galaxy spectrum) then runs pPXF inputs: galaxy- a full length galaxy spectrum (i.e 4112 pixels for SWIFT) noise- noise spectrum, same length as galaxy templates- array of templates lamRange2- wavelength range of the templates, pre masking logLam1- log rebinned wavelength array for galaxy logLam2- log rebinned wavelength array for templates fit_range- range over which you want to fit the galaxy start- starting guess for V and Sigma (or h3, h4, etc if you want higher moments) sky- optionally fit a sky spectra at the same time. Set to None if you're not using outputs: pp- the ppxf class sky- the input sky spectrum, but masked. If sky=None, then this is None galaxy- the input galaxy spectrum, but masked noise- the input noise spectrum, but masked """ #We need to pad the templates for ~100 angstroms each side of the galaxy spectrum. lower_lam, upper_lam = np.array(fit_range) #print(upper_lam) #print(lamRange2) pad = 300 #make the template mask tmask = np.where((logLam2 >= np.log((lower_lam - pad))) & (logLam2 <= np.log((upper_lam + pad))))[0] #mask the wavelength range and the templates logLam2 = logLam2[tmask] templates = templates[tmask, :] #make the mask for the galaxy spectrum #mask=np.where((logLam1 >= np.log(lower_lam)) & (logLam1 <= np.log(upper_lam)))[0] #mask the galaxy, sky and variance, plus the wavelengh ranges """ logLam1=logLam1[mask] galaxy=galaxy[mask] if SKY_FLAG: sky=sky[mask, :] noise=noise[mask] """ ################################################################################# # 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 (see above). # c = 299792.458 #print("logam2[0] is {}, loglam1[0] is {}, logLam1/(1+z) is {}".format(logLam2[0], logLam1[0],logLam1[0]/(1+z))) dv = (logLam2[0] - logLam1[0]) * c # km/s vel, sigma = start z_ppxf = np.exp( vel / c) - 1 # Relation between velocity and redshift in pPXF if goodpixels is None: goodpixels = util.determine_goodpixels(logLam1, lamRange2, z_ppxf) print("#########################################################") print("Velocity shift DV is {}".format(dv)) print("The templates are shape {}".format(np.shape(templates))) print("The galaxy is shape {}".format(np.shape(galaxy))) print("#########################################################") # Here the actual fit starts. The best fit is plotted on the screen. # Gas emission lines are excluded from the pPXF fit using the GOODPIXELS keyword. # t = clock() if sky is not None: pp = ppxf(templates, galaxy, noise, velscale, start, goodpixels=goodpixels, plot=plot, moments=moments, degree=degree, mdegree=mdegree, vsyst=dv, sky=sky) else: pp = ppxf(templates, galaxy, noise, velscale, start, goodpixels=goodpixels, plot=plot, moments=moments, degree=degree, mdegree=mdegree, vsyst=dv) print("Formal errors:") print(" dV dsigma dh3 dh4") print("".join("%8.2g" % f for f in pp.error * np.sqrt(pp.chi2))) print('Elapsed time in PPXF: %.2f s' % (clock() - t)) return (pp, sky, galaxy, noise)
def ppxf_example_two_components(): ppxf_dir = path.dirname(path.realpath(ppxf_package.__file__)) hdu = fits.open( ppxf_dir + '/miles_models/Mun1.30Zp0.00T12.5893_iPp0.00_baseFe_linear_FWHM_2.51.fits' ) # Solar metallicitly, Age=12.59 Gyr gal_lin = hdu[0].data h1 = hdu[0].header lamRange1 = h1['CRVAL1'] + np.array( [0., h1['CDELT1'] * (h1['NAXIS1'] - 1)]) c = 299792.458 # speed of light in km/s velscale = c * h1['CDELT1'] / max( lamRange1) # Do not degrade original velocity sampling model1, logLam1, velscale = util.log_rebin(lamRange1, gal_lin, velscale=velscale) model1 /= np.median(model1) hdu = fits.open( ppxf_dir + '/miles_models/Mun1.30Zp0.00T01.0000_iPp0.00_baseFe_linear_FWHM_2.51.fits' ) # Solar metallicitly, Age=1.00 Gyr gal_lin = hdu[0].data model2, logLam1, velscale = util.log_rebin(lamRange1, gal_lin, velscale=velscale) model2 /= np.median(model2) model = np.column_stack([model1, model2]) galaxy = np.empty_like(model) # These are the input values in spectral pixels # for the (V,sigma) of the two kinematic components # vel = np.array([0., 300.]) / velscale sigma = np.array([200., 100.]) / velscale # The synthetic galaxy model consists of the sum of two # SSP spectra with age of 1Gyr and 13Gyr respectively # with different velocity and dispersion # for j in range(len(vel)): dx = int(abs(vel[j]) + 4. * sigma[j]) # Sample the Gaussian at least to vel+4*sigma v = np.linspace(-dx, dx, 2 * dx + 1) losvd = np.exp(-0.5 * ((v - vel[j]) / sigma[j])**2) # Gaussian LOSVD losvd /= np.sum(losvd) # normaize LOSVD galaxy[:, j] = signal.fftconvolve(model[:, j], losvd, mode="same") galaxy[:, j] /= np.median(model[:, j]) galaxy = np.sum(galaxy, axis=1) sn = 100. noise = galaxy / sn galaxy = np.random.normal(galaxy, noise) # add noise to galaxy # Adopts two templates per kinematic component # templates = np.column_stack([model1, model2, model1, model2]) # Start both kinematic components from the same guess. # With multiple stellar kinematic components # a good starting guess is essential # start = [np.mean(vel) * velscale, np.mean(sigma) * velscale] start = [start, start] goodPixels = np.arange(20, 6000) t = clock() plt.clf() plt.subplot(211) plt.title("Two components pPXF fit") print("+++++++++++++++++++++++++++++++++++++++++++++") pp = ppxf(templates, galaxy, noise, velscale, start, goodpixels=goodPixels, plot=True, degree=4, moments=[2, 2], component=[0, 0, 1, 1]) plt.subplot(212) plt.title("Single component pPXF fit") print("---------------------------------------------") start = start[0] pp = ppxf(templates, galaxy, noise, velscale, start, goodpixels=goodPixels, plot=True, degree=4, moments=2) print("=============================================") print("Total elapsed time %.2f s" % (clock() - t)) plt.tight_layout() plt.pause(1)
def kinematics_sdss(cube_id, y_data_var, fit_range): file_loc = "ppxf_results" + "/cube_" + str(int(cube_id)) if not os.path.exists(file_loc): os.mkdir(file_loc) # reading cube_data cube_file = ( "/Volumes/Jacky_Cao/University/level4/project/cubes_better/cube_" + str(cube_id) + ".fits") hdu = fits.open(cube_file) t = hdu[1].data spectra = cube_reader.spectrum_creator(cube_file) # using our redshift estimate from lmfit cube_result_file = ("cube_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("cube_results/cube_" + str(int(cube_id)) + "/cube_" + str(int(cube_id)) + "_cbd_x.npy") if (np.sum(y_data_var) == 0): cube_y_data = np.load("cube_results/cube_" + str(int(cube_id)) + "/cube_" + str(int(cube_id)) + "_cbs_y.npy") else: cube_y_data = y_data_var cube_x_original = cube_x_data cube_y_original = cube_y_data # masking the data to ignore initial 'noise' / non-features initial_mask = (cube_x_data > 3540 * (1 + z)) cube_x_data = cube_x_original[initial_mask] cube_y_data = cube_y_original[initial_mask] # calculating the signal to noise sn_region = np.array([4000, 4080]) * (1 + z) sn_region_mask = ((cube_x_data > sn_region[0]) & (cube_x_data < sn_region[1])) cube_y_sn_region = cube_y_data[sn_region_mask] cy_sn_mean = np.mean(cube_y_sn_region) cy_sn_std = np.std(cube_y_sn_region) cy_sn = cy_sn_mean / cy_sn_std #print("s/n:") #print(cy_sn, cy_sn_mean, cy_sn_std) # cube noise cube_noise_data = cube_noise() spectrum_noise = cube_noise_data['spectrum_noise'] spec_noise = spectrum_noise[initial_mask] # will need this for when we are considering specific ranges if (isinstance(fit_range, str)): pass else: rtc = fit_range * (1 + z ) # create a new mask and mask our x and y data rtc_mask = ((cube_x_data > rtc[0]) & (cube_x_data < rtc[1])) cube_x_data = cube_x_data[rtc_mask] cube_y_data = cube_y_data[rtc_mask] spec_noise = spec_noise[rtc_mask] lamRange = np.array([np.min(cube_x_data), np.max(cube_x_data)]) specNew, logLam, velscale = log_rebin(lamRange, cube_y_data) lam = np.exp(logLam) loglam = np.log10(lam) # Only use the wavelength range in common between galaxy and stellar library. mask = (loglam > np.log10(3540)) & (loglam < np.log10(9464)) flux = specNew[mask] galaxy = flux / np.median( flux) # Normalize spectrum to avoid numerical issues loglam_gal = loglam[mask] lam_gal = 10**loglam_gal # galaxy spectrum not scaled galaxy_ns = flux segmentation_data = hdu[2].data seg_loc_rows, seg_loc_cols = np.where(segmentation_data == cube_id) signal_pixels = len(seg_loc_rows) spec_noise = spec_noise[mask] noise = (spec_noise * np.sqrt(signal_pixels)) / np.median(flux) # sky noise sky_noise = cube_reader.sky_noise("data/skyvariance_csub.fits") skyNew, skyLogLam, skyVelScale = log_rebin(lamRange, sky_noise[initial_mask]) skyNew = skyNew[mask] c = 299792.458 # speed of light in km/s frac = lam_gal[1] / lam_gal[0] # Constant lambda fraction per pixel dlam_gal = (frac - 1) * lam_gal # Size of every pixel in Angstrom data_shape = np.shape(galaxy) wdisp = np.full(data_shape, 1, dtype=float) # Intrinsic dispersion of every pixel fwhm_gal = 2.51 * wdisp * dlam_gal # Resolution FWHM of every pixel, in Angstroms velscale = np.log(frac) * c # Constant velocity scale in km/s per pixel # If the galaxy is at significant redshift, one should bring the galaxy # spectrum roughly to the rest-frame wavelength, before calling pPXF # (See Sec2.4 of Cappellari 2017). In practice there is no # need to modify the spectrum in any way, given that a red shift # corresponds to a linear shift of the log-rebinned spectrum. # One just needs to compute the wavelength range in the rest-frame # and adjust the instrumental resolution of the galaxy observations. # This is done with the following three commented lines: # #lam_gal = lam_gal/(1+z) # Compute approximate restframe wavelength #fwhm_gal = fwhm_gal/(1+z) # Adjust resolution in Angstrom # Read the list of filenames from the Single Stellar Population library # by Vazdekis (2010, MNRAS, 404, 1639) http://miles.iac.es/. A subset # of the library is included for this example with permission #template_set = glob.glob('miles_models/Mun1.30Z*.fits') #template_set = glob.glob('jacoby_models/jhc0*.fits') #fwhm_tem = 4.5 # instrumental resolution in Ångstroms. # NOAO Coudé templates template_set = glob.glob("noao_templates/*.fits") fwhm_tem = 1.35 # Extract the wavelength range and logarithmically rebin one spectrum # to the same velocity scale of the SDSS galaxy spectrum, to determine # the size needed for the array which will contain the template spectra. # #hdu = fits.open(template_set[0]) #ssp = hdu[0].data #h2 = hdu[0].header #lam_temp = h2['CRVAL1'] + h2['CDELT1']*np.arange(h2['NAXIS1']) hdu = fits.open(template_set[0]) noao_data = hdu[1].data[0] ssp = noao_data[1] lam_temp = noao_data[0] lamRange_temp = [np.min(lam_temp), np.max(lam_temp)] sspNew = util.log_rebin(lamRange_temp, ssp, velscale=velscale)[0] templates = np.empty((sspNew.size, len(template_set))) # Interpolates the galaxy spectral resolution at the location of every pixel # of the templates. Outside the range of the galaxy spectrum the resolution # will be extrapolated, but this is irrelevant as those pixels cannot be # used in the fit anyway. fwhm_gal = np.interp(lam_temp, lam_gal, fwhm_gal) # Convolve the whole Vazdekis library of spectral templates # with the quadratic difference between the SDSS and the # Vazdekis instrumental resolution. Logarithmically rebin # and store each template as a column in the array TEMPLATES. # Quadratic sigma difference in pixels Vazdekis --> SDSS # The formula below is rigorously valid if the shapes of the # instrumental spectral profiles are well approximated by Gaussians. # # In the line below, the fwhm_dif is set to zero when fwhm_gal < fwhm_tem. # In principle it should never happen and a higher resolution template should be used. # fwhm_dif = np.sqrt((fwhm_gal**2 - fwhm_tem**2).clip(0)) #sigma = fwhm_dif/2.355/h2['CDELT1'] # Sigma difference in pixels spacing = lam_temp[1] - lam_temp[0] sigma = fwhm_dif / 2.355 / spacing # Sigma difference in pixels for j, fname in enumerate(template_set): hdu = fits.open(fname) #ssp = hdu[0].data noao_data = hdu[1].data[0] ssp = noao_data[1] ssp = util.gaussian_filter1d( ssp, sigma) # perform convolution with variable sigma sspNew = util.log_rebin(lamRange_temp, ssp, velscale=velscale)[0] templates[:, j] = sspNew / np.median(sspNew) # Normalizes 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 (see above). # c = 299792.458 dv = np.log(lam_temp[0] / lam_gal[0]) * c # km/s #lam_gal_alt = lam_gal * (1+z) #lamRange_temp = [np.min(lam_temp), np.max(lam_temp)] goodpixels = util.determine_goodpixels(np.log(lam_gal), lamRange_temp, z) # Here the actual fit starts. The best fit is plotted on the screen. # Gas emission lines are excluded from the pPXF fit using the GOODPIXELS keyword. # vel = c * np.log(1 + z) # eq.(8) of Cappellari (2017) start = [vel, 200.] # (km/s), starting guess for [V, sigma] t = process_time() f = io.StringIO() with redirect_stdout(f): pp = ppxf(templates, galaxy, noise, velscale, start, sky=skyNew, goodpixels=goodpixels, plot=True, moments=4, degree=12, vsyst=dv, clean=False, lam=lam_gal) ppxf_variables = pp.sol ppxf_errors = pp.error red_chi2 = pp.chi2 best_fit = pp.bestfit x_data = cube_x_data[mask] y_data = cube_y_data[mask] print(ppxf_variables) #plt.show() if ((np.sum(y_data_var) == 0) and isinstance(fit_range, str)): np.save(file_loc + "/cube_" + str(int(cube_id)) + "_lamgal", lam_gal) np.save(file_loc + "/cube_" + str(int(cube_id)) + "_flux", flux) np.save(file_loc + "/cube_" + str(int(cube_id)) + "_x", x_data) np.save(file_loc + "/cube_" + str(int(cube_id)) + "_y", y_data) np.save(file_loc + "/cube_" + str(int(cube_id)) + "_noise", noise) # if best fit i.e. perturbation is 0, save everything kinematics_file = open( file_loc + "/cube_" + str(int(cube_id)) + "_kinematics.txt", 'w') np.save(file_loc + "/cube_" + str(int(cube_id)) + "_model", best_fit) print("Rough reduced chi-squared from ppxf: " + str(pp.chi2)) data_to_file = f.getvalue() kinematics_file.write(data_to_file) kinematics_file.write("") kinematics_file.write("Formal errors: \n") kinematics_file.write(" dV dsigma dh3 dh4 \n") kinematics_file.write("".join("%8.2g" % f for f in pp.error * np.sqrt(pp.chi2)) + "\n") kinematics_file.write('Elapsed time in PPXF: %.2f s' % (process_time() - t) + "\n") plt.tight_layout() graph_loc = "ppxf_results" + "/cube_" + str(int(cube_id)) if not os.path.exists(graph_loc): os.mkdir(graph_loc) kinematics_graph = (graph_loc + "/cube_" + str(int(cube_id)) + "_kinematics.pdf") plt.savefig(kinematics_graph) #plt.show() plt.close("all") if not isinstance(fit_range, str): # saving graphs if not original range fit_range = fit_range * (1 + z) fitting_plotter(cube_id, fit_range, x_data, y_data, best_fit, noise) # If the galaxy is at significant redshift z and the wavelength has been # de-redshifted with the three lines "z = 1.23..." near the beginning of # this procedure, the best-fitting redshift is now given by the following # commented line (equation 2 of Cappellari et al. 2009, ApJ, 704, L34): # #print, 'Best-fitting redshift z:', (z + 1)*(1 + sol[0]/c) - 1 return { 'reduced_chi2': red_chi2, 'noise': noise, 'variables': ppxf_variables, 'y_data': galaxy, 'x_data': lam_gal, 'redshift': z, 'y_data_original': cube_y_original, 'non_scaled_y': galaxy_ns, 'model_data': best_fit, 'noise_original': spec_noise, 'errors': ppxf_errors }
def run_ppxf(group, specs, redo=False, ncomp=2, logdir="ppxf", window=50, w1=None, w2=None, **kwargs): """ New function to run pPXF. """ global velscale tempfile = os.path.join( home, "MILES/templates/" "templates_w3540.5_7409.6_res4.7.fits") stars = pf.getdata(tempfile, 0) emission = pf.getdata(tempfile, 1) logLam_temp = wavelength_array(tempfile, axis=1, extension=0) ngas = len(emission) nstars = len(stars) templates = np.column_stack((stars.T, emission.T)) ########################################################################## # Set components if ncomp == 1: components = np.zeros(nstars + ngas) kwargs["component"] = components elif ncomp == 2: components = np.hstack((np.zeros(nstars), np.ones(ngas))).astype(int) kwargs["component"] = components ########################################################################## for spec in specs: os.chdir(os.path.join(data_dir, group)) outdir = os.path.join(os.getcwd(), logdir) if not os.path.exists(outdir): os.mkdir(outdir) data = pf.getdata(spec) w = wavelength_array(spec) if w1 == None: w1 = w[0] if w2 == None: w2 = w[-1] idx = np.where((w >= w1) & (w <= w2))[0] data = data[idx] w = w[idx] ####################################################################### # Preparing output output = os.path.join(outdir, spec.replace(".fits", ".pkl")) outroot = output.replace(".pkl", "") if os.path.exists(output) and not redo: continue print group, spec signal, noise, sn = snr(data) galaxy, logLam, vtemp = util.log_rebin([w[0], w[-1]], data, velscale=velscale) lam = np.exp(logLam) ################################################################### # Trimming spectra idx = np.where(np.exp(logLam) > np.exp(logLam_temp[0]))[0] didx = int(1500. / velscale) # Space in the beginning of the spec idx = idx[didx:] galaxy = galaxy[idx] lam = lam[idx] logLam = logLam[idx] ####################################################################### # Masking bad pixels goodpixels = np.arange(len(lam), dtype=float) gaps = set_badpixels(group) for gap in gaps: idx = np.where((lam > gap[0] - window / 2.) & (lam < gap[1] + window / 2.))[0] goodpixels[idx] = np.nan goodpixels = goodpixels[~np.isnan(goodpixels)].astype(int) kwargs["goodpixels"] = goodpixels ####################################################################### kwargs["lam"] = lam dv = (logLam_temp[0] - logLam[0]) * c kwargs["vsyst"] = dv noise = np.ones_like(galaxy) * noise # First fitting to obtain realistic noise pp0 = ppxf(templates, galaxy, noise, velscale, **kwargs) pp0.has_emission = True pp0.dv = dv pp0.w = np.exp(logLam) pp0.velscale = velscale pp0.ngas = ngas pp0.ntemplates = nstars pp0.templates = 0 pp0.name = spec pp0.title = "" pp0 = pPXF(pp0, velscale) pp0.calc_sn() res = (pp0.galaxy - pp0.bestfit) noise = rolling_std(res, window, center=True) noise[:window / 2] = noise[window + 1] noise[-window / 2 + 1:] = noise[-window / 2] # Second fitting using results from first interaction pp = ppxf(templates, galaxy, noise, velscale, **kwargs) title = "Group {} , Spectrum {}".format( group.upper(), spec.replace(".fits", "").replace("spec", "")) # Adding other things to the pp object pp.has_emission = True pp.dv = dv pp.w = np.exp(logLam) pp.velscale = velscale pp.ngas = ngas pp.ntemplates = nstars pp.templates = 0 pp.id = id pp.name = spec pp.title = title ppsave(pp, outroot=outroot) ppf = ppload(outroot) ppf = pPXF(ppf, velscale) ppf.plot("{1}/{0}.png".format(pp.name.replace(".fits", ""), outdir)) return