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,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