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,477 @@
1
+ """Validation tools: exact reference posteriors, simulation-based calibration, coverage.
2
+
3
+ * :func:`exact_posterior` computes the posterior on a fine grid over the
4
+ simplex for ``K = 2`` or ``3`` bins (a reference for testing engines).
5
+ * :func:`sbc` runs simulation-based calibration (Talts et al. 2018): if the
6
+ inference is calibrated, the rank of the true value among posterior draws is
7
+ uniform. Ranks are computed for every bin fraction and for the joint
8
+ log-likelihood (Modrák et al. 2025), and their uniformity is tested by a
9
+ rank-histogram chi-square or the ECDF test of Säilynoja et al. (2022).
10
+ * :func:`coverage` measures the empirical coverage of credible intervals for
11
+ bin fractions and user-defined functionals.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from collections.abc import Callable, Iterator, Sequence
17
+ from dataclasses import dataclass, field
18
+ from typing import Any
19
+
20
+ import numpy as np
21
+ from scipy import special, stats
22
+
23
+ from . import _utils
24
+ from ._quadrature import MAX_ELEMENTS
25
+ from ._utils import RNGLike, as_generator, chunk_slices, spawn, warn
26
+ from .likelihoods import Likelihoods
27
+ from .model import PopulationModel
28
+ from .priors import PopulationPrior
29
+ from .results import _SYMBOL, PopulationResult, _central, _flat_draws
30
+ from .warnings import ConvergenceWarning
31
+
32
+ __all__ = ["CoverageResult", "ExactPosterior", "SBCResult", "coverage", "exact_posterior", "sbc"]
33
+
34
+ #: Most bins of an SBC rank histogram when ``n_bins`` is ``None``.
35
+ SBC_MAX_BINS = 20
36
+ #: Fewest simulations per bin of an SBC rank histogram when ``n_bins`` is ``None``.
37
+ SBC_SIMULATIONS_PER_BIN = 5
38
+
39
+
40
+ # ----------------------------------------------------------------------------
41
+ # exact posterior by quadrature
42
+ # ----------------------------------------------------------------------------
43
+ @dataclass
44
+ class ExactPosterior:
45
+ """Posterior on quadrature nodes over the simplex (``K <= 3``).
46
+
47
+ Attributes
48
+ ----------
49
+ points : (n, K) array
50
+ Quadrature nodes (fractions).
51
+ weights : (n,) array
52
+ Normalised posterior weights (quadrature weight times density), summing to one.
53
+ log_density : (n,) array
54
+ Unnormalised log posterior density of the fractions at each node (its
55
+ maximum locates the mode; the weights also carry the node spacing).
56
+ """
57
+
58
+ points: np.ndarray
59
+ weights: np.ndarray
60
+ log_density: np.ndarray
61
+
62
+ @property
63
+ def mean(self) -> np.ndarray:
64
+ return self.weights @ self.points
65
+
66
+ def cov(self) -> np.ndarray:
67
+ d = self.points - self.mean
68
+ return (d * self.weights[:, None]).T @ d
69
+
70
+ @property
71
+ def std(self) -> np.ndarray:
72
+ return np.sqrt(np.diag(self.cov()))
73
+
74
+ def quantile(self, q: Any, k: int) -> np.ndarray:
75
+ """Quantiles of the marginal posterior of fraction ``k``.
76
+
77
+ Parameters
78
+ ----------
79
+ q : float or array_like
80
+ Probabilities in [0, 1].
81
+ k : int
82
+ Bin index.
83
+ """
84
+ x = self.points[:, k]
85
+ o = np.argsort(x)
86
+ c = np.cumsum(self.weights[o])
87
+ return np.interp(q, c - 0.5 * self.weights[o], x[o])
88
+
89
+
90
+ def exact_posterior(
91
+ model: PopulationModel | Likelihoods | Any,
92
+ prior: PopulationPrior | None = None,
93
+ n_grid: int = 200,
94
+ max_elements: int = MAX_ELEMENTS,
95
+ ) -> ExactPosterior:
96
+ r"""Quadrature posterior of the fractions for ``K = 2`` or ``K = 3`` bins.
97
+
98
+ Works for priors with a closed-form simplex density (Dirichlet family).
99
+ Selection effects are included. The fractions are stick-broken,
100
+ :math:`\rho = (x, (1-x)y, (1-x)(1-y))`, each stick coordinate substituted
101
+ as :math:`(1-\cos\pi s)/2` and integrated by Gauss-Legendre with
102
+ ``n_grid`` nodes, which places nodes densely at the boundary and handles
103
+ the integrable singularities of concentrations below one (audit G-3).
104
+ With the default ``n_grid``, means of a known Dirichlet posterior are
105
+ exact to 0.01 posterior SD for concentrations down to 0.2, and tail
106
+ probabilities to 0.005 for concentrations of 0.5 and above (at 0.2 the
107
+ mass below a 6e-9 quantile is 22% off). ``max_elements`` bounds the size of
108
+ the (objects x nodes) blocks evaluated at once (audit G-5).
109
+
110
+ Parameters
111
+ ----------
112
+ model : PopulationModel, Likelihoods or array_like (N, K)
113
+ The model, or likelihoods to build one from with ``prior``.
114
+ prior : PopulationPrior, optional
115
+ Population prior; ``None`` gives ``Dirichlet(1.0)``.
116
+ n_grid : int, optional
117
+ Gauss-Legendre nodes per stick coordinate.
118
+ max_elements : int, optional
119
+ Largest block of objects times nodes evaluated at once.
120
+ """
121
+ if not isinstance(model, PopulationModel):
122
+ model = PopulationModel(model, prior)
123
+ if model.kind != "fractions" or not model.prior.has_density:
124
+ raise ValueError(
125
+ "exact_posterior needs a fractions model with a closed-form prior density."
126
+ )
127
+ if model.K not in (2, 3):
128
+ raise ValueError("exact_posterior supports K = 2 or 3 bins.")
129
+ if model.joint:
130
+ raise ValueError(
131
+ "exact_posterior integrates the population only; a selection from injections or "
132
+ "a tallied response would be fixed. Pass selection.at_mean() or response.fixed() "
133
+ "to compare with that."
134
+ )
135
+ s, ws = np.polynomial.legendre.leggauss(int(n_grid))
136
+ s, ws = 0.5 * (s + 1.0), 0.5 * ws
137
+ x = 0.5 * (1.0 - np.cos(np.pi * s))
138
+ log_dx = np.log(0.5 * np.pi * np.sin(np.pi * s) * ws) # quadrature weight in x
139
+ if model.K == 2:
140
+ pts = np.stack([x, 1.0 - x], axis=1)
141
+ log_q = log_dx
142
+ else:
143
+ X, Y = np.meshgrid(x, x, indexing="ij")
144
+ pts = np.stack([X.ravel(), ((1 - X) * Y).ravel(), ((1 - X) * (1 - Y)).ravel()], axis=1)
145
+ # d(rho_1, rho_2) = (1 - x) dx dy
146
+ log_q = (log_dx[:, None] + log_dx[None, :] + np.log1p(-X)).ravel()
147
+ L, w = model._L, model._w
148
+ logp = np.empty(pts.shape[0])
149
+ for sl in chunk_slices(pts.shape[0], max(1, int(max_elements) // max(model.N, 1))):
150
+ P = pts[sl]
151
+ with np.errstate(divide="ignore"):
152
+ ll = w @ np.log(np.asarray(L @ P.T))
153
+ if model.selection is not None:
154
+ ll = ll - model.W * np.log(P @ model.selection)
155
+ logp[sl] = ll
156
+ with np.errstate(divide="ignore", invalid="ignore"):
157
+ logp = logp + model.prior.log_density(pts)
158
+ logp = np.where(np.isfinite(logp), logp, -np.inf)
159
+ logw = logp + log_q
160
+ wts = np.exp(logw - special.logsumexp(logw))
161
+ return ExactPosterior(points=pts, weights=wts, log_density=logp)
162
+
163
+
164
+ # ----------------------------------------------------------------------------
165
+ # SBC
166
+ # ----------------------------------------------------------------------------
167
+ Simulator = Callable[[np.random.Generator], tuple[Likelihoods, np.ndarray]]
168
+ Fitter = Callable[[Likelihoods, np.random.Generator], PopulationResult]
169
+
170
+
171
+ @dataclass
172
+ class SBCResult:
173
+ """Ranks from simulation-based calibration.
174
+
175
+ Attributes
176
+ ----------
177
+ ranks : (n_simulations, n_quantities) int array
178
+ Number of posterior draws below the true value.
179
+ n_draws : int
180
+ Draws per simulation used for ranking (ranks lie in ``0..n_draws``).
181
+ names : list of str
182
+ Quantity names (``"rho[k]"``, ``"lambda[k]"`` for rates or ``"w[k]"`` for the
183
+ component weights of a :class:`~binference.Basis`, and ``"loglike"``).
184
+ min_ess : (n_simulations,) array
185
+ Smallest bulk effective sample size of each fit (NaN without chains);
186
+ :func:`sbc` warns when it falls below ``n_draws``.
187
+ """
188
+
189
+ ranks: np.ndarray
190
+ n_draws: int
191
+ names: list[str]
192
+ min_ess: np.ndarray = field(default_factory=lambda: np.empty(0))
193
+
194
+ def _edges(self, n_bins: int | None) -> np.ndarray:
195
+ n_bins = n_bins or self._default_bins()
196
+ if not 1 <= n_bins <= self.n_draws + 1:
197
+ raise ValueError(f"n_bins must be between 1 and n_draws + 1 = {self.n_draws + 1}.")
198
+ return np.linspace(0, self.n_draws + 1, n_bins + 1)
199
+
200
+ def histogram(self, n_bins: int | None = None) -> np.ndarray:
201
+ """Rank histograms, shape ``(n_quantities, n_bins)``.
202
+
203
+ Parameters
204
+ ----------
205
+ n_bins : int, optional
206
+ Histogram bins; ``None`` takes the smallest of ``SBC_MAX_BINS``,
207
+ ``n_simulations // SBC_SIMULATIONS_PER_BIN`` and ``n_draws + 1``, at least 2.
208
+ """
209
+ edges = self._edges(n_bins)
210
+ return np.stack([np.histogram(r, bins=edges)[0] for r in self.ranks.T])
211
+
212
+ def expected(self, n_bins: int | None = None) -> np.ndarray:
213
+ """Expected count per histogram bin under uniform ranks, ``(n_bins,)``.
214
+
215
+ Bins hold different numbers of the ``n_draws + 1`` possible ranks unless
216
+ ``n_bins`` divides ``n_draws + 1`` (audit G-1).
217
+
218
+ Parameters
219
+ ----------
220
+ n_bins : int, optional
221
+ Histogram bins; ``None`` takes the smallest of ``SBC_MAX_BINS``,
222
+ ``n_simulations // SBC_SIMULATIONS_PER_BIN`` and ``n_draws + 1``, at least 2.
223
+ """
224
+ per_bin = np.histogram(np.arange(self.n_draws + 1), bins=self._edges(n_bins))[0]
225
+ return per_bin * self.ranks.shape[0] / (self.n_draws + 1)
226
+
227
+ def _default_bins(self) -> int:
228
+ n = self.ranks.shape[0]
229
+ return int(max(2, min(SBC_MAX_BINS, n // SBC_SIMULATIONS_PER_BIN, self.n_draws + 1)))
230
+
231
+ def pvalues(self, n_bins: int | None = None, *, method: str = "chisquare") -> np.ndarray:
232
+ r"""P-values of rank uniformity, one per quantity.
233
+
234
+ Parameters
235
+ ----------
236
+ n_bins : int, optional
237
+ Histogram bins for ``method="chisquare"``; ``None`` as for :meth:`histogram`.
238
+ method : {"chisquare", "ecdf"}, optional
239
+ Chi-square test of the rank histogram or ECDF test of the ranks.
240
+ ``"chisquare"``: Pearson's chi-square test of the rank histogram.
241
+ ``"ecdf"``: the ECDF test of Säilynoja, Bürkner & Vehtari (2022).
242
+ With :math:`S` simulations and :math:`M` = ``n_draws``, the count
243
+ :math:`C_j` of ranks at most :math:`j` is
244
+ :math:`\mathrm{Binomial}(S, (j+1)/(M+1))` under uniformity. The
245
+ statistic is the smallest two-sided binomial tail probability
246
+
247
+ .. math:: \hat\gamma = \min_j 2\min\{P(C \le C_j), P(C \ge C_j)\}
248
+
249
+ over :math:`j = 0, \dots, M-1`, and the p-value is
250
+ :math:`P(\gamma \le \hat\gamma)` for uniform ranks, computed
251
+ exactly from the joint (multinomial) law of the counts: the smallest
252
+ :math:`\alpha` for which the rank ECDF leaves the simultaneous
253
+ :math:`1 - \alpha` band. It takes no ``n_bins``.
254
+
255
+ Returns
256
+ -------
257
+ (n_quantities,) array
258
+ """
259
+ if method == "ecdf":
260
+ if n_bins is not None:
261
+ raise ValueError('n_bins applies to method="chisquare" only.')
262
+ return np.array([_ecdf_pvalue(r, self.n_draws) for r in self.ranks.T])
263
+ if method != "chisquare":
264
+ raise ValueError(f'method must be "chisquare" or "ecdf"; got {method!r}.')
265
+ H = self.histogram(n_bins)
266
+ e = self.expected(n_bins)
267
+ return np.array([stats.chisquare(h, f_exp=e).pvalue for h in H])
268
+
269
+ def passed(
270
+ self, alpha: float = 0.01, n_bins: int | None = None, *, method: str = "chisquare"
271
+ ) -> bool:
272
+ """True if no quantity rejects uniformity at Bonferroni-corrected level ``alpha``.
273
+
274
+ Each p-value from :meth:`pvalues` must lie above ``alpha / n_quantities``.
275
+
276
+ Parameters
277
+ ----------
278
+ alpha : float, optional
279
+ Significance level, Bonferroni-corrected over the quantities.
280
+ n_bins : int, optional
281
+ Histogram bins for ``method="chisquare"``, as for :meth:`pvalues`.
282
+ method : {"chisquare", "ecdf"}, optional
283
+ Chi-square test of the rank histogram or ECDF test of the ranks.
284
+ """
285
+ p = self.pvalues(n_bins, method=method)
286
+ return bool(np.all(p > alpha / p.size))
287
+
288
+ def plot(self, **kwargs: Any):
289
+ from .plotting import plot_sbc
290
+
291
+ return plot_sbc(self, **kwargs)
292
+
293
+
294
+ def _ecdf_pvalue(ranks: np.ndarray, n_draws: int) -> float:
295
+ """P-value of the ECDF test of :meth:`SBCResult.pvalues` for one quantity."""
296
+ S, L = ranks.size, n_draws + 1
297
+ F = np.arange(1, L) / L
298
+ c = np.arange(S + 1)
299
+ # two-sided binomial tail probability of every count c at every rank j < n_draws
300
+ tail = 2 * np.minimum(stats.binom.cdf(c, S, F[:, None]), stats.binom.sf(c - 1, S, F[:, None]))
301
+ C = np.cumsum(np.bincount(ranks, minlength=L))[:-1]
302
+ g = tail[np.arange(L - 1), C].min()
303
+ if g >= 1.0:
304
+ return 1.0
305
+ # each tail is a p-value, so p <= (L - 1) g; below 1e-12 that bound is returned, which
306
+ # spares the recursion its widest bands (S x S work per step for badly skewed ranks)
307
+ if (L - 1) * g < 1e-12:
308
+ return float((L - 1) * g)
309
+ # P(every C_j has tail > g): C_j - C_{j-1} | C_{j-1} ~ Binomial(S - C_{j-1}, 1/(L - j))
310
+ lo_prev, prob = 0, np.ones(1)
311
+ for j in range(L - 1):
312
+ inside = np.flatnonzero(tail[j] > g) # an interval: the binomial is unimodal
313
+ if inside.size == 0:
314
+ return 1.0
315
+ prev = lo_prev + np.arange(prob.size)
316
+ new = np.arange(inside[0], inside[-1] + 1)
317
+ prob = prob @ stats.binom.pmf(new - prev[:, None], S - prev[:, None], 1.0 / (L - j))
318
+ lo_prev = inside[0]
319
+ return float(min(1.0, max(0.0, 1.0 - prob.sum())))
320
+
321
+
322
+ def _fits(
323
+ simulate: Simulator, fit: Fitter, n_simulations: int, rng: RNGLike, what: str, show: bool
324
+ ) -> Iterator[tuple[PopulationResult, np.ndarray]]:
325
+ # each fit to a simulated data set, and its true value
326
+ gen = as_generator(rng)
327
+ for _ in _utils.progress(range(n_simulations), n_simulations, what, show):
328
+ r_sim, r_fit = spawn(gen, 2)
329
+ lk, truth = simulate(r_sim)
330
+ yield fit(lk, r_fit), truth
331
+
332
+
333
+ def _names(res: PopulationResult) -> list[str]:
334
+ """Names of the model's parameters: the component weights ``w`` with a
335
+ :class:`~binference.Basis` (as the prior draws them), else fractions or rates."""
336
+ symbol = "w" if res.basis is not None else _SYMBOL[res.kind]
337
+ return [f"{symbol}[{k}]" for k in range(res.component_draws.shape[-1])]
338
+
339
+
340
+ def sbc(
341
+ simulate: Simulator,
342
+ fit: Fitter,
343
+ n_simulations: int = 100,
344
+ *,
345
+ n_draws: int = 99,
346
+ log_likelihood: bool = True,
347
+ rng: RNGLike = None,
348
+ progress: bool = False,
349
+ ) -> SBCResult:
350
+ """Simulation-based calibration.
351
+
352
+ Parameters
353
+ ----------
354
+ simulate : callable
355
+ ``simulate(rng) -> (likelihoods, true_value)`` drawing a population from
356
+ the prior and a catalogue from it (see
357
+ :func:`binference.simulate.prior_predictive`). The true value is the
358
+ fractions, or the rates for rate models.
359
+ fit : callable
360
+ ``fit(likelihoods, rng) -> PopulationResult`` running the inference to
361
+ be validated (with the same prior as the simulator).
362
+ n_simulations : int
363
+ Number of simulated data sets.
364
+ n_draws : int
365
+ Posterior draws ranked per fit, thinned evenly from its draws. They
366
+ should be roughly independent, i.e. fewer than the ESS: a warning
367
+ names how many fits have a smallest bulk ESS below it, since
368
+ autocorrelated draws make the rank histograms look over-dispersed.
369
+ log_likelihood : bool
370
+ Also rank the joint log-likelihood of the true population.
371
+ rng : int, Generator or None
372
+ Seed or generator of the simulations and fits.
373
+ progress : bool
374
+ Show a progress bar over the simulations.
375
+ """
376
+ ranks, ess_min = [], []
377
+ names: list[str] = []
378
+ for res, truth in _fits(simulate, fit, n_simulations, rng, "SBC", progress):
379
+ if res.n_draws < n_draws:
380
+ raise ValueError(
381
+ f"Need at least {n_draws} posterior draws per fit; got {res.n_draws}."
382
+ )
383
+ names = _names(res) + (["loglike"] if log_likelihood else [])
384
+ D = _flat_draws(res.component_draws, 1, n_draws)
385
+ row = list((D < truth[None, :]).sum(axis=0))
386
+ if log_likelihood:
387
+ m = res.model
388
+ ll_true = m.log_likelihood(truth, absolute=False)
389
+ ll_draws = m._log_likelihood_many(D)
390
+ row.append(int((ll_draws < ll_true).sum()))
391
+ ranks.append(row)
392
+ d = res.diagnostics
393
+ ess_min.append(np.nanmin(d["ess_bulk"]) if "ess_bulk" in d else np.nan)
394
+ ess_min = np.asarray(ess_min)
395
+ low = int(np.sum(ess_min < n_draws)) # NaN (no chains) compares False
396
+ if low:
397
+ warn(
398
+ f"In {low} of {ess_min.size} fits the smallest bulk ESS is below n_draws={n_draws}; "
399
+ "autocorrelated draws make the ranks look over-dispersed.",
400
+ ConvergenceWarning,
401
+ )
402
+ return SBCResult(np.asarray(ranks, dtype=int), n_draws, names, ess_min)
403
+
404
+
405
+ # ----------------------------------------------------------------------------
406
+ # coverage
407
+ # ----------------------------------------------------------------------------
408
+ @dataclass
409
+ class CoverageResult:
410
+ """Empirical coverage of central credible intervals."""
411
+
412
+ levels: np.ndarray
413
+ covered: np.ndarray # (n_simulations, n_levels, n_quantities) bool
414
+ names: list[str]
415
+
416
+ def coverage_rates(self) -> np.ndarray:
417
+ """Coverage rate per level and quantity, ``(n_levels, n_quantities)``."""
418
+ return self.covered.mean(axis=0)
419
+
420
+ def stderr(self) -> np.ndarray:
421
+ """Binomial standard error of the nominal coverage, ``(n_levels, 1)``."""
422
+ n = self.covered.shape[0]
423
+ return np.sqrt(self.levels * (1 - self.levels) / n)[:, None]
424
+
425
+ def average(self) -> np.ndarray:
426
+ """Coverage per level averaged over quantities, ``(n_levels,)``."""
427
+ return self.coverage_rates().mean(axis=1)
428
+
429
+
430
+ def coverage(
431
+ simulate: Simulator,
432
+ fit: Fitter,
433
+ n_simulations: int = 100,
434
+ *,
435
+ levels: Sequence[float] = (0.5, 0.68, 0.9, 0.95),
436
+ functionals: dict[str, Callable[[np.ndarray], float]] | None = None,
437
+ rng: RNGLike = None,
438
+ progress: bool = False,
439
+ ) -> CoverageResult:
440
+ """Empirical coverage of central credible intervals over simulated data sets.
441
+
442
+ ``functionals`` maps names to functions of the fractions, or of the rates
443
+ for rate models (e.g. a mean
444
+ redshift); their intervals are checked alongside every bin fraction.
445
+
446
+ Parameters
447
+ ----------
448
+ simulate : callable
449
+ ``simulate(rng) -> (likelihoods, truth)``, as in :func:`sbc`.
450
+ fit : callable
451
+ ``fit(likelihoods, rng) -> result``, as in :func:`sbc`.
452
+ n_simulations : int, optional
453
+ Number of simulated data sets.
454
+ levels : sequence of float, optional
455
+ Credible levels whose coverage is measured.
456
+ functionals : dict, optional
457
+ Named functions of the fractions (or rates) whose intervals are also checked.
458
+ rng : int, Generator or None
459
+ Random number generator.
460
+ progress : bool, optional
461
+ Show a progress bar.
462
+ """
463
+ levels_arr = np.asarray(levels, dtype=float)
464
+ functionals = functionals or {}
465
+ out = []
466
+ names: list[str] = []
467
+ for res, truth in _fits(simulate, fit, n_simulations, rng, "coverage", progress):
468
+ names = _names(res) + list(functionals)
469
+ X = _flat_draws(res.component_draws, 1, None)
470
+ Q = np.column_stack([X, *(np.array([f(r) for r in X]) for f in functionals.values())])
471
+ tq = np.array([*truth, *(f(truth) for f in functionals.values())])
472
+ cov = np.empty((levels_arr.size, Q.shape[1]), dtype=bool)
473
+ for j, lev in enumerate(levels_arr):
474
+ lo, hi = _central(Q, lev)
475
+ cov[j] = (tq >= lo) & (tq <= hi)
476
+ out.append(cov)
477
+ return CoverageResult(levels_arr, np.asarray(out), names)
binference/warnings.py ADDED
@@ -0,0 +1,48 @@
1
+ """Warning classes emitted by binference.
2
+
3
+ All warnings derive from :class:`BinferenceWarning`, so they can be filtered in one
4
+ place, e.g. ``warnings.simplefilter("ignore", binference.BinferenceWarning)``.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ __all__ = [
10
+ "ApproximationWarning",
11
+ "BinferenceWarning",
12
+ "ConvergenceWarning",
13
+ "DiagnosticsError",
14
+ "InputWarning",
15
+ ]
16
+
17
+
18
+ class BinferenceWarning(UserWarning):
19
+ """Base class for all binference warnings."""
20
+
21
+
22
+ class InputWarning(BinferenceWarning):
23
+ """Inputs look inconsistent with the declared conventions.
24
+
25
+ Examples: posterior masses whose rows do not sum to one, values that look
26
+ like densities when masses were declared, or objects with too few samples.
27
+ """
28
+
29
+
30
+ class ConvergenceWarning(BinferenceWarning):
31
+ """A sampler or optimiser shows signs of non-convergence.
32
+
33
+ Examples: large R-hat, low effective sample size, divergent transitions, or
34
+ an optimiser that stopped before meeting its tolerance.
35
+ """
36
+
37
+
38
+ class ApproximationWarning(BinferenceWarning):
39
+ """A result comes from an approximation with known failure modes.
40
+
41
+ Mean-field variational Bayes, Laplace approximations without importance
42
+ correction (or with an unreliable correction), and bootstrap intervals are
43
+ flagged with this warning.
44
+ """
45
+
46
+
47
+ class DiagnosticsError(RuntimeError):
48
+ """``result.check(strict=True)`` found problems (the messages its warnings carry)."""
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.5
2
+ Name: binference
3
+ Version: 0.1.0
4
+ Summary: Population and hierarchical inference with PDFs: honest population distributions from noisy per-object PDFs over bins.
5
+ Project-URL: Homepage, https://github.com/joshspeagle/binference
6
+ Project-URL: Source, https://github.com/joshspeagle/binference
7
+ Project-URL: Documentation, https://joshspeagle.github.io/binference/
8
+ Project-URL: Issues, https://github.com/joshspeagle/binference/issues
9
+ Author: Joshua S. Speagle
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: bayesian,deconvolution,dirichlet,hierarchical,mcmc,photometric redshifts,population inference,quantification,unfolding
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Scientific/Engineering :: Astronomy
21
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
22
+ Classifier: Topic :: Scientific/Engineering :: Physics
23
+ Requires-Python: >=3.13
24
+ Requires-Dist: numpy>=2.1
25
+ Requires-Dist: scipy>=1.14.1
26
+ Requires-Dist: threadpoolctl>=3.1
27
+ Provides-Extra: all
28
+ Requires-Dist: matplotlib>=3.9.2; extra == 'all'
29
+ Requires-Dist: xarray>=2024.10; extra == 'all'
30
+ Provides-Extra: dev
31
+ Requires-Dist: fonttools>=4.47; extra == 'dev'
32
+ Requires-Dist: ipykernel>=6; extra == 'dev'
33
+ Requires-Dist: matplotlib>=3.9.2; extra == 'dev'
34
+ Requires-Dist: myst-nb>=1.1; extra == 'dev'
35
+ Requires-Dist: numpydoc>=1.6; extra == 'dev'
36
+ Requires-Dist: pre-commit>=3; extra == 'dev'
37
+ Requires-Dist: pytest-cov>=5; extra == 'dev'
38
+ Requires-Dist: pytest>=8; extra == 'dev'
39
+ Requires-Dist: sphinx-copybutton>=0.5; extra == 'dev'
40
+ Requires-Dist: sphinx>=7.2; extra == 'dev'
41
+ Requires-Dist: sphinxcontrib-bibtex>=2.6; extra == 'dev'
42
+ Requires-Dist: xarray>=2024.10; extra == 'dev'
43
+ Provides-Extra: docs
44
+ Requires-Dist: fonttools>=4.47; extra == 'docs'
45
+ Requires-Dist: ipykernel>=6; extra == 'docs'
46
+ Requires-Dist: matplotlib>=3.9.2; extra == 'docs'
47
+ Requires-Dist: myst-nb>=1.1; extra == 'docs'
48
+ Requires-Dist: numpydoc>=1.6; extra == 'docs'
49
+ Requires-Dist: sphinx-copybutton>=0.5; extra == 'docs'
50
+ Requires-Dist: sphinx>=7.2; extra == 'docs'
51
+ Requires-Dist: sphinxcontrib-bibtex>=2.6; extra == 'docs'
52
+ Provides-Extra: plot
53
+ Requires-Dist: matplotlib>=3.9.2; extra == 'plot'
54
+ Provides-Extra: test
55
+ Requires-Dist: matplotlib>=3.9.2; extra == 'test'
56
+ Requires-Dist: pytest-cov>=5; extra == 'test'
57
+ Requires-Dist: pytest>=8; extra == 'test'
58
+ Provides-Extra: xarray
59
+ Requires-Dist: xarray>=2024.10; extra == 'xarray'
60
+ Description-Content-Type: text/markdown
61
+
62
+ # binference
63
+
64
+ **Population and hierarchical inference with PDFs.**
65
+
66
+ Many objects are each measured with noise: galaxies, merger events, patients,
67
+ classified documents. binference reads each object's likelihood, posterior, samples,
68
+ binned measurement or classifier output. From these it infers the population the objects
69
+ come from, with calibrated uncertainty, and each object's posterior under that
70
+ population.
71
+
72
+ ![Population posteriors for a mock catalogue][figure]
73
+
74
+ [figure]: https://raw.githubusercontent.com/joshspeagle/binference/main/docs/_static/quickstart.png
75
+
76
+ *The population inferred from 2000 simulated objects, under the default Dirichlet prior
77
+ (top) and a Gaussian-process prior (bottom). Each panel shows the posterior median, its
78
+ 68% and 95% intervals, the true density (dashed) and the stacked estimate (dotted).*
79
+
80
+ ## Install
81
+
82
+ ```bash
83
+ pip install "binference[plot]"
84
+ ```
85
+
86
+ The [installation page](https://joshspeagle.github.io/binference/start/install.html)
87
+ lists the extras and the supported Python versions.
88
+
89
+ ## Example
90
+
91
+ ```python
92
+ import binference
93
+
94
+ bins = binference.Bins.linear(0.0, 3.0, 15)
95
+
96
+ # a simulated stand-in for your arrays: measured values and their Gaussian errors
97
+ mock = binference.simulate.mock_1d(2000, bins, rng=42)
98
+ z_obs, z_err = mock.measurements["mean"], mock.measurements["sigma"]
99
+
100
+ lk = binference.Likelihoods.from_gaussian(z_obs, z_err, bins)
101
+ result = binference.infer(lk, rng=1) # method="auto" chooses the engine
102
+ print(result.summary())
103
+ ```
104
+
105
+ ## Documentation
106
+
107
+ - [Quick start][quickstart]
108
+ - [Is binference right for my data?][is-it-right]
109
+ - [Tutorials][tutorials]
110
+ - [Full documentation][docs]
111
+
112
+ [quickstart]: https://joshspeagle.github.io/binference/start/quickstart.html
113
+ [is-it-right]: https://joshspeagle.github.io/binference/start/is-it-right.html
114
+ [tutorials]: https://joshspeagle.github.io/binference/tutorials/index.html
115
+ [docs]: https://joshspeagle.github.io/binference/
116
+
117
+ To contribute, see [`CONTRIBUTING.md`][contributing].
118
+
119
+ [contributing]: https://github.com/joshspeagle/binference/blob/main/CONTRIBUTING.md
120
+
121
+ ## Status and citing
122
+
123
+ binference is alpha software, and its API may change before 1.0. If you use it, please
124
+ cite it as described on the [citing page][citing].
125
+
126
+ [citing]: https://joshspeagle.github.io/binference/reference/citing.html
127
+
128
+ ## License
129
+
130
+ MIT; see [LICENSE](https://github.com/joshspeagle/binference/blob/main/LICENSE).