From 5132c9293a578de89ec60ec6401af74426f6e888 Mon Sep 17 00:00:00 2001 From: drabbit17 Date: Sun, 28 Mar 2021 19:24:35 +0100 Subject: [PATCH 01/19] Update tests following distributions refactoring The distributions refactoring moves the random variable sampling to aesara. This relies on numpy and scipy random variables implementation. So, now the only thing we care about testing is that the parametrization on the PyMC side is sendible given the one on the Aesara side (effectively the numpy/scipy one) More details can be found on issue #4554 https://github.com/pymc-devs/pymc3/issues/4554 --- pymc3/tests/test_distributions_random.py | 113 ++++++++++++++--------- 1 file changed, 71 insertions(+), 42 deletions(-) diff --git a/pymc3/tests/test_distributions_random.py b/pymc3/tests/test_distributions_random.py index 0d13bc572..9452c4556 100644 --- a/pymc3/tests/test_distributions_random.py +++ b/pymc3/tests/test_distributions_random.py @@ -24,13 +24,13 @@ import pytest import scipy.stats as st +from numpy.testing import assert_almost_equal from scipy import linalg from scipy.special import expit import pymc3 as pm from pymc3.aesaraf import change_rv_size, floatX, intX -from pymc3.distributions.dist_math import clipped_beta_rvs from pymc3.distributions.shape_utils import to_tuple from pymc3.exceptions import ShapeError from pymc3.tests.helpers import SeededTest, select_by_precision @@ -524,6 +524,76 @@ def test_dirichlet_random_shape(self, shape, size): assert pm.Dirichlet.dist(a=np.ones(shape)).random(size=size).shape == out_shape +class TestCorrectParametrizationMappingPymcToScipy(SeededTest): + @staticmethod + def get_inputs_from_apply_node_outputs(outputs): + parents = outputs.get_parents() + if not parents: + raise Exception("Parent Apply node missing for output") + # I am assuming there will always only be 1 Apply parent node in this context + return parents[0].inputs + + def test_pymc_params_match_rv_ones( + self, pymc_params, expected_aesara_params, pymc_dist, decimal=6 + ): + pymc_dist_output = pymc_dist.dist(**dict(pymc_params)) + aesera_dist_inputs = self.get_inputs_from_apply_node_outputs(pymc_dist_output)[3:] + assert len(expected_aesara_params) == len(aesera_dist_inputs) + for (expected_name, expected_value), actual_variable in zip( + expected_aesara_params, aesera_dist_inputs + ): + assert_almost_equal(expected_value, actual_variable.eval(), decimal=decimal) + + def test_normal(self): + params = [("mu", 5.0), ("sigma", 10.0)] + self.test_pymc_params_match_rv_ones(params, params, pm.Normal) + + def test_uniform(self): + params = [("lower", 0.5), ("upper", 1.5)] + self.test_pymc_params_match_rv_ones(params, params, pm.Uniform) + + def test_half_normal(self): + params, expected_aesara_params = [("sigma", 10.0)], [("mean", 0), ("sigma", 10.0)] + self.test_pymc_params_match_rv_ones(params, expected_aesara_params, pm.HalfNormal) + + def test_beta_alpha_beta(self): + params = [("alpha", 2.0), ("beta", 5.0)] + self.test_pymc_params_match_rv_ones(params, params, pm.Beta) + + def test_beta_mu_sigma(self): + params = [("mu", 2.0), ("sigma", 5.0)] + expected_alpha, expected_beta = pm.Beta.get_alpha_beta(mu=params[0][1], sigma=params[1][1]) + expected_params = [("alpha", expected_alpha), ("beta", expected_beta)] + self.test_pymc_params_match_rv_ones(params, expected_params, pm.Beta) + + @pytest.mark.skip(reason="Expected to fail due to bug") + def test_exponential(self): + params = [("lam", 10.0)] + expected_params = [("lam", 1 / params[0][1])] + self.test_pymc_params_match_rv_ones(params, expected_params, pm.Exponential) + + def test_cauchy(self): + params = [("alpha", 2.0), ("beta", 5.0)] + self.test_pymc_params_match_rv_ones(params, params, pm.Cauchy) + + def test_half_cauchy(self): + params = [("alpha", 2.0), ("beta", 5.0)] + self.test_pymc_params_match_rv_ones(params, params, pm.HalfCauchy) + + @pytest.mark.skip(reason="Expected to fail due to bug") + def test_gamma_alpha_beta(self): + params = [("alpha", 2.0), ("beta", 5.0)] + expected_params = [("alpha", params[0][1]), ("beta", 1 / params[1][1])] + self.test_pymc_params_match_rv_ones(params, expected_params, pm.Gamma) + + @pytest.mark.skip(reason="Expected to fail due to bug") + def test_gamma_mu_sigma(self): + params = [("mu", 2.0), ("sigma", 5.0)] + expected_alpha, expected_beta = pm.Gamma.get_alpha_beta(mu=params[0][1], sigma=params[1][1]) + expected_params = [("alpha", expected_alpha), ("beta", 1 / expected_beta)] + self.test_pymc_params_match_rv_ones(params, expected_params, pm.Gamma) + + class TestScalarParameterSamples(SeededTest): @pytest.mark.xfail(reason="This distribution has not been refactored for v4") def test_bounded(self): @@ -535,20 +605,6 @@ def ref_rand(size, tau): pymc3_random(BoundedNormal, {"tau": Rplus}, ref_rand=ref_rand) - @pytest.mark.skip(reason="This test is covered by Aesara") - def test_uniform(self): - def ref_rand(size, lower, upper): - return st.uniform.rvs(size=size, loc=lower, scale=upper - lower) - - pymc3_random(pm.Uniform, {"lower": -Rplus, "upper": Rplus}, ref_rand=ref_rand) - - @pytest.mark.skip(reason="This test is covered by Aesara") - def test_normal(self): - def ref_rand(size, mu, sigma): - return st.norm.rvs(size=size, loc=mu, scale=sigma) - - pymc3_random(pm.Normal, {"mu": R, "sigma": Rplus}, ref_rand=ref_rand) - @pytest.mark.xfail(reason="This distribution has not been refactored for v4") def test_truncated_normal(self): def ref_rand(size, mu, sigma, lower, upper): @@ -587,13 +643,6 @@ def ref_rand(size, alpha, mu, sigma): pymc3_random(pm.SkewNormal, {"mu": R, "sigma": Rplus, "alpha": R}, ref_rand=ref_rand) - @pytest.mark.skip(reason="This test is covered by Aesara") - def test_half_normal(self): - def ref_rand(size, tau): - return st.halfnorm.rvs(size=size, loc=0, scale=tau ** -0.5) - - pymc3_random(pm.HalfNormal, {"tau": Rplus}, ref_rand=ref_rand) - @pytest.mark.xfail(reason="This distribution has not been refactored for v4") def test_wald(self): # Cannot do anything too exciting as scipy wald is a @@ -607,13 +656,6 @@ def ref_rand(size, mu, lam, alpha): ref_rand=ref_rand, ) - @pytest.mark.skip(reason="This test is covered by Aesara") - def test_beta(self): - def ref_rand(size, alpha, beta): - return clipped_beta_rvs(a=alpha, b=beta, size=size) - - pymc3_random(pm.Beta, {"alpha": Rplus, "beta": Rplus}, ref_rand=ref_rand) - @pytest.mark.xfail(reason="This distribution has not been refactored for v4") def test_laplace(self): def ref_rand(size, mu, b): @@ -648,20 +690,7 @@ def ref_rand(size, nu, mu, lam): pymc3_random(pm.StudentT, {"nu": Rplus, "mu": R, "lam": Rplus}, ref_rand=ref_rand) @pytest.mark.skip(reason="This test is covered by Aesara") - def test_cauchy(self): - def ref_rand(size, alpha, beta): - return st.cauchy.rvs(alpha, beta, size=size) - - pymc3_random(pm.Cauchy, {"alpha": R, "beta": Rplusbig}, ref_rand=ref_rand) - @pytest.mark.skip(reason="This test is covered by Aesara") - def test_half_cauchy(self): - def ref_rand(size, beta): - return st.halfcauchy.rvs(scale=beta, size=size) - - pymc3_random(pm.HalfCauchy, {"beta": Rplusbig}, ref_rand=ref_rand) - - @pytest.mark.skip(reason="This test is covered by Aesara") def test_inverse_gamma(self): def ref_rand(size, alpha, beta): return st.invgamma.rvs(a=alpha, scale=beta, size=size) From 45180b1f81222256ca819802ad942993a55f9208 Mon Sep 17 00:00:00 2001 From: drabbit17 Date: Thu, 1 Apr 2021 19:54:24 +0100 Subject: [PATCH 02/19] Change tests for more refactored distributions. More details can be found on issue #4554 https://github.com/pymc-devs/pymc3/issues/4554 --- pymc3/distributions/discrete.py | 7 +- pymc3/tests/test_distributions_random.py | 95 ++++++++++++------------ 2 files changed, 50 insertions(+), 52 deletions(-) diff --git a/pymc3/distributions/discrete.py b/pymc3/distributions/discrete.py index 5595864fa..633539d17 100644 --- a/pymc3/distributions/discrete.py +++ b/pymc3/distributions/discrete.py @@ -713,16 +713,16 @@ def NegBinom(a, m, x): @classmethod def dist(cls, mu=None, alpha=None, p=None, n=None, *args, **kwargs): - n, p = cls.get_mu_alpha(mu, alpha, p, n) + n, p = cls.get_n_p(mu, alpha, p, n) n = at.as_tensor_variable(floatX(n)) p = at.as_tensor_variable(floatX(p)) return super().dist([n, p], *args, **kwargs) @classmethod - def get_mu_alpha(cls, mu=None, alpha=None, p=None, n=None): + def get_n_p(cls, mu=None, alpha=None, p=None, n=None): if n is None: if alpha is not None: - n = at.as_tensor_variable(floatX(alpha)) + n = alpha else: raise ValueError("Incompatible parametrization. Must specify either alpha or n.") elif alpha is not None: @@ -730,7 +730,6 @@ def get_mu_alpha(cls, mu=None, alpha=None, p=None, n=None): if p is None: if mu is not None: - mu = at.as_tensor_variable(floatX(mu)) p = n / (mu + n) else: raise ValueError("Incompatible parametrization. Must specify either mu or p.") diff --git a/pymc3/tests/test_distributions_random.py b/pymc3/tests/test_distributions_random.py index 9452c4556..077d4ed46 100644 --- a/pymc3/tests/test_distributions_random.py +++ b/pymc3/tests/test_distributions_random.py @@ -533,9 +533,7 @@ def get_inputs_from_apply_node_outputs(outputs): # I am assuming there will always only be 1 Apply parent node in this context return parents[0].inputs - def test_pymc_params_match_rv_ones( - self, pymc_params, expected_aesara_params, pymc_dist, decimal=6 - ): + def _pymc_params_match_rv_ones(self, pymc_params, expected_aesara_params, pymc_dist, decimal=6): pymc_dist_output = pymc_dist.dist(**dict(pymc_params)) aesera_dist_inputs = self.get_inputs_from_apply_node_outputs(pymc_dist_output)[3:] assert len(expected_aesara_params) == len(aesera_dist_inputs) @@ -546,52 +544,88 @@ def test_pymc_params_match_rv_ones( def test_normal(self): params = [("mu", 5.0), ("sigma", 10.0)] - self.test_pymc_params_match_rv_ones(params, params, pm.Normal) + self._pymc_params_match_rv_ones(params, params, pm.Normal) def test_uniform(self): params = [("lower", 0.5), ("upper", 1.5)] - self.test_pymc_params_match_rv_ones(params, params, pm.Uniform) + self._pymc_params_match_rv_ones(params, params, pm.Uniform) def test_half_normal(self): params, expected_aesara_params = [("sigma", 10.0)], [("mean", 0), ("sigma", 10.0)] - self.test_pymc_params_match_rv_ones(params, expected_aesara_params, pm.HalfNormal) + self._pymc_params_match_rv_ones(params, expected_aesara_params, pm.HalfNormal) def test_beta_alpha_beta(self): params = [("alpha", 2.0), ("beta", 5.0)] - self.test_pymc_params_match_rv_ones(params, params, pm.Beta) + self._pymc_params_match_rv_ones(params, params, pm.Beta) def test_beta_mu_sigma(self): params = [("mu", 2.0), ("sigma", 5.0)] expected_alpha, expected_beta = pm.Beta.get_alpha_beta(mu=params[0][1], sigma=params[1][1]) expected_params = [("alpha", expected_alpha), ("beta", expected_beta)] - self.test_pymc_params_match_rv_ones(params, expected_params, pm.Beta) + self._pymc_params_match_rv_ones(params, expected_params, pm.Beta) @pytest.mark.skip(reason="Expected to fail due to bug") def test_exponential(self): params = [("lam", 10.0)] expected_params = [("lam", 1 / params[0][1])] - self.test_pymc_params_match_rv_ones(params, expected_params, pm.Exponential) + self._pymc_params_match_rv_ones(params, expected_params, pm.Exponential) def test_cauchy(self): params = [("alpha", 2.0), ("beta", 5.0)] - self.test_pymc_params_match_rv_ones(params, params, pm.Cauchy) + self._pymc_params_match_rv_ones(params, params, pm.Cauchy) def test_half_cauchy(self): params = [("alpha", 2.0), ("beta", 5.0)] - self.test_pymc_params_match_rv_ones(params, params, pm.HalfCauchy) + self._pymc_params_match_rv_ones(params, params, pm.HalfCauchy) @pytest.mark.skip(reason="Expected to fail due to bug") def test_gamma_alpha_beta(self): params = [("alpha", 2.0), ("beta", 5.0)] expected_params = [("alpha", params[0][1]), ("beta", 1 / params[1][1])] - self.test_pymc_params_match_rv_ones(params, expected_params, pm.Gamma) + self._pymc_params_match_rv_ones(params, expected_params, pm.Gamma) @pytest.mark.skip(reason="Expected to fail due to bug") def test_gamma_mu_sigma(self): params = [("mu", 2.0), ("sigma", 5.0)] expected_alpha, expected_beta = pm.Gamma.get_alpha_beta(mu=params[0][1], sigma=params[1][1]) expected_params = [("alpha", expected_alpha), ("beta", 1 / expected_beta)] - self.test_pymc_params_match_rv_ones(params, expected_params, pm.Gamma) + self._pymc_params_match_rv_ones(params, expected_params, pm.Gamma) + + def test_inverse_gamma_alpha_beta(self): + params = [("alpha", 2.0), ("beta", 5.0)] + self._pymc_params_match_rv_ones(params, params, pm.InverseGamma) + + def test_inverse_gamma_mu_sigma(self): + params = [("mu", 2.0), ("sigma", 5.0)] + expected_alpha, expected_beta = pm.InverseGamma._get_alpha_beta( + mu=params[0][1], sigma=params[1][1], alpha=None, beta=None + ) + expected_params = [("alpha", expected_alpha), ("beta", expected_beta)] + self._pymc_params_match_rv_ones(params, expected_params, pm.InverseGamma) + + def test_binomial(self): + params = [("n", 100), ("p", 0.33)] + self._pymc_params_match_rv_ones(params, params, pm.Binomial) + + def test_negative_binomial(self): + params = [("n", 100), ("p", 0.33)] + self._pymc_params_match_rv_ones(params, params, pm.NegativeBinomial) + + def test_negative_binomial_mu_sigma(self): + params = [("mu", 5.0), ("alpha", 8.0)] + expected_n, expected_p = pm.NegativeBinomial.get_n_p( + mu=params[0][1], alpha=params[1][1], n=None, p=None + ) + expected_params = [("n", expected_n), ("p", expected_p)] + self._pymc_params_match_rv_ones(params, expected_params, pm.NegativeBinomial) + + def test_bernoulli(self): + params = [("p", 0.33)] + self._pymc_params_match_rv_ones(params, params, pm.Bernoulli) + + def test_poisson(self): + params = [("mu", 4)] + self._pymc_params_match_rv_ones(params, params, pm.Poisson) class TestScalarParameterSamples(SeededTest): @@ -689,14 +723,6 @@ def ref_rand(size, nu, mu, lam): pymc3_random(pm.StudentT, {"nu": Rplus, "mu": R, "lam": Rplus}, ref_rand=ref_rand) - @pytest.mark.skip(reason="This test is covered by Aesara") - - def test_inverse_gamma(self): - def ref_rand(size, alpha, beta): - return st.invgamma.rvs(a=alpha, scale=beta, size=size) - - pymc3_random(pm.InverseGamma, {"alpha": Rplus, "beta": Rplus}, ref_rand=ref_rand) - @pytest.mark.xfail(reason="This distribution has not been refactored for v4") def test_ex_gaussian(self): def ref_rand(size, mu, sigma, nu): @@ -736,10 +762,6 @@ def test_half_flat(self): with pytest.raises(ValueError): f.random(1) - @pytest.mark.skip(reason="This test is covered by Aesara") - def test_binomial(self): - pymc3_random_discrete(pm.Binomial, {"n": Nat, "p": Unit}, ref_rand=st.binom.rvs) - @pytest.mark.xfail(reason="This distribution has not been refactored for v4") @pytest.mark.xfail( sys.platform.startswith("win"), @@ -753,29 +775,6 @@ def test_beta_binomial(self): def _beta_bin(self, n, alpha, beta, size=None): return st.binom.rvs(n, st.beta.rvs(a=alpha, b=beta, size=size)) - @pytest.mark.skip(reason="This test is covered by Aesara") - def test_bernoulli(self): - pymc3_random_discrete( - pm.Bernoulli, {"p": Unit}, ref_rand=lambda size, p=None: st.bernoulli.rvs(p, size=size) - ) - - @pytest.mark.skip(reason="This test is covered by Aesara") - def test_poisson(self): - pymc3_random_discrete(pm.Poisson, {"mu": Rplusbig}, size=500, ref_rand=st.poisson.rvs) - - @pytest.mark.skip(reason="This test is covered by Aesara") - def test_negative_binomial(self): - def ref_rand(size, alpha, mu): - return st.nbinom.rvs(alpha, alpha / (mu + alpha), size=size) - - pymc3_random_discrete( - pm.NegativeBinomial, - {"mu": Rplusbig, "alpha": Rplusbig}, - size=100, - fails=50, - ref_rand=ref_rand, - ) - @pytest.mark.xfail(reason="This distribution has not been refactored for v4") def test_geometric(self): pymc3_random_discrete(pm.Geometric, {"p": Unit}, size=500, fails=50, ref_rand=nr.geometric) From 7fed12851df19e9a082302a9c9b2b41ce392a696 Mon Sep 17 00:00:00 2001 From: drabbit17 Date: Thu, 1 Apr 2021 23:44:26 +0100 Subject: [PATCH 03/19] Change tests for refactored distributions More details can be found on issue #4554 https://github.com/pymc-devs/pymc3/issues/4554 --- pymc3/tests/test_distributions_random.py | 112 ++++++----------------- 1 file changed, 29 insertions(+), 83 deletions(-) diff --git a/pymc3/tests/test_distributions_random.py b/pymc3/tests/test_distributions_random.py index 077d4ed46..b742103d6 100644 --- a/pymc3/tests/test_distributions_random.py +++ b/pymc3/tests/test_distributions_random.py @@ -25,12 +25,12 @@ import scipy.stats as st from numpy.testing import assert_almost_equal -from scipy import linalg from scipy.special import expit import pymc3 as pm from pymc3.aesaraf import change_rv_size, floatX, intX +from pymc3.distributions.multivariate import quaddist_matrix from pymc3.distributions.shape_utils import to_tuple from pymc3.exceptions import ShapeError from pymc3.tests.helpers import SeededTest, select_by_precision @@ -41,7 +41,6 @@ NatSmall, PdMatrix, PdMatrixChol, - PdMatrixCholUpper, R, RandomPdMatrix, RealMatrix, @@ -627,6 +626,34 @@ def test_poisson(self): params = [("mu", 4)] self._pymc_params_match_rv_ones(params, params, pm.Poisson) + def test_mv_distribution(self): + params = [("mu", np.array([1.0, 2.0])), ("cov", np.array([[2.0, 0.0], [0.0, 3.5]]))] + self._pymc_params_match_rv_ones(params, params, pm.MvNormal) + + def test_mv_distribution_chol(self): + params = [("mu", np.array([1.0, 2.0])), ("chol", np.array([[2.0, 0.0], [0.0, 3.5]]))] + expected_cov = quaddist_matrix(chol=params[1][1]) + expected_params = [("mu", np.array([1.0, 2.0])), ("cov", expected_cov.eval())] + self._pymc_params_match_rv_ones(params, expected_params, pm.MvNormal) + + def test_mv_distribution_tau(self): + params = [("mu", np.array([1.0, 2.0])), ("tau", np.array([[2.0, 0.0], [0.0, 3.5]]))] + expected_cov = quaddist_matrix(tau=params[1][1]) + expected_params = [("mu", np.array([1.0, 2.0])), ("cov", expected_cov.eval())] + self._pymc_params_match_rv_ones(params, expected_params, pm.MvNormal) + + def test_dirichlet(self): + params = [("a", np.array([1.0, 2.0]))] + self._pymc_params_match_rv_ones(params, params, pm.Dirichlet) + + def test_multinomial(self): + params = [("n", 85), ("p", np.array([0.28, 0.62, 0.10]))] + self._pymc_params_match_rv_ones(params, params, pm.Multinomial) + + def test_categorical(self): + params = [("p", np.array([0.28, 0.62, 0.10]))] + self._pymc_params_match_rv_ones(params, params, pm.Categorical) + class TestScalarParameterSamples(SeededTest): @pytest.mark.xfail(reason="This distribution has not been refactored for v4") @@ -815,14 +842,6 @@ def ref_rand(size, q, beta): pm.DiscreteWeibull, {"q": Unit, "beta": Rplusdunif}, ref_rand=ref_rand ) - @pytest.mark.skip(reason="This test is covered by Aesara") - @pytest.mark.parametrize("s", [2, 3, 4]) - def test_categorical_random(self, s): - def ref_rand(size, p): - return nr.choice(np.arange(p.shape[0]), p=p, size=size) - - pymc3_random_discrete(pm.Categorical, {"p": Simplex(s)}, ref_rand=ref_rand) - @pytest.mark.xfail(reason="This distribution has not been refactored for v4") def test_constant_dist(self): def ref_rand(size, c): @@ -830,51 +849,6 @@ def ref_rand(size, c): pymc3_random_discrete(pm.Constant, {"c": I}, ref_rand=ref_rand) - @pytest.mark.skip(reason="This test is covered by Aesara") - def test_mv_normal(self): - def ref_rand(size, mu, cov): - return st.multivariate_normal.rvs(mean=mu, cov=cov, size=size) - - def ref_rand_tau(size, mu, tau): - return ref_rand(size, mu, linalg.inv(tau)) - - def ref_rand_chol(size, mu, chol): - return ref_rand(size, mu, np.dot(chol, chol.T)) - - def ref_rand_uchol(size, mu, chol): - return ref_rand(size, mu, np.dot(chol.T, chol)) - - for n in [2, 3]: - pymc3_random( - pm.MvNormal, - {"mu": Vector(R, n), "cov": PdMatrix(n)}, - size=100, - valuedomain=Vector(R, n), - ref_rand=ref_rand, - ) - pymc3_random( - pm.MvNormal, - {"mu": Vector(R, n), "tau": PdMatrix(n)}, - size=100, - valuedomain=Vector(R, n), - ref_rand=ref_rand_tau, - ) - pymc3_random( - pm.MvNormal, - {"mu": Vector(R, n), "chol": PdMatrixChol(n)}, - size=100, - valuedomain=Vector(R, n), - ref_rand=ref_rand_chol, - ) - pymc3_random( - pm.MvNormal, - {"mu": Vector(R, n), "chol": PdMatrixCholUpper(n)}, - size=100, - valuedomain=Vector(R, n), - ref_rand=ref_rand_uchol, - extra_args={"lower": False}, - ) - @pytest.mark.xfail(reason="This distribution has not been refactored for v4") def test_matrix_normal(self): def ref_rand(size, mu, rowcov, colcov): @@ -1017,20 +991,6 @@ def ref_rand(size, nu, Sigma, mu): ref_rand=ref_rand, ) - @pytest.mark.skip(reason="This test is covered by Aesara") - def test_dirichlet(self): - def ref_rand(size, a): - return st.dirichlet.rvs(a, size=size) - - for n in [2, 3]: - pymc3_random( - pm.Dirichlet, - {"a": Vector(Rplus, n)}, - valuedomain=Simplex(n), - size=100, - ref_rand=ref_rand, - ) - @pytest.mark.xfail(reason="This distribution has not been refactored for v4") def test_dirichlet_multinomial(self): def ref_rand(size, a, n): @@ -1098,20 +1058,6 @@ def test_dirichlet_multinomial_dist_ShapeError(self, n, a, shape, expectation): with expectation: m.random() - @pytest.mark.skip(reason="This test is covered by Aesara") - def test_multinomial(self): - def ref_rand(size, p, n): - return nr.multinomial(pvals=p, n=n, size=size) - - for n in [2, 3]: - pymc3_random_discrete( - pm.Multinomial, - {"p": Simplex(n), "n": Nat}, - valuedomain=Vector(Nat, n), - size=100, - ref_rand=ref_rand, - ) - @pytest.mark.xfail(reason="This distribution has not been refactored for v4") def test_gumbel(self): def ref_rand(size, mu, beta): From 6cb7a6b4f93c9662e7f70f543b5be42eb80ff9af Mon Sep 17 00:00:00 2001 From: drabbit17 Date: Fri, 2 Apr 2021 00:33:23 +0100 Subject: [PATCH 04/19] Remove tests for random variable samples shape and size Most of the random variable logic has been moved to aesara, as well as most of the relative tests. More details can be found on issue #4554 --- pymc3/tests/test_distributions_random.py | 106 +---------------------- 1 file changed, 1 insertion(+), 105 deletions(-) diff --git a/pymc3/tests/test_distributions_random.py b/pymc3/tests/test_distributions_random.py index b742103d6..2d5d0741e 100644 --- a/pymc3/tests/test_distributions_random.py +++ b/pymc3/tests/test_distributions_random.py @@ -240,12 +240,6 @@ class TestGaussianRandomWalk(BaseTestCases.BaseTestCase): default_shape = (1,) -@pytest.mark.skip(reason="This test is covered by Aesara") -class TestNormal(BaseTestCases.BaseTestCase): - distribution = pm.Normal - params = {"mu": 0.0, "tau": 1.0} - - @pytest.mark.xfail(reason="This distribution has not been refactored for v4") class TestTruncatedNormal(BaseTestCases.BaseTestCase): distribution = pm.TruncatedNormal @@ -270,18 +264,6 @@ class TestSkewNormal(BaseTestCases.BaseTestCase): params = {"mu": 0.0, "sigma": 1.0, "alpha": 5.0} -@pytest.mark.skip(reason="This test is covered by Aesara") -class TestHalfNormal(BaseTestCases.BaseTestCase): - distribution = pm.HalfNormal - params = {"tau": 1.0} - - -@pytest.mark.skip(reason="This test is covered by Aesara") -class TestUniform(BaseTestCases.BaseTestCase): - distribution = pm.Uniform - params = {"lower": 0.0, "upper": 1.0} - - @pytest.mark.xfail(reason="This distribution has not been refactored for v4") class TestTriangular(BaseTestCases.BaseTestCase): distribution = pm.Triangular @@ -305,12 +287,6 @@ class TestKumaraswamy(BaseTestCases.BaseTestCase): params = {"a": 1.0, "b": 1.0} -@pytest.mark.skip(reason="This test is covered by Aesara") -class TestExponential(BaseTestCases.BaseTestCase): - distribution = pm.Exponential - params = {"lam": 1.0} - - @pytest.mark.xfail(reason="This distribution has not been refactored for v4") class TestLaplace(BaseTestCases.BaseTestCase): distribution = pm.Laplace @@ -335,30 +311,6 @@ class TestStudentT(BaseTestCases.BaseTestCase): params = {"nu": 5.0, "mu": 0.0, "lam": 1.0} -@pytest.mark.skip(reason="This test is covered by Aesara") -class TestCauchy(BaseTestCases.BaseTestCase): - distribution = pm.Cauchy - params = {"alpha": 1.0, "beta": 1.0} - - -@pytest.mark.skip(reason="This test is covered by Aesara") -class TestHalfCauchy(BaseTestCases.BaseTestCase): - distribution = pm.HalfCauchy - params = {"beta": 1.0} - - -@pytest.mark.skip(reason="This test is covered by Aesara") -class TestGamma(BaseTestCases.BaseTestCase): - distribution = pm.Gamma - params = {"alpha": 1.0, "beta": 1.0} - - -@pytest.mark.skip(reason="This test is covered by Aesara") -class TestInverseGamma(BaseTestCases.BaseTestCase): - distribution = pm.InverseGamma - params = {"alpha": 0.5, "beta": 0.5} - - @pytest.mark.xfail(reason="This distribution has not been refactored for v4") class TestChiSquared(BaseTestCases.BaseTestCase): distribution = pm.ChiSquared @@ -401,12 +353,6 @@ class TestLogitNormal(BaseTestCases.BaseTestCase): params = {"mu": 0.0, "sigma": 1.0} -@pytest.mark.skip(reason="This test is covered by Aesara") -class TestBinomial(BaseTestCases.BaseTestCase): - distribution = pm.Binomial - params = {"n": 5, "p": 0.5} - - @pytest.mark.xfail(reason="This distribution has not been refactored for v4") class TestBetaBinomial(BaseTestCases.BaseTestCase): distribution = pm.BetaBinomial @@ -419,23 +365,12 @@ class TestBernoulli(BaseTestCases.BaseTestCase): params = {"p": 0.5} +@pytest.mark.xfail(reason="This distribution has not been refactored for v4") class TestDiscreteWeibull(BaseTestCases.BaseTestCase): distribution = pm.DiscreteWeibull params = {"q": 0.25, "beta": 2.0} -@pytest.mark.skip(reason="This test is covered by Aesara") -class TestPoisson(BaseTestCases.BaseTestCase): - distribution = pm.Poisson - params = {"mu": 1.0} - - -@pytest.mark.skip(reason="This test is covered by Aesara") -class TestNegativeBinomial(BaseTestCases.BaseTestCase): - distribution = pm.NegativeBinomial - params = {"mu": 1.0, "alpha": 1.0} - - @pytest.mark.xfail(reason="This distribution has not been refactored for v4") class TestConstant(BaseTestCases.BaseTestCase): distribution = pm.Constant @@ -484,45 +419,6 @@ class TestMoyal(BaseTestCases.BaseTestCase): params = {"mu": 0.0, "sigma": 1.0} -@pytest.mark.skip(reason="This test is covered by Aesara") -class TestCategorical(BaseTestCases.BaseTestCase): - distribution = pm.Categorical - params = {"p": np.ones(BaseTestCases.BaseTestCase.shape)} - - def get_random_variable( - self, shape, with_vector_params=False, **kwargs - ): # don't transform categories - return super().get_random_variable(shape, with_vector_params=False, **kwargs) - - def test_probability_vector_shape(self): - """Check that if a 2d array of probabilities are passed to categorical correct shape is returned""" - p = np.ones((10, 5)) - assert pm.Categorical.dist(p=p).random().shape == (10,) - assert pm.Categorical.dist(p=p).random(size=4).shape == (4, 10) - p = np.ones((3, 7, 5)) - assert pm.Categorical.dist(p=p).random().shape == (3, 7) - assert pm.Categorical.dist(p=p).random(size=4).shape == (4, 3, 7) - - -@pytest.mark.skip(reason="This test is covered by Aesara") -class TestDirichlet(SeededTest): - @pytest.mark.parametrize( - "shape, size", - [ - ((2), (1)), - ((2), (2)), - ((2, 2), (2, 100)), - ((3, 4), (3, 4)), - ((3, 4), (3, 4, 100)), - ((3, 4), (100)), - ((3, 4), (1)), - ], - ) - def test_dirichlet_random_shape(self, shape, size): - out_shape = to_tuple(size) + to_tuple(shape) - assert pm.Dirichlet.dist(a=np.ones(shape)).random(size=size).shape == out_shape - - class TestCorrectParametrizationMappingPymcToScipy(SeededTest): @staticmethod def get_inputs_from_apply_node_outputs(outputs): From fe5d7d97baf4eb171e8b7d674f4a9c4cae7e5ea3 Mon Sep 17 00:00:00 2001 From: drabbit17 Date: Sat, 3 Apr 2021 18:29:30 +0100 Subject: [PATCH 05/19] Fix test for half cauchy, renmae mv normal tests and add test for Bernoulli --- pymc3/tests/test_distributions_random.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/pymc3/tests/test_distributions_random.py b/pymc3/tests/test_distributions_random.py index 2d5d0741e..801b1b93f 100644 --- a/pymc3/tests/test_distributions_random.py +++ b/pymc3/tests/test_distributions_random.py @@ -470,8 +470,9 @@ def test_cauchy(self): self._pymc_params_match_rv_ones(params, params, pm.Cauchy) def test_half_cauchy(self): - params = [("alpha", 2.0), ("beta", 5.0)] - self._pymc_params_match_rv_ones(params, params, pm.HalfCauchy) + params = [("beta", 5.0)] + expected_params = [("alpha", 0.0), ("beta", 5.0)] + self._pymc_params_match_rv_ones(params, expected_params, pm.HalfCauchy) @pytest.mark.skip(reason="Expected to fail due to bug") def test_gamma_alpha_beta(self): @@ -518,21 +519,27 @@ def test_bernoulli(self): params = [("p", 0.33)] self._pymc_params_match_rv_ones(params, params, pm.Bernoulli) + def test_bernoulli_logit_p(self): + logit_p_parameter = 1.0 + bernoulli_sample = pm.Bernoulli.dist(logit_p=logit_p_parameter) + with pytest.raises(ValueError): + bernoulli_sample.eval() + def test_poisson(self): params = [("mu", 4)] self._pymc_params_match_rv_ones(params, params, pm.Poisson) - def test_mv_distribution(self): + def test_mv_normal_distribution(self): params = [("mu", np.array([1.0, 2.0])), ("cov", np.array([[2.0, 0.0], [0.0, 3.5]]))] self._pymc_params_match_rv_ones(params, params, pm.MvNormal) - def test_mv_distribution_chol(self): + def test_mv_normal_distribution_chol(self): params = [("mu", np.array([1.0, 2.0])), ("chol", np.array([[2.0, 0.0], [0.0, 3.5]]))] expected_cov = quaddist_matrix(chol=params[1][1]) expected_params = [("mu", np.array([1.0, 2.0])), ("cov", expected_cov.eval())] self._pymc_params_match_rv_ones(params, expected_params, pm.MvNormal) - def test_mv_distribution_tau(self): + def test_mv_normal_distribution_tau(self): params = [("mu", np.array([1.0, 2.0])), ("tau", np.array([[2.0, 0.0], [0.0, 3.5]]))] expected_cov = quaddist_matrix(tau=params[1][1]) expected_params = [("mu", np.array([1.0, 2.0])), ("cov", expected_cov.eval())] From 6b576c42d5bc90dd40342b3684376b742e595ef6 Mon Sep 17 00:00:00 2001 From: drabbit17 Date: Sun, 4 Apr 2021 10:53:31 +0100 Subject: [PATCH 06/19] Add test checking PyMC samples match the aesara ones Also mark test_categorical as expected to fail due to bug on aesara side. The bug is going to be fixed with 2.0.5 release, so we need to bump the version for categorical and the test to pass. --- pymc3/tests/helpers.py | 6 ++ pymc3/tests/test_distributions_random.py | 82 ++++++++++++++++-------- 2 files changed, 60 insertions(+), 28 deletions(-) diff --git a/pymc3/tests/helpers.py b/pymc3/tests/helpers.py index 243154774..2cd817b59 100644 --- a/pymc3/tests/helpers.py +++ b/pymc3/tests/helpers.py @@ -27,6 +27,7 @@ class SeededTest: random_seed = 20160911 + random_state = None @classmethod def setup_class(cls): @@ -40,6 +41,11 @@ def setup_method(self): def teardown_method(self): set_at_rng(self.old_at_rng) + def get_random_state(self): + if self.random_state is None: + self.random_state = nr.RandomState(self.random_seed) + return self.random_state + class LoggingHandler(BufferingHandler): def __init__(self, matcher): diff --git a/pymc3/tests/test_distributions_random.py b/pymc3/tests/test_distributions_random.py index 801b1b93f..019a84a9d 100644 --- a/pymc3/tests/test_distributions_random.py +++ b/pymc3/tests/test_distributions_random.py @@ -24,7 +24,7 @@ import pytest import scipy.stats as st -from numpy.testing import assert_almost_equal +from numpy.testing import assert_almost_equal, assert_array_almost_equal from scipy.special import expit import pymc3 as pm @@ -428,8 +428,21 @@ def get_inputs_from_apply_node_outputs(outputs): # I am assuming there will always only be 1 Apply parent node in this context return parents[0].inputs - def _pymc_params_match_rv_ones(self, pymc_params, expected_aesara_params, pymc_dist, decimal=6): - pymc_dist_output = pymc_dist.dist(**dict(pymc_params)) + def _compare_pymc_sampling_with_aesara_one( + self, pymc_params, expected_aesara_params, pymc_dist, size=15, decimal=6 + ): + pymc_params.append(("size", size)) + pymc_dist_output = pymc_dist.dist( + rng=aesara.shared(self.get_random_state()), **dict(pymc_params) + ) + self._pymc_params_match_aesara_rv_ones(pymc_dist_output, expected_aesara_params, decimal) + self._pymc_sample_matches_aeasara_rv_one( + pymc_dist_output, pymc_dist, expected_aesara_params, size, decimal + ) + + def _pymc_params_match_aesara_rv_ones( + self, pymc_dist_output, expected_aesara_params, decimal=6 + ): aesera_dist_inputs = self.get_inputs_from_apply_node_outputs(pymc_dist_output)[3:] assert len(expected_aesara_params) == len(aesera_dist_inputs) for (expected_name, expected_value), actual_variable in zip( @@ -437,59 +450,69 @@ def _pymc_params_match_rv_ones(self, pymc_params, expected_aesara_params, pymc_d ): assert_almost_equal(expected_value, actual_variable.eval(), decimal=decimal) + def _pymc_sample_matches_aeasara_rv_one( + self, pymc_dist_output, pymc_dist, expected_aesara_params, size, decimal + ): + sample = pymc_dist.rv_op( + *[p[1] for p in expected_aesara_params], + size=size, + rng=aesara.shared(self.get_random_state()), + ) + assert_array_almost_equal(pymc_dist_output.eval(), sample.eval(), decimal=decimal) + def test_normal(self): params = [("mu", 5.0), ("sigma", 10.0)] - self._pymc_params_match_rv_ones(params, params, pm.Normal) + self._compare_pymc_sampling_with_aesara_one(params, list(params), pm.Normal) def test_uniform(self): params = [("lower", 0.5), ("upper", 1.5)] - self._pymc_params_match_rv_ones(params, params, pm.Uniform) + self._compare_pymc_sampling_with_aesara_one(params, list(params), pm.Uniform) def test_half_normal(self): params, expected_aesara_params = [("sigma", 10.0)], [("mean", 0), ("sigma", 10.0)] - self._pymc_params_match_rv_ones(params, expected_aesara_params, pm.HalfNormal) + self._compare_pymc_sampling_with_aesara_one(params, expected_aesara_params, pm.HalfNormal) def test_beta_alpha_beta(self): params = [("alpha", 2.0), ("beta", 5.0)] - self._pymc_params_match_rv_ones(params, params, pm.Beta) + self._compare_pymc_sampling_with_aesara_one(params, list(params), pm.Beta) def test_beta_mu_sigma(self): - params = [("mu", 2.0), ("sigma", 5.0)] + params = [("mu", 0.5), ("sigma", 0.25)] expected_alpha, expected_beta = pm.Beta.get_alpha_beta(mu=params[0][1], sigma=params[1][1]) expected_params = [("alpha", expected_alpha), ("beta", expected_beta)] - self._pymc_params_match_rv_ones(params, expected_params, pm.Beta) + self._compare_pymc_sampling_with_aesara_one(params, expected_params, pm.Beta) @pytest.mark.skip(reason="Expected to fail due to bug") def test_exponential(self): params = [("lam", 10.0)] expected_params = [("lam", 1 / params[0][1])] - self._pymc_params_match_rv_ones(params, expected_params, pm.Exponential) + self._compare_pymc_sampling_with_aesara_one(params, expected_params, pm.Exponential) def test_cauchy(self): params = [("alpha", 2.0), ("beta", 5.0)] - self._pymc_params_match_rv_ones(params, params, pm.Cauchy) + self._compare_pymc_sampling_with_aesara_one(params, list(params), pm.Cauchy) def test_half_cauchy(self): params = [("beta", 5.0)] expected_params = [("alpha", 0.0), ("beta", 5.0)] - self._pymc_params_match_rv_ones(params, expected_params, pm.HalfCauchy) + self._compare_pymc_sampling_with_aesara_one(params, expected_params, pm.HalfCauchy) @pytest.mark.skip(reason="Expected to fail due to bug") def test_gamma_alpha_beta(self): params = [("alpha", 2.0), ("beta", 5.0)] expected_params = [("alpha", params[0][1]), ("beta", 1 / params[1][1])] - self._pymc_params_match_rv_ones(params, expected_params, pm.Gamma) + self._compare_pymc_sampling_with_aesara_one(params, expected_params, pm.Gamma) @pytest.mark.skip(reason="Expected to fail due to bug") def test_gamma_mu_sigma(self): params = [("mu", 2.0), ("sigma", 5.0)] expected_alpha, expected_beta = pm.Gamma.get_alpha_beta(mu=params[0][1], sigma=params[1][1]) expected_params = [("alpha", expected_alpha), ("beta", 1 / expected_beta)] - self._pymc_params_match_rv_ones(params, expected_params, pm.Gamma) + self._compare_pymc_sampling_with_aesara_one(params, expected_params, pm.Gamma) def test_inverse_gamma_alpha_beta(self): params = [("alpha", 2.0), ("beta", 5.0)] - self._pymc_params_match_rv_ones(params, params, pm.InverseGamma) + self._compare_pymc_sampling_with_aesara_one(params, list(params), pm.InverseGamma) def test_inverse_gamma_mu_sigma(self): params = [("mu", 2.0), ("sigma", 5.0)] @@ -497,15 +520,15 @@ def test_inverse_gamma_mu_sigma(self): mu=params[0][1], sigma=params[1][1], alpha=None, beta=None ) expected_params = [("alpha", expected_alpha), ("beta", expected_beta)] - self._pymc_params_match_rv_ones(params, expected_params, pm.InverseGamma) + self._compare_pymc_sampling_with_aesara_one(params, expected_params, pm.InverseGamma) def test_binomial(self): params = [("n", 100), ("p", 0.33)] - self._pymc_params_match_rv_ones(params, params, pm.Binomial) + self._compare_pymc_sampling_with_aesara_one(params, list(params), pm.Binomial) def test_negative_binomial(self): params = [("n", 100), ("p", 0.33)] - self._pymc_params_match_rv_ones(params, params, pm.NegativeBinomial) + self._compare_pymc_sampling_with_aesara_one(params, list(params), pm.NegativeBinomial) def test_negative_binomial_mu_sigma(self): params = [("mu", 5.0), ("alpha", 8.0)] @@ -513,11 +536,11 @@ def test_negative_binomial_mu_sigma(self): mu=params[0][1], alpha=params[1][1], n=None, p=None ) expected_params = [("n", expected_n), ("p", expected_p)] - self._pymc_params_match_rv_ones(params, expected_params, pm.NegativeBinomial) + self._compare_pymc_sampling_with_aesara_one(params, expected_params, pm.NegativeBinomial) def test_bernoulli(self): params = [("p", 0.33)] - self._pymc_params_match_rv_ones(params, params, pm.Bernoulli) + self._compare_pymc_sampling_with_aesara_one(params, list(params), pm.Bernoulli) def test_bernoulli_logit_p(self): logit_p_parameter = 1.0 @@ -526,36 +549,39 @@ def test_bernoulli_logit_p(self): bernoulli_sample.eval() def test_poisson(self): - params = [("mu", 4)] - self._pymc_params_match_rv_ones(params, params, pm.Poisson) + params = [("mu", 4.0)] + self._compare_pymc_sampling_with_aesara_one(params, list(params), pm.Poisson) def test_mv_normal_distribution(self): params = [("mu", np.array([1.0, 2.0])), ("cov", np.array([[2.0, 0.0], [0.0, 3.5]]))] - self._pymc_params_match_rv_ones(params, params, pm.MvNormal) + self._compare_pymc_sampling_with_aesara_one(params, list(params), pm.MvNormal) def test_mv_normal_distribution_chol(self): params = [("mu", np.array([1.0, 2.0])), ("chol", np.array([[2.0, 0.0], [0.0, 3.5]]))] expected_cov = quaddist_matrix(chol=params[1][1]) expected_params = [("mu", np.array([1.0, 2.0])), ("cov", expected_cov.eval())] - self._pymc_params_match_rv_ones(params, expected_params, pm.MvNormal) + self._compare_pymc_sampling_with_aesara_one(params, expected_params, pm.MvNormal) def test_mv_normal_distribution_tau(self): params = [("mu", np.array([1.0, 2.0])), ("tau", np.array([[2.0, 0.0], [0.0, 3.5]]))] expected_cov = quaddist_matrix(tau=params[1][1]) expected_params = [("mu", np.array([1.0, 2.0])), ("cov", expected_cov.eval())] - self._pymc_params_match_rv_ones(params, expected_params, pm.MvNormal) + self._compare_pymc_sampling_with_aesara_one(params, expected_params, pm.MvNormal) def test_dirichlet(self): params = [("a", np.array([1.0, 2.0]))] - self._pymc_params_match_rv_ones(params, params, pm.Dirichlet) + self._compare_pymc_sampling_with_aesara_one(params, list(params), pm.Dirichlet) def test_multinomial(self): params = [("n", 85), ("p", np.array([0.28, 0.62, 0.10]))] - self._pymc_params_match_rv_ones(params, params, pm.Multinomial) + self._compare_pymc_sampling_with_aesara_one(params, list(params), pm.Multinomial) + @pytest.mark.xfail( + reason="Requires bug fix in aesara 2.0.5 release. Commit id 02378861f1a77135f2556018630092a09262ea76" + ) def test_categorical(self): params = [("p", np.array([0.28, 0.62, 0.10]))] - self._pymc_params_match_rv_ones(params, params, pm.Categorical) + self._compare_pymc_sampling_with_aesara_one(params, list(params), pm.Categorical) class TestScalarParameterSamples(SeededTest): From b50e92fa72e48a3cb61dcb25ae8ae5ad748d8997 Mon Sep 17 00:00:00 2001 From: drabbit17 Date: Sun, 4 Apr 2021 11:04:14 +0100 Subject: [PATCH 07/19] Move Aesara to 2.0.5 to include Gumbel distribution --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2ecc4c058..a508d1ef9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -aesara>=2.0.1 +aesara>=2.0.5 arviz>=0.11.2 cachetools>=4.2.1 dill From 78ac5aced486b433c4c10326ac082603ee82d84e Mon Sep 17 00:00:00 2001 From: drabbit17 Date: Sun, 4 Apr 2021 12:37:23 +0100 Subject: [PATCH 08/19] Enamble exponential and gamma tests following bug-fix --- pymc3/tests/test_distributions_random.py | 29 ++++++++++++++++-------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/pymc3/tests/test_distributions_random.py b/pymc3/tests/test_distributions_random.py index 019a84a9d..4ceb101fc 100644 --- a/pymc3/tests/test_distributions_random.py +++ b/pymc3/tests/test_distributions_random.py @@ -429,15 +429,23 @@ def get_inputs_from_apply_node_outputs(outputs): return parents[0].inputs def _compare_pymc_sampling_with_aesara_one( - self, pymc_params, expected_aesara_params, pymc_dist, size=15, decimal=6 + self, + pymc_params, + expected_aesara_params, + pymc_dist, + size=15, + decimal=6, + sampling_aesara_params=None, ): pymc_params.append(("size", size)) pymc_dist_output = pymc_dist.dist( rng=aesara.shared(self.get_random_state()), **dict(pymc_params) ) self._pymc_params_match_aesara_rv_ones(pymc_dist_output, expected_aesara_params, decimal) + if sampling_aesara_params is None: + sampling_aesara_params = expected_aesara_params self._pymc_sample_matches_aeasara_rv_one( - pymc_dist_output, pymc_dist, expected_aesara_params, size, decimal + pymc_dist_output, pymc_dist, sampling_aesara_params, size, decimal ) def _pymc_params_match_aesara_rv_ones( @@ -482,7 +490,6 @@ def test_beta_mu_sigma(self): expected_params = [("alpha", expected_alpha), ("beta", expected_beta)] self._compare_pymc_sampling_with_aesara_one(params, expected_params, pm.Beta) - @pytest.mark.skip(reason="Expected to fail due to bug") def test_exponential(self): params = [("lam", 10.0)] expected_params = [("lam", 1 / params[0][1])] @@ -497,25 +504,29 @@ def test_half_cauchy(self): expected_params = [("alpha", 0.0), ("beta", 5.0)] self._compare_pymc_sampling_with_aesara_one(params, expected_params, pm.HalfCauchy) - @pytest.mark.skip(reason="Expected to fail due to bug") def test_gamma_alpha_beta(self): params = [("alpha", 2.0), ("beta", 5.0)] expected_params = [("alpha", params[0][1]), ("beta", 1 / params[1][1])] - self._compare_pymc_sampling_with_aesara_one(params, expected_params, pm.Gamma) + sampling_aesara_params = [("alpha", params[0][1]), ("beta", params[1][1])] + self._compare_pymc_sampling_with_aesara_one( + params, expected_params, pm.Gamma, sampling_aesara_params=sampling_aesara_params + ) - @pytest.mark.skip(reason="Expected to fail due to bug") def test_gamma_mu_sigma(self): - params = [("mu", 2.0), ("sigma", 5.0)] + params = [("mu", 0.5), ("sigma", 0.25)] expected_alpha, expected_beta = pm.Gamma.get_alpha_beta(mu=params[0][1], sigma=params[1][1]) expected_params = [("alpha", expected_alpha), ("beta", 1 / expected_beta)] - self._compare_pymc_sampling_with_aesara_one(params, expected_params, pm.Gamma) + sampling_aesara_params = [("alpha", expected_alpha), ("beta", expected_beta)] + self._compare_pymc_sampling_with_aesara_one( + params, expected_params, pm.Gamma, sampling_aesara_params=sampling_aesara_params + ) def test_inverse_gamma_alpha_beta(self): params = [("alpha", 2.0), ("beta", 5.0)] self._compare_pymc_sampling_with_aesara_one(params, list(params), pm.InverseGamma) def test_inverse_gamma_mu_sigma(self): - params = [("mu", 2.0), ("sigma", 5.0)] + params = [("mu", 0.5), ("sigma", 0.25)] expected_alpha, expected_beta = pm.InverseGamma._get_alpha_beta( mu=params[0][1], sigma=params[1][1], alpha=None, beta=None ) From b7afa5d61c743cd175961dc726f27e71dbcab5e0 Mon Sep 17 00:00:00 2001 From: drabbit17 Date: Sun, 4 Apr 2021 12:47:04 +0100 Subject: [PATCH 09/19] Enable categorical test following aesara version bump to 2.0.5 and relative bug-fix --- pymc3/tests/test_distributions_random.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pymc3/tests/test_distributions_random.py b/pymc3/tests/test_distributions_random.py index 4ceb101fc..0a0a0dac5 100644 --- a/pymc3/tests/test_distributions_random.py +++ b/pymc3/tests/test_distributions_random.py @@ -587,9 +587,6 @@ def test_multinomial(self): params = [("n", 85), ("p", np.array([0.28, 0.62, 0.10]))] self._compare_pymc_sampling_with_aesara_one(params, list(params), pm.Multinomial) - @pytest.mark.xfail( - reason="Requires bug fix in aesara 2.0.5 release. Commit id 02378861f1a77135f2556018630092a09262ea76" - ) def test_categorical(self): params = [("p", np.array([0.28, 0.62, 0.10]))] self._compare_pymc_sampling_with_aesara_one(params, list(params), pm.Categorical) From d6c38479d117e7e3e5ad1073dd2a7b64185c2b5b Mon Sep 17 00:00:00 2001 From: drabbit17 Date: Mon, 5 Apr 2021 11:38:21 +0100 Subject: [PATCH 10/19] Few small cosmetic changes: - replace list of tuples with dict - rename 1 method - move pymc_dist as first argument in function call - replace list(params) with params.copy() --- pymc3/tests/test_distributions_random.py | 166 ++++++++++++----------- 1 file changed, 85 insertions(+), 81 deletions(-) diff --git a/pymc3/tests/test_distributions_random.py b/pymc3/tests/test_distributions_random.py index 0a0a0dac5..d0e1c1725 100644 --- a/pymc3/tests/test_distributions_random.py +++ b/pymc3/tests/test_distributions_random.py @@ -428,168 +428,172 @@ def get_inputs_from_apply_node_outputs(outputs): # I am assuming there will always only be 1 Apply parent node in this context return parents[0].inputs - def _compare_pymc_sampling_with_aesara_one( + def _compare_pymc_sampling_with_aesara( self, + pymc_dist, pymc_params, expected_aesara_params, - pymc_dist, size=15, decimal=6, sampling_aesara_params=None, ): - pymc_params.append(("size", size)) - pymc_dist_output = pymc_dist.dist( - rng=aesara.shared(self.get_random_state()), **dict(pymc_params) - ) - self._pymc_params_match_aesara_rv_ones(pymc_dist_output, expected_aesara_params, decimal) + pymc_params["size"] = size + pymc_dist_output = pymc_dist.dist(rng=aesara.shared(self.get_random_state()), **pymc_params) + self._pymc_params_match_aesara_rv(pymc_dist_output, expected_aesara_params, decimal) if sampling_aesara_params is None: sampling_aesara_params = expected_aesara_params - self._pymc_sample_matches_aeasara_rv_one( + self._pymc_sample_matches_aeasara_rv( pymc_dist_output, pymc_dist, sampling_aesara_params, size, decimal ) - def _pymc_params_match_aesara_rv_ones( - self, pymc_dist_output, expected_aesara_params, decimal=6 - ): + def _pymc_params_match_aesara_rv(self, pymc_dist_output, expected_aesara_params, decimal=6): aesera_dist_inputs = self.get_inputs_from_apply_node_outputs(pymc_dist_output)[3:] assert len(expected_aesara_params) == len(aesera_dist_inputs) for (expected_name, expected_value), actual_variable in zip( - expected_aesara_params, aesera_dist_inputs + expected_aesara_params.items(), aesera_dist_inputs ): assert_almost_equal(expected_value, actual_variable.eval(), decimal=decimal) - def _pymc_sample_matches_aeasara_rv_one( + def _pymc_sample_matches_aeasara_rv( self, pymc_dist_output, pymc_dist, expected_aesara_params, size, decimal ): sample = pymc_dist.rv_op( - *[p[1] for p in expected_aesara_params], + *[p for p in expected_aesara_params.values()], size=size, rng=aesara.shared(self.get_random_state()), ) assert_array_almost_equal(pymc_dist_output.eval(), sample.eval(), decimal=decimal) def test_normal(self): - params = [("mu", 5.0), ("sigma", 10.0)] - self._compare_pymc_sampling_with_aesara_one(params, list(params), pm.Normal) + params = {"mu": 5.0, "sigma": 10.0} + self._compare_pymc_sampling_with_aesara(pm.Normal, params, params.copy()) def test_uniform(self): - params = [("lower", 0.5), ("upper", 1.5)] - self._compare_pymc_sampling_with_aesara_one(params, list(params), pm.Uniform) + params = {"lower": 0.5, "upper": 1.5} + self._compare_pymc_sampling_with_aesara(pm.Uniform, params, params.copy()) def test_half_normal(self): - params, expected_aesara_params = [("sigma", 10.0)], [("mean", 0), ("sigma", 10.0)] - self._compare_pymc_sampling_with_aesara_one(params, expected_aesara_params, pm.HalfNormal) + params, expected_aesara_params = {"sigma": 10.0}, {"mean": 0, "sigma": 10.0} + self._compare_pymc_sampling_with_aesara( + pm.HalfNormal, + params, + expected_aesara_params, + ) def test_beta_alpha_beta(self): - params = [("alpha", 2.0), ("beta", 5.0)] - self._compare_pymc_sampling_with_aesara_one(params, list(params), pm.Beta) + params = {"alpha": 2.0, "beta": 5.0} + self._compare_pymc_sampling_with_aesara(pm.Beta, params, params.copy()) def test_beta_mu_sigma(self): - params = [("mu", 0.5), ("sigma", 0.25)] - expected_alpha, expected_beta = pm.Beta.get_alpha_beta(mu=params[0][1], sigma=params[1][1]) - expected_params = [("alpha", expected_alpha), ("beta", expected_beta)] - self._compare_pymc_sampling_with_aesara_one(params, expected_params, pm.Beta) + params = {"mu": 0.5, "sigma": 0.25} + expected_alpha, expected_beta = pm.Beta.get_alpha_beta( + mu=params["mu"], sigma=params["sigma"] + ) + expected_params = {"alpha": expected_alpha, "beta": expected_beta} + self._compare_pymc_sampling_with_aesara(pm.Beta, params, expected_params) def test_exponential(self): - params = [("lam", 10.0)] - expected_params = [("lam", 1 / params[0][1])] - self._compare_pymc_sampling_with_aesara_one(params, expected_params, pm.Exponential) + params = {"lam": 10.0} + expected_params = {"lam": 1.0 / params["lam"]} + self._compare_pymc_sampling_with_aesara(pm.Exponential, params, expected_params) def test_cauchy(self): - params = [("alpha", 2.0), ("beta", 5.0)] - self._compare_pymc_sampling_with_aesara_one(params, list(params), pm.Cauchy) + params = {"alpha": 2.0, "beta": 5.0} + self._compare_pymc_sampling_with_aesara(pm.Cauchy, params, params.copy()) def test_half_cauchy(self): - params = [("beta", 5.0)] - expected_params = [("alpha", 0.0), ("beta", 5.0)] - self._compare_pymc_sampling_with_aesara_one(params, expected_params, pm.HalfCauchy) + params = {"beta": 5.0} + expected_params = {"alpha": 0.0, "beta": 5.0} + self._compare_pymc_sampling_with_aesara(pm.HalfCauchy, params, expected_params) def test_gamma_alpha_beta(self): - params = [("alpha", 2.0), ("beta", 5.0)] - expected_params = [("alpha", params[0][1]), ("beta", 1 / params[1][1])] - sampling_aesara_params = [("alpha", params[0][1]), ("beta", params[1][1])] - self._compare_pymc_sampling_with_aesara_one( - params, expected_params, pm.Gamma, sampling_aesara_params=sampling_aesara_params + params = {"alpha": 2.0, "beta": 5.0} + expected_params = {"alpha": params["alpha"], "beta": 1 / params["beta"]} + sampling_aesara_params = {"alpha": params["alpha"], "beta": params["beta"]} + self._compare_pymc_sampling_with_aesara( + pm.Gamma, params, expected_params, sampling_aesara_params=sampling_aesara_params ) def test_gamma_mu_sigma(self): - params = [("mu", 0.5), ("sigma", 0.25)] - expected_alpha, expected_beta = pm.Gamma.get_alpha_beta(mu=params[0][1], sigma=params[1][1]) - expected_params = [("alpha", expected_alpha), ("beta", 1 / expected_beta)] - sampling_aesara_params = [("alpha", expected_alpha), ("beta", expected_beta)] - self._compare_pymc_sampling_with_aesara_one( - params, expected_params, pm.Gamma, sampling_aesara_params=sampling_aesara_params + params = {"mu": 0.5, "sigma": 0.25} + expected_alpha, expected_beta = pm.Gamma.get_alpha_beta( + mu=params["mu"], sigma=params["sigma"] + ) + expected_params = {"alpha": expected_alpha, "beta": 1 / expected_beta} + sampling_aesara_params = {"alpha": expected_alpha, "beta": expected_beta} + self._compare_pymc_sampling_with_aesara( + pm.Gamma, params, expected_params, sampling_aesara_params=sampling_aesara_params ) def test_inverse_gamma_alpha_beta(self): - params = [("alpha", 2.0), ("beta", 5.0)] - self._compare_pymc_sampling_with_aesara_one(params, list(params), pm.InverseGamma) + params = {"alpha": 2.0, "beta": 5.0} + self._compare_pymc_sampling_with_aesara(pm.InverseGamma, params, params.copy()) def test_inverse_gamma_mu_sigma(self): - params = [("mu", 0.5), ("sigma", 0.25)] + params = {"mu": 0.5, "sigma": 0.25} expected_alpha, expected_beta = pm.InverseGamma._get_alpha_beta( - mu=params[0][1], sigma=params[1][1], alpha=None, beta=None + alpha=None, beta=None, mu=params["mu"], sigma=params["sigma"] ) - expected_params = [("alpha", expected_alpha), ("beta", expected_beta)] - self._compare_pymc_sampling_with_aesara_one(params, expected_params, pm.InverseGamma) + expected_params = {"alpha": expected_alpha, "beta": expected_beta} + self._compare_pymc_sampling_with_aesara(pm.InverseGamma, params, expected_params) def test_binomial(self): - params = [("n", 100), ("p", 0.33)] - self._compare_pymc_sampling_with_aesara_one(params, list(params), pm.Binomial) + params = {"n": 100, "p": 0.33} + self._compare_pymc_sampling_with_aesara(pm.Binomial, params, params.copy()) def test_negative_binomial(self): - params = [("n", 100), ("p", 0.33)] - self._compare_pymc_sampling_with_aesara_one(params, list(params), pm.NegativeBinomial) + params = {"n": 100, "p": 0.33} + self._compare_pymc_sampling_with_aesara(pm.NegativeBinomial, params, params.copy()) def test_negative_binomial_mu_sigma(self): - params = [("mu", 5.0), ("alpha", 8.0)] + params = {"mu": 5.0, "alpha": 8.0} expected_n, expected_p = pm.NegativeBinomial.get_n_p( - mu=params[0][1], alpha=params[1][1], n=None, p=None + mu=params["mu"], alpha=params["alpha"], n=None, p=None ) - expected_params = [("n", expected_n), ("p", expected_p)] - self._compare_pymc_sampling_with_aesara_one(params, expected_params, pm.NegativeBinomial) + expected_params = {"n": expected_n, "p": expected_p} + self._compare_pymc_sampling_with_aesara(pm.NegativeBinomial, params, expected_params) def test_bernoulli(self): - params = [("p", 0.33)] - self._compare_pymc_sampling_with_aesara_one(params, list(params), pm.Bernoulli) + params = {"p": 0.33} + self._compare_pymc_sampling_with_aesara(pm.Bernoulli, params, params.copy()) def test_bernoulli_logit_p(self): - logit_p_parameter = 1.0 - bernoulli_sample = pm.Bernoulli.dist(logit_p=logit_p_parameter) + params = {"logit_p": 1.0} + bernoulli_sample = pm.Bernoulli.dist(logit_p=params["logit_p"]) with pytest.raises(ValueError): bernoulli_sample.eval() def test_poisson(self): - params = [("mu", 4.0)] - self._compare_pymc_sampling_with_aesara_one(params, list(params), pm.Poisson) + params = {"mu": 4.0} + self._compare_pymc_sampling_with_aesara(pm.Poisson, params, params.copy()) def test_mv_normal_distribution(self): - params = [("mu", np.array([1.0, 2.0])), ("cov", np.array([[2.0, 0.0], [0.0, 3.5]]))] - self._compare_pymc_sampling_with_aesara_one(params, list(params), pm.MvNormal) + params = {"mu": np.array([1.0, 2.0]), "cov": np.array([[2.0, 0.0], [0.0, 3.5]])} + self._compare_pymc_sampling_with_aesara(pm.MvNormal, params, params.copy()) def test_mv_normal_distribution_chol(self): - params = [("mu", np.array([1.0, 2.0])), ("chol", np.array([[2.0, 0.0], [0.0, 3.5]]))] - expected_cov = quaddist_matrix(chol=params[1][1]) - expected_params = [("mu", np.array([1.0, 2.0])), ("cov", expected_cov.eval())] - self._compare_pymc_sampling_with_aesara_one(params, expected_params, pm.MvNormal) + params = {"mu": np.array([1.0, 2.0]), "chol": np.array([[2.0, 0.0], [0.0, 3.5]])} + expected_cov = quaddist_matrix(chol=params["chol"]) + expected_params = {"mu": np.array([1.0, 2.0]), "cov": expected_cov.eval()} + self._compare_pymc_sampling_with_aesara(pm.MvNormal, params, expected_params) def test_mv_normal_distribution_tau(self): - params = [("mu", np.array([1.0, 2.0])), ("tau", np.array([[2.0, 0.0], [0.0, 3.5]]))] - expected_cov = quaddist_matrix(tau=params[1][1]) - expected_params = [("mu", np.array([1.0, 2.0])), ("cov", expected_cov.eval())] - self._compare_pymc_sampling_with_aesara_one(params, expected_params, pm.MvNormal) + params = {"mu": np.array([1.0, 2.0]), "tau": np.array([[2.0, 0.0], [0.0, 3.5]])} + expected_cov = quaddist_matrix(tau=params["tau"]) + expected_params = {"mu": np.array([1.0, 2.0]), "cov": expected_cov.eval()} + self._compare_pymc_sampling_with_aesara(pm.MvNormal, params, expected_params) def test_dirichlet(self): - params = [("a", np.array([1.0, 2.0]))] - self._compare_pymc_sampling_with_aesara_one(params, list(params), pm.Dirichlet) + params = {"a": np.array([1.0, 2.0])} + self._compare_pymc_sampling_with_aesara(pm.Dirichlet, params, params.copy()) def test_multinomial(self): - params = [("n", 85), ("p", np.array([0.28, 0.62, 0.10]))] - self._compare_pymc_sampling_with_aesara_one(params, list(params), pm.Multinomial) + params = {"n": 85, "p": np.array([0.28, 0.62, 0.10])} + self._compare_pymc_sampling_with_aesara(pm.Multinomial, params, params.copy()) def test_categorical(self): - params = [("p", np.array([0.28, 0.62, 0.10]))] - self._compare_pymc_sampling_with_aesara_one(params, list(params), pm.Categorical) + params = {"p": np.array([0.28, 0.62, 0.10])} + self._compare_pymc_sampling_with_aesara(pm.Categorical, params, params.copy()) class TestScalarParameterSamples(SeededTest): From 7b5899ca4efa39d55860a452d8ebab30a33b96c0 Mon Sep 17 00:00:00 2001 From: drabbit17 Date: Mon, 5 Apr 2021 11:41:13 +0100 Subject: [PATCH 11/19] Remove redundant tests --- pymc3/tests/test_distributions_random.py | 35 +----------------------- 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/pymc3/tests/test_distributions_random.py b/pymc3/tests/test_distributions_random.py index d0e1c1725..23d18c957 100644 --- a/pymc3/tests/test_distributions_random.py +++ b/pymc3/tests/test_distributions_random.py @@ -33,7 +33,7 @@ from pymc3.distributions.multivariate import quaddist_matrix from pymc3.distributions.shape_utils import to_tuple from pymc3.exceptions import ShapeError -from pymc3.tests.helpers import SeededTest, select_by_precision +from pymc3.tests.helpers import SeededTest from pymc3.tests.test_distributions import ( Domain, I, @@ -1758,36 +1758,3 @@ def test_with_cov_rv(self, sample_shape, dist_shape, mu_shape): prior = pm.sample_prior_predictive(samples=sample_shape) assert prior["mv"].shape == to_tuple(sample_shape) + dist_shape - - -def test_exponential_parameterization(): - test_lambda = floatX(10.0) - - exp_pymc = pm.Exponential.dist(lam=test_lambda) - (rv_scale,) = exp_pymc.owner.inputs[3:] - - npt.assert_almost_equal(rv_scale.eval(), 1 / test_lambda) - - -def test_gamma_parameterization(): - - test_alpha = floatX(10.0) - test_beta = floatX(100.0) - - gamma_pymc = pm.Gamma.dist(alpha=test_alpha, beta=test_beta) - rv_alpha, rv_inv_beta = gamma_pymc.owner.inputs[3:] - - assert np.array_equal(rv_alpha.eval(), test_alpha) - - decimal = select_by_precision(float64=6, float32=3) - - npt.assert_almost_equal(rv_inv_beta.eval(), 1.0 / test_beta, decimal) - - test_mu = test_alpha / test_beta - test_sigma = np.sqrt(test_mu / test_beta) - - gamma_pymc = pm.Gamma.dist(mu=test_mu, sigma=test_sigma) - rv_alpha, rv_inv_beta = gamma_pymc.owner.inputs[3:] - - npt.assert_almost_equal(rv_alpha.eval(), test_alpha, decimal) - npt.assert_almost_equal(rv_inv_beta.eval(), 1.0 / test_beta, decimal) From b1c40ef1d3aee0c8bac52a57a65d43b69f7949a7 Mon Sep 17 00:00:00 2001 From: drabbit17 Date: Fri, 9 Apr 2021 00:37:28 +0100 Subject: [PATCH 12/19] Further refactoring The refactoring should make it possible testing both the distribution parametrization and sampled values according to need, as well as any other future test. More details on PR #4608 --- pymc3/tests/test_distributions_random.py | 446 +++++++++++++++-------- 1 file changed, 286 insertions(+), 160 deletions(-) diff --git a/pymc3/tests/test_distributions_random.py b/pymc3/tests/test_distributions_random.py index 23d18c957..fefc6673f 100644 --- a/pymc3/tests/test_distributions_random.py +++ b/pymc3/tests/test_distributions_random.py @@ -11,11 +11,12 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - +import functools import itertools import sys from contextlib import ExitStack as does_not_raise +from typing import Callable import aesara import numpy as np @@ -30,6 +31,7 @@ import pymc3 as pm from pymc3.aesaraf import change_rv_size, floatX, intX +from pymc3.distributions.continuous import get_tau_sigma from pymc3.distributions.multivariate import quaddist_matrix from pymc3.distributions.shape_utils import to_tuple from pymc3.exceptions import ShapeError @@ -365,7 +367,6 @@ class TestBernoulli(BaseTestCases.BaseTestCase): params = {"p": 0.5} -@pytest.mark.xfail(reason="This distribution has not been refactored for v4") class TestDiscreteWeibull(BaseTestCases.BaseTestCase): distribution = pm.DiscreteWeibull params = {"q": 0.25, "beta": 2.0} @@ -419,181 +420,314 @@ class TestMoyal(BaseTestCases.BaseTestCase): params = {"mu": 0.0, "sigma": 1.0} -class TestCorrectParametrizationMappingPymcToScipy(SeededTest): - @staticmethod - def get_inputs_from_apply_node_outputs(outputs): - parents = outputs.get_parents() - if not parents: - raise Exception("Parent Apply node missing for output") - # I am assuming there will always only be 1 Apply parent node in this context - return parents[0].inputs +class BaseTestDistribution(SeededTest): + pymc_dist = None + pymc_dist_params = dict() + expected_dist = None + expected_dist_params = dict() + expected_rv_op_params = dict() + tests_to_run = [] + size = 15 + decimal = 6 + + def test_distribution(self) -> None: + with pm.Model(): + self.pymc_dist_output = self.pymc_dist( + **self.pymc_dist_params, + size=self.size, + rng=aesara.shared(self.get_random_state()), + name=f"{self.pymc_dist.rv_op.name}_test", + ) + if self.expected_dist is not None: + self.expected_dist_outcome = self.expected_dist()( + **self.expected_dist_params, size=self.size + ) + for test_name in self.tests_to_run: + self.run_test(test_name) + + def run_test(self, test_name): + { + "check_pymc_dist_matches_expected": self._check_pymc_draws_match_expected, + "check_pymc_params_match_rv_op": self._check_pymc_params_match_rv_op, + }[test_name]() - def _compare_pymc_sampling_with_aesara( + def _check_pymc_draws_match_expected( self, - pymc_dist, - pymc_params, - expected_aesara_params, - size=15, - decimal=6, - sampling_aesara_params=None, ): - pymc_params["size"] = size - pymc_dist_output = pymc_dist.dist(rng=aesara.shared(self.get_random_state()), **pymc_params) - self._pymc_params_match_aesara_rv(pymc_dist_output, expected_aesara_params, decimal) - if sampling_aesara_params is None: - sampling_aesara_params = expected_aesara_params - self._pymc_sample_matches_aeasara_rv( - pymc_dist_output, pymc_dist, sampling_aesara_params, size, decimal + assert_array_almost_equal( + self.pymc_dist_output.eval(), self.expected_dist_outcome, decimal=self.decimal ) - def _pymc_params_match_aesara_rv(self, pymc_dist_output, expected_aesara_params, decimal=6): - aesera_dist_inputs = self.get_inputs_from_apply_node_outputs(pymc_dist_output)[3:] - assert len(expected_aesara_params) == len(aesera_dist_inputs) + def _check_pymc_params_match_rv_op(self) -> None: + try: + aesera_dist_inputs = self.pymc_dist_output.get_parents()[0].inputs[3:] + except: + raise Exception("Parent Apply node missing from output") + assert len(self.expected_rv_op_params) == len(aesera_dist_inputs) for (expected_name, expected_value), actual_variable in zip( - expected_aesara_params.items(), aesera_dist_inputs + self.expected_rv_op_params.items(), aesera_dist_inputs ): - assert_almost_equal(expected_value, actual_variable.eval(), decimal=decimal) + assert_almost_equal(expected_value, actual_variable.eval(), decimal=self.decimal) - def _pymc_sample_matches_aeasara_rv( - self, pymc_dist_output, pymc_dist, expected_aesara_params, size, decimal - ): - sample = pymc_dist.rv_op( - *[p for p in expected_aesara_params.values()], - size=size, - rng=aesara.shared(self.get_random_state()), - ) - assert_array_almost_equal(pymc_dist_output.eval(), sample.eval(), decimal=decimal) - - def test_normal(self): - params = {"mu": 5.0, "sigma": 10.0} - self._compare_pymc_sampling_with_aesara(pm.Normal, params, params.copy()) - - def test_uniform(self): - params = {"lower": 0.5, "upper": 1.5} - self._compare_pymc_sampling_with_aesara(pm.Uniform, params, params.copy()) - - def test_half_normal(self): - params, expected_aesara_params = {"sigma": 10.0}, {"mean": 0, "sigma": 10.0} - self._compare_pymc_sampling_with_aesara( - pm.HalfNormal, - params, - expected_aesara_params, - ) - def test_beta_alpha_beta(self): - params = {"alpha": 2.0, "beta": 5.0} - self._compare_pymc_sampling_with_aesara(pm.Beta, params, params.copy()) +def seeded_scipy_distribution_builder(dist_name: str) -> Callable: + return lambda self: functools.partial( + getattr(st, dist_name).rvs, random_state=self.get_random_state() + ) - def test_beta_mu_sigma(self): - params = {"mu": 0.5, "sigma": 0.25} - expected_alpha, expected_beta = pm.Beta.get_alpha_beta( - mu=params["mu"], sigma=params["sigma"] - ) - expected_params = {"alpha": expected_alpha, "beta": expected_beta} - self._compare_pymc_sampling_with_aesara(pm.Beta, params, expected_params) - - def test_exponential(self): - params = {"lam": 10.0} - expected_params = {"lam": 1.0 / params["lam"]} - self._compare_pymc_sampling_with_aesara(pm.Exponential, params, expected_params) - - def test_cauchy(self): - params = {"alpha": 2.0, "beta": 5.0} - self._compare_pymc_sampling_with_aesara(pm.Cauchy, params, params.copy()) - - def test_half_cauchy(self): - params = {"beta": 5.0} - expected_params = {"alpha": 0.0, "beta": 5.0} - self._compare_pymc_sampling_with_aesara(pm.HalfCauchy, params, expected_params) - - def test_gamma_alpha_beta(self): - params = {"alpha": 2.0, "beta": 5.0} - expected_params = {"alpha": params["alpha"], "beta": 1 / params["beta"]} - sampling_aesara_params = {"alpha": params["alpha"], "beta": params["beta"]} - self._compare_pymc_sampling_with_aesara( - pm.Gamma, params, expected_params, sampling_aesara_params=sampling_aesara_params - ) - def test_gamma_mu_sigma(self): - params = {"mu": 0.5, "sigma": 0.25} - expected_alpha, expected_beta = pm.Gamma.get_alpha_beta( - mu=params["mu"], sigma=params["sigma"] - ) - expected_params = {"alpha": expected_alpha, "beta": 1 / expected_beta} - sampling_aesara_params = {"alpha": expected_alpha, "beta": expected_beta} - self._compare_pymc_sampling_with_aesara( - pm.Gamma, params, expected_params, sampling_aesara_params=sampling_aesara_params - ) +def seeded_numpy_distribution_builder(dist_name: str) -> Callable: + return lambda self: functools.partial( + getattr(np.random.RandomState, dist_name), self.get_random_state() + ) - def test_inverse_gamma_alpha_beta(self): - params = {"alpha": 2.0, "beta": 5.0} - self._compare_pymc_sampling_with_aesara(pm.InverseGamma, params, params.copy()) - def test_inverse_gamma_mu_sigma(self): - params = {"mu": 0.5, "sigma": 0.25} - expected_alpha, expected_beta = pm.InverseGamma._get_alpha_beta( - alpha=None, beta=None, mu=params["mu"], sigma=params["sigma"] - ) - expected_params = {"alpha": expected_alpha, "beta": expected_beta} - self._compare_pymc_sampling_with_aesara(pm.InverseGamma, params, expected_params) +class TestGumbelDistribution(BaseTestDistribution): + pymc_dist = pm.Gumbel + pymc_dist_params = {"mu": 1.5, "beta": 3.0} + expected_rv_op_params = {"mu": 1.5, "beta": 3.0} + expected_dist_params = {"loc": 1.5, "scale": 3.0} + size = 15 + expected_dist = seeded_scipy_distribution_builder("gumbel_r") + tests_to_run = ["check_pymc_params_match_rv_op", "check_pymc_dist_matches_expected"] - def test_binomial(self): - params = {"n": 100, "p": 0.33} - self._compare_pymc_sampling_with_aesara(pm.Binomial, params, params.copy()) - def test_negative_binomial(self): - params = {"n": 100, "p": 0.33} - self._compare_pymc_sampling_with_aesara(pm.NegativeBinomial, params, params.copy()) +class TestNormalDistribution(BaseTestDistribution): + pymc_dist = pm.Normal + pymc_dist_params = {"mu": 5.0, "sigma": 10.0} + expected_rv_op_params = {"mu": 5.0, "sigma": 10.0} + expected_dist_params = {"loc": 5.0, "scale": 10.0} + size = 15 + expected_dist = seeded_numpy_distribution_builder("normal") + tests_to_run = ["check_pymc_params_match_rv_op", "check_pymc_dist_matches_expected"] - def test_negative_binomial_mu_sigma(self): - params = {"mu": 5.0, "alpha": 8.0} - expected_n, expected_p = pm.NegativeBinomial.get_n_p( - mu=params["mu"], alpha=params["alpha"], n=None, p=None - ) - expected_params = {"n": expected_n, "p": expected_p} - self._compare_pymc_sampling_with_aesara(pm.NegativeBinomial, params, expected_params) - def test_bernoulli(self): - params = {"p": 0.33} - self._compare_pymc_sampling_with_aesara(pm.Bernoulli, params, params.copy()) +class TestNormalTauDistribution(BaseTestDistribution): + pymc_dist = pm.Normal + tau, sigma = get_tau_sigma(tau=25.0) + pymc_dist_params = {"mu": 1.0, "sigma": sigma} + expected_rv_op_params = {"mu": 1.0, "sigma": 0.2} + tests_to_run = ["check_pymc_params_match_rv_op"] + + +class TestNormalSdDistribution(BaseTestDistribution): + pymc_dist = pm.Normal + pymc_dist_params = {"mu": 1.0, "sd": 5.0} + expected_rv_op_params = {"mu": 1.0, "sigma": 5.0} + tests_to_run = ["check_pymc_params_match_rv_op"] - def test_bernoulli_logit_p(self): - params = {"logit_p": 1.0} - bernoulli_sample = pm.Bernoulli.dist(logit_p=params["logit_p"]) - with pytest.raises(ValueError): - bernoulli_sample.eval() - def test_poisson(self): - params = {"mu": 4.0} - self._compare_pymc_sampling_with_aesara(pm.Poisson, params, params.copy()) +class TestUniformDistribution(BaseTestDistribution): + pymc_dist = pm.Uniform + pymc_dist_params = {"lower": 0.5, "upper": 1.5} + expected_rv_op_params = {"lower": 0.5, "upper": 1.5} + tests_to_run = ["check_pymc_params_match_rv_op"] - def test_mv_normal_distribution(self): - params = {"mu": np.array([1.0, 2.0]), "cov": np.array([[2.0, 0.0], [0.0, 3.5]])} - self._compare_pymc_sampling_with_aesara(pm.MvNormal, params, params.copy()) - def test_mv_normal_distribution_chol(self): - params = {"mu": np.array([1.0, 2.0]), "chol": np.array([[2.0, 0.0], [0.0, 3.5]])} - expected_cov = quaddist_matrix(chol=params["chol"]) - expected_params = {"mu": np.array([1.0, 2.0]), "cov": expected_cov.eval()} - self._compare_pymc_sampling_with_aesara(pm.MvNormal, params, expected_params) +class TestHalfNormalDistribution(BaseTestDistribution): + pymc_dist = pm.HalfNormal + pymc_dist_params = {"sigma": 10.0} + expected_rv_op_params = {"mean": 0, "sigma": 10.0} + tests_to_run = ["check_pymc_params_match_rv_op"] - def test_mv_normal_distribution_tau(self): - params = {"mu": np.array([1.0, 2.0]), "tau": np.array([[2.0, 0.0], [0.0, 3.5]])} - expected_cov = quaddist_matrix(tau=params["tau"]) - expected_params = {"mu": np.array([1.0, 2.0]), "cov": expected_cov.eval()} - self._compare_pymc_sampling_with_aesara(pm.MvNormal, params, expected_params) - def test_dirichlet(self): - params = {"a": np.array([1.0, 2.0])} - self._compare_pymc_sampling_with_aesara(pm.Dirichlet, params, params.copy()) +class TestHalfNormalTauDistribution(BaseTestDistribution): + pymc_dist = pm.Normal + tau, sigma = get_tau_sigma(tau=25.0) + pymc_dist_params = {"sigma": sigma} + expected_rv_op_params = {"mu": 0.0, "sigma": 0.2} + tests_to_run = ["check_pymc_params_match_rv_op"] - def test_multinomial(self): - params = {"n": 85, "p": np.array([0.28, 0.62, 0.10])} - self._compare_pymc_sampling_with_aesara(pm.Multinomial, params, params.copy()) - def test_categorical(self): - params = {"p": np.array([0.28, 0.62, 0.10])} - self._compare_pymc_sampling_with_aesara(pm.Categorical, params, params.copy()) +class TestHalfNormalSdDistribution(BaseTestDistribution): + pymc_dist = pm.Normal + pymc_dist_params = {"sd": 5.0} + expected_rv_op_params = {"mu": 0.0, "sigma": 5.0} + tests_to_run = ["check_pymc_params_match_rv_op"] + + +class TestBetaAlphaBetaDistribution(BaseTestDistribution): + pymc_dist = pm.Beta + pymc_dist_params = {"alpha": 2.0, "beta": 5.0} + expected_rv_op_params = {"alpha": 2.0, "beta": 5.0} + tests_to_run = ["check_pymc_params_match_rv_op"] + + +class TestBetaMuSigmaDistribution(BaseTestDistribution): + pymc_dist = pm.Beta + pymc_dist_params = {"mu": 0.5, "sigma": 0.25} + expected_alpha, expected_beta = pm.Beta.get_alpha_beta( + mu=pymc_dist_params["mu"], sigma=pymc_dist_params["sigma"] + ) + expected_rv_op_params = {"alpha": expected_alpha, "beta": expected_beta} + tests_to_run = ["check_pymc_params_match_rv_op"] + + +class TestExponentialDistribution(BaseTestDistribution): + pymc_dist = pm.Exponential + pymc_dist_params = {"lam": 10.0} + expected_rv_op_params = {"lam": 1.0 / pymc_dist_params["lam"]} + tests_to_run = ["check_pymc_params_match_rv_op"] + + +class TestCauchyDistribution(BaseTestDistribution): + pymc_dist = pm.Cauchy + pymc_dist_params = {"alpha": 2.0, "beta": 5.0} + expected_rv_op_params = {"alpha": 2.0, "beta": 5.0} + tests_to_run = ["check_pymc_params_match_rv_op"] + + +class TestHalfCauchyDistribution(BaseTestDistribution): + pymc_dist = pm.HalfCauchy + pymc_dist_params = {"beta": 5.0} + expected_rv_op_params = {"alpha": 0.0, "beta": 5.0} + tests_to_run = ["check_pymc_params_match_rv_op"] + + +class TestGammaAlphaBetaDistribution(BaseTestDistribution): + pymc_dist = pm.Gamma + pymc_dist_params = {"alpha": 2.0, "beta": 5.0} + expected_rv_op_params = {"alpha": 2.0, "beta": 1 / 5.0} + tests_to_run = ["check_pymc_params_match_rv_op"] + + +class TestGammaMuSigmaDistribution(BaseTestDistribution): + pymc_dist = pm.Gamma + pymc_dist_params = {"mu": 0.5, "sigma": 0.25} + expected_alpha, expected_beta = pm.Gamma.get_alpha_beta( + mu=pymc_dist_params["mu"], sigma=pymc_dist_params["sigma"] + ) + expected_rv_op_params = {"alpha": expected_alpha, "beta": 1 / expected_beta} + tests_to_run = ["check_pymc_params_match_rv_op"] + + +class TestInverseGammaAlphaBetaDistribution(BaseTestDistribution): + pymc_dist = pm.InverseGamma + pymc_dist_params = {"alpha": 2.0, "beta": 5.0} + expected_rv_op_params = {"alpha": 2.0, "beta": 5.0} + tests_to_run = ["check_pymc_params_match_rv_op"] + + +class TestInverseGammaMuSigmaDistribution(BaseTestDistribution): + pymc_dist = pm.InverseGamma + pymc_dist_params = {"mu": 0.5, "sigma": 0.25} + expected_alpha, expected_beta = pm.InverseGamma._get_alpha_beta( + alpha=None, + beta=None, + mu=pymc_dist_params["mu"], + sigma=pymc_dist_params["sigma"], + ) + expected_rv_op_params = {"alpha": expected_alpha, "beta": expected_beta} + tests_to_run = ["check_pymc_params_match_rv_op"] + + +class TestBinomialDistribution(BaseTestDistribution): + pymc_dist = pm.Binomial + pymc_dist_params = {"n": 100, "p": 0.33} + expected_rv_op_params = {"n": 100, "p": 0.33} + tests_to_run = ["check_pymc_params_match_rv_op"] + + +class TestNegativeBinomialVDistribution(BaseTestDistribution): + pymc_dist = pm.NegativeBinomial + pymc_dist_params = {"n": 100, "p": 0.33} + expected_rv_op_params = {"n": 100, "p": 0.33} + tests_to_run = ["check_pymc_params_match_rv_op"] + + +class TestNegativeBinomialMuSigmaDistribution(BaseTestDistribution): + pymc_dist = pm.NegativeBinomial + pymc_dist_params = {"mu": 5.0, "alpha": 8.0} + expected_n, expected_p = pm.NegativeBinomial.get_n_p( + mu=pymc_dist_params["mu"], + alpha=pymc_dist_params["alpha"], + n=None, + p=None, + ) + expected_rv_op_params = {"n": expected_n, "p": expected_p} + tests_to_run = ["check_pymc_params_match_rv_op"] + + +class TestBernoulliDistribution(BaseTestDistribution): + pymc_dist = pm.Bernoulli + pymc_dist_params = {"p": 0.33} + expected_rv_op_params = {"p": 0.33} + tests_to_run = ["check_pymc_params_match_rv_op"] + + +@pytest.mark.skip("Still not implemented") +class TestBernoulliLogitPDistribution(BaseTestDistribution): + pymc_dist = pm.Bernoulli + pymc_dist_params = {"logit_p": 1.0} + expected_rv_op_params = {"mean": 0, "sigma": 10.0} + tests_to_run = ["check_pymc_params_match_rv_op"] + + +class TestPoissonDistribution(BaseTestDistribution): + pymc_dist = pm.Poisson + pymc_dist_params = {"mu": 4.0} + expected_rv_op_params = {"mu": 4.0} + tests_to_run = ["check_pymc_params_match_rv_op"] + + +class TestMVNormalDistributionDistribution(BaseTestDistribution): + pymc_dist = pm.MvNormal + pymc_dist_params = { + "mu": np.array([1.0, 2.0]), + "cov": np.array([[2.0, 0.0], [0.0, 3.5]]), + } + expected_rv_op_params = { + "mu": np.array([1.0, 2.0]), + "cov": np.array([[2.0, 0.0], [0.0, 3.5]]), + } + tests_to_run = ["check_pymc_params_match_rv_op"] + + +class TestMVNormalDistributionCholDistribution(BaseTestDistribution): + pymc_dist = pm.MvNormal + pymc_dist_params = { + "mu": np.array([1.0, 2.0]), + "chol": np.array([[2.0, 0.0], [0.0, 3.5]]), + } + expected_rv_op_params = { + "mu": np.array([1.0, 2.0]), + "cov": quaddist_matrix(chol=pymc_dist_params["chol"]).eval(), + } + tests_to_run = ["check_pymc_params_match_rv_op"] + + +class TestMVNormalDistributionTauDistribution(BaseTestDistribution): + pymc_dist = pm.MvNormal + pymc_dist_params = { + "mu": np.array([1.0, 2.0]), + "tau": np.array([[2.0, 0.0], [0.0, 3.5]]), + } + expected_rv_op_params = { + "mu": np.array([1.0, 2.0]), + "cov": quaddist_matrix(tau=pymc_dist_params["tau"]).eval(), + } + tests_to_run = ["check_pymc_params_match_rv_op"] + + +class TestDirichletDistribution(BaseTestDistribution): + pymc_dist = pm.Dirichlet + pymc_dist_params = {"a": np.array([1.0, 2.0])} + expected_rv_op_params = {"a": np.array([1.0, 2.0])} + tests_to_run = ["check_pymc_params_match_rv_op"] + + +class TestMultinomialDistribution(BaseTestDistribution): + pymc_dist = pm.Multinomial + pymc_dist_params = {"n": 85, "p": np.array([0.28, 0.62, 0.10])} + expected_rv_op_params = {"n": 85, "p": np.array([0.28, 0.62, 0.10])} + tests_to_run = ["check_pymc_params_match_rv_op"] + + +class TestCategoricalDistribution(BaseTestDistribution): + pymc_dist = pm.Categorical + pymc_dist_params = {"p": np.array([0.28, 0.62, 0.10])} + expected_rv_op_params = {"p": np.array([0.28, 0.62, 0.10])} + tests_to_run = ["check_pymc_params_match_rv_op"] class TestScalarParameterSamples(SeededTest): @@ -999,13 +1133,6 @@ def test_dirichlet_multinomial_dist_ShapeError(self, n, a, shape, expectation): with expectation: m.random() - @pytest.mark.xfail(reason="This distribution has not been refactored for v4") - def test_gumbel(self): - def ref_rand(size, mu, beta): - return st.gumbel_r.rvs(loc=mu, scale=beta, size=size) - - pymc3_random(pm.Gumbel, {"mu": R, "beta": Rplus}, ref_rand=ref_rand) - @pytest.mark.xfail(reason="This distribution has not been refactored for v4") def test_logistic(self): def ref_rand(size, mu, s): @@ -1675,7 +1802,6 @@ def test_issue_3706(self): Sigma = np.eye(2) with pm.Model() as model: - X = pm.MvNormal("X", mu=np.zeros(2), cov=Sigma, shape=(N, 2)) betas = pm.Normal("betas", 0, 1, shape=2) y = pm.Deterministic("y", pm.math.dot(X, betas)) From a817a7ebafb079ea3bbf6235b025695270ce57c0 Mon Sep 17 00:00:00 2001 From: drabbit17 Date: Sat, 17 Apr 2021 12:07:04 +0100 Subject: [PATCH 13/19] Add size tests to new rv testing framework --- pymc3/tests/helpers.py | 4 +- pymc3/tests/test_distributions_random.py | 54 ++++++++++++++++++++---- 2 files changed, 47 insertions(+), 11 deletions(-) diff --git a/pymc3/tests/helpers.py b/pymc3/tests/helpers.py index 2cd817b59..ee730f8aa 100644 --- a/pymc3/tests/helpers.py +++ b/pymc3/tests/helpers.py @@ -41,8 +41,8 @@ def setup_method(self): def teardown_method(self): set_at_rng(self.old_at_rng) - def get_random_state(self): - if self.random_state is None: + def get_random_state(self, reset=False): + if self.random_state is None or reset: self.random_state = nr.RandomState(self.random_seed) return self.random_state diff --git a/pymc3/tests/test_distributions_random.py b/pymc3/tests/test_distributions_random.py index fefc6673f..ac76c4829 100644 --- a/pymc3/tests/test_distributions_random.py +++ b/pymc3/tests/test_distributions_random.py @@ -431,13 +431,7 @@ class BaseTestDistribution(SeededTest): decimal = 6 def test_distribution(self) -> None: - with pm.Model(): - self.pymc_dist_output = self.pymc_dist( - **self.pymc_dist_params, - size=self.size, - rng=aesara.shared(self.get_random_state()), - name=f"{self.pymc_dist.rv_op.name}_test", - ) + self._instantiate_pymc_distribution() if self.expected_dist is not None: self.expected_dist_outcome = self.expected_dist()( **self.expected_dist_params, size=self.size @@ -449,11 +443,23 @@ def run_test(self, test_name): { "check_pymc_dist_matches_expected": self._check_pymc_draws_match_expected, "check_pymc_params_match_rv_op": self._check_pymc_params_match_rv_op, + "check_distribution_size": self._check_distribution_size, }[test_name]() + def _instantiate_pymc_distribution(self): + with pm.Model(): + self.pymc_dist_output = self.pymc_dist( + **self.pymc_dist_params, + size=self.size, + rng=aesara.shared(self.get_random_state(reset=True)), + name=f"{self.pymc_dist.rv_op.name}_test", + ) + def _check_pymc_draws_match_expected( self, ): + # need to re-instantiate it to make sure that the order of drawings match the reference distribution one + self._instantiate_pymc_distribution() assert_array_almost_equal( self.pymc_dist_output.eval(), self.expected_dist_outcome, decimal=self.decimal ) @@ -469,6 +475,28 @@ def _check_pymc_params_match_rv_op(self) -> None: ): assert_almost_equal(expected_value, actual_variable.eval(), decimal=self.decimal) + def _check_distribution_size(self): + sizes_to_check, sizes_expected = [None, (), 1, (1,), 5, (4, 5), (2, 4, 2)], [ + (), + (), + (1,), + (1,), + (5,), + (4, 5), + (2, 4, 2), + ] + for size, expected in zip(sizes_to_check, sizes_expected): + pymc_dist_output_resized = change_rv_size(self.pymc_dist_output, size) + actual = pymc_dist_output_resized.eval().shape + print(actual, expected) + assert actual == expected + + # test negative sizes raise + with pytest.raises(ValueError): + change_rv_size(self.pymc_dist_output, -2).eval() + with pytest.raises(ValueError): + change_rv_size(self.pymc_dist_output, (3, -2)).eval() + def seeded_scipy_distribution_builder(dist_name: str) -> Callable: return lambda self: functools.partial( @@ -489,7 +517,11 @@ class TestGumbelDistribution(BaseTestDistribution): expected_dist_params = {"loc": 1.5, "scale": 3.0} size = 15 expected_dist = seeded_scipy_distribution_builder("gumbel_r") - tests_to_run = ["check_pymc_params_match_rv_op", "check_pymc_dist_matches_expected"] + tests_to_run = [ + "check_pymc_params_match_rv_op", + "check_distribution_size", + "check_pymc_dist_matches_expected", + ] class TestNormalDistribution(BaseTestDistribution): @@ -499,7 +531,11 @@ class TestNormalDistribution(BaseTestDistribution): expected_dist_params = {"loc": 5.0, "scale": 10.0} size = 15 expected_dist = seeded_numpy_distribution_builder("normal") - tests_to_run = ["check_pymc_params_match_rv_op", "check_pymc_dist_matches_expected"] + tests_to_run = [ + "check_pymc_params_match_rv_op", + "check_distribution_size", + "check_pymc_dist_matches_expected", + ] class TestNormalTauDistribution(BaseTestDistribution): From 1c88e5590cc2d27b11966f6088459dd437a9a240 Mon Sep 17 00:00:00 2001 From: drabbit17 Date: Sun, 18 Apr 2021 01:02:00 +0100 Subject: [PATCH 14/19] Add tests for multivariate and for univariate multi-parameters --- pymc3/tests/test_distributions_random.py | 70 ++++++++++++++++-------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/pymc3/tests/test_distributions_random.py b/pymc3/tests/test_distributions_random.py index ac76c4829..6d38fb9cc 100644 --- a/pymc3/tests/test_distributions_random.py +++ b/pymc3/tests/test_distributions_random.py @@ -16,7 +16,7 @@ import sys from contextlib import ExitStack as does_not_raise -from typing import Callable +from typing import Callable, List, Optional import aesara import numpy as np @@ -421,17 +421,21 @@ class TestMoyal(BaseTestCases.BaseTestCase): class BaseTestDistribution(SeededTest): - pymc_dist = None + pymc_dist: Optional[Callable] = None pymc_dist_params = dict() - expected_dist = None + expected_dist: Optional[Callable] = None expected_dist_params = dict() expected_rv_op_params = dict() tests_to_run = [] size = 15 decimal = 6 - def test_distribution(self) -> None: - self._instantiate_pymc_distribution() + sizes_to_check: Optional[List] = None + sizes_expected: Optional[List] = None + repeated_params_shape = 5 + + def test_distribution(self): + self._instantiate_pymc_rv() if self.expected_dist is not None: self.expected_dist_outcome = self.expected_dist()( **self.expected_dist_params, size=self.size @@ -446,20 +450,19 @@ def run_test(self, test_name): "check_distribution_size": self._check_distribution_size, }[test_name]() - def _instantiate_pymc_distribution(self): + def _instantiate_pymc_rv(self, dist_params=None): + params = dist_params if dist_params else self.pymc_dist_params with pm.Model(): self.pymc_dist_output = self.pymc_dist( - **self.pymc_dist_params, + **params, size=self.size, rng=aesara.shared(self.get_random_state(reset=True)), name=f"{self.pymc_dist.rv_op.name}_test", ) - def _check_pymc_draws_match_expected( - self, - ): + def _check_pymc_draws_match_expected(self): # need to re-instantiate it to make sure that the order of drawings match the reference distribution one - self._instantiate_pymc_distribution() + self._instantiate_pymc_rv() assert_array_almost_equal( self.pymc_dist_output.eval(), self.expected_dist_outcome, decimal=self.decimal ) @@ -476,7 +479,9 @@ def _check_pymc_params_match_rv_op(self) -> None: assert_almost_equal(expected_value, actual_variable.eval(), decimal=self.decimal) def _check_distribution_size(self): - sizes_to_check, sizes_expected = [None, (), 1, (1,), 5, (4, 5), (2, 4, 2)], [ + # test sizes + sizes_to_check = self.sizes_to_check or [None, (), 1, (1,), 5, (4, 5), (2, 4, 2)] + sizes_expected = self.sizes_expected or [ (), (), (1,), @@ -486,16 +491,29 @@ def _check_distribution_size(self): (2, 4, 2), ] for size, expected in zip(sizes_to_check, sizes_expected): - pymc_dist_output_resized = change_rv_size(self.pymc_dist_output, size) - actual = pymc_dist_output_resized.eval().shape - print(actual, expected) + actual = change_rv_size(self.pymc_dist_output, size).eval().shape assert actual == expected # test negative sizes raise - with pytest.raises(ValueError): - change_rv_size(self.pymc_dist_output, -2).eval() - with pytest.raises(ValueError): - change_rv_size(self.pymc_dist_output, (3, -2)).eval() + for size in [-2, (3, -2)]: + with pytest.raises(ValueError): + change_rv_size(self.pymc_dist_output, size).eval() + + # test multi-parameters sampling for univariate distributions + if self.pymc_dist.rv_op.ndim_supp == 0: + params = { + k: p * np.ones(self.repeated_params_shape) for k, p in self.pymc_dist_params.items() + } + self._instantiate_pymc_rv(params) + sizes_to_check = [None, self.repeated_params_shape, (5, self.repeated_params_shape)] + sizes_expected = [ + (self.repeated_params_shape,), + (self.repeated_params_shape,), + (5, self.repeated_params_shape), + ] + for size, expected in zip(sizes_to_check, sizes_expected): + actual = change_rv_size(self.pymc_dist_output, size).eval().shape + assert actual == expected def seeded_scipy_distribution_builder(dist_name: str) -> Callable: @@ -706,7 +724,7 @@ class TestPoissonDistribution(BaseTestDistribution): tests_to_run = ["check_pymc_params_match_rv_op"] -class TestMVNormalDistributionDistribution(BaseTestDistribution): +class TestMvNormalDistributionDistribution(BaseTestDistribution): pymc_dist = pm.MvNormal pymc_dist_params = { "mu": np.array([1.0, 2.0]), @@ -716,10 +734,12 @@ class TestMVNormalDistributionDistribution(BaseTestDistribution): "mu": np.array([1.0, 2.0]), "cov": np.array([[2.0, 0.0], [0.0, 3.5]]), } - tests_to_run = ["check_pymc_params_match_rv_op"] + sizes_to_check = [None, (1), (2, 3)] + sizes_expected = [(2,), (1, 2), (2, 3, 2)] + tests_to_run = ["check_pymc_params_match_rv_op", "check_distribution_size"] -class TestMVNormalDistributionCholDistribution(BaseTestDistribution): +class TestMvNormalDistributionCholDistribution(BaseTestDistribution): pymc_dist = pm.MvNormal pymc_dist_params = { "mu": np.array([1.0, 2.0]), @@ -732,7 +752,7 @@ class TestMVNormalDistributionCholDistribution(BaseTestDistribution): tests_to_run = ["check_pymc_params_match_rv_op"] -class TestMVNormalDistributionTauDistribution(BaseTestDistribution): +class TestMvNormalDistributionTauDistribution(BaseTestDistribution): pymc_dist = pm.MvNormal pymc_dist_params = { "mu": np.array([1.0, 2.0]), @@ -756,7 +776,9 @@ class TestMultinomialDistribution(BaseTestDistribution): pymc_dist = pm.Multinomial pymc_dist_params = {"n": 85, "p": np.array([0.28, 0.62, 0.10])} expected_rv_op_params = {"n": 85, "p": np.array([0.28, 0.62, 0.10])} - tests_to_run = ["check_pymc_params_match_rv_op"] + sizes_to_check = [None, (1), (4,), (3, 2)] + sizes_expected = [(3,), (1, 3), (4, 3), (3, 2, 3)] + tests_to_run = ["check_pymc_params_match_rv_op", "check_distribution_size"] class TestCategoricalDistribution(BaseTestDistribution): From bf68a3abcabfb67ce8fdeeca72a489c479ac8a61 Mon Sep 17 00:00:00 2001 From: drabbit17 Date: Sun, 18 Apr 2021 01:03:13 +0100 Subject: [PATCH 15/19] remove test already covered in aesara --- pymc3/tests/test_distributions_random.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pymc3/tests/test_distributions_random.py b/pymc3/tests/test_distributions_random.py index 6d38fb9cc..b232c5f88 100644 --- a/pymc3/tests/test_distributions_random.py +++ b/pymc3/tests/test_distributions_random.py @@ -361,12 +361,6 @@ class TestBetaBinomial(BaseTestCases.BaseTestCase): params = {"n": 5, "alpha": 1.0, "beta": 1.0} -@pytest.mark.skip(reason="This test is covered by Aesara") -class TestBernoulli(BaseTestCases.BaseTestCase): - distribution = pm.Bernoulli - params = {"p": 0.5} - - class TestDiscreteWeibull(BaseTestCases.BaseTestCase): distribution = pm.DiscreteWeibull params = {"q": 0.25, "beta": 2.0} From 55b4a0f6ebe80b8ec16528de877a7c044ef9bc5f Mon Sep 17 00:00:00 2001 From: drabbit17 Date: Sun, 18 Apr 2021 01:06:41 +0100 Subject: [PATCH 16/19] fix few names --- pymc3/tests/test_distributions_random.py | 72 ++++++++++-------------- 1 file changed, 29 insertions(+), 43 deletions(-) diff --git a/pymc3/tests/test_distributions_random.py b/pymc3/tests/test_distributions_random.py index b232c5f88..1aa8c2719 100644 --- a/pymc3/tests/test_distributions_random.py +++ b/pymc3/tests/test_distributions_random.py @@ -337,12 +337,6 @@ class TestVonMises(BaseTestCases.BaseTestCase): params = {"mu": 0.0, "kappa": 1.0} -@pytest.mark.xfail(reason="This distribution has not been refactored for v4") -class TestGumbel(BaseTestCases.BaseTestCase): - distribution = pm.Gumbel - params = {"mu": 0.0, "beta": 1.0} - - @pytest.mark.xfail(reason="This distribution has not been refactored for v4") class TestLogistic(BaseTestCases.BaseTestCase): distribution = pm.Logistic @@ -417,8 +411,8 @@ class TestMoyal(BaseTestCases.BaseTestCase): class BaseTestDistribution(SeededTest): pymc_dist: Optional[Callable] = None pymc_dist_params = dict() - expected_dist: Optional[Callable] = None - expected_dist_params = dict() + reference_dist: Optional[Callable] = None + reference_dist_params = dict() expected_rv_op_params = dict() tests_to_run = [] size = 15 @@ -430,40 +424,40 @@ class BaseTestDistribution(SeededTest): def test_distribution(self): self._instantiate_pymc_rv() - if self.expected_dist is not None: - self.expected_dist_outcome = self.expected_dist()( - **self.expected_dist_params, size=self.size + if self.reference_dist is not None: + self.reference_dist_draws = self.reference_dist()( + **self.reference_dist_params, size=self.size ) for test_name in self.tests_to_run: self.run_test(test_name) def run_test(self, test_name): { - "check_pymc_dist_matches_expected": self._check_pymc_draws_match_expected, + "check_pymc_dist_matches_reference": self._check_pymc_draws_match_reference, "check_pymc_params_match_rv_op": self._check_pymc_params_match_rv_op, - "check_distribution_size": self._check_distribution_size, + "check_rv_size": self._check_rv_size, }[test_name]() def _instantiate_pymc_rv(self, dist_params=None): params = dist_params if dist_params else self.pymc_dist_params with pm.Model(): - self.pymc_dist_output = self.pymc_dist( + self.pymc_rv = self.pymc_dist( **params, size=self.size, rng=aesara.shared(self.get_random_state(reset=True)), name=f"{self.pymc_dist.rv_op.name}_test", ) - def _check_pymc_draws_match_expected(self): + def _check_pymc_draws_match_reference(self): # need to re-instantiate it to make sure that the order of drawings match the reference distribution one self._instantiate_pymc_rv() assert_array_almost_equal( - self.pymc_dist_output.eval(), self.expected_dist_outcome, decimal=self.decimal + self.pymc_rv.eval(), self.reference_dist_draws, decimal=self.decimal ) def _check_pymc_params_match_rv_op(self) -> None: try: - aesera_dist_inputs = self.pymc_dist_output.get_parents()[0].inputs[3:] + aesera_dist_inputs = self.pymc_rv.get_parents()[0].inputs[3:] except: raise Exception("Parent Apply node missing from output") assert len(self.expected_rv_op_params) == len(aesera_dist_inputs) @@ -472,26 +466,18 @@ def _check_pymc_params_match_rv_op(self) -> None: ): assert_almost_equal(expected_value, actual_variable.eval(), decimal=self.decimal) - def _check_distribution_size(self): + def _check_rv_size(self): # test sizes sizes_to_check = self.sizes_to_check or [None, (), 1, (1,), 5, (4, 5), (2, 4, 2)] - sizes_expected = self.sizes_expected or [ - (), - (), - (1,), - (1,), - (5,), - (4, 5), - (2, 4, 2), - ] + sizes_expected = self.sizes_expected or [(), (), (1,), (1,), (5,), (4, 5), (2, 4, 2)] for size, expected in zip(sizes_to_check, sizes_expected): - actual = change_rv_size(self.pymc_dist_output, size).eval().shape + actual = change_rv_size(self.pymc_rv, size).eval().shape assert actual == expected # test negative sizes raise for size in [-2, (3, -2)]: with pytest.raises(ValueError): - change_rv_size(self.pymc_dist_output, size).eval() + change_rv_size(self.pymc_rv, size).eval() # test multi-parameters sampling for univariate distributions if self.pymc_dist.rv_op.ndim_supp == 0: @@ -506,7 +492,7 @@ def _check_distribution_size(self): (5, self.repeated_params_shape), ] for size, expected in zip(sizes_to_check, sizes_expected): - actual = change_rv_size(self.pymc_dist_output, size).eval().shape + actual = change_rv_size(self.pymc_rv, size).eval().shape assert actual == expected @@ -526,13 +512,13 @@ class TestGumbelDistribution(BaseTestDistribution): pymc_dist = pm.Gumbel pymc_dist_params = {"mu": 1.5, "beta": 3.0} expected_rv_op_params = {"mu": 1.5, "beta": 3.0} - expected_dist_params = {"loc": 1.5, "scale": 3.0} + reference_dist_params = {"loc": 1.5, "scale": 3.0} size = 15 - expected_dist = seeded_scipy_distribution_builder("gumbel_r") + reference_dist = seeded_scipy_distribution_builder("gumbel_r") tests_to_run = [ "check_pymc_params_match_rv_op", - "check_distribution_size", - "check_pymc_dist_matches_expected", + "check_rv_size", + "check_pymc_dist_matches_reference", ] @@ -540,13 +526,13 @@ class TestNormalDistribution(BaseTestDistribution): pymc_dist = pm.Normal pymc_dist_params = {"mu": 5.0, "sigma": 10.0} expected_rv_op_params = {"mu": 5.0, "sigma": 10.0} - expected_dist_params = {"loc": 5.0, "scale": 10.0} + reference_dist_params = {"loc": 5.0, "scale": 10.0} size = 15 - expected_dist = seeded_numpy_distribution_builder("normal") + reference_dist = seeded_numpy_distribution_builder("normal") tests_to_run = [ "check_pymc_params_match_rv_op", - "check_distribution_size", - "check_pymc_dist_matches_expected", + "check_rv_size", + "check_pymc_dist_matches_reference", ] @@ -718,7 +704,7 @@ class TestPoissonDistribution(BaseTestDistribution): tests_to_run = ["check_pymc_params_match_rv_op"] -class TestMvNormalDistributionDistribution(BaseTestDistribution): +class TestMvNormalDistribution(BaseTestDistribution): pymc_dist = pm.MvNormal pymc_dist_params = { "mu": np.array([1.0, 2.0]), @@ -730,10 +716,10 @@ class TestMvNormalDistributionDistribution(BaseTestDistribution): } sizes_to_check = [None, (1), (2, 3)] sizes_expected = [(2,), (1, 2), (2, 3, 2)] - tests_to_run = ["check_pymc_params_match_rv_op", "check_distribution_size"] + tests_to_run = ["check_pymc_params_match_rv_op", "check_rv_size"] -class TestMvNormalDistributionCholDistribution(BaseTestDistribution): +class TestMvNormalDistributionChol(BaseTestDistribution): pymc_dist = pm.MvNormal pymc_dist_params = { "mu": np.array([1.0, 2.0]), @@ -746,7 +732,7 @@ class TestMvNormalDistributionCholDistribution(BaseTestDistribution): tests_to_run = ["check_pymc_params_match_rv_op"] -class TestMvNormalDistributionTauDistribution(BaseTestDistribution): +class TestMvNormalDistributionTau(BaseTestDistribution): pymc_dist = pm.MvNormal pymc_dist_params = { "mu": np.array([1.0, 2.0]), @@ -772,7 +758,7 @@ class TestMultinomialDistribution(BaseTestDistribution): expected_rv_op_params = {"n": 85, "p": np.array([0.28, 0.62, 0.10])} sizes_to_check = [None, (1), (4,), (3, 2)] sizes_expected = [(3,), (1, 3), (4, 3), (3, 2, 3)] - tests_to_run = ["check_pymc_params_match_rv_op", "check_distribution_size"] + tests_to_run = ["check_pymc_params_match_rv_op", "check_rv_size"] class TestCategoricalDistribution(BaseTestDistribution): From 706308ee722966eaa86fa29650eb3d50c2c1efd6 Mon Sep 17 00:00:00 2001 From: Ricardo Date: Thu, 22 Apr 2021 10:02:47 +0200 Subject: [PATCH 17/19] Remove "distribution" from test class names --- pymc3/tests/test_distributions_random.py | 58 ++++++++++++------------ 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/pymc3/tests/test_distributions_random.py b/pymc3/tests/test_distributions_random.py index 1aa8c2719..ab0bc12d0 100644 --- a/pymc3/tests/test_distributions_random.py +++ b/pymc3/tests/test_distributions_random.py @@ -508,7 +508,7 @@ def seeded_numpy_distribution_builder(dist_name: str) -> Callable: ) -class TestGumbelDistribution(BaseTestDistribution): +class TestGumbel(BaseTestDistribution): pymc_dist = pm.Gumbel pymc_dist_params = {"mu": 1.5, "beta": 3.0} expected_rv_op_params = {"mu": 1.5, "beta": 3.0} @@ -522,7 +522,7 @@ class TestGumbelDistribution(BaseTestDistribution): ] -class TestNormalDistribution(BaseTestDistribution): +class TestNormal(BaseTestDistribution): pymc_dist = pm.Normal pymc_dist_params = {"mu": 5.0, "sigma": 10.0} expected_rv_op_params = {"mu": 5.0, "sigma": 10.0} @@ -536,7 +536,7 @@ class TestNormalDistribution(BaseTestDistribution): ] -class TestNormalTauDistribution(BaseTestDistribution): +class TestNormalTau(BaseTestDistribution): pymc_dist = pm.Normal tau, sigma = get_tau_sigma(tau=25.0) pymc_dist_params = {"mu": 1.0, "sigma": sigma} @@ -544,28 +544,28 @@ class TestNormalTauDistribution(BaseTestDistribution): tests_to_run = ["check_pymc_params_match_rv_op"] -class TestNormalSdDistribution(BaseTestDistribution): +class TestNormalSd(BaseTestDistribution): pymc_dist = pm.Normal pymc_dist_params = {"mu": 1.0, "sd": 5.0} expected_rv_op_params = {"mu": 1.0, "sigma": 5.0} tests_to_run = ["check_pymc_params_match_rv_op"] -class TestUniformDistribution(BaseTestDistribution): +class TestUniform(BaseTestDistribution): pymc_dist = pm.Uniform pymc_dist_params = {"lower": 0.5, "upper": 1.5} expected_rv_op_params = {"lower": 0.5, "upper": 1.5} tests_to_run = ["check_pymc_params_match_rv_op"] -class TestHalfNormalDistribution(BaseTestDistribution): +class TestHalfNormal(BaseTestDistribution): pymc_dist = pm.HalfNormal pymc_dist_params = {"sigma": 10.0} expected_rv_op_params = {"mean": 0, "sigma": 10.0} tests_to_run = ["check_pymc_params_match_rv_op"] -class TestHalfNormalTauDistribution(BaseTestDistribution): +class TestHalfNormalTau(BaseTestDistribution): pymc_dist = pm.Normal tau, sigma = get_tau_sigma(tau=25.0) pymc_dist_params = {"sigma": sigma} @@ -573,21 +573,21 @@ class TestHalfNormalTauDistribution(BaseTestDistribution): tests_to_run = ["check_pymc_params_match_rv_op"] -class TestHalfNormalSdDistribution(BaseTestDistribution): +class TestHalfNormalSd(BaseTestDistribution): pymc_dist = pm.Normal pymc_dist_params = {"sd": 5.0} expected_rv_op_params = {"mu": 0.0, "sigma": 5.0} tests_to_run = ["check_pymc_params_match_rv_op"] -class TestBetaAlphaBetaDistribution(BaseTestDistribution): +class TestBeta(BaseTestDistribution): pymc_dist = pm.Beta pymc_dist_params = {"alpha": 2.0, "beta": 5.0} expected_rv_op_params = {"alpha": 2.0, "beta": 5.0} tests_to_run = ["check_pymc_params_match_rv_op"] -class TestBetaMuSigmaDistribution(BaseTestDistribution): +class TestBetaMuSigma(BaseTestDistribution): pymc_dist = pm.Beta pymc_dist_params = {"mu": 0.5, "sigma": 0.25} expected_alpha, expected_beta = pm.Beta.get_alpha_beta( @@ -597,35 +597,35 @@ class TestBetaMuSigmaDistribution(BaseTestDistribution): tests_to_run = ["check_pymc_params_match_rv_op"] -class TestExponentialDistribution(BaseTestDistribution): +class TestExponential(BaseTestDistribution): pymc_dist = pm.Exponential pymc_dist_params = {"lam": 10.0} expected_rv_op_params = {"lam": 1.0 / pymc_dist_params["lam"]} tests_to_run = ["check_pymc_params_match_rv_op"] -class TestCauchyDistribution(BaseTestDistribution): +class TestCauchy(BaseTestDistribution): pymc_dist = pm.Cauchy pymc_dist_params = {"alpha": 2.0, "beta": 5.0} expected_rv_op_params = {"alpha": 2.0, "beta": 5.0} tests_to_run = ["check_pymc_params_match_rv_op"] -class TestHalfCauchyDistribution(BaseTestDistribution): +class TestHalfCauchyn(BaseTestDistribution): pymc_dist = pm.HalfCauchy pymc_dist_params = {"beta": 5.0} expected_rv_op_params = {"alpha": 0.0, "beta": 5.0} tests_to_run = ["check_pymc_params_match_rv_op"] -class TestGammaAlphaBetaDistribution(BaseTestDistribution): +class TestGamma(BaseTestDistribution): pymc_dist = pm.Gamma pymc_dist_params = {"alpha": 2.0, "beta": 5.0} expected_rv_op_params = {"alpha": 2.0, "beta": 1 / 5.0} tests_to_run = ["check_pymc_params_match_rv_op"] -class TestGammaMuSigmaDistribution(BaseTestDistribution): +class TestGammaMuSigma(BaseTestDistribution): pymc_dist = pm.Gamma pymc_dist_params = {"mu": 0.5, "sigma": 0.25} expected_alpha, expected_beta = pm.Gamma.get_alpha_beta( @@ -635,14 +635,14 @@ class TestGammaMuSigmaDistribution(BaseTestDistribution): tests_to_run = ["check_pymc_params_match_rv_op"] -class TestInverseGammaAlphaBetaDistribution(BaseTestDistribution): +class TestInverseGamma(BaseTestDistribution): pymc_dist = pm.InverseGamma pymc_dist_params = {"alpha": 2.0, "beta": 5.0} expected_rv_op_params = {"alpha": 2.0, "beta": 5.0} tests_to_run = ["check_pymc_params_match_rv_op"] -class TestInverseGammaMuSigmaDistribution(BaseTestDistribution): +class TestInverseGammaMuSigma(BaseTestDistribution): pymc_dist = pm.InverseGamma pymc_dist_params = {"mu": 0.5, "sigma": 0.25} expected_alpha, expected_beta = pm.InverseGamma._get_alpha_beta( @@ -655,21 +655,21 @@ class TestInverseGammaMuSigmaDistribution(BaseTestDistribution): tests_to_run = ["check_pymc_params_match_rv_op"] -class TestBinomialDistribution(BaseTestDistribution): +class TestBinomial(BaseTestDistribution): pymc_dist = pm.Binomial pymc_dist_params = {"n": 100, "p": 0.33} expected_rv_op_params = {"n": 100, "p": 0.33} tests_to_run = ["check_pymc_params_match_rv_op"] -class TestNegativeBinomialVDistribution(BaseTestDistribution): +class TestNegativeBinomial(BaseTestDistribution): pymc_dist = pm.NegativeBinomial pymc_dist_params = {"n": 100, "p": 0.33} expected_rv_op_params = {"n": 100, "p": 0.33} tests_to_run = ["check_pymc_params_match_rv_op"] -class TestNegativeBinomialMuSigmaDistribution(BaseTestDistribution): +class TestNegativeBinomialMuSigma(BaseTestDistribution): pymc_dist = pm.NegativeBinomial pymc_dist_params = {"mu": 5.0, "alpha": 8.0} expected_n, expected_p = pm.NegativeBinomial.get_n_p( @@ -682,7 +682,7 @@ class TestNegativeBinomialMuSigmaDistribution(BaseTestDistribution): tests_to_run = ["check_pymc_params_match_rv_op"] -class TestBernoulliDistribution(BaseTestDistribution): +class TestBernoulli(BaseTestDistribution): pymc_dist = pm.Bernoulli pymc_dist_params = {"p": 0.33} expected_rv_op_params = {"p": 0.33} @@ -690,21 +690,21 @@ class TestBernoulliDistribution(BaseTestDistribution): @pytest.mark.skip("Still not implemented") -class TestBernoulliLogitPDistribution(BaseTestDistribution): +class TestBernoulliLogitP(BaseTestDistribution): pymc_dist = pm.Bernoulli pymc_dist_params = {"logit_p": 1.0} expected_rv_op_params = {"mean": 0, "sigma": 10.0} tests_to_run = ["check_pymc_params_match_rv_op"] -class TestPoissonDistribution(BaseTestDistribution): +class TestPoisson(BaseTestDistribution): pymc_dist = pm.Poisson pymc_dist_params = {"mu": 4.0} expected_rv_op_params = {"mu": 4.0} tests_to_run = ["check_pymc_params_match_rv_op"] -class TestMvNormalDistribution(BaseTestDistribution): +class TestMvNormal(BaseTestDistribution): pymc_dist = pm.MvNormal pymc_dist_params = { "mu": np.array([1.0, 2.0]), @@ -719,7 +719,7 @@ class TestMvNormalDistribution(BaseTestDistribution): tests_to_run = ["check_pymc_params_match_rv_op", "check_rv_size"] -class TestMvNormalDistributionChol(BaseTestDistribution): +class TestMvNormalChol(BaseTestDistribution): pymc_dist = pm.MvNormal pymc_dist_params = { "mu": np.array([1.0, 2.0]), @@ -732,7 +732,7 @@ class TestMvNormalDistributionChol(BaseTestDistribution): tests_to_run = ["check_pymc_params_match_rv_op"] -class TestMvNormalDistributionTau(BaseTestDistribution): +class TestMvNormalTau(BaseTestDistribution): pymc_dist = pm.MvNormal pymc_dist_params = { "mu": np.array([1.0, 2.0]), @@ -745,14 +745,14 @@ class TestMvNormalDistributionTau(BaseTestDistribution): tests_to_run = ["check_pymc_params_match_rv_op"] -class TestDirichletDistribution(BaseTestDistribution): +class TestDirichlet(BaseTestDistribution): pymc_dist = pm.Dirichlet pymc_dist_params = {"a": np.array([1.0, 2.0])} expected_rv_op_params = {"a": np.array([1.0, 2.0])} tests_to_run = ["check_pymc_params_match_rv_op"] -class TestMultinomialDistribution(BaseTestDistribution): +class TestMultinomial(BaseTestDistribution): pymc_dist = pm.Multinomial pymc_dist_params = {"n": 85, "p": np.array([0.28, 0.62, 0.10])} expected_rv_op_params = {"n": 85, "p": np.array([0.28, 0.62, 0.10])} @@ -761,7 +761,7 @@ class TestMultinomialDistribution(BaseTestDistribution): tests_to_run = ["check_pymc_params_match_rv_op", "check_rv_size"] -class TestCategoricalDistribution(BaseTestDistribution): +class TestCategorical(BaseTestDistribution): pymc_dist = pm.Categorical pymc_dist_params = {"p": np.array([0.28, 0.62, 0.10])} expected_rv_op_params = {"p": np.array([0.28, 0.62, 0.10])} From 3d2808769eb531686e5f12bddfc536beaac15edb Mon Sep 17 00:00:00 2001 From: drabbit17 Date: Fri, 23 Apr 2021 00:56:14 +0100 Subject: [PATCH 18/19] Add discrete Weibull, improve Beta and some minor refactoring --- pymc3/tests/test_distributions_random.py | 68 ++++++++++++++---------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/pymc3/tests/test_distributions_random.py b/pymc3/tests/test_distributions_random.py index ab0bc12d0..cfe0538a8 100644 --- a/pymc3/tests/test_distributions_random.py +++ b/pymc3/tests/test_distributions_random.py @@ -32,6 +32,7 @@ from pymc3.aesaraf import change_rv_size, floatX, intX from pymc3.distributions.continuous import get_tau_sigma +from pymc3.distributions.dist_math import clipped_beta_rvs from pymc3.distributions.multivariate import quaddist_matrix from pymc3.distributions.shape_utils import to_tuple from pymc3.exceptions import ShapeError @@ -278,11 +279,6 @@ class TestWald(BaseTestCases.BaseTestCase): params = {"mu": 1.0, "lam": 1.0, "alpha": 0.0} -class TestBeta(BaseTestCases.BaseTestCase): - distribution = pm.Beta - params = {"alpha": 1.0, "beta": 1.0} - - @pytest.mark.xfail(reason="This distribution has not been refactored for v4") class TestKumaraswamy(BaseTestCases.BaseTestCase): distribution = pm.Kumaraswamy @@ -355,11 +351,6 @@ class TestBetaBinomial(BaseTestCases.BaseTestCase): params = {"n": 5, "alpha": 1.0, "beta": 1.0} -class TestDiscreteWeibull(BaseTestCases.BaseTestCase): - distribution = pm.DiscreteWeibull - params = {"q": 0.25, "beta": 2.0} - - @pytest.mark.xfail(reason="This distribution has not been refactored for v4") class TestConstant(BaseTestCases.BaseTestCase): distribution = pm.Constant @@ -426,17 +417,10 @@ def test_distribution(self): self._instantiate_pymc_rv() if self.reference_dist is not None: self.reference_dist_draws = self.reference_dist()( - **self.reference_dist_params, size=self.size + size=self.size, **self.reference_dist_params ) - for test_name in self.tests_to_run: - self.run_test(test_name) - - def run_test(self, test_name): - { - "check_pymc_dist_matches_reference": self._check_pymc_draws_match_reference, - "check_pymc_params_match_rv_op": self._check_pymc_params_match_rv_op, - "check_rv_size": self._check_rv_size, - }[test_name]() + for check_name in self.tests_to_run: + getattr(self, check_name)() def _instantiate_pymc_rv(self, dist_params=None): params = dist_params if dist_params else self.pymc_dist_params @@ -448,25 +432,22 @@ def _instantiate_pymc_rv(self, dist_params=None): name=f"{self.pymc_dist.rv_op.name}_test", ) - def _check_pymc_draws_match_reference(self): + def check_pymc_draws_match_reference(self): # need to re-instantiate it to make sure that the order of drawings match the reference distribution one self._instantiate_pymc_rv() assert_array_almost_equal( self.pymc_rv.eval(), self.reference_dist_draws, decimal=self.decimal ) - def _check_pymc_params_match_rv_op(self) -> None: - try: - aesera_dist_inputs = self.pymc_rv.get_parents()[0].inputs[3:] - except: - raise Exception("Parent Apply node missing from output") + def check_pymc_params_match_rv_op(self) -> None: + aesera_dist_inputs = self.pymc_rv.get_parents()[0].inputs[3:] assert len(self.expected_rv_op_params) == len(aesera_dist_inputs) for (expected_name, expected_value), actual_variable in zip( self.expected_rv_op_params.items(), aesera_dist_inputs ): assert_almost_equal(expected_value, actual_variable.eval(), decimal=self.decimal) - def _check_rv_size(self): + def check_rv_size(self): # test sizes sizes_to_check = self.sizes_to_check or [None, (), 1, (1,), 5, (4, 5), (2, 4, 2)] sizes_expected = self.sizes_expected or [(), (), (1,), (1,), (5,), (4, 5), (2, 4, 2)] @@ -508,6 +489,28 @@ def seeded_numpy_distribution_builder(dist_name: str) -> Callable: ) +class TestDiscreteWeibull(BaseTestDistribution): + def discrete_weibul_rng_fn(self): + p = seeded_numpy_distribution_builder("uniform") + return ( + lambda size, q, beta: np.ceil( + np.power(np.log(1 - p(self)(size=size)) / np.log(q), 1.0 / beta) + ) + - 1 + ) + + pymc_dist = pm.DiscreteWeibull + pymc_dist_params = {"q": 0.25, "beta": 2.0} + expected_rv_op_params = {"q": 0.25, "beta": 2.0} + reference_dist_params = {"q": 0.25, "beta": 2.0} + reference_dist = discrete_weibul_rng_fn + tests_to_run = [ + "check_pymc_params_match_rv_op", + "check_rv_size", + "check_pymc_dist_matches_reference", + ] + + class TestGumbel(BaseTestDistribution): pymc_dist = pm.Gumbel pymc_dist_params = {"mu": 1.5, "beta": 3.0} @@ -584,7 +587,16 @@ class TestBeta(BaseTestDistribution): pymc_dist = pm.Beta pymc_dist_params = {"alpha": 2.0, "beta": 5.0} expected_rv_op_params = {"alpha": 2.0, "beta": 5.0} - tests_to_run = ["check_pymc_params_match_rv_op"] + reference_dist_params = {"a": 2.0, "b": 5.0} + size = 15 + reference_dist = lambda self: functools.partial( + clipped_beta_rvs, random_state=self.get_random_state() + ) + tests_to_run = [ + "check_pymc_params_match_rv_op", + "check_rv_size", + "check_pymc_params_match_rv_op", + ] class TestBetaMuSigma(BaseTestDistribution): From 9b52e1b530051495357fb940824a68096d9efac6 Mon Sep 17 00:00:00 2001 From: drabbit17 Date: Fri, 23 Apr 2021 09:02:34 +0100 Subject: [PATCH 19/19] Fix typos in checks naming and add sanity check --- pymc3/tests/test_distributions_random.py | 32 ++++++++++++++---------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/pymc3/tests/test_distributions_random.py b/pymc3/tests/test_distributions_random.py index cfe0538a8..03190f3bc 100644 --- a/pymc3/tests/test_distributions_random.py +++ b/pymc3/tests/test_distributions_random.py @@ -414,6 +414,7 @@ class BaseTestDistribution(SeededTest): repeated_params_shape = 5 def test_distribution(self): + self.validate_tests_list() self._instantiate_pymc_rv() if self.reference_dist is not None: self.reference_dist_draws = self.reference_dist()( @@ -439,7 +440,7 @@ def check_pymc_draws_match_reference(self): self.pymc_rv.eval(), self.reference_dist_draws, decimal=self.decimal ) - def check_pymc_params_match_rv_op(self) -> None: + def check_pymc_params_match_rv_op(self): aesera_dist_inputs = self.pymc_rv.get_parents()[0].inputs[3:] assert len(self.expected_rv_op_params) == len(aesera_dist_inputs) for (expected_name, expected_value), actual_variable in zip( @@ -476,6 +477,11 @@ def check_rv_size(self): actual = change_rv_size(self.pymc_rv, size).eval().shape assert actual == expected + def validate_tests_list(self): + assert len(self.tests_to_run) == len( + set(self.tests_to_run) + ), "There are duplicates in the list of tests_to_run" + def seeded_scipy_distribution_builder(dist_name: str) -> Callable: return lambda self: functools.partial( @@ -490,24 +496,24 @@ def seeded_numpy_distribution_builder(dist_name: str) -> Callable: class TestDiscreteWeibull(BaseTestDistribution): - def discrete_weibul_rng_fn(self): - p = seeded_numpy_distribution_builder("uniform") - return ( - lambda size, q, beta: np.ceil( - np.power(np.log(1 - p(self)(size=size)) / np.log(q), 1.0 / beta) - ) - - 1 + def discrete_weibul_rng_fn(self, size, q, beta, uniform_rng_fct): + return np.ceil(np.power(np.log(1 - uniform_rng_fct(size=size)) / np.log(q), 1.0 / beta)) - 1 + + def seeded_discrete_weibul_rng_fn(self): + uniform_rng_fct = functools.partial( + getattr(np.random.RandomState, "uniform"), self.get_random_state() ) + return functools.partial(self.discrete_weibul_rng_fn, uniform_rng_fct=uniform_rng_fct) pymc_dist = pm.DiscreteWeibull pymc_dist_params = {"q": 0.25, "beta": 2.0} expected_rv_op_params = {"q": 0.25, "beta": 2.0} reference_dist_params = {"q": 0.25, "beta": 2.0} - reference_dist = discrete_weibul_rng_fn + reference_dist = seeded_discrete_weibul_rng_fn tests_to_run = [ "check_pymc_params_match_rv_op", "check_rv_size", - "check_pymc_dist_matches_reference", + "check_pymc_draws_match_reference", ] @@ -521,7 +527,7 @@ class TestGumbel(BaseTestDistribution): tests_to_run = [ "check_pymc_params_match_rv_op", "check_rv_size", - "check_pymc_dist_matches_reference", + "check_pymc_draws_match_reference", ] @@ -535,7 +541,7 @@ class TestNormal(BaseTestDistribution): tests_to_run = [ "check_pymc_params_match_rv_op", "check_rv_size", - "check_pymc_dist_matches_reference", + "check_pymc_draws_match_reference", ] @@ -595,7 +601,7 @@ class TestBeta(BaseTestDistribution): tests_to_run = [ "check_pymc_params_match_rv_op", "check_rv_size", - "check_pymc_params_match_rv_op", + "check_pymc_draws_match_reference", ]