Source code for eniric.precision

"""Calculate the Radial Velocity Precision of NIR Spectra.

    Notes
    -----
    Extract from https://arxiv.org/pdf/1511.07468v1.pdf
    From Eq. (11) and (12) of
    https://arxiv.org/pdf/1511.07468v1.pdf#cite.2001A%26A...374..733B
    it follows that the RV uncertainty associated with the information content
    of a given spectra is given by

        RV_rms = c /(Q *Sum{Ne}) = c / (sqrt{sum{W(i)}})

    in which c is the speed of light in vacuum, Q the quality factor of a
    spectrum, and N_e the total number of photoelectrons collected inside the
    wavelength range of interest. However, the precision can only be calculated
    using the concept of optimal pixel weight W(i) for each of the pixels i
    that compose the spectra.

        W(i) = lambda(i)**2 (d'A_0(i) / d'lambda(i))**2 / (A_0(i) + sigma_D**2)

    in which lambda(i) and A_0(i) are the values of each pixel wave-length and
    flux, respectively. The weight will be proportional to the information
    content of the spectrum, given by the derivative of the amplitude, and
    calculated following Connes (1985).
    The denominator of the previous equation is the variance of the flux of
    the pixel A_0(i), depending on the flux value itself and on the
    detector noise sigma_D. In this paper we exclusively consider the high
    signal-to-noise ratio regime, so we can approximate A_0(i) + sigma_D**2
    to A_0(i).

    Spectral Quality:
        Q = sqrt{sum{W(i)}} / sqrt{sum{A_0{i}}

    The spectral quality, Q, is independent of the flux level and is only
    a function of the spectral profile.

    Notes
    -----
    Extract from https://arxiv.org/pdf/1511.07468v1.pdf

    The precision can only be calculated using the concept of
    optimal pixel weight W(i) for each of the pixels i that compose the spectra.

        W(i) = lambda(i)**2 (d'A_0(i) / d'lambda(i))**2 / (A_0(i) + sigma_D**2)

    in which lambda(i) and A_0(i) are the values of each pixel wave-length and
    flux, respectively. The weight will be proportional to the information
    content of the spectrum, given by the derivative of the amplitude, and
    calculated following Connes (1985).

"""
import warnings
from typing import Optional, Tuple, Union

import astropy.units as u
import numpy as np
from astropy import constants as const
from astropy.units.quantity import Quantity
from numpy import ndarray

from eniric.resample import log_chunks

c = const.c


[docs]def rv_precision( wavelength: Union[Quantity, ndarray], flux: Union[Quantity, ndarray], mask: Optional[ndarray] = None, **kwargs, ) -> Quantity: """Calculate the theoretical RV precision achievable on a spectrum. Parameters ---------- wavelength: array-like or Quantity Wavelength of spectrum. flux: array-like or Quantity Flux of spectrum. mask: array-like, Quantity or None Masking function array to apply to the pixel weights. kwargs: Kwargs for sqrt_sum_wis Returns ------- RVrms: astropy.Quantity Radial velocity precision of spectra in m/s. """ return c / sqrt_sum_wis(wavelength, flux, mask=mask, **kwargs)
[docs]def quality( wavelength: Union[Quantity, ndarray], flux: Union[Quantity, ndarray], mask: Optional[ndarray] = None, **kwargs, ) -> float: """Calculation of the spectral Quality, Q, for a spectrum. ``Q = sqrt{sum{W(i)}} / sqrt{sum{A_0{i}}`` The spectral quality, Q, is independent of the flux level and is only a function of the spectral profile. Parameters ---------- wavelength: array-like or Quantity Wavelength of spectrum. flux: array-like or Quantity Flux of spectrum. mask: array-like, Quantity or None Masking function array to apply to the pixel weights. kwargs: Kwargs for sqrt_sum_wis (including mask). Returns ------- q: float Spectral quality. """ flux = flux * u.dimensionless_unscaled # Turn into Quantity if not already flux = flux / flux.unit # Remove units from flux (sqrt(N_e) is unitless) wis = sqrt_sum_wis(wavelength, flux, mask=mask, **kwargs) q = wis / np.sqrt(np.nansum(flux)) return q.value
def sqrt_sum_wis( wavelength: Union[Quantity, ndarray], flux: Union[Quantity, ndarray], mask: Optional[Union[Quantity, ndarray]] = None, grad: bool = True, ) -> Union[float, Quantity]: """Calculation of the Square root of the sum of the weights(Wis) for a spectrum. Mask is used to apply a masking function to the weights (to mask out telluric lines for example). W(i) = W(i) * m(i) Parameters ---------- wavelength: array-like or Quantity Wavelength of spectrum. flux: array-like or Quantity Flux of spectrum. mask: array-like, Quantity or None Weighting mask function. Default is all ones. grad: bool Flag to use np.gradient. The original publication used a less precise method. Default=True. Returns ------- sqrt_sum_wis: float or astropy.Quantity Square root of the sum of the pixel weights (Wis). """ if mask is None: # Don't use np.ones_like() as it will take units of a Quantity. mask = np.ones(len(wavelength)) mask_check(mask) pixel_wis = pixel_weights(wavelength, flux, grad=grad) # Apply masking function if grad: masked_wis = pixel_wis * mask else: masked_wis = pixel_wis * mask[:-1] sqrt_sum = np.sqrt(np.nansum(masked_wis)) if not np.isfinite(sqrt_sum): warnings.warn("Weight sum is not finite = {}".format(sqrt_sum)) if sqrt_sum == 0: warnings.warn( "Sum of weights sum is = {}. This will cause infinite errors.".format( sqrt_sum ) ) return sqrt_sum def mask_check(mask): """Checks for mask array.""" if isinstance(mask, u.Quantity): if not (mask.unit == u.dimensionless_unscaled): raise TypeError( "Mask should not be a non-dimensionless and unscaled Quantity!" ) mask = mask.value # Check for values of mask if np.any(mask > 1) or np.any(mask < 0): raise ValueError("Mask should be within range from 0 to 1 only.") def slope(wavelength: Union[ndarray, Quantity], flux: Union[ndarray, Quantity]): """Forward Finite difference derivative which loses one value of array. f' = (f(x+h)-f(x)) / h. f'[i] = (flux[i+1] - flux[i])/ (wavelength[i+1] - wavelength[i]) Parameters ---------- wavelength: numpy.ndarray or astropy.Quantty Wavelength array. flux: numpy.ndarray or astropy.Quantty Flux array. Returns ------- ffd: numpy.ndarray FFD slope of spectrum with n-1 points. """ ffd = np.diff(flux) / np.diff(wavelength) return ffd def pixel_weights( wavelength: Union[ndarray, Quantity], flux: Union[ndarray, Quantity], grad: bool = True, ): r"""Calculate individual pixel weights. w(i) = \lambda(i)^2 (\partial A(i)/\partial\lambda)^2 / A(i) Parameters ---------- wavelength: Union[ndarray, Quantity] Wavelength array. flux: Union[ndarray, Quantity] Flux array. grad: bool Toggle function for spectral slope. Default False + forward finite difference. Returns ------- wis: ndarray Array of pixel weigths. """ if isinstance(flux, u.Quantity): # The units of variance are squared. flux_variance = flux.value * flux.unit * flux.unit else: flux_variance = flux dydx_unit = 1 if grad: # Hack for quantities with numpy gradient if isinstance(flux, Quantity): dydx_unit *= flux.unit flux = flux.value if isinstance(wavelength, Quantity): dydx_unit /= wavelength.unit wave = wavelength.value else: wave = wavelength derivf_over_lambda = np.gradient(flux, wave) * dydx_unit return (wavelength * derivf_over_lambda) ** 2.0 / flux_variance else: derivf_over_lambda = slope(wavelength, flux) return (wavelength[:-1] * derivf_over_lambda) ** 2.0 / flux_variance[:-1] def incremental_quality( wavelength: ndarray, flux: ndarray, *, mask: Optional[Union[Quantity, ndarray]] = None, percent: Union[int, float] = 10, **kwargs, ) -> Tuple[ndarray, ndarray]: """Determine spectral quality in incremental sections. Parameters ---------- wavelength: array-like or Quantity Wavelength of spectrum. flux: array-like or Quantity Flux of spectrum. mask: array-like, Quantity or None Pixel weight mask. percent: Union[int, float] (default=10) The percent size of chunk around each wavelength position. kwargs: Extra arguments passed onto quality() (including mask). Returns ------- x: ndarray Central wavelength values of each section. q: ndarray Spectral quality for each section. """ positions = log_chunks(wavelength, percent) qualities = [] for pos1, pos2 in zip(positions[:-1], positions[1:]): pos_mask = (wavelength >= pos1) & (wavelength < pos2) if np.sum(pos_mask) <= 1: # 1 or less points in this section continue x = wavelength[pos_mask] y = flux[pos_mask] if mask is not None: z = mask[pos_mask] else: z = mask # None try: q = quality(x, y, mask=z, **kwargs) except: q = np.nan qualities.append([np.nanmean(x), q]) x, q = np.asarray(qualities).T return x, q def incremental_rv( wavelength: ndarray, flux: ndarray, *, mask: Optional[Union[Quantity, ndarray]] = None, percent: float = 10, **kwargs, ) -> Tuple[ndarray, ndarray]: """Determine spectral RV precision in incremental sections. Parameters ---------- wavelength: array-like or Quantity Wavelength of spectrum. flux: array-like or Quantity Flux of spectrum. mask: array-like, Quantity or None Pixel weight mask. percent: Union[int, float] (default=10) The percent size of chunk around each wavelength position. kwargs: Extra arguments passed onto rv_precision(). Returns ------- x: ndarray Central wavelength values of each section. rv: ndarray Spectral RV precision for each section. """ positions = log_chunks(wavelength, percent) velocities = [] for pos1, pos2 in zip(positions[:-1], positions[1:]): pos_mask = (wavelength >= pos1) & (wavelength < pos2) if np.sum(pos_mask) <= 1: # 1 or less points in this section continue x = wavelength[pos_mask] y = flux[pos_mask] if mask is not None: z = mask[pos_mask] else: z = mask # None try: rv_calc = rv_precision(x, y, mask=z, **kwargs).value except: rv_calc = np.nan velocities.append([np.nanmean(x), rv_calc]) x, rv = np.asarray(velocities).T return x, rv