This file provides guidance to AI coding agents working with code in this repository.
Dependencies are managed with uv (uv.lock is committed). Dev tooling lives in PEP 735 dependency groups, not extras, so the pip install -e '.[dev,...]' line in Makefile/CONTRIBUTING.md is out of date. Mirror CI instead:
uv sync --extra cpu --group dev --group test # core development
uv sync --extra cpu --group dev --group test --group docs --group examples # + docs / examplesPrefix commands with uv run (or activate .venv). funsor is pulled from git ([tool.uv.sources]).
make lint # ruff check, ruff format --check, license-header check, ty check
make format # adds license headers, ruff format, ruff check --fix
make test # lint, then pytest -v test (slow: the full suite)
make docs # sphinx html -> docs/build/html (needs pandoc)
make doctest # docstring tests via sphinx, forced to CPU
python -m doctest -v README.md # README snippets are tested in CISingle test / subset:
pytest -vs test/test_distributions.py::test_log_prob -k Gompertz
pytest -vs -n auto test/infer/test_svi.py # needs pytest-xdistVariants CI runs, which matter when touching the corresponding code:
JAX_ENABLE_X64=1 pytest -vs test/infer/test_mcmc.py -k x64 # double precision
XLA_FLAGS="--xla_force_host_platform_device_count=2" pytest -vs test/infer/test_mcmc.py -k "chain or pmap or vmap" # multi-chain
JAX_ENABLE_CUSTOM_PRNG=1 pytest -vs test/infer/test_mcmc.py # typed PRNG keys
JAX_CHECK_TRACER_LEAKS=1 pytest -vs test/infer/test_mcmc.py::test_chain_inside_jit
CI=1 XLA_FLAGS="--xla_force_host_platform_device_count=2" pytest -vs -k test_example # runs examples/ scriptsCI shards the suite as: everything except test/infer and test/contrib with -k "not test_example" (modeling); test/contrib + test/infer (inference); -k test_example (examples). test/contrib/test_nested_sampling.py requires JAX_ENABLE_X64=1.
Benchmarks (compared against the merge base on PRs): python -m benchmarks.runner --list, NUMPYRO_BENCH_QUICK=1 python -m benchmarks.runner --suite handlers -o smoke.json. See benchmarks/README.md.
- Warnings are errors.
pyproject.tomlsets pytestfilterwarnings = ["error", ...]; a newDeprecationWarning/UserWarningon a tested path fails the test. Usepytest.warnsor fix the source. - License headers. Every non-empty
.pyfile must begin with the two-line Pyro copyright /SPDX-License-Identifier: Apache-2.0header.make license(ormake format) adds it. - Type annotations are enforced selectively. Ruff's
ANNrules apply only to the typed modules listed in[tool.ruff.lint.per-file-ignores](diagnostics.py,handlers.py,optim.py,patch.py,primitives.py,infer/elbo.py,distributions/distribution.py), andty checkcovers the include list in[tool.ty.src](all ofnumpyro/distributions, most ofnumpyro/infer, several contrib packages). Code in those paths must be annotated and type-check; shared aliases are innumpyro/_typing.py. - Import order is ruff-isort with a custom
known-jaxsection (flax,jax,optax,tensorflow_probability) placed between third-party and first-party,force-sort-within-sections. Letmake formatdo it. - pre-commit additionally runs
codespell,yamlfmt, andtombi(TOML formatting) —pyproject.tomledits must stay tombi-formatted. test/conftest.pyforces the CPU platform, reseeds withset_rng_seed(0)before every test, enables x64 whenJAX_ENABLE_X64is set, and assertsjax.live_arrays()is empty before the first test — so test modules must not create JAX arrays at import/collection time (build parametrize data with NumPy).
NumPyro is Pyro's modeling API re-implemented on JAX. Three layers, each usable independently:
A model is a plain Python function calling numpyro.sample, param, deterministic, plate, factor, etc. Each primitive builds a message dict (type, name, fn, args, kwargs, value, is_observed, scale, mask, cond_indep_stack, infer, ...) and passes it through apply_stack over the global _PYRO_STACK of Messengers: process_message runs from the top of the stack down, then the default sampler runs if no handler set value, then postprocess_message runs back up. With an empty stack, sample just calls the distribution (which is why a rng_key is then required).
Handlers (trace, seed, substitute, condition, uncondition, block, replay, scale, mask, reparam, collapse, do, lift, scope, infer_config) are Messenger subclasses used as context managers or function wrappers. All inference is built by composing them, e.g. trace(substitute(seed(model, key), params)).get_trace(...). Because JAX has no global RNG, randomness only enters through the seed handler, which splits its key per sample site. plate is itself a Messenger: it records a CondIndepStackFrame, broadcasts fn batch shapes via expand, and sets scale for subsampling.
Handlers execute Python side effects, so they run at trace time: inference code traces the model once under jit to turn it into a pure function. Python control flow over traced values inside models must go through contrib/control_flow (scan, cond), which are handler-aware.
Mirrors torch.distributions: Distribution (distribution.py) with batch_shape/event_shape, arg_constraints, support, reparametrized_params, sample(key, sample_shape), log_prob, plus wrappers Independent (.to_event), MaskedDistribution (.mask), ExpandedDistribution (.expand), TransformedDistribution, Delta, Unit, ImproperUniform.
- Every distribution, transform, and constraint is a JAX pytree, registered automatically in
__init_subclass__. Array parameters are leaves (derived fromarg_constraintspluspytree_data_fields); anything static must be listed inpytree_aux_fields. Putting array-valued data in aux fields breaksjit/vmap(hash/equality on tracers) — see the recent refactor keeping truncation bounds out of the static treedef.batch_util.py(vmap_over,promote_batch_shape) has per-class registrations needed for distributions to pass throughvmap. constraints.py→transforms.py:biject_to(constraint)is a registry mapping each support to a bijection from unconstrained space. HMC, autoguides andparam(constraint=...)all rely on it, so a new constraint needs abiject_toregistration.- Argument validation is "omnistaging":
validate_argschecks work both eagerly and underjit(where they can only warn/NaN rather than raise). kl.pyusesmultipledispatchforkl_divergence(p, q);conjugate.py,truncated.py,censored.py,mixtures.py,directional.py,copula.py,flows.pyhold the families their names suggest.
Adding a distribution touches several places: the class (in continuous.py/discrete.py/...), export in distributions/__init__.py, an entry in docs/source/distributions.rst, and test cases in test/test_distributions.py — add T(dist_cls, *params) rows to the CONTINUOUS/DISCRETE/DIRECTIONAL lists and, if SciPy has an equivalent, a mapping in _DIST_MAP. The generic parametrized tests (shapes, log_prob vs SciPy, gradients, cdf/icdf, mean/var, constraints, pytree/vmap, expand, sample goodness-of-fit) then run automatically. vmap-related tests may need a vmap_over registration in batch_util.py. Recent distribution docstrings follow a "mathematical details" format with LaTeX pdf/support/moments and linked citations (issue gh-2187).
infer/util.py is the bridge between models and algorithms: log_density (trace + substitute → joint log prob honoring scale/mask), initialize_model (finds valid initial params via the strategies in initialization.py, and returns potential_fn, postprocess_fn, the unconstraining transforms), transform_fn/constrain_fn/unconstrain_fn, potential_energy, and the Predictive / log_likelihood utilities. Algorithms work in unconstrained space and map back with biject_to transforms.
- MCMC (
mcmc.py):MCMCdrives anyMCMCKernel(init/sample/postprocess_fn, state is a namedtuple pytree). It owns warmup/sample collection viafori_collect, progress bars, andchain_method=parallel(pmap; needs multiple devices, hence theXLA_FLAGStests),vectorized(vmap), orsequential. Kernels:HMC/NUTS(hmc.py, with the functional core — leapfrog, tree building, step-size and mass-matrix adaptation — inhmc_util.py),SA,BarkerMH, ensemble samplersAIES/ESS(ensemble.py), and the Gibbs family —Gibbs/CustomGibbs/DiscreteGibbsingibbs.py, withHMCGibbs/DiscreteHMCGibbs(subclasses ofGibbs) andHMCECSinhmc_gibbs.py, plusMixedHMCinmixed_hmc.py— composable kernels that wrap inner kernels. - SVI (
svi.py,elbo.py,autoguide.py):SVI(model, guide, optim, loss)with functionalinit/update/runover anSVIState. ELBOs (Trace_ELBO,TraceMeanField_ELBO,TraceGraph_ELBO,TraceEnum_ELBO,RenyiELBO) replay the model against a guide trace. Autoguides build guides from the model's latent sites usinginitialize_model-style setup.numpyro/optim.pywrapsjax.example_libraries.optimizersand adaptsoptaxtransforms to the sameinit/update/get_paramsinterface. - Reparameterization (
reparam.py) is applied throughhandlers.reparam, not inside distributions. - Discrete latents: enumeration is delegated to
funsorvianumpyro/contrib/funsor(enum,config_enumerate,infer_discrete, enum-awarelog_density).funsoris an optional dependency — keep its imports lazy/inside contrib.
numpyro/contrib/: optional-dependency integrations, each imported lazily —module.py(flax linen/nnx and equinox modules viaflax_module/nnx_module/eqx_moduleand theirrandom_*_moduleBayesian variants),tfp/(wrap TFP distributions and MCMC kernels),einstein/(SteinVI),hsgp/(Hilbert-space GP approximations),stochastic_support/(DCC),nested_sampling.py(jaxns),control_flow/,render.py(render_modelvia graphviz),ecs_proxies.py.numpyro/compat/: a Pyro-style API shim, validated againstpyro-apiintest/pyroapi.numpyro/ops/:indexing(Vindex),provenance(dependency tracking used byinfer/inspect.pyandrender_model),pytree.numpyro/util.py:fori_loop/fori_collect/cond/while_loopwrappers that fall back to plain Python control flow inside thecontrol_flow_prims_disabled()context (used for debugging and by tests, usually together withjax.disable_jit()),soft_vmap,set_host_device_count,enable_x64,set_platform, and model-checking helpers (format_shapes,check_model_guide_match).examples/andnotebooks/source/are rendered into the docs (sphinx-gallery / nbsphinx);test/test_examples.pyexecutes each script listed in itsEXAMPLESlist with small arguments, so a new example must be added there.- New public API needs an entry in the matching
docs/source/*.rstfile to appear in the docs and be doctested.
- Version lives in
numpyro/version.py(scripts/update_version.pybumps it everywhere). - Tests compare against SciPy / analytic results with tolerances chosen for float32 (the default); use
JAX_ENABLE_X64=1only where a test is explicitly x64. - PRs target
masteronpyro-ppl/numpyro. Recent commit subjects use a conventional-style prefix, e.g.fix(distributions): ...,doc(gh-2187): ...,refactor: ....