From 3dfce04e405dcd183fd6a3b3633f87d9eda8fe7e Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Fri, 12 Jun 2020 15:38:31 +0200 Subject: [PATCH 01/14] treat array of shape (1,) as scalar --- pymc3/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymc3/util.py b/pymc3/util.py index ce3782ada1..196c7be008 100644 --- a/pymc3/util.py +++ b/pymc3/util.py @@ -140,7 +140,7 @@ def get_variable_name(variable): except IndexError: pass value = variable.eval() - if not value.shape: + if not value.shape or value.shape == (1,): return asscalar(value) return "array" return r"\text{%s}" % name From c4851618a6538886e1c993e91d1c2d256ab3900a Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Mon, 10 Aug 2020 16:59:00 +0200 Subject: [PATCH 02/14] adding generic machinery for generating string representations --- pymc3/distributions/distribution.py | 45 +++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/pymc3/distributions/distribution.py b/pymc3/distributions/distribution.py index 2bdf9d88c0..7c9465d257 100644 --- a/pymc3/distributions/distribution.py +++ b/pymc3/distributions/distribution.py @@ -15,6 +15,7 @@ import numbers import contextvars import dill +import inspect from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Optional, Callable @@ -22,6 +23,7 @@ import numpy as np import theano.tensor as tt from theano import function +from pymc3.util import get_variable_name import theano from ..memoize import memoize from ..model import ( @@ -135,9 +137,48 @@ def getattr_value(self, val): return val - def _repr_latex_(self, name=None, dist=None): + def _distr_parameters(self): + """Return the names of the parameters for this distribution (e.g. "mu" + and "sigma" for Normal). Used in generating string (and LaTeX etc.) + representations of Distribution objects. By default based on inspection + of __init__, but can be overwritten if necessary (e.g. to avoid including + "sd" and "tau"). + """ + return inspect.getfullargspec(self.__init__).args[1:] + + def _distr_name(self): + return self.__class__.__name__ + + def _str_repr(self, name=None, dist=None, formatting='plain'): + """Generate string representation for this distribution, optionally + including LaTeX markup (formatting='latex'). + """ + if dist is None: + dist = self + if name is None: + name = '[unnamed]' + + param_names = self._distr_parameters() + param_values = [get_variable_name(getattr(dist, x)) for x in param_names] + + if formatting == "latex": + param_string = ",~".join([r"\mathit{{{name}}}={value}".format(name=name, + value=value) for name, value in zip(param_names, param_values)]) + return r"$\text{{{var_name}}} \sim \text{{{distr_name}}}({params})$".format(var_name=name, + distr_name=dist._distr_name(), params=param_string) + else: + # 'plain' is default option + param_string = ", ".join(["{name}={value}".format(name=name, + value=value) for name, value in zip(param_names, param_values)]) + return "{var_name} ~ {distr_name}({params})".format(var_name=name, + distr_name=dist._distr_name(), params=param_string) + + def __str__(self, **kwargs): + return self._str_repr(formatting='plain', **kwargs) + + def _repr_latex_(self, **kwargs): """Magic method name for IPython to use for LaTeX formatting.""" - return None + return self._str_repr(formatting='latex', **kwargs) def logp_nojac(self, *args, **kwargs): """Return the logp, but do not include a jacobian term for transforms. From 4ec411e9c3032f2f3fa24cec6c473f6b067841fc Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Mon, 10 Aug 2020 17:07:40 +0200 Subject: [PATCH 03/14] double quotes for strings --- pymc3/distributions/distribution.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pymc3/distributions/distribution.py b/pymc3/distributions/distribution.py index 7c9465d257..ea38c8acaa 100644 --- a/pymc3/distributions/distribution.py +++ b/pymc3/distributions/distribution.py @@ -174,11 +174,11 @@ def _str_repr(self, name=None, dist=None, formatting='plain'): distr_name=dist._distr_name(), params=param_string) def __str__(self, **kwargs): - return self._str_repr(formatting='plain', **kwargs) + return self._str_repr(formatting="plain", **kwargs) def _repr_latex_(self, **kwargs): """Magic method name for IPython to use for LaTeX formatting.""" - return self._str_repr(formatting='latex', **kwargs) + return self._str_repr(formatting="latex", **kwargs) def logp_nojac(self, *args, **kwargs): """Return the logp, but do not include a jacobian term for transforms. From 65eed8599bdef145bc189b9f0e56729dc1467485 Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Tue, 11 Aug 2020 09:20:21 +0200 Subject: [PATCH 04/14] Update pymc3/distributions/distribution.py Co-authored-by: Thomas Wiecki --- pymc3/distributions/distribution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymc3/distributions/distribution.py b/pymc3/distributions/distribution.py index ea38c8acaa..8805d57ee3 100644 --- a/pymc3/distributions/distribution.py +++ b/pymc3/distributions/distribution.py @@ -23,7 +23,7 @@ import numpy as np import theano.tensor as tt from theano import function -from pymc3.util import get_variable_name +from .util import get_variable_name import theano from ..memoize import memoize from ..model import ( From 15c62e8748ba85e47925f499eb29c0a915431e35 Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Tue, 11 Aug 2020 09:22:25 +0200 Subject: [PATCH 05/14] restoring return None behavior of TransformedDistribution::_repr_latex_ --- pymc3/distributions/transforms.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pymc3/distributions/transforms.py b/pymc3/distributions/transforms.py index 48f1d27fc3..af7ff83256 100644 --- a/pymc3/distributions/transforms.py +++ b/pymc3/distributions/transforms.py @@ -196,6 +196,11 @@ def logp_nojac(self, x): """ return self.dist.logp(self.transform_used.backward(x)) + def _repr_latex_(self, **kwargs): + # prevent TransformedDistributions from ending up in LaTeX representations + # of models + return None + transform = Transform From 4d0f16025e33b9bac0210a8b067d05a0af37d7fe Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Tue, 11 Aug 2020 09:28:17 +0200 Subject: [PATCH 06/14] extra . for import --- pymc3/distributions/distribution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pymc3/distributions/distribution.py b/pymc3/distributions/distribution.py index 8805d57ee3..c4be27715a 100644 --- a/pymc3/distributions/distribution.py +++ b/pymc3/distributions/distribution.py @@ -23,7 +23,7 @@ import numpy as np import theano.tensor as tt from theano import function -from .util import get_variable_name +from ..util import get_variable_name import theano from ..memoize import memoize from ..model import ( From d8fa4cd4ef2ebc66d46dfacacfd027d7e21a8478 Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Tue, 11 Aug 2020 09:35:38 +0200 Subject: [PATCH 07/14] renaming _distr_parameters() to _distr_parameters_for_repr() to avoid confusion --- pymc3/distributions/distribution.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pymc3/distributions/distribution.py b/pymc3/distributions/distribution.py index c4be27715a..7ec424266a 100644 --- a/pymc3/distributions/distribution.py +++ b/pymc3/distributions/distribution.py @@ -137,7 +137,7 @@ def getattr_value(self, val): return val - def _distr_parameters(self): + def _distr_parameters_for_repr(self): """Return the names of the parameters for this distribution (e.g. "mu" and "sigma" for Normal). Used in generating string (and LaTeX etc.) representations of Distribution objects. By default based on inspection @@ -146,7 +146,7 @@ def _distr_parameters(self): """ return inspect.getfullargspec(self.__init__).args[1:] - def _distr_name(self): + def _distr_name_for_repr(self): return self.__class__.__name__ def _str_repr(self, name=None, dist=None, formatting='plain'): @@ -158,20 +158,20 @@ def _str_repr(self, name=None, dist=None, formatting='plain'): if name is None: name = '[unnamed]' - param_names = self._distr_parameters() + param_names = self._distr_parameters_for_repr() param_values = [get_variable_name(getattr(dist, x)) for x in param_names] if formatting == "latex": param_string = ",~".join([r"\mathit{{{name}}}={value}".format(name=name, value=value) for name, value in zip(param_names, param_values)]) return r"$\text{{{var_name}}} \sim \text{{{distr_name}}}({params})$".format(var_name=name, - distr_name=dist._distr_name(), params=param_string) + distr_name=dist._distr_name_for_repr(), params=param_string) else: # 'plain' is default option param_string = ", ".join(["{name}={value}".format(name=name, value=value) for name, value in zip(param_names, param_values)]) return "{var_name} ~ {distr_name}({params})".format(var_name=name, - distr_name=dist._distr_name(), params=param_string) + distr_name=dist._distr_name_for_repr(), params=param_string) def __str__(self, **kwargs): return self._str_repr(formatting="plain", **kwargs) From c67300289fdf604d858bd9c1e857fffa3fdd6225 Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Tue, 11 Aug 2020 10:29:28 +0200 Subject: [PATCH 08/14] replacing old _repr_latex_ functionality with new one --- pymc3/distributions/continuous.py | 307 ++++------------------------ pymc3/distributions/discrete.py | 139 +------------ pymc3/distributions/distribution.py | 3 + pymc3/distributions/mixture.py | 15 +- pymc3/distributions/multivariate.py | 65 +----- pymc3/distributions/timeseries.py | 69 +------ pymc3/tests/test_distributions.py | 2 +- 7 files changed, 67 insertions(+), 533 deletions(-) diff --git a/pymc3/distributions/continuous.py b/pymc3/distributions/continuous.py index b9f0a74b8c..9cf2f40b63 100644 --- a/pymc3/distributions/continuous.py +++ b/pymc3/distributions/continuous.py @@ -239,15 +239,6 @@ def logp(self, value): return bound(-tt.log(upper - lower), value >= lower, value <= upper) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - lower = dist.lower - upper = dist.upper - name = r'\text{%s}' % name - return r'${} \sim \text{{Uniform}}(\mathit{{lower}}={},~\mathit{{upper}}={})$'.format( - name, get_variable_name(lower), get_variable_name(upper)) - def logcdf(self, value): """ Compute the log of the cumulative distribution function for Uniform distribution @@ -315,10 +306,6 @@ def logp(self, value): """ return tt.zeros_like(value) - def _repr_latex_(self, name=None, dist=None): - name = r'\text{%s}' % name - return r'${} \sim \text{{Flat}}()$'.format(name) - def logcdf(self, value): """ Compute the log of the cumulative distribution function for Flat distribution @@ -382,10 +369,6 @@ def logp(self, value): """ return bound(tt.zeros_like(value), value > 0) - def _repr_latex_(self, name=None, dist=None): - name = r'\text{%s}' % name - return r'${} \sim \text{{HalfFlat}}()$'.format(name) - def logcdf(self, value): """ Compute the log of the cumulative distribution function for HalfFlat distribution @@ -538,15 +521,8 @@ def logp(self, value): return bound((-tau * (value - mu)**2 + tt.log(tau / np.pi / 2.)) / 2., sigma > 0) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - sigma = dist.sigma - mu = dist.mu - name = r'\text{%s}' % name - return r'${} \sim \text{{Normal}}(\mathit{{mu}}={},~\mathit{{sigma}}={})$'.format(name, - get_variable_name(mu), - get_variable_name(sigma)) + def _distr_parameters_for_repr(self): + return ["mu", "sigma"] def logcdf(self, value): """ @@ -766,21 +742,8 @@ def _normalization(self): else: return normal_lcdf(mu, sigma, self.upper) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - name = r'\text{%s}' % name - return ( - r'${} \sim \text{{TruncatedNormal}}(' - r'\mathit{{mu}}={},~\mathit{{sigma}}={},a={},b={})$' - .format( - name, - get_variable_name(self.mu), - get_variable_name(self.sigma), - get_variable_name(self.lower), - get_variable_name(self.upper), - ) - ) + def _distr_parameters_for_repr(self): + return ["mu", "sigma", "lower", "upper"] class HalfNormal(PositiveContinuous): @@ -907,13 +870,8 @@ def logp(self, value): value >= 0, tau > 0, sigma > 0) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - sigma = dist.sigma - name = r'\text{%s}' % name - return r'${} \sim \text{{HalfNormal}}(\mathit{{sigma}}={})$'.format(name, - get_variable_name(sigma)) + def _distr_parameters_for_repr(self): + return ["sigma"] def logcdf(self, value): """ @@ -1110,17 +1068,8 @@ def logp(self, value): value > 0, value - alpha > 0, mu > 0, lam > 0, alpha >= 0) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - lam = dist.lam - mu = dist.mu - alpha = dist.alpha - name = r'\text{%s}' % name - return r'${} \sim \text{{Wald}}(\mathit{{mu}}={},~\mathit{{lam}}={},~\mathit{{alpha}}={})$'.format(name, - get_variable_name(mu), - get_variable_name(lam), - get_variable_name(alpha)) + def _distr_parameters_for_repr(self): + return ["mu", "lam", "alpha"] def logcdf(self, value): """ @@ -1351,15 +1300,8 @@ def logcdf(self, value): ) ) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - alpha = dist.alpha - beta = dist.beta - name = r'\text{%s}' % name - return r'${} \sim \text{{Beta}}(\mathit{{alpha}}={},~\mathit{{beta}}={})$'.format(name, - get_variable_name(alpha), - get_variable_name(beta)) + def _distr_parameters_for_repr(self): + return ["alpha", "beta"] class Kumaraswamy(UnitContinuous): R""" @@ -1467,16 +1409,6 @@ def logp(self, value): value >= 0, value <= 1, a > 0, b > 0) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - a = dist.a - b = dist.b - name = r'\text{%s}' % name - return r'${} \sim \text{{Kumaraswamy}}(\mathit{{a}}={},~\mathit{{b}}={})$'.format(name, - get_variable_name(a), - get_variable_name(b)) - class Exponential(PositiveContinuous): R""" @@ -1565,14 +1497,6 @@ def logp(self, value): lam = self.lam return bound(tt.log(lam) - lam * value, value >= 0, lam > 0) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - lam = dist.lam - name = r'\text{%s}' % name - return r'${} \sim \text{{Exponential}}(\mathit{{lam}}={})$'.format(name, - get_variable_name(lam)) - def logcdf(self, value): r""" Compute the log of cumulative distribution function for the Exponential distribution @@ -1700,16 +1624,6 @@ def logp(self, value): return -tt.log(2 * b) - abs(value - mu) / b - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - b = dist.b - mu = dist.mu - name = r'\text{%s}' % name - return r'${} \sim \text{{Laplace}}(\mathit{{mu}}={},~\mathit{{b}}={})$'.format(name, - get_variable_name(mu), - get_variable_name(b)) - def logcdf(self, value): """ Compute the log of the cumulative distribution function for Laplace distribution @@ -1871,15 +1785,8 @@ def logp(self, value): - tt.log(value), tau > 0) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - tau = dist.tau - mu = dist.mu - name = r'\text{%s}' % name - return r'${} \sim \text{{Lognormal}}(\mathit{{mu}}={},~\mathit{{tau}}={})$'.format(name, - get_variable_name(mu), - get_variable_name(tau)) + def _distr_parameters_for_repr(self): + return ["mu", "tau"] def logcdf(self, value): """ @@ -2045,17 +1952,8 @@ def logp(self, value): - (nu + 1.0) / 2.0 * tt.log1p(lam * (value - mu)**2 / nu), lam > 0, nu > 0, sigma > 0) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - nu = dist.nu - mu = dist.mu - lam = dist.lam - name = r'\text{%s}' % name - return r'${} \sim \text{{StudentT}}(\mathit{{nu}}={},~\mathit{{mu}}={},~\mathit{{lam}}={})$'.format(name, - get_variable_name(nu), - get_variable_name(mu), - get_variable_name(lam)) + def _distr_parameters_for_repr(self): + return ["nu", "mu", "lam"] def logcdf(self, value): """ @@ -2192,15 +2090,8 @@ def logp(self, value): - logpow(value, alpha + 1), value >= m, alpha > 0, m > 0) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - alpha = dist.alpha - m = dist.m - name = r'\text{%s}' % name - return r'${} \sim \text{{Pareto}}(\mathit{{alpha}}={},~\mathit{{m}}={})$'.format(name, - get_variable_name(alpha), - get_variable_name(m)) + def _distr_parameters_for_repr(self): + return ["alpha", "m"] def logcdf(self, value): """ @@ -2330,16 +2221,6 @@ def logp(self, value): - tt.log1p(((value - alpha) / beta)**2), beta > 0) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - alpha = dist.alpha - beta = dist.beta - name = r'\text{%s}' % name - return r'${} \sim \text{{Cauchy}}(\mathit{{alpha}}={},~\mathit{{beta}}={})$'.format(name, - get_variable_name(alpha), - get_variable_name(beta)) - def logcdf(self, value): """ Compute the log of the cumulative distribution function for Cauchy distribution @@ -2450,14 +2331,6 @@ def logp(self, value): - tt.log1p((value / beta)**2), value >= 0, beta > 0) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - beta = dist.beta - name = r'\text{%s}' % name - return r'${} \sim \text{{HalfCauchy}}(\mathit{{beta}}={})$'.format(name, - get_variable_name(beta)) - def logcdf(self, value): """ Compute the log of the cumulative distribution function for HalfCauchy distribution @@ -2641,15 +2514,8 @@ def logcdf(self, value): alpha > 0, beta > 0) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - beta = dist.beta - alpha = dist.alpha - name = r'\text{%s}' % name - return r'${} \sim \text{{Gamma}}(\mathit{{alpha}}={},~\mathit{{beta}}={})$'.format(name, - get_variable_name(alpha), - get_variable_name(beta)) + def _distr_parameters_for_repr(self): + return ["alpha", "beta"] class InverseGamma(PositiveContinuous): @@ -2791,15 +2657,8 @@ def logp(self, value): + logpow(value, -alpha - 1), value > 0, alpha > 0, beta > 0) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - beta = dist.beta - alpha = dist.alpha - name = r'\text{%s}' % name - return r'${} \sim \text{{InverseGamma}}(\mathit{{alpha}}={},~\mathit{{beta}}={})$'.format(name, - get_variable_name(alpha), - get_variable_name(beta)) + def _distr_parameters_for_repr(self): + return ["alpha", "beta"] class ChiSquared(Gamma): @@ -2844,14 +2703,6 @@ def __init__(self, nu, *args, **kwargs): self.nu = nu = tt.as_tensor_variable(floatX(nu)) super().__init__(alpha=nu / 2., beta=0.5, *args, **kwargs) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - nu = dist.nu - name = r'\text{%s}' % name - return r'${} \sim \Chi^2(\mathit{{nu}}={})$'.format(name, - get_variable_name(nu)) - class Weibull(PositiveContinuous): R""" @@ -2959,16 +2810,6 @@ def logp(self, value): - (value / beta)**alpha, value >= 0, alpha > 0, beta > 0) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - beta = dist.beta - alpha = dist.alpha - name = r'\text{%s}' % name - return r'${} \sim \text{{Weibull}}(\mathit{{alpha}}={},~\mathit{{beta}}={})$'.format(name, - get_variable_name(alpha), - get_variable_name(beta)) - def logcdf(self, value): r""" Compute the log of the cumulative distribution function for Weibull distribution @@ -3127,15 +2968,8 @@ def logp(self, value): - (nu + 1.0) / 2.0 * tt.log1p(value ** 2 / (nu * sigma**2)), sigma > 0, lam > 0, nu > 0, value >= 0) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - nu = dist.nu - sigma = dist.sigma - name = r'\text{%s}' % name - return r'${} \sim \text{{HalfStudentT}}(\mathit{{nu}}={},~\mathit{{sigma}}={})$'.format(name, - get_variable_name(nu), - get_variable_name(sigma)) + def _distr_parameters_for_repr(self): + return ["nu", "lam"] class ExGaussian(Continuous): @@ -3285,17 +3119,8 @@ def logp(self, value): return bound(lp, sigma > 0.0, nu > 0.0) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - sigma = dist.sigma - mu = dist.mu - nu = dist.nu - name = r'\text{%s}' % name - return r'${} \sim \text{{ExGaussian}}(\mathit{{mu}}={},~\mathit{{sigma}}={},~\mathit{{nu}}={})$'.format(name, - get_variable_name(mu), - get_variable_name(sigma), - get_variable_name(nu)) + def _distr_parameters_for_repr(self): + return ["mu", "sigma", "nu"] def logcdf(self, value): """ @@ -3429,16 +3254,8 @@ def logp(self, value): return bound(kappa * tt.cos(mu - value) - (tt.log(2 * np.pi) + log_i0(kappa)), kappa > 0, value >= -np.pi, value <= np.pi) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - kappa = dist.kappa - mu = dist.mu - name = r'\text{%s}' % name - return r'${} \sim \text{{VonMises}}(\mathit{{mu}}={},~\mathit{{kappa}}={})$'.format(name, - get_variable_name(mu), - get_variable_name(kappa)) - + def _distr_parameters_for_repr(self): + return ["mu", "kappa"] class SkewNormal(Continuous): @@ -3572,17 +3389,8 @@ def logp(self, value): + tt.log(tau / np.pi / 2.)) / 2., tau > 0, sigma > 0) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - sigma = dist.sigma - mu = dist.mu - alpha = dist.alpha - name = r'\text{%s}' % name - return r'${} \sim \text{{Skew-Normal}}(\mathit{{mu}}={},~\mathit{{sigma}}={},~\mathit{{alpha}}={})$'.format(name, - get_variable_name(mu), - get_variable_name(sigma), - get_variable_name(alpha)) + def _distr_parameters_for_repr(self): + return ["mu", "sigma", "alpha"] class Triangular(BoundedContinuous): @@ -3708,18 +3516,6 @@ def logp(self, value): tt.log(2 * (upper - value) / ((upper - lower) * (upper - c))), np.inf))) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - lower = dist.lower - upper = dist.upper - c = dist.c - name = r'\text{%s}' % name - return r'${} \sim \text{{Triangular}}(\mathit{{c}}={},~\mathit{{lower}}={},~\mathit{{upper}}={})$'.format(name, - get_variable_name(c), - get_variable_name(lower), - get_variable_name(upper)) - def logcdf(self, value): """ Compute the log of the cumulative distribution function for Triangular distribution @@ -3853,16 +3649,6 @@ def logp(self, value): scaled = (value - self.mu) / self.beta return bound(-scaled - tt.exp(-scaled) - tt.log(self.beta), self.beta > 0) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - beta = dist.beta - mu = dist.mu - name = r'\text{%s}' % name - return r'${} \sim \text{{Gumbel}}(\mathit{{mu}}={},~\mathit{{beta}}={})$'.format(name, - get_variable_name(mu), - get_variable_name(beta)) - def logcdf(self, value): """ Compute the log of the cumulative distribution function for Gumbel distribution @@ -4031,6 +3817,9 @@ def logp(self, value): value > 0, ) + def _distr_parameters_for_repr(self): + return ["nu", "sigma"] + class Logistic(Continuous): R""" @@ -4129,16 +3918,6 @@ def random(self, point=None, size=None): dist_shape=self.shape, size=size) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - mu = dist.mu - s = dist.s - name = r'\text{%s}' % name - return r'${} \sim \text{{Logistic}}(\mathit{{mu}}={},~\mathit{{s}}={})$'.format(name, - get_variable_name(mu), - get_variable_name(s)) - def logcdf(self, value): r""" Compute the log of the cumulative distribution function for Logistic distribution @@ -4280,15 +4059,8 @@ def logp(self, value): + 0.5 * tt.log(tau / (2. * np.pi)) - tt.log(value * (1 - value)), value > 0, value < 1, tau > 0) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - sigma = dist.sigma - mu = dist.mu - name = r'\text{%s}' % name - return r'${} \sim \text{{LogitNormal}}(\mathit{{mu}}={},~\mathit{{sigma}}={})$'.format(name, - get_variable_name(mu), - get_variable_name(sigma)) + def _distr_parameters_for_repr(self): + return ["mu", "sigma"] class Interpolated(BoundedContinuous): @@ -4392,6 +4164,9 @@ def logp(self, value): """ return tt.log(self.interp_op(value) / self.Z) + def _distr_parameters_for_repr(self): + return [] + class Moyal(Continuous): R""" @@ -4494,16 +4269,6 @@ def logp(self, value): - tt.log(self.sigma) - (1 / 2) * tt.log(2 * np.pi)), self.sigma > 0) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - sigma = dist.sigma - mu = dist.mu - name = r'\text{%s}' % name - return r'${} \sim \text{{Moyal}}(\mathit{{mu}}={},~\mathit{{sigma}}={})$'.format(name, - get_variable_name(mu), - get_variable_name(sigma)) - def logcdf(self, value): """ Compute the log of the cumulative distribution function for Moyal distribution diff --git a/pymc3/distributions/discrete.py b/pymc3/distributions/discrete.py index c5d43706f3..a8057b32ad 100644 --- a/pymc3/distributions/discrete.py +++ b/pymc3/distributions/discrete.py @@ -123,15 +123,6 @@ def logp(self, value): 0 <= value, value <= n, 0 <= p, p <= 1) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - n = dist.n - p = dist.p - name = r'\text{%s}' % name - return r'${} \sim \text{{Binomial}}(\mathit{{n}}={},~\mathit{{p}}={})$'.format(name, - get_variable_name(n), - get_variable_name(p)) class BetaBinomial(Discrete): R""" @@ -259,16 +250,6 @@ def logp(self, value): value >= 0, value <= self.n, alpha > 0, beta > 0) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - alpha = dist.alpha - beta = dist.beta - name = r'\text{%s}' % name - return r'${} \sim \text{{BetaBinomial}}(\mathit{{alpha}}={},~\mathit{{beta}}={})$'.format(name, - get_variable_name(alpha), - get_variable_name(beta)) - class Bernoulli(Discrete): R"""Bernoulli log-likelihood @@ -371,13 +352,8 @@ def logp(self, value): value >= 0, value <= 1, p >= 0, p <= 1) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - p = dist.p - name = r'\text{%s}' % name - return r'${} \sim \text{{Bernoulli}}(\mathit{{p}}={})$'.format(name, - get_variable_name(p)) + def _distr_parameters_for_repr(self): + return ["p"] class DiscreteWeibull(Discrete): @@ -486,16 +462,6 @@ def random(self, point=None, size=None): dist_shape=self.shape, size=size) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - q = dist.q - beta = dist.beta - name = r'\text{%s}' % name - return r'${} \sim \text{{DiscreteWeibull}}(\mathit{{q}}={},~\mathit{{beta}}={})$'.format(name, - get_variable_name(q), - get_variable_name(beta)) - class Poisson(Discrete): R""" @@ -590,14 +556,6 @@ def logp(self, value): return tt.switch(tt.eq(mu, 0) * tt.eq(value, 0), 0, log_prob) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - mu = dist.mu - name = r'\text{%s}' % name - return r'${} \sim \text{{Poisson}}(\mathit{{mu}}={})$'.format(name, - get_variable_name(mu)) - class NegativeBinomial(Discrete): R""" @@ -717,16 +675,6 @@ def logp(self, value): Poisson.dist(self.mu).logp(value), negbinom) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - mu = dist.mu - alpha = dist.alpha - name = r'\text{%s}' % name - return r'${} \sim \text{{NegativeBinomial}}(\mathit{{mu}}={},~\mathit{{alpha}}={})$'.format(name, - get_variable_name(mu), - get_variable_name(alpha)) - class Geometric(Discrete): R""" @@ -810,14 +758,6 @@ def logp(self, value): return bound(tt.log(p) + logpow(1 - p, value - 1), 0 <= p, p <= 1, value >= 1) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - p = dist.p - name = r'\text{%s}' % name - return r'${} \sim \text{{Geometric}}(\mathit{{p}}={})$'.format(name, - get_variable_name(p)) - class DiscreteUniform(Discrete): R""" @@ -913,16 +853,6 @@ def logp(self, value): return bound(-tt.log(upper - lower + 1), lower <= value, value <= upper) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - lower = dist.lower - upper = dist.upper - name = r'\text{%s}' % name - return r'${} \sim \text{{DiscreteUniform}}(\mathit{{lower}}={},~\mathit{{upper}}={})$'.format(name, - get_variable_name(lower), - get_variable_name(upper)) - class Categorical(Discrete): R""" @@ -1044,14 +974,6 @@ def logp(self, value): return bound(a, value >= 0, value <= (k - 1), tt.all(p_ >= 0, axis=-1), tt.all(p <= 1, axis=-1)) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - p = dist.p - name = r'\text{%s}' % name - return r'${} \sim \text{{Categorical}}(\mathit{{p}}={})$'.format(name, - get_variable_name(p)) - class Constant(Discrete): r""" @@ -1112,12 +1034,6 @@ def logp(self, value): c = self.c return bound(0, tt.eq(value, c)) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - name = r'\text{%s}' % name - return r'${} \sim \text{{Constant}}()$'.format(name) - ConstantDist = Constant @@ -1231,16 +1147,6 @@ def logp(self, value): 0 <= psi, psi <= 1, 0 <= theta) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - theta = dist.theta - psi = dist.psi - name = r'\text{%s}' % name - return r'${} \sim \text{{ZeroInflatedPoisson}}(\mathit{{theta}}={},~\mathit{{psi}}={})$'.format(name, - get_variable_name(theta), - get_variable_name(psi)) - class ZeroInflatedBinomial(Discrete): R""" @@ -1354,22 +1260,6 @@ def logp(self, value): 0 <= psi, psi <= 1, 0 <= p, p <= 1) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - n = dist.n - p = dist.p - psi = dist.psi - - name_n = get_variable_name(n) - name_p = get_variable_name(p) - name_psi = get_variable_name(psi) - name = r'\text{%s}' % name - return (r'${} \sim \text{{ZeroInflatedBinomial}}' - r'(\mathit{{n}}={},~\mathit{{p}}={},~' - r'\mathit{{psi}}={})$' - .format(name, name_n, name_p, name_psi)) - class ZeroInflatedNegativeBinomial(Discrete): R""" @@ -1523,22 +1413,6 @@ def logp(self, value): 0 <= psi, psi <= 1, mu > 0, alpha > 0) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - mu = dist.mu - alpha = dist.alpha - psi = dist.psi - - name_mu = get_variable_name(mu) - name_alpha = get_variable_name(alpha) - name_psi = get_variable_name(psi) - name = r'\text{%s}' % name - return (r'${} \sim \text{{ZeroInflatedNegativeBinomial}}' - r'(\mathit{{mu}}={},~\mathit{{alpha}}={},~' - r'\mathit{{psi}}={})$' - .format(name, name_mu, name_alpha, name_psi)) - class OrderedLogistic(Categorical): R""" @@ -1619,12 +1493,3 @@ def __init__(self, eta, cutpoints, *args, **kwargs): p = p_cum[..., 1:] - p_cum[..., :-1] super().__init__(p=p, *args, **kwargs) - - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - name_eta = get_variable_name(dist.eta) - name_cutpoints = get_variable_name(dist.cutpoints) - return (r'${} \sim \text{{OrderedLogistic}}' - r'(\mathit{{eta}}={}, \mathit{{cutpoints}}={}$' - .format(name, name_eta, name_cutpoints)) diff --git a/pymc3/distributions/distribution.py b/pymc3/distributions/distribution.py index 7ec424266a..8d5f115f2d 100644 --- a/pymc3/distributions/distribution.py +++ b/pymc3/distributions/distribution.py @@ -542,6 +542,9 @@ def random(self, point=None, size=None, **kwargs): "Define a custom random method and pass it as kwarg random" ) + def _distr_parameters_for_repr(self): + return [] + class _DrawValuesContext(metaclass=ContextMeta, context_class='_DrawValuesContext'): """ A context manager class used while drawing values with draw_values diff --git a/pymc3/distributions/mixture.py b/pymc3/distributions/mixture.py index a041c3bc15..87afc85e79 100644 --- a/pymc3/distributions/mixture.py +++ b/pymc3/distributions/mixture.py @@ -578,6 +578,8 @@ def random(self, point=None, size=None): samples = np.reshape(samples, size + dist_shape) return samples + def _distr_parameters_for_repr(self): + return [] class NormalMixture(Mixture): R""" @@ -627,14 +629,5 @@ def __init__(self, w, mu, sigma=None, tau=None, sd=None, comp_shape=(), *args, * super().__init__(w, Normal.dist(mu, sigma=sigma, shape=comp_shape), *args, **kwargs) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - mu = dist.mu - w = dist.w - sigma = dist.sigma - name = r'\text{%s}' % name - return r'${} \sim \text{{NormalMixture}}(\mathit{{w}}={},~\mathit{{mu}}={},~\mathit{{sigma}}={})$'.format(name, - get_variable_name(w), - get_variable_name(mu), - get_variable_name(sigma)) + def _distr_parameters_for_repr(self): + return ["w", "mu", "sigma"] diff --git a/pymc3/distributions/multivariate.py b/pymc3/distributions/multivariate.py index d3c76d5df3..6b19089c0e 100755 --- a/pymc3/distributions/multivariate.py +++ b/pymc3/distributions/multivariate.py @@ -151,18 +151,11 @@ def _quaddist_tau(self, delta): logdet = -tt.sum(tt.log(diag)) return quaddist, logdet, ok - def _repr_cov_params(self, dist=None): - if dist is None: - dist = self - if self._cov_type == 'chol': - chol = get_variable_name(self.chol_cov) - return r'\mathit{{chol}}={}'.format(chol) - elif self._cov_type == 'cov': - cov = get_variable_name(self.cov) - return r'\mathit{{cov}}={}'.format(cov) - elif self._cov_type == 'tau': - tau = get_variable_name(self.tau) - return r'\mathit{{tau}}={}'.format(tau) + def _cov_param_for_repr(self): + if self._cov_type == "chol": + return "chol_cov" + else: + return self._cov_type class MvNormal(_QuadFormBase): @@ -330,14 +323,8 @@ def logp(self, value): norm = - 0.5 * k * pm.floatX(np.log(2 * np.pi)) return bound(norm - 0.5 * quaddist - logdet, ok) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - mu = dist.mu - name_mu = get_variable_name(mu) - return (r'${} \sim \text{{MvNormal}}' - r'(\mathit{{mu}}={}, {})$' - .format(name, name_mu, self._repr_cov_params(dist))) + def _distr_parameters_for_repr(self): + return ["mu", self._cov_param_for_repr()] class MvStudentT(_QuadFormBase): @@ -448,17 +435,8 @@ def logp(self, value): inner = - (self.nu + k) / 2. * tt.log1p(quaddist / self.nu) return bound(norm + inner - logdet, ok) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - mu = dist.mu - nu = dist.nu - name_nu = get_variable_name(nu) - name_mu = get_variable_name(mu) - return (r'${} \sim \text{{MvStudentT}}' - r'(\mathit{{nu}}={}, \mathit{{mu}}={}, ' - r'{})$' - .format(name, name_nu, name_mu, self._repr_cov_params(dist))) + def _distr_parameters_for_repr(self): + return ["mu", "nu", self._cov_param_for_repr()] class Dirichlet(Continuous): @@ -580,12 +558,8 @@ def logp(self, value): np.logical_not(a.broadcastable), tt.all(a > 0), broadcast_conditions=False) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - a = dist.a - return r'${} \sim \text{{Dirichlet}}(\mathit{{a}}={})$'.format(name, - get_variable_name(a)) + def _distr_parameters_for_repr(self): + return ["a"] class Multinomial(Discrete): @@ -735,15 +709,6 @@ def logp(self, x): broadcast_conditions=False ) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - n = dist.n - p = dist.p - return r'${} \sim \text{{Multinomial}}(\mathit{{n}}={}, \mathit{{p}}={})$'.format(name, - get_variable_name(n), - get_variable_name(p)) - def posdef(AA): try: @@ -901,14 +866,6 @@ def logp(self, X): broadcast_conditions=False ) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - nu = dist.nu - V = dist.V - return r'${} \sim \text{{Wishart}}(\mathit{{nu}}={}, \mathit{{V}}={})$'.format(name, - get_variable_name(nu), - get_variable_name(V)) def WishartBartlett(name, S, nu, is_cholesky=False, return_cholesky=False, testval=None): R""" diff --git a/pymc3/distributions/timeseries.py b/pymc3/distributions/timeseries.py index 4443661a74..add71f7660 100644 --- a/pymc3/distributions/timeseries.py +++ b/pymc3/distributions/timeseries.py @@ -80,16 +80,6 @@ def logp(self, x): innov_like = Normal.dist(k * x_im1, tau=tau_e).logp(x_i) return boundary(x[0]) + tt.sum(innov_like) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - k = dist.k - tau_e = dist.tau_e - name = r"\text{%s}" % name - return r"${} \sim \text{{AR1}}(\mathit{{k}}={},~\mathit{{tau_e}}={})$".format( - name, get_variable_name(k), get_variable_name(tau_e) - ) - class AR(distribution.Continuous): r""" @@ -327,15 +317,8 @@ def _random(self, sigma, mu, size, sample_shape): data = data - data[0] return data - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - mu = dist.mu - sigma = dist.sigma - name = r"\text{%s}" % name - return r"${} \sim \text{{GaussianRandomWalk}}(\mathit{{mu}}={},~\mathit{{sigma}}={})$".format( - name, get_variable_name(mu), get_variable_name(sigma) - ) + def _distr_parameters_for_repr(self): + return ["mu", "sigma"] class GARCH11(distribution.Continuous): @@ -401,19 +384,8 @@ def logp(self, x): vol = self.get_volatility(x) return tt.sum(Normal.dist(0.0, sigma=vol).logp(x)) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - omega = dist.omega - alpha_1 = dist.alpha_1 - beta_1 = dist.beta_1 - name = r"\text{%s}" % name - return r"${} \sim \text{GARCH}(1,~1,~\mathit{{omega}}={},~\mathit{{alpha_1}}={},~\mathit{{beta_1}}={})$".format( - name, - get_variable_name(omega), - get_variable_name(alpha_1), - get_variable_name(beta_1), - ) + def _distr_parameters_for_repr(self): + return ["omega", "alpha_1", "beta_1"] class EulerMaruyama(distribution.Continuous): @@ -455,14 +427,8 @@ def logp(self, x): sd = tt.sqrt(self.dt) * g return tt.sum(Normal.dist(mu=mu, sigma=sd).logp(x[1:])) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - dt = dist.dt - name = r"\text{%s}" % name - return r"${} \sim \text{EulerMaruyama}(\mathit{{dt}}={})$".format( - name, get_variable_name(dt) - ) + def _distr_parameters_for_repr(self): + return ["dt"] class MvGaussianRandomWalk(distribution.Continuous): @@ -525,15 +491,8 @@ def logp(self, x): return self.init.logp_sum(x[0]) + self.innov.logp_sum(x_i - x_im1) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - mu = dist.innov.mu - cov = dist.innov.cov - name = r"\text{%s}" % name - return r"${} \sim \text{MvGaussianRandomWalk}(\mathit{{mu}}={},~\mathit{{cov}}={})$".format( - name, get_variable_name(mu), get_variable_name(cov) - ) + def _distr_parameters_for_repr(self): + return ["mu", "cov"] class MvStudentTRandomWalk(MvGaussianRandomWalk): @@ -560,13 +519,5 @@ def __init__(self, nu, *args, **kwargs): self.nu = tt.as_tensor_variable(nu) self.innov = multivariate.MvStudentT.dist(self.nu, None, *self.innovArgs) - def _repr_latex_(self, name=None, dist=None): - if dist is None: - dist = self - nu = dist.innov.nu - mu = dist.innov.mu - cov = dist.innov.cov - name = r"\text{%s}" % name - return r"${} \sim \text{MvStudentTRandomWalk}(\mathit{{nu}}={},~\mathit{{mu}}={},~\mathit{{cov}}={})$".format( - name, get_variable_name(nu), get_variable_name(mu), get_variable_name(cov) - ) + def _distr_parameters_for_repr(self): + return ["nu", "mu", "cov"] diff --git a/pymc3/tests/test_distributions.py b/pymc3/tests/test_distributions.py index a52ff63a4a..fbfb74ab19 100644 --- a/pymc3/tests/test_distributions.py +++ b/pymc3/tests/test_distributions.py @@ -1805,7 +1805,7 @@ def setup_class(self): r"$\text{sigma} \sim \text{HalfNormal}(\mathit{sigma}=1.0)$", r"$\text{mu} \sim \text{Deterministic}(\text{alpha},~\text{Constant},~\text{beta})$", r"$\text{beta} \sim \text{Normal}(\mathit{mu}=0.0,~\mathit{sigma}=10.0)$", - r"$Z \sim \text{MvNormal}(\mathit{mu}=array, \mathit{chol}=array)$", + r"$\text{Z} \sim \text{MvNormal}(\mathit{mu}=array,~\mathit{chol_cov}=array)$", r"$\text{Y_obs} \sim \text{Normal}(\mathit{mu}=\text{mu},~\mathit{sigma}=f(\text{sigma}))$", ) From 9940f4ac0046f80fd191da5914ddf31a529a8d6a Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Tue, 11 Aug 2020 10:55:56 +0200 Subject: [PATCH 09/14] adding new repr functionality to Deterministic --- pymc3/model.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/pymc3/model.py b/pymc3/model.py index ccd123b7a0..7c381f8859 100644 --- a/pymc3/model.py +++ b/pymc3/model.py @@ -1822,27 +1822,28 @@ def __ne__(self, other): return not self == other -def _walk_up_rv(rv): +def _walk_up_rv(rv, formatting='plain'): """Walk up theano graph to get inputs for deterministic RV.""" all_rvs = [] parents = list(itertools.chain(*[j.inputs for j in rv.get_parents()])) if parents: for parent in parents: - all_rvs.extend(_walk_up_rv(parent)) + all_rvs.extend(_walk_up_rv(parent, formatting=formatting)) else: - if rv.name: - all_rvs.append(r"\text{%s}" % rv.name) - else: - all_rvs.append(r"\text{Constant}") + name = rv.name if rv.name else "Constant" + fmt = r"\text{{{name}}}" if formatting == "latex" else "{name}" + all_rvs.append(fmt.format(name=name)) return all_rvs -def _latex_repr_rv(rv): +def _repr_deterministic_rv(rv, formatting='plain'): """Make latex string for a Deterministic variable""" - return r"$\text{%s} \sim \text{Deterministic}(%s)$" % ( - rv.name, - r",~".join(_walk_up_rv(rv)), - ) + if formatting == 'latex': + return r"$\text{{{name}}} \sim \text{{Deterministic}}({args})$".format( + name=rv.name, args=r",~".join(_walk_up_rv(rv, formatting=formatting))) + else: + return "{name} ~ Deterministic({args})".format( + name=rv.name, args=", ".join(_walk_up_rv(rv, formatting=formatting))) def Deterministic(name, var, model=None, dims=None): @@ -1861,8 +1862,9 @@ def Deterministic(name, var, model=None, dims=None): var = var.copy(model.name_for(name)) model.deterministics.append(var) model.add_random_variable(var, dims) - var._repr_latex_ = functools.partial(_latex_repr_rv, var) + var._repr_latex_ = functools.partial(_repr_deterministic_rv, var, formatting='latex') var.__latex__ = var._repr_latex_ + var.__str__ = functools.partial(_repr_deterministic_rv, var, formatting='plain') return var From 0e31ad32a6b74801f14c181aea94c95b8da0ec2d Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Tue, 11 Aug 2020 11:35:07 +0200 Subject: [PATCH 10/14] replacing old with new str repr functionality in PyMC3Variable --- pymc3/distributions/continuous.py | 1 - pymc3/distributions/discrete.py | 1 - pymc3/distributions/distribution.py | 5 +-- pymc3/distributions/mixture.py | 1 - pymc3/distributions/multivariate.py | 1 - pymc3/distributions/timeseries.py | 1 - pymc3/model.py | 54 +++++++++++------------------ pymc3/util.py | 19 ++++++---- 8 files changed, 37 insertions(+), 46 deletions(-) diff --git a/pymc3/distributions/continuous.py b/pymc3/distributions/continuous.py index 9cf2f40b63..55e4484dc3 100644 --- a/pymc3/distributions/continuous.py +++ b/pymc3/distributions/continuous.py @@ -27,7 +27,6 @@ from pymc3.theanof import floatX from . import transforms -from pymc3.util import get_variable_name from .special import log_i0 from ..math import invlogit, logit, logdiffexp from .dist_math import ( diff --git a/pymc3/distributions/discrete.py b/pymc3/distributions/discrete.py index a8057b32ad..090949c905 100644 --- a/pymc3/distributions/discrete.py +++ b/pymc3/distributions/discrete.py @@ -17,7 +17,6 @@ from scipy import stats import warnings -from pymc3.util import get_variable_name from .dist_math import bound, factln, binomln, betaln, logpow, random_choice from .distribution import Discrete, draw_values, generate_samples from .shape_utils import broadcast_distribution_samples diff --git a/pymc3/distributions/distribution.py b/pymc3/distributions/distribution.py index 8d5f115f2d..a5468a7e4c 100644 --- a/pymc3/distributions/distribution.py +++ b/pymc3/distributions/distribution.py @@ -23,7 +23,7 @@ import numpy as np import theano.tensor as tt from theano import function -from ..util import get_variable_name +from ..util import get_repr_for_variable import theano from ..memoize import memoize from ..model import ( @@ -159,7 +159,8 @@ def _str_repr(self, name=None, dist=None, formatting='plain'): name = '[unnamed]' param_names = self._distr_parameters_for_repr() - param_values = [get_variable_name(getattr(dist, x)) for x in param_names] + param_values = [get_repr_for_variable(getattr(dist, x), formatting=formatting) + for x in param_names] if formatting == "latex": param_string = ",~".join([r"\mathit{{{name}}}={value}".format(name=name, diff --git a/pymc3/distributions/mixture.py b/pymc3/distributions/mixture.py index 87afc85e79..3169aabe52 100644 --- a/pymc3/distributions/mixture.py +++ b/pymc3/distributions/mixture.py @@ -18,7 +18,6 @@ import theano.tensor as tt import warnings -from pymc3.util import get_variable_name from ..math import logsumexp from .dist_math import bound, random_choice from .distribution import (Discrete, Distribution, draw_values, diff --git a/pymc3/distributions/multivariate.py b/pymc3/distributions/multivariate.py index 6b19089c0e..aeea0b39c6 100755 --- a/pymc3/distributions/multivariate.py +++ b/pymc3/distributions/multivariate.py @@ -30,7 +30,6 @@ from pymc3.theanof import floatX from . import transforms -from pymc3.util import get_variable_name from .distribution import (Continuous, Discrete, draw_values, generate_samples, _DrawValuesContext) from ..model import Deterministic diff --git a/pymc3/distributions/timeseries.py b/pymc3/distributions/timeseries.py index add71f7660..3956550d82 100644 --- a/pymc3/distributions/timeseries.py +++ b/pymc3/distributions/timeseries.py @@ -19,7 +19,6 @@ from theano import scan import numpy as np -from pymc3.util import get_variable_name from .continuous import get_tau_sigma, Normal, Flat from .shape_utils import to_tuple from . import multivariate diff --git a/pymc3/model.py b/pymc3/model.py index 7c381f8859..44c9f89f05 100644 --- a/pymc3/model.py +++ b/pymc3/model.py @@ -64,6 +64,27 @@ class PyMC3Variable(TensorVariable): def __rmatmul__(self, other): return tt.dot(other, self) + def _str_repr(self, name=None, dist=None, formatting="plain"): + if getattr(self, "distribution", None) is None: + if formatting == "latex": + return None + else: + return super().__str__() + + if name is None and hasattr(self, 'name'): + name = self.name + if dist is None and hasattr(self, 'distribution'): + dist = self.distribution + return self.distribution._str_repr(name=name, dist=dist, formatting=formatting) + + def __str__(self, **kwargs): + return self._str_repr(**kwargs) + + def _repr_latex_(self, **kwargs): + return self._str_repr(formatting="latex", **kwargs) + + __latex__ = _repr_latex_ + class InstanceMethod: """Class for hiding references to instance methods so they can be pickled. @@ -1607,17 +1628,6 @@ def __init__( wrapper=InstanceMethod, ) - def _repr_latex_(self, name=None, dist=None): - if self.distribution is None: - return None - if name is None: - name = self.name - if dist is None: - dist = self.distribution - return self.distribution._repr_latex_(name=name, dist=dist) - - __latex__ = _repr_latex_ - @property def init_value(self): """Convenience attribute to return tag.test_value""" @@ -1752,17 +1762,6 @@ def __init__( self.tag.test_value = theano.compile.view_op(data).tag.test_value self.scaling = _get_scaling(total_size, data.shape, data.ndim) - def _repr_latex_(self, name=None, dist=None): - if self.distribution is None: - return None - if name is None: - name = self.name - if dist is None: - dist = self.distribution - return self.distribution._repr_latex_(name=name, dist=dist) - - __latex__ = _repr_latex_ - @property def init_value(self): """Convenience attribute to return tag.test_value""" @@ -1942,17 +1941,6 @@ def __init__( wrapper=InstanceMethod, ) - def _repr_latex_(self, name=None, dist=None): - if self.distribution is None: - return None - if name is None: - name = self.name - if dist is None: - dist = self.distribution - return self.distribution._repr_latex_(name=name, dist=dist) - - __latex__ = _repr_latex_ - @property def init_value(self): """Convenience attribute to return tag.test_value""" diff --git a/pymc3/util.py b/pymc3/util.py index 196c7be008..46e35d595c 100644 --- a/pymc3/util.py +++ b/pymc3/util.py @@ -124,26 +124,33 @@ def get_default_varnames(var_iterator, include_transformed): return [var for var in var_iterator if not is_transformed_name(str(var))] -def get_variable_name(variable): - r"""Returns the variable data type if it is a constant, otherwise - returns the argument name. +def get_repr_for_variable(variable, formatting="plain"): + """Build a human-readable string representation for a variable. """ name = variable.name if name is None: if hasattr(variable, "get_parents"): try: names = [ - get_variable_name(item) for item in variable.get_parents()[0].inputs + get_repr_for_variable(item, formatting=formatting) + for item in variable.get_parents()[0].inputs ] # do not escape_latex these, since it is not idempotent - return "f(%s)" % ",~".join([n for n in names if isinstance(n, str)]) + if formatting == "latex": + return "f({args})".format(args=",~".join([n for n in names if isinstance(n, str)])) + else: + return "f({args})".format(args=", ".join([n for n in names if isinstance(n, str)])) except IndexError: pass value = variable.eval() if not value.shape or value.shape == (1,): return asscalar(value) return "array" - return r"\text{%s}" % name + + if formatting == "latex": + return r"\text{{{name}}}".format(name=name) + else: + return name def update_start_vals(a, b, model): From b75d6728fa93e78e2ffba668a673e43831898445 Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Tue, 11 Aug 2020 11:54:05 +0200 Subject: [PATCH 11/14] ensure that TransformedDistribution does not mess up its str repr --- pymc3/distributions/transforms.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pymc3/distributions/transforms.py b/pymc3/distributions/transforms.py index af7ff83256..305c092285 100644 --- a/pymc3/distributions/transforms.py +++ b/pymc3/distributions/transforms.py @@ -201,6 +201,9 @@ def _repr_latex_(self, **kwargs): # of models return None + def _distr_parameters_for_repr(self): + return [] + transform = Transform From d0c70e85e9f8fe45d5b6348a53c177acbc59ed7a Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Tue, 11 Aug 2020 13:53:48 +0200 Subject: [PATCH 12/14] new str repr functionality in Model --- pymc3/model.py | 44 ++++++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/pymc3/model.py b/pymc3/model.py index 44c9f89f05..d5df8000b4 100644 --- a/pymc3/model.py +++ b/pymc3/model.py @@ -1347,20 +1347,36 @@ def check_test_point(self, test_point=None, round_vals=2): name="Log-probability of test_point", ) - def _repr_latex_(self, name=None, dist=None): - tex_vars = [] - for rv in itertools.chain(self.unobserved_RVs, self.observed_RVs): - rv_tex = rv.__latex__() - if rv_tex is not None: - array_rv = rv_tex.replace(r"\sim", r"&\sim &").strip("$") - tex_vars.append(array_rv) - return r"""$$ - \begin{{array}}{{rcl}} - {} - \end{{array}} - $$""".format( - "\\\\".join(tex_vars) - ) + def _str_repr(self, formatting="plain", **kwargs): + all_rv = itertools.chain(self.unobserved_RVs, self.observed_RVs) + + if formatting == "latex": + rv_reprs = [rv.__latex__() for rv in all_rv] + rv_reprs = [rv_repr.replace(r"\sim", r"&\sim &").strip("$") + for rv_repr in rv_reprs if rv_repr is not None] + return r"""$$ + \begin{{array}}{{rcl}} + {} + \end{{array}} + $$""".format( + "\\\\".join(rv_reprs)) + else: + rv_reprs = [rv.__str__() for rv in all_rv] + rv_reprs = [rv_repr for rv_repr in rv_reprs if not 'TransformedDistribution()' in rv_repr] + # align vars on their ~ + names = [s[:s.index('~')-1] for s in rv_reprs] + distrs = [s[s.index('~')+2:] for s in rv_reprs] + maxlen = str(max(len(x) for x in names)) + rv_reprs = [('{name:>' + maxlen + '} ~ {distr}').format(name=n, distr=d) + for n, d in zip(names, distrs)] + return "\n".join(rv_reprs) + + + def __str__(self, **kwargs): + return self._str_repr(**kwargs) + + def _repr_latex_(self, **kwargs): + return self._str_repr(formatting="latex", **kwargs) __latex__ = _repr_latex_ From aa2e0d0323379d529b802f776e832c8b03bc0ab3 Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Mon, 24 Aug 2020 14:56:56 +0200 Subject: [PATCH 13/14] don't touch __str__ for now --- pymc3/distributions/distribution.py | 3 --- pymc3/model.py | 8 -------- 2 files changed, 11 deletions(-) diff --git a/pymc3/distributions/distribution.py b/pymc3/distributions/distribution.py index a5468a7e4c..8b8e924f2a 100644 --- a/pymc3/distributions/distribution.py +++ b/pymc3/distributions/distribution.py @@ -174,9 +174,6 @@ def _str_repr(self, name=None, dist=None, formatting='plain'): return "{var_name} ~ {distr_name}({params})".format(var_name=name, distr_name=dist._distr_name_for_repr(), params=param_string) - def __str__(self, **kwargs): - return self._str_repr(formatting="plain", **kwargs) - def _repr_latex_(self, **kwargs): """Magic method name for IPython to use for LaTeX formatting.""" return self._str_repr(formatting="latex", **kwargs) diff --git a/pymc3/model.py b/pymc3/model.py index d5df8000b4..cae3e12ca5 100644 --- a/pymc3/model.py +++ b/pymc3/model.py @@ -77,9 +77,6 @@ def _str_repr(self, name=None, dist=None, formatting="plain"): dist = self.distribution return self.distribution._str_repr(name=name, dist=dist, formatting=formatting) - def __str__(self, **kwargs): - return self._str_repr(**kwargs) - def _repr_latex_(self, **kwargs): return self._str_repr(formatting="latex", **kwargs) @@ -1371,10 +1368,6 @@ def _str_repr(self, formatting="plain", **kwargs): for n, d in zip(names, distrs)] return "\n".join(rv_reprs) - - def __str__(self, **kwargs): - return self._str_repr(**kwargs) - def _repr_latex_(self, **kwargs): return self._str_repr(formatting="latex", **kwargs) @@ -1879,7 +1872,6 @@ def Deterministic(name, var, model=None, dims=None): model.add_random_variable(var, dims) var._repr_latex_ = functools.partial(_repr_deterministic_rv, var, formatting='latex') var.__latex__ = var._repr_latex_ - var.__str__ = functools.partial(_repr_deterministic_rv, var, formatting='plain') return var From 9ab696775f139b20c04b6648698f6dd199029a1e Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Mon, 24 Aug 2020 17:12:51 +0200 Subject: [PATCH 14/14] fixing _repr_latex_ for Simulator --- pymc3/distributions/distribution.py | 3 +++ pymc3/distributions/simulator.py | 17 +++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/pymc3/distributions/distribution.py b/pymc3/distributions/distribution.py index 8b8e924f2a..3d3cd002eb 100644 --- a/pymc3/distributions/distribution.py +++ b/pymc3/distributions/distribution.py @@ -239,6 +239,9 @@ def logp(self, x): """ return tt.zeros_like(x) + def _distr_parameters_for_repr(self): + return [] + class Discrete(Distribution): """Base class for discrete distributions""" diff --git a/pymc3/distributions/simulator.py b/pymc3/distributions/simulator.py index 5320f3be9d..e0e8e456fd 100644 --- a/pymc3/distributions/simulator.py +++ b/pymc3/distributions/simulator.py @@ -34,7 +34,7 @@ def __init__( ): """ This class stores a function defined by the user in Python language. - + function: function Python function defined by the user. params: list @@ -53,7 +53,7 @@ def __init__( If a callable is based it should return a number or a 1d numpy array. epsilon: float Standard deviation of the gaussian_kernel. - *args and **kwargs: + *args and **kwargs: Arguments and keywords arguments that the function takes. """ @@ -115,7 +115,7 @@ def random(self, point=None, size=None): else: return np.array([self.function(*params) for _ in range(size)]) - def _repr_latex_(self, name=None, dist=None): + def _str_repr(self, name=None, dist=None, formatting="plain"): if dist is None: dist = self name = name @@ -123,7 +123,12 @@ def _repr_latex_(self, name=None, dist=None): params = ", ".join([var.name for var in dist.params]) sum_stat = self.sum_stat.__name__ if hasattr(self.sum_stat, "__call__") else self.sum_stat distance = self.distance.__name__ - return f"$\\text{{{name}}} \sim \\text{{Simulator}}(\\text{{{function}}}({params}), \\text{{{distance}}}, \\text{{{sum_stat}}})$" + + if formatting == "latex": + return f"$\\text{{{name}}} \sim \\text{{Simulator}}(\\text{{{function}}}({params}), \\text{{{distance}}}, \\text{{{sum_stat}}})$" + else: + return f"{name} ~ Simulator({function}({params}), {distance}, {sum_stat})" + def identity(x): @@ -138,7 +143,7 @@ def gaussian_kernel(epsilon, obs_data, sim_data): def wasserstein(epsilon, obs_data, sim_data): """Wasserstein distance function. - + We are assuming obs_data and sim_data are already sorted! """ return np.mean(np.abs((obs_data - sim_data) / epsilon)) @@ -146,7 +151,7 @@ def wasserstein(epsilon, obs_data, sim_data): def energy(epsilon, obs_data, sim_data): """Energy distance function. - + We are assuming obs_data and sim_data are already sorted! """ return 1.4142 * np.mean(((obs_data - sim_data) / epsilon) ** 2) ** 0.5