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,248 @@
|
|
|
1
|
+
"""Base classes for population priors.
|
|
2
|
+
|
|
3
|
+
A :class:`PopulationPrior` is a lightweight configuration object (what the user writes);
|
|
4
|
+
``prior.bind(bins)`` validates it against the bins and returns a
|
|
5
|
+
:class:`BoundPrior` that implements the numerical interface used by models and
|
|
6
|
+
engines:
|
|
7
|
+
|
|
8
|
+
* an unconstrained parametrisation ``x`` (``forward``) mapping to softmax logits
|
|
9
|
+
(fractions) or log-rates (rates), with the prior log-density in ``x``-space
|
|
10
|
+
(including Jacobians) and its gradient by the chain rule;
|
|
11
|
+
* prior draws, and (where defined) the density on the simplex and a
|
|
12
|
+
unit-cube ``prior_transform`` for nested samplers;
|
|
13
|
+
* for conjugate priors, posterior draws / modes given latent counts and
|
|
14
|
+
mean-field variational updates.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import hashlib
|
|
20
|
+
import inspect
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
from scipy import special
|
|
25
|
+
|
|
26
|
+
from ..bins import Bins
|
|
27
|
+
|
|
28
|
+
__all__ = ["BoundPrior", "PopulationPrior"]
|
|
29
|
+
|
|
30
|
+
#: Floor of drawn fractions and rates, so that their logarithms stay finite.
|
|
31
|
+
_TINY = 1e-300
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _log_gamma_variates(a: Any, rng: np.random.Generator, shape: tuple) -> np.ndarray:
|
|
35
|
+
"""Logarithms of Gamma(a, 1) draws, without underflow for small shapes.
|
|
36
|
+
|
|
37
|
+
For ``min(a) < 1`` they use ``Gamma(a) = Gamma(a + 1) * U**(1/a)`` in log space;
|
|
38
|
+
ordinary draws underflow to exactly 0 (about 12% of ``Gamma(0.003)`` draws).
|
|
39
|
+
"""
|
|
40
|
+
a = np.broadcast_to(np.asarray(a, dtype=float), shape)
|
|
41
|
+
if a.size == 0 or np.min(a) >= 1.0:
|
|
42
|
+
return np.log(rng.standard_gamma(a))
|
|
43
|
+
return np.log(rng.standard_gamma(a + 1.0)) + np.log(rng.random(shape)) / a
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _stick_break(a: np.ndarray, u: np.ndarray) -> np.ndarray:
|
|
47
|
+
"""Dirichlet(a) from uniforms ``u`` (last axis of length ``len(a) - 1``) by stick-breaking."""
|
|
48
|
+
tail = np.cumsum(a[::-1])[::-1] # tail[k] = sum_{j >= k} a_j
|
|
49
|
+
out = np.empty((*u.shape[:-1], a.size))
|
|
50
|
+
remaining = np.ones(u.shape[:-1])
|
|
51
|
+
for k in range(a.size - 1):
|
|
52
|
+
v = special.betaincinv(a[k], tail[k + 1], u[..., k])
|
|
53
|
+
out[..., k] = remaining * v
|
|
54
|
+
remaining = remaining * (1.0 - v)
|
|
55
|
+
out[..., -1] = remaining
|
|
56
|
+
return out
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class PopulationPrior:
|
|
60
|
+
"""Base class for priors on population fractions or rates."""
|
|
61
|
+
|
|
62
|
+
#: ``"fractions"`` (a distribution on the simplex) or ``"rates"``
|
|
63
|
+
kind: str = "fractions"
|
|
64
|
+
|
|
65
|
+
def bind(self, bins: Bins) -> BoundPrior: # pragma: no cover - abstract
|
|
66
|
+
raise NotImplementedError
|
|
67
|
+
|
|
68
|
+
def _params(self) -> dict[str, Any]:
|
|
69
|
+
# the constructor arguments, read from the attributes of the same name (audit P-14);
|
|
70
|
+
# a subclass that stores an argument under another name simply omits it
|
|
71
|
+
params = list(inspect.signature(type(self).__init__).parameters.values())[1:]
|
|
72
|
+
named = (p.name for p in params if p.kind not in (p.VAR_POSITIONAL, p.VAR_KEYWORD))
|
|
73
|
+
return {k: getattr(self, k) for k in named if hasattr(self, k)}
|
|
74
|
+
|
|
75
|
+
def __repr__(self) -> str:
|
|
76
|
+
args = ", ".join(f"{k}={_short(v)}" for k, v in self._params().items())
|
|
77
|
+
return f"{type(self).__name__}({args})"
|
|
78
|
+
|
|
79
|
+
def _fingerprint(self) -> str:
|
|
80
|
+
"""A digest of the type and every argument in full (the repr shortens arrays)."""
|
|
81
|
+
h = hashlib.sha256(type(self).__name__.encode())
|
|
82
|
+
for k, v in self._params().items():
|
|
83
|
+
h.update(k.encode())
|
|
84
|
+
if isinstance(v, np.ndarray):
|
|
85
|
+
h.update(f"{v.dtype}{v.shape}".encode() + np.ascontiguousarray(v).tobytes())
|
|
86
|
+
else:
|
|
87
|
+
h.update((v._fingerprint() if hasattr(v, "_fingerprint") else repr(v)).encode())
|
|
88
|
+
return h.hexdigest()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _short(v: Any) -> str:
|
|
92
|
+
if isinstance(v, np.ndarray):
|
|
93
|
+
if v.size <= 4:
|
|
94
|
+
return np.array2string(v, precision=3, separator=", ")
|
|
95
|
+
return f"array(shape={v.shape})"
|
|
96
|
+
return repr(v)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _per_item(value: Any, n: int, name: str) -> np.ndarray:
|
|
100
|
+
"""A scalar broadcast to ``n`` values, or an array of exactly ``n`` values."""
|
|
101
|
+
v = np.asarray(value)
|
|
102
|
+
if v.ndim and v.shape != (n,):
|
|
103
|
+
raise ValueError(f"{name} must be a scalar or have length {n}; got shape {v.shape}.")
|
|
104
|
+
return np.broadcast_to(v, (n,)).copy()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class BoundPrior:
|
|
108
|
+
"""Numerical interface of a prior bound to specific bins (internal).
|
|
109
|
+
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
prior : PopulationPrior
|
|
113
|
+
The prior.
|
|
114
|
+
bins : Bins
|
|
115
|
+
The bins it is bound to.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
kind: str = "fractions"
|
|
119
|
+
conjugate: bool = False
|
|
120
|
+
has_density: bool = False
|
|
121
|
+
has_prior_transform: bool = False
|
|
122
|
+
#: whether it has partial non-centring weights (``with_centering``; the logistic family)
|
|
123
|
+
has_centering: bool = False
|
|
124
|
+
#: counts its mode adds to the data's, ``alpha - 1`` (Dirichlet) or ``a - 1``
|
|
125
|
+
#: (GammaRates), as in EM's M-step; ``None`` for other priors
|
|
126
|
+
pseudocounts: np.ndarray | None = None
|
|
127
|
+
|
|
128
|
+
def __init__(self, prior: PopulationPrior, bins: Bins) -> None:
|
|
129
|
+
self.prior = prior
|
|
130
|
+
self.bins = bins
|
|
131
|
+
self.K = bins.K
|
|
132
|
+
|
|
133
|
+
# -- unconstrained parametrisation --------------------------------------
|
|
134
|
+
dim: int
|
|
135
|
+
|
|
136
|
+
def forward(self, x: np.ndarray) -> tuple[np.ndarray, Any]: # pragma: no cover
|
|
137
|
+
"""Map unconstrained ``x`` to logits / log-rates ``eta`` (and a cache).
|
|
138
|
+
|
|
139
|
+
Parameters
|
|
140
|
+
----------
|
|
141
|
+
x : ndarray (dim,)
|
|
142
|
+
Unconstrained point.
|
|
143
|
+
"""
|
|
144
|
+
raise NotImplementedError
|
|
145
|
+
|
|
146
|
+
def log_prior_and_grad(
|
|
147
|
+
self, x: np.ndarray, cache: Any, value: np.ndarray, d_eta: np.ndarray
|
|
148
|
+
) -> tuple[float, np.ndarray]: # pragma: no cover
|
|
149
|
+
"""Prior log-density in ``x``-space and the total gradient.
|
|
150
|
+
|
|
151
|
+
``d_eta`` is the gradient of the log-likelihood with respect to
|
|
152
|
+
``eta``; the returned gradient is ``d/dx [log prior(x) + loglike]``.
|
|
153
|
+
|
|
154
|
+
Parameters
|
|
155
|
+
----------
|
|
156
|
+
x : ndarray (dim,)
|
|
157
|
+
Unconstrained point.
|
|
158
|
+
cache : object
|
|
159
|
+
The cache :meth:`forward` returned for ``x``.
|
|
160
|
+
value : ndarray (K,)
|
|
161
|
+
Fractions or rates at ``x``.
|
|
162
|
+
d_eta : ndarray (K,)
|
|
163
|
+
Gradient of the log-likelihood with respect to ``eta``.
|
|
164
|
+
"""
|
|
165
|
+
raise NotImplementedError
|
|
166
|
+
|
|
167
|
+
def init_from_value(self, value: np.ndarray) -> np.ndarray: # pragma: no cover
|
|
168
|
+
"""An unconstrained starting point near the given fractions or rates.
|
|
169
|
+
|
|
170
|
+
Exact for the Dirichlet family and rates; for the logistic priors a
|
|
171
|
+
ridge-regularised fit that shrinks towards the prior mean.
|
|
172
|
+
|
|
173
|
+
Parameters
|
|
174
|
+
----------
|
|
175
|
+
value : ndarray (K,)
|
|
176
|
+
Fractions or rates.
|
|
177
|
+
"""
|
|
178
|
+
raise NotImplementedError
|
|
179
|
+
|
|
180
|
+
# -- draws and densities -------------------------------------------------
|
|
181
|
+
def sample(self, n: int, rng: np.random.Generator) -> np.ndarray: # pragma: no cover
|
|
182
|
+
raise NotImplementedError
|
|
183
|
+
|
|
184
|
+
def log_density(self, value: np.ndarray) -> np.ndarray:
|
|
185
|
+
"""Log-density of fractions (on the simplex) or rates.
|
|
186
|
+
|
|
187
|
+
Parameters
|
|
188
|
+
----------
|
|
189
|
+
value : ndarray (..., K)
|
|
190
|
+
Fractions or rates.
|
|
191
|
+
"""
|
|
192
|
+
raise NotImplementedError(f"{type(self.prior).__name__} has no closed-form density.")
|
|
193
|
+
|
|
194
|
+
def unbounded_density(self) -> bool:
|
|
195
|
+
"""Whether the density of the fractions (or rates) is unbounded at the boundary.
|
|
196
|
+
|
|
197
|
+
Then its mode can lie on the boundary (components exactly zero), and
|
|
198
|
+
NUTS meets very long tails in log space for components without data.
|
|
199
|
+
"""
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
def grad_log_density(self, value: np.ndarray) -> np.ndarray:
|
|
203
|
+
"""Gradient of :meth:`log_density` with respect to the fractions/rates.
|
|
204
|
+
|
|
205
|
+
Components with zero value are returned as zero (they are handled as
|
|
206
|
+
active constraints by callers).
|
|
207
|
+
|
|
208
|
+
Parameters
|
|
209
|
+
----------
|
|
210
|
+
value : ndarray (K,)
|
|
211
|
+
Fractions or rates.
|
|
212
|
+
"""
|
|
213
|
+
raise NotImplementedError(f"{type(self.prior).__name__} has no closed-form density.")
|
|
214
|
+
|
|
215
|
+
def prior_transform(self, u: np.ndarray) -> np.ndarray:
|
|
216
|
+
"""Map the unit hypercube ``[0, 1]^dim`` to prior draws (nested sampling).
|
|
217
|
+
|
|
218
|
+
``dim`` is ``K - 1`` for fractions and ``K`` for rates.
|
|
219
|
+
|
|
220
|
+
Parameters
|
|
221
|
+
----------
|
|
222
|
+
u : ndarray (..., dim)
|
|
223
|
+
Points in the unit hypercube.
|
|
224
|
+
"""
|
|
225
|
+
raise NotImplementedError(f"{type(self.prior).__name__} has no prior transform.")
|
|
226
|
+
|
|
227
|
+
# -- conjugate updates ----------------------------------------------------
|
|
228
|
+
def posterior_sample(
|
|
229
|
+
self, counts: np.ndarray, rng: np.random.Generator, exposure: np.ndarray | None = None
|
|
230
|
+
) -> np.ndarray:
|
|
231
|
+
raise NotImplementedError(f"{type(self.prior).__name__} is not conjugate.")
|
|
232
|
+
|
|
233
|
+
def map_from_counts(
|
|
234
|
+
self, counts: np.ndarray, exposure: np.ndarray | None = None
|
|
235
|
+
) -> np.ndarray:
|
|
236
|
+
raise NotImplementedError(f"{type(self.prior).__name__} is not conjugate.")
|
|
237
|
+
|
|
238
|
+
def vb_update(self, counts: np.ndarray, exposure: np.ndarray | None = None) -> Any:
|
|
239
|
+
raise NotImplementedError(f"{type(self.prior).__name__} is not conjugate.")
|
|
240
|
+
|
|
241
|
+
def vb_expected_log(self, state: Any) -> np.ndarray:
|
|
242
|
+
raise NotImplementedError
|
|
243
|
+
|
|
244
|
+
def vb_sample(self, state: Any, n: int, rng: np.random.Generator) -> np.ndarray:
|
|
245
|
+
raise NotImplementedError
|
|
246
|
+
|
|
247
|
+
def vb_mean(self, state: Any) -> np.ndarray:
|
|
248
|
+
raise NotImplementedError
|