def read_simple_templates(velscale, lamrange): hdul = fits.open(UT.lgal_dir() + "/simple_mocks/template_fluxbc03.fits") wave_s = hdul[1].data['wave'] flux_bulge = hdul[1].data['L_bulge'] flux_disk = hdul[1].data['L_disk'] hdul.close() wave, flux_bulge = to_common_grid(wave_s, flux_bulge, lamrange[0], lamrange[1]) wave, flux_disk = to_common_grid(wave_s, flux_disk, lamrange[0], lamrange[1]) mask = ((wave >= lamrange[0]) & (wave <= lamrange[1])) wave = wave[mask] flux_bulge = flux_bulge[mask] model1, logLam1, velscale_out = util.log_rebin([wave[0], wave[-1]], flux_bulge, velscale=velscale) model1 /= np.median(model1) print(velscale, velscale_out) flux_disk = flux_disk[mask] model2, logLam2, velscale_out = util.log_rebin([wave[0], wave[-1]], flux_disk, velscale=velscale) model2 /= np.median(model2) templates = np.column_stack([model1, model2]) #print([wave[0], wave[-1]]) plt.plot(np.exp(1)**logLam1, model1) plt.plot(np.exp(1)**logLam2, model2) return (logLam1, templates)
def make_templates(wave, stars, emission, velscale, w1, w2, fits): """ Prepare file with templates. """ idx = np.where((wave >= w1) & (wave <= w2))[0] wave = wave[idx] lrange = np.array([wave[0], wave[-1]]) stars = stars[idx] emission = emission[idx] # Rebin data tmp, logLam, velscale = util.log_rebin(lrange, stars[:,0], velscale=velscale) tmp2, logLam2, velscale = util.log_rebin(lrange, emission[:,0], velscale=velscale) starsnew = np.zeros((len(logLam), len(stars[0]))) for i in range(len(stars[0])): star, l, velscale = util.log_rebin(lrange, stars[:,i], velscale=velscale) starsnew[:,i] = star emissionnew = np.zeros((len(logLam), len(emission[0]))) for i in range(len(emission[0])): em, logLam, velscale = util.log_rebin([wave[0], wave[-1]], emission[:,i], velscale=velscale) emissionnew[:,i] = em # print range(len(stars)) # for i in range(len(stars[0])): # plt.plot(logLam, starsnew[i], "-k") # plt.show() hdu1 = pf.PrimaryHDU(starsnew.T) hdu2 = pf.ImageHDU(emissionnew.T) hdu1.header["CRVAL1"] = logLam[0] hdu1.header["CD1_1"] = logLam[1] - logLam[0] hdu1.header["CRPIX1"] = 1. hdulist = pf.HDUList([hdu1, hdu2]) hdulist.writeto(fits, clobber=True)
def prepstellarfit(self, spectrum=None, wvl=None): """ Prepare stacked cube for spectral fitting with pPXF. Only need to run this once per galaxy! Arguments: spectrum, wvl (1D arrays): observed spectrum to use as template (default: use stacked spectrum) """ print('Preparing for stellar kinematics fit...') # Define path to pPXF directory ppxf_dir = path.dirname(path.realpath(ppxf_package.__file__)) # Define spectrum spectrum = self.stacked_spec[0] if spectrum is None else spectrum wvl = self.wvl_cropped if wvl is None else wvl # Define wavelength range self.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(self.lamRange1, spectrum) # Read the list of filenames from the E-MILES SSP library vazdekis = glob.glob(ppxf_dir + '/miles_models/Mun*.fits') #vazdekis = glob.glob(ppxf_dir + '/miles_stellar/s*.fits') fwhm_tem = 2.51 # Vazdekis+10 spectra have a constant resolution FWHM of 2.51A. # Open template spectrum in order to make get the size of the template array hdu = fits.open(vazdekis[0]) ssp = np.squeeze(hdu[0].data) h2 = hdu[0].header self.lamRange2 = h2['CRVAL1'] + np.array([0., h2['CDELT1']*(h2['NAXIS1'] - 1)]) sspNew, logLam2, velscale_temp = util.log_rebin(self.lamRange2, ssp, velscale=velscale) self.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)) self.sigma = fwhm_dif/2.355/h2['CDELT1'] # Sigma difference in pixels galspec = ndimage.gaussian_filter1d(spectrum, self.sigma) # Now logarithmically rebin this new observed spectrum galaxy, logLam1, velscale = util.log_rebin(self.lamRange1, galspec, velscale=velscale) # Open and normalize all the templates for j, file in enumerate(vazdekis): hdu = fits.open(file) ssp = np.squeeze(hdu[0].data) sspNew, self.logLam2, velscale_temp = util.log_rebin(self.lamRange2, ssp, velscale=velscale) self.templates[:, j] = sspNew/np.median(sspNew) # Normalizes templates return
def read_simple_templates(velscale, lamrange): hdul = fits.open(UT.lgal_dir() + "/simple_mocks/template_fluxbc03.fits") wave = hdul[1].data['wave'] flux_bulge = hdul[1].data['L_bulge'] flux_disk = hdul[1].data['L_disk'] hdul.close() #put on constant grid, as this is assumed by log_rebin wave_s, flux_bulge = to_constant_grid(wave, flux_bulge, lamrange[0], lamrange[1]) wave_s, flux_disk = to_constant_grid(wave, flux_disk, lamrange[0], lamrange[1]) print('full model wavelength range: ', wave_s[0], wave_s[-1]) print('requested model wavelength range:', lamrange) #flux_bulge = flux_bulge[mask] model1, logLam1, velscale_out = util.log_rebin([lamrange[0], lamrange[1]], flux_bulge, velscale=velscale) norm1 = np.median(model1) model1 /= norm1 print(velscale, velscale_out) #flux_disk = flux_disk[mask] model2, logLam2, velscale_out = util.log_rebin([lamrange[0], lamrange[1]], flux_disk, velscale=velscale) norm2 = np.median(model2) model2 /= norm2 print(velscale, velscale_out) #print([wave[0], wave[-1]]) #protect against data goint outside of models wavelength range #Stelib library is defined below 3400, but at lower resolution if lamrange[0] < 3400.0: lamrange_new = [3400.0, lamrange[1]] print(lamrange_new) mask = ((np.exp(1)**logLam1 >= lamrange_new[0]) & (np.exp(1)**logLam1 <= lamrange_new[1])) logLam1 = logLam1[mask] logLam2 = logLam2[mask] model1 = model1[mask] model2 = model2[mask] templates = np.column_stack([model1, model2]) plt.plot(np.exp(1)**logLam1, model1) plt.plot(np.exp(1)**logLam2, model2) return (logLam1, templates, [norm1, norm2])
def load_templates_regul(velscale): """ Load templates into 2D array for regularization""" # cd into template folder to make calculations current_dir = os.getcwd() os.chdir(os.path.join(home, "miles_models")) # Extract the wavelength range and logarithmically rebin one spectrum # to the same velocity scale of the SAURON galaxy spectrum, to determine # the size needed for the array which will contain the template spectra. miles = [x for x in os.listdir(".") if x.endswith(".fits")] hdu = pf.open(miles[0]) ssp = hdu[0].data h2 = hdu[0].header lamRange2 = h2["CRVAL1"] + np.array([0.0, h2["CDELT1"] * (h2["NAXIS1"] - 1)]) sspNew, logLam2, velscale = util.log_rebin(lamRange2, ssp, velscale=velscale) # Ordered array of metallicities Zs = set([x.split("Z")[1].split("T")[0] for x in miles]) Zs = [float(x.replace("m", "-").replace("p", "")) for x in Zs] Zs.sort() Z2 = Zs[:] for i in range(len(Zs)): if Zs[i] < 0: Zs[i] = "{0:.2f}".format(Zs[i]).replace("-", "m") else: Zs[i] = "p{0:.2f}".format(Zs[i]) # Ordered array of ages Ts = list(set([x.split("T")[1].split(".fits")[0] for x in miles])) Ts.sort() # Create a three dimensional array to store the # two dimensional grid of model spectra # nAges = len(Ts) nMetal = len(Zs) templates = np.empty((sspNew.size, nAges, nMetal)) # Here we make sure the spectra are sorted in both [M/H] # and Age along the two axes of the rectangular grid of templates. # A simple alphabetical ordering of Vazdekis's naming convention # does not sort the files by [M/H], so we do it explicitly below miles = [] for k in range(nMetal): for j in range(nAges): filename = "{3}1.30Z{0}T{1}.fits".format(Zs[k], Ts[j], imf) ssp = pf.getdata(filename) sspNew, logLam2, velscale = util.log_rebin(lamRange2, ssp, velscale=velscale) templates[:, j, k] = sspNew # Templates are *not* normalized here miles.append(filename) templates /= np.median(templates) # Normalizes templates by a scalar os.chdir(current_dir) return templates, logLam2, Ts, Z2, miles, h2["CDELT1"]
def rebin_spectra(cube, x, y, lamRange, lamb): """ Extracts the a single spectrum for a data given the desired x,y by binning the data using pPXF util.log_rebin Parameters ---------- cube : the datacube x : x-coordinate of the spaxel y : y-coordinate of the spaxel lamRange : the wavelength range o lamb : the wavelength array Returns ------- specNew : the binned spectrum logLam : the logged wavelength array velscale: the velocity scale of the spectrum """ x = np.atleast_1d(x) y = np.atleast_1d(y) spectrum = np.median(np.array( [cube[:, int(ypair), int(xpair)] for ypair, xpair in zip(y, x)]), axis=0) c = 299792.458 # speed of light in km/s specNew, logLam, velscale = util.log_rebin(lamRange, spectrum, oversample=False, velscale=c * np.log(lamb[1] / lamb[0]), flux=False) return (specNew, logLam, velscale)
def load_CaT_templates(FWHM_galaxy, velscale, cat_dir="FIXME"): """ Load the CaT template library """ CaT = glob.glob(cat_dir + 'Cun1.*.fits') CaT.sort() #CaT Templates are at a FWHM of 1.51 Angstroms FWHM_tem = 1.51 #FIXME #This depends on where in the wavelength range you are. For SWIFT: #9300A- 3.44A #10120A- 4.55A #8700- 4.04 #To get a more accurate value for a certain lamda, fit a gaussian to a skyline! hdu = fits.open(CaT[0]) ssp = hdu[0].data h2 = hdu[0].header lam_temp = h2['CRVAL1'] + h2['CDELT1'] * np.arange(h2['NAXIS1']) lamRange2 = [np.min(lam_temp), np.max(lam_temp)] print("LamRange2 is {}".format(lamRange2)) sspNew, logLam2, velscale = util.log_rebin(lamRange2, ssp, velscale=velscale) templates = np.empty((sspNew.size, len(CaT))) t_num = len(CaT) FWHM_dif = np.sqrt((FWHM_galaxy**2 - FWHM_tem**2)) sigma = FWHM_dif / 2.355 / h2['CDELT1'] # Sigma difference in pixels print("Sigma is {}".format(sigma)) for j, fname in enumerate(CaT): hdu = fits.open(fname) ssp = hdu[0].data ssp = util.gaussian_filter1d( ssp, sigma) # perform convolution with variable sigma sspNew, logLam2, velscale = util.log_rebin(lamRange2, ssp, velscale=velscale) templates[:, j] = sspNew / np.median(sspNew) # Normalizes templates return templates, loglam2
def mask_and_log_rebin(lamdas, spec, lamRange, velscale=None): """Mask and log rebin a spectrum """ low_lam, high_lam = lamRange mask = np.where((lamdas >= low_lam) & (lamdas <= high_lam))[0] spec = spec[mask] if velscale is None: spec, logLam1, velscale = util.log_rebin(lamRange, spec) else: spec, logLam1, velscale = util.log_rebin(lamRange, spec) return spec, logLam1, velscale
def retrieveTemplates(velscale, FWHM_WiFeS_ang, range_sel=None): """ This function reads and sets up the stellar libraries for pPXF This function was written by Amelia Fraser-McKelvie """ templatesInput = glob.glob( '/Users/vaishalp/Dropbox/Amelia/IC1059/Archive/StellarLibraries/INDO-US_Valdes/*.txt' ) FWHM_tem_ang = 1.35 #In \AA #SINGLE TEMPLATE wv_temp1, inten_temp1 = readAsciiSpec(templatesInput[0]) if range_sel: range_sel_rest = range_sel else: range_sel_rest = [np.min(wv_temp1), np.max(wv_temp1)] #Selection Chunk of template sel_temp = np.nonzero((wv_temp1 >= range_sel_rest[0]) & (wv_temp1 <= range_sel_rest[1])) wv_temp, inten_temp = wv_temp1[sel_temp], inten_temp1[sel_temp] sspNew, logLam2, velscale = util.log_rebin([wv_temp[0], wv_temp[-1]], inten_temp, velscale=velscale) #ALL TEMPLATES templates = np.empty((sspNew.size, len(templatesInput))) #convolution FWHM_dif = np.sqrt(FWHM_WiFeS_ang**2 - FWHM_tem_ang**2) sigma = FWHM_dif / 2.355 / (wv_temp[1] - wv_temp[0] ) # Sigma difference in pixels for ii in range(len(templatesInput)): wv_temp2, inten_temp2 = readAsciiSpec(templatesInput[ii]) wv_temp_ii, inten_temp_ii = wv_temp2[sel_temp], inten_temp2[sel_temp] ssp = ndimage.gaussian_filter1d( inten_temp_ii, sigma) #One-dimensional Gaussian filter. sspNew, logLam2, velscale = util.log_rebin([wv_temp[0], wv_temp[-1]], ssp, velscale=velscale) templates[:, ii] = sspNew / np.median(sspNew) stdout.write("\rDONE %i/%i" % (ii + 1, len(templatesInput))) stdout.flush() return logLam2, templates
def stellar_templates(velscale): """ Load files with stellar library used as templates. """ current_dir = os.getcwd() # Template directory is also set in config.py os.chdir(templates_dir) miles = [x for x in os.listdir(".") if x.startswith("Mbi1.30") and x.endswith(".fits")] # Ordered array of metallicities Zs = set([x.split("Z")[1].split("T")[0] for x in miles]) Zs = [float(x.replace("m", "-").replace("p", "").replace("_", ".")) for x in Zs] Zs.sort() for i in range(len(Zs)): if Zs[i] < 0: Zs[i] = "{0:.2f}".format(Zs[i]).replace("-", "m") else: Zs[i] = "p{0:.2f}".format(Zs[i]) # Ordered array of ages Ts = list(set([x.split("T")[1][:7] for x in miles])) Ts.sort() # Ordered array of [alpha/Fe] As = sorted(list(set([x.split("Ep")[1][:4] for x in miles]))) miles = [] metal_ages_alphas = [] for a in As: for m in Zs: for t in Ts: filename = "Mbi1.30Z{0}T{1}_iTp{2}_Ep{2}_" "linear_FWHM_2.51.fits".format(m, t, a) if os.path.exists(filename): miles.append(filename) metal_ages_alphas.append( [m.replace("_", ".").replace("p", "+").replace("m", "-"), t.replace("_", "."), a] ) hdu = pf.open(miles[0]) ssp = hdu[0].data h2 = hdu[0].header lamRange2 = h2["CRVAL1"] + np.array([0.0, h2["CDELT1"] * (h2["NAXIS1"] - 1)]) sspNew, logLam2, velscale = util.log_rebin(lamRange2, ssp, velscale=velscale) templates = np.empty((sspNew.size, len(miles))) for j in range(len(miles)): hdu = pf.open(miles[j]) ssp = hdu[0].data sspNew, logLam2, velscale = util.log_rebin(lamRange2, ssp, velscale=velscale) templates[:, j] = sspNew templates /= np.median(templates) os.chdir(current_dir) return templates, logLam2, h2["CDELT1"], miles
def make_ssp_templates_bc03(velscale, lamrange, ages, metallicities): N = len(ages) * len(metallicities) #total number of templates age_m, flux_m, wave_m, Z_m = my_utils.read_models_bc03() D = len(wave_m) #first interpolate in age, keep metallicity models_t = np.zeros((len(Z_m), D, len(ages)), dtype=np.float32) for l in range(0, D): for z in range(0, len(Z_m)): #print(l,z) models_t[z, l, :] = np.interp(np.log10(ages * 1e9), np.log10(age_m * 1e9), flux_m[z, l, :]) #then interpolate in metallicity #flux_out= np.zeros( (len(metallicities), D, len(ages)), dtype=np.float32) templates = np.zeros((D, N)) c = 0 for j in range(0, len(metallicities)): for i in range(0, len(ages)): for l in range(0, D): templates[l, c] = np.interp(np.log10(metallicities[j]), np.log10(Z_m), models_t[:, l, i]) c = c + 1 #now mask and re-grid mask = ((wave_m >= lamrange[0]) & (wave_m <= lamrange[1])) wave = wave_m[mask] template_rb, logLam, velscale_out = util.log_rebin([wave[0], wave[-1]], templates[mask, 0], velscale=velscale) templates_out = np.zeros((len(logLam), N)) templates_out[:, 0] = template_rb / np.median(template_rb) plt.plot(np.exp(1)**logLam, templates_out[:, 0]) for i in range(1, N): templates_out[:, i], logLam, velscale_out = util.log_rebin( [wave[0], wave[-1]], templates[mask, i], velscale=velscale) templates_out[:, i] = templates_out[:, i] / np.median(templates_out[:, i]) plt.plot(np.exp(1)**logLam, templates_out[:, i]) return logLam, templates_out
def make_ssp_templates_fsps(velscale, lamrange, ages, metallicities): #initialise FSPS models sp = fsps.StellarPopulation(compute_vega_mags=False, zcontinuous=1, sfh=0) #total number of templates N = len(ages) * len(metallicities) #put in log10(Z/Z_solar) metallicities = np.log10(np.array(metallicities) / 0.019) #get the wavelength vector and initialise the array to hold all the templates wave = set_common_grid() templates = np.zeros((len(wave), N)) #now loop through the given metallicities and ages to create each template in turn c = 0 for Z in metallicities: sp.params['logzsol'] = Z for age in ages: wave_fsps, flux_fsps = sp.get_spectrum(tage=age, peraa=True) #in L_solar/AA #now to common grid wave, flux = to_common_grid(wave_fsps, flux_fsps, lamrange[0], lamrange[1]) templates[:, c] = flux / np.median(flux) c = c + 1 #now mask and re-grid mask = ((wave >= lamrange[0]) & (wave <= lamrange[1])) wave = wave[mask] template_rb, logLam, velscale_out = util.log_rebin([wave[0], wave[-1]], templates[mask, 0], velscale=velscale) templates_out = np.zeros((len(logLam), N)) templates_out[:, 0] = template_rb plt.plot(np.exp(1)**logLam, templates_out[:, 0]) for i in range(1, N): templates_out[:, i], logLam, velscale_out = util.log_rebin( [wave[0], wave[-1]], templates[mask, i], velscale=velscale) templates_out[:, i] = templates_out[:, i] plt.plot(np.exp(1)**logLam, templates_out[:, i]) return logLam, templates_out
def emission_templates(velscale): """ Load files with stellar library used as templates. """ emission = [x for x in os.listdir(".") if x.startswith("emission") and x.endswith(".fits")] emission.sort() c = 299792.458 FWHM_tem = 2.54 # MILES library spectra have a resolution FWHM of 2.54A. # Extract the wavelength range and logarithmically rebin one spectrum # to the same velocity scale of the SAURON galaxy spectrum, to determine # the size needed for the array which will contain the template spectra. # hdu = pf.open(emission[0]) ssp = hdu[0].data h2 = hdu[0].header lamRange2 = h2["CRVAL1"] + np.array([0.0, h2["CDELT1"] * (h2["NAXIS1"] - 1)]) sspNew, logLam2, velscale = util.log_rebin(lamRange2, ssp, velscale=velscale) templates = np.empty((sspNew.size, len(emission))) for j in range(len(emission)): hdu = pf.open(emission[j]) ssp = hdu[0].data sspNew, logLam2, velscale = util.log_rebin(lamRange2, ssp, velscale=velscale) templates[:, j] = sspNew # templates *= 1e5 # Normalize templates return templates, logLam2, h2["CDELT1"], emission
def make_templates(wave, stars, emission, velscale, w1, w2, fits): """ Prepare file with templates. """ idx = np.where((wave >= w1) & (wave <= w2))[0] wave = wave[idx] lrange = np.array([wave[0], wave[-1]]) stars = stars[idx] emission = emission[idx] # Rebin data tmp, logLam, velscale = util.log_rebin(lrange, stars[:, 0], velscale=velscale) tmp2, logLam2, velscale = util.log_rebin(lrange, emission[:, 0], velscale=velscale) starsnew = np.zeros((len(logLam), len(stars[0]))) for i in range(len(stars[0])): star, l, velscale = util.log_rebin(lrange, stars[:, i], velscale=velscale) starsnew[:, i] = star emissionnew = np.zeros((len(logLam), len(emission[0]))) for i in range(len(emission[0])): em, logLam, velscale = util.log_rebin([wave[0], wave[-1]], emission[:, i], velscale=velscale) emissionnew[:, i] = em # print range(len(stars)) # for i in range(len(stars[0])): # plt.plot(logLam, starsnew[i], "-k") # plt.show() hdu1 = pf.PrimaryHDU(starsnew.T) hdu2 = pf.ImageHDU(emissionnew.T) hdu1.header["CRVAL1"] = logLam[0] hdu1.header["CD1_1"] = logLam[1] - logLam[0] hdu1.header["CRPIX1"] = 1. hdulist = pf.HDUList([hdu1, hdu2]) hdulist.writeto(fits, clobber=True)
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 __init__(self, velscale, sigma, _noread=False): """ Load Miles templates for simulations. """ self.velscale = velscale self.sigma = sigma miles_path = os.path.join(context.basedir, "ppxf/miles_models") # Search for spectra and their properties fitsfiles = [_ for _ in os.listdir(miles_path) if _.endswith(".fits")] # Define the different values of metallicities and ages of templates self.metals = np.unique([ float( _.split("Z")[1].split("T")[0].replace("m", "-").replace("p", "+")) for _ in fitsfiles ]) self.ages = np.unique([ float( _.split("T")[1].split("_iP")[0].replace("m", "-").replace("p", "+")) for _ in fitsfiles ]) # Defining arrays self.ages2D, self.metals2D = np.meshgrid(self.ages, self.metals) self.metals1D = self.metals2D.reshape(-1) self.ages1D = self.ages2D.reshape(-1) if _noread: return templates, norms = [], [] for metal, age in zip(self.metals1D, self.ages1D): template_file = os.path.join(miles_path, self.miles_filename(metal, age)) spec = read_fits_spectrum1d(template_file) wave = spec.dispersion flux = spec.flux speclog, logwave, _ = util.log_rebin([wave[0], wave[-1]], flux, velscale=self.velscale) speclog = gaussian_filter1d(speclog, sigma / velscale) wave = wave[1:-2] flux = spectres(wave, np.exp(logwave), speclog) norm = np.sum(flux) templates.append(flux / norm) self.templates = np.array(templates) self.norms = np.array(norms) self.wave = wave return
def emission_line_template(lines, velscale, res=2.5, intens=None, resamp=15, return_log=True): lines = np.atleast_1d(lines) if intens == None: intens = np.ones_like(lines) * 1e-5 current_dir = os.getcwd() os.chdir(templates_dir) refspec = [x for x in os.listdir(".") if x.endswith(".fits") and x.startswith("Mbi")][0] lamb = wavelength_array(refspec) delta = lamb[1] - lamb[0] lamb2 = np.linspace(lamb[0] - delta / 2.0, lamb[-1] + delta / 2.0, len(lamb + 1) * resamp) sigma = res / (2.0 * np.sqrt(2.0 * np.log(2.0))) spec = np.zeros_like(lamb2) for line, intensity in zip(lines, intens): spec += intensity * np.exp(-(lamb2 - line) ** 2 / (2 * sigma * sigma)) spec = np.sum(spec.reshape(len(lamb), resamp), axis=1) if not return_log: return spec specNew, logLam2, velscale = util.log_rebin([lamb[0], lamb[-1]], spec, velscale=velscale) os.chdir(current_dir) return specNew
def make_gaussian_spectrum(lamdas, continuum, z, sigma_kms, peak_flux): scale = sigma_kms / const.c * 1000.0 * np.sqrt(2 * np.pi) specNew, logLam, velscale = util.log_rebin( np.array([lamdas[0], lamdas[-1]]), continuum) emission_line = gaussian(np.exp(logLam), 0.65626 * (1 + z), sigma_kms / const.c * 1000.0, scale) #Hack to get the height roughly right... #Come back to this! ha_pixel = np.argmin(np.abs(lamdas - 0.65626 * (1 + z))) continuum_at_emission_line = np.mean(continuum[ha_pixel - 10:ha_pixel + 10]) log_spec = specNew + emission_line * (continuum_at_emission_line) * ( peak_flux - 1) interpolator = si.interp1d(np.exp(logLam), log_spec, bounds_error=False) spectrum = interpolator(lamdas) spectrum[~np.isfinite(spectrum)] = spectrum[-2] #Make the spectrum by combining the continuum and emission line # spectrum=continuum + lin_spec return spectrum
""" Log rebin data """ start_wave = min(rss._wave) end_wave = max(rss._wave) print(start_wave, end_wave) dataconv = item() for yvar in range(y): [specNew, logLam, velscale] = log_rebin((start_wave, end_wave), rss._data[yvar, :]) [errNew, logLam_err, velscale_err] = log_rebin((start_wave, end_wave), (rss._error[yvar, :])**2) dataconv.y.append(yvar) dataconv.flux.append(specNew) dataconv.err.append(np.sqrt(errNew)) dataconv.wave.append(np.exp(logLam)) dataconv.specres.append(velscale) dataconv.lrange.append( [int(min(np.exp(logLam))), math.ceil(max(np.exp(logLam)))]) print(np.shape(dataconv.y)) print(np.shape(dataconv.flux)) print(np.array(dataconv.wave)[0, :]) print(rss._header['CRVAL1'])
def ppxf_example_kinematics_sdss(dirfile, galaxy, lam_gal, plateifu, mask, noise, redshift): ppxf_dir = path.dirname(path.realpath(ppxf_package.__file__)) z = redshift lam_gal = lam_gal mask = mask c = 299792.458 frac = lam_gal[1] / lam_gal[0] a = np.full((1, 4563), 2.76) fwhm_gal = a[0][mask] velscale = np.log(frac) * c vazdekis = glob.glob(ppxf_dir + '/miles_models/Mun1.30Z*.fits') fwhm_tem = 2.51 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))) fwhm_gal = np.interp(lam_temp, lam_gal, fwhm_gal) 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 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) vel = c * np.log(1 + z) # eq.(8) of Cappellari (2017) start = [vel, 200.] # (km/s), starting guess for [V, sigma] pp = stellar.ppxf(dirfile, templates, galaxy, noise, velscale, start, z, goodpixels=goodpixels, plot=True, moments=4, degree=12, vsyst=dv, clean=False, lam=lam_gal) return pp.bestfit, pp.lam
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 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 degrade_templates(template_dir, FWHM_templates, FWHM_observation, velscale_obs, vel_ratio=1): """ Function that combines pPXF "log_rebin" and "gaussian_filter1d" convolution to return a list of template spectra that match the resolution and gridding of the observed data. :param template_dir: A character string describing the path to the directory containing the raw template files. :param FWHM_templates: A float describing the resolution of the templates. :param FWHM_observation: A float describing the resolution of the observation. This can be different from the instrumental resolution if the galaxy is at a signifcant redshift. :param vescale_obs: A float describing the velocity scale in km/s per pixels of the observed spectra. :param vel_ratio: A float describing if you would like the velocity scale of the templates to be sampled at a higher rate. :return: An array of templates that have been degraded and rebinned to match the observations. """ vazdekis = glob.glob( template_dir ) # returns list of all template files contained within directory FWHM_tem = FWHM_templates FWHM_gal = FWHM_observation velscale = velscale_obs hdu = fits.open(vazdekis[0]) # opening the first template file in the list ssp = hdu[0].data # and measuring the wavelength range and SSP h2 = hdu[0].header # size for initiallising the template array. lamRange2 = h2['CRVAL1'] + np.array( [0., h2['CDELT1'] * (h2['NAXIS1'] - 1)]) # Gives the range of the spectra wavelengths sspNew, logLam2, velscale_temp = util.log_rebin(lamRange2, ssp, velscale=velscale / vel_ratio) templates = np.empty((sspNew.size, len(vazdekis))) FWHM_dif = np.sqrt(FWHM_gal**2 - FWHM_tem**2) sigma = FWHM_dif / 2.355 / h2['CDELT1'] # Sigma difference in resolutions 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 / vel_ratio) if np.median(sspNew) != 0: templates[:, j] = sspNew / np.median(sspNew) # Normalizes templates else: templates[:, j] = 0. return templates, logLam2
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 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 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 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
z = vel/c velscale_ratio = 1 h2, lamRange2, templates = read_models(vazdekis, z) ### Cosmological Redshift Correction ### lamRange1 = lamRange1 / (1+z) FWHM_gal = FWHM_gal / (1+z) goodlam = (lam/(1+z) > 3900) & (lam/(1+z) < 6797) lam = lam[goodlam] lamRange1 = np.array([lam[0], lam[-1]]) / (1+z) summedspec = summedspec[goodlam] gal, logLam1, velscale = util.log_rebin(lamRange1, summedspec) median = np.nanmedian(gal) #galaxy = galaxy / median noise = np.zeros(spec.shape[0]) for i in range(spec.shape[0]): noise[i] = DER_SNR(spec[i,:,:]) noise = noise[goodlam]/median z2 = copy.copy(z) z=0 #Bring Galaxy to Rest Frame gal = gal/median #galaxy = galaxy[overlaplam] #noise = noise[overlaplam] lam_temp = h2['CRVAL1'] + h2['CDELT1']*np.arange(h2['NAXIS1'])
def create_emiles_templates(lam_range, fwhm_gal, velscale, ages, metals): """ Uploads and modifies stellar template spectra from the EMILES library. Performs the following steps: 1.) Cuts the spectra to the specified wavelengths 2.) Log rebins the spectra to a specified velocity scale 3.) Convolves the spectra to a certain instrumental resolution :param lam_range: :param ages: :param metals: :param fwhm_gal: :param velscale: :return: """ # Use the EMILES SSP library as our template spectra file_dir = path.dirname(path.realpath(__file__)) # Metallicities available in the EMILES library metals_avail = [ -2.27, -1.79, -1.49, -1.26, -0.96, -0.66, -0.35, -0.25, 0.06, 0.15, 0.26, 0.4 ] # SSP ages available ages_avail = [ 0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.1, 0.15, 0.20, 0.25, 0.30, 0.35, 0.40, 0.45, 0.50, 0.6, 0.7, 0.8, 0.9, 1.0, 1.25, 1.50, 1.75, 2.0, 2.25, 2.50, 2.75, 3.0, 3.25, 3.5, 3.75, 4.0, 4.5, 5.0, 5.5, 6.0, 6.5, 7.0, 7.5, 8.0, 8.5, 9.0, 9.5, 10.0, 10.5, 11.0, 11.5, 12.0, 12.5, 13.0, 13.5, 14.0 ] # Gather the specified SSP spectra if (ages == 'all') & (metals == 'all'): emiles = glob.glob( file_dir + '/emiles_models/Pa00/EMILES_PADOVA00_BASE_CH_FITS/*.fits') elif (ages == 'all') & np.isscalar(metals): if metals in metals_avail: if metals < 0: mstr = 'm' + str(metals) else: mstr = 'p' + str(metals) emiles = glob.glob( file_dir + '/emiles_models/Pa00/EMILES_PADOVA00_BASE_CH_FITS/*' + mstr + '*.fits') else: raise ValueError('Specified metallicity not' 'part of EMILES library.') elif np.isscalar(ages) & (metals == 'all'): if ages in ages_avail: astr = '{0:07.4f}'.format(ages) emiles = glob.glob( file_dir + '/emiles_models/Pa00/EMILES_PADOVA00_BASE_CH_FITS/*T' + astr + '*.fits') else: raise ValueError('Specified SSP age not' 'part of EMILES library.') elif ages == 'all': emiles = [] for m in metals: if m in metals_avail: if m < 0: mstr = 'm' + str(metals) else: mstr = 'p' + str(metals) emiles += glob.glob( file_dir + '/emiles_models/Pa00/EMILES_PADOVA00_BASE_CH_FITS/*' + mstr + '*.fits') else: raise ValueError('M/H = {0} not in EMILES library'.format(m)) elif metals == 'all': emiles = [] for a in ages: if a in ages_avail: astr = '{0:07.4f}'.format(a) emiles += glob.glob( file_dir + '/emiles_models/Pa00/EMILES_PADOVA00_BASE_CH_FITS/*T' + astr + '*.fits') else: raise ValueError( 'SSP Age = {0} not in EMILES library'.format(a)) elif np.isscalar(ages) & np.isscalar(metals): if (ages in ages_avail) & (metals in metals_avail): if metals < 0: mstr = 'm' + str(metals) else: mstr = 'p' + str(metals) astr = '{0:07.4f}'.format(ages) emiles = glob.glob( file_dir + '/emiles_models/Pa00/EMILES_PADOVA00_BASE_CH_FITS/*Z' + mstr + '*T' + astr + '*.fits') else: raise ValueError('Check the SSP age and/or metallicity provided!') elif np.isscalar(ages): if ages in ages_avail: astr = '{0:07.4f}'.format(ages) emiles = [] for m in metals: if m in metals_avail: if m < 0: mstr = 'm' + str(m) else: mstr = 'p' + str(m) emiles += glob.glob( file_dir + '/emiles_models/Pa00/EMILES_PADOVA00_BASE_CH_FITS/*Z' + mstr + '*T' + astr + '*.fits') else: raise ValueError( 'M/H = {0} not in EMILES library'.format(m)) else: raise ValueError('Specified SSP age not in EMILES library.') elif np.isscalar(metals): if metals in metals_avail: emiles = [] if metals < 0: mstr = 'm' + str(metals) else: mstr = 'p' + str(metals) for a in ages: if a in ages_avail: astr = '{0:07.4f}'.format(a) emiles.append( file_dir + '/emiles_models/Pa00/EMILES_PADOVA00_BASE_CH_FITS/Ech1.30Z' + mstr + 'T' + astr + '_iTp0.00_baseFe.fits') else: raise ValueError( 'SSP Age = {0} not in EMILES library'.format(a)) else: raise ValueError('M/H = {0} not in EMILES library'.format(metals)) else: emiles = [] for a in ages: if a in ages_avail: astr = '{0:07.4f}'.format(a) else: raise ValueError( 'SSP Age = {0} not in EMILES library'.format(a)) for m in metals: if m in metals_avail: if m < 0: mstr = 'm' + str(m) else: mstr = 'p' + str(m) else: raise ValueError( 'M/H = {0} not in EMILES library'.format(m)) emiles.append( file_dir + '/emiles_models/Pa00/EMILES_PADOVA00_BASE_CH_FITS/Ech1.30Z' + mstr + 'T' + astr + '_iTp0.00_baseFe.fits') print('Using {0} stellar templates.'.format(len(emiles))) # Extract the wavelength range and logarithmically rebin one spectrum # to a velocity scale equal to the galaxy spectrum, to determine # the size needed for the array which will contain the template spectra. spec0 = fits.getdata(emiles[0]) header = fits.getheader(emiles[0]) lam_emiles = (np.arange(header['NAXIS1']) * header['CDELT1'] + header['CRVAL1']) lam_emiles = lam_emiles / 1e4 # Convert from Angstrom to micron mask = (lam_emiles >= lam_range[0]) & (lam_emiles <= lam_range[1]) spec0 = spec0[mask] lam_emiles = lam_emiles[mask] spec0_new, log_lam, velscale = ppxf_util.log_rebin( [lam_emiles[0], lam_emiles[-1]], spec0, velscale=velscale) templates = np.empty((spec0_new.size, len(emiles))) # Convolve each stellar template with a Gaussian to match the instrumental resolution # of our data. Then log rebin each one and store in "templates". fwhm_emiles = 2.35482 * 60. sigma_kms = np.sqrt(fwhm_gal**2 - fwhm_emiles**2) / 2.355 sigma_pix = sigma_kms / ckms * lam_emiles / (lam_emiles[1] - lam_emiles[0]) for i, f in enumerate(emiles): hdu = fits.open(f) spec = hdu[0].data spec_cut = spec[mask] spec_conv = ppxf_util.gaussian_filter1d(spec_cut, sigma_pix) spec_new, log_lam, velscale_temp = ppxf_util.log_rebin( [lam_emiles[0], lam_emiles[-1]], spec_conv, velscale=velscale) templates[:, i] = spec_new / np.median(spec_new) # Remove any templates that are just NaNs nan_temps = np.any(np.isnan(templates), axis=0) temps = templates[:, ~nan_temps] print('Removed {0} templates.'.format(np.sum(nan_temps))) print('Now using {0} templates.'.format(temps.shape[1])) return temps, log_lam
def load_CvD_templates( FWHM_galaxy, lamRange1, velscale, z=0.0, cvd_dir=os.path.expanduser('~/z/Data/stellarpops/CvD1.2')): """ FWHM_galaxy: this is the FWHM of the galaxy spectrum in Angstoms. lamRange1: start and stop wavelength values of the galaxy spectrum z: the rough redshift """ import glob from sam_python.CvD12tools import loadCvD12spec cvd = glob.glob('{}/t*.ssp'.format(cvd_dir)) cvd.sort() #CvD Templates are at resolution 2000, so work out lamda/R for the middle wavelength in your array FWHM_tem = np.median(lamRange1 * (1 + z)) / 2000 #FIXME #This depends on where in the wavelength range you are. For SWIFT: #9300A- 3.44A #10120A- 4.55A #8700- 4.04 #To get a more accurate value for a certain lamda, fit a gaussian to a skyline! #Use Simon's CvDTools function to read in the CvD models and get them into proper units cvd_data = loadCvD12spec(cvd[0]) #They're returned in Ryan's spectrum class. spec.lam is wavelengths, spec.flam is flux in lamda units lams = cvd_data.lam lamRange2 = np.array([lams[0], lams[-1]]) cdelt = lams[10] - lams[9] FWHM_dif = np.sqrt((FWHM_galaxy**2 - FWHM_tem**2).clip(0)) sigma = FWHM_dif / 2.355 / cdelt # Sigma difference in pixels #Log Rebin one spectrum to get the length of the templates array right ssp = cvd_data.flam[0] sspNew, logLam2, velscale = util.log_rebin(lamRange2, ssp, velscale=velscale) t_num = 63 #There are 63 spectra in the CvD models templates = np.empty((len(sspNew), t_num)) #Do the same for all the models for j, filename in enumerate(cvd): cvd_data = loadCvD12spec(filename) print(filename) for ssp in cvd_data.flam: #print(ssp) ssp = ndimage.gaussian_filter1d(ssp, sigma) sspNew, logLam2, velscale = util.log_rebin(lamRange2, ssp, velscale=velscale) templates[:, j] = sspNew / np.median(sspNew) # Normalizes templates return templates, logLam2, lamRange2
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_cube(cube, lam_gal, fwhm_gal, z=0, velscale=None, noise_cube=None, velscale_ratio=1, ages='all', metals='all', lam_range_min_temp=1.4, lam_range_max_temp=2.5, dv_mask=800., mask_h2_lines=True, mask_ionized_lines=True, mask_telluric=False, mask_broad_bry=False, mask_stellar_bry=False, mask_hband_bry=False, velocity_guess=0., dispersion_guess=100., add_poly_deg=4, smooth=False, parallel=False, ncores=None): """ Fit an entire IFU cube :param cube: data cube to be fit :param lam_gal: wavelength array of the data cube :param fwhm_gal: instrumental resolution of the data in km/s :param z: redshift of the data :param velscale: optional, velocity scale to rebin to :param noise_cube: optional, error cube :param velscale_ratio: optional, velocity scale ratio to determine the velocity scale to rebin the templates :param ages: optional, which stellar age templates to use. Default is 'all' :param metals: optional, which metallicity templates to use. Default is 'all' :param lam_range_min_temp: optional, minimum wavelength of the template spectra. Default is 1.4 micron. :param lam_range_max_temp: optional, maximum wavelength of the template spectra. Default is 2.5 micron. :param dv_mask: optional, Width of mask to apply to emission lines in km/s. Default is 800 km/s. :param mask_h2_lines: optional, Whether to mask all H2 emission lines. Default is True. :param mask_ionized_lines: optional, Whether to mask all ionized gas lines. Default is True. :param mask_telluric: optional, Whether to mask regions with strong telluric residuals. Default is False. :param mask_broad_bry: optional, Whether to mask a broad Bry component. Default is False. :param mask_stellar_bry: optional, Whether to mask Bry feature from telluric star. Default is False. :param mask_hband_bry: optional, Whether to mask H-band Bry emission lines. Default is False. :param velocity_guess: optional, Initial guess for the stellar velocity. Default is 0 km/s. :param dispersion_guess: optional, Initial guess for the stellar velocity dispersion. Default is 100 km/s. :param add_poly_deg: optional, Degree of additive polynomial. Default is 4. :param smooth: optional, Whether to smooth the data cube to the template resolution before fitting. Default is False. :param parallel: optional, Whether to use parallel processing. Default is False. :param ncores: optional, If using parallel processing, how many CPUS to use. Default is None. :return: result: Very large list of the fitting results of each spaxel waves: The rebinned wavelength array Notes ----- Use construct_fit_products(result, cube.shape) to reconstruct the fit products into maps and cubes. """ # Grab a single spectrum from the cube to setup the wavelength scale test_spec = cube[:, 0, 0] test_spec_new, loglam_gal, velscale = ppxf_util.log_rebin( [lam_gal[0], lam_gal[-1]], test_spec, velscale=velscale) # Setup the stellar template spectra lam_range_temp = [lam_range_min_temp, lam_range_max_temp] if not smooth: temps, loglam_temp = create_emiles_templates(lam_range_temp, fwhm_gal, velscale / velscale_ratio, ages, metals) else: temps, loglam_temp = create_emiles_templates(lam_range_temp, 2.35482 * 60., velscale / velscale_ratio, ages, metals) # Setup the parameters for the PPXF fit dv, gp, start = setup_fit(loglam_gal, loglam_temp, z=z, dv_mask=dv_mask, velscale_ratio=velscale_ratio, mask_h2_lines=mask_h2_lines, mask_ionized_lines=mask_ionized_lines, mask_broad_bry=mask_broad_bry, mask_stellar_bry=mask_stellar_bry, mask_hband_bry=mask_hband_bry, velocity_guess=velocity_guess, dispersion_guess=dispersion_guess, smooth=smooth, mask_telluric=mask_telluric) # If smoothing the cube spectra, setup the smoothing lengths per pixel if smooth: fwhm_emiles = 60. * 2.35482 sigma_kms = np.sqrt(fwhm_emiles**2 - fwhm_gal**2) / 2.35482 sigma_pix = sigma_kms / ckms * lam_gal / (lam_gal[1] - lam_gal[0]) else: sigma_pix = None # Fit each of the spectra in the cube using parallel processing, if requested nrows = cube.shape[1] ncolumns = cube.shape[2] ind = np.indices((nrows, ncolumns)) xx = ind[0].ravel() yy = ind[1].ravel() pixels = [(xx[i], yy[i]) for i in range(len(xx))] if parallel: import multiprocessing if ncores is None: ncores = multiprocessing.cpu_count() pool = multiprocessing.Pool(ncores) if noise_cube is not None: result = [ (p, pool.apply_async(fit_single_spectrum, args=(lam_gal, cube[:, p[0], p[1]], temps, None, start, velscale_ratio, noise_cube[:, p[0], p[1]], gp, dv, add_poly_deg, smooth, sigma_pix))) for p in pixels ] else: result = [ (p, pool.apply_async(fit_single_spectrum, args=(lam_gal, cube[:, p[0], p[1]], temps, None, start, velscale_ratio, None, gp, dv, add_poly_deg, smooth, sigma_pix))) for p in pixels ] result = [(p[0], p[1].get()) for p in result] pool.close() else: if noise_cube is not None: result = [(p, fit_single_spectrum(lam_gal, cube[:, p[0], p[1]], temps, None, start, velscale_ratio, noise_cube[:, p[0], p[1]], gp, dv, add_poly_deg, smooth, sigma_pix)) for p in pixels] else: result = [(p, fit_single_spectrum(lam_gal, cube[:, p[0], p[1]], temps, None, start, velscale_ratio, None, gp, dv, add_poly_deg, smooth, sigma_pix)) for p in pixels] return result, np.exp(loglam_gal)
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 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 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))