Source code for pints._log_pdfs
#
# Main Log PDF functions
#
# This file is part of PINTS (https://github.com/pints-team/pints/) which is
# released under the BSD 3-clause license. See accompanying LICENSE.md for
# copyright notice and full license details.
#
import numpy as np
[docs]
class LogPDF():
"""
Represents the natural logarithm of a (not necessarily normalised)
probability density function (PDF).
All :class:`LogPDF` types are callable: when called with a vector argument
``x`` they return some value ``log(f(x))`` where ``f(x)`` is an
unnormalised PDF. The size of the argument ``x`` is given by
:meth:`n_parameters()`. In PINTS, all parameters must be continuous and
real.
All subclasses of ``LogPDF`` should provide an implementation of
:meth:`__call__` and :meth:`n_parameters`. Providing :meth:`evaluateS1` is
optional.
"""
[docs]
def __call__(self, x):
""" Evaluates this LogPDF for parameters ``x``. """
raise NotImplementedError
[docs]
def evaluateS1(self, x):
"""
Evaluates this LogPDF, and returns the result plus the partial
derivatives of the result with respect to the parameters.
The returned data is a tuple ``(L, L')`` where ``L`` is a scalar value
and ``L'`` is a sequence of length ``n_parameters``.
Note that the derivative returned is of the log-pdf, so
``L' = d/dp log(f(p))``, evaluated at ``p=x``.
*This is an optional method that is not always implemented.*
"""
raise NotImplementedError
[docs]
def n_parameters(self):
"""
Returns the dimension of the space this :class:`LogPDF` is defined
over.
"""
raise NotImplementedError
[docs]
class LogPrior(LogPDF):
"""
Represents the natural logarithm ``log(f(theta))`` of a known probability
density function ``f(theta)``.
Priors are *usually* normalised (i.e. the integral ``f(theta)`` over all
points ``theta`` in parameter space sums to 1), but this is not a strict
requirement.
Extends :class:`LogPDF`.
"""
[docs]
def cdf(self, x):
"""
Returns the cumulative density function at point(s) ``x``.
``x`` should be an ``n x d`` array, where ``n`` is the number of input
samples and ``d`` is the dimension of the parameter space.
"""
raise NotImplementedError
[docs]
def convert_from_unit_cube(self, u):
"""
Converts samples ``u`` uniformly drawn from the unit cube into those
drawn from the prior space, typically by transforming using
:meth:`LogPrior.icdf()`.
``u`` should be an ``n x d`` array, where ``n`` is the number of input
samples and ``d`` is the dimension of the parameter space.
"""
return self.icdf(u)
[docs]
def convert_to_unit_cube(self, x):
"""
Converts samples from the prior ``x`` to be drawn uniformly from the
unit cube, typically by transforming using :meth:`LogPrior.cdf()`.
``x`` should be an ``n x d`` array, where ``n`` is the number of input
samples and ``d`` is the dimension of the parameter space.
"""
return self.cdf(x)
[docs]
def icdf(self, p):
"""
Returns the inverse cumulative density function at cumulative
probability/probabilities ``p``.
``p`` should be an ``n x d`` array, where ``n`` is the number of input
samples and ``d`` is the dimension of the parameter space.
"""
raise NotImplementedError
[docs]
def mean(self):
"""
Returns the analytical value of the expectation of a random variable
distributed according to this :class:`LogPDF`.
"""
raise NotImplementedError
[docs]
def sample(self, n=1):
"""
Returns ``n`` random samples from the underlying prior distribution.
The returned value is a NumPy array with shape ``(n, d)`` where ``n``
is the requested number of samples, and ``d`` is the dimension of the
prior.
"""
raise NotImplementedError
[docs]
class LogLikelihood(LogPDF):
"""
Represents a log-likelihood defined on a parameter space.
This class adds no new functionality, but exists to indicate when a LogPDF
represents the probability of a data set *given* a set of parameters,
rather than a probability of those parameters.
*Extends:* :class:`LogPDF`
"""
[docs]
class ProblemLogLikelihood(LogLikelihood):
"""
Represents a log-likelihood on a problem's parameter space, used to
indicate the likelihood of an observed (fixed) time-series given a
particular parameter set (variable).
Extends :class:`LogLikelihood`.
Parameters
----------
problem
The time-series problem this log-likelihood is defined for.
"""
def __init__(self, problem):
super().__init__()
self._problem = problem
# Cache some problem variables
self._values = problem.values()
self._times = problem.times()
self._n_parameters = problem.n_parameters()
[docs]
def n_parameters(self):
""" See :meth:`LogPDF.n_parameters()`. """
return self._n_parameters
def problem(self):
return self._problem
[docs]
class LogPosterior(LogPDF):
"""
Represents the sum of a :class:`LogLikelihood` and a :class:`LogPrior`
defined on the same parameter space.
As an optimisation, if the :class:`LogPrior` evaluates as `-inf` for a
particular point in parameter space, the corresponding :class:`LogPDF` will
not be evaluated.
Extends :class:`LogPDF`.
Parameters
----------
log_likelihood
A :class:`LogLikelihood`, defined on the same parameter space.
log_prior
A :class:`LogPrior`, representing prior knowledge of the parameter
space.
"""
def __init__(self, log_likelihood, log_prior):
super().__init__()
# Check arguments
if not isinstance(log_prior, LogPrior):
raise ValueError(
'Given prior must extend pints.LogPrior.')
if not isinstance(log_likelihood, LogLikelihood):
raise ValueError(
'Given log_likelihood must extend pints.LogLikelihood.')
# Check dimensions
self._n_parameters = log_prior.n_parameters()
if log_likelihood.n_parameters() != self._n_parameters:
raise ValueError(
'Given log_prior and log_likelihood must have same dimension.')
# Store prior and likelihood
self._log_prior = log_prior
self._log_likelihood = log_likelihood
# Store -inf, for later use
self._minf = -np.inf
[docs]
def __call__(self, x):
# Evaluate log-prior first, assuming this is very cheap
log_prior = self._log_prior(x)
if log_prior == self._minf:
return self._minf
return log_prior + self._log_likelihood(x)
[docs]
def evaluateS1(self, x):
"""
Evaluates this LogPDF, and returns the result plus the partial
derivatives of the result with respect to the parameters.
The returned data has the shape ``(L, L')`` where ``L`` is a scalar
value and ``L'`` is a sequence of length ``n_parameters``.
*This method only works if the underlying :class:`LogPDF` and
:class:`LogPrior` implement the optional method
:meth:`LogPDF.evaluateS1()`!*
"""
#TODO: Is there an optimisation to be made here?
a, da = self._log_prior.evaluateS1(x)
b, db = self._log_likelihood.evaluateS1(x)
return a + b, da + db
[docs]
def log_likelihood(self):
""" Returns the :class:`LogLikelihood` used by this posterior. """
return self._log_likelihood
[docs]
def log_prior(self):
""" Returns the :class:`LogPrior` used by this posterior. """
return self._log_prior
[docs]
def n_parameters(self):
""" See :meth:`LogPDF.n_parameters()`. """
return self._n_parameters