123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605 |
- from sympy.core.basic import Basic
- from sympy.core.mul import prod
- from sympy.core.numbers import pi
- from sympy.core.singleton import S
- from sympy.functions.elementary.exponential import exp
- from sympy.functions.special.gamma_functions import multigamma
- from sympy.core.sympify import sympify, _sympify
- from sympy.matrices import (ImmutableMatrix, Inverse, Trace, Determinant,
- MatrixSymbol, MatrixBase, Transpose, MatrixSet,
- matrix2numpy)
- from sympy.stats.rv import (_value_check, RandomMatrixSymbol, NamedArgsMixin, PSpace,
- _symbol_converter, MatrixDomain, Distribution)
- from sympy.external import import_module
- ################################################################################
- #------------------------Matrix Probability Space------------------------------#
- ################################################################################
- class MatrixPSpace(PSpace):
- """
- Represents probability space for
- Matrix Distributions.
- """
- def __new__(cls, sym, distribution, dim_n, dim_m):
- sym = _symbol_converter(sym)
- dim_n, dim_m = _sympify(dim_n), _sympify(dim_m)
- if not (dim_n.is_integer and dim_m.is_integer):
- raise ValueError("Dimensions should be integers")
- return Basic.__new__(cls, sym, distribution, dim_n, dim_m)
- distribution = property(lambda self: self.args[1])
- symbol = property(lambda self: self.args[0])
- @property
- def domain(self):
- return MatrixDomain(self.symbol, self.distribution.set)
- @property
- def value(self):
- return RandomMatrixSymbol(self.symbol, self.args[2], self.args[3], self)
- @property
- def values(self):
- return {self.value}
- def compute_density(self, expr, *args):
- rms = expr.atoms(RandomMatrixSymbol)
- if len(rms) > 1 or (not isinstance(expr, RandomMatrixSymbol)):
- raise NotImplementedError("Currently, no algorithm has been "
- "implemented to handle general expressions containing "
- "multiple matrix distributions.")
- return self.distribution.pdf(expr)
- def sample(self, size=(), library='scipy', seed=None):
- """
- Internal sample method
- Returns dictionary mapping RandomMatrixSymbol to realization value.
- """
- return {self.value: self.distribution.sample(size, library=library, seed=seed)}
- def rv(symbol, cls, args):
- args = list(map(sympify, args))
- dist = cls(*args)
- dist.check(*args)
- dim = dist.dimension
- pspace = MatrixPSpace(symbol, dist, dim[0], dim[1])
- return pspace.value
- class SampleMatrixScipy:
- """Returns the sample from scipy of the given distribution"""
- def __new__(cls, dist, size, seed=None):
- return cls._sample_scipy(dist, size, seed)
- @classmethod
- def _sample_scipy(cls, dist, size, seed):
- """Sample from SciPy."""
- from scipy import stats as scipy_stats
- import numpy
- scipy_rv_map = {
- 'WishartDistribution': lambda dist, size, rand_state: scipy_stats.wishart.rvs(
- df=int(dist.n), scale=matrix2numpy(dist.scale_matrix, float), size=size),
- 'MatrixNormalDistribution': lambda dist, size, rand_state: scipy_stats.matrix_normal.rvs(
- mean=matrix2numpy(dist.location_matrix, float),
- rowcov=matrix2numpy(dist.scale_matrix_1, float),
- colcov=matrix2numpy(dist.scale_matrix_2, float), size=size, random_state=rand_state)
- }
- sample_shape = {
- 'WishartDistribution': lambda dist: dist.scale_matrix.shape,
- 'MatrixNormalDistribution' : lambda dist: dist.location_matrix.shape
- }
- dist_list = scipy_rv_map.keys()
- if dist.__class__.__name__ not in dist_list:
- return None
- if seed is None or isinstance(seed, int):
- rand_state = numpy.random.default_rng(seed=seed)
- else:
- rand_state = seed
- samp = scipy_rv_map[dist.__class__.__name__](dist, prod(size), rand_state)
- return samp.reshape(size + sample_shape[dist.__class__.__name__](dist))
- class SampleMatrixNumpy:
- """Returns the sample from numpy of the given distribution"""
- ### TODO: Add tests after adding matrix distributions in numpy_rv_map
- def __new__(cls, dist, size, seed=None):
- return cls._sample_numpy(dist, size, seed)
- @classmethod
- def _sample_numpy(cls, dist, size, seed):
- """Sample from NumPy."""
- numpy_rv_map = {
- }
- sample_shape = {
- }
- dist_list = numpy_rv_map.keys()
- if dist.__class__.__name__ not in dist_list:
- return None
- import numpy
- if seed is None or isinstance(seed, int):
- rand_state = numpy.random.default_rng(seed=seed)
- else:
- rand_state = seed
- samp = numpy_rv_map[dist.__class__.__name__](dist, prod(size), rand_state)
- return samp.reshape(size + sample_shape[dist.__class__.__name__](dist))
- class SampleMatrixPymc:
- """Returns the sample from pymc3 of the given distribution"""
- def __new__(cls, dist, size, seed=None):
- return cls._sample_pymc3(dist, size, seed)
- @classmethod
- def _sample_pymc3(cls, dist, size, seed):
- """Sample from PyMC3."""
- import pymc3
- pymc3_rv_map = {
- 'MatrixNormalDistribution': lambda dist: pymc3.MatrixNormal('X',
- mu=matrix2numpy(dist.location_matrix, float),
- rowcov=matrix2numpy(dist.scale_matrix_1, float),
- colcov=matrix2numpy(dist.scale_matrix_2, float),
- shape=dist.location_matrix.shape),
- 'WishartDistribution': lambda dist: pymc3.WishartBartlett('X',
- nu=int(dist.n), S=matrix2numpy(dist.scale_matrix, float))
- }
- sample_shape = {
- 'WishartDistribution': lambda dist: dist.scale_matrix.shape,
- 'MatrixNormalDistribution' : lambda dist: dist.location_matrix.shape
- }
- dist_list = pymc3_rv_map.keys()
- if dist.__class__.__name__ not in dist_list:
- return None
- import logging
- logging.getLogger("pymc3").setLevel(logging.ERROR)
- with pymc3.Model():
- pymc3_rv_map[dist.__class__.__name__](dist)
- samps = pymc3.sample(draws=prod(size), chains=1, progressbar=False, random_seed=seed, return_inferencedata=False, compute_convergence_checks=False)['X']
- return samps.reshape(size + sample_shape[dist.__class__.__name__](dist))
- _get_sample_class_matrixrv = {
- 'scipy': SampleMatrixScipy,
- 'pymc3': SampleMatrixPymc,
- 'numpy': SampleMatrixNumpy
- }
- ################################################################################
- #-------------------------Matrix Distribution----------------------------------#
- ################################################################################
- class MatrixDistribution(Distribution, NamedArgsMixin):
- """
- Abstract class for Matrix Distribution.
- """
- def __new__(cls, *args):
- args = [ImmutableMatrix(arg) if isinstance(arg, list)
- else _sympify(arg) for arg in args]
- return Basic.__new__(cls, *args)
- @staticmethod
- def check(*args):
- pass
- def __call__(self, expr):
- if isinstance(expr, list):
- expr = ImmutableMatrix(expr)
- return self.pdf(expr)
- def sample(self, size=(), library='scipy', seed=None):
- """
- Internal sample method
- Returns dictionary mapping RandomSymbol to realization value.
- """
- libraries = ['scipy', 'numpy', 'pymc3']
- if library not in libraries:
- raise NotImplementedError("Sampling from %s is not supported yet."
- % str(library))
- if not import_module(library):
- raise ValueError("Failed to import %s" % library)
- samps = _get_sample_class_matrixrv[library](self, size, seed)
- if samps is not None:
- return samps
- raise NotImplementedError(
- "Sampling for %s is not currently implemented from %s"
- % (self.__class__.__name__, library)
- )
- ################################################################################
- #------------------------Matrix Distribution Types-----------------------------#
- ################################################################################
- #-------------------------------------------------------------------------------
- # Matrix Gamma distribution ----------------------------------------------------
- class MatrixGammaDistribution(MatrixDistribution):
- _argnames = ('alpha', 'beta', 'scale_matrix')
- @staticmethod
- def check(alpha, beta, scale_matrix):
- if not isinstance(scale_matrix, MatrixSymbol):
- _value_check(scale_matrix.is_positive_definite, "The shape "
- "matrix must be positive definite.")
- _value_check(scale_matrix.is_square, "Should "
- "be square matrix")
- _value_check(alpha.is_positive, "Shape parameter should be positive.")
- _value_check(beta.is_positive, "Scale parameter should be positive.")
- @property
- def set(self):
- k = self.scale_matrix.shape[0]
- return MatrixSet(k, k, S.Reals)
- @property
- def dimension(self):
- return self.scale_matrix.shape
- def pdf(self, x):
- alpha, beta, scale_matrix = self.alpha, self.beta, self.scale_matrix
- p = scale_matrix.shape[0]
- if isinstance(x, list):
- x = ImmutableMatrix(x)
- if not isinstance(x, (MatrixBase, MatrixSymbol)):
- raise ValueError("%s should be an isinstance of Matrix "
- "or MatrixSymbol" % str(x))
- sigma_inv_x = - Inverse(scale_matrix)*x / beta
- term1 = exp(Trace(sigma_inv_x))/((beta**(p*alpha)) * multigamma(alpha, p))
- term2 = (Determinant(scale_matrix))**(-alpha)
- term3 = (Determinant(x))**(alpha - S(p + 1)/2)
- return term1 * term2 * term3
- def MatrixGamma(symbol, alpha, beta, scale_matrix):
- """
- Creates a random variable with Matrix Gamma Distribution.
- The density of the said distribution can be found at [1].
- Parameters
- ==========
- alpha: Positive Real number
- Shape Parameter
- beta: Positive Real number
- Scale Parameter
- scale_matrix: Positive definite real square matrix
- Scale Matrix
- Returns
- =======
- RandomSymbol
- Examples
- ========
- >>> from sympy.stats import density, MatrixGamma
- >>> from sympy import MatrixSymbol, symbols
- >>> a, b = symbols('a b', positive=True)
- >>> M = MatrixGamma('M', a, b, [[2, 1], [1, 2]])
- >>> X = MatrixSymbol('X', 2, 2)
- >>> density(M)(X).doit()
- exp(Trace(Matrix([
- [-2/3, 1/3],
- [ 1/3, -2/3]])*X)/b)*Determinant(X)**(a - 3/2)/(3**a*sqrt(pi)*b**(2*a)*gamma(a)*gamma(a - 1/2))
- >>> density(M)([[1, 0], [0, 1]]).doit()
- exp(-4/(3*b))/(3**a*sqrt(pi)*b**(2*a)*gamma(a)*gamma(a - 1/2))
- References
- ==========
- .. [1] https://en.wikipedia.org/wiki/Matrix_gamma_distribution
- """
- if isinstance(scale_matrix, list):
- scale_matrix = ImmutableMatrix(scale_matrix)
- return rv(symbol, MatrixGammaDistribution, (alpha, beta, scale_matrix))
- #-------------------------------------------------------------------------------
- # Wishart Distribution ---------------------------------------------------------
- class WishartDistribution(MatrixDistribution):
- _argnames = ('n', 'scale_matrix')
- @staticmethod
- def check(n, scale_matrix):
- if not isinstance(scale_matrix, MatrixSymbol):
- _value_check(scale_matrix.is_positive_definite, "The shape "
- "matrix must be positive definite.")
- _value_check(scale_matrix.is_square, "Should "
- "be square matrix")
- _value_check(n.is_positive, "Shape parameter should be positive.")
- @property
- def set(self):
- k = self.scale_matrix.shape[0]
- return MatrixSet(k, k, S.Reals)
- @property
- def dimension(self):
- return self.scale_matrix.shape
- def pdf(self, x):
- n, scale_matrix = self.n, self.scale_matrix
- p = scale_matrix.shape[0]
- if isinstance(x, list):
- x = ImmutableMatrix(x)
- if not isinstance(x, (MatrixBase, MatrixSymbol)):
- raise ValueError("%s should be an isinstance of Matrix "
- "or MatrixSymbol" % str(x))
- sigma_inv_x = - Inverse(scale_matrix)*x / S(2)
- term1 = exp(Trace(sigma_inv_x))/((2**(p*n/S(2))) * multigamma(n/S(2), p))
- term2 = (Determinant(scale_matrix))**(-n/S(2))
- term3 = (Determinant(x))**(S(n - p - 1)/2)
- return term1 * term2 * term3
- def Wishart(symbol, n, scale_matrix):
- """
- Creates a random variable with Wishart Distribution.
- The density of the said distribution can be found at [1].
- Parameters
- ==========
- n: Positive Real number
- Represents degrees of freedom
- scale_matrix: Positive definite real square matrix
- Scale Matrix
- Returns
- =======
- RandomSymbol
- Examples
- ========
- >>> from sympy.stats import density, Wishart
- >>> from sympy import MatrixSymbol, symbols
- >>> n = symbols('n', positive=True)
- >>> W = Wishart('W', n, [[2, 1], [1, 2]])
- >>> X = MatrixSymbol('X', 2, 2)
- >>> density(W)(X).doit()
- exp(Trace(Matrix([
- [-1/3, 1/6],
- [ 1/6, -1/3]])*X))*Determinant(X)**(n/2 - 3/2)/(2**n*3**(n/2)*sqrt(pi)*gamma(n/2)*gamma(n/2 - 1/2))
- >>> density(W)([[1, 0], [0, 1]]).doit()
- exp(-2/3)/(2**n*3**(n/2)*sqrt(pi)*gamma(n/2)*gamma(n/2 - 1/2))
- References
- ==========
- .. [1] https://en.wikipedia.org/wiki/Wishart_distribution
- """
- if isinstance(scale_matrix, list):
- scale_matrix = ImmutableMatrix(scale_matrix)
- return rv(symbol, WishartDistribution, (n, scale_matrix))
- #-------------------------------------------------------------------------------
- # Matrix Normal distribution ---------------------------------------------------
- class MatrixNormalDistribution(MatrixDistribution):
- _argnames = ('location_matrix', 'scale_matrix_1', 'scale_matrix_2')
- @staticmethod
- def check(location_matrix, scale_matrix_1, scale_matrix_2):
- if not isinstance(scale_matrix_1, MatrixSymbol):
- _value_check(scale_matrix_1.is_positive_definite, "The shape "
- "matrix must be positive definite.")
- if not isinstance(scale_matrix_2, MatrixSymbol):
- _value_check(scale_matrix_2.is_positive_definite, "The shape "
- "matrix must be positive definite.")
- _value_check(scale_matrix_1.is_square, "Scale matrix 1 should be "
- "be square matrix")
- _value_check(scale_matrix_2.is_square, "Scale matrix 2 should be "
- "be square matrix")
- n = location_matrix.shape[0]
- p = location_matrix.shape[1]
- _value_check(scale_matrix_1.shape[0] == n, "Scale matrix 1 should be"
- " of shape %s x %s"% (str(n), str(n)))
- _value_check(scale_matrix_2.shape[0] == p, "Scale matrix 2 should be"
- " of shape %s x %s"% (str(p), str(p)))
- @property
- def set(self):
- n, p = self.location_matrix.shape
- return MatrixSet(n, p, S.Reals)
- @property
- def dimension(self):
- return self.location_matrix.shape
- def pdf(self, x):
- M, U, V = self.location_matrix, self.scale_matrix_1, self.scale_matrix_2
- n, p = M.shape
- if isinstance(x, list):
- x = ImmutableMatrix(x)
- if not isinstance(x, (MatrixBase, MatrixSymbol)):
- raise ValueError("%s should be an isinstance of Matrix "
- "or MatrixSymbol" % str(x))
- term1 = Inverse(V)*Transpose(x - M)*Inverse(U)*(x - M)
- num = exp(-Trace(term1)/S(2))
- den = (2*pi)**(S(n*p)/2) * Determinant(U)**S(p)/2 * Determinant(V)**S(n)/2
- return num/den
- def MatrixNormal(symbol, location_matrix, scale_matrix_1, scale_matrix_2):
- """
- Creates a random variable with Matrix Normal Distribution.
- The density of the said distribution can be found at [1].
- Parameters
- ==========
- location_matrix: Real ``n x p`` matrix
- Represents degrees of freedom
- scale_matrix_1: Positive definite matrix
- Scale Matrix of shape ``n x n``
- scale_matrix_2: Positive definite matrix
- Scale Matrix of shape ``p x p``
- Returns
- =======
- RandomSymbol
- Examples
- ========
- >>> from sympy import MatrixSymbol
- >>> from sympy.stats import density, MatrixNormal
- >>> M = MatrixNormal('M', [[1, 2]], [1], [[1, 0], [0, 1]])
- >>> X = MatrixSymbol('X', 1, 2)
- >>> density(M)(X).doit()
- 2*exp(-Trace((Matrix([
- [-1],
- [-2]]) + X.T)*(Matrix([[-1, -2]]) + X))/2)/pi
- >>> density(M)([[3, 4]]).doit()
- 2*exp(-4)/pi
- References
- ==========
- .. [1] https://en.wikipedia.org/wiki/Matrix_normal_distribution
- """
- if isinstance(location_matrix, list):
- location_matrix = ImmutableMatrix(location_matrix)
- if isinstance(scale_matrix_1, list):
- scale_matrix_1 = ImmutableMatrix(scale_matrix_1)
- if isinstance(scale_matrix_2, list):
- scale_matrix_2 = ImmutableMatrix(scale_matrix_2)
- args = (location_matrix, scale_matrix_1, scale_matrix_2)
- return rv(symbol, MatrixNormalDistribution, args)
- #-------------------------------------------------------------------------------
- # Matrix Student's T distribution ---------------------------------------------------
- class MatrixStudentTDistribution(MatrixDistribution):
- _argnames = ('nu', 'location_matrix', 'scale_matrix_1', 'scale_matrix_2')
- @staticmethod
- def check(nu, location_matrix, scale_matrix_1, scale_matrix_2):
- if not isinstance(scale_matrix_1, MatrixSymbol):
- _value_check(scale_matrix_1.is_positive_definite != False, "The shape "
- "matrix must be positive definite.")
- if not isinstance(scale_matrix_2, MatrixSymbol):
- _value_check(scale_matrix_2.is_positive_definite != False, "The shape "
- "matrix must be positive definite.")
- _value_check(scale_matrix_1.is_square != False, "Scale matrix 1 should be "
- "be square matrix")
- _value_check(scale_matrix_2.is_square != False, "Scale matrix 2 should be "
- "be square matrix")
- n = location_matrix.shape[0]
- p = location_matrix.shape[1]
- _value_check(scale_matrix_1.shape[0] == p, "Scale matrix 1 should be"
- " of shape %s x %s" % (str(p), str(p)))
- _value_check(scale_matrix_2.shape[0] == n, "Scale matrix 2 should be"
- " of shape %s x %s" % (str(n), str(n)))
- _value_check(nu.is_positive != False, "Degrees of freedom must be positive")
- @property
- def set(self):
- n, p = self.location_matrix.shape
- return MatrixSet(n, p, S.Reals)
- @property
- def dimension(self):
- return self.location_matrix.shape
- def pdf(self, x):
- from sympy.matrices.dense import eye
- if isinstance(x, list):
- x = ImmutableMatrix(x)
- if not isinstance(x, (MatrixBase, MatrixSymbol)):
- raise ValueError("%s should be an isinstance of Matrix "
- "or MatrixSymbol" % str(x))
- nu, M, Omega, Sigma = self.nu, self.location_matrix, self.scale_matrix_1, self.scale_matrix_2
- n, p = M.shape
- K = multigamma((nu + n + p - 1)/2, p) * Determinant(Omega)**(-n/2) * Determinant(Sigma)**(-p/2) \
- / ((pi)**(n*p/2) * multigamma((nu + p - 1)/2, p))
- return K * (Determinant(eye(n) + Inverse(Sigma)*(x - M)*Inverse(Omega)*Transpose(x - M))) \
- **(-(nu + n + p -1)/2)
- def MatrixStudentT(symbol, nu, location_matrix, scale_matrix_1, scale_matrix_2):
- """
- Creates a random variable with Matrix Gamma Distribution.
- The density of the said distribution can be found at [1].
- Parameters
- ==========
- nu: Positive Real number
- degrees of freedom
- location_matrix: Positive definite real square matrix
- Location Matrix of shape ``n x p``
- scale_matrix_1: Positive definite real square matrix
- Scale Matrix of shape ``p x p``
- scale_matrix_2: Positive definite real square matrix
- Scale Matrix of shape ``n x n``
- Returns
- =======
- RandomSymbol
- Examples
- ========
- >>> from sympy import MatrixSymbol,symbols
- >>> from sympy.stats import density, MatrixStudentT
- >>> v = symbols('v',positive=True)
- >>> M = MatrixStudentT('M', v, [[1, 2]], [[1, 0], [0, 1]], [1])
- >>> X = MatrixSymbol('X', 1, 2)
- >>> density(M)(X)
- gamma(v/2 + 1)*Determinant((Matrix([[-1, -2]]) + X)*(Matrix([
- [-1],
- [-2]]) + X.T) + Matrix([[1]]))**(-v/2 - 1)/(pi**1.0*gamma(v/2)*Determinant(Matrix([[1]]))**1.0*Determinant(Matrix([
- [1, 0],
- [0, 1]]))**0.5)
- References
- ==========
- .. [1] https://en.wikipedia.org/wiki/Matrix_t-distribution
- """
- if isinstance(location_matrix, list):
- location_matrix = ImmutableMatrix(location_matrix)
- if isinstance(scale_matrix_1, list):
- scale_matrix_1 = ImmutableMatrix(scale_matrix_1)
- if isinstance(scale_matrix_2, list):
- scale_matrix_2 = ImmutableMatrix(scale_matrix_2)
- args = (nu, location_matrix, scale_matrix_1, scale_matrix_2)
- return rv(symbol, MatrixStudentTDistribution, args)
|