Source code for cuqi.likelihood._likelihood
from __future__ import annotations
from typing import Union
from cuqi.model import Model
from cuqi.utilities import get_non_default_args, _get_python_variable_name
from cuqi.geometry import _DefaultGeometry1D
from cuqi.density import Density, EvaluatedDensity
import warnings
from copy import copy
[docs]
class Likelihood(Density):
"""Likelihood function defined from a conditional distribution and some observed data.
The parameters of the likelihood function is defined as the conditioning variables
of a conditional distribution.
The geometry is automatically determined from the model of data distribution.
Generates instance of cuqi.likelihood.Likelihood
Parameters
------------
distribution : ~cuqi.distribution.Distribution
| Distribution to create likelihood from.
data : ~cuqi.array.CUQIarray or array_like
| Observation to create likelihood from.
"""
[docs]
def __init__(self, distribution, data):
# Check if distribution is conditional
if not distribution.is_cond:
raise TypeError("Input distribution must be a conditional distribution to convert to likelihood.")
self.distribution = distribution
self.data = data
@property
def name(self):
""" Return name of likelihood """
return self.distribution.name
@name.setter
def name(self, value):
self.distribution.name = value
@property
def _name(self):
return self.distribution._name
@_name.setter
def _name(self, value):
self.distribution._name = value
@property
def FD_enabled(self):
""" Return FD_enabled of the likelihood from the underlying distribution """
return self.distribution.FD_enabled
@property
def FD_epsilon(self):
""" Return FD_epsilon of the likelihood from the underlying distribution """
return self.distribution.FD_epsilon
@property
def _constant(self):
return self.distribution._constant
def _logd(self, *args, **kwargs):
"""Return the log-likelihood function at given value"""
return self.distribution(*args, **kwargs).logd(self.data)
def _gradient(self, *args, **kwargs):
"""Return gradient of the log-likelihood function at given value"""
return self.distribution.gradient(self.data, *args, **kwargs)
@property
def dim(self):
""" Return dimension of likelihood """
return self.geometry.par_dim
@property
def par_shape(self):
""" Return parameter shape of likelihood """
return self.geometry.par_shape
@property
def fun_shape(self):
""" Return function space shape of likelihood """
return self.geometry.fun_shape
@property
def geometry(self):
""" Return geometry of likelihood """
if self.model is None:
return _DefaultGeometry1D()
if len(self.get_parameter_names()) > 1:
warnings.warn(
f"Likelihood depends on multiple parameters {self.get_parameter_names()}.\n"
f"Returned geometry is only with respect to the model parameter {get_non_default_args(self.model)}."
)
return self.model.domain_geometry
[docs]
def get_parameter_names(self):
"""Return parameter names of likelihood"""
return self.distribution.get_conditioning_variables()
def __repr__(self) -> str:
return "CUQI {} {} function. Parameters {}.".format(self.distribution.__class__.__name__,self.__class__.__name__,self.get_parameter_names())
@property
def model(self) -> Model:
""" Extract model from data distribution.
Returns
-------
model: cuqi.model.Model or None
Forward model used in defining the data distribution or None if no model is found.
"""
model_value = None
for var in self.distribution.get_mutable_variables():
value = getattr(self.distribution, var)
if isinstance(value, Model):
if model_value is None:
model_value = value
else:
raise ValueError(f"Multiple models found in data distribution {self.distribution} of {self}. Extracting model is ambiguous and not supported.")
return model_value
def _condition(self, *args, **kwargs):
""" Fix some parameters of the likelihood function by conditioning on the underlying distribution. """
new_likelihood = copy(self)
new_likelihood.distribution = self.distribution(*args, **kwargs)
# If dist is no longer conditional, return a constant density
if not new_likelihood.distribution.is_cond:
return new_likelihood.distribution.to_likelihood(self.data) # TODO: Consider renaming to_likelihood as to_density
return new_likelihood
# Overload parent to add type hint.
def __call__(self, *args, **kwargs) -> Union[Likelihood, EvaluatedDensity]:
return super().__call__(*args, **kwargs)
[docs]
def enable_FD(self, epsilon=1e-8):
""" Call enable_FD of the underlying distribution """
self.distribution.enable_FD(epsilon)
[docs]
def disable_FD(self):
""" Call disable_FD of the underlying distribution """
self.distribution.disable_FD()
[docs]
class UserDefinedLikelihood(object):
""" Class to wrap user-defined likelihood functions.
Parameters
------------
dim : int
Dimension of the likelihood.
logpdf_func : callable
Function evaluating log density function.
gradient_func : callable
Function evaluating the gradient of the log density.
geometry : Geometry
Geometry of the likelihood.
"""
[docs]
def __init__(self, dim=None, logpdf_func=None, gradient_func=None, geometry=None, name=None):
self.dim = dim
self.logpdf_func = logpdf_func
self.gradient_func = gradient_func
self.geometry = geometry
self._name = name
@property
def model(self):
""" Return model of likelihood """
return None
@property
def name(self):
""" Return name of likelihood """
if self._name is None:
self._name = _get_python_variable_name(self)
return self._name
@property
def dim(self):
""" Return dimension of likelihood """
return self._dim
@dim.setter
def dim(self, value):
self._dim = value
[docs]
def logd(self, *args, **kwargs):
"""Returns value of likelihood function"""
return self.logpdf_func(*args, **kwargs)
[docs]
def gradient(self, *args, **kwargs):
"""Return gradient of likelihood function"""
return self.gradient_func(*args, **kwargs)
[docs]
def get_parameter_names(self):
"""Return parameter names of likelihood"""
return get_non_default_args(self.logpdf_func)
def __repr__(self) -> str:
return "CUQI {} function. Parameters {}.".format(self.__class__.__name__,self.get_parameter_names())