def test_both_gradients_possible(test_spec): wav = test_spec[0] flux = test_spec[1] transmission = test_spec[2] __ = rv_precision(wav, flux, mask=transmission, grad=False).value __ = rv_precision(wav, flux, mask=transmission, grad=True).value assert True
def test_sqrt_sum_wis_with_mask_with_unit_fails(test_spec, wav_unit, flux_unit, trans_unit2): """Assert a transmission with a unit fails with type error.""" wav = test_spec[0] * wav_unit flux = test_spec[1] * flux_unit transmission = np.random.rand(len(wav)) * trans_unit2 with pytest.raises(TypeError): sqrt_sum_wis(wav, flux, mask=transmission**2) with pytest.raises(TypeError): rv_precision(wav, flux, mask=transmission**2)
def test_transmission_reduces_precision(test_spec): """Check that a transmission vector reduces precision calculation.""" wav = test_spec[0] flux = test_spec[1] transmission = test_spec[2] # Value should be less then normal if trans <=1 if transmission is not None: assert rv_precision(wav, flux, mask=None) < rv_precision( wav, flux, mask=transmission) # mask=None is the same as mask of all 1. assert rv_precision(wav, flux, mask=None) == rv_precision(wav, flux, mask=np.ones_like(wav))
def RVprec_calc_weights_masked( wavelength: ndarray, flux: ndarray, mask: Optional[ndarray] = None, **kwargs ) -> Quantity: """RV precision setting weights of telluric lines to zero. Instead of splitting the spectra after every telluric line and individually calculating precision and taking the weighted average this just sets the pixel weights to zero. Parameters ---------- wavelength: ndarray Wavelength array flux: ndarray Flux array mask: array or None Mask of transmission cuts. Zero values are excluded and used to cut up the spectrum. Returns ------- RV_value: Quantity scalar RV precision. RVprec_calc_weights_masked is just a wrapper around rv_precision. """ return rv_precision(wavelength, flux, mask=mask, **kwargs)
def test_rvprev_calc_with_lists(test_spec): """Test that it can handle list input also.""" wav = list(test_spec[0]) flux = list(test_spec[1]) mask = test_spec[2] rv = rv_precision(wav, flux, mask) assert not hasattr(rv.value, "__len__") # assert value is a scalar assert isinstance(rv, u.Quantity) assert rv.unit == m_per_s
def test_relation_of_rv_to_sqrtsumwis(test_spec, wav_unit, flux_unit, trans_unit): """Test relation of sqrtsumwis to rv_precision.""" wav = test_spec[0] * wav_unit flux = test_spec[1] * flux_unit mask = test_spec[2] if mask is not None: mask *= trans_unit mask = mask ** 2 assert np.all( rv_precision(wav, flux, mask=mask) == c / sqrt_sum_wis(wav, flux, mask=mask) )
def test_rvprev_calc(test_spec, wav_unit, flux_unit, trans_unit): """Test that rv_precision can handle inputs as Quantities or unitless and returns a scalar Quantity.""" wav = test_spec[0] * wav_unit flux = test_spec[1] * flux_unit mask = test_spec[2] if test_spec[2] is not None: mask *= trans_unit rv = rv_precision(wav, flux, mask) assert rv.unit == m_per_s assert not hasattr(rv.value, "__len__") # assert value is a scalar assert isinstance(rv, u.Quantity)
def test_sqrt_sum_wis_transmission_outofbounds(test_spec, wav_unit, flux_unit): """Transmission must be within 0-1.""" wav = test_spec[0] * wav_unit flux = test_spec[1] * flux_unit mask_1 = np.random.randn(len(wav)) mask_2 = np.random.rand(len(wav)) mask_1[0] = 5 # Outside 0-1 mask_2[-1] = -2 # Outside 0-1 # Higher value with pytest.raises(ValueError): rv_precision(wav, flux, mask=mask_1) with pytest.raises(ValueError): sqrt_sum_wis(wav, flux, mask=mask_1) # Lower value with pytest.raises(ValueError): sqrt_sum_wis(wav, flux, mask=mask_2) with pytest.raises(ValueError): sqrt_sum_wis(wav, flux, mask=mask_2)
def test_increments_rv_accumulate_same_as_full(real_spec, increment_percent, no_mask): """Assuming that the weighted rv from the steps should equal the rv from the band.""" wav, flux, mask = real_spec[0], real_spec[1], real_spec[2] if no_mask: # Try with mask= None also. mask = None rv_full = rv_precision(wav, flux, mask=mask).value x, incremented_rv = incremental_rv(wav, flux, mask=mask, percent=increment_percent) incremented_weighted = weighted_error(incremented_rv) assert np.round(rv_full, 2) == np.round(incremented_weighted, 2) assert x[0] > wav[0] assert [-1] < wav[-1]
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 RVprec_calc_masked( wavelength: Union[List[List[Any]], ndarray], flux: Union[ndarray, List[List[Any]]], mask: Optional[ndarray] = None, **kwargs, ) -> Quantity: """RV precision for split apart spectra. The same as rv_precision, but now wavelength and flux are organized into chunks according to the mask and the weighted average formula is used to calculate the combined precision. When considering the average RV as delivered by several slices of a spectrum, the error on the average is given by the error on a weighted average. mean(RV_rms) = 1 / sqrt(sum_i((1 / RV_rms(i))**2)) Parameters ---------- wavelength: array-like or list(array-like) Wavelength values of chunks. flux: array-like or list(array-like) Flux values of the chunks. mask: array-like of bool or None Mask of transmission cuts. Zero values are excluded and used to cut up the spectrum. kwargs: Kwargs for sqrt_sum_wis Returns ------- RV_value: Quantity scalar Weighted average RV value of spectral chunks. Notes ----- A "clump" is defined as a contiguous region of the array. Solution for clumping comes from https://stackoverflow.com/questions/14605734/numpy-split-1d-array-of-chunks-separated-by-nans-into-a-list-of-the-chunks """ if mask is not None: # Turn wavelength and flux into masked arrays wavelength_clumps, flux_clumps = mask_clumping(wavelength, flux, mask) else: # When given a already clumped solution and no mask given. assert isinstance(wavelength, list) assert isinstance(flux, list) wavelength_clumps = wavelength flux_clumps = flux # Turn an ndarray into quantity array. # Need to use np.zeros instead of np.empty. Unassigned zeros are removed after with nonzero. # The "empty" values (1e-300) do not get removed and effect precision slice_rvs = Quantity( np.zeros(len(wavelength_clumps), dtype=float), unit=u.meter / u.second ) # Radial velocity of each slice for i, (wav_slice, flux_slice) in enumerate(zip(wavelength_clumps, flux_clumps)): if len(wav_slice) == 1: # Results in infinite rv, can not determine the slope of single point. continue else: wav_slice = np.asarray(wav_slice) flux_slice = np.asarray(flux_slice) slice_rvs[i] = rv_precision(wav_slice, flux_slice, **kwargs) # Zeros created from the initial empty array, when skipping single element chunks) slice_rvs = slice_rvs[np.nonzero(slice_rvs)] # Only use nonzero values. return 1.0 / (np.sqrt(np.nansum((1.0 / slice_rvs) ** 2.0)))