From 6504275dd48a92057cde608f94794e66c09e9bc8 Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Fri, 12 Jun 2020 15:38:31 +0200 Subject: [PATCH 01/19] 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 ce3782ada18..196c7be008e 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 c6d5cdf0d4c688416bdede20bfd6f138f2bece34 Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Fri, 12 Jun 2020 16:03:58 +0200 Subject: [PATCH 02/19] adding option to display parameters of distributions to graphviz generation --- pymc3/model_graph.py | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/pymc3/model_graph.py b/pymc3/model_graph.py index dd9f6caaab4..792bf449a6c 100644 --- a/pymc3/model_graph.py +++ b/pymc3/model_graph.py @@ -24,6 +24,7 @@ from .util import get_default_varnames from .model import ObservedRV import pymc3 as pm +from pymc3.util import get_variable_name class ModelGraph: @@ -33,6 +34,10 @@ def __init__(self, model): self.var_list = self.model.named_vars.values() self.transform_map = {v.transformed: v.name for v in self.var_list if hasattr(v, 'transformed')} self._deterministics = None + self._distr_params = { + 'Normal': ['mu', 'sigma'], + 'Uniform': ['lower', 'upper'], + } def get_deterministics(self, var): """Compute the deterministic nodes of the graph, **not** including var itself.""" @@ -120,7 +125,7 @@ def update_input_map(key: str, val: Set[VarName]): pass return input_map - def _make_node(self, var_name, graph): + def _make_node(self, var_name, graph, include_prior_params): """Attaches the given variable to a graphviz Digraph""" v = self.model[var_name] @@ -146,9 +151,21 @@ def _make_node(self, var_name, graph): distribution = 'Deterministic' attrs['shape'] = 'box' - graph.node(var_name.replace(':', '&'), - '{var_name}\n~\n{distribution}'.format(var_name=var_name, distribution=distribution), - **attrs) + node_text = '{var_name}\n~\n{distribution}'.format(var_name=var_name, distribution=distribution) + if include_prior_params and distribution in self._distr_params: + param_strings = [] + for param in self._distr_params[distribution]: + val = get_variable_name(getattr(v.distribution, param)) + if type(val) is str and len(val) > 100: + val = '' + try: + val = '{val:.3g}'.format(val=float(val)) + except ValueError: + pass + param_strings.append('{param}={val}'.format(param=param, + val=val)) + node_text += '(' + ', '.join(param_strings) + ')' + graph.node(var_name.replace(':', '&'), node_text, **attrs) def get_plates(self): """ Rough but surprisingly accurate plate detection. @@ -181,7 +198,7 @@ def get_plates(self): plates[shape].add(var_name) return plates - def make_graph(self): + def make_graph(self, include_prior_params=False): """Make graphviz Digraph of PyMC3 model Returns @@ -203,12 +220,12 @@ def make_graph(self): # must be preceded by 'cluster' to get a box around it with graph.subgraph(name='cluster' + label) as sub: for var_name in var_names: - self._make_node(var_name, sub) + self._make_node(var_name, sub, include_prior_params) # plate label goes bottom right sub.attr(label=label, labeljust='r', labelloc='b', style='rounded') else: for var_name in var_names: - self._make_node(var_name, graph) + self._make_node(var_name, graph, include_prior_params) for key, values in self.make_compute_graph().items(): for value in values: @@ -216,7 +233,7 @@ def make_graph(self): return graph -def model_to_graphviz(model=None): +def model_to_graphviz(model=None, **kwargs): """Produce a graphviz Digraph from a PyMC3 model. Requires graphviz, which may be installed most easily with @@ -228,4 +245,4 @@ def model_to_graphviz(model=None): for more information. """ model = pm.modelcontext(model) - return ModelGraph(model).make_graph() + return ModelGraph(model).make_graph(**kwargs) From b436ee5008e98a3fb18055fba897ec1c10635bcf Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Mon, 10 Aug 2020 16:59:00 +0200 Subject: [PATCH 03/19] 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 7176fb47add..1aad5ab43ee 100644 --- a/pymc3/distributions/distribution.py +++ b/pymc3/distributions/distribution.py @@ -14,6 +14,7 @@ import numbers import contextvars +import inspect from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Optional, Callable @@ -21,6 +22,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 ( @@ -134,9 +136,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 6d0b450814d64b62418f0d8398fd06665b3242f5 Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Mon, 10 Aug 2020 17:07:40 +0200 Subject: [PATCH 04/19] 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 1aad5ab43ee..d94e14f6b75 100644 --- a/pymc3/distributions/distribution.py +++ b/pymc3/distributions/distribution.py @@ -173,11 +173,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 19e8d77d3bfeca34ad2051b9727e134cd018f763 Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Tue, 11 Aug 2020 09:20:21 +0200 Subject: [PATCH 05/19] 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 1aad5ab43ee..b0e62b869ab 100644 --- a/pymc3/distributions/distribution.py +++ b/pymc3/distributions/distribution.py @@ -22,7 +22,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 1e047eb3d1478e7872cca9a5b8137b0ffa17c6e8 Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Tue, 11 Aug 2020 09:22:25 +0200 Subject: [PATCH 06/19] 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 48f1d27fc3e..af7ff832564 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 4aa4df1a63580579f393f58781309fd3ab9d4352 Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Tue, 11 Aug 2020 09:28:17 +0200 Subject: [PATCH 07/19] 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 24dce0574ff..8b45d567bfd 100644 --- a/pymc3/distributions/distribution.py +++ b/pymc3/distributions/distribution.py @@ -22,7 +22,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 717326126ef05295125f2e74b135269166c7be49 Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Tue, 11 Aug 2020 09:35:38 +0200 Subject: [PATCH 08/19] 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 8b45d567bfd..63d3cb94226 100644 --- a/pymc3/distributions/distribution.py +++ b/pymc3/distributions/distribution.py @@ -136,7 +136,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 @@ -145,7 +145,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'): @@ -157,20 +157,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 a8e8701eba175a9f2e293191a4efc6f167f5adc0 Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Tue, 11 Aug 2020 10:29:28 +0200 Subject: [PATCH 09/19] 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 dfa82dc70bd..1b64fdd34bd 100644 --- a/pymc3/distributions/continuous.py +++ b/pymc3/distributions/continuous.py @@ -238,15 +238,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 @@ -314,10 +305,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 @@ -381,10 +368,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 @@ -537,15 +520,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): """ @@ -765,21 +741,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): @@ -906,13 +869,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): """ @@ -1109,17 +1067,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): """ @@ -1350,15 +1299,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""" @@ -1466,16 +1408,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""" @@ -1564,14 +1496,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 @@ -1699,16 +1623,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 @@ -1870,15 +1784,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): """ @@ -2044,17 +1951,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): """ @@ -2191,15 +2089,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): """ @@ -2329,16 +2220,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 @@ -2449,14 +2330,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 @@ -2640,15 +2513,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): @@ -2790,15 +2656,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): @@ -2843,14 +2702,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""" @@ -2958,16 +2809,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 @@ -3126,15 +2967,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): @@ -3276,17 +3110,8 @@ def logp(self, value): - 0.5 * ((value - mu) / sigma)**2) return bound(lp, sigma > 0., nu > 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): """ @@ -3420,16 +3245,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): @@ -3563,17 +3380,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): @@ -3699,18 +3507,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 @@ -3844,16 +3640,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 @@ -4022,6 +3808,9 @@ def logp(self, value): value > 0, ) + def _distr_parameters_for_repr(self): + return ["nu", "sigma"] + class Logistic(Continuous): R""" @@ -4120,16 +3909,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 @@ -4271,15 +4050,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): @@ -4383,6 +4155,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""" @@ -4485,16 +4260,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 c5d43706f3b..a8057b32ade 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 63d3cb94226..5a206a9993a 100644 --- a/pymc3/distributions/distribution.py +++ b/pymc3/distributions/distribution.py @@ -528,6 +528,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 a041c3bc159..87afc85e79f 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 d3c76d5df33..6b19089c0ed 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 d5d836b4655..13cb900e9d1 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""" @@ -323,15 +313,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): @@ -397,19 +380,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): @@ -451,14 +423,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): @@ -521,15 +487,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): @@ -556,13 +515,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 2897211e9bc..8f4efd09a9a 100644 --- a/pymc3/tests/test_distributions.py +++ b/pymc3/tests/test_distributions.py @@ -1804,7 +1804,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 451cc05d3fcfbd1c95f8b0edd35cf46fe9af679f Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Tue, 11 Aug 2020 10:55:56 +0200 Subject: [PATCH 10/19] 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 5634a856d08..e705b543901 100644 --- a/pymc3/model.py +++ b/pymc3/model.py @@ -1821,27 +1821,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): @@ -1860,8 +1861,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 4cfaf0652d94ae8371bf124c0f8dc4530d555f3f Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Tue, 11 Aug 2020 11:35:07 +0200 Subject: [PATCH 11/19] 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/model_graph.py | 4 +-- pymc3/util.py | 19 ++++++---- 9 files changed, 39 insertions(+), 48 deletions(-) diff --git a/pymc3/distributions/continuous.py b/pymc3/distributions/continuous.py index 1b64fdd34bd..66014dcf0af 100644 --- a/pymc3/distributions/continuous.py +++ b/pymc3/distributions/continuous.py @@ -26,7 +26,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 a8057b32ade..090949c9053 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 5a206a9993a..216312440c0 100644 --- a/pymc3/distributions/distribution.py +++ b/pymc3/distributions/distribution.py @@ -22,7 +22,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 ( @@ -158,7 +158,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 87afc85e79f..3169aabe52c 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 6b19089c0ed..aeea0b39c6b 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 13cb900e9d1..bd4117bc84a 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 e705b543901..328868348f7 100644 --- a/pymc3/model.py +++ b/pymc3/model.py @@ -63,6 +63,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. @@ -1606,17 +1627,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""" @@ -1751,17 +1761,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""" @@ -1941,17 +1940,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/model_graph.py b/pymc3/model_graph.py index 792bf449a6c..b6bf8126803 100644 --- a/pymc3/model_graph.py +++ b/pymc3/model_graph.py @@ -24,7 +24,7 @@ from .util import get_default_varnames from .model import ObservedRV import pymc3 as pm -from pymc3.util import get_variable_name +from pymc3.util import get_repr_for_variable class ModelGraph: @@ -155,7 +155,7 @@ def _make_node(self, var_name, graph, include_prior_params): if include_prior_params and distribution in self._distr_params: param_strings = [] for param in self._distr_params[distribution]: - val = get_variable_name(getattr(v.distribution, param)) + val = get_repr_for_variable(getattr(v.distribution, param)) if type(val) is str and len(val) > 100: val = '' try: diff --git a/pymc3/util.py b/pymc3/util.py index 196c7be008e..46e35d595cf 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 cb12783065239d2478747ce9e57abef7e6208f6e Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Tue, 11 Aug 2020 11:54:05 +0200 Subject: [PATCH 12/19] 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 af7ff832564..305c092285d 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 8f794079f4803d06f814dab0ba7f92ca54188c91 Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Tue, 11 Aug 2020 13:53:48 +0200 Subject: [PATCH 13/19] 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 328868348f7..063687e1df9 100644 --- a/pymc3/model.py +++ b/pymc3/model.py @@ -1346,20 +1346,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 864193404cf1d011155a7c2b791d384c3b676df2 Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Tue, 11 Aug 2020 14:06:19 +0200 Subject: [PATCH 14/19] adding unit tests for new __str__ functionality --- pymc3/tests/test_distributions.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/pymc3/tests/test_distributions.py b/pymc3/tests/test_distributions.py index 8f4efd09a9a..363852eca4a 100644 --- a/pymc3/tests/test_distributions.py +++ b/pymc3/tests/test_distributions.py @@ -1770,7 +1770,7 @@ def test_bound(): BoundPoissonPositionalArgs = Bound(Poisson, upper=6)("x", 2.0) -class TestLatex: +class TestStrAndLatexRepr: def setup_class(self): # True parameter values alpha, sigma = 1, 1 @@ -1799,7 +1799,7 @@ def setup_class(self): # Likelihood (sampling distribution) of observations Y_obs = Normal("Y_obs", mu=mu, sigma=sigma, observed=Y) self.distributions = [alpha, sigma, mu, b, Z, Y_obs] - self.expected = ( + self.expected_latex = ( r"$\text{alpha} \sim \text{Normal}(\mathit{mu}=0.0,~\mathit{sigma}=10.0)$", r"$\text{sigma} \sim \text{HalfNormal}(\mathit{sigma}=1.0)$", r"$\text{mu} \sim \text{Deterministic}(\text{alpha},~\text{Constant},~\text{beta})$", @@ -1807,22 +1807,38 @@ def setup_class(self): 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}))$", ) + self.expected_str = ( + r"alpha ~ Normal(mu=0.0, sigma=10.0)", + r"sigma ~ HalfNormal(sigma=1.0)", + r"mu ~ Deterministic(alpha, Constant, beta)", + r"beta ~ Normal(mu=0.0, sigma=10.0)", + r"Z ~ MvNormal(mu=array, chol_cov=array)", + r"Y_obs ~ Normal(mu=mu, sigma=f(sigma))", + ) def test__repr_latex_(self): - for distribution, tex in zip(self.distributions, self.expected): + for distribution, tex in zip(self.distributions, self.expected_latex): assert distribution._repr_latex_() == tex model_tex = self.model._repr_latex_() - for tex in self.expected: # make sure each variable is in the model + for tex in self.expected_latex: # make sure each variable is in the model for segment in tex.strip("$").split(r"\sim"): assert segment in model_tex def test___latex__(self): - for distribution, tex in zip(self.distributions, self.expected): + for distribution, tex in zip(self.distributions, self.expected_latex): assert distribution._repr_latex_() == distribution.__latex__() assert self.model._repr_latex_() == self.model.__latex__() + def test___str__(self): + for distribution, str_repr in zip(self.distributions, self.expected_str): + assert distribution.__str__() == str_repr + + model_str = self.model.__str__() + for str_repr in self.expected_str: + assert str_repr in model_str + def test_discrete_trafo(): with pytest.raises(ValueError) as err: From 39488b26275b01c8a399c15b1df70a56c2bfa989 Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Tue, 11 Aug 2020 14:37:49 +0200 Subject: [PATCH 15/19] updating graphviz rendering with new repr functionality --- pymc3/model.py | 8 +++--- pymc3/model_graph.py | 61 ++++++++++++++++++++------------------------ 2 files changed, 32 insertions(+), 37 deletions(-) diff --git a/pymc3/model.py b/pymc3/model.py index 063687e1df9..39832a0981a 100644 --- a/pymc3/model.py +++ b/pymc3/model.py @@ -1850,14 +1850,16 @@ def _walk_up_rv(rv, formatting='plain'): return all_rvs -def _repr_deterministic_rv(rv, formatting='plain'): +def _repr_deterministic_rv(rv, name=None, formatting='plain'): """Make latex string for a Deterministic variable""" + if name is None: + name = rv.name if formatting == 'latex': return r"$\text{{{name}}} \sim \text{{Deterministic}}({args})$".format( - name=rv.name, args=r",~".join(_walk_up_rv(rv, formatting=formatting))) + name=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))) + name=name, args=", ".join(_walk_up_rv(rv, formatting=formatting))) def Deterministic(name, var, model=None, dims=None): diff --git a/pymc3/model_graph.py b/pymc3/model_graph.py index b6bf8126803..e833e418833 100644 --- a/pymc3/model_graph.py +++ b/pymc3/model_graph.py @@ -34,10 +34,6 @@ def __init__(self, model): self.var_list = self.model.named_vars.values() self.transform_map = {v.transformed: v.name for v in self.var_list if hasattr(v, 'transformed')} self._deterministics = None - self._distr_params = { - 'Normal': ['mu', 'sigma'], - 'Uniform': ['lower', 'upper'], - } def get_deterministics(self, var): """Compute the deterministic nodes of the graph, **not** including var itself.""" @@ -125,7 +121,7 @@ def update_input_map(key: str, val: Set[VarName]): pass return input_map - def _make_node(self, var_name, graph, include_prior_params): + def _make_node(self, var_name, graph, full_details): """Attaches the given variable to a graphviz Digraph""" v = self.model[var_name] @@ -134,37 +130,27 @@ def _make_node(self, var_name, graph, include_prior_params): if isinstance(v, pm.model.ObservedRV): attrs['style'] = 'filled' - # make Data be roundtangle, instead of rectangle - if isinstance(v, SharedVariable): - attrs['style'] = 'rounded, filled' - - # Get name for node if v in self.model.potentials: - distribution = 'Potential' + node_text = '{name}\n~\nPotential'.format(name=var_name) attrs['shape'] = 'octagon' - elif hasattr(v, 'distribution'): - distribution = v.distribution.__class__.__name__ elif isinstance(v, SharedVariable): - distribution = 'Data' + node_text = '{name}\n~\nData'.format(name=var_name) attrs['shape'] = 'box' + attrs['style'] = 'rounded, filled' + elif not full_details: + if hasattr(v, 'distribution'): + node_text = '{name}\n~\n{dist}'.format(name=var_name, + dist=v.distribution.__class__.__name__) + else: + node_text = '{name}\n~\nDeterministic'.format(name=var_name) else: - distribution = 'Deterministic' + node_text = v.__str__(name=var_name).replace(' ~ ', '\n~\n') + + + if not hasattr(v, 'distribution'): + # v is Deterministic attrs['shape'] = 'box' - node_text = '{var_name}\n~\n{distribution}'.format(var_name=var_name, distribution=distribution) - if include_prior_params and distribution in self._distr_params: - param_strings = [] - for param in self._distr_params[distribution]: - val = get_repr_for_variable(getattr(v.distribution, param)) - if type(val) is str and len(val) > 100: - val = '' - try: - val = '{val:.3g}'.format(val=float(val)) - except ValueError: - pass - param_strings.append('{param}={val}'.format(param=param, - val=val)) - node_text += '(' + ', '.join(param_strings) + ')' graph.node(var_name.replace(':', '&'), node_text, **attrs) def get_plates(self): @@ -198,7 +184,7 @@ def get_plates(self): plates[shape].add(var_name) return plates - def make_graph(self, include_prior_params=False): + def make_graph(self, full_details=True): """Make graphviz Digraph of PyMC3 model Returns @@ -220,12 +206,12 @@ def make_graph(self, include_prior_params=False): # must be preceded by 'cluster' to get a box around it with graph.subgraph(name='cluster' + label) as sub: for var_name in var_names: - self._make_node(var_name, sub, include_prior_params) + self._make_node(var_name, sub, full_details) # plate label goes bottom right sub.attr(label=label, labeljust='r', labelloc='b', style='rounded') else: for var_name in var_names: - self._make_node(var_name, graph, include_prior_params) + self._make_node(var_name, graph, full_details) for key, values in self.make_compute_graph().items(): for value in values: @@ -233,7 +219,7 @@ def make_graph(self, include_prior_params=False): return graph -def model_to_graphviz(model=None, **kwargs): +def model_to_graphviz(model=None, full_details=True): """Produce a graphviz Digraph from a PyMC3 model. Requires graphviz, which may be installed most easily with @@ -243,6 +229,13 @@ def model_to_graphviz(model=None, **kwargs): and then `pip install graphviz` to get the python bindings. See http://graphviz.readthedocs.io/en/stable/manual.html for more information. + + Parameters + ---------- + full_details: bool (default True) + Whether to include full details of PyMC3 model RVs + (e.g. prior (hyper)parameters)) in the node boxes. + Set to False to include only RV names. """ model = pm.modelcontext(model) - return ModelGraph(model).make_graph(**kwargs) + return ModelGraph(model).make_graph(full_details=full_details) From 8026f278c1ea15bf7ec184bea2e467b9e2df52a9 Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Tue, 11 Aug 2020 15:20:49 +0200 Subject: [PATCH 16/19] updating unit tests to reflect new graph info --- pymc3/tests/test_data_container.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pymc3/tests/test_data_container.py b/pymc3/tests/test_data_container.py index 03fb747288e..e1996cba6ca 100644 --- a/pymc3/tests/test_data_container.py +++ b/pymc3/tests/test_data_container.py @@ -191,9 +191,9 @@ def test_model_to_graphviz_for_model_with_data_container(self): text = 'x [label="x\n~\nData" shape=box style="rounded, filled"]' assert text in g.source # Didn't break ordinary variables? - text = 'beta [label="beta\n~\nNormal"]' + text = 'beta [label="beta\n~\nNormal(mu=0.0, sigma=10.0)"]' assert text in g.source - text = 'obs [label="obs\n~\nNormal" style=filled]' + text = 'obs [label="obs\n~\nNormal(mu=f(f(beta), x), sigma=0.1)" style=filled]' assert text in g.source def test_explicit_coords(self): From 3589c8999c04f94601c76075d2ddcc492fe50fa4 Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Tue, 11 Aug 2020 15:21:27 +0200 Subject: [PATCH 17/19] ensure that variable keys always use the simple plain variable name, rather than full new str repr --- pymc3/model.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pymc3/model.py b/pymc3/model.py index 39832a0981a..a59c40fce06 100644 --- a/pymc3/model.py +++ b/pymc3/model.py @@ -1485,8 +1485,14 @@ def Point(*args, **kwargs): d = dict(*args, **kwargs) except Exception as e: raise TypeError("can't turn {} and {} into a dict. {}".format(args, kwargs, e)) + def _mystr(k): + # make sure to use "plain" string representations (i.e. names) of variables + if isinstance(k, TensorVariable): + return super(TensorVariable, k).__str__() + else: + return str(k) return dict( - (str(k), np.array(v)) for k, v in d.items() if str(k) in map(str, model.vars) + (_mystr(k), np.array(v)) for k, v in d.items() if _mystr(k) in map(_mystr, model.vars) ) From 00d9f1c9be8bac5ac93b8211f4c4aa6a98161fe6 Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Tue, 11 Aug 2020 15:46:57 +0200 Subject: [PATCH 18/19] removing unused import --- pymc3/model_graph.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pymc3/model_graph.py b/pymc3/model_graph.py index e833e418833..0c72044ff71 100644 --- a/pymc3/model_graph.py +++ b/pymc3/model_graph.py @@ -24,7 +24,6 @@ from .util import get_default_varnames from .model import ObservedRV import pymc3 as pm -from pymc3.util import get_repr_for_variable class ModelGraph: From 273f1b6c0eaba696f4ba3e6e580a44bbcd42132a Mon Sep 17 00:00:00 2001 From: Eelke Spaak Date: Tue, 11 Aug 2020 16:32:28 +0200 Subject: [PATCH 19/19] ensure usage of plain string (i.e. names) of variables rather than user-facing __str__ --- pymc3/model.py | 10 ++-------- pymc3/step_methods/arraystep.py | 3 ++- pymc3/tuning/scaling.py | 3 ++- pymc3/util.py | 9 +++++++++ 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/pymc3/model.py b/pymc3/model.py index a59c40fce06..1de26e677a1 100644 --- a/pymc3/model.py +++ b/pymc3/model.py @@ -35,7 +35,7 @@ from .theanof import gradient, hessian, inputvars, generator from .vartypes import typefilter, discrete_types, continuous_types, isgenerator from .blocking import DictToArrayBijection, ArrayOrdering -from .util import get_transformed_name +from .util import get_transformed_name, plain_str from .exceptions import ImputationWarning __all__ = [ @@ -1485,14 +1485,8 @@ def Point(*args, **kwargs): d = dict(*args, **kwargs) except Exception as e: raise TypeError("can't turn {} and {} into a dict. {}".format(args, kwargs, e)) - def _mystr(k): - # make sure to use "plain" string representations (i.e. names) of variables - if isinstance(k, TensorVariable): - return super(TensorVariable, k).__str__() - else: - return str(k) return dict( - (_mystr(k), np.array(v)) for k, v in d.items() if _mystr(k) in map(_mystr, model.vars) + (plain_str(k), np.array(v)) for k, v in d.items() if plain_str(k) in map(plain_str, model.vars) ) diff --git a/pymc3/step_methods/arraystep.py b/pymc3/step_methods/arraystep.py index fde0a3f6707..86aa56d43e9 100644 --- a/pymc3/step_methods/arraystep.py +++ b/pymc3/step_methods/arraystep.py @@ -15,6 +15,7 @@ from .compound import CompoundStep from ..model import modelcontext from ..theanof import inputvars +from ..util import plain_str from ..blocking import ArrayOrdering, DictToArrayBijection import numpy as np from numpy.random import uniform @@ -175,7 +176,7 @@ def __init__(self, vars, shared, blocked=True): """ self.vars = vars self.ordering = ArrayOrdering(vars) - self.shared = {str(var): shared for var, shared in shared.items()} + self.shared = {plain_str(var): shared for var, shared in shared.items()} self.blocked = blocked self.bij = None diff --git a/pymc3/tuning/scaling.py b/pymc3/tuning/scaling.py index 37f7079199a..6e964114988 100644 --- a/pymc3/tuning/scaling.py +++ b/pymc3/tuning/scaling.py @@ -17,6 +17,7 @@ from ..model import modelcontext, Point from ..theanof import hessian_diag, inputvars from ..blocking import DictToArrayBijection, ArrayOrdering +from ..util import plain_str __all__ = ['find_hessian', 'trace_cov', 'guess_scaling'] @@ -135,7 +136,7 @@ def trace_cov(trace, vars=None, model=None): vars = trace.varnames def flat_t(var): - x = trace[str(var)] + x = trace[plain_str(var)] return x.reshape((x.shape[0], np.prod(x.shape[1:], dtype=int))) return np.cov(np.concatenate(list(map(flat_t, vars)), 1).T) diff --git a/pymc3/util.py b/pymc3/util.py index 46e35d595cf..e4ac48c0424 100644 --- a/pymc3/util.py +++ b/pymc3/util.py @@ -20,6 +20,7 @@ import arviz from numpy import asscalar, ndarray +from theano.tensor import TensorVariable LATEX_ESCAPE_RE = re.compile(r"(%|_|\$|#|&)", re.MULTILINE) @@ -230,3 +231,11 @@ def chains_and_samples( nchains = coords["chain"].sizes["chain"] nsamples = coords["draw"].sizes["draw"] return nchains, nsamples + + +def plain_str(k): + # make sure to use "plain" string representations (i.e. names) of variables + if isinstance(k, TensorVariable): + return super(TensorVariable, k).__str__() + else: + return str(k)