binference: implementation plan (v0.1)¶
This document describes how to build what spec.md specifies: the code layout, the internal interfaces, the milestones and the documentation. Tests are described in test-plan.md; every milestone ships with its tests. The milestones (§3) are the original plan; the layout, interfaces, conventions and CI (§1, §2, §4, §6) were brought up to date with the code on 2026-10-26 (issue #79).
1. Repository layout¶
binference/
├── pyproject.toml # hatchling build; deps; extras; ruff, pytest and coverage config
├── README.md # pitch, install, 10-line quickstart, links
├── CHANGELOG.md CONTRIBUTING.md CITATION.cff LICENSE
├── .pre-commit-config.yaml # ruff (lint + format), end-of-file, trailing whitespace, check-yaml
├── .github/workflows/ci.yml # lint, test matrix, minimum dependencies, slow tests, docs, build
├── src/binference/
│ ├── __init__.py # public API re-exports, __version__
│ ├── _version.py
│ ├── warnings.py # BinferenceWarning, InputWarning, ConvergenceWarning, ApproximationWarning
│ ├── _utils.py # rng handling (SPEC 7), array validation, chunking, progress
│ ├── _simplex.py # Helmert basis, softmax / log-softmax, ILR, CLR
│ ├── _kernels.py # dense/sparse matvecs, expected counts, log-likelihood, label sampling
│ ├── _quadrature.py # adaptive bin averages in the measure coordinate (DECISIONS #23)
│ ├── measure.py # Measure: the coordinate in which a bin is flat (DECISIONS #21)
│ ├── bins.py # Bins (measures, masks, coarsen/refine)
│ ├── basis.py # Basis: flat or hat components at a finer resolution (DECISIONS #76)
│ ├── density.py # Density: interim priors, base measures, simulation truth (DECISIONS #26)
│ ├── selection.py # Selection: modes, scales, functions, injections (DECISIONS #7, #46, #80)
│ ├── response.py # Response: binned measurements and tallied responses (DECISIONS #27, #81)
│ ├── likelihoods.py # Likelihoods + constructors
│ ├── priors/
│ │ ├── __init__.py
│ │ ├── base.py # Prior / bound-prior protocol
│ │ ├── hyper.py # HalfNormal, LogNormal, InverseGamma, Gamma, Fixed (scalar hyperpriors)
│ │ ├── dirichlet.py # Dirichlet, GeneralizedDirichlet, DirichletTree, PolyaTree
│ │ ├── logistic.py # LogisticNormal, LogisticRandomWalk, LogisticGP
│ │ └── rates.py # GammaRates
│ ├── model.py # PopulationModel: log-densities, gradients, transforms, selection
│ ├── samplers/
│ │ ├── __init__.py
│ │ ├── base.py # Engine base class, chain bookkeeping, result assembly
│ │ ├── _nuts_core.py # tree building, leapfrog, U-turn checks (pure functions)
│ │ ├── _curvature.py # finite-difference Hessians shared by NUTS and Laplace
│ │ ├── nuts.py # NUTS engine with adaptation, multi-chain, resumable
│ │ ├── gibbs.py # data-augmentation Gibbs (selection, conjugate priors)
│ │ ├── optimize.py # MAP: mix-SQP, EM + SQUAREM, L-BFGS
│ │ ├── laplace.py # Laplace + PSIS importance resampling
│ │ ├── vb.py # mean-field VB
│ │ └── bootstrap.py # bootstrap over objects
│ ├── results.py # PopulationResult, PointEstimate, Summary
│ ├── diagnostics.py # ess, rhat, autocorr_time, mcse, psis, kkt_gap, missing_information
│ ├── inference.py # infer(), method registry, Auto (method="auto")
│ ├── estimator.py # PopulationEstimator (scikit-learn style)
│ ├── simulate.py # mock catalogues + Truth
│ ├── validation.py # exact_posterior, sbc, coverage
│ ├── plotting.py # matplotlib helpers (optional dependency)
│ └── io.py # save/load (npz, versioned schema), to_datatree
├── tests/ # see test-plan.md
├── benchmarks/ # kernel timings and the benchmark matrix (non-gating)
└── docs/ # Sphinx + MyST-NB (see §5)
2. Internal interfaces¶
2.1 Numerical kernels (_kernels.py)¶
All functions accept dense ndarray or CSR L of shape (N, K) and w of shape (N,).
Function |
Returns |
Notes |
|---|---|---|
|
|
BLAS or sparse |
|
|
|
|
|
|
|
|
chunked |
|
|
dense: inverse-CDF on cumulative sums; CSR: global cumulative sum + |
|
|
duplicates via |
2.2 Priors (priors/base.py)¶
A user-facing prior is a lightweight, picklable configuration object. PopulationModel calls prior.bind(bins), which validates the prior against the bins and precomputes bases. It returns a bound prior that implements:
kind: "fractions" | "rates"
dim: int # unconstrained dimension
conjugate: bool
forward(x) -> (eta, cache) # softmax logits (fractions) or log-rates (rates)
log_prior_and_grad(x, cache, d_eta) -> (lp, grad_x)
# lp includes the Jacobian to unconstrained space;
# grad_x = J^T d_eta + grad lp (chain rule through forward)
has_density: bool ; has_prior_transform: bool
init_from_value(value) -> x # starting point (ridge-shrunk fit for logistic priors)
sample(n, rng) -> value # prior draws on the simplex (or rates)
log_density(value), grad_log_density(value) # where has_density
unbounded_density() -> bool # density unbounded at the boundary (MAP space choice)
prior_transform(u) -> value # where supported (nested sampling)
# conjugate priors only:
posterior_sample(counts, rng, exposure=None) -> rho or lambda
map_from_counts(nhat, exposure=None) -> rho or lambda
vb_update(nhat, exposure=None) -> state ; vb_expected_log(state) -> E[log rho]
vb_sample(state, n, rng) ; vb_mean(state)
The likelihood side always hands the prior a gradient with respect to eta. The prior applies the chain rule. This keeps model.py independent of the prior family.
2.3 Model (model.py)¶
PopulationModel(likelihoods, prior=None, *, selection=None, selection_mode=None, bins=None)
.K .N .W .dim .kind .bins .likelihoods .prior (bound) .selection .joint
.log_density_unconstrained(x) / .log_density_unconstrained_and_grad(x) # exact, with Jacobian
.to_value(x) / .to_rho(x) / .from_value(value)
.log_likelihood(value) # includes the selection normaliser
.log_prior(value) / .log_posterior(value) # densities of the fractions or rates
.prior_transform(u)
.responsibilities(value, idx=None) / .expected_counts(value)
.kkt_gap(value) / .missing_information(value)
.selection_at(x) / .response_at(x) # an inferred selection or response
2.4 Engines (samplers/)¶
Engine(model, *, rng=None, progress=False, **config)
.run(n_samples=None, **overrides) -> PopulationResult | PointEstimate
.sample(n_samples) -> generator of per-iteration draws (NUTS/Gibbs)
.state # resumable chain state
NUTS
Round-robin over
n_chains. Each chain carries its own position, step size, metric and adaptation state.The tree-building core is pure functions taking
(logp_grad_fn, metric).Adaptation follows Stan’s windows; metric
"auto","diag"or"dense". Logistic priors are partially centred and start a curvature-based metric early in warm-up (spec §6.2).Chains are initialised from the posterior mode plus jitter, or from a user-supplied
init.
Gibbs
Requires
prior.conjugate.Each iteration: label step, then optional selection augmentation (negative binomial), then
posterior_sample.Optional stored labels (
store_labels=Trueor an index array).
MAP
Dirichlet priors with α ≥ 1 (flat only with selection) and
GammaRateswith shapes ≥ 1: mix-SQP.Other conjugate priors with a bounded density: SQUAREM-accelerated EM on ρ (or λ). The M-step uses
map_from_counts; selection adds the expected number of missed objects.Otherwise: L-BFGS-B on
-log_density_unconstrainedin unconstrained coordinates, with an analytic gradient.Reports the KKT gap.
Laplace
Finds the mode in unconstrained coordinates by L-BFGS, warm-started from MAP/EM.
The Hessian is symmetrised central differences of the exact gradient (
samplers/_curvature.py) for every prior.Draws from the Gaussian, then optionally applies PSIS importance resampling and records k̂.
VB: CAVI over the conjugate prior’s variational state.
Bootstrap: reweights objects (multinomial or Dirichlet weights) and calls MAP repeatedly.
2.5 Results (results.py)¶
PopulationResult stores:
draws in unconstrained space, together with the transformed
samples(fractions) andrates;the chain layout,
log_density_unconstrained,method,approximateandinfo;a reference to the model, used for individual-object methods.
Derived quantities are computed lazily and cached. PointEstimate holds the MAP outputs. Both use the Summary table class for summary().
3. Milestones (each lands with its tests and docs stubs)¶
M |
Content |
Exit criteria |
|---|---|---|
M0 |
Scaffolding: pyproject, layout, warnings, utilities, CI workflow, pre-commit, docs skeleton |
|
M1 |
|
tests 2.1–2.3 green |
M2 |
Priors: Dirichlet family, logistic family, rates, hyperpriors |
test 2.4 green |
M3 |
|
test 2.5 green (finite-difference gradients for every family) |
M4 |
Engines: MAP, NUTS, Gibbs, Laplace, VB, Bootstrap |
tests 2.6 green; prototype benchmarks reproduced |
M5 |
Results, diagnostics, |
tests 2.7–2.9 green |
M6 |
|
tests 2.10 green; slow 2.11 green locally |
M7 |
Plotting |
smoke tests (Agg backend) |
M8 |
Docs: theory, user guide, tutorials (executed), API reference, README |
docs build with executed notebooks, no warnings |
M9 |
Independent review, fixes, benchmarks, polish |
review findings resolved; full suite and docs green |
4. Engineering conventions¶
Style
NumPy-style docstrings with
Examples, cross-referencing equations intheory.md.ruff formatting with line length 99.
Type hints on public functions, with
from __future__ import annotations.
Numerics
Never form
N × Ktemporaries outside chunked loops in sampler hot paths.Guard
log(0).Use
float64throughout.Draw all randomness from an explicit
Generator; chains get independent children viarng.spawn(n).
Errors and warnings
Validate public inputs early with actionable messages.
Engines never swallow non-finite log-probabilities. They raise with the offending state summarised, except inside NUTS, where they count as divergences.
Dependencies: core
numpy>=2.1,scipy>=1.14.1andthreadpoolctl>=3.1. matplotlib, xarray, pandas and tqdm are imported lazily inside the functions that need them;binference.plottingandbinference.ioload on first use.
5. Documentation plan¶
Toolchain: Sphinx, MyST-NB, pydata-sphinx-theme, numpydoc, sphinx-copybutton and sphinx-design.
Tutorials are MyST Markdown notebooks under
docs/tutorials/, executed at build time with caching. Each runs in under 2 minutes on a laptop CPU.Pages:
Getting started: install, then the quickstart from the README, expanded.
User guide:
inputs and conventions (likelihoods vs posteriors, interim priors, masses vs densities, samples, adapters);
priors;
selection effects and rates;
inference engines and how to choose one;
results (population vs realised counts, individual objects, functionals);
diagnostics and validation;
performance;
pitfalls (stacking, interim priors, unequal bins, VB under-dispersion, binning choice).
Theory: the normative maths from spec §3, in clean notation, with derivations (EM as iterated stacking, the selection augmentation, tree densities).
Tutorials:
Quickstart: n(z) from Gaussian photo-z likelihoods, comparing stacking and binference, with individual shrinkage.
Inputs and conventions: posteriors with interim priors, samples, unequal bins (wrong vs right).
Choosing an engine: NUTS vs Gibbs vs Laplace vs VB vs MAP, accuracy against speed, with diagnostics.
Priors and smoothness: Dirichlet α, generalised Dirichlet, Pólya tree, random-walk and GP priors with hyperparameter inference.
Selection effects and rates.
Categories and unfolding: classifier quantification and a response-matrix example.
Validating your setup with SBC and coverage.
(should) A two-dimensional CMD-like population.
API reference (autosummary).
Design notes: spec, plans, landscape report.
Changelog, contributing, citing.
6. CI (GitHub Actions)¶
Job |
Contents |
|---|---|
|
the pre-commit hooks (ruff check and format, file hygiene) |
|
ubuntu, macOS and Windows × Python 3.13 and 3.14; |
|
ubuntu, Python 3.13, the lowest versions |
|
ubuntu, Python 3.13, weekly, by hand or on a pull request labelled |
|
the README, installation and user-guide code ( |
|
|
7. Risks and mitigations¶
Risk |
Mitigation |
|---|---|
NUTS bugs, which are subtle |
Port the validated prototype; test it on Gaussian, Dirichlet and quadrature targets; add SBC and cross-engine tests |
Slow mixing for broad PDFs or many bins |
Document it; prefer NUTS; offer smoothness priors; report the missing-information fraction |
Python overhead in NUTS for small N |
Acceptable for v0.1; a JAX backend is on the roadmap |
Memory for large dense L |
Sparse storage, |
Ambiguous user inputs |
Explicit representation and interim-prior arguments, plus sanity warnings |