from abc import ABC, abstractmethod
import numpy as np
def _check_finite(arguments, note=""):
"""Check that the arguments defining a prior are finite.
Every comparison with a NaN is false and an infinity satisfies a strict
order, so the order checks of the box priors pass such arguments while
the density they define is not a density.
Parameters
----------
arguments : dict
The arguments by name, each a scalar or an array.
note : str, optional
A sentence appended to the error message, to say what the prior
needs. Default `""`.
Raises
------
ValueError
If any element of any argument is NaN or infinite.
"""
for name, value in arguments.items():
if not np.all(np.isfinite(value)):
raise ValueError(
f"All elements of {name}={value} should be finite." + note
)
[docs]
class Prior(ABC):
"""Abstract base class for PyVBMC prior distributions."""
[docs]
def log_pdf(self, x, keepdims=True):
"""Compute the log-pdf of the distribution.
Parameters
----------
x : np.ndarray
The array of input point(s), of dimension `(D,)` or `(n, D)`, where
`D` is the distribution dimension. The point(s) are read as
`float64`, whatever dtype the array carries.
keepdims : bool
Whether to keep the input dimensions and return an array of shape
`(n, 1)`, or discard them and return an array of shape `(n,)`.
Returns
-------
log_pdf : np.ndarray
The log-density of the prior at the input point(s), of dimension
`(n, 1)` or `(n,)` (depending on ``keepdims``).
"""
x_orig_shape = x.shape
# The densities accumulate in the dtype of the input, so an integer
# or a narrower float input would truncate or wrap the result.
x = x.astype(np.float64, copy=False)
x = np.atleast_2d(x)
n, D = x.shape
if self.D == 1 and n == 1:
x = x.T
elif D != self.D:
raise ValueError(
f"x should have shape ({self.D},) or (n, {self.D}) but has shape {x_orig_shape}."
)
log_pdf = self._log_pdf(x)
if keepdims:
return log_pdf
else:
return log_pdf.ravel()
[docs]
def pdf(self, x, keepdims=True):
"""Compute the pdf of the distribution.
Parameters
----------
x : np.ndarray
The array of input point(s), of dimension `(D,)` or `(n, D)`, where
`D` is the distribution dimension.
keepdims : bool
Whether to keep the input dimensions and return an array of shape
`(n, 1)`, or discard them and return an array of shape `(n,)`.
Returns
-------
pdf : np.ndarray
The density of the prior at the input point(s), of dimension `(n,
1)` or `(n,)` (depending on ``keepdims``).
"""
return np.exp(self.log_pdf(x, keepdims=keepdims))
[docs]
def support(self):
r"""Returns the box that bounds the support of the distribution.
For priors that define ``_support``, this is the smallest
axis-aligned box containing the support, except that a ``Product``
reads each coordinate from its marginal, so a marginal that defines
no ``_support`` (a ``UserFunction``) spans the whole line. Otherwise,
the method reports the whole space. ``VBMC`` checks that the hard
bounds lie inside the reported box.
Returns
-------
lb, ub : tuple(np.ndarray, np.ndarray)
A tuple of lower and upper bounds of the support, such that
[``lb[i]``, ``ub[i]``] bounds the support of the ``i``\ th marginal.
"""
if hasattr(self, "_support"):
return self._support()
else:
return np.full(self.D, -np.inf), np.full(self.D, np.inf)
@abstractmethod
def __init__(self):
"""Initialize the distribution."""
self.D = 1
@abstractmethod
def _log_pdf(self, x):
"""Compute the log-pdf of the distribution.
This private method is wrapped by ``self.log_pdf()``, which handles
input validation and output shape.
Parameters
----------
x : np.ndarray
The array of input point(s), of dimension `(D,)` or `(n, D)`, where
`D` is the distribution dimension.
Returns
-------
log_pdf : np.ndarray
The log-density of the prior at the input point(s), of dimension
`(n, 1)`.
"""
pass
[docs]
@abstractmethod
def sample(self, n, rng=None):
"""Sample random variables from the distribution.
Parameters
----------
n : int
The number of points to sample.
rng : None, int, SeedSequence or Generator, optional
Random generator or seed; if None a generator is derived from
NumPy's global random state.
Returns
-------
rvs : np.ndarray
The samples points, of shape `(n, D)`, where `D` is the dimension.
"""
pass
@classmethod
@abstractmethod
def _generic(cls, D=1):
"""Return a generic instance of the class (used for tests)."""
return cls(D=D)