import numpy as np
import scipy.integrate as integrate
import scipy.stats as stats
from scipy.linalg import sqrtm
from pybop.parameters.distributions import (
BaseDistribution,
Distribution,
Gaussian,
LogNormal,
Uniform,
)
from pybop.transformation.base_transformation import Transformation
from pybop.transformation.transformations import (
ComposedTransformation,
IdentityTransformation,
LogTransformation,
ScaledTransformation,
)
[docs]
class BaseMultivariateDistribution(Distribution):
"""
A base class for defining multivariate parameter distributions.
This class extends ``pybop.Distribution`` with methods that reduce the
output of a multivariate distribution to its individual dimensions.
Note that, unlike in ``pybop.Distribution``, distribution attributes
are stored in a dictionary, as multivariate distributions in SciPy
do not follow the loc/scale convention.
Attributes
----------
distribution : scipy.stats.rv_continuous
The underlying continuous random variable distribution.
properties : dict
A dictionary with distribution keyword argument names as string
keys and their values as float values.
"""
[docs]
def support(self) -> np.ndarray:
"""Return the support as a numpy array of dimensions (2, n_parameters)."""
return self.distribution.support(**self.properties)
[docs]
def pdf(self, x):
"""Calculates the probability density function (PDF) of the distribution at x."""
return self.distribution.pdf(x, **self.properties)
[docs]
def logpdf(self, x):
"""Calculates the logarithm of the probability density function of the distribution at x."""
return self.distribution.logpdf(x, **self.properties)
icdf = None
"""Multivariate distributions have no invertible CDF."""
[docs]
def icdf_marginal(self, q, position: int):
raise NotImplementedError
[docs]
def cdf(self, x):
return self.distribution.cdf(x, **self.properties)
[docs]
def cdf_marginal(self, x, position: int):
"""
Takes the marginal cumulative distribution function (CDF) at x.
Parameters
----------
x : numpy.ndarray
The point(s) at which to evaluate the CDF.
position : int
The dimension to which to reduce the CDF to.
Returns
-------
float
The marginal cumulative distribution function value at x.
"""
return integrate.quad(self.marginal(position), -np.inf, x)[0]
[docs]
def rvs(self, size: int = 1, random_state: int | None = None):
"""Generates random variates from the distribution."""
return self.distribution.rvs(
**self.properties, size=size, random_state=random_state
)
[docs]
def cov(self):
"""Get the covariance matrix."""
return self.properties["cov"]
[docs]
def std(self):
"""Get the square root of the covariance matrix."""
return sqrtm(self.cov())
[docs]
def marginal(self, position: int):
"""Return univariate marginal distribution of parameter with index 'position'."""
raise NotImplementedError
[docs]
class MultivariateNonparametric(BaseMultivariateDistribution):
"""
Represents a "freeform" distribution, i.e., one that is defined from
a random sampling and a kernel density estimate on that sampling.
Parameters
----------
samples : numpy.ndarray
The random variates to base the distribution on.
"""
def __init__(self, samples: np.ndarray):
super().__init__(
distribution=stats.gaussian_kde(samples), n_parameters=samples.shape[0]
)
[docs]
def mean(self):
# scipy.stats.gaussian_kde does not have the method "mean"
# compute mean based on dataset
return np.mean(self.distribution.dataset)
[docs]
def std(self):
# scipy.stats.gaussian_kde does not have the method "std"
# compute standard deviation based on dataset
return np.std(self.distribution.dataset, ddof=1)
[docs]
def rvs(self, size: int = 1, random_state: int | None = None):
return self.distribution.resample(size, random_state)
[docs]
def marginal(self, position: int):
return MultivariateNonparametric(self.distribution.marginal(position).dataset)
[docs]
class MultivariateGaussian(BaseMultivariateDistribution):
"""
Represents a multivariate Gaussian (normal) distribution with a
given mean and covariance.
Parameters
----------
mean : numpy.ndarray
The mean (µ) of the multivariate Gaussian distribution.
covariance: numpy.ndarray
The covariance matrix (Σ) of the multivariate Gaussian
distribution. Note that what is called σ in 1D would be the
square root of Σ here.
"""
def __init__(self, mean: np.ndarray, covariance: np.ndarray):
super().__init__(
distribution=stats.multivariate_normal,
properties={"mean": np.asarray(mean), "cov": np.asarray(covariance)},
n_parameters=len(mean),
)
[docs]
def support(self) -> np.ndarray:
"""Return the support as a numpy array of dimensions (2, n_parameters)."""
return np.asarray([[-np.inf, np.inf]] * self._n_parameters).T
[docs]
def mean(self):
return self.properties["mean"]
[docs]
def cov(self):
return self.properties["cov"]
[docs]
def marginal(self, position: int):
# Note that what is called σ in 1D is the square root of Σ here
return Gaussian(
mean=self.properties["mean"][position],
sigma=np.sqrt(self.properties["cov"][position, position]),
)
[docs]
class MultivariateLogNormal(BaseMultivariateDistribution):
"""
Represents a multivariate log-normal distribution with a
given mean and covariance of the normally distributed log(x).
Parameters
----------
mean_log_x : numpy.ndarray
The mean (µ) of the multivariate Gaussian distribution of log(x).
covariance_log_x: numpy.ndarray
The covariance matrix (Σ) of the multivariate Gaussian
distribution of log(x). Note that what is called σ in 1D would be the
square root of Σ here.
"""
def __init__(self, mean_log_x, covariance_log_x):
super().__init__(distribution=None, n_parameters=len(mean_log_x))
self.distribution_log_x = stats.multivariate_normal
self.properties_log_x = {
"mean": np.asarray(mean_log_x),
"cov": np.asarray(covariance_log_x),
}
self.properties = self._get_properties()
[docs]
def _get_properties(self):
"""
Method to compute the mean and covariance of x given the mean and
covariance of log(x).
"""
mean = np.zeros(self._n_parameters)
for i in range(self._n_parameters):
mean[i] = np.exp(
self.properties_log_x["mean"][i]
+ 0.5 * self.properties_log_x["cov"][i, i]
)
covariance = np.zeros((self._n_parameters, self._n_parameters))
for i in range(self._n_parameters):
for j in range(self._n_parameters):
covariance[i, j] = (
(np.exp(self.properties_log_x["cov"][i, j]) - 1) * mean[i] * mean[j]
)
return {"mean": mean, "cov": covariance}
[docs]
def support(self) -> np.ndarray:
"""Return the support as a numpy array of dimensions (2, n_parameters)."""
return np.asarray([[0, np.inf]] * self._n_parameters).T
[docs]
def mean(self):
return self.properties["mean"]
[docs]
def cov(self):
return self.properties["cov"]
[docs]
def pdf(self, x):
if np.any(x <= 0):
return 0.0
# change of variable
log_x = np.log(x)
# get pdf of normal distribution for transformed variable
mvn_pdf = self.distribution_log_x.pdf(log_x, **self.properties_log_x)
# determinant of the jacobian of the transformation
jacobian = np.prod(x)
return mvn_pdf / jacobian
[docs]
def logpdf(self, x):
if np.any(x <= 0):
return -np.inf
# change of variable
log_x = np.log(x)
# use normal distribution to get logpdf of transformed variable
mvn_logpdf = self.distribution_log_x.logpdf(log_x, **self.properties_log_x)
# log of determinant of jacobian
return mvn_logpdf - np.sum(log_x)
[docs]
def cdf(self, x):
if np.any(x <= 0):
return 0.0
# change of variable
log_x = np.log(x)
# use normal distribution to get cdf of transformed variable
return self.distribution_log_x.cdf(log_x, **self.properties_log_x)
[docs]
def rvs(self, size: int = 1, random_state: int | None = None):
# use normal distribution of log(x) to compute rvs
return np.exp(
self.distribution_log_x.rvs(
**self.properties_log_x, size=size, random_state=random_state
)
)
[docs]
def marginal(self, position: int):
# determine marginal distribution based on properties of distribution of log(x)
return LogNormal(
mean_log_x=self.properties_log_x["mean"][position],
sigma=np.sqrt(self.properties_log_x["cov"][position, position]),
)
[docs]
class MarginalDistribution(Distribution):
"""
Represents a univariate marginal distribution of a pybop.BaseMultivariateDistribution.
Relies on the "marginal" method of the multivariate distribution.
Sub-class of `pybop.Distribution` with the additional properties
"parent_distribution" and "position"
Parameters
----------
parent_distribution: pybop.BaseMultivariateDistribution
A multivariate distribution
position: int
position of the parameter for which to create the marginal distribution
"""
def __init__(
self, parent_distribution: BaseMultivariateDistribution, position: int
):
# Get marginal distribution and initialise parent class
distribution = parent_distribution.marginal(position)
super().__init__(distribution=distribution)
# add additional properties
self._position = position
self.parent_distribution = parent_distribution
@property
def position(self) -> int:
return self._position
[docs]
def __repr__(self) -> str:
return f"Marginal, position {self.position}, parent {self.parent_distribution.__repr__()}"