Skip to content

Commit 6d11e9a

Browse files
authored
DEV: make sure all priors return float when needed (#979)
* DEV: make sure all priors return float when needed * FEAT: add fermi-dirac CDF * TST: add testing of fermi dirac and symmetric log uniform priors * FMT: remove extraneous whitespace * BUG: revert bad changes to equal comparisons * BUG: Fix syntax error in return statement * BUG: Fix list return * BUG: fix array type output for rescale * BUG: Fix missing parentheses in squeeze method call * BUG: fix flattening logic * BUG: stop using arr[np.where(cond)] * Address comments * BUG: fix scipy integrate imports * BUG: Remove unused import of interp1d Removed unused import of interp1d from scipy.interpolate.
1 parent 400a173 commit 6d11e9a

8 files changed

Lines changed: 197 additions & 117 deletions

File tree

bilby/core/prior/analytical.py

Lines changed: 68 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,10 @@ def cdf(self, val):
169169
_cdf = (np.log(val / self.minimum) /
170170
np.log(self.maximum / self.minimum))
171171
else:
172-
_cdf = np.atleast_1d(val ** (self.alpha + 1) - self.minimum ** (self.alpha + 1)) / \
173-
(self.maximum ** (self.alpha + 1) - self.minimum ** (self.alpha + 1))
172+
_cdf = (
173+
(val ** (self.alpha + 1) - self.minimum ** (self.alpha + 1))
174+
/ (self.maximum ** (self.alpha + 1) - self.minimum ** (self.alpha + 1))
175+
)
174176
_cdf = np.minimum(_cdf, 1)
175177
_cdf = np.maximum(_cdf, 0)
176178
return _cdf
@@ -367,16 +369,16 @@ def ln_prob(self, val):
367369
return np.nan_to_num(- np.log(2 * np.abs(val)) - np.log(np.log(self.maximum / self.minimum)))
368370

369371
def cdf(self, val):
370-
val = np.atleast_1d(val)
371372
norm = 0.5 / np.log(self.maximum / self.minimum)
372-
cdf = np.zeros((len(val)))
373-
lower_indices = np.where(np.logical_and(-self.maximum <= val, val <= -self.minimum))[0]
374-
upper_indices = np.where(np.logical_and(self.minimum <= val, val <= self.maximum))[0]
375-
cdf[lower_indices] = -norm * np.log(-val[lower_indices] / self.maximum)
376-
cdf[np.where(np.logical_and(-self.minimum < val, val < self.minimum))] = 0.5
377-
cdf[upper_indices] = 0.5 + norm * np.log(val[upper_indices] / self.minimum)
378-
cdf[np.where(self.maximum < val)] = 1
379-
return cdf
373+
_cdf = (
374+
-norm * np.log(abs(val) / self.maximum)
375+
* (val <= -self.minimum) * (val >= -self.maximum)
376+
+ (0.5 + norm * np.log(abs(val) / self.minimum))
377+
* (val >= self.minimum) * (val <= self.maximum)
378+
+ 0.5 * (val > -self.minimum) * (val < self.minimum)
379+
+ 1 * (val > self.maximum)
380+
)
381+
return _cdf
380382

381383

382384
class Cosine(Prior):
@@ -426,10 +428,12 @@ def prob(self, val):
426428
return np.cos(val) / 2 * self.is_in_prior_range(val)
427429

428430
def cdf(self, val):
429-
_cdf = np.atleast_1d((np.sin(val) - np.sin(self.minimum)) /
430-
(np.sin(self.maximum) - np.sin(self.minimum)))
431-
_cdf[val > self.maximum] = 1
432-
_cdf[val < self.minimum] = 0
431+
_cdf = (
432+
(np.sin(val) - np.sin(self.minimum))
433+
/ (np.sin(self.maximum) - np.sin(self.minimum))
434+
* (val >= self.minimum) * (val <= self.maximum)
435+
+ 1 * (val > self.maximum)
436+
)
433437
return _cdf
434438

435439

@@ -480,10 +484,12 @@ def prob(self, val):
480484
return np.sin(val) / 2 * self.is_in_prior_range(val)
481485

482486
def cdf(self, val):
483-
_cdf = np.atleast_1d((np.cos(val) - np.cos(self.minimum)) /
484-
(np.cos(self.maximum) - np.cos(self.minimum)))
485-
_cdf[val > self.maximum] = 1
486-
_cdf[val < self.minimum] = 0
487+
_cdf = (
488+
(np.cos(val) - np.cos(self.minimum))
489+
/ (np.cos(self.maximum) - np.cos(self.minimum))
490+
* (val >= self.minimum) * (val <= self.maximum)
491+
+ 1 * (val > self.maximum)
492+
)
487493
return _cdf
488494

489495

@@ -625,11 +631,13 @@ def prob(self, val):
625631
/ self.sigma / self.normalisation * self.is_in_prior_range(val)
626632

627633
def cdf(self, val):
628-
val = np.atleast_1d(val)
629-
_cdf = (erf((val - self.mu) / 2 ** 0.5 / self.sigma) - erf(
630-
(self.minimum - self.mu) / 2 ** 0.5 / self.sigma)) / 2 / self.normalisation
631-
_cdf[val > self.maximum] = 1
632-
_cdf[val < self.minimum] = 0
634+
_cdf = (
635+
(
636+
erf((val - self.mu) / 2 ** 0.5 / self.sigma)
637+
- erf((self.minimum - self.mu) / 2 ** 0.5 / self.sigma)
638+
) / 2 / self.normalisation * (val >= self.minimum) * (val <= self.maximum)
639+
+ 1 * (val > self.maximum)
640+
)
633641
return _cdf
634642

635643

@@ -1367,6 +1375,8 @@ def __init__(self, sigma, mu=None, r=None, name=None, latex_label=None,
13671375
raise ValueError("For the Fermi-Dirac prior the values of sigma and r "
13681376
"must be positive.")
13691377

1378+
self.expr = np.exp(self.r)
1379+
13701380
def rescale(self, val):
13711381
"""
13721382
'Rescale' a sample from the unit line element to the appropriate Fermi-Dirac prior.
@@ -1384,21 +1394,8 @@ def rescale(self, val):
13841394
.. [1] M. Pitkin, M. Isi, J. Veitch & G. Woan, `arXiv:1705.08978v1
13851395
<https:arxiv.org/abs/1705.08978v1>`_, 2017.
13861396
"""
1387-
inv = (-np.exp(-1. * self.r) + (1. + np.exp(self.r)) ** -val +
1388-
np.exp(-1. * self.r) * (1. + np.exp(self.r)) ** -val)
1389-
1390-
# if val is 1 this will cause inv to be negative (due to numerical
1391-
# issues), so return np.inf
1392-
if isinstance(val, (float, int)):
1393-
if inv < 0:
1394-
return np.inf
1395-
else:
1396-
return -self.sigma * np.log(inv)
1397-
else:
1398-
idx = inv >= 0.
1399-
tmpinv = np.inf * np.ones(len(np.atleast_1d(val)))
1400-
tmpinv[idx] = -self.sigma * np.log(inv[idx])
1401-
return tmpinv
1397+
inv = -1 / self.expr + (1 + self.expr)**-val + (1 + self.expr)**-val / self.expr
1398+
return -self.sigma * np.log(np.maximum(inv, 0))
14021399

14031400
def prob(self, val):
14041401
"""Return the prior probability of val.
@@ -1411,7 +1408,11 @@ def prob(self, val):
14111408
=======
14121409
float: Prior probability of val
14131410
"""
1414-
return np.exp(self.ln_prob(val))
1411+
return (
1412+
(np.exp((val - self.mu) / self.sigma) + 1)**-1
1413+
/ (self.sigma * np.log1p(self.expr))
1414+
* (val >= self.minimum)
1415+
)
14151416

14161417
def ln_prob(self, val):
14171418
"""Return the log prior probability of val.
@@ -1424,19 +1425,34 @@ def ln_prob(self, val):
14241425
=======
14251426
Union[float, array_like]: Log prior probability of val
14261427
"""
1428+
return np.log(self.prob(val))
14271429

1428-
norm = -np.log(self.sigma * np.log(1. + np.exp(self.r)))
1429-
if isinstance(val, (float, int)):
1430-
if val < self.minimum:
1431-
return -np.inf
1432-
else:
1433-
return norm - np.logaddexp((val / self.sigma) - self.r, 0.)
1434-
else:
1435-
val = np.atleast_1d(val)
1436-
lnp = -np.inf * np.ones(len(val))
1437-
idx = val >= self.minimum
1438-
lnp[idx] = norm - np.logaddexp((val[idx] / self.sigma) - self.r, 0.)
1439-
return lnp
1430+
def cdf(self, val):
1431+
"""
1432+
Evaluate the CDF of the Fermi-Dirac distribution using a slightly
1433+
modified form of Equation 23 of [1]_.
1434+
1435+
Parameters
1436+
==========
1437+
val: Union[float, int, array_like]
1438+
The value(s) to evaluate the CDF at
1439+
1440+
Returns
1441+
=======
1442+
Union[float, array_like]:
1443+
The CDF value(s)
1444+
1445+
References
1446+
==========
1447+
1448+
.. [1] M. Pitkin, M. Isi, J. Veitch & G. Woan, `arXiv:1705.08978v1
1449+
<https:arxiv.org/abs/1705.08978v1>`_, 2017.
1450+
"""
1451+
result = (
1452+
(np.logaddexp(0, -self.r) - np.logaddexp(-val / self.sigma, -self.r))
1453+
/ np.logaddexp(0, self.r)
1454+
)
1455+
return np.clip(result, 0, 1)
14401456

14411457

14421458
class DiscreteValues(Prior):

bilby/core/prior/base.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55

66
import numpy as np
77
import scipy.stats
8-
from scipy.interpolate import interp1d
98

109
from ..utils import (
1110
infer_args_from_method,
1211
BilbyJsonEncoder,
1312
decode_bilby_json,
1413
logger,
1514
get_dict_with_properties,
15+
WrappedInterp1d as interp1d,
1616
)
1717

1818

@@ -178,7 +178,10 @@ def cdf(self, val):
178178
cdf = cumulative_trapezoid(pdf, x, initial=0)
179179
interp = interp1d(x, cdf, assume_sorted=True, bounds_error=False,
180180
fill_value=(0, 1))
181-
return interp(val)
181+
output = interp(val)
182+
if isinstance(val, (int, float)):
183+
output = float(output)
184+
return output
182185

183186
def ln_prob(self, val):
184187
"""Return the prior ln probability of val, this should be overwritten

bilby/core/prior/dict.py

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,9 @@ def check_efficiency(n_tested, n_valid):
504504
def normalize_constraint_factor(
505505
self, keys, min_accept=10000, sampling_chunk=50000, nrepeats=10
506506
):
507-
if keys in self._cached_normalizations.keys():
507+
if len(self.constraint_keys) == 0:
508+
return 1
509+
elif keys in self._cached_normalizations.keys():
508510
return self._cached_normalizations[keys]
509511
else:
510512
factor_estimates = [
@@ -566,8 +568,10 @@ def check_prob(self, sample, prob):
566568
return 0.0
567569
else:
568570
constrained_prob = np.zeros_like(prob)
569-
keep = np.array(self.evaluate_constraints(sample), dtype=bool)
570-
constrained_prob[keep] = prob[keep] * ratio
571+
in_bounds = np.isfinite(prob)
572+
subsample = {key: sample[key][in_bounds] for key in sample}
573+
keep = np.array(self.evaluate_constraints(subsample), dtype=bool)
574+
constrained_prob[in_bounds] = prob[in_bounds] * keep * ratio
571575
return constrained_prob
572576

573577
def ln_prob(self, sample, axis=None, normalized=True):
@@ -608,8 +612,10 @@ def check_ln_prob(self, sample, ln_prob, normalized=True):
608612
return -np.inf
609613
else:
610614
constrained_ln_prob = -np.inf * np.ones_like(ln_prob)
611-
keep = np.array(self.evaluate_constraints(sample), dtype=bool)
612-
constrained_ln_prob[keep] = ln_prob[keep] + np.log(ratio)
615+
in_bounds = np.isfinite(ln_prob)
616+
subsample = {key: sample[key][in_bounds] for key in sample}
617+
keep = np.log(np.array(self.evaluate_constraints(subsample), dtype=bool))
618+
constrained_ln_prob[in_bounds] = ln_prob[in_bounds] + keep + np.log(ratio)
613619
return constrained_ln_prob
614620

615621
def cdf(self, sample):
@@ -643,12 +649,9 @@ def rescale(self, keys, theta):
643649
=======
644650
list: List of floats containing the rescaled sample
645651
"""
646-
theta = list(theta)
647-
samples = []
648-
for key, units in zip(keys, theta):
649-
samps = self[key].rescale(units)
650-
samples += list(np.asarray(samps).flatten())
651-
return samples
652+
return list(
653+
[self[key].rescale(sample) for key, sample in zip(keys, theta)]
654+
)
652655

653656
def test_redundancy(self, key, disable_logging=False):
654657
"""Empty redundancy test, should be overwritten in subclasses"""
@@ -670,9 +673,7 @@ def test_has_redundant_keys(self):
670673
del temp[key]
671674
if temp.test_redundancy(key, disable_logging=True):
672675
logger.warning(
673-
"{} is a redundant key in this {}.".format(
674-
key, self.__class__.__name__
675-
)
676+
f"{key} is a redundant key in this {self.__class__.__name__}."
676677
)
677678
redundant = True
678679
return redundant
@@ -880,17 +881,43 @@ def rescale(self, keys, theta):
880881
self._check_resolved()
881882
self._update_rescale_keys(keys)
882883
result = dict()
884+
joint = dict()
883885
for key, index in zip(
884886
self.sorted_keys_without_fixed_parameters, self._rescale_indexes
885887
):
886888
result[key] = self[key].rescale(
887889
theta[index], **self.get_required_variables(key)
888890
)
889891
self[key].least_recently_sampled = result[key]
890-
samples = []
891-
for key in keys:
892-
samples += list(np.asarray(result[key]).flatten())
893-
return samples
892+
if isinstance(self[key], JointPrior) and self[key].dist.distname not in joint:
893+
joint[self[key].dist.distname] = [key]
894+
elif isinstance(self[key], JointPrior):
895+
joint[self[key].dist.distname].append(key)
896+
for names in joint.values():
897+
# this is needed to unpack how joint prior rescaling works
898+
# as an example of a joint prior over {a, b, c, d} we might
899+
# get the following based on the order within the joint prior
900+
# {a: [], b: [], c: [1, 2, 3, 4], d: []}
901+
# -> [1, 2, 3, 4]
902+
# -> {a: 1, b: 2, c: 3, d: 4}
903+
values = list()
904+
for key in names:
905+
values = np.concatenate([values, result[key]])
906+
for key, value in zip(names, values):
907+
result[key] = value
908+
909+
def safe_flatten(value):
910+
"""
911+
this is gross but can be removed whenever we switch to returning
912+
arrays, flatten converts 0-d arrays to 1-d so has to be special
913+
cased
914+
"""
915+
if isinstance(value, (float, int)):
916+
return value
917+
else:
918+
return result[key].flatten()
919+
920+
return [safe_flatten(result[key]) for key in keys]
894921

895922
def _update_rescale_keys(self, keys):
896923
if not keys == self._least_recently_rescaled_keys:

bilby/core/prior/interpolated.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import numpy as np
22
from scipy.integrate import trapezoid
3-
from scipy.interpolate import interp1d
43

54
from .base import Prior
6-
from ..utils import logger
5+
from ..utils import logger, WrappedInterp1d as interp1d
76

87

98
class Interped(Prior):
@@ -87,10 +86,7 @@ def rescale(self, val):
8786
8887
This maps to the inverse CDF. This is done using interpolation.
8988
"""
90-
rescaled = self.inverse_cumulative_distribution(val)
91-
if rescaled.shape == ():
92-
rescaled = float(rescaled)
93-
return rescaled
89+
return self.inverse_cumulative_distribution(val)
9490

9591
@property
9692
def minimum(self):

bilby/core/prior/slabspike.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,11 @@ def rescale(self, val):
8888
original_is_number = isinstance(val, Number)
8989
val = np.atleast_1d(val)
9090

91-
lower_indices = np.where(val < self.inverse_cdf_below_spike)[0]
92-
intermediate_indices = np.where(np.logical_and(
91+
lower_indices = val < self.inverse_cdf_below_spike
92+
intermediate_indices = np.logical_and(
9393
self.inverse_cdf_below_spike <= val,
94-
val <= self.inverse_cdf_below_spike + self.spike_height))[0]
95-
higher_indices = np.where(val > self.inverse_cdf_below_spike + self.spike_height)[0]
94+
val <= (self.inverse_cdf_below_spike + self.spike_height))
95+
higher_indices = val > (self.inverse_cdf_below_spike + self.spike_height)
9696

9797
res = np.zeros(len(val))
9898
res[lower_indices] = self._contracted_rescale(val[lower_indices])
@@ -137,7 +137,7 @@ def prob(self, val):
137137
original_is_number = isinstance(val, Number)
138138
res = self.slab.prob(val) * self.slab_fraction
139139
res = np.atleast_1d(res)
140-
res[np.where(val == self.spike_location)] = np.inf
140+
res[val == self.spike_location] = np.inf
141141
if original_is_number:
142142
try:
143143
res = res[0]
@@ -161,7 +161,7 @@ def ln_prob(self, val):
161161
original_is_number = isinstance(val, Number)
162162
res = self.slab.ln_prob(val) + np.log(self.slab_fraction)
163163
res = np.atleast_1d(res)
164-
res[np.where(val == self.spike_location)] = np.inf
164+
res[val == self.spike_location] = np.inf
165165
if original_is_number:
166166
try:
167167
res = res[0]
@@ -185,7 +185,5 @@ def cdf(self, val):
185185
186186
"""
187187
res = self.slab.cdf(val) * self.slab_fraction
188-
res = np.atleast_1d(res)
189-
indices_above_spike = np.where(val > self.spike_location)[0]
190-
res[indices_above_spike] += self.spike_height
188+
res += self.spike_height * (val > self.spike_location)
191189
return res

0 commit comments

Comments
 (0)