def test_snr_normalization(desired_snr, band, testing_spectrum): """Test SNR after normalizing function is the desired value. Testing on middle of J band. """ wav, flux = testing_spectrum band_mid = utils.band_middle(band) # Searching for the closest index to 1.25 index_reference = np.searchsorted(wav, [band_mid])[0] snr_estimate = np.sqrt( np.sum(flux[index_reference - 1:index_reference + 2])) assert round(snr_estimate, 1) != desired_snr # Assert SNR is not correct norm_const = snrnorm.snr_constant_band(wav, flux, snr=desired_snr, band=band) new_flux = flux / norm_const new_snr_estimate = np.sqrt( np.sum(new_flux[index_reference - 1:index_reference + 2])) assert round(new_snr_estimate, 0) == desired_snr
def test_snr_normalization_constant(desired_snr, band, testing_spectrum): """Test snr_constant_band and snr_constant_wav produce same result.""" wav, flux = testing_spectrum band_mid = utils.band_middle(band) assert snrnorm.snr_constant_band( wav, flux, band=band, snr=desired_snr ) == snrnorm.snr_constant_wav(wav, flux, band_mid, snr=desired_snr)
def test_band_middle(band): """Test band middle is middle of band. Some repeated coding. """ lower, upper = utils.band_limits(band) middle = utils.band_middle(band) assert lower < middle assert middle < upper assert (lower + upper) / 2 == middle
def snr_constant_band( wav: ndarray, flux: ndarray, snr: Union[int, float] = 100, band: str = "J", sampling: Union[int, float] = 3.0, verbose: bool = False, ) -> float: """Determine the normalization constant to achieve a SNR in the middle of a given band. SNR estimated by the square root of the number of photons in a resolution element. Parameters ---------- wav: ndarray Wavelength array in microns. flux: ndarray Photon flux array (photons/s/cm**2). snr: int or float SNR per resolution element to achieve. Default is 100. band: str Band to use for normalization. Default is "J". sampling: int or float Number of pixels per resolution element. Default is 3. verbose: bool Enable verbose. Default is False. Returns ------- norm_value: float Normalization value to divide spectrum by to achieve a signal-to-noise level of snr within an resolution element in the middle of the band. Note ---- This is a wrapper around `snr_constant_wav`, using the band center. Warning ------- Wavelength is expected in microns! """ band_middle = utils.band_middle(band) if not (wav[0] < band_middle < wav[-1]): raise ValueError("Band center not in wavelength range.") return snr_constant_wav(wav, flux, wav_ref=band_middle, snr=snr, sampling=sampling, verbose=verbose)
def test_snr_constant_band_returns_mid_value_const(band): size = 100 np.random.seed(40) flux = 500 * np.random.rand( size ) # To give a random spectrum (but consistent between tests) lim = utils.band_limits(band) wav = np.linspace(lim[0], lim[1], size) band_const = snrnorm.snr_constant_band(wav, flux, band=band) wav_const = snrnorm.snr_constant_wav(wav, flux, wav_ref=utils.band_middle(band)) assert isinstance(band_const, float) assert isinstance(wav_const, float) assert band_const == wav_const # Since band calls wave at midpoint
def snr_constant_band( wav: ndarray, flux: ndarray, snr: Union[int, float] = 100, band: str = "J", sampling: Union[int, float] = 3.0, ) -> float: """Determine the normalization constant to achieve a SNR in the middle of a given band. SNR estimated by the square root of the number of photons in a resolution element. Parameters ---------- wav: ndarray Wavelength array (microns) flux: ndarray Photon flux array (photons/s/cm**2) snr: int, default = 100 SNR to normalize to. band: str, default = "J" Band to use for normalization. sampling: int or float Number of pixels per resolution element. Returns ------- normalization_value: float Normalization value to divide spectrum by to achieve a signal-to-noise level of snr within an resolution element in the middle of the band. Note ---- If sampling is a float it will rounded to the nearest integer for indexing. """ band_middle = utils.band_middle(band) if not (wav[0] < band_middle < wav[-1]): raise ValueError("Band center not in wavelength range.") norm_constant = snr_constant_wav( wav, flux, wav_ref=band_middle, snr=snr, sampling=sampling ) return norm_constant
def test_band_midpoint_j(): """Middle of J is 1.25 microns.""" assert utils.band_middle("J") == 1.25
def do_analysis( star_params, vsini: float, R: float, band: str, sampling: float = 3.0, conv_kwargs=None, snr: float = 100.0, ref_band: str = "J", rv: float = 0.0, air: bool = False, model: str = "aces", verbose: bool = False, ) -> Tuple[Quantity, ...]: """Calculate RV precision and Quality for specific parameter set. Parameters ---------- star_param: Stellar parameters [temp, logg, feh, alpha] for phoenix model libraries. vsini: float Stellar equatorial rotation. R: float Instrumental resolution. band: str Spectral band. sampling: float (default=False) Per pixel sampling (after convolutions) conv_kwargs: Dict (default=None) Arguments specific for the convolutions, 'epsilon', 'fwhm_lim', 'num_procs', 'normalize', 'verbose'. snr: float (default=100) SNR normalization level. SNR per pixel and the center of the ref_band. ref_band: str (default="J") Reference band for SNR normalization. rv: float Radial velocity in km/s (default = 0.0). air: bool Get model in air wavelengths (default=False). model: str Name of synthetic library (aces, btsettl) to use. Default = 'aces'. verbose: Enable verbose (default=False). Returns ------- q: astropy.Quality Spectral quality. result_1: astropy.Quality RV precision under condition 1. result_2 : astropy.Quality RV precision under condition 2. result_3: astropy.Quality RV precision under condition 3. Notes ----- We apply the radial velocity doppler shift after - convolution (rotation and resolution) - resampling - SNR normalization. in this way the RV only effects the precision due to the telluric mask interaction. Physically the RV should be applied between the rotational and instrumental convolution but we assume this effect is negligible. """ if conv_kwargs is None: conv_kwargs = { "epsilon": 0.6, "fwhm_lim": 5.0, "num_procs": num_cpu_minus_1, "normalize": True, "verbose": verbose, } if ref_band.upper() == "SELF": ref_band = band model = check_model(model) if model == "aces": wav, flux = load_aces_spectrum(star_params, photons=True, air=air) elif model == "btsettl": wav, flux = load_btsettl_spectrum(star_params, photons=True, air=air) else: raise Exception("Invalid model name reached.") wav_grid, sampled_flux = convolve_and_resample(wav, flux, vsini, R, band, sampling, **conv_kwargs) # Doppler shift try: if rv != 0: sampled_flux = doppler_shift_flux(wav_grid, sampled_flux, vel=rv) except Exception as e: print("Doppler shift was unsuccessful") raise e # Scale normalization for precision wav_ref, sampled_ref = convolve_and_resample(wav, flux, vsini, R, ref_band, sampling, **conv_kwargs) snr_normalize = snr_constant_band(wav_ref, sampled_ref, snr=snr, band=ref_band, sampling=sampling, verbose=verbose) sampled_flux = sampled_flux / snr_normalize if (ref_band == band) and verbose: mid_point = band_middle(ref_band) index_ref = np.searchsorted( wav_grid, mid_point) # searching for the index closer to 1.25 micron snr_estimate = np.sqrt( np.sum(sampled_flux[index_ref - 1:index_ref + 2])) print( "\tSanity Check: The S/N at {0:4.02} micron = {1:4.2f}, (should be {2:g})." .format(mid_point, snr_estimate, snr)) # Load Atmosphere for this band. atm = Atmosphere.from_band(band=band, bary=True).at(wav_grid) assert np.allclose(atm.wl, wav_grid), "The atmosphere does not cover the wav_grid" # Spectral Quality/Precision q = quality(wav_grid, sampled_flux) # Precision given by the first condition: result_1 = rv_precision(wav_grid, sampled_flux, mask=None) # Precision as given by the second condition result_2 = rv_precision(wav_grid, sampled_flux, mask=atm.mask) # Precision as given by the third condition: M = T**2 result_3 = rv_precision(wav_grid, sampled_flux, mask=atm.transmission**2) # Turn quality back into a Quantity (to give it a .value method) q = q * u.dimensionless_unscaled return q, result_1, result_2, result_3
def do_analysis( star_params, vsini: float, R: float, band: str, sampling: float = 3.0, conv_kwargs=None, snr: float = 100.0, ref_band: str = "J", rv: float = 0.0, air: bool = False, model="phoenix", ): """Precision and Quality for specific parameter set. Parameters ---------- air: bool Get model in air wavelengths. model: str Name of synthetic library to use. (phoenix, btsettl). rv: float Radial velocity. Notes: We apply the radial velocity doppler shift after - convolution (rotation and resolution) - resampling - SNR normalization. in this way the RV only effects the precision due to the telluric mask interaction. The RV should maybe come between the rotational and instrumental convolution but we assume this effect is negligible. """ if conv_kwargs is None: conv_kwargs = { "epsilon": 0.6, "fwhm_lim": 5.0, "num_procs": num_procs_minus_1, "normalize": True, } if ref_band.upper() == "SELF": ref_band = band if model == "phoenix": # Full photon count spectrum wav, flux = load_aces_spectrum(star_params, photons=True, air=air) elif model == "btsettl": wav, flux = load_btsettl_spectrum(star_params, photons=True, air=air) else: raise ValueError( "Model name error in '{}'. Valid choices are 'phoenix and 'btsettl'" .format(model)) wav_grid, sampled_flux = convolve_and_resample(wav, flux, vsini, R, band, sampling, **conv_kwargs) # Doppler shift try: if rv != 0: sampled_flux = doppler_shift_flux(wav_grid, sampled_flux, vel=rv) except Exception as e: print("Doppler shift was unsuccessful") raise e # Spectral Quality q = quality(wav_grid, sampled_flux) # Scale normalization for precision wav_ref, sampled_ref = convolve_and_resample(wav, flux, vsini, R, ref_band, sampling, **conv_kwargs) snr_normalize = snr_constant_band(wav_ref, sampled_ref, snr=snr, band=ref_band, sampling=sampling) sampled_flux = sampled_flux / snr_normalize if ref_band == band: mid_point = band_middle(ref_band) index_ref = np.searchsorted( wav_grid, mid_point) # searching for the index closer to 1.25 micron snr_estimate = np.sqrt( np.sum(sampled_flux[index_ref - 1:index_ref + 2])) print( "\tSanity Check: The S/N at {0:4.02} micron = {1:4.2f}, (should be {2:g})." .format(mid_point, snr_estimate, snr)) # Load Atmosphere for this band. atm = Atmosphere.from_band(band=band, bary=True).at(wav_grid) assert np.allclose(atm.wl, wav_grid), "The atmosphere does not cover the wav_grid" # Spectral Quality/Precision q = quality(wav_grid, sampled_flux) # Precision given by the first condition: prec1 = rv_precision(wav_grid, sampled_flux, mask=None) # Precision as given by the second condition prec2 = rv_precision(wav_grid, sampled_flux, mask=atm.mask) # Precision as given by the third condition: M = T**2 prec3 = rv_precision(wav_grid, sampled_flux, mask=atm.transmission**2) # Turn quality back into a Quantity (to give it a .value method) q = q * u.dimensionless_unscaled return [q, prec1, prec2, prec3]