xrtoolz.prob — Architecture¶
Package layout¶
src/xrtoolz/prob/
├── __init__.py # Public re-exports
├── _src/ # Layer-0 pure functions
│ ├── distributions.py # Normal, HalfNormal, LogNormal, Beta, Poisson, MultivariateNormal, ...
│ ├── _base.py # DistributionBridge — common label-bookkeeping for all dists
│ ├── plate.py # plate() context manager
│ ├── primitives.py # sample(), log_prob(), prior_predictive(), posterior_predictive()
│ ├── io.py # to_dataset() — convert raw numpyro samples to xr.Dataset
│ └── _shape.py # Translate dim names <-> numpyro batch/event/sample axes
├── operators.py # Layer-1: Sample, LogProb, …
└── pyrox.py # Thin re-exports of PyroxModule, PyroxParam, PyroxSample, pyrox_method
Layer stack¶
┌────────────────────────────────────────────────────────────────────┐
│ Layer 2 — Models │
│ User's numpyro / pyrox model functions and modules │
│ Composed via numpyro handlers + pyrox PyroxModule │
├────────────────────────────────────────────────────────────────────┤
│ Layer 1 — Operators │
│ Sample(dist, key=…, sample_shape=…) -> DataArray | Dataset │
│ LogProb(dist, *, var=…) -> 0-d DataArray │
│ PriorPredictive(model, key=…) -> Dataset │
│ PosteriorPredictive(model, posterior_samples, key=…) -> Dataset │
├────────────────────────────────────────────────────────────────────┤
│ Layer 0 — Distributions and primitives │
│ DistributionBridge subclasses: Normal, HalfNormal, MVN, … │
│ .sample(key, sample_shape=…) -> DataArray │
│ .log_prob(x: DataArray) -> jax scalar │
│ .to_numpyro() -> numpyro.distributions.Distribution │
│ Module-level: sample(), plate(), to_dataset() │
├────────────────────────────────────────────────────────────────────┤
│ Bridge core │
│ DataArray params -> numpyro args: │
│ - introspect dims │
│ - reconcile via dim-name alignment │
│ - convert to jax arrays (broadcast-compatible) │
│ Sample shape interpretation: │
│ - ("draw=100",) -> prepend dim 'draw' with size 100 │
│ Output reshape: │
│ - assemble result dims as (*sample_shape, *param_dims) │
│ - forward coords from inputs │
├────────────────────────────────────────────────────────────────────┤
│ Backend │
│ numpyro, pyrox, jax │
└────────────────────────────────────────────────────────────────────┘
The DistributionBridge base¶
class DistributionBridge:
"""Base class for labeled wrappers around numpyro distributions.
Subclasses register the underlying numpyro distribution class and
declare which constructor kwargs are "parameter-shaped" (can be
DataArrays) versus "scalar-shaped" (always floats/0-d arrays).
The bridge handles:
- dim-name reconciliation across DataArray-typed parameters;
- conversion to JAX arrays for numpyro;
- parameter broadcasting along the union of declared dims;
- sample-shape interpretation and output relabeling.
Subclasses must not override sample / log_prob / to_numpyro —
they should only declare the parameter set.
"""
numpyro_cls: type[numpyro.distributions.Distribution]
param_names: tuple[str, ...]
event_param: str | None = None # For multivariate (e.g. "cov")
def __init__(self, **params: xr.DataArray | float | jax.Array) -> None: ...
def to_numpyro(self) -> numpyro.distributions.Distribution: ...
def sample(
self,
key: jax.Array,
*,
sample_shape: tuple[str, ...] | tuple[int, ...] = (),
) -> xr.DataArray: ...
def log_prob(self, x: xr.DataArray) -> jax.Array: ...
@property
def batch_dims(self) -> tuple[str, ...]:
"""Union of parameter dim names, in canonical order."""
Subclassing is one-line per distribution:
class Normal(DistributionBridge):
numpyro_cls = numpyro.distributions.Normal
param_names = ("loc", "scale")
class MultivariateNormal(DistributionBridge):
numpyro_cls = numpyro.distributions.MultivariateNormal
param_names = ("loc", "covariance_matrix")
event_param = "covariance_matrix"
Parameter reconciliation¶
When a Normal(loc=loc_da, scale=scale_da) is constructed, the
bridge runs:
1. Collect parameter DataArrays; non-DataArray params become 0-d.
2. Compute the union of dim names, preserving first-appearance order:
batch_dims = ("month", "lat", "lon")
3. For each DataArray param:
- assert its dims are a subset of batch_dims;
- check shared-dim coords match (or raise CoordMismatch);
- transpose to the canonical order (with missing dims left out).
4. Convert each param to a jax array, then ``jnp.broadcast_to`` to
the union shape.
5. Stash (param_jax_arrays, batch_dims, coords) on the bridge for
downstream sample / log_prob / to_numpyro calls.
The reconciled batch shape is the bridge's source of truth for both
.sample() output dims and .to_numpyro() distribution batch_shape.
Sample-shape interpretation¶
def _parse_sample_shape(
sample_shape: tuple[str, ...] | tuple[int, ...],
) -> tuple[tuple[str, ...], tuple[int, ...]]:
"""Return (dim_names, sizes). Accepts:
- tuple of "name=size" tokens -> named output dims
- tuple of ints -> auto-named "sample_0", "sample_1", ... with warning
- empty -> ((), ())
"""
The sampled DataArray's dims are (*sample_dim_names, *batch_dims,
*event_dims) — the leading sample axes carry user-named draws, then
the broadcast batch axes, then any event axes for multivariate
distributions (MultivariateNormal → ("event",), Dirichlet →
("category",), etc.). Univariate distributions have an empty
event_dims so the tail collapses to (*sample_dim_names,
*batch_dims). Coords are forwarded from the parameter DataArrays
(for batch dims) and from the multivariate parameter (for event
dims, when present); sample-dim coords come from whatever the caller
passed.
plate semantics¶
@contextmanager
def plate(
name: str,
size_or_array: int | xr.DataArray | None = None,
*,
subsample_size: int | None = None,
) -> Iterator[numpyro.plate]:
"""Open a numpyro plate keyed by a dim name.
Args:
name: dim name. Inside the context, distributions whose
parameters carry this dim treat it as conditionally
independent.
size_or_array: either an int (passed to numpyro.plate as
``size=``), a DataArray (size taken from its ``name``
dim), or None (size inferred from the first distribution
that carries the dim).
subsample_size: optional minibatch size (passed through to
numpyro).
Yields:
The numpyro plate object, so the caller can nest it normally.
"""
The bridge resolves dim= numpyro plate kwargs from the dim's
position in the canonical batch_dims of distributions inside the
context.
to_dataset — posterior IO¶
def to_dataset(
samples: Mapping[str, jax.Array],
*,
dims_per_site: Mapping[str, tuple[str, ...]],
coords: Mapping[str, xr.DataArray] | None = None,
chain_dim: str = "chain",
draw_dim: str = "draw",
) -> xr.Dataset:
"""Convert raw numpyro MCMC / SVI samples to an xarray Dataset.
Args:
samples: ``mcmc.get_samples()`` or ``svi.get_samples()`` output.
dims_per_site: dim names per sample site, in the order numpyro
laid out batch axes. The bridge prepends (chain, draw) as
needed based on the array shape.
coords: optional coords to attach.
chain_dim, draw_dim: names for the leading axes.
Returns:
Dataset with one variable per site, dims named per
``dims_per_site``.
"""
This is the canonical exit point from a numpyro inference run back to the xarray world.
Operator class skeleton¶
class Sample(Operator):
"""Draw from a distribution inside a Sequential / Graph."""
def __init__(
self,
distribution: DistributionBridge,
*,
key: jax.Array,
sample_shape: tuple[str, ...] = (),
) -> None:
self.distribution = distribution
self.key = key
self.sample_shape = sample_shape
def _apply(self, _ds: xr.Dataset | None = None) -> xr.DataArray:
return self.distribution.sample(self.key, sample_shape=self.sample_shape)
Sample is a generator operator (no input). LogProb is a
single-input operator that returns a 0-d DataArray.
pyrox integration¶
pyrox.py re-exports the pyrox primitives:
Users importing from xrtoolz.prob get the pyrox surface directly:
from xrtoolz.prob import PyroxModule, pyrox_method, Normal
class MyPrior(PyroxModule):
pyrox_name = "MyPrior"
@pyrox_method
def __call__(self, climatology):
return self.pyrox_sample(
"field",
Normal(loc=climatology, scale=0.1).to_numpyro(),
)
No wrapper, no shim — just convenience re-exports.
Dependencies¶
| Dep | Version | Purpose |
|---|---|---|
numpyro |
>=0.14 | Distributions, plates, handlers, inference. |
pyrox |
latest | PyroxModule, Parameterized, etc. |
gaussx |
latest | For MultivariateNormal cov; bridge to xrtoolz.linalg. |
jax |
latest | Array backend. |
Lazy-imported per the xrtoolz D4 pattern. import xrtoolz does not
pull numpyro or pyrox.
Error policy¶
| Condition | Behaviour |
|---|---|
| Two DataArray params have unequal coords on a shared dim | Raise CoordMismatch from xrtoolz.prob.errors. |
| Parameter has a dim numpyro doesn't know what to do with | Raise ValueError naming the offending dim. |
sample_shape is an int tuple |
Auto-name dims and warnings.warn once per call site. |
plate(name, ...) opened with no inner distribution carrying name |
Raise ValueError — the plate would be vacuous. |
log_prob(x) with x.dims incompatible with distribution batch_dims |
Raise DimMismatch. |
Integration with existing xrtoolz¶
xrtoolz.einx.rearrangereshapes parameter arrays before they're fed into distributions.xrtoolz.linalg.NamedOperatoris the covariance type forxrtoolz.prob.MultivariateNormal.SampleandLogProbcompose intoSequential/Graphlike any other operator.xrtoolz.signature.Signatureextends naturally — distribution batch_dims contribute to output sigs.