def get_sun(time): """ Determines the location of the sun at a given time (or times, if the input is an array `~astropy.time.Time` object), in geocentric coordinates. Parameters ---------- time : `~astropy.time.Time` The time(s) at which to compute the location of the sun. Returns ------- newsc : `~astropy.coordinates.SkyCoord` The location of the sun as a `~astropy.coordinates.SkyCoord` in the `~astropy.coordinates.GCRS` frame. Notes ----- The algorithm for determining the sun/earth relative position is based on the simplified version of VSOP2000 that is part of ERFA. Compared to JPL's ephemeris, it should be good to about 4 km (in the Sun-Earth vector) from 1900-2100 C.E., 8 km for the 1800-2200 span, and perhaps 250 km over the 1000-3000. """ earth_pv_helio, earth_pv_bary = erfa.epv00(*get_jd12(time, 'tdb')) # We have to manually do aberration because we're outputting directly into # GCRS earth_p = earth_pv_helio['p'] earth_v = earth_pv_bary['v'] # convert barycentric velocity to units of c, but keep as array for passing in to erfa earth_v /= c.to_value(u.au/u.d) dsun = np.sqrt(np.sum(earth_p**2, axis=-1)) invlorentz = (1-np.sum(earth_v**2, axis=-1))**0.5 properdir = erfa.ab(earth_p/dsun.reshape(dsun.shape + (1,)), -earth_v, dsun, invlorentz) cartrep = CartesianRepresentation(x=-dsun*properdir[..., 0] * u.AU, y=-dsun*properdir[..., 1] * u.AU, z=-dsun*properdir[..., 2] * u.AU) return SkyCoord(cartrep, frame=GCRS(obstime=time))
def get_sun(time): """ Determines the location of the sun at a given time (or times, if the input is an array `~astropy.time.Time` object), in geocentric coordinates. Parameters ---------- time : `~astropy.time.Time` The time(s) at which to compute the location of the sun. Returns ------- newsc : `~astropy.coordinates.SkyCoord` The location of the sun as a `~astropy.coordinates.SkyCoord` in the `~astropy.coordinates.GCRS` frame. Notes ----- The algorithm for determining the sun/earth relative position is based on the simplified version of VSOP2000 that is part of ERFA. Compared to JPL's ephemeris, it should be good to about 4 km (in the Sun-Earth vector) from 1900-2100 C.E., 8 km for the 1800-2200 span, and perhaps 250 km over the 1000-3000. """ earth_pv_helio, earth_pv_bary = erfa.epv00(*get_jd12(time, 'tdb')) # We have to manually do aberration because we're outputting directly into # GCRS earth_p = earth_pv_helio['p'] earth_v = earth_pv_bary['v'] # convert barycentric velocity to units of c, but keep as array for passing in to erfa earth_v /= c.to_value(u.au / u.d) dsun = np.sqrt(np.sum(earth_p**2, axis=-1)) invlorentz = (1 - np.sum(earth_v**2, axis=-1))**0.5 properdir = erfa.ab(earth_p / dsun.reshape(dsun.shape + (1, )), -earth_v, dsun, invlorentz) cartrep = CartesianRepresentation(x=-dsun * properdir[..., 0] * u.AU, y=-dsun * properdir[..., 1] * u.AU, z=-dsun * properdir[..., 2] * u.AU) return SkyCoord(cartrep, frame=GCRS(obstime=time))
def calc_Vpix(nu_obs, nu_emit, pixel_nu, survey, cosmo): assert survey in ['TIME', 'CONCERTO', 'CCAT-p'], "Survey not recognized!" totaldeg = 4. * np.pi * (180. / np.pi)**2 z = (nu_emit - nu_obs) / nu_obs if survey == 'CCAT-p': thetabeam = 53. / 3600. omegabeam = 2. * np.pi * (thetabeam / 2.355)**2 else: D = 12 # m line = c.to_value(u.m * u.GHz) / nu_obs thetabeam = 1.22 * line / D thetabeam *= (180. / np.pi) omegabeam = 2. * np.pi * (thetabeam / 2.355)**2 dup_pix = comoving_distance_at_freq(nu_obs - pixel_nu / 2, nu_emit, cosmo) dlow_pix = comoving_distance_at_freq(nu_obs + pixel_nu / 2, nu_emit, cosmo) Vpix = (omegabeam / totaldeg) * (4. * np.pi / 3.) * (dup_pix**3 - dlow_pix**3) return Vpix
import astropy.units as apu from astropy.constants import h, c, u import numpy as np one_arcsec = 1.0/3600.0 erg_per_eV = apu.eV.to("erg") erg_per_keV = erg_per_eV * 1.0e3 keV_per_erg = 1.0 / erg_per_keV eV_per_erg = 1.0 / erg_per_eV hc = (h*c).to("keV*angstrom").value clight = c.to("cm/s").value ckms = c.to_value("km/s") sigma_to_fwhm = 2.*np.sqrt(2.*np.log(2.)) sqrt2pi = np.sqrt(2.*np.pi) m_u = u.to("g").value elem_names = ["", "H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne", "Na", "Mg", "Al", "Si", "P", "S", "Cl", "Ar", "K", "Ca", "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn"] cosmic_elem = [1, 2, 3, 4, 5, 9, 11, 15, 17, 19, 21, 22, 23, 24, 25, 27, 29, 30] metal_elem = [6, 7, 8, 10, 12, 13, 14, 16, 18, 20, 26, 28] atomic_weights = np.array([0.0, 1.00794, 4.00262, 6.941, 9.012182, 10.811, 12.0107, 14.0067, 15.9994, 18.9984, 20.1797,
def line_width_equiv(rest): from astropy.constants import c ckms = c.to_value('km/s') forward = lambda x: rest * x / ckms backward = lambda x: x / rest * ckms return [(u.km / u.s, u.keV, forward, backward)]
'3-2': 866, '4-3': 651, '5-4': 521, '6-5': 434, '7-6': 372, '8-7': 325, '9-8': 289, '10-9': 260, '11-10': 237, '12-11': 217, '13-12': 200, 'CII': 157.7 } # Convert CO wavelengths to GHz speed_of_light = c.to_value(u.micron * u.GHz) CO_lines = {key: speed_of_light / wave for key, wave in CO_lines_wave.items()} n = 111.52 CO_lines_LT16 = { '1-0': n, '2-1': 2 * n, '3-2': 3 * n, '4-3': 4 * n, '5-4': 5 * n, '6-5': 6 * n, '7-6': 7 * n, '8-7': 8 * n, '9-8': 9 * n, '10-9': 10 * n,
import astropy.units as apu from astropy.constants import h, c, u import numpy as np one_arcsec = 1.0 / 3600.0 erg_per_eV = apu.eV.to("erg") erg_per_keV = erg_per_eV * 1.0e3 keV_per_erg = 1.0 / erg_per_keV eV_per_erg = 1.0 / erg_per_eV hc = (h * c).to("keV*angstrom").value clight = c.to("cm/s").value ckms = c.to_value("km/s") sigma_to_fwhm = 2. * np.sqrt(2. * np.log(2.)) sqrt2pi = np.sqrt(2. * np.pi) m_u = u.to("g").value elem_names = [ "", "H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne", "Na", "Mg", "Al", "Si", "P", "S", "Cl", "Ar", "K", "Ca", "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn" ] cosmic_elem = [ 1, 2, 3, 4, 5, 9, 11, 15, 17, 19, 21, 22, 23, 24, 25, 27, 29, 30 ] metal_elem = [6, 7, 8, 10, 12, 13, 14, 16, 18, 20, 26, 28]
def run(self, spectra, cross_correlation_reference): max_nsysrem = self.max_sysrem_iterations max_nsysrem_after = self.max_sysrem_iterations_afterwards rv_range = self.rv_range rv_points = self.rv_points rv_step = 2 * rv_range / rv_points skip = self.skip_segments skip_mask = np.full(spectra.shape[1], True) for seg in skip: skip_mask[spectra.segments[seg] : spectra.segments[seg + 1]] = False # reference = cross_correlation_reference flux = spectra.flux.to_value(1) flux = flux if spectra.uncertainty is not None: unc = spectra.uncertainty.array else: unc = np.ones_like(flux) correlation = {} for n in tqdm(range(max_nsysrem), desc="Sysrem N"): corrected_flux = sysrem(flux, num_errors=n, errors=unc) # Normalize by the standard deviation in this wavelength column std = np.nanstd(corrected_flux, axis=0) std[std == 0] = 1 corrected_flux /= std # reference_flux = np.copy(reference.flux.to_value(1)) # reference_flux -= np.nanmean(reference_flux, axis=1)[:, None] # reference_flux /= np.nanstd(reference_flux, axis=1)[:, None] wave_noshift = spectra.wavelength[0].to_value("AA") c_light = c.to_value("km/s") # Run the cross correlation for all times and radial velocity offsets corr = np.zeros((len(spectra), int(rv_points))) for i in tqdm(range(len(spectra) - 1), leave=False, desc="Observation"): for j in tqdm(range(rv_points), leave=False, desc="radial velocity",): # Doppler Shift the next spectrum rv = -rv_range + j * rv_step wave_shift = wave_noshift * (1 + rv / c_light) newspectra = np.interp( wave_shift, wave_noshift, corrected_flux[i + 1] ) # Mask bad pixels m = np.isfinite(corrected_flux[i]) m &= np.isfinite(newspectra[i + 1]) m &= skip_mask # Cross correlate! corr[i, j] += np.correlate( corrected_flux[i][m], newspectra[m], "valid", ) # Normalize to the number of data points used corr[i, j] *= m.size / np.count_nonzero(m) correlation[f"{n}"] = np.copy(corr) for i in tqdm( range(max_nsysrem_after), leave=False, desc="Sysrem on Cross Correlation", ): correlation[f"{n}.{i}"] = sysrem(np.copy(corr), i) self.save(correlation) return correlation
def line_width_equiv(rest): from astropy.constants import c ckms = c.to_value('km/s') forward = lambda x: rest*x/ckms backward = lambda x: x/rest*ckms return [(u.km/u.s, u.keV, forward, backward)]
def combine_observations(spectra: SpectrumArray): # TODO: The telluric spectrum will change between observations # and therefore influence the recovered stellar parameters # Especially when we combine data from different transits! # for i in range(spectra.shape[0]): # plt.plot(spectra.wavelength[i], spectra.flux[i], "r") # Shift to the same reference frame (telescope) print("Shift observations to the telescope restframe") spectra = spectra.shift("telescope", inplace=True) # Arbitrarily choose the central grid as the common one print("Combine all observations") wavelength = spectra.wavelength[len(spectra) // 2] spectra = spectra.resample(wavelength, inplace=True, method="linear") # Detects ouliers based on the Median absolute deviation mask = detect_ouliers(spectra) # TODO: other approach # s(i) = f(i) (1 - w / dw * g) + f(i+1) w / dw * g # g = sqrt((1 + beta) / (1 - beta)) - 1 rv = np.zeros(len(spectra)) for i in range(len(spectra)): rv[i] = spectra.reference_frame.to_barycentric(spectra.datetime[i]).to_value( "km/s" ) rv -= np.mean(rv) rv *= -1 rv /= c.to_value("km/s") rv = np.sqrt((1 + rv) / (1 - rv)) - 1 wave = np.copy(wavelength.to_value("AA")) for l, r in zip(spectra.segments[:-1], spectra.segments[1:]): wave[l:r] /= np.gradient(wave[l:r]) g = wave[None, :] * rv[:, None] # TODO: the tellurics are scaled by the airmass, which we should account for here, when creating the master stellar # TODO: Could we fit a linear polynomial to each wavelength point? as a function of time/airmass? # TODO: Then the spectrum would be constant, since there is no change, but for tellurics we would see a difference # TODO: But there is a different offset for the tellurics, and the stellar features yflux = spectra.flux.to_value(1) flux = np.zeros(yflux.shape) coeff = np.zeros((yflux.shape[1], 2)) airmass = spectra.airmass mask = ~mask for i in tqdm(range(spectra.flux.shape[1])): coeff[i] = np.polyfit(airmass[mask[:, i]], yflux[:, i][mask[:, i]], 1) flux[:, i] = np.polyval(coeff[i], airmass) # fitfunc = lambda t0, t1, f: (t0[None, :] + t1[None, :] * airmass[:, None]) * ( # f + g * np.diff(f, append=f[-2]) # ) def plotfunc(airmass, t0, t1, f, fp, g): tell = t0 + t1 * airmass[:, None] tell = np.clip(tell, 0, 1) stel = f + g * (fp - f) # np.diff(f, append=2 * f[-1] - f[-2]) obs = tell * stel return obs def fitfunc(param): t0 = 1 n = param.size // 2 t1 = param[:n] f = param[n:] fp = np.roll(f, -1) model = plotfunc(airmass, t0, t1, f, fp, g[:, l:r]) resid = model - yflux[:, l:r] return resid.ravel() def regularization(param): n = param.size // 2 t1 = param[:n] f = param[n:] d1 = np.gradient(t1) d2 = np.gradient(f) reg = np.concatenate((d1, d2)) return reg ** 2 t0 = np.ones_like(coeff[:, 1]) t1 = coeff[:, 0] / coeff[:, 1] t1[(t1 > 0.1) | (t1 < -2)] = -2 f = np.copy(coeff[:, 1]) for k in tqdm(range(1)): for l, r in tqdm( zip(spectra.segments[:-1], spectra.segments[1:]), total=spectra.nseg, leave=False, ): n = r - l # Smooth first guess mu = gaussian_filter1d(t1[l:r], 1) var = gaussian_filter1d((t1[l:r] - mu) ** 2, 11) sig = np.sqrt(var) * 80 + 0.5 sig = np.nan_to_num(sig) smax = int(np.ceil(np.nanmax(sig))) + 1 points = [t1[l:r]] + [gaussian_filter1d(t1[l:r], i) for i in range(1, smax)] smooth = RegularGridInterpolator((np.arange(smax), np.arange(n)), points)( (sig, np.arange(n)) ) t1[l:r] = smooth # plt.plot(t1[l:r]) # plt.plot(f[l:r]) # plt.show() # fold = np.copy(f[l:r]) # told = np.copy(t1[l:r]) # Bounds for the optimisation bounds = np.zeros((2, 2 * n)) bounds[0, :n], bounds[0, n:] = -2, 0 bounds[1, :n], bounds[1, n:] = 0, 1 x0 = np.concatenate((t1[l:r], f[l:r])) x0 = np.nan_to_num(x0) x0 = np.clip(x0, bounds[0], bounds[1]) res = least_squares( fitfunc, x0, method="trf", bounds=bounds, max_nfev=200, tr_solver="lsmr", tr_options={"atol": 1e-2, "btol": 1e-2}, jac_sparsity="auto", regularization=regularization, r_scale=0.2, verbose=2, diff_step=0.01, ) t0[l:r] = 1 t1[l:r] = res.x[:n] f[l:r] = res.x[n:] # lower, upper = [-2, 0, 0], [0, 1, 1] # for i in tqdm(range(l, r - 1), leave=False): # x0 = [t1[i], f[i], f[i + 1]] # x0 = np.nan_to_num(x0) # x0 = np.clip(x0, lower, upper) # res = least_squares(fitfunc, x0, method="trf", bounds=[lower, upper]) # t0[i] = 1 # t1[i], f[i] = res.x[0], res.x[1] # t0[l:r] = gaussian_filter1d(t0[l:r], 0.5) # t1[l:r] = gaussian_filter1d(t1[l:r], 0.5) # f[l:r] = gaussian_filter1d(f[l:r], 0.5) # total = 0 # for i in range(len(spectra)): # total += np.sum( # (plotfunc(airmass[i], t0, t1, f, np.roll(f, -1), g[i]) - yflux[i]) ** 2 # ) # print(total) # TODO: t0 should be 1 in theory, however it is not in practice because ? tell = t0 + t1 * airmass[:, None] tell = np.clip(tell, 0, 1) tell = tell << spectra.flux.unit # i = 10 # plt.plot(wavelength, yflux[i], label="observation") # plt.plot( # wavelength, # plotfunc(airmass[i], t0, t1, f, np.roll(f, -1), g[i]), # label="combined", # ) # plt.plot(wavelength, tell[i], label="telluric") # plt.plot(wavelength, f, label="stellar") # plt.legend() # plt.show() flux = f + g * (np.roll(f, -1, axis=0) - f) # flux = np.tile(f, (len(spectra), 1)) flux = flux << spectra.flux.unit wave = np.tile(wavelength, (len(spectra), 1)) << spectra.wavelength.unit uncs = np.nanstd(flux, axis=0) uncs = np.tile(uncs, (len(spectra), 1)) uncs = StdDevUncertainty(uncs, copy=False) spec = SpectrumArray( flux=flux, spectral_axis=wave, uncertainty=uncs, segments=spectra.segments, datetime=spectra.datetime, star=spectra.star, planet=spectra.planet, observatory_location=spectra.observatory_location, reference_frame="telescope", ) tell = SpectrumArray( flux=tell, spectral_axis=wave, uncertainty=uncs, segments=spectra.segments, datetime=spectra.datetime, star=spectra.star, planet=spectra.planet, observatory_location=spectra.observatory_location, reference_frame="telescope", ) # print("Shift observations to the telescope restframe") # spec = spec.shift("barycentric", inplace=True) # spec = spec.shift("telescope", inplace=True) spec = spec.resample(spectra.wavelength, method="linear", inplace=True) tell = tell.resample(spectra.wavelength, method="linear", inplace=True) return spec, tell