diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 753f00b45a..1bb05cd86f 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -29,6 +29,7 @@ - Changed initialization of `Categorical.p`. `p` is now normalized to sum to `1` inside `logp` and `random`, but not during initialization. This could hide negative values supplied to `p` as mentioned in #2082. - `Categorical` now accepts elements of `p` equal to `0`. `logp` will return `-inf` if there are `values` that index to the zero probability categories. - Add `sigma`, `tau`, and `sd` to signature of `NormalMixture`. +- Resolved issue #3248. Set default lower and upper values of -inf and inf for pm.distributions.continuous.TruncatedNormal. This avoids errors caused by their previous values of None. ### Deprecations diff --git a/docs/release-notes/pymc3-3.0.md b/docs/release-notes/pymc3-3.0.md index e69de29bb2..8b13789179 100644 --- a/docs/release-notes/pymc3-3.0.md +++ b/docs/release-notes/pymc3-3.0.md @@ -0,0 +1 @@ + diff --git a/pymc3/distributions/continuous.py b/pymc3/distributions/continuous.py index 6b05961191..0cf4bb5026 100644 --- a/pymc3/distributions/continuous.py +++ b/pymc3/distributions/continuous.py @@ -571,15 +571,17 @@ def __init__(self, mu=0, sigma=None, tau=None, lower=None, upper=None, tau, sigma = get_tau_sigma(tau=tau, sigma=sigma) self.sigma = self.sd = tt.as_tensor_variable(sigma) self.tau = tt.as_tensor_variable(tau) - self.lower = tt.as_tensor_variable(floatX(lower)) if lower is not None else lower - self.upper = tt.as_tensor_variable(floatX(upper)) if upper is not None else upper + self.lower_check = tt.as_tensor_variable(floatX(lower)) if lower is not None else lower + self.upper_check = tt.as_tensor_variable(floatX(upper)) if upper is not None else upper + self.lower = tt.as_tensor_variable(floatX(lower)) if lower is not None else tt.as_tensor_variable(-np.inf) + self.upper = tt.as_tensor_variable(floatX(upper)) if upper is not None else tt.as_tensor_variable(np.inf) self.mu = tt.as_tensor_variable(floatX(mu)) - if self.lower is None and self.upper is None: + if self.lower_check is None and self.upper_check is None: self._defaultval = mu - elif self.lower is None and self.upper is not None: + elif self.lower_check is None and self.upper_check is not None: self._defaultval = self.upper - 1. - elif self.lower is not None and self.upper is None: + elif self.lower_check is not None and self.upper_check is None: self._defaultval = self.lower + 1. else: self._defaultval = (self.lower + self.upper) / 2 @@ -639,19 +641,19 @@ def logp(self, value): logp = Normal.dist(mu=mu, sigma=sigma).logp(value) - norm bounds = [sigma > 0] - if self.lower is not None: + if self.lower_check is not None: bounds.append(value >= self.lower) - if self.upper is not None: + if self.upper_check is not None: bounds.append(value <= self.upper) return bound(logp, *bounds) def _normalization(self): mu, sigma = self.mu, self.sigma - if self.lower is None and self.upper is None: + if self.lower_check is None and self.upper_check is None: return 0. - if self.lower is not None and self.upper is not None: + if self.lower_check is not None and self.upper_check is not None: lcdf_a = normal_lcdf(mu, sigma, self.lower) lcdf_b = normal_lcdf(mu, sigma, self.upper) lsf_a = normal_lccdf(mu, sigma, self.lower) @@ -663,7 +665,7 @@ def _normalization(self): logdiffexp(lcdf_b, lcdf_a), ) - if self.lower is not None: + if self.lower_check is not None: return normal_lccdf(mu, sigma, self.lower) else: return normal_lcdf(mu, sigma, self.upper) diff --git a/pymc3/tests/test_distributions_random.py b/pymc3/tests/test_distributions_random.py index 6fa49ca843..ab8988ef9f 100644 --- a/pymc3/tests/test_distributions_random.py +++ b/pymc3/tests/test_distributions_random.py @@ -257,7 +257,15 @@ class TestNormal(BaseTestCases.BaseTestCase): class TestTruncatedNormal(BaseTestCases.BaseTestCase): distribution = pm.TruncatedNormal - params = {'mu': 0., 'tau': 1., 'lower':-0.5, 'upper':0.5} + params = {'mu': 0., 'tau': 1., 'lower': -0.5, 'upper': 0.5} + +class TestTruncatedNormalLower(BaseTestCases.BaseTestCase): + distribution = pm.TruncatedNormal + params = {'mu': 0., 'tau': 1., 'lower': -0.5} + +class TestTruncatedNormalUpper(BaseTestCases.BaseTestCase): + distribution = pm.TruncatedNormal + params = {'mu': 0., 'tau': 1., 'upper': 0.5} class TestSkewNormal(BaseTestCases.BaseTestCase): distribution = pm.SkewNormal @@ -461,7 +469,6 @@ def ref_rand(size, tau): 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) def test_normal(self): @@ -471,8 +478,20 @@ def ref_rand(size, mu, sigma): def test_truncated_normal(self): def ref_rand(size, mu, sigma, lower, upper): - return st.truncnorm.rvs((lower-mu)/sigma, (upper-mu)/sigma, size=size, loc=mu, scale=sigma) - pymc3_random(pm.TruncatedNormal, {'mu': R, 'sigma': Rplusbig, 'lower':-Rplusbig, 'upper':Rplusbig}, + return st.truncnorm.rvs((lower - mu) / sigma, (upper - mu) / sigma, size=size, loc=mu, scale=sigma) + pymc3_random(pm.TruncatedNormal, {'mu': R, 'sigma': Rplusbig, 'lower': -Rplusbig, 'upper': Rplusbig}, + ref_rand=ref_rand) + + def test_truncated_normal_lower(self): + def ref_rand(size, mu, sigma, lower): + return st.truncnorm.rvs((lower - mu) / sigma, np.inf, size=size, loc=mu, scale=sigma) + pymc3_random(pm.TruncatedNormal, {'mu': R, 'sigma': Rplusbig, 'lower': -Rplusbig}, + ref_rand=ref_rand) + + def test_truncated_normal_upper(self): + def ref_rand(size, mu, sigma, upper): + return st.truncnorm.rvs(-np.inf, (upper - mu) / sigma, size=size, loc=mu, scale=sigma) + pymc3_random(pm.TruncatedNormal, {'mu': R, 'sigma': Rplusbig, 'upper': Rplusbig}, ref_rand=ref_rand) def test_skew_normal(self):