Skip to content

Commit 1e78da4

Browse files
author
Junpeng Lao
authored
Merge pull request #2277 from aseyboldt/fix-bound
Allow arrays in pm.Bound
2 parents 3039b3a + 248aba4 commit 1e78da4

File tree

7 files changed

+320
-149
lines changed

7 files changed

+320
-149
lines changed

pymc3/distributions/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@
4242
from .discrete import Geometric
4343
from .discrete import Categorical
4444

45-
from .distribution import Bound
4645
from .distribution import DensityDist
4746
from .distribution import Distribution
4847
from .distribution import Continuous
@@ -76,6 +75,8 @@
7675
from .transforms import log
7776
from .transforms import sum_to_1
7877

78+
from .bound import Bound
79+
7980
__all__ = ['Uniform',
8081
'Flat',
8182
'Normal',
@@ -136,5 +137,6 @@
136137
'Triangular',
137138
'DiscreteWeibull',
138139
'Gumbel',
139-
'Interpolated'
140+
'Interpolated',
141+
'Bound',
140142
]

pymc3/distributions/bound.py

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
from numbers import Real
2+
3+
import numpy as np
4+
import theano.tensor as tt
5+
import theano
6+
7+
from pymc3.distributions.distribution import (
8+
Distribution, Discrete, Continuous, draw_values, generate_samples)
9+
from pymc3.distributions import transforms
10+
from pymc3.distributions.dist_math import bound
11+
12+
__all__ = ['Bound']
13+
14+
15+
class _Bounded(Distribution):
16+
def __init__(self, distribution, lower, upper, default, *args, **kwargs):
17+
self.lower = lower
18+
self.upper = upper
19+
self._wrapped = distribution.dist(*args, **kwargs)
20+
21+
if default is None:
22+
defaults = self._wrapped.defaults
23+
for name in defaults:
24+
setattr(self, name, getattr(self._wrapped, name))
25+
else:
26+
defaults = ('_default',)
27+
self._default = default
28+
29+
super(_Bounded, self).__init__(
30+
shape=self._wrapped.shape,
31+
dtype=self._wrapped.dtype,
32+
testval=self._wrapped.testval,
33+
defaults=defaults,
34+
transform=self._wrapped.transform)
35+
36+
def logp(self, value):
37+
logp = self._wrapped.logp(value)
38+
bounds = []
39+
if self.lower is not None:
40+
bounds.append(value >= self.lower)
41+
if self.upper is not None:
42+
bounds.append(value <= self.upper)
43+
if len(bounds) > 0:
44+
return bound(logp, *bounds)
45+
else:
46+
return logp
47+
48+
def _random(self, lower, upper, point=None, size=None):
49+
lower = np.asarray(lower)
50+
upper = np.asarray(upper)
51+
if lower.size > 1 or upper.size > 1:
52+
raise ValueError('Drawing samples from distributions with '
53+
'array-valued bounds is not supported.')
54+
samples = np.zeros(size, dtype=self.dtype).flatten()
55+
i, n = 0, len(samples)
56+
while i < len(samples):
57+
sample = self._wrapped.random(point=point, size=n)
58+
select = sample[np.logical_and(sample >= lower, sample <= upper)]
59+
samples[i:(i + len(select))] = select[:]
60+
i += len(select)
61+
n -= len(select)
62+
if size is not None:
63+
return np.reshape(samples, size)
64+
else:
65+
return samples
66+
67+
def random(self, point=None, size=None, repeat=None):
68+
if self.lower is None and self.upper is None:
69+
return self._wrapped.random(point=point, size=size)
70+
elif self.lower is not None and self.upper is not None:
71+
lower, upper = draw_values([self.lower, self.upper], point=point)
72+
return generate_samples(self._random, lower, upper, point,
73+
dist_shape=self.shape,
74+
size=size)
75+
elif self.lower is not None:
76+
lower = draw_values([self.lower], point=point)
77+
return generate_samples(self._random, lower, np.inf, point,
78+
dist_shape=self.shape,
79+
size=size)
80+
else:
81+
upper = draw_values([self.upper], point=point)
82+
return generate_samples(self._random, -np.inf, upper, point,
83+
dist_shape=self.shape,
84+
size=size)
85+
86+
87+
class _DiscreteBounded(_Bounded, Discrete):
88+
def __init__(self, distribution, lower, upper,
89+
transform='infer', *args, **kwargs):
90+
if transform == 'infer':
91+
transform = None
92+
if transform is not None:
93+
raise ValueError('Can not transform discrete variable.')
94+
95+
if lower is None and upper is None:
96+
default = None
97+
elif lower is not None and upper is not None:
98+
default = (lower + upper) // 2
99+
if upper is not None:
100+
default = upper - 1
101+
if lower is not None:
102+
default = lower + 1
103+
104+
super(_DiscreteBounded, self).__init__(
105+
distribution=distribution, lower=lower, upper=upper,
106+
default=default, *args, **kwargs)
107+
108+
109+
class _ContinuousBounded(_Bounded, Continuous):
110+
R"""
111+
An upper, lower or upper+lower bounded distribution
112+
113+
Parameters
114+
----------
115+
distribution : pymc3 distribution
116+
Distribution to be transformed into a bounded distribution
117+
lower : float (optional)
118+
Lower bound of the distribution, set to -inf to disable.
119+
upper : float (optional)
120+
Upper bound of the distribibution, set to inf to disable.
121+
tranform : 'infer' or object
122+
If 'infer', infers the right transform to apply from the supplied bounds.
123+
If transform object, has to supply .forward() and .backward() methods.
124+
See pymc3.distributions.transforms for more information.
125+
"""
126+
127+
def __init__(self, distribution, lower, upper,
128+
transform='infer', *args, **kwargs):
129+
dtype = kwargs.get('dtype', theano.config.floatX)
130+
131+
if lower is not None:
132+
lower = tt.as_tensor_variable(lower).astype(dtype)
133+
if upper is not None:
134+
upper = tt.as_tensor_variable(upper).astype(dtype)
135+
136+
if transform == 'infer':
137+
if lower is None and upper is None:
138+
transform = None
139+
default = None
140+
elif lower is not None and upper is not None:
141+
transform = transforms.interval(lower, upper)
142+
default = 0.5 * (lower + upper)
143+
elif upper is not None:
144+
transform = transforms.upperbound(upper)
145+
default = upper - 1
146+
else:
147+
transform = transforms.lowerbound(lower)
148+
default = lower + 1
149+
else:
150+
default = None
151+
152+
super(_ContinuousBounded, self).__init__(
153+
distribution=distribution, lower=lower, upper=upper,
154+
transform=transform, default=default, *args, **kwargs)
155+
156+
157+
class Bound(object):
158+
R"""
159+
Create a new upper, lower or upper+lower bounded distribution.
160+
161+
The resulting distribution is not normalized anymore. This
162+
is usually fine if the bounds are constants. If you need
163+
truncated distributions, use `Bound` in combination with
164+
a `pm.Potential` with the cumulative probability function.
165+
166+
The bounds are inclusive for discrete distributions.
167+
168+
Parameters
169+
----------
170+
distribution : pymc3 distribution
171+
Distribution to be transformed into a bounded distribution.
172+
lower : float or array like, optional
173+
Lower bound of the distribution.
174+
upper : float or array like, optional
175+
Upper bound of the distribution.
176+
177+
Example
178+
-------
179+
# Bounded distribution can be defined before the model context
180+
PositiveNormal = pm.Bound(pm.Normal, lower=0.0)
181+
with pm.Model():
182+
par1 = PositiveNormal('par1', mu=0.0, sd=1.0, testval=1.0)
183+
# or within the model context
184+
NegativeNormal = pm.Bound(pm.Normal, upper=0.0)
185+
par2 = NegativeNormal('par2', mu=0.0, sd=1.0, testval=1.0)
186+
187+
# or you can define it implicitly within the model context
188+
par3 = pm.Bound(pm.Normal, lower=-1.0, upper=1.0)(
189+
'par3', mu=0.0, sd=1.0, testval=1.0)
190+
"""
191+
192+
def __init__(self, distribution, lower=None, upper=None):
193+
if isinstance(lower, Real) and lower == -np.inf:
194+
lower = None
195+
if isinstance(upper, Real) and upper == np.inf:
196+
upper = None
197+
198+
if not issubclass(distribution, Distribution):
199+
raise ValueError('"distribution" must be a Distribution subclass.')
200+
201+
self.distribution = distribution
202+
self.lower = lower
203+
self.upper = upper
204+
205+
def __call__(self, *args, **kwargs):
206+
if 'observed' in kwargs:
207+
raise ValueError('Observed Bound distributions are not supported. '
208+
'If you want to model truncated data '
209+
'you can use a pm.Potential in combination '
210+
'with the cumulative probability function. See '
211+
'pymc3/examples/censored_data.py for an example.')
212+
first, args = args[0], args[1:]
213+
214+
if issubclass(self.distribution, Continuous):
215+
return _ContinuousBounded(first, self.distribution,
216+
self.lower, self.upper, *args, **kwargs)
217+
elif issubclass(self.distribution, Discrete):
218+
return _DiscreteBounded(first, self.distribution,
219+
self.lower, self.upper, *args, **kwargs)
220+
else:
221+
raise ValueError('Distribution is neither continuous nor discrete.')
222+
223+
def dist(self, *args, **kwargs):
224+
if issubclass(self.distribution, Continuous):
225+
return _ContinuousBounded.dist(
226+
self.distribution, self.lower, self.upper, *args, **kwargs)
227+
228+
elif issubclass(self.distribution, Discrete):
229+
return _DiscreteBounded.dist(
230+
self.distribution, self.lower, self.upper, *args, **kwargs)
231+
else:
232+
raise ValueError('Distribution is neither continuous nor discrete.')

pymc3/distributions/continuous.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
bound, logpow, gammaln, betaln, std_cdf, i0,
2222
i1, alltrue_elemwise, SplineWrapper
2323
)
24-
from .distribution import Continuous, draw_values, generate_samples, Bound
24+
from .distribution import Continuous, draw_values, generate_samples
25+
from .bound import Bound
2526

2627
__all__ = ['Uniform', 'Flat', 'Normal', 'Beta', 'Exponential', 'Laplace',
2728
'StudentT', 'Cauchy', 'HalfCauchy', 'Gamma', 'Weibull',

0 commit comments

Comments
 (0)