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.
Files changed (43) hide show
  1. binference/__init__.py +85 -0
  2. binference/_kernels.py +252 -0
  3. binference/_quadrature.py +202 -0
  4. binference/_simplex.py +54 -0
  5. binference/_utils.py +139 -0
  6. binference/_version.py +1 -0
  7. binference/basis.py +314 -0
  8. binference/bins.py +645 -0
  9. binference/density.py +323 -0
  10. binference/diagnostics.py +400 -0
  11. binference/estimator.py +197 -0
  12. binference/inference.py +368 -0
  13. binference/io.py +309 -0
  14. binference/likelihoods.py +1684 -0
  15. binference/measure.py +246 -0
  16. binference/model.py +833 -0
  17. binference/plotting.py +538 -0
  18. binference/priors/__init__.py +47 -0
  19. binference/priors/base.py +248 -0
  20. binference/priors/dirichlet.py +640 -0
  21. binference/priors/hyper.py +240 -0
  22. binference/priors/logistic.py +676 -0
  23. binference/priors/rates.py +139 -0
  24. binference/response.py +539 -0
  25. binference/results.py +2271 -0
  26. binference/samplers/__init__.py +19 -0
  27. binference/samplers/_curvature.py +67 -0
  28. binference/samplers/_nuts_core.py +284 -0
  29. binference/samplers/base.py +118 -0
  30. binference/samplers/bootstrap.py +136 -0
  31. binference/samplers/gibbs.py +287 -0
  32. binference/samplers/laplace.py +200 -0
  33. binference/samplers/nuts.py +681 -0
  34. binference/samplers/optimize.py +488 -0
  35. binference/samplers/vb.py +118 -0
  36. binference/selection.py +621 -0
  37. binference/simulate.py +720 -0
  38. binference/validation.py +477 -0
  39. binference/warnings.py +48 -0
  40. binference-0.1.0.dist-info/METADATA +130 -0
  41. binference-0.1.0.dist-info/RECORD +43 -0
  42. binference-0.1.0.dist-info/WHEEL +4 -0
  43. 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__}.")