binference 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- binference/__init__.py +85 -0
- binference/_kernels.py +252 -0
- binference/_quadrature.py +202 -0
- binference/_simplex.py +54 -0
- binference/_utils.py +139 -0
- binference/_version.py +1 -0
- binference/basis.py +314 -0
- binference/bins.py +645 -0
- binference/density.py +323 -0
- binference/diagnostics.py +400 -0
- binference/estimator.py +197 -0
- binference/inference.py +368 -0
- binference/io.py +309 -0
- binference/likelihoods.py +1684 -0
- binference/measure.py +246 -0
- binference/model.py +833 -0
- binference/plotting.py +538 -0
- binference/priors/__init__.py +47 -0
- binference/priors/base.py +248 -0
- binference/priors/dirichlet.py +640 -0
- binference/priors/hyper.py +240 -0
- binference/priors/logistic.py +676 -0
- binference/priors/rates.py +139 -0
- binference/response.py +539 -0
- binference/results.py +2271 -0
- binference/samplers/__init__.py +19 -0
- binference/samplers/_curvature.py +67 -0
- binference/samplers/_nuts_core.py +284 -0
- binference/samplers/base.py +118 -0
- binference/samplers/bootstrap.py +136 -0
- binference/samplers/gibbs.py +287 -0
- binference/samplers/laplace.py +200 -0
- binference/samplers/nuts.py +681 -0
- binference/samplers/optimize.py +488 -0
- binference/samplers/vb.py +118 -0
- binference/selection.py +621 -0
- binference/simulate.py +720 -0
- binference/validation.py +477 -0
- binference/warnings.py +48 -0
- binference-0.1.0.dist-info/METADATA +130 -0
- binference-0.1.0.dist-info/RECORD +43 -0
- binference-0.1.0.dist-info/WHEEL +4 -0
- binference-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Scalar hyperpriors for positive hyperparameters (scales, length scales, ...).
|
|
2
|
+
|
|
3
|
+
Each hyperprior is parametrised on ``u = log(theta)`` for sampling: the
|
|
4
|
+
methods return the log-density of ``u`` (including the Jacobian ``theta``) and
|
|
5
|
+
its derivative. :class:`Fixed` marks a hyperparameter that is not inferred.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from numbers import Real
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
from scipy import optimize, special
|
|
14
|
+
|
|
15
|
+
__all__ = ["Fixed", "Gamma", "HalfNormal", "Hyperprior", "InverseGamma", "LogNormal"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Hyperprior:
|
|
19
|
+
"""Base class for priors on a positive scalar hyperparameter."""
|
|
20
|
+
|
|
21
|
+
fixed: bool = False
|
|
22
|
+
|
|
23
|
+
def logpdf_u(self, u: float) -> float: # pragma: no cover - abstract
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
|
|
26
|
+
def dlogpdf_u(self, u: float) -> float: # pragma: no cover - abstract
|
|
27
|
+
raise NotImplementedError
|
|
28
|
+
|
|
29
|
+
def sample(self, n: int, rng: np.random.Generator) -> np.ndarray: # pragma: no cover
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
|
|
32
|
+
def median(self) -> float: # pragma: no cover - abstract
|
|
33
|
+
raise NotImplementedError
|
|
34
|
+
|
|
35
|
+
def __repr__(self) -> str:
|
|
36
|
+
args = ", ".join(f"{k}={v!r}" for k, v in vars(self).items())
|
|
37
|
+
return f"{type(self).__name__}({args})"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Fixed(Hyperprior):
|
|
41
|
+
"""A hyperparameter held fixed at ``value`` (not inferred).
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
value : float
|
|
46
|
+
The fixed value.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
fixed = True
|
|
50
|
+
|
|
51
|
+
def __init__(self, value: float) -> None:
|
|
52
|
+
if not np.isfinite(value) or value <= 0:
|
|
53
|
+
raise ValueError("Fixed hyperparameter values must be positive and finite.")
|
|
54
|
+
self.value = float(value)
|
|
55
|
+
|
|
56
|
+
def sample(self, n: int, rng: np.random.Generator) -> np.ndarray:
|
|
57
|
+
return np.full(n, self.value)
|
|
58
|
+
|
|
59
|
+
def median(self) -> float:
|
|
60
|
+
return self.value
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class HalfNormal(Hyperprior):
|
|
64
|
+
"""Half-normal prior with scale ``scale`` on a positive hyperparameter.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
scale : float, optional
|
|
69
|
+
Scale of the half-normal.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(self, scale: float = 1.0) -> None:
|
|
73
|
+
if not scale > 0:
|
|
74
|
+
raise ValueError("scale must be positive.")
|
|
75
|
+
self.scale = float(scale)
|
|
76
|
+
|
|
77
|
+
def logpdf_u(self, u: float) -> float:
|
|
78
|
+
t = np.exp(u)
|
|
79
|
+
s = self.scale
|
|
80
|
+
return float(np.log(2.0) - np.log(s) - 0.5 * np.log(2 * np.pi) - 0.5 * (t / s) ** 2 + u)
|
|
81
|
+
|
|
82
|
+
def dlogpdf_u(self, u: float) -> float:
|
|
83
|
+
t = np.exp(u)
|
|
84
|
+
return float(1.0 - (t / self.scale) ** 2)
|
|
85
|
+
|
|
86
|
+
def sample(self, n: int, rng: np.random.Generator) -> np.ndarray:
|
|
87
|
+
return np.abs(rng.normal(0.0, self.scale, size=n))
|
|
88
|
+
|
|
89
|
+
def median(self) -> float:
|
|
90
|
+
return float(self.scale * special.ndtri(0.75))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class LogNormal(Hyperprior):
|
|
94
|
+
"""Log-normal prior: ``log(theta) ~ Normal(mu, sigma)``.
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
mu : float, optional
|
|
99
|
+
Mean of the log of the hyperparameter.
|
|
100
|
+
sigma : float, optional
|
|
101
|
+
Standard deviation of the log of the hyperparameter.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(self, mu: float = 0.0, sigma: float = 1.0) -> None:
|
|
105
|
+
if not sigma > 0:
|
|
106
|
+
raise ValueError("sigma must be positive.")
|
|
107
|
+
self.mu = float(mu)
|
|
108
|
+
self.sigma = float(sigma)
|
|
109
|
+
|
|
110
|
+
def logpdf_u(self, u: float) -> float:
|
|
111
|
+
z = (u - self.mu) / self.sigma
|
|
112
|
+
return float(-0.5 * z * z - np.log(self.sigma) - 0.5 * np.log(2 * np.pi))
|
|
113
|
+
|
|
114
|
+
def dlogpdf_u(self, u: float) -> float:
|
|
115
|
+
return float(-(u - self.mu) / self.sigma**2)
|
|
116
|
+
|
|
117
|
+
def sample(self, n: int, rng: np.random.Generator) -> np.ndarray:
|
|
118
|
+
return np.exp(rng.normal(self.mu, self.sigma, size=n))
|
|
119
|
+
|
|
120
|
+
def median(self) -> float:
|
|
121
|
+
return float(np.exp(self.mu))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class InverseGamma(Hyperprior):
|
|
125
|
+
"""Inverse-gamma prior with shape ``a`` and scale ``b``.
|
|
126
|
+
|
|
127
|
+
Parameters
|
|
128
|
+
----------
|
|
129
|
+
a : float, optional
|
|
130
|
+
Shape.
|
|
131
|
+
b : float, optional
|
|
132
|
+
Scale.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
def __init__(self, a: float = 2.0, b: float = 1.0) -> None:
|
|
136
|
+
if not (a > 0 and b > 0):
|
|
137
|
+
raise ValueError("a and b must be positive.")
|
|
138
|
+
self.a = float(a)
|
|
139
|
+
self.b = float(b)
|
|
140
|
+
|
|
141
|
+
def logpdf_u(self, u: float) -> float:
|
|
142
|
+
return float(
|
|
143
|
+
self.a * np.log(self.b) - special.gammaln(self.a) - self.a * u - self.b * np.exp(-u)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def dlogpdf_u(self, u: float) -> float:
|
|
147
|
+
return float(-self.a + self.b * np.exp(-u))
|
|
148
|
+
|
|
149
|
+
def sample(self, n: int, rng: np.random.Generator) -> np.ndarray:
|
|
150
|
+
return self.b / rng.gamma(self.a, 1.0, size=n)
|
|
151
|
+
|
|
152
|
+
def median(self) -> float:
|
|
153
|
+
return float(self.b / special.gammaincinv(self.a, 0.5))
|
|
154
|
+
|
|
155
|
+
@classmethod
|
|
156
|
+
def from_quantiles(cls, lower: float, upper: float, tail: float = 0.01) -> InverseGamma:
|
|
157
|
+
"""The inverse gamma with ``P(< lower) = P(> upper) = tail``.
|
|
158
|
+
|
|
159
|
+
Useful for GP length scales: put little mass below the resolution of
|
|
160
|
+
the grid and above the size of the domain (Betancourt 2017, "Robust
|
|
161
|
+
Gaussian processes in Stan").
|
|
162
|
+
|
|
163
|
+
Parameters
|
|
164
|
+
----------
|
|
165
|
+
lower, upper : float
|
|
166
|
+
Values below and above which the prior puts mass ``tail``.
|
|
167
|
+
tail : float, optional
|
|
168
|
+
Prior mass below ``lower`` and above ``upper``.
|
|
169
|
+
"""
|
|
170
|
+
if not 0 < lower < upper:
|
|
171
|
+
raise ValueError("Need 0 < lower < upper.")
|
|
172
|
+
if not 0 < tail < 0.5:
|
|
173
|
+
raise ValueError("tail must be in (0, 0.5).")
|
|
174
|
+
|
|
175
|
+
# The tail conditions fix b = lower * Qinv(a, tail) = upper * Qinv(a, 1 - tail)
|
|
176
|
+
# (Qinv: inverse regularised upper incomplete gamma). The ratio
|
|
177
|
+
# Qinv(a, tail) / Qinv(a, 1 - tail) falls monotonically from infinity to 1 as a
|
|
178
|
+
# grows, so a follows from a bracketed one-dimensional root.
|
|
179
|
+
target = np.log(upper / lower)
|
|
180
|
+
|
|
181
|
+
def h(log_a):
|
|
182
|
+
a = np.exp(log_a)
|
|
183
|
+
with np.errstate(divide="ignore"): # tiny a: the lower quantile underflows
|
|
184
|
+
return (
|
|
185
|
+
np.log(special.gammainccinv(a, tail))
|
|
186
|
+
- np.log(special.gammainccinv(a, 1.0 - tail))
|
|
187
|
+
- target
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
lo_, hi_ = np.log(1e-3), np.log(1e7)
|
|
191
|
+
if not (h(lo_) > 0 > h(hi_)):
|
|
192
|
+
raise ValueError("Cannot match these quantiles with an inverse gamma.")
|
|
193
|
+
log_a = optimize.brentq(h, lo_, hi_, xtol=1e-12)
|
|
194
|
+
a = float(np.exp(log_a))
|
|
195
|
+
b = float(lower * special.gammainccinv(a, tail))
|
|
196
|
+
return cls(a, b)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class Gamma(Hyperprior):
|
|
200
|
+
"""Gamma prior with shape ``a`` and rate ``b``.
|
|
201
|
+
|
|
202
|
+
Parameters
|
|
203
|
+
----------
|
|
204
|
+
a : float, optional
|
|
205
|
+
Shape.
|
|
206
|
+
b : float, optional
|
|
207
|
+
Rate.
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
def __init__(self, a: float = 2.0, b: float = 1.0) -> None:
|
|
211
|
+
if not (a > 0 and b > 0):
|
|
212
|
+
raise ValueError("a and b must be positive.")
|
|
213
|
+
self.a = float(a)
|
|
214
|
+
self.b = float(b)
|
|
215
|
+
|
|
216
|
+
def logpdf_u(self, u: float) -> float:
|
|
217
|
+
return float(
|
|
218
|
+
self.a * np.log(self.b) - special.gammaln(self.a) + self.a * u - self.b * np.exp(u)
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def dlogpdf_u(self, u: float) -> float:
|
|
222
|
+
return float(self.a - self.b * np.exp(u))
|
|
223
|
+
|
|
224
|
+
def sample(self, n: int, rng: np.random.Generator) -> np.ndarray:
|
|
225
|
+
return rng.gamma(self.a, 1.0 / self.b, size=n)
|
|
226
|
+
|
|
227
|
+
def median(self) -> float:
|
|
228
|
+
return float(special.gammaincinv(self.a, 0.5) / self.b)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def as_hyperprior(value: Hyperprior | float | int) -> Hyperprior:
|
|
232
|
+
"""Interpret a number as :class:`Fixed` and pass hyperpriors through."""
|
|
233
|
+
if isinstance(value, Hyperprior):
|
|
234
|
+
return value
|
|
235
|
+
number = isinstance(value, Real) or (
|
|
236
|
+
isinstance(value, np.ndarray) and value.ndim == 0 and value.dtype.kind in "iuf"
|
|
237
|
+
)
|
|
238
|
+
if number and not isinstance(value, bool | np.bool_):
|
|
239
|
+
return Fixed(float(value))
|
|
240
|
+
raise TypeError(f"Expected a Hyperprior or a positive number; got {type(value).__name__}.")
|