Converting BlackJAX output to DataTree#
DataTree is the data format ArviZ relies on.
This page covers multiple ways to generate a DataTree from BlackJAX sampling output.
See also
Conversion from Python, numpy or pandas objects
DataTree for Exploratory Analysis of Bayesian Models for an overview of
InferenceDataand its role within ArviZ.Converting NumPyro objects to DataTree for a higher-level PPL that also runs on JAX.
BlackJAX is a low-level sampling library built on
JAX. Unlike NumPyro or PyMC it has no probabilistic model abstraction — users write an explicit
log-probability function and call samplers directly. from_blackjax therefore works with the raw
arrays BlackJAX returns rather than a higher-level inference object.
We will use the classic eight-schools example throughout.
import arviz_base as az
import blackjax
import jax
import jax.numpy as jnp
import numpy as np
# Observed data
J = 8
y_obs = jnp.array([28.0, 8.0, -3.0, 7.0, -1.0, 1.0, 18.0, 12.0])
sigma = jnp.array([15.0, 10.0, 16.0, 11.0, 9.0, 11.0, 10.0, 18.0])
school_names = [
"Choate", "Deerfield", "Phillips Andover", "Phillips Exeter",
"Hotchkiss", "Lawrenceville", "St. Paul's", "Mt. Hermon",
]
Log-probability function#
BlackJAX requires an explicit log-probability function. We use a non-centred
parameterisation: theta = mu + exp(log_tau) * theta_tilde. The position is a plain
dict with keys mu, log_tau, and theta_tilde.
def log_prob(position):
"""Log-joint of the non-centred eight-schools model."""
mu = position["mu"]
log_tau = position["log_tau"]
tau = jnp.exp(log_tau)
theta_tilde = position["theta_tilde"]
theta = mu + tau * theta_tilde
# priors
lp = -0.5 * (mu / 5.0) ** 2 # Normal(0, 5)
lp += log_tau - jnp.log1p((log_tau / jnp.log(5.0)) ** 2) # Half-Cauchy(0, 5) on tau
lp += -0.5 * jnp.sum(theta_tilde ** 2) # Normal(0, 1) per school
# likelihood
lp += -0.5 * jnp.sum(((y_obs - theta) / sigma) ** 2)
return lp
init_position = {
"mu": 0.0,
"log_tau": 0.0,
"theta_tilde": jnp.zeros(J),
}
Single-chain NUTS#
The standard BlackJAX pattern: use window_adaptation to tune the step size and mass
matrix, then run NUTS via jax.lax.scan. The sampler returns a (state, info) tuple
at every step; state.position contains the parameter dict and info contains
diagnostics such as acceptance_rate and is_divergent.
num_warmup = 500
num_samples = 1000
rng_key = jax.random.PRNGKey(0)
# --- warmup / adaptation ---
warmup = blackjax.window_adaptation(blackjax.nuts, log_prob)
rng_key, warmup_key = jax.random.split(rng_key)
(state, parameters), _ = warmup.run(warmup_key, init_position, num_steps=num_warmup)
# --- sampling ---
kernel = blackjax.nuts(log_prob, **parameters)
def one_step(state, rng_key):
state, info = kernel.step(rng_key, state)
return state, (state, info)
rng_key, sample_key = jax.random.split(rng_key)
draw_keys = jax.random.split(sample_key, num_samples)
_, (states, infos) = jax.lax.scan(one_step, state, draw_keys)
Basic conversion#
Pass states.position as posterior and infos as info. Single-chain output has
shape (n_draws, *event_shape); from_blackjax inserts a chain dimension of size 1
automatically when sample_dims includes "chain" (the default).
idata = az.from_blackjax(
posterior=states.position,
info=infos,
observed_data={"y": np.array(y_obs)},
coords={"school": school_names},
dims={"theta_tilde": ["school"]},
)
idata
The posterior group has dimensions (chain, draw). The sample_stats group
contains standard ArviZ names (acceptance_rate, diverging, energy,
n_steps, step_size, tree_depth) mapped from the BlackJAX info fields.
print(idata.posterior.dims)
print(list(idata.sample_stats.data_vars))
Adding reached_max_tree_depth#
Pass the tree depth cap you used during sampling to get a boolean flag in
sample_stats that indicates when the sampler hit the limit.
idata_mtd = az.from_blackjax(
posterior=states.position,
info=infos,
max_tree_depth=10,
)
print("reached_max_tree_depth" in idata_mtd.sample_stats)
print(idata_mtd.sample_stats["reached_max_tree_depth"].values[0])
Multi-chain NUTS via jax.vmap#
BlackJAX has no built-in chain management. The idiomatic approach is to
vmap a function that runs a single chain over a batch of random keys.
The resulting arrays have shape (n_chains, n_draws, *event_shape).
Pass num_chains so from_blackjax can identify the leading chain axis.
num_chains = 4
rng_key = jax.random.PRNGKey(1)
chain_keys = jax.random.split(rng_key, num_chains)
def run_chain(key):
warmup = blackjax.window_adaptation(blackjax.nuts, log_prob)
(state, parameters), _ = warmup.run(key, init_position, num_steps=num_warmup)
kernel = blackjax.nuts(log_prob, **parameters)
def one_step(state, rng_key):
state, info = kernel.step(rng_key, state)
return state, (state, info)
draw_keys = jax.random.split(jax.random.fold_in(key, 1), num_samples)
_, (states, infos) = jax.lax.scan(one_step, state, draw_keys)
return states, infos
mc_states, mc_infos = jax.vmap(run_chain)(chain_keys)
idata_mc = az.from_blackjax(
posterior=mc_states.position,
info=mc_infos,
num_chains=num_chains,
observed_data={"y": np.array(y_obs)},
coords={"school": school_names},
dims={"theta_tilde": ["school"]},
)
idata_mc
print(idata_mc.posterior.sizes)
Prior and prior predictive#
BlackJAX has no prior-sampling utility, so prior samples must be generated
separately (e.g. with NumPy or JAX). Pass them as a dict to prior.
Variables whose names appear in posterior are placed in the prior group;
any remaining variables (not sampled during NUTS) are placed in prior_predictive.
rng = np.random.default_rng(42)
n_prior = 1000
prior_mu = rng.normal(0, 5, size=(1, n_prior))
prior_log_tau = rng.standard_cauchy(size=(1, n_prior))
prior_tau = np.exp(prior_log_tau)
prior_theta_tilde = rng.standard_normal(size=(1, n_prior, J))
prior_theta = prior_mu[..., None] + prior_tau[..., None] * prior_theta_tilde
# y_hat is NOT a parameter in the posterior -> goes to prior_predictive
prior_y_hat = rng.normal(prior_theta, np.array(sigma))
prior_dict = {
"mu": prior_mu,
"log_tau": prior_log_tau,
"theta_tilde": prior_theta_tilde,
"y_hat": prior_y_hat,
}
idata_prior = az.from_blackjax(
posterior=states.position,
info=infos,
prior=prior_dict,
observed_data={"y": np.array(y_obs)},
coords={"school": school_names},
dims={"theta_tilde": ["school"], "y_hat": ["school"]},
)
idata_prior
# mu, log_tau, theta_tilde -> prior (overlap with posterior)
# y_hat -> prior_predictive (not in posterior)
print("prior:", list(idata_prior.prior.data_vars))
print("prior_predictive:", list(idata_prior.prior_predictive.data_vars))
sample_dims and LOO compatibility#
By default from_blackjax uses sample_dims=["chain", "draw"], matching the
ArviZ convention and making the output immediately compatible with functions like
az.loo and az.rhat that require a chain dimension.
If you prefer a flat sample representation — e.g. after thinning many chains into
one array — pass sample_dims=["sample"]. No chain dimension will be inserted.
idata_flat = az.from_blackjax(
posterior=states.position,
info=infos,
sample_dims=["sample"],
)
print(idata_flat.posterior.dims)
print(idata_flat.posterior.sizes)
If you later need chain and draw dimensions (for example to call az.loo),
you can add them with map_over_datasets and expand_dims:
from arviz_base import map_over_datasets
idata_loo_ready = map_over_datasets(
lambda ds: ds.expand_dims(chain=1, axis=0).rename({"sample": "draw"}),
idata_flat,
)
print(idata_loo_ready.posterior.dims)
NamedTuple and bare-array positions#
from_blackjax accepts any of the three position formats BlackJAX can produce:
dict (most common) — keys become variable names.
NamedTuple — field names become variable names.
bare array — stored under the variable name
"x".
from collections import namedtuple
Position = namedtuple("Position", ["mu", "log_tau", "theta_tilde"])
nt_position = Position(
mu=states.position["mu"],
log_tau=states.position["log_tau"],
theta_tilde=states.position["theta_tilde"],
)
idata_nt = az.from_blackjax(posterior=nt_position)
print(list(idata_nt.posterior.data_vars)) # ['mu', 'log_tau', 'theta_tilde']
# Bare array -- stored under 'x'
idata_bare = az.from_blackjax(posterior=states.position["mu"])
print(list(idata_bare.posterior.data_vars)) # ['x']
Watermark#
%load_ext watermark
%watermark -n -u -v -iv -w