import warnings
import numbers
import numpy as np
import numpy.linalg as nplinalg
import scipy.stats as sps
import scipy.sparse as spa
import scipy.linalg as splinalg
from cuqi import config
from cuqi.geometry import _get_identity_geometries
from cuqi.utilities import force_ndarray, sparse_cholesky, check_if_conditional_from_attr
from cuqi.distribution import Distribution
# We potentially allow the use of sksparse.cholmod for sparse Cholesky
# if it is installed. If not, we use our own sparse Cholesky.
# TODO #129: add full support for sparse covariance matrices without cholmod library (missing efficient logdet computation)
try:
import sksparse.cholmod as skchol
has_cholmod = True
except ImportError:
has_cholmod = False
[docs]
class Gaussian(Distribution):
"""
General Gaussian probability distribution. Generates instance of cuqi.distribution.Gaussian.
The Gaussian is defined via the probability density function
.. math::
p(x) = \\frac{1}{(2\\pi)^{\\frac{d}{2}}|\\Sigma|^{\\frac{1}{2}}} \\exp\\left(-\\frac{1}{2}(x-\\mu)^{\\top}\\Sigma^{-1}(x-\\mu)\\right)
where :math:`\\mu` is the mean, :math:`\\Sigma` is the covariance matrix, and :math:`d` is the dimension of the Gaussian.
Depending on the specific Gaussian distribution, it is useful to have the option of defining the Gaussian by a mean and one of the following:
covariance, precision, square root of covariance, or square root of precision matrices. The relationship between these matrices is described in
the parameters section below.
Internally the class will always convert the given matrices to the square root of the precision matrix for efficiency. It is therefore best for
computational efficiency to define the Gaussian via the square root of the precision matrix in the multivariate case. For the i.i.d. case the
overhead of computing the square root of the precision matrix is negligible.
Parameters
----------
mean : scalar or 1d-array
Mean vector of Gaussian. If a scalar value, all entries in the mean vector are set to that scalar.
cov : scalar, 1d-array or 2d-array (sparse matrix is supported)
Covariance matrix of Gaussian. If a scalar or 1d-array, the value defines the diagonal entries of the covariance matrix.
prec : scalar, 1d-array or 2d-array (sparse matrix is supported)
Precision matrix of Gaussian defined as the inverse of the covariance.
If a scalar or 1d-array, the value defines the diagonal entries of the precision matrix.
sqrtcov : scalar, 1d-array or 2d-array (sparse matrix is supported)
Square root of covariance matrix of Gaussian. Defined as matrix R, where R.T@R = cov.
If a scalar or 1d-array the value is assumed to be the standard deviation of each component of the Gaussian.
sqrtprec : scalar or 1d-array or 2d-array (sparse matrix is supported)
Square root of precision matrix of Gaussian. Defined as matrix R, where R.T@R = prec.
If a scalar or 1d-array the value is assumed to be the inverse standard deviation of each component of the Gaussian.
Example
-----------
.. code-block:: python
# Generate an i.i.d. n-dim Gaussian with zero mean and 2 variance.
n = 4
x = cuqi.distribution.Gaussian(mean=np.zeros(n), cov=2)
.. code-block:: python
# Generate an 2-dim Gaussian with zero mean and standard deviations [2, 10].
x = cuqi.distribution.Gaussian(mean=0, sqrtcov=np.array([2, 10]))
.. code-block:: python
# Generate an n-dim Gaussian from given scipy.sparse precision matrix.
n = 5
prec = scipy.sparse.diags([-1, 2, -1], [-1, 0, 1], shape=(n, n))
x = cuqi.distribution.Gaussian(mean=0, prec=prec)
.. code-block:: python
# Generate an n-dim Gaussian from given scipy.sparse square root of precision matrix.
n = 5
sqrtprec = scipy.sparse.diags([1, -1], [0, 1], shape=(n, n))
x = cuqi.distribution.Gaussian(mean=0, sqrtprec=sqrtprec)
"""
[docs]
def __init__(self, mean=None, cov=None, prec=None, sqrtcov=None, sqrtprec=None, is_symmetric=True, **kwargs):
super().__init__(is_symmetric=is_symmetric, **kwargs)
self.mean = mean
# If everything is None we default to covariance as the mutable variables
# If more than one of the matrices are given we throw an error
if (cov is not None) + (prec is not None) + (sqrtprec is not None) + (sqrtcov is not None) == 0:
self._mutable_vars = ['mean', 'cov']
self.cov = cov
elif (cov is not None) + (prec is not None) + (sqrtprec is not None) + (sqrtcov is not None) != 1:
raise ValueError("Exactly one of 'cov', 'prec', 'sqrtcov', or 'sqrtprec' may be specified")
# This sets the mutable variables according to which matrix is given
if cov is not None:
self._mutable_vars = ['mean', 'cov']
self.cov = cov
elif prec is not None:
self._mutable_vars = ['mean', 'prec']
self.prec = prec
elif sqrtcov is not None:
self._mutable_vars = ['mean', 'sqrtcov']
self.sqrtcov = sqrtcov
elif sqrtprec is not None:
self._mutable_vars = ['mean', 'sqrtprec']
self.sqrtprec = sqrtprec
self._check_geometry_consistency()
@property
def mean(self):
""" Mean of the distribution """
return self._mean
@mean.setter
def mean(self, value):
self._mean = force_ndarray(value, flatten=True)
@property
def cov(self):
""" Covariance of the distribution """
if not hasattr(self, '_cov') or (self._cov is None and not 'cov' in self._mutable_vars):
raise NotImplementedError(f"Covariance is not computed by default for a Gaussian initialized with {self.get_mutable_variables()} and dim {self.dim}. Use method compute_cov() to compute it if needed.")
return self._cov
@cov.setter
def cov(self, value):
if not 'cov' in self._mutable_vars:
raise ValueError(f"Mutable variables are {self._mutable_vars}")
value = force_ndarray(value)
self._cov = value
if (value is not None) and (not callable(value)):
if self.dim > config.MIN_DIM_SPARSE:
sparse_flag = True # do sparse computations
else:
sparse_flag = False # use numpy
prec, sqrtprec, logdet, rank = get_sqrtprec_from_cov(self.dim, value, sparse_flag)
self._prec = prec
self._sqrtprec = sqrtprec
self._logdet = logdet
self._rank = rank
@property
def prec(self):
""" Precision of the distribution """
if not hasattr(self, '_prec'):
raise NotImplementedError(f"Precision is not computed by default for a Gaussian initialized with {self.get_mutable_variables()} and dim {self.dim}")
return self._prec
@prec.setter
def prec(self, value):
if not 'prec' in self._mutable_vars:
raise ValueError(f"Mutable variables are {self._mutable_vars}")
value = force_ndarray(value)
self._prec = value
self._cov = None # Reset covariance (in case it was computed before)
if (value is not None) and (not callable(value)):
if self.dim > config.MIN_DIM_SPARSE:
sparse_flag = True # do sparse computations
else:
sparse_flag = False # use numpy
sqrtprec, logdet, rank = get_sqrtprec_from_prec(self.dim, value, sparse_flag)
self._sqrtprec = sqrtprec
self._logdet = logdet
self._rank = rank
@property
def sqrtcov(self):
""" Square root of the covariance of the distribution. For 1D Gaussian this is the standard deviation. """
if not hasattr(self, '_sqrtcov'):
raise NotImplementedError(f"Square root of covariance is not computed by default for a Gaussian initialized with {self.get_mutable_variables()} and dim {self.dim}")
return self._sqrtcov
@sqrtcov.setter
def sqrtcov(self, value):
if not 'sqrtcov' in self._mutable_vars:
raise ValueError(f"Mutable variables are {self._mutable_vars}")
value = force_ndarray(value)
self._sqrtcov = value
self._cov = None # Reset covariance (in case it was computed before)
if (value is not None) and (not callable(value)):
if self.dim > config.MIN_DIM_SPARSE:
sparse_flag = True # do sparse computations
else:
sparse_flag = False # use numpy
prec, sqrtprec, logdet, rank = get_sqrtprec_from_sqrtcov(self.dim, value, sparse_flag)
self._prec = prec
self._sqrtprec = sqrtprec
self._logdet = logdet
self._rank = rank
@property
def sqrtprec(self):
""" Square root of the precision of the distribution. For 1D Gaussian this is the inverse standard deviation. """
return self._sqrtprec
@sqrtprec.setter
def sqrtprec(self, value):
if not 'sqrtprec' in self._mutable_vars:
raise ValueError(f"Mutable variables are {self._mutable_vars}")
value = force_ndarray(value)
self._sqrtprec = value
self._cov = None # Reset covariance (in case it was computed before)
if not check_if_conditional_from_attr(value):
if self.dim > config.MIN_DIM_SPARSE:
sparse_flag = True # do sparse computations
else:
sparse_flag = False # use numpy
sqrtprec, logdet, rank = get_sqrtprec_from_sqrtprec(self.dim, value, sparse_flag)
self._sqrtprec = sqrtprec
self._logdet = logdet
self._rank = rank
@property
def logdet(self):
""" Logarithm of the determinant of the covariance of the distribution """
if not hasattr(self, '_logdet'):
raise NotImplementedError(f"Log determinant is not computed by default for a Gaussian initialized with {self.get_mutable_variables()} {self.get_mutable_variables()} and dim {self.dim}")
return self._logdet
@property
def rank(self):
return self._rank
@property
def sqrtprecTimesMean(self):
mean = np.repeat(self.mean, self.dim) if len(self.mean) == 1 else self.mean
return (self.sqrtprec@mean).flatten()
[docs]
def compute_cov(self):
""" Computes the covariance matrix regardless of the mutable variables.
This is useful for smaller scale problems where we may want to use the full covariance matrix.
"""
# First determine which mutable variables are set
mutable_vars = self.get_mutable_variables()
# Check if main matrix is set
main_matrix = getattr(self, mutable_vars[1]) # 1. index is the main matrix
if main_matrix is None or callable(main_matrix):
raise ValueError(f"Mutable variable {mutable_vars[1]} is not set. Cannot get covariance matrix.")
# If dim is too large, we do not support computing the covariance matrix
if self.dim > config.MAX_DIM_INV:
raise NotImplementedError(f"Extracting the full covariance matrix is not implemented for dim > {config.MAX_DIM_INV}. To modify this edit the option cuqi.config.MAX_DIM_INV to a larger number.")
# First check if covariance is already computed
if 'cov' in mutable_vars:
cov = self.cov
if cov is not None and not callable(cov):
if cov.shape[0] == 1: # Scalar
computed_cov = cov.ravel()[0]*np.eye(self.dim)
elif len(cov.shape) == 1: # Vector
computed_cov = np.diag(cov)
else:
computed_cov = cov
# If not, we compute it via sqrtprec which is guaranteed to exist
else:
sqrtprec = self.sqrtprec
prec = sqrtprec.T@sqrtprec
if spa.issparse(prec):
computed_cov = spa.linalg.inv(prec).todense()
else:
computed_cov = np.linalg.inv(prec)
self._cov = computed_cov
return computed_cov
def _logupdf(self, x):
""" Un-normalized log density """
dev = x - self.mean
mahadist = np.sum(np.square(self.sqrtprec @ dev.T), axis=0)
return -0.5*mahadist.flatten()
[docs]
def logpdf(self, x):
if self.logdet is None:
raise NotImplementedError("Normalized density is not implemented for Gaussian when precision or covariance is sparse and cholmod is not installed.")
Z = -0.5*(self.rank*np.log(2*np.pi) + self.logdet.flatten()) # normalizing constant
logup = self._logupdf(x) # un-normalized density
return Z + logup
[docs]
def cdf(self, x1): # no closed form, we rely on scipy with full covariance
cov = self.compute_cov() # Ensure that we have the full covariance matrix
return sps.multivariate_normal.cdf(x1, self.mean, cov)
def _gradient(self, val, *args, **kwargs):
#Avoid complicated geometries that change the gradient.
if not type(self.geometry) in _get_identity_geometries() and \
not hasattr(self.geometry, 'gradient'):
raise NotImplementedError("Gradient not implemented for distribution {} with geometry {}".format(self,self.geometry))
if not callable(self.mean): # for prior
return -( self.prec @ (val - self.mean).T )
elif hasattr(self.mean, "gradient"): # for likelihood
model = self.mean
dev = val - model.forward(*args, **kwargs)
if isinstance(dev, numbers.Number):
dev = np.array([dev])
return model.gradient(self.prec @ dev, *args, **kwargs)
else:
warnings.warn('Gradient not implemented for {}'.format(type(self.mean)))
def _sample(self, N=1, rng=None):
""" Generate samples of the Gaussian distribution using
`s = mean + pseudoinverse(sqrtprec)*e`,
where `e` is a standard Gaussian random vector and `s` is the desired sample
"""
# Sample N(0,I)
if rng is not None:
e = rng.randn(np.shape(self.sqrtprec)[0], N)
else:
e = np.random.randn(np.shape(self.sqrtprec)[0], N)
# Compute perturbation
if spa.issparse(self.sqrtprec): # do sparse
if (N == 1):
perturbation = spa.linalg.spsolve(self.sqrtprec, e)[:, None]
else:
perturbation = spa.linalg.spsolve(self.sqrtprec, e)
else:
if np.allclose(self.sqrtprec, np.tril(self.sqrtprec)): # matrix is triangular
perturbation = splinalg.solve_triangular(self.sqrtprec, e)
else:
perturbation = splinalg.solve(self.sqrtprec, e)
# Add mean
s = self.mean[:, None] + perturbation
return s
# ======= Helper functions for Gaussian distribution =======
def get_sqrtprec_from_cov(dim, cov, sparse_flag):
""" Compute square root of precision matrix from covariance matrix.
Also computes log determinant and rank of covariance matrix.
Parameters
----------
dim : int
Dimension of the covariance matrix.
cov : 1-d or 2-d ndarray or sparse matrix
Covariance matrix. If 1-dimensional, then assumed to be a diagonal covariance matrix.
sparse_flag: bool
Whether to store matrices as Dense or Sparse
"""
# cov is scalar
if (cov.shape[0] == 1):
var = cov.ravel()[0]
logdet = dim*np.log(var)
rank = dim
if sparse_flag:
prec = (1/var)*spa.identity(dim, format="csr")
sqrtprec = np.sqrt(1/var)*spa.identity(dim, format="csr")
else:
prec = (1/var)*np.identity(dim)
sqrtprec = np.sqrt(1/var)*np.identity(dim)
# cov is vector
elif not spa.issparse(cov) and cov.shape[0] == np.size(cov):
logdet = np.sum(np.log(cov))
rank = dim
if sparse_flag:
prec = spa.diags(1/cov, format="csr")
sqrtprec = spa.diags(np.sqrt(1/cov), format="csr")
else:
prec = np.diag(1/cov)
sqrtprec = np.diag(np.sqrt(1/cov))
# cov diagonal
elif hasattr(cov, 'diagonal') and np.count_nonzero(cov-np.diag(cov.diagonal())) == 0:
var = cov.diagonal()
logdet = np.sum(np.log(var))
rank = dim
if sparse_flag:
prec = spa.diags(1/var, format="csr")
sqrtprec = spa.diags(np.sqrt(1/var), format="csr")
else:
prec = np.diag(1/var)
sqrtprec = np.diag(np.sqrt(1/var))
# cov is full
else:
if spa.issparse(cov):
if has_cholmod:
L_cholmod = skchol.cholesky(cov, ordering_method='natural')
prec = L_cholmod.inv()
sqrtprec = sparse_cholesky(prec)
logdet = L_cholmod.logdet()
rank = spa.csgraph.structural_rank(cov) # or nplinalg.matrix_rank(cov.todense())
# sqrtcov = L_cholmod.L()
else:
prec = spa.linalg.inv(cov)
sqrtprec = sparse_cholesky(prec)
logdet = None # np.log(nplinalg.det(cov.todense()))
rank = spa.csgraph.structural_rank(cov)
else:
if not np.allclose(cov, cov.T):
raise ValueError("Covariance matrix has to be symmetric.")
if sparse_flag:
# this comes from scipy implementation
s, u = splinalg.eigh(cov, check_finite=True)
eps = eigvalsh_to_eps(s)
if np.min(s) < -eps:
raise ValueError("The input matrix must be symmetric positive semidefinite.")
d = s[s > eps]
s_pinv = np.array([0 if abs(x) <= eps else 1/x for x in s], dtype=float)
U = np.multiply(u, np.sqrt(s_pinv))
sqrtprec = U @ np.diag(np.sign(np.diag(U))) #ensure sign is deterministic (scipy gives non-deterministic result)
sqrtprec = U.T # We want to have the columns as the eigenvectors
rank = len(d)
logdet = np.sum(np.log(d))
prec = sqrtprec.T @ sqrtprec
else:
rank = nplinalg.matrix_rank(cov)
logdet = np.log(nplinalg.det(cov))
prec = nplinalg.inv(cov)
sqrtprec = nplinalg.cholesky(prec).T
return prec, sqrtprec, logdet, rank
def get_sqrtprec_from_prec(dim, prec, sparse_flag):
""" Compute square root of precision matrix from precision matrix.
Also computes log determinant and rank of precision matrix.
Parameters
----------
dim : int
Dimension of the precision matrix.
prec : 1-d or 2-d ndarray or sparse matrix
Precision matrix. If 1-dimensional, then assumed to be a diagonal matrix.
sparse_flag: bool
Whether to store matrices as Dense or Sparse
"""
# prec is scalar
if (prec.shape[0] == 1):
precision = prec.ravel()[0]
logdet = -dim*np.log(precision)
rank = dim
if sparse_flag:
# cov = (1/precision)*spa.identity(dim, format="csr") # For computational efficiency we do not compute cov. We leave code for reference.
sqrtprec = np.sqrt(precision)*spa.identity(dim, format="csr")
else:
# cov = (1/precision)*np.identity(dim) # For computational efficiency we do not compute cov. We leave code for reference.
sqrtprec = np.sqrt(precision)*np.identity(dim)
# prec is vector
elif not spa.issparse(prec) and prec.shape[0] == np.size(prec):
logdet = np.sum(-np.log(prec))
rank = dim
if sparse_flag:
# cov = spa.diags(1/prec, format="csr") # For computational efficiency we do not compute cov. We leave code for reference.
sqrtprec = spa.diags(np.sqrt(prec), format="csr")
else:
# cov = np.diag(1/cov) # For computational efficiency we do not compute cov. We leave code for reference.
sqrtprec = np.diag(np.sqrt(prec))
# prec diagonal
elif hasattr(prec, 'diagonal') and np.count_nonzero(prec-np.diag(prec.diagonal())) == 0:
precision = prec.diagonal()
logdet = np.sum(-np.log(precision))
rank = dim
if sparse_flag:
# cov = spa.diags(1/precision, format="csr") # For computational efficiency we do not compute cov. We leave code for reference.
sqrtprec = spa.diags(np.sqrt(precision), format="csr")
else:
# cov = np.diag(1/precision) # For computational efficiency we do not compute cov. We leave code for reference.
sqrtprec = np.diag(np.sqrt(precision))
# prec is full
else:
if spa.issparse(prec):
if has_cholmod:
L_cholmod = skchol.cholesky(prec, ordering_method='natural')
sqrtprec = L_cholmod.L().T
# cov = L_cholmod.inv() # For computational efficiency we do not compute cov. We leave code for reference.
logdet = -L_cholmod.logdet()
rank = spa.csgraph.structural_rank(prec)# or nplinalg.matrix_rank(cov.todense())
else:
# cov = spa.linalg.inv(cov) # For computational efficiency we do not compute cov. We leave code for reference.
sqrtprec = sparse_cholesky(prec)
logdet = None # np.log(nplinalg.det(cov.todense()))
rank = spa.csgraph.structural_rank(prec)
else:
if not np.allclose(prec, prec.T):
raise ValueError("Precision matrix has to be symmetric.")
if sparse_flag:
s, u = splinalg.eigh(prec, check_finite=True)
eps = eigvalsh_to_eps(s)
if np.min(s) < -eps:
raise ValueError("The input matrix must be symmetric positive semidefinite.")
d = s[s > eps]
U = np.multiply(u, np.sqrt(s))
sqrtprec = U @ np.diag(np.sign(np.diag(U))) #ensure sign is deterministic (scipy gives non-deterministic result)
sqrtprec = U.T # We want to have the columns as the eigenvectors
rank = len(d)
logdet = -np.sum(np.log(d))
else:
rank = nplinalg.matrix_rank(prec)
logdet = -np.log(nplinalg.det(prec))
# cov = nplinalg.inv(prec) # For computational efficiency we do not compute cov. We leave code for reference.
sqrtprec = nplinalg.cholesky(prec).T
return sqrtprec, logdet, rank
def get_sqrtprec_from_sqrtcov(dim, sqrtcov, sparse_flag):
""" Compute square root of precision matrix from square root of covariance matrix.
Also computes log determinant and rank of precision matrix.
Parameters
----------
dim : int
Dimension of the square root of the covariance matrix.
sqrtcov : 1-d or 2-d ndarray or sparse matrix
Square root of covariance matrix. If 1-dimensional, then assumed to be a diagonal matrix.
sparse_flag: bool
Whether to store matrices as Dense or Sparse
"""
# sqrtcov is scalar
if (sqrtcov.shape[0] == 1):
sqrtcov = sqrtcov.ravel()[0]
var = sqrtcov**2
logdet = dim*np.log(var)
rank = dim
if sparse_flag:
prec = (1/var)*spa.identity(dim, format="csr")
sqrtprec = (1/sqrtcov)*spa.identity(dim, format="csr")
else:
prec = (1/var)*np.identity(dim)
sqrtprec = (1/sqrtcov)*np.identity(dim)
# sqrtcov is vector
elif not spa.issparse(sqrtcov) and sqrtcov.shape[0] == np.size(sqrtcov):
cov = sqrtcov**2
logdet = np.sum(np.log(cov))
rank = dim
if sparse_flag:
prec = spa.diags(1/cov, format="csr")
sqrtprec = spa.diags(1/sqrtcov, format="csr")
else:
prec = np.diag(1/cov)
sqrtprec = np.diag(1/sqrtcov)
# sqrtcov diagonal
elif hasattr(sqrtcov, 'diagonal') and np.count_nonzero(sqrtcov-np.diag(sqrtcov.diagonal())) == 0:
std = sqrtcov.diagonal()
var = std**2
logdet = np.sum(np.log(var))
rank = dim
if sparse_flag:
prec = spa.diags(1/var, format="csr")
sqrtprec = spa.diags(1/std, format="csr")
else:
prec = np.diag(1/var)
sqrtprec = np.diag(1/std)
# sqrtcov is full
else:
if spa.issparse(sqrtcov):
if has_cholmod:
cov = sqrtcov@sqrtcov.T
L_cholmod = skchol.cholesky(cov, ordering_method='natural')
prec = L_cholmod.inv()
sqrtprec = spa.linalg.inv(sqrtcov) # sparse_cholesky(prec)
logdet = L_cholmod.logdet()
rank = spa.csgraph.structural_rank(cov)# or nplinalg.matrix_rank(cov.todense())
# sqrtcov = L_cholmod.L() # For computational efficiency we do not compute sqrtcov. We leave code for reference.
else:
sqrtprec = spa.linalg.inv(sqrtcov)
prec = sqrtprec.T@sqrtprec
logdet = None # np.log(nplinalg.det(cov.todense()))
rank = spa.csgraph.structural_rank(prec)
else:
if sparse_flag:
# this comes from scipy implementation
cov = sqrtcov@sqrtcov.T
s, u = splinalg.eigh(cov, check_finite=True)
eps = eigvalsh_to_eps(s)
if np.min(s) < -eps:
raise ValueError("The input matrix must be symmetric positive semidefinite.")
d = s[s > eps]
s_pinv = np.array([0 if abs(x) <= eps else 1/x for x in s], dtype=float)
U = np.multiply(u, np.sqrt(s_pinv))
sqrtprec = U @ np.diag(np.sign(np.diag(U))) #ensure sign is deterministic (scipy gives non-deterministic result)
sqrtprec = U.T # We want to have the columns as the eigenvectors
rank = len(d)
logdet = np.sum(np.log(d))
prec = sqrtprec.T @ sqrtprec
else:
cov = sqrtcov@sqrtcov.T
rank = nplinalg.matrix_rank(cov)
logdet = np.log(nplinalg.det(cov))
prec = nplinalg.inv(cov)
sqrtprec = nplinalg.cholesky(prec).T
return prec, sqrtprec, logdet, rank
def get_sqrtprec_from_sqrtprec(dim, sqrtprec, sparse_flag):
""" This computes the log determinant and rank of the precision matrix from the square root of the precision matrix.
Stores the square root of the precision as a matrix.
Parameters
----------
dim : int
Dimension of the sqrtprec matrix.
sqrtprec : 1-d or 2-d ndarray or sparse matrix or scipy.sparse.linalg.LinearOperator
Square root of precision matrix. If 1-dimensional, then assumed to be a diagonal matrix.
sparse_flag: bool
Whether to store matrices as Dense or Sparse
"""
# sqrtprec is scalar
if (sqrtprec.shape[0] == 1):
logdet = -dim*np.log(sqrtprec**2)
rank = dim
dia = np.ones(dim)*sqrtprec.flatten()
if sparse_flag:
sqrtprec = spa.diags(dia)
else:
sqrtprec = np.diag(dia)
# sqrtprec is vector
elif not spa.issparse(sqrtprec) and sqrtprec.shape[0] == np.size(sqrtprec):
logdet = np.sum(-np.log(sqrtprec**2))
rank = dim
if sparse_flag:
sqrtprec = spa.diags(sqrtprec)
else:
sqrtprec = np.diag(sqrtprec)
# check if sqrtprec matrix is square
elif sqrtprec.ndim == 2 and sqrtprec.shape[0] != sqrtprec.shape[1]:
raise ValueError("sqrtprec must be square")
# sqrtprec is sparse diagonal
elif spa.isspmatrix_dia(sqrtprec):
logdet = np.sum(-np.log(sqrtprec.data**2))
rank = dim
# sqrtprec is LinearOperator
elif isinstance(sqrtprec, spa.linalg.LinearOperator):
if hasattr(sqrtprec, 'logdet'):
logdet = sqrtprec.logdet
else:
logdet = None
rank = dim
# sqrtprec diagonal
elif np.count_nonzero(sqrtprec-np.diag(sqrtprec.diagonal())) == 0:
stdinv = sqrtprec.diagonal()
precision = stdinv**2
logdet = np.sum(-np.log(precision))
rank = dim
# sqrtprec is full
else:
if spa.issparse(sqrtprec):
if has_cholmod:
prec = sqrtprec@sqrtprec.T
L_cholmod = skchol.cholesky(prec, ordering_method='natural')
logdet = -L_cholmod.logdet()
rank = spa.csgraph.structural_rank(prec)# or nplinalg.matrix_rank(cov.todense())
else:
prec = sqrtprec@sqrtprec.T
logdet = None # np.log(nplinalg.det(cov.todense()))
rank = spa.csgraph.structural_rank(prec)
else:
if sparse_flag:
prec = sqrtprec@sqrtprec.T
s, _ = splinalg.eigh(prec, check_finite=True)
eps = eigvalsh_to_eps(s)
if np.min(s) < -eps:
raise ValueError("The input matrix must be symmetric positive semidefinite.")
d = s[s > eps]
rank = len(d)
logdet = -np.sum(np.log(d))
else:
prec = sqrtprec@sqrtprec.T
rank = nplinalg.matrix_rank(prec)
logdet = -np.log(nplinalg.det(prec))
return sqrtprec, logdet, rank
def eigvalsh_to_eps(spectrum, cond=None, rcond=None):
#Determine which eigenvalues are "small" given the spectrum.
if rcond is not None:
cond = rcond
if cond in [None, -1]:
t = spectrum.dtype.char.lower()
factor = {'f': 1E3, 'd': 1E6}
cond = factor[t] * np.finfo(t).eps
eps = cond * np.max(abs(spectrum))
return eps
[docs]
class JointGaussianSqrtPrec(Distribution):
"""
Joint Gaussian probability distribution defined by means and sqrt of precision matricies of independent Gaussians.
Generates instance of cuqi.distribution.JointGaussianSqrtPrec.
Parameters
------------
means: List of means for each Gaussian distribution.
sqrtprecs: List of sqrt precision matricies for each Gaussian distribution.
"""
[docs]
def __init__(self,means=None,sqrtprecs=None,is_symmetric=True,**kwargs):
# Check if given as list
if not isinstance(means,list) or not isinstance(sqrtprecs,list):
raise ValueError("Means and sqrtprecs need to be a list of vectors and matrices respectively.")
# Force to numpy arrays
for i in range(len(means)):
means[i] = force_ndarray(means[i],flatten=True)
for i in range(len(sqrtprecs)):
sqrtprecs[i] = force_ndarray(sqrtprecs[i])
# Check dimension match TODO: move to setter methods for means and sqrtprecs
dim1 = len(means[0])
for mean in means:
if dim1 != len(mean):
raise ValueError("All means must have the same dimension")
dim2 = sqrtprecs[0].shape[1]
for sqrtprec in sqrtprecs:
if dim2 != sqrtprec.shape[1]:
raise ValueError("All sqrtprecs must have the same number of columns")
super().__init__(is_symmetric=is_symmetric,**kwargs)
self._means = means
self._sqrtprecs = sqrtprecs
self._dim = max(dim1,dim2)
def _sample(self,N):
raise NotImplementedError("Sampling not implemented")
[docs]
def logpdf(self,x):
raise NotImplementedError("pdf not implemented")
@property
def dim(self):
return self._dim
@property
def sqrtprec(self):
""" Returns the sqrt precision matrix of the joined gaussian in stacked form. """
if spa.issparse(self._sqrtprecs[0]):
return spa.vstack((self._sqrtprecs))
else:
return np.vstack((self._sqrtprecs))
@property
def sqrtprecTimesMean(self):
""" Returns the sqrt precision matrix times the mean of the distribution."""
result = []
for i in range(len(self._means)):
result.append((self._sqrtprecs[i]@self._means[i]).flatten())
return np.hstack(result)