Source code for gadfly.gp


import numpy as np

import astropy.units as u
from astropy.units import cds  # noqa
from astropy.time import Time

from celerite2 import GaussianProcess as CeleriteGaussianProcess

__all__ = ['GaussianProcess']


[docs]class GaussianProcess(CeleriteGaussianProcess): """ The ``gadfly`` interface to the celerite2 Gaussian Process (GP) solver. This subclass of the :py:class:`~celerite2.GaussianProcess` class in ``celerite2`` knows how to handle :py:mod:`~astropy.units`, for ease of use in ``gadfly``. """ def __init__(self, kernel, t=None, mean=0.0, light_curve=None, **kwargs): """ Parameters ---------- kernel : Subclass of :py:class:`~celerite2.terms.Term` ``celerite2``-compatible kernel. Could be a :py:class:`~gadfly.SolarOscillatorKernel` or :py:class:`~gadfly.StellarOscillatorKernel`, for example. t : ~astropy.units.Quantity or ~astropy.time.Time mean : float or callable The mean function of the process. This can either be a callable (it will be evaluated with a single argument, a vector of ``x`` values) or a scalar. (default: ``0.0``) light_curve : ~lightkurve.LightCurve The light curve on which predictions will be computed kwargs : dict Other arguments will be passed directly to :py:meth:`~gadfly.GaussianProcess.compute` if the argument ``t`` is specified. """ self._original_flux_median = None if t is not None: # convert the times into units compatible with the gadfly parameterization: t = self._time_to_freq(t) if light_curve is not None: t = self._time_to_freq(light_curve.time) if hasattr(light_curve.flux, 'unmasked'): # for compatibility with astropy v5.2 median_flux = np.nanmedian(light_curve.flux.unmasked) else: median_flux = np.median(light_curve.flux) self._original_flux_median = median_flux kwargs['yerr'] = self._flux_to_ppm(light_curve.flux_err, is_error=True) super().__init__(kernel, t=t, mean=mean, **kwargs) @staticmethod def _time_to_freq(time, freq_unit=u.uHz): """ Convert times into units compatible with the parameterization of kernel hyperparameters in gadfly. Parameters ---------- time : ~astropy.units.Quantity, ~astropy.time.Time, or ~np.ndarray Time quantity to be converted, or a numpy array (returned as-is). freq_unit : ~astropy.units.Quantity Frequency unit. ``time`` will be converted into units of the inverse of ``freq_unit``. Returns ------- time_value : ~numpy.ndarray Times in units of inverse of ``freq_unit`` """ if isinstance(time, Time): time = time.jd * u.day if not hasattr(time, 'unit'): # assume time is already in the correct units: return time return time.to(1 / freq_unit).value def _flux_to_ppm(self, flux, flux_unit=u.cds.ppm, is_error=False): """ Convert fluxes into units compatible with the parameterization of kernel hyperparameters in gadfly. Parameters ---------- flux : ~astropy.units.Quantity or ~np.ndarray Must be either: (1) a convertible flux quantity or (2) a numpy array already in units of ppm (in this case, the method will simply return the input). flux_unit : ~astropy.units.Quantity Flux unit. ``flux`` will be converted into units of ``flux_unit``. is_error : bool Set to true if converting a unitful flux uncertainty to its equivalent in [ppm]. Returns ------- time_value : ~numpy.ndarray Times in units of inverse of ``freq_unit`` """ if isinstance(flux, np.ndarray) and not hasattr(flux, 'unit'): # assume already in [ppm] if given an ndarray return flux elif hasattr(flux, 'unit') and flux.unit.is_equivalent(u.electron / u.s): # if given lightkurve.LightCurve.flux, which is units of e-/s, # normalize the light curve into the correct units. # Cache the median so we can reconstruct the original units # on a call to `_ppm_to_flux` later: if is_error: flux_ppm = 1e6 * (flux / self._original_flux_median).value else: flux_ppm = 1e6 * (flux / self._original_flux_median - 1).value return flux_ppm return flux.to(flux_unit).value def _ppm_to_flux(self, value_in_ppm, power=1): """ Convert ~np.ndarray fluxes into unitful quantities. Parameters ---------- value_in_ppm : Must be either: (1) a convertible flux quantity or (2) a numpy array already in units of ppm (in this case, the method will simply return the input). unit : ~astropy.units.Quantity Flux unit. ``flux`` will be returned in units of ``flux_unit``. Returns ------- flux_ppm : ~astropy.units.Quantity Fluxes in units of [ppm] """ if self._original_flux_median is not None and power == 1: # this is a flux value: value_in_original_units = ( (1e-6 * value_in_ppm + 1) * self._original_flux_median ) return value_in_original_units elif self._original_flux_median is not None and power == 2: # this is a variance: value_in_original_units = ( (1e-6 * value_in_ppm) * self._original_flux_median * self._original_flux_median.unit ) return value_in_original_units else: # otherwise return the flux with units of ppm: return u.Quantity(value_in_ppm, unit=u.cds.ppm)
[docs] def compute( self, t, yerr=None, diag=None, check_sorted=True, quiet=False ): """ Compute the Cholesky factorization of the GP covariance matrix. Parameters ---------- t : ~astropy.units.Quantity or ~astropy.time.Time The independent coordinates of the observations. This must be sorted in increasing order. yerr : ~astropy.units.Quantity If provided, the diagonal standard deviation of the observation model. diag : ~astropy.units.Quantity If provided, the diagonal variance of the observation model. check_sorted : bool If ``True``, a check is performed to make sure that ``t`` is correctly sorted. A ``ValueError`` will be thrown when this check fails. quiet : bool If ``True``, when the matrix cannot be factorized (because of numerics or otherwise) the solver's ``LinAlgError`` will be silenced and the determiniant will be set to zero. Otherwise, the exception will be propagated. """ if isinstance(t, Time) or hasattr(t, 'unit'): t = self._time_to_freq(t) if yerr is not None and hasattr(yerr, 'unit'): yerr = self._flux_to_ppm(yerr, is_error=True) if diag is not None and hasattr(yerr, 'unit'): diag = self._flux_to_ppm(diag) super().compute( t, yerr=yerr, diag=diag, check_sorted=check_sorted, quiet=quiet )
[docs] def condition( self, y, t=None, include_mean=True, kernel=None, return_quantity=False ): """ Condition the Gaussian process given observations ``y``. Parameters ---------- y : ~astropy.units.Quantity Observations t : ~astropy.units.Quantity or ~astropy.time.Time Times include_mean : bool Include the mean model in the prediction kernel : subclass of :py:class:`~celerite2.terms.Term` Evaluate conditional distribution given this kernel return_quantity : bool Return the fluxes as a :py:class:`~astropy.units.Quantity` in the same units as the input light curve. """ if t is not None and (isinstance(t, Time) or hasattr(t, 'unit')): t = self._time_to_freq(t) if hasattr(y, 'unit'): y = self._flux_to_ppm(y) result = self.conditional_distribution( self, y, t=t, include_mean=include_mean, kernel=kernel ) if return_quantity: return self._ppm_to_flux(result.mean) return result
[docs] def predict( self, y, t=None, return_cov=False, return_var=False, include_mean=True, kernel=None, return_quantity=False ): """ Compute the conditional distribution The factorized matrix from the previous call to :py:meth:`~gadfly.GaussianProcess.compute` is used, so that method must be called first. Parameters ---------- y (shape[N]) : ~astropy.units.Quantity The observations at coordinates ``t`` as defined by :py:meth:`gadfly.GaussianProcess.compute`. t (shape[M]) : ~astropy.units.Quantity or ~astropy.time.Time The independent coordinates where the prediction should be evaluated. If not provided, this will be evaluated at the observations ``t`` from :py:meth:`~gadfly.GaussianProcess.compute`. return_var : bool Return the variance of the conditional distribution. return_cov : bool Return the full covariance matrix of the conditional distribution. include_mean : bool Include the mean function in the prediction. kernel : subclass of :py:class:`~celerite2.terms.Term` If provided, compute the conditional distribution using a different kernel. This is generally used to separate the contributions from different model components. Note that the computational cost and scaling will be worse when using this parameter. return_quantity : bool Return the fluxes as a :py:class:`~astropy.units.Quantity` in the same units as the input light curve. """ if hasattr(y, 'unit'): y = self._flux_to_ppm(y) if isinstance(t, Time) or hasattr(t, 'unit'): t = self._time_to_freq(t) cond = self.condition(y, t=t, include_mean=include_mean, kernel=kernel) if return_var and return_quantity: return self._ppm_to_flux(cond.mean), self._ppm_to_flux(cond.variance, power=2) elif return_cov and return_quantity: return self._ppm_to_flux(cond.mean), self._ppm_to_flux(cond.covariance, power=2) elif return_quantity: return self._ppm_to_flux(cond.mean) elif return_var: return cond.mean, cond.variance elif return_cov: return cond.mean, cond.covariance return cond.mean
[docs] def dot_tril(self, y, *, inplace=False): """ Dot the Cholesky factor of the GP system into a vector or matrix Compute ``x = L.y`` where ``K = L.L^T`` and ``K`` is the covariance matrix of the GP. .. note:: The mean function is not applied in this method. Parameters ---------- y : ~astropy.units.Quantity The vector or matrix ``y`` described above. inplace : bool If ``True``, ``y`` will be overwritten with the result ``x``. """ if hasattr(y, 'unit'): y = self._flux_to_ppm(y) return super().dot_tril(y, inplace=inplace)
[docs] def log_likelihood(self, y, *, inplace=False): """ Compute the marginalized likelihood of the GP model The factorized matrix from the previous call to :py:meth:`~gadfly.GaussianProcess.compute` is used so that method must be called first. Parameters ---------- y (shape[N]) : ~astropy.units.Quantity The observations at coordinates ``t`` as defined by :py:meth:`~gadfly.GaussianProcess.compute`. inplace : bool If ``True``, ``y`` will be overwritten in the process of the calculation. This will reduce the memory footprint, but should be used with care since this will overwrite the data. """ if hasattr(y, 'unit'): y = self._flux_to_ppm(y) return super().log_likelihood(y, inplace=inplace)
[docs] def apply_inverse(self, y, *, inplace=False): """ Apply the inverse of the covariance matrix to a vector or matrix Solve ``K.x = y`` for ``x`` where ``K`` is the covariance matrix of the GP. .. note:: The mean function is not applied in this method. Parameters ---------- y : ~astropy.units.Quantity The vector or matrix ``y`` described above. inplace : bool If ``True``, ``y`` will be overwritten with the result ``x``. """ if hasattr(y, 'unit'): y = self._flux_to_ppm(y) return super().apply_inverse(y, inplace=inplace)
[docs] def sample(self, *, size=None, include_mean=True, return_quantity=False): """ Generate random samples from the prior implied by the GP system The factorized matrix from the previous call to :py:meth:`~gadfly.GaussianProcess.compute` is used so that method must be called first. Parameters ---------- size : int The number of samples to generate. If not provided, only one sample will be produced. include_mean : bool Include the mean function in the prediction. return_quantity : bool Return the fluxes as a :py:class:`~astropy.units.Quantity` in the same units as the input light curve. """ result = super().sample(size=size, include_mean=include_mean) result -= result.mean(axis=0 if result.ndim == 2 else None) if return_quantity: return self._ppm_to_flux(result) return result