Skip to content

Commit f366b08

Browse files
JasperMartinsmj-will
authored andcommitted
Added WeightedCategorical-Prior (bilby-dev#893)
* Added WeightedCategorical-Prior * Implemented suggestions * Added WeightedDiscreteValues-priors to the general prior tests * Fixed init-from-repr, docstring and cdf-function, simplified code * Docstring fix
1 parent 54f7c9e commit f366b08

3 files changed

Lines changed: 302 additions & 68 deletions

File tree

bilby/core/prior/analytical.py

Lines changed: 157 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1455,33 +1455,73 @@ def cdf(self, val):
14551455
return np.clip(result, 0, 1)
14561456

14571457

1458-
class DiscreteValues(Prior):
1459-
def __init__(self, values, name=None, latex_label=None,
1460-
unit=None, boundary="periodic"):
1461-
""" An equal-weighted discrete-valued prior
1458+
class WeightedDiscreteValues(Prior):
1459+
def __init__(
1460+
self,
1461+
values,
1462+
weights=None,
1463+
name=None,
1464+
latex_label=None,
1465+
unit=None,
1466+
boundary="periodic",
1467+
):
1468+
"""A weighted discrete-valued prior
14621469
14631470
Parameters
14641471
==========
1465-
values: array
1472+
values: 1d array_like of numeric type
14661473
The discrete values of the prior.
1474+
weights: 1d array_like of numeric type or None
1475+
The weights of each category. If None, then all values are
1476+
equally weighted. Default: None.
14671477
name: str
1468-
See superclass
1478+
The name of the parameter
14691479
latex_label: str
1470-
See superclass
1480+
The latex label of the parameter. Used for plotting.
14711481
unit: str
1472-
See superclass
1482+
The unit of the parameter. Used for plotting.
14731483
"""
1484+
14741485
nvalues = len(values)
1486+
values = np.array(values)
1487+
if values.shape != (nvalues,):
1488+
raise ValueError(
1489+
f"Shape of argument 'values' must be 1d array-like but has shape {values.shape}"
1490+
)
14751491
minimum = np.min(values)
14761492
# Small delta added to help with MCMC walking
14771493
maximum = np.max(values) * (1 + 1e-15)
1478-
super(DiscreteValues, self).__init__(
1494+
super(WeightedDiscreteValues, self).__init__(
14791495
name=name, latex_label=latex_label, minimum=minimum,
14801496
maximum=maximum, unit=unit, boundary=boundary)
14811497
self.nvalues = nvalues
1482-
self.values = np.sort(np.array(values))
1483-
self.p = 1 / self.nvalues
1484-
self.lnp = -np.log(self.nvalues)
1498+
sorter = np.argsort(values)
1499+
self._values_array = values[sorter]
1500+
1501+
# inititialization of priors from repr only supports
1502+
# python buildins
1503+
self.values = self._values_array.tolist()
1504+
1505+
weights = (
1506+
np.array(weights) / np.sum(weights)
1507+
if weights is not None
1508+
else np.ones(self.nvalues) / self.nvalues
1509+
)
1510+
# check for consistent shape of input
1511+
if weights.shape != (self.nvalues,):
1512+
raise ValueError(
1513+
"Inconsistent shape of weights and number of values:"
1514+
+ f"weights has shape {weights.shape} "
1515+
+ f"while number of values is {self.values}"
1516+
)
1517+
self._weights_array = weights[sorter]
1518+
self.weights = self._weights_array.tolist()
1519+
self._lnweights_array = np.log(self._weights_array)
1520+
1521+
# save cdf for rescaling
1522+
_cumulative_weights_array = np.cumsum(self._weights_array)
1523+
# insert 0 for values smaller than minimum
1524+
self._cumulative_weights_array = np.insert(_cumulative_weights_array, 0, 0)
14851525

14861526
def rescale(self, val):
14871527
"""
@@ -1498,8 +1538,22 @@ def rescale(self, val):
14981538
=======
14991539
Union[float, array_like]: Rescaled probability
15001540
"""
1501-
idx = np.asarray(np.floor(val * self.nvalues), dtype=int)
1502-
return self.values[idx]
1541+
index = np.searchsorted(self._cumulative_weights_array[1:], val)
1542+
return self._values_array[index]
1543+
1544+
def cdf(self, val):
1545+
"""Return the cumulative prior probability of val.
1546+
1547+
Parameters
1548+
==========
1549+
val: Union[float, int, array_like]
1550+
1551+
Returns
1552+
=======
1553+
float: cumulative prior probability of val
1554+
"""
1555+
index = np.searchsorted(self._values_array, val, side="right")
1556+
return self._cumulative_weights_array[index]
15031557

15041558
def prob(self, val):
15051559
"""Return the prior probability of val.
@@ -1512,17 +1566,11 @@ def prob(self, val):
15121566
=======
15131567
float: Prior probability of val
15141568
"""
1515-
if isinstance(val, (float, int)):
1516-
if val in self.values:
1517-
return self.p
1518-
else:
1519-
return 0
1520-
else:
1521-
val = np.atleast_1d(val)
1522-
probs = np.zeros_like(val, dtype=np.float64)
1523-
idxs = np.isin(val, self.values)
1524-
probs[idxs] = self.p
1525-
return probs
1569+
index = np.searchsorted(self._values_array, val)
1570+
index = np.clip(index, 0, self.nvalues - 1)
1571+
p = np.where(self._values_array[index] == val, self._weights_array[index], 0)
1572+
# turn 0d numpy array to scalar
1573+
return p[()]
15261574

15271575
def ln_prob(self, val):
15281576
"""Return the logarithmic prior probability of val
@@ -1536,24 +1584,86 @@ def ln_prob(self, val):
15361584
float:
15371585
15381586
"""
1539-
if isinstance(val, (float, int)):
1540-
if val in self.values:
1541-
return self.lnp
1542-
else:
1543-
return -np.inf
1544-
else:
1545-
val = np.atleast_1d(val)
1546-
probs = -np.inf * np.ones_like(val, dtype=np.float64)
1547-
idxs = np.isin(val, self.values)
1548-
probs[idxs] = self.lnp
1549-
return probs
1587+
index = np.searchsorted(self._values_array, val)
1588+
index = np.clip(index, 0, self.nvalues - 1)
1589+
lnp = np.where(
1590+
self._values_array[index] == val, self._lnweights_array[index], -np.inf
1591+
)
1592+
# turn 0d numpy array to scalar
1593+
return lnp[()]
15501594

15511595

1552-
class Categorical(DiscreteValues):
1553-
def __init__(self, ncategories, name=None, latex_label=None,
1596+
class DiscreteValues(WeightedDiscreteValues):
1597+
def __init__(self, values, name=None, latex_label=None,
15541598
unit=None, boundary="periodic"):
1555-
""" An equal-weighted Categorical prior
1599+
"""An equal-weighted discrete-valued prior
15561600
1601+
Parameters
1602+
==========
1603+
values: array
1604+
The discrete values of the prior.
1605+
name: str
1606+
See superclass
1607+
latex_label: str
1608+
See superclass
1609+
unit: str
1610+
See superclass
1611+
"""
1612+
weights = np.ones_like(values)
1613+
super(DiscreteValues, self).__init__(
1614+
values=values,
1615+
weights=weights,
1616+
name=name,
1617+
latex_label=latex_label,
1618+
unit=unit,
1619+
boundary=boundary,
1620+
)
1621+
1622+
1623+
class WeightedCategorical(WeightedDiscreteValues):
1624+
def __init__(
1625+
self,
1626+
ncategories,
1627+
weights=None,
1628+
name=None,
1629+
latex_label=None,
1630+
unit=None,
1631+
boundary="periodic",
1632+
):
1633+
"""A weighted Categorical prior
1634+
1635+
Parameters
1636+
==========
1637+
ncategories: int
1638+
The number of available categories. The prior mass support is then
1639+
integers [0, ncategories - 1].
1640+
weights: 1d array_like or None
1641+
The weights of each category. If None, then all categories are
1642+
equally weighted. Default None.
1643+
name: str
1644+
The name of the parameter
1645+
latex_label: str
1646+
The latex label of the parameter. Used for plotting.
1647+
unit: str
1648+
The unit of the parameter. Used for plotting.
1649+
"""
1650+
self.ncategories = ncategories
1651+
values = np.arange(0, ncategories)
1652+
super(WeightedCategorical, self).__init__(
1653+
values=values,
1654+
weights=weights,
1655+
name=name,
1656+
latex_label=latex_label,
1657+
unit=unit,
1658+
boundary=boundary,
1659+
)
1660+
1661+
1662+
class Categorical(DiscreteValues):
1663+
def __init__(
1664+
self, ncategories, name=None, latex_label=None, unit=None, boundary="periodic"
1665+
):
1666+
"""An equal-weighted Categorical prior
15571667
Parameters
15581668
==========
15591669
ncategories: int
@@ -1566,9 +1676,15 @@ def __init__(self, ncategories, name=None, latex_label=None,
15661676
unit: str
15671677
See superclass
15681678
"""
1679+
self.ncategories = ncategories
15691680
values = np.arange(0, ncategories)
1570-
DiscreteValues.__init__(self, values=values, name=name, latex_label=latex_label,
1571-
unit=unit, boundary=boundary)
1681+
super(Categorical, self).__init__(
1682+
values=values,
1683+
name=name,
1684+
latex_label=latex_label,
1685+
unit=unit,
1686+
boundary=boundary,
1687+
)
15721688

15731689

15741690
class Triangular(Prior):

test/core/prior/analytical_test.py

Lines changed: 87 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -119,12 +119,7 @@ def test_single_probability(self):
119119
def test_array_probability(self):
120120
N = 3
121121
categorical_prior = bilby.core.prior.Categorical(N)
122-
self.assertTrue(
123-
np.all(
124-
categorical_prior.prob([0, 1, 1, 2, 3])
125-
== np.array([1 / N, 1 / N, 1 / N, 1 / N, 0])
126-
)
127-
)
122+
self.assertTrue(np.all(categorical_prior.prob([0, 1, 1, 2, 3]) == np.array([1 / N, 1 / N, 1 / N, 1 / N, 0])))
128123

129124
def test_single_lnprobability(self):
130125
N = 3
@@ -137,12 +132,92 @@ def test_single_lnprobability(self):
137132
def test_array_lnprobability(self):
138133
N = 3
139134
categorical_prior = bilby.core.prior.Categorical(N)
140-
self.assertTrue(
141-
np.all(
142-
categorical_prior.ln_prob([0, 1, 1, 2, 3])
143-
== np.array([-np.log(N), -np.log(N), -np.log(N), -np.log(N), -np.inf])
144-
)
145-
)
135+
self.assertTrue(np.all(categorical_prior.ln_prob([0, 1, 1, 2, 3]) == np.array(
136+
[-np.log(N), -np.log(N), -np.log(N), -np.log(N), -np.inf])))
137+
138+
139+
class TestWeightedCategoricalPrior(unittest.TestCase):
140+
def test_single_sample(self):
141+
categorical_prior = bilby.core.prior.WeightedCategorical(3, [1, 2, 3])
142+
in_prior = True
143+
for _ in range(1000):
144+
s = categorical_prior.sample()
145+
if s not in [0, 1, 2]:
146+
in_prior = False
147+
self.assertTrue(in_prior)
148+
149+
def test_fail_init(self):
150+
with self.assertRaises(ValueError):
151+
bilby.core.prior.WeightedCategorical(3, [[1, 2], [2, 3], [3, 4]])
152+
with self.assertRaises(ValueError):
153+
bilby.core.prior.WeightedCategorical(3, [1, 2, 3, 4])
154+
155+
def test_array_sample(self):
156+
ncat = 4
157+
weights = np.arange(1, ncat + 1)
158+
categorical_prior = bilby.core.prior.WeightedCategorical(ncat, weights=weights)
159+
N = 100000
160+
s = categorical_prior.sample(N)
161+
cases = 0
162+
for i in categorical_prior.values:
163+
case = np.sum(s == i)
164+
cases += case
165+
self.assertAlmostEqual(case / N, categorical_prior.prob(i), places=int(np.log10(np.sqrt(N))))
166+
self.assertAlmostEqual(case / N, weights[i] / np.sum(weights), places=int(np.log10(np.sqrt(N))))
167+
self.assertEqual(cases, N)
168+
169+
def test_single_probability(self):
170+
N = 3
171+
weights = np.arange(1, N + 1)
172+
categorical_prior = bilby.core.prior.WeightedCategorical(N, weights=weights)
173+
for i in categorical_prior.values:
174+
self.assertEqual(categorical_prior.prob(i), weights[i] / np.sum(weights))
175+
self.assertEqual(categorical_prior.prob(0.5), 0)
176+
177+
def test_array_probability(self):
178+
N = 3
179+
test_cases = [0, 1, 1, 2, 3]
180+
weights = np.arange(1, N + 1)
181+
categorical_prior = bilby.core.prior.WeightedCategorical(N, weights=weights)
182+
probs = np.arange(1, N + 2) / np.sum(weights)
183+
probs[-1] = 0
184+
self.assertTrue(np.all(categorical_prior.prob(test_cases) == probs[test_cases]))
185+
186+
def test_single_lnprobability(self):
187+
N = 3
188+
weights = np.arange(1, N + 1)
189+
categorical_prior = bilby.core.prior.WeightedCategorical(N, weights=weights)
190+
for i in categorical_prior.values:
191+
self.assertEqual(categorical_prior.ln_prob(i), np.log(weights[i] / np.sum(weights)))
192+
self.assertEqual(categorical_prior.prob(0.5), 0)
193+
194+
def test_array_lnprobability(self):
195+
N = 3
196+
test_cases = [0, 1, 1, 2, 3]
197+
weights = np.arange(1, N + 1)
198+
199+
categorical_prior = bilby.core.prior.WeightedCategorical(N, weights=weights)
200+
ln_probs = np.log(np.arange(1, N + 2) / np.sum(weights))
201+
ln_probs[-1] = -np.inf
202+
203+
self.assertTrue(np.all(categorical_prior.ln_prob(test_cases) == ln_probs[test_cases]))
204+
205+
def test_cdf(self):
206+
"""
207+
Test that the CDF method is the inverse of the rescale method.
208+
209+
Note that the format of inputs/outputs is different between the two methods.
210+
"""
211+
N = 3
212+
weights = np.arange(1, N + 1)
213+
214+
categorical_prior = bilby.core.prior.WeightedCategorical(N, weights=weights)
215+
sample = categorical_prior.sample(size=10)
216+
original = np.asarray(sample)
217+
new = np.array(categorical_prior.rescale(
218+
categorical_prior.cdf(sample)
219+
))
220+
np.testing.assert_array_equal(original, new)
146221

147222

148223
if __name__ == "__main__":

0 commit comments

Comments
 (0)