Skip to content

Inference

Pure kernels, adaptation utilities and compiled runners. See Inference for how they fit together.

Runners

probjax.inference.run

run(key, kernel, state, params, num_steps, *, args=None, adaptor=None, collect=None, thin=1, progress=False)

Run num_steps transitions and keep every thin-th.

Parameters:

Name Type Description Default
key RngKey

Random key.

required
kernel Kernel

The kernel to run.

required
state Any

Initial kernel state.

required
params Any

Kernel parameters.

required
num_steps int

Number of transitions; must be a multiple of thin.

required
args tuple[Any, ...] | None

Per-step extra arguments (e.g. minibatches), a tuple whose leaves have a leading axis of length num_steps.

None
adaptor Adaptor | None

Adapts params after every transition. The returned params are the adaptor's finalized ones.

None
collect tuple[str, ...] | None

State or info fields to record at every kept step; "state" and "info" record them whole. The default records the position of MCMC-style states and nothing for population states such as SMC's.

None
thin int

Keep every thin-th transition.

1
progress bool | tuple[str, ...]

Show a progress bar. True shows the log density and acceptance rate; a tuple of names shows those state/info fields.

False

Returns:

Name Type Description
A RunResult

class:RunResult whose trace maps each name in collect to

RunResult

an array with num_steps // thin rows.

Source code in probjax/inference/runner.py
@partial(
    jax.jit,
    static_argnames=("kernel", "num_steps", "adaptor", "collect", "thin", "progress"),
)
def run(
    key: RngKey,
    kernel: Kernel,
    state: Any,
    params: Any,
    num_steps: int,
    *,
    args: tuple[Any, ...] | None = None,
    adaptor: Adaptor | None = None,
    collect: tuple[str, ...] | None = None,
    thin: int = 1,
    progress: bool | tuple[str, ...] = False,
) -> RunResult:
    """Run ``num_steps`` transitions and keep every ``thin``-th.

    Args:
        key: Random key.
        kernel: The kernel to run.
        state: Initial kernel state.
        params: Kernel parameters.
        num_steps: Number of transitions; must be a multiple of ``thin``.
        args: Per-step extra arguments (e.g. minibatches), a tuple whose
            leaves have a leading axis of length ``num_steps``.
        adaptor: Adapts ``params`` after every transition. The returned
            params are the adaptor's finalized ones.
        collect: State or info fields to record at every kept step;
            ``"state"`` and ``"info"`` record them whole. The
            default records the position of MCMC-style states and nothing
            for population states such as SMC's.
        thin: Keep every ``thin``-th transition.
        progress: Show a progress bar. ``True`` shows the log density and
            acceptance rate; a tuple of names shows those state/info fields.

    Returns:
        A :class:`RunResult` whose ``trace`` maps each name in ``collect`` to
        an array with ``num_steps // thin`` rows.
    """
    if num_steps % thin:
        raise ValueError(f"num_steps={num_steps} must be a multiple of thin={thin}.")
    num_kept = num_steps // thin
    keys = jax.random.split(key, (num_kept, thin))
    if args is None:
        args = ()
    args = jax.tree.map(lambda a: a.reshape((num_kept, thin) + a.shape[1:]), args)
    adaptor_state = None if adaptor is None else adaptor.init(state, params)
    if collect is None:
        collect = ("position",) if hasattr(state, "position") else ()

    names = DEFAULT_PROGRESS_STATS if progress is True else progress or ()
    if names:
        # Show only the stats this kernel has, found from the output shapes.
        first_args = jax.tree.map(lambda a: a[0, 0], args)
        state_shape, info_shape = jax.eval_shape(
            kernel.step, keys[0, 0], state, params, *first_args
        )
        names = tuple(
            name
            for name in names
            if hasattr(state_shape, name) or hasattr(info_shape, name)
        )

    def transition(carry, xs):
        state, params, adaptor_state = carry
        step_key, step_args = xs
        state, params, adaptor_state, info = _step(
            step_key,
            kernel,
            state,
            params,
            *step_args,
            adaptor=adaptor,
            adaptor_state=adaptor_state,
        )
        return (state, params, adaptor_state), info

    def keep_one(carry, xs):
        carry, info = jax.lax.scan(transition, tuple(carry), xs)
        state, last_info = carry[0], jax.tree.map(lambda v: v[-1], info)
        record = {name: _field(name, state, last_info) for name in collect}
        stats = tuple(
            jnp.mean(jnp.asarray(_field(name, state, last_info), jnp.float32))
            for name in names
        )
        return carry, (record, stats)

    carry = (state, params, adaptor_state)
    if names:
        carry, (trace, _) = progress_scan(
            keep_one,
            carry,
            (keys, args),
            num_kept,
            names=names,
            stats_fn=lambda output: output[1],
        )
    else:
        carry, (trace, _) = jax.lax.scan(keep_one, carry, (keys, args))
    state, params, adaptor_state = carry
    if adaptor is not None:
        params, _ = adaptor.finalize(adaptor_state, params)
    return RunResult(state, params, trace if collect else None)

probjax.inference.warmup

warmup(key, kernel, state, params, num_steps, policy=None)

Fit the kernel parameters over num_steps transitions.

policy is an :class:Adaptor (run with :func:run, then finalized) or a :class:Warmup procedure. None uses the kernel's default_adaptation.

Source code in probjax/inference/runner.py
def warmup(
    key: RngKey,
    kernel: Kernel,
    state: Any,
    params: Any,
    num_steps: int,
    policy: Adaptor | Warmup | None = None,
) -> RunResult:
    """Fit the kernel parameters over ``num_steps`` transitions.

    ``policy`` is an :class:`Adaptor` (run with :func:`run`, then finalized) or
    a :class:`Warmup` procedure. ``None`` uses the kernel's
    ``default_adaptation``.
    """
    if policy is None:
        if kernel.default_adaptation is None:
            raise ValueError(
                "This kernel has no default adaptation; pass policy= explicitly."
            )
        policy = kernel.default_adaptation()
    if isinstance(policy, Warmup):
        return policy.run(key, kernel, state, params, num_steps)
    if isinstance(policy, Adaptor):
        return run(key, kernel, state, params, num_steps, adaptor=policy, collect=())
    raise TypeError(
        f"policy must be an Adaptor or a Warmup, got {type(policy).__name__}."
    )

probjax.inference.step

step(key, kernel, state, params, *args, adaptor=None, adaptor_state=None)

Take one transition, then apply one adaptor update if given.

Returns (state, params, adaptor_state, info). Use it to write your own outer loop, for example one that alternates sampling with fitting.

Source code in probjax/inference/runner.py
@partial(jax.jit, static_argnames=("kernel", "adaptor"))
def step(
    key: RngKey,
    kernel: Kernel,
    state: Any,
    params: Any,
    *args: Any,
    adaptor: Adaptor | None = None,
    adaptor_state: Any = None,
) -> tuple[Any, Any, Any, Any]:
    """Take one transition, then apply one adaptor update if given.

    Returns ``(state, params, adaptor_state, info)``. Use it to write your own
    outer loop, for example one that alternates sampling with fitting.
    """
    return _step(
        key, kernel, state, params, *args, adaptor=adaptor, adaptor_state=adaptor_state
    )

MCMC kernels

probjax.inference.hmc

Bases: MarkovKernelAPI

Hamiltonian Monte Carlo with a fixed number of integration steps.

Tunable parameters come from kernel.init_params(state, step_size=0.5, inverse_mass_matrix=None) (see :class:HMCParams); the step size is scaled by the initial gradient norm. Default adaptation: :func:window_warmup.

Parameters:

Name Type Description Default
logdensity_fn

Log density of the target.

required
num_integration_steps

Number of integrator steps per proposal.

required
integrator

Symplectic integrator from blackjax.mcmc.integrators.

required
divergence_threshold

Energy error above which a transition is flagged as divergent.

required
Source code in probjax/inference/mcmc/hmc.py
class hmc(MarkovKernelAPI):
    """Hamiltonian Monte Carlo with a fixed number of integration steps.

    Tunable parameters come from ``kernel.init_params(state, step_size=0.5,
    inverse_mass_matrix=None)`` (see :class:`HMCParams`); the step size is
    scaled by the initial gradient norm. Default adaptation:
    :func:`window_warmup`.

    Args:
        logdensity_fn: Log density of the target.
        num_integration_steps: Number of integrator steps per proposal.
        integrator: Symplectic integrator from ``blackjax.mcmc.integrators``.
        divergence_threshold: Energy error above which a transition is
            flagged as divergent.
    """

    init = staticmethod(blackjax.hmc.init)
    init_params = staticmethod(init_params)
    build_step = staticmethod(build_step)
    default_adaptation_fn = staticmethod(window_warmup)

probjax.inference.nuts

Bases: MarkovKernelAPI

No-U-Turn sampler: HMC with an adaptively chosen trajectory length.

Tunable parameters are the same as for :class:hmc (step_size, inverse_mass_matrix). Default adaptation: :func:window_warmup.

Parameters:

Name Type Description Default
logdensity_fn

Log density of the target.

required
max_num_doublings

Maximum tree depth; a trajectory has at most 2**max_num_doublings integrator steps.

required
divergence_threshold

Energy error above which a transition is flagged as divergent.

required
integrator

Symplectic integrator from blackjax.mcmc.integrators.

required
Source code in probjax/inference/mcmc/hmc.py
class nuts(MarkovKernelAPI):
    """No-U-Turn sampler: HMC with an adaptively chosen trajectory length.

    Tunable parameters are the same as for :class:`hmc`
    (``step_size``, ``inverse_mass_matrix``). Default adaptation:
    :func:`window_warmup`.

    Args:
        logdensity_fn: Log density of the target.
        max_num_doublings: Maximum tree depth; a trajectory has at most
            ``2**max_num_doublings`` integrator steps.
        divergence_threshold: Energy error above which a transition is
            flagged as divergent.
        integrator: Symplectic integrator from ``blackjax.mcmc.integrators``.
    """

    init = staticmethod(blackjax.hmc.init)
    init_params = staticmethod(init_params)
    build_step = staticmethod(build_kernel_nuts)
    default_adaptation_fn = staticmethod(window_warmup)

probjax.inference.dynamic_hmc

Bases: MarkovKernelAPI

HMC whose number of integration steps changes from step to step.

The step count follows a deterministic or random sequence with the given average, which avoids the periodicity of fixed-length HMC. Tunable parameters are the same as for :class:hmc. Default adaptation: :func:window_warmup.

Parameters:

Name Type Description Default
logdensity_fn

Log density of the target.

required
average_integration_steps

Mean number of integrator steps.

required
integration_steps_sequence

How the step counts are drawn: "halton" (low-discrepancy sequence), "random" (uniform in [1, 2 * average)) or a (next_random_arg_fn, integration_steps_fn) pair.

required
divergence_threshold

Energy error above which a transition is flagged as divergent.

required
integrator

Symplectic integrator from blackjax.mcmc.integrators.

required
Source code in probjax/inference/mcmc/dynamic_hmc.py
class dynamic_hmc(MarkovKernelAPI):
    """HMC whose number of integration steps changes from step to step.

    The step count follows a deterministic or random sequence with the given
    average, which avoids the periodicity of fixed-length HMC. Tunable
    parameters are the same as for :class:`hmc`. Default adaptation:
    :func:`window_warmup`.

    Args:
        logdensity_fn: Log density of the target.
        average_integration_steps: Mean number of integrator steps.
        integration_steps_sequence: How the step counts are drawn:
            ``"halton"`` (low-discrepancy sequence), ``"random"`` (uniform in
            ``[1, 2 * average)``) or a ``(next_random_arg_fn,
            integration_steps_fn)`` pair.
        divergence_threshold: Energy error above which a transition is
            flagged as divergent.
        integrator: Symplectic integrator from ``blackjax.mcmc.integrators``.
    """

    init = staticmethod(init)
    init_params = staticmethod(init_params)
    build_step = staticmethod(build_dynamic_hmc_step)
    default_adaptation_fn = staticmethod(window_warmup)

probjax.inference.mala

Bases: MarkovKernelAPI

Metropolis-adjusted Langevin algorithm.

Proposes a gradient step plus Gaussian noise and accepts it with a Metropolis-Hastings correction. Tunable parameters come from kernel.init_params(state, step_size=1e-2) (see :class:MALAParams); the step size is scaled by the initial gradient norm. Default adaptation: :func:step_size_adaptor.

Parameters:

Name Type Description Default
logdensity_fn

Log density of the target.

required
Source code in probjax/inference/mcmc/mala.py
class mala(MarkovKernelAPI):
    """Metropolis-adjusted Langevin algorithm.

    Proposes a gradient step plus Gaussian noise and accepts it with a
    Metropolis-Hastings correction. Tunable parameters come from
    ``kernel.init_params(state, step_size=1e-2)`` (see :class:`MALAParams`);
    the step size is scaled by the initial gradient norm. Default adaptation:
    :func:`step_size_adaptor`.

    Args:
        logdensity_fn: Log density of the target.
    """

    init = staticmethod(blackjax.mala.init)
    init_params = staticmethod(init_params)
    build_step = staticmethod(build_step)
    default_adaptation_fn = staticmethod(step_size_adaptor)

probjax.inference.mclmc

Bases: MarkovKernelAPI

Microcanonical Langevin Monte Carlo without Metropolis adjustment.

Isokinetic dynamics with partial momentum refreshment; asymptotically biased, so tune step_size and L with :func:mclmc_warmup (the default adaptation). Tunable parameters come from kernel.init_params(state, step_size=0.1, L=1.0, inverse_mass_matrix=None) (see :class:MCLMCParams).

Parameters:

Name Type Description Default
logdensity_fn

Log density of the target.

required
integrator

Isokinetic integrator from blackjax.mcmc.integrators.

required
Source code in probjax/inference/mcmc/mclmc.py
class mclmc(MarkovKernelAPI):
    """Microcanonical Langevin Monte Carlo without Metropolis adjustment.

    Isokinetic dynamics with partial momentum refreshment; asymptotically
    biased, so tune ``step_size`` and ``L`` with :func:`mclmc_warmup` (the
    default adaptation). Tunable parameters come from
    ``kernel.init_params(state, step_size=0.1, L=1.0,
    inverse_mass_matrix=None)`` (see :class:`MCLMCParams`).

    Args:
        logdensity_fn: Log density of the target.
        integrator: Isokinetic integrator from ``blackjax.mcmc.integrators``.
    """

    init = staticmethod(blackjax.mclmc.init)
    init_params = staticmethod(init_mclmc_params)
    build_step = staticmethod(build_mclmc_step)
    default_adaptation_fn = staticmethod(mclmc_warmup)

probjax.inference.adjusted_mclmc

Bases: MarkovKernelAPI

Metropolis-adjusted MCLMC with a fixed number of integration steps.

Tunable parameters come from kernel.init_params(state, step_size=0.5, num_integration_steps=10, inverse_mass_matrix=None, L_proposal_factor=inf) (see :class:AdjustedMCLMCParams). Default adaptation: step-size dual averaging towards a 0.9 acceptance rate.

Parameters:

Name Type Description Default
logdensity_fn

Log density of the target.

required
integrator

Isokinetic integrator from blackjax.mcmc.integrators.

required
divergence_threshold

Energy error above which a transition is flagged as divergent.

required
Source code in probjax/inference/mcmc/mclmc.py
class adjusted_mclmc(MarkovKernelAPI):
    """Metropolis-adjusted MCLMC with a fixed number of integration steps.

    Tunable parameters come from ``kernel.init_params(state, step_size=0.5,
    num_integration_steps=10, inverse_mass_matrix=None,
    L_proposal_factor=inf)`` (see :class:`AdjustedMCLMCParams`). Default
    adaptation: step-size dual averaging towards a 0.9 acceptance rate.

    Args:
        logdensity_fn: Log density of the target.
        integrator: Isokinetic integrator from ``blackjax.mcmc.integrators``.
        divergence_threshold: Energy error above which a transition is
            flagged as divergent.
    """

    init = staticmethod(blackjax.adjusted_mclmc.init)
    init_params = staticmethod(init_params)
    build_step = staticmethod(build_step)
    default_adaptation_fn = staticmethod(_adjusted_mclmc_adaptation)

probjax.inference.mh

Bases: MarkovKernelAPI

Metropolis-Hastings with a user-supplied proposal.

The kernel params are passed to the proposal as params=; the default init_params returns the empty :class:RWParams.

Parameters:

Name Type Description Default
logdensity_fn

Log density of the target.

required
transition_proposal_fn

(key, position, params) -> new_position.

required
transition_proposal_logpdf

(new_state, state, params) -> log q, evaluated on BlackJAX RWState s; needed only for asymmetric proposals.

required
Source code in probjax/inference/mcmc/mh.py
class mh(MarkovKernelAPI):
    r"""Metropolis-Hastings with a user-supplied proposal.

    The kernel params are passed to the proposal as ``params=``; the default
    ``init_params`` returns the empty :class:`RWParams`.

    Args:
        logdensity_fn: Log density of the target.
        transition_proposal_fn: ``(key, position, params) -> new_position``.
        transition_proposal_logpdf: ``(new_state, state, params) -> log q``,
            evaluated on BlackJAX ``RWState``\ s; needed only for asymmetric
            proposals.
    """

    init = staticmethod(blackjax.rmh.init)
    init_params = staticmethod(init_params_mh)
    build_step = staticmethod(build_mh_step)

probjax.inference.gauss_rwmh

Bases: MarkovKernelAPI

Gaussian random-walk Metropolis-Hastings.

Proposes x + step_size * scale @ eps with standard normal eps. Tunable parameters come from kernel.init_params(state, step_size=0.5, scale=None) (see :class:RWParamsGauss); scale is a diagonal (vector) or a dense matrix square root and defaults to the identity. Default adaptation: a covariance estimate for scale over the first half of the warmup, then the step size towards a 0.234 acceptance rate.

Parameters:

Name Type Description Default
logdensity_fn

Log density of the target.

required
Source code in probjax/inference/mcmc/mh.py
class gauss_rwmh(MarkovKernelAPI):
    """Gaussian random-walk Metropolis-Hastings.

    Proposes ``x + step_size * scale @ eps`` with standard normal ``eps``.
    Tunable parameters come from ``kernel.init_params(state, step_size=0.5,
    scale=None)`` (see :class:`RWParamsGauss`); ``scale`` is a diagonal
    (vector) or a dense matrix square root and defaults to the identity.
    Default adaptation: a covariance estimate for ``scale`` over the first half
    of the warmup, then the step size towards a 0.234 acceptance rate.

    Args:
        logdensity_fn: Log density of the target.
    """

    init = staticmethod(blackjax.rmh.init)
    init_params = staticmethod(init_params_gaussian_rw)
    build_step = staticmethod(
        partial(build_mh_step, transition_proposal_fn=gaussian_transition_proposal)
    )
    default_adaptation_fn = staticmethod(_gauss_rwmh_adaptation)

probjax.inference.imh

Bases: MarkovKernelAPI

Independent Metropolis-Hastings with a :class:~probjax.inference.Density proposal.

The kernel's params are the proposal's params, so a learned proposal's weights live in the compiled scan and can be trained there. Default adaptation: :func:~probjax.inference.fit_adaptor maximising the proposal's likelihood on Rao-Blackwellised chain output, one Adam step per transition. Pass fit_adaptor(..., every=k, num_fit_steps=m) as the warmup policy to alternate sampling and fitting blocks instead.

Parameters:

Name Type Description Default
logdensity_fn

Log density of the target.

required
proposal

The independent proposal. Build one from a flow with :func:probjax.inference.neural.density.

required
Source code in probjax/inference/mcmc/imh.py
class imh(MarkovKernelAPI):
    """Independent Metropolis-Hastings with a :class:`~probjax.inference.Density` proposal.

    The kernel's ``params`` are the proposal's params, so a learned proposal's
    weights live in the compiled scan and can be trained there. Default
    adaptation: :func:`~probjax.inference.fit_adaptor` maximising the
    proposal's likelihood on Rao-Blackwellised chain output, one Adam step
    per transition. Pass ``fit_adaptor(..., every=k, num_fit_steps=m)`` as the
    warmup policy to alternate sampling and fitting blocks instead.

    Args:
        logdensity_fn: Log density of the target.
        proposal: The independent proposal. Build one from a flow with
            :func:`probjax.inference.neural.density`.
    """

    init = staticmethod(blackjax.irmh.init)
    init_params = staticmethod(_init_proposal_params)
    build_step = staticmethod(_build_imh_step)
    default_adaptation_fn = staticmethod(_imh_adaptation)

probjax.inference.slice

Bases: MarkovKernelAPI

Slice sampling along random one-dimensional slices.

Each step brackets the slice by stepping out with step_size and then shrinks it until a point is accepted. Tunable parameters come from kernel.init_params(state, step_size=0.5) (see :class:SliceParams). Default adaptation: :func:slice_step_size_adaptor, balancing bracket expansions and contractions.

Parameters:

Name Type Description Default
logdensity_fn

Log density of the target.

required
max_evals

Maximum number of density evaluations per step.

required
slice_fn

Slice geometry: "linear" (random direction), "poly" (random polynomial curve), "axis" (random coordinate), "none" or a custom (position, arg) -> (t -> position) builder.

required
slice_fn_arg

(key, position) -> arg sampling the slice argument; inferred when slice_fn is a string.

required
**kwargs

Options for the "poly" slice-argument sampler.

required
Source code in probjax/inference/mcmc/slice.py
class slice(MarkovKernelAPI):
    """Slice sampling along random one-dimensional slices.

    Each step brackets the slice by stepping out with ``step_size`` and then
    shrinks it until a point is accepted. Tunable parameters come from
    ``kernel.init_params(state, step_size=0.5)`` (see :class:`SliceParams`).
    Default adaptation: :func:`slice_step_size_adaptor`, balancing bracket
    expansions and contractions.

    Args:
        logdensity_fn: Log density of the target.
        max_evals: Maximum number of density evaluations per step.
        slice_fn: Slice geometry: ``"linear"`` (random direction), ``"poly"``
            (random polynomial curve), ``"axis"`` (random coordinate),
            ``"none"`` or a custom ``(position, arg) -> (t -> position)``
            builder.
        slice_fn_arg: ``(key, position) -> arg`` sampling the slice argument;
            inferred when ``slice_fn`` is a string.
        **kwargs: Options for the ``"poly"`` slice-argument sampler.
    """

    init = staticmethod(init)
    init_params = staticmethod(init_params)
    build_step = staticmethod(build_step)
    default_adaptation_fn = staticmethod(slice_step_size_adaptor)

probjax.inference.latent_slice

Bases: MarkovKernelAPI

Latent slice sampling with an auxiliary, per-coordinate bracket width.

The bracket width is resampled every step from an exponential with rate step_size, so it adapts to the local scale. Tunable parameters come from kernel.init_params(state, step_size=0.5) (see :class:LatentSliceParams). Default adaptation: step size towards a target number of density evaluations.

Parameters:

Name Type Description Default
logdensity_fn

Log density of the target.

required
max_evals

Maximum number of density evaluations per step.

required
Source code in probjax/inference/mcmc/latent_slice.py
class latent_slice(MarkovKernelAPI):
    """Latent slice sampling with an auxiliary, per-coordinate bracket width.

    The bracket width is resampled every step from an exponential with rate
    ``step_size``, so it adapts to the local scale. Tunable parameters come
    from ``kernel.init_params(state, step_size=0.5)`` (see
    :class:`LatentSliceParams`). Default adaptation: step size towards a
    target number of density evaluations.

    Args:
        logdensity_fn: Log density of the target.
        max_evals: Maximum number of density evaluations per step.
    """

    init = staticmethod(init)
    init_params = staticmethod(init_params)
    build_step = staticmethod(build_step)
    default_adaptation_fn = staticmethod(_latent_slice_adaptation)

probjax.inference.elliptical_slice

Bases: MarkovKernelAPI

Elliptical slice sampling for targets with a Gaussian prior.

The target is split as N(mean, cov_matrix) * likelihood. Rejection free and without tunable parameters.

Parameters:

Name Type Description Default
logdensity_fn

Log density of the full target.

required
cov_matrix

Prior covariance, a diagonal (vector) or a dense matrix.

required
mean

Prior mean.

required
loglikelihood_fn

Log likelihood. Defaults to logdensity_fn minus the Gaussian prior log density.

required
Source code in probjax/inference/mcmc/elliptical_slice.py
class elliptical_slice(MarkovKernelAPI):
    """Elliptical slice sampling for targets with a Gaussian prior.

    The target is split as ``N(mean, cov_matrix) * likelihood``. Rejection
    free and without tunable parameters.

    Args:
        logdensity_fn: Log density of the full target.
        cov_matrix: Prior covariance, a diagonal (vector) or a dense matrix.
        mean: Prior mean.
        loglikelihood_fn: Log likelihood. Defaults to ``logdensity_fn`` minus
            the Gaussian prior log density.
    """

    init = staticmethod(blackjax.elliptical_slice.init)
    init_params = staticmethod(init_params)
    build_step = staticmethod(build_elliptical_slice_step)

probjax.inference.arms

Bases: MarkovKernelAPI

Adaptive rejection Metropolis sampling for scalar targets.

Proposes from a piecewise-exponential envelope of the log density and corrects with Metropolis-Hastings; rejected proposals refine the envelope. The envelope is set up by kernel.init(key, position, init_points=None, lb=-inf, ub=inf, max_points=50). No tunable parameters.

Parameters:

Name Type Description Default
logdensity_fn

Log density of the scalar target.

required
Source code in probjax/inference/mcmc/arms.py
class arms(MarkovKernelAPI):
    """Adaptive rejection Metropolis sampling for scalar targets.

    Proposes from a piecewise-exponential envelope of the log density and
    corrects with Metropolis-Hastings; rejected proposals refine the
    envelope. The envelope is set up by ``kernel.init(key, position,
    init_points=None, lb=-inf, ub=inf, max_points=50)``. No tunable
    parameters.

    Args:
        logdensity_fn: Log density of the scalar target.
    """

    init = staticmethod(init)
    init_params = staticmethod(init_params)
    build_step = staticmethod(
        lambda logdensity_fn, **_: _arms_step(logdensity_fn, control=False)
    )

probjax.inference.a2rms

Bases: MarkovKernelAPI

ARMS that also refines the envelope after some accepted proposals.

Like :class:arms, but an accepted proposal still updates the envelope with a probability that grows with the envelope's gap to the target, so the envelope keeps improving where ARMS would stop.

Parameters:

Name Type Description Default
logdensity_fn

Log density of the scalar target.

required
Source code in probjax/inference/mcmc/arms.py
class a2rms(MarkovKernelAPI):
    """ARMS that also refines the envelope after some accepted proposals.

    Like :class:`arms`, but an accepted proposal still updates the envelope
    with a probability that grows with the envelope's gap to the target, so
    the envelope keeps improving where ARMS would stop.

    Args:
        logdensity_fn: Log density of the scalar target.
    """

    init = staticmethod(init)
    init_params = staticmethod(init_params)
    build_step = staticmethod(
        lambda logdensity_fn, **_: _arms_step(logdensity_fn, control=True)
    )

probjax.inference.gaussian_imh

Bases: MarkovKernelAPI

Independent Metropolis-Hastings with a Gaussian proposal.

Tunable parameters come from kernel.init_params(state, mean=None, cov=None) (see :class:GaussianIMHParams). Default adaptation: running estimates of the mean and covariance.

Parameters:

Name Type Description Default
logdensity_fn

Log density of the target.

required
Source code in probjax/inference/mcmc/imh.py
class gaussian_imh(MarkovKernelAPI):
    """Independent Metropolis-Hastings with a Gaussian proposal.

    Tunable parameters come from ``kernel.init_params(state, mean=None,
    cov=None)`` (see :class:`GaussianIMHParams`). Default adaptation: running
    estimates of the mean and covariance.

    Args:
        logdensity_fn: Log density of the target.
    """

    init = staticmethod(blackjax.irmh.init)
    init_params = staticmethod(init_gaussian_imh_params)
    build_step = staticmethod(_build_gaussian_imh_step)
    default_adaptation_fn = staticmethod(_gaussian_imh_adaptation)

probjax.inference.adjusted_mclmc_dynamic

Bases: MarkovKernelAPI

Metropolis-adjusted MCLMC with a varying number of integration steps.

Tunable parameters come from kernel.init_params(state, step_size=0.5, inverse_mass_matrix=None, L_proposal_factor=inf) (see :class:AdjustedMCLMCDynamicParams). Default adaptation: step-size dual averaging towards a 0.9 acceptance rate.

Parameters:

Name Type Description Default
logdensity_fn

Log density of the target.

required
average_integration_steps

Mean number of integrator steps.

required
integration_steps_sequence

How the step counts are drawn: "halton", "random" or a (next_random_arg_fn, integration_steps_fn) pair.

required
divergence_threshold

Energy error above which a transition is flagged as divergent.

required
integrator

Isokinetic integrator from blackjax.mcmc.integrators.

required
next_random_arg_fn

Advances the random argument; required with integration_steps_fn.

required
integration_steps_fn

Custom step-count function, overriding integration_steps_sequence.

required
Source code in probjax/inference/mcmc/mclmc.py
class adjusted_mclmc_dynamic(MarkovKernelAPI):
    """Metropolis-adjusted MCLMC with a varying number of integration steps.

    Tunable parameters come from ``kernel.init_params(state, step_size=0.5,
    inverse_mass_matrix=None, L_proposal_factor=inf)`` (see
    :class:`AdjustedMCLMCDynamicParams`). Default adaptation:
    step-size dual averaging towards a 0.9 acceptance rate.

    Args:
        logdensity_fn: Log density of the target.
        average_integration_steps: Mean number of integrator steps.
        integration_steps_sequence: How the step counts are drawn:
            ``"halton"``, ``"random"`` or a ``(next_random_arg_fn,
            integration_steps_fn)`` pair.
        divergence_threshold: Energy error above which a transition is
            flagged as divergent.
        integrator: Isokinetic integrator from ``blackjax.mcmc.integrators``.
        next_random_arg_fn: Advances the random argument; required with
            ``integration_steps_fn``.
        integration_steps_fn: Custom step-count function, overriding
            ``integration_steps_sequence``.
    """

    init = staticmethod(init_dynamic)
    init_params = staticmethod(init_dynamic_params)
    build_step = staticmethod(build_dynamic_step)
    default_adaptation_fn = staticmethod(_adjusted_mclmc_adaptation)

probjax.inference.pseudo_marginal

pseudo_marginal(inner_kernel_cls, stochastic_logdensity_fn, num_samples=1, **inner_kernel_kwargs)

Wrap a probjax MCMC kernel for pseudo-marginal inference.

Parameters:

Name Type Description Default
inner_kernel_cls

A probjax kernel class (e.g. hmc, mala, gauss_rwmh) or a configured one (e.g. hmc.configure(num_integration_steps=10)) that exposes build_step, init and init_params.

required
stochastic_logdensity_fn Callable

A callable (position, rng) -> float that returns an unbiased estimate of the log-density (or log-marginal-likelihood) given auxiliary randomness from rng.

required
num_samples int

Number of independent keys to evaluate and average for variance reduction (default 1).

1
**inner_kernel_kwargs

Options for the inner kernel. They are routed to whichever of init, init_params and build_step accepts them.

{}

Returns:

Name Type Description
A Kernel

class:Kernel whose step splits the PRNG key,

Kernel

fixes one sub-key for the stochastic log-density, and delegates

Kernel

the transition to the inner kernel.

Source code in probjax/inference/mcmc/pmmcmc.py
def pseudo_marginal(
    inner_kernel_cls,
    stochastic_logdensity_fn: Callable,
    num_samples: int = 1,
    **inner_kernel_kwargs,
) -> Kernel:
    """Wrap a probjax MCMC kernel for pseudo-marginal inference.

    Args:
        inner_kernel_cls: A probjax kernel class (e.g. ``hmc``, ``mala``,
            ``gauss_rwmh``) or a configured one (e.g.
            ``hmc.configure(num_integration_steps=10)``) that exposes
            ``build_step``, ``init`` and ``init_params``.
        stochastic_logdensity_fn: A callable
            ``(position, rng) -> float`` that returns an unbiased
            estimate of the log-density (or log-marginal-likelihood)
            given auxiliary randomness from *rng*.
        num_samples: Number of independent keys to evaluate and average
            for variance reduction (default 1).
        **inner_kernel_kwargs: Options for the inner kernel. They are routed
            to whichever of ``init``, ``init_params`` and ``build_step``
            accepts them.

    Returns:
        A :class:`Kernel` whose ``step`` splits the PRNG key,
        fixes one sub-key for the stochastic log-density, and delegates
        the transition to the inner kernel.
    """
    if num_samples < 1:
        raise ValueError("num_samples must be >= 1")

    configured = as_configured(inner_kernel_cls, **inner_kernel_kwargs)

    # ------------------------------------------------------------------
    # init: evaluate the stochastic logdensity once to populate state
    # ------------------------------------------------------------------
    def init(rng: RngKey, position: Any):
        key_init, key_logdensity = jax.random.split(rng)
        fixed_logdensity = _make_fixed_logdensity(
            stochastic_logdensity_fn, key_logdensity, num_samples
        )
        # The inner kernel's underlying init (e.g. blackjax.hmc.init)
        # takes (position, logdensity_fn) and evaluates at position.
        return configured.init(
            position, logdensity_fn=fixed_logdensity, rng=key_init
        )

    # ------------------------------------------------------------------
    # step: fix randomness for this transition, delegate to inner kernel
    # ------------------------------------------------------------------
    def step(rng, state, params, *args):
        key_kernel, key_logdensity = jax.random.split(rng)
        fixed_logdensity = _make_fixed_logdensity(
            stochastic_logdensity_fn, key_logdensity, num_samples
        )
        # build_step returns a raw step(key, state, params) closure
        inner_step = configured.build_step(fixed_logdensity)
        return inner_step(key_kernel, state, params, *args)

    def init_params(state, *args, **kwargs):
        return configured.init_params(state, *args, **kwargs)

    # Inherit the inner kernel's recommended warmup, unless it needs the
    # target: a stochastic log-density has no single one to bind.
    default_adaptation = configured.default_adaptation_fn
    if default_adaptation is not None and (
        "logdensity_fn" in inspect.signature(default_adaptation).parameters
    ):
        default_adaptation = None
    default_adaptation = bind_default_adaptation(
        default_adaptation, **inner_kernel_kwargs
    )
    return pseudo_marginal_type(init, step, init_params, default_adaptation)

Exact samplers

Each step returns one independent, exact draw.

probjax.inference.ars

Bases: MarkovKernelAPI

Adaptive rejection sampling for univariate log-concave targets.

Every step returns one exact, independent draw: run collects iid samples, not a Markov chain. Draws come from a piecewise-exponential envelope of the log density; a draw that misses the squeeze costs one log-density evaluation and adds a tangent there, so the envelope tightens as sampling proceeds and later draws are cheaper. The envelope lives in the state; there are no parameters to adapt.

The envelope is set up by kernel.init(key, position, init_points=None, lower=-inf, upper=inf, max_points=50). init_points default to position + [-1, 0, 1]; on an unbounded side they must straddle the mode. The target must be log-concave.

Parameters:

Name Type Description Default
logdensity_fn

Log density of the scalar target.

required
max_proposals

Envelope draws allowed per step before giving up.

required
Source code in probjax/inference/rejection/adaptive.py
class ars(MarkovKernelAPI):
    """Adaptive rejection sampling for univariate log-concave targets.

    Every step returns one *exact, independent* draw: ``run`` collects iid
    samples, not a Markov chain. Draws come from a piecewise-exponential
    envelope of the log density; a draw that misses the squeeze costs one
    log-density evaluation and adds a tangent there, so the envelope tightens
    as sampling proceeds and later draws are cheaper. The envelope lives in the
    state; there are no parameters to adapt.

    The envelope is set up by ``kernel.init(key, position, init_points=None,
    lower=-inf, upper=inf, max_points=50)``. ``init_points`` default to
    ``position + [-1, 0, 1]``; on an unbounded side they must straddle the
    mode. The target must be log-concave.

    Args:
        logdensity_fn: Log density of the scalar target.
        max_proposals: Envelope draws allowed per step before giving up.
    """

    init = staticmethod(init)
    init_params = staticmethod(init_params)
    build_step = staticmethod(build_step)

probjax.inference.rejection

Bases: MarkovKernelAPI

Rejection sampling with a :class:~probjax.inference.Density proposal.

Every step returns one exact, independent draw from the target, provided params.log_bound bounds log p - log q from above: proposals x ~ q are accepted with probability exp(log p(x) - log q(x) - log M) until one is. log p need not be normalised.

Params come from kernel.init_params(state, proposal_params, log_bound=-inf). The default adaptation, :func:bound_adaptor, learns the bound as the largest ratio seen, so warm up before collecting draws. A learned proposal trains with :func:~probjax.inference.fit_adaptor on field="proposal"; the accepted draws are exact target samples, so the default data_fn is the right training data.

Parameters:

Name Type Description Default
logdensity_fn

Log density of the target, up to a constant.

required
proposal

The proposal. It needs heavier tails than the target, or no finite bound exists.

required
max_proposals

Proposals allowed per step before giving up.

required
Source code in probjax/inference/rejection/proposal.py
class rejection(MarkovKernelAPI):
    """Rejection sampling with a :class:`~probjax.inference.Density` proposal.

    Every step returns one *exact, independent* draw from the target, provided
    ``params.log_bound`` bounds ``log p - log q`` from above: proposals
    ``x ~ q`` are accepted with probability ``exp(log p(x) - log q(x) - log M)``
    until one is. ``log p`` need not be normalised.

    Params come from ``kernel.init_params(state, proposal_params,
    log_bound=-inf)``. The default adaptation, :func:`bound_adaptor`, learns
    the bound as the largest ratio seen, so warm up before collecting draws. A
    learned proposal trains with :func:`~probjax.inference.fit_adaptor` on
    ``field="proposal"``; the accepted draws are exact target samples, so the
    default ``data_fn`` is the right training data.

    Args:
        logdensity_fn: Log density of the target, up to a constant.
        proposal: The proposal. It needs heavier tails than the target, or no
            finite bound exists.
        max_proposals: Proposals allowed per step before giving up.
    """

    init = staticmethod(init)
    init_params = staticmethod(init_params)
    build_step = staticmethod(build_step)
    default_adaptation_fn = staticmethod(_rejection_adaptation)

probjax.inference.bound_adaptor

bound_adaptor(field='log_bound', margin=0.0, axis_name=None)

Raise params.<field> to the largest log p - log q seen.

The bound only grows, so draws taken while it is still growing are slightly biased towards where it was too low; run this as warmup and keep the bound fixed for the draws you report. With axis_name the chains under jax.vmap share the maximum over all of them.

Source code in probjax/inference/rejection/proposal.py
def bound_adaptor(
    field: str = "log_bound", margin: float = 0.0, axis_name: str | None = None
) -> Adaptor:
    """Raise ``params.<field>`` to the largest ``log p - log q`` seen.

    The bound only grows, so draws taken while it is still growing are
    slightly biased towards where it was too low; run this as warmup and keep
    the bound fixed for the draws you report. With ``axis_name`` the chains
    under ``jax.vmap`` share the maximum over all of them.
    """

    def init(state: Any, params: Any) -> tuple[()]:
        return ()

    def update(state: Any, info: RejectionInfo, adaptor_state: Any, params: Any):
        seen = info.max_log_ratio + margin
        if axis_name is not None:
            seen = jax.lax.pmax(seen, axis_name)
        bound = jnp.maximum(get_param(params, field), seen)
        return adaptor_state, replace_params(params, **{field: bound}), None

    def finalize(adaptor_state: Any, params: Any) -> tuple[Any, None]:
        return params, None

    return Adaptor(init, update, finalize)

Stochastic-gradient MCMC

probjax.inference.sgld

sgld(grad_estimator, temperature=1.0)

Stochastic Gradient Langevin Dynamics.

Parameters:

Name Type Description Default
grad_estimator Callable

Function (position, minibatch) -> pytree that estimates the gradient of the log-posterior.

required
temperature float

Temperature parameter (default 1.0).

1.0
Source code in probjax/inference/mcmc/sgmcmc.py
def sgld(grad_estimator: Callable, temperature: float = 1.0) -> Kernel:
    """Stochastic Gradient Langevin Dynamics.

    Args:
        grad_estimator: Function ``(position, minibatch) -> pytree`` that
            estimates the gradient of the log-posterior.
        temperature: Temperature parameter (default 1.0).
    """
    kernel = _make_sgmcmc_kernel(
        grad_estimator,
        blackjax.sgld.build_kernel,
        blackjax_init=blackjax.sgld.init,
        temperature=temperature,
    )
    return kernel

probjax.inference.sghmc

sghmc(grad_estimator, num_integration_steps=10, alpha=0.01, beta=0.0, temperature=1.0)

Stochastic Gradient Hamiltonian Monte Carlo.

Parameters:

Name Type Description Default
grad_estimator Callable

Function (position, minibatch) -> pytree that estimates the gradient of the log-posterior.

required
num_integration_steps int

Number of leapfrog steps per sample.

10
alpha float

Friction coefficient.

0.01
beta float

Noise scaling.

0.0
temperature float

Temperature parameter.

1.0
Source code in probjax/inference/mcmc/sgmcmc.py
def sghmc(
    grad_estimator: Callable,
    num_integration_steps: int = 10,
    alpha: float = 0.01,
    beta: float = 0.0,
    temperature: float = 1.0,
) -> Kernel:
    """Stochastic Gradient Hamiltonian Monte Carlo.

    Args:
        grad_estimator: Function ``(position, minibatch) -> pytree`` that
            estimates the gradient of the log-posterior.
        num_integration_steps: Number of leapfrog steps per sample.
        alpha: Friction coefficient.
        beta: Noise scaling.
        temperature: Temperature parameter.
    """
    return _make_sgmcmc_kernel(
        grad_estimator,
        partial(blackjax.sghmc.build_kernel, alpha=alpha, beta=beta),
        blackjax_init=blackjax.sghmc.init,
        extra_step_kwargs={"num_integration_steps": num_integration_steps},
        temperature=temperature,
    )

probjax.inference.sgnht

sgnht(grad_estimator, alpha=0.01, beta=0.0, temperature=1.0)

Stochastic Gradient Nosé-Hoover Thermostat.

SGNHT extends SGHMC with a thermostat variable that automatically adjusts the kinetic energy to maintain the target temperature.

Parameters:

Name Type Description Default
grad_estimator Callable

Function (position, minibatch) -> pytree that estimates the gradient of the log-posterior.

required
alpha float

Friction coefficient.

0.01
beta float

Noise scaling.

0.0
temperature float

Temperature parameter.

1.0
Note

init requires the rng to initialise momentum::

state = sampler.init(key, position)
Source code in probjax/inference/mcmc/sgmcmc.py
def sgnht(
    grad_estimator: Callable,
    alpha: float = 0.01,
    beta: float = 0.0,
    temperature: float = 1.0,
) -> Kernel:
    """Stochastic Gradient Nosé-Hoover Thermostat.

    SGNHT extends SGHMC with a thermostat variable that automatically
    adjusts the kinetic energy to maintain the target temperature.

    Args:
        grad_estimator: Function ``(position, minibatch) -> pytree`` that
            estimates the gradient of the log-posterior.
        alpha: Friction coefficient.
        beta: Noise scaling.
        temperature: Temperature parameter.

    Note:
        ``init`` requires the rng to initialise momentum::

            state = sampler.init(key, position)
    """
    kernel = blackjax.sgnht.build_kernel(alpha=alpha, beta=beta)

    def sgnht_state_init(rng: RngKey, position: Any):
        return _sgnht_init(position, rng=rng, alpha=alpha)

    return _make_sgmcmc_kernel(
        grad_estimator,
        lambda: kernel,
        state_init=sgnht_state_init,
        temperature=temperature,
    )

Warmup and adaptation

probjax.inference.window_warmup

window_warmup(*, diagonal=True, target_acceptance_rate=0.8, initial_buffer_size=75, final_buffer_size=50, first_window_size=25)

Finite Stan-style warmup using BlackJAX's window-adaptation base.

Source code in probjax/inference/mcmc/adaptation.py
def window_warmup(
    *,
    diagonal: bool = True,
    target_acceptance_rate: float = 0.8,
    initial_buffer_size: int = 75,
    final_buffer_size: int = 50,
    first_window_size: int = 25,
) -> Warmup:
    """Finite Stan-style warmup using BlackJAX's window-adaptation base."""
    import warnings

    # `window_adaptation.base` is deprecated upstream in favor of the bound
    # `window_adaptation`/`staged_adaptation` algorithms, but those don't expose
    # the init/update/final triple this Warmup protocol needs. Keep using the
    # base primitives and silence the deprecation until the protocol migrates
    # to the staged engine.
    with warnings.catch_warnings():
        warnings.filterwarnings(
            "ignore", message=".*window_adaptation.base.*", category=DeprecationWarning
        )
        init_window, update_window, final_window = window_adaptation_base(
            diagonal, target_acceptance_rate
        )

    @partial(jax.jit, static_argnames=("kernel", "num_steps"))
    def run(rng, kernel: Kernel, state: Any, params: Any, num_steps: int) -> RunResult:
        schedule = build_schedule(
            num_steps,
            initial_buffer_size=initial_buffer_size,
            final_buffer_size=final_buffer_size,
            first_window_size=first_window_size,
        )
        get_param(params, "inverse_mass_matrix")
        warmup_state = init_window(state.position, get_param(params, "step_size"))
        keys = jax.random.split(rng, num_steps)

        def one_step(carry, xs):
            state, params, warmup_state = carry
            step_key, stage = xs
            state, info = kernel.step(step_key, state, params)
            warmup_state = update_window(
                warmup_state, stage, state.position, info.acceptance_rate
            )
            params = replace_params(
                params,
                step_size=warmup_state.step_size,
                inverse_mass_matrix=warmup_state.inverse_mass_matrix,
            )
            return (state, params, warmup_state), None

        (state, params, warmup_state), _ = jax.lax.scan(
            one_step, (state, params, warmup_state), (keys, schedule)
        )
        step_size, inverse_mass_matrix = final_window(warmup_state)
        params = replace_params(
            params,
            step_size=step_size,
            inverse_mass_matrix=inverse_mass_matrix,
        )
        return RunResult(state, params)

    return Warmup(run)

probjax.inference.pathfinder_warmup

pathfinder_warmup(algorithm, logdensity_fn, *, initial_step_size=1.0, target_acceptance_rate=0.8, collect=False, **extra_parameters)

Create a BlackJAX Pathfinder warmup procedure.

Source code in probjax/inference/mcmc/warmup.py
def pathfinder_warmup(
    algorithm,
    logdensity_fn,
    *,
    initial_step_size: float = 1.0,
    target_acceptance_rate: float = 0.8,
    collect: bool = False,
    **extra_parameters,
) -> Warmup:
    """Create a BlackJAX Pathfinder warmup procedure."""
    info_fn = return_all_adapt_info if collect else lambda *_: None
    # BlackJAX types the adaptation procedure's ``run`` with two arguments,
    # but the Pathfinder procedure also takes ``num_steps``.
    procedure: Any = blackjax.pathfinder_adaptation(
        algorithm,
        logdensity_fn,
        initial_step_size=initial_step_size,
        target_acceptance_rate=target_acceptance_rate,
        adaptation_info_fn=info_fn,
        **extra_parameters,
    )

    @partial(jax.jit, static_argnames=("_kernel", "num_steps"))
    def run(rng, _kernel, state, params, num_steps):
        result, info = procedure.run(rng, state.position, num_steps)
        params = replace_params(params, **result.parameters)
        return RunResult(result.state, params, info if collect else None)

    return Warmup(run)

probjax.inference.mclmc_warmup

mclmc_warmup(logdensity_fn, *, collect=False, **options)

Create BlackJAX's MCLMC warmup for an :class:~probjax.inference.mclmc kernel.

Advances the chain while estimating L, the step size and, optionally, a diagonal preconditioner. It tunes the kernel it is run with, so a custom integrator is respected. options go to blackjax.mclmc_find_L_and_step_size.

Source code in probjax/inference/mcmc/warmup.py
def mclmc_warmup(
    logdensity_fn,
    *,
    collect: bool = False,
    **options,
) -> Warmup:
    """Create BlackJAX's MCLMC warmup for an :class:`~probjax.inference.mclmc` kernel.

    Advances the chain while estimating ``L``, the step size and, optionally,
    a diagonal preconditioner. It tunes the kernel it is run with, so a custom
    integrator is respected. ``options`` go to
    ``blackjax.mclmc_find_L_and_step_size``.
    """

    @partial(jax.jit, static_argnames=("kernel", "num_steps"))
    def run(rng, kernel, state, params, num_steps):
        def tuned_kernel(
            rng_key, state, logdensity_fn, inverse_mass_matrix, L, step_size
        ):
            tuned = replace_params(
                params,
                L=L,
                step_size=step_size,
                inverse_mass_matrix=inverse_mass_matrix,
            )
            return kernel.step(rng_key, state, tuned)

        adaptation_params = MCLMCAdaptationState(
            get_param(params, "L"),
            get_param(params, "step_size"),
            get_param(params, "inverse_mass_matrix"),
        )
        state, adaptation_params, work = blackjax.mclmc_find_L_and_step_size(
            mclmc_kernel=tuned_kernel,
            num_steps=num_steps,
            state=state,
            rng_key=rng,
            logdensity_fn=logdensity_fn,
            params=adaptation_params,
            **options,
        )
        params = replace_params(
            params,
            L=adaptation_params.L,
            step_size=adaptation_params.step_size,
            inverse_mass_matrix=adaptation_params.inverse_mass_matrix,
        )
        return RunResult(state, params, work if collect else None)

    return Warmup(run)

probjax.inference.step_size_adaptor

step_size_adaptor(target=0.8, *, target_from_info_fn=lambda info: acceptance_rate, t0=10, gamma=0.05, kappa=0.75)

Local dual-averaging rule backed by BlackJAX's adaptation primitive.

Source code in probjax/inference/mcmc/adaptation.py
def step_size_adaptor(
    target: float = 0.8,
    *,
    target_from_info_fn=lambda info: info.acceptance_rate,
    t0: int = 10,
    gamma: float = 0.05,
    kappa: float = 0.75,
) -> Adaptor:
    """Local dual-averaging rule backed by BlackJAX's adaptation primitive."""
    init_da, update_da, final_da = dual_averaging_adaptation(
        target, t0=t0, gamma=gamma, kappa=kappa
    )

    def init(_state: Any, params: Any) -> Any:
        return init_da(get_param(params, "step_size"))

    def update(
        _state: Any, info: Any, adaptor_state: Any, params: Any
    ) -> tuple[Any, Any, None]:
        adaptor_state = update_da(adaptor_state, target_from_info_fn(info))
        params = replace_params(params, step_size=jnp.exp(adaptor_state.log_step_size))
        return adaptor_state, params, None

    def finalize(adaptor_state: Any, params: Any) -> tuple[Any, None]:
        return replace_params(params, step_size=final_da(adaptor_state)), None

    return Adaptor(init, update, finalize)

probjax.inference.mass_matrix_adaptor

mass_matrix_adaptor(*, diagonal=True)

Local mass-matrix estimator backed by BlackJAX Welford adaptation.

Source code in probjax/inference/mcmc/adaptation.py
def mass_matrix_adaptor(*, diagonal: bool = True) -> Adaptor:
    """Local mass-matrix estimator backed by BlackJAX Welford adaptation."""
    init_mm, update_mm, final_mm = mass_matrix_adaptation(diagonal)

    def init(state: Any, params: Any) -> Any:
        position = ravel_position(state)
        get_param(params, "inverse_mass_matrix")
        return init_mm(position.size)

    def update(
        state: Any, _info: Any, adaptor_state: Any, params: Any
    ) -> tuple[Any, Any, None]:
        return update_mm(adaptor_state, state.position), params, None

    def finalize(adaptor_state: Any, params: Any) -> tuple[Any, None]:
        adaptor_state = final_mm(adaptor_state)
        return replace_params(
            params, inverse_mass_matrix=adaptor_state.inverse_mass_matrix
        ), None

    return Adaptor(init, update, finalize)

probjax.inference.covariance_adaptor

covariance_adaptor(*, field='scale', diagonal=True, output='standard_deviation')

Local proposal-geometry estimator backed by BlackJAX Welford updates.

Source code in probjax/inference/mcmc/adaptation.py
def covariance_adaptor(
    *,
    field: str = "scale",
    diagonal: bool = True,
    output: str = "standard_deviation",
) -> Adaptor:
    """Local proposal-geometry estimator backed by BlackJAX Welford updates."""
    init_cov, update_cov, final_cov = welford_algorithm(diagonal)
    if output not in {"variance", "covariance", "standard_deviation", "cholesky"}:
        raise ValueError(f"Unsupported covariance output: {output}")

    def init(state: Any, params: Any) -> CovarianceAdaptorState:
        position = ravel_position(state)
        get_param(params, field)
        return CovarianceAdaptorState(init_cov(position.size))

    def update(
        state: Any, _info: Any, adaptor_state: CovarianceAdaptorState, params: Any
    ) -> tuple[CovarianceAdaptorState, Any, None]:
        position = ravel_position(state)
        state_out = update_cov(adaptor_state.welford_state, position)
        return CovarianceAdaptorState(state_out), params, None

    def finalize(
        adaptor_state: CovarianceAdaptorState, params: Any
    ) -> tuple[Any, None]:
        covariance, _, _ = final_cov(adaptor_state.welford_state)
        if output == "standard_deviation":
            value = jnp.sqrt(covariance)
        elif output == "cholesky":
            value = jnp.linalg.cholesky(covariance)
        else:
            value = covariance
        return replace_params(params, **{field: value}), None

    return Adaptor(init, update, finalize)

probjax.inference.mean_adaptor

mean_adaptor(*, field='mean', diagonal=True)

Local proposal-mean estimator backed by BlackJAX Welford updates.

Source code in probjax/inference/mcmc/adaptation.py
def mean_adaptor(
    *,
    field: str = "mean",
    diagonal: bool = True,
) -> Adaptor:
    """Local proposal-mean estimator backed by BlackJAX Welford updates."""
    init_mean, update_mean, final_mean = welford_algorithm(diagonal)

    def init(state: Any, params: Any) -> CovarianceAdaptorState:
        position = ravel_position(state)
        get_param(params, field)
        return CovarianceAdaptorState(init_mean(position.size))

    def update(
        state: Any, _info: Any, adaptor_state: CovarianceAdaptorState, params: Any
    ) -> tuple[CovarianceAdaptorState, Any, None]:
        position = ravel_position(state)
        state_out = update_mean(adaptor_state.welford_state, position)
        return CovarianceAdaptorState(state_out), params, None

    def finalize(
        adaptor_state: CovarianceAdaptorState, params: Any
    ) -> tuple[Any, None]:
        _, _, mean = final_mean(adaptor_state.welford_state)
        return replace_params(params, **{field: mean}), None

    return Adaptor(init, update, finalize)

probjax.inference.compose_adaptors

compose_adaptors(**adaptors)

Compose adaptors, threading parameter updates in declaration order.

Source code in probjax/inference/adaptation.py
def compose_adaptors(**adaptors: Adaptor) -> Adaptor:
    """Compose adaptors, threading parameter updates in declaration order."""
    if not adaptors:
        raise ValueError("At least one adaptor is required")
    names = tuple(adaptors)
    children = tuple(adaptors.values())

    def init(state: Any, params: Any) -> tuple[Any, ...]:
        return tuple(child.init(state, params) for child in children)

    def update(
        state: Any, info: Any, adaptor_states: tuple[Any, ...], params: Any
    ) -> tuple[tuple[Any, ...], Any, dict[str, Any]]:
        new_states = []
        child_info = {}
        for name, child, child_state in zip(
            names, children, adaptor_states, strict=True
        ):
            child_state, params, info_out = child.update(
                state, info, child_state, params
            )
            new_states.append(child_state)
            child_info[name] = info_out
        return tuple(new_states), params, child_info

    def finalize(
        adaptor_states: tuple[Any, ...], params: Any
    ) -> tuple[Any, dict[str, Any]]:
        child_info = {}
        for name, child, child_state in zip(
            names, children, adaptor_states, strict=True
        ):
            params, info_out = child.finalize(child_state, params)
            child_info[name] = info_out
        return params, child_info

    return Adaptor(init, update, finalize)

probjax.inference.acceptance_rate_adaptor

acceptance_rate_adaptor(target=0.65, field='step_size')

Scale params[field] by exp(acceptance_rate - target).

Too many rejections shrink the step, too few grow it. Steps that report no acceptance rate (NaN) leave it unchanged.

Source code in probjax/inference/smc/adaptation.py
def acceptance_rate_adaptor(target: float = 0.65, field: str = "step_size") -> Adaptor:
    """Scale ``params[field]`` by ``exp(acceptance_rate - target)``.

    Too many rejections shrink the step, too few grow it. Steps that report no
    acceptance rate (NaN) leave it unchanged.
    """

    def tune(_state: Any, info: Any, params: Any) -> Any:
        rate = info.acceptance_rate
        factor = jnp.where(jnp.isnan(rate), 1.0, jnp.exp(rate - target))
        return replace_params(params, **{field: get_param(params, field) * factor})

    return _stateless(tune)

probjax.inference.slice_step_size_adaptor

slice_step_size_adaptor(target=0.5, **kwargs)

Dual averaging of the slice width on the share of bracket expansions.

A step size that is too small spends its evaluations stepping out, one that is too large spends them shrinking the bracket. The expanding share falls monotonically with the step size, so driving it to target (expansions as many as contractions by default, as in Karamanis & Beutler, 2021) is stable, unlike targeting the total number of evaluations.

Source code in probjax/inference/mcmc/adaptation.py
def slice_step_size_adaptor(target: float = 0.5, **kwargs: Any) -> Adaptor:
    """Dual averaging of the slice width on the share of bracket expansions.

    A step size that is too small spends its evaluations stepping out, one
    that is too large spends them shrinking the bracket. The expanding share
    falls monotonically with the step size, so driving it to ``target``
    (expansions as many as contractions by default, as in Karamanis & Beutler,
    2021) is stable, unlike targeting the total number of evaluations.
    """
    return step_size_adaptor(
        target=target,
        target_from_info_fn=lambda info: info.num_expansions / info.num_evals,
        **kwargs,
    )

probjax.inference.particle_adaptor

particle_adaptor(field='inverse_mass_matrix')

Set params[field] to the covariance of the particles.

A one-dimensional field gets the variances, a two-dimensional one the full covariance, matching what HMC-style moves expect of an inverse mass matrix.

Source code in probjax/inference/smc/adaptation.py
def particle_adaptor(field: str = "inverse_mass_matrix") -> Adaptor:
    """Set ``params[field]`` to the covariance of the particles.

    A one-dimensional field gets the variances, a two-dimensional one the full
    covariance, matching what HMC-style moves expect of an inverse mass matrix.
    """

    def tune(state: Any, _info: Any, params: Any) -> Any:
        flat = jax.vmap(lambda x: ravel_pytree(x)[0])(state.particles)
        current = jnp.asarray(get_param(params, field))
        if current.ndim == 2:
            value = jnp.atleast_2d(jnp.cov(flat, rowvar=False))
        else:
            value = jnp.var(flat, axis=0)
        return replace_params(params, **{field: value.astype(current.dtype)})

    return _stateless(tune)

probjax.inference.pooled

pooled(adaptor, axis_name='chain')

Share one adaptation across chains run under jax.vmap.

Run the chains with jax.vmap(..., axis_name=axis_name). After every update each chain's floating-point adaptor statistics and parameters are replaced by their mean over chains, so all chains adapt as one. For dual averaging this equals adapting on the mean acceptance rate. Welford estimates measure deviations from the pooled mean, so they include the spread between chains; they start slightly low (the bias shrinks like log(n) / n after n updates).

Do not pool a :func:fit_adaptor: averaging would mix the chains' buffered points. Give it axis_name instead, which averages gradients.

Source code in probjax/inference/adaptation.py
def pooled(adaptor: Adaptor, axis_name: str = "chain") -> Adaptor:
    """Share one adaptation across chains run under ``jax.vmap``.

    Run the chains with ``jax.vmap(..., axis_name=axis_name)``. After every
    update each chain's floating-point adaptor statistics and parameters are
    replaced by their mean over chains, so all chains adapt as one. For dual
    averaging this equals adapting on the mean acceptance rate. Welford
    estimates measure deviations from the pooled mean, so they include the
    spread between chains; they start slightly low (the bias shrinks like
    ``log(n) / n`` after ``n`` updates).

    Do not pool a :func:`fit_adaptor`: averaging would mix the chains'
    buffered points. Give it ``axis_name`` instead, which averages gradients.
    """

    def mean_over_chains(tree: Any) -> Any:
        def mean(value):
            if jnp.issubdtype(jnp.result_type(value), jnp.inexact):
                return jax.lax.pmean(value, axis_name)
            return value

        return jax.tree.map(mean, tree)

    def update(
        state: Any, info: Any, adaptor_state: Any, params: Any
    ) -> tuple[Any, Any, Any]:
        adaptor_state, params, adaptor_info = adaptor.update(
            state, info, adaptor_state, params
        )
        return mean_over_chains(adaptor_state), mean_over_chains(params), adaptor_info

    return Adaptor(adaptor.init, update, adaptor.finalize)

probjax.inference.fit_adaptor

fit_adaptor(loss_fn, optimizer, *, field=None, data_fn=None, buffer_size=4096, batch_size=256, every=1, num_fit_steps=1, axis_name=None, seed=0)

Train learned parameters on the chain's own output, inside the scan.

Each transition appends data_fn(state, info) -> (points, weights) to a ring buffer of buffer_size points shaped like state.position. Every every transitions the adaptor takes num_fit_steps optimizer steps on loss_fn(params, points, weights) over minibatches of batch_size buffered points (None: the whole buffer). every=1, num_fit_steps=1 trains a little after every transition; every=200, num_fit_steps=200 alternates sampling and fitting blocks. Both run fully compiled.

Parameters:

Name Type Description Default
loss_fn Callable[[Any, Any, Any], Any]

the training loss, e.g. :func:maximum_likelihood.

required
optimizer GradientTransformation

an optax transformation.

required
field str | None

the parameter field to train; None trains all of params.

None
data_fn Callable[[Any, Any], tuple[Any, Any]] | None

what to store per transition; defaults to the new position with weight one.

None
axis_name str | None

average gradients over this jax.vmap axis, so chains run in parallel train one shared set of parameters.

None
seed int

seeds the minibatch draws (adaptors receive no key).

0
Source code in probjax/inference/adaptation.py
def fit_adaptor(
    loss_fn: Callable[[Any, Any, Any], Any],
    optimizer: optax.GradientTransformation,
    *,
    field: str | None = None,
    data_fn: Callable[[Any, Any], tuple[Any, Any]] | None = None,
    buffer_size: int = 4096,
    batch_size: int | None = 256,
    every: int = 1,
    num_fit_steps: int = 1,
    axis_name: str | None = None,
    seed: int = 0,
) -> Adaptor:
    """Train learned parameters on the chain's own output, inside the scan.

    Each transition appends ``data_fn(state, info) -> (points, weights)`` to a
    ring buffer of ``buffer_size`` points shaped like ``state.position``.
    Every ``every`` transitions the adaptor takes ``num_fit_steps`` optimizer
    steps on ``loss_fn(params, points, weights)`` over minibatches of
    ``batch_size`` buffered points (``None``: the whole buffer). ``every=1,
    num_fit_steps=1`` trains a little after every transition;
    ``every=200, num_fit_steps=200`` alternates sampling and fitting blocks.
    Both run fully compiled.

    Args:
        loss_fn: the training loss, e.g. :func:`maximum_likelihood`.
        optimizer: an optax transformation.
        field: the parameter field to train; ``None`` trains all of ``params``.
        data_fn: what to store per transition; defaults to the new position
            with weight one.
        axis_name: average gradients over this ``jax.vmap`` axis, so chains
            run in parallel train one shared set of parameters.
        seed: seeds the minibatch draws (adaptors receive no key).
    """
    if data_fn is None:
        data_fn = _position_data

    def read(params: Any) -> Any:
        return params if field is None else get_param(params, field)

    def write(params: Any, value: Any) -> Any:
        return value if field is None else replace_params(params, **{field: value})

    def init(state: Any, params: Any) -> FitState:
        points = jax.tree.map(
            lambda leaf: jnp.zeros(
                (buffer_size,) + jnp.shape(leaf), jnp.result_type(leaf)
            ),
            state.position,
        )
        return FitState(
            points,
            jnp.zeros((buffer_size,)),
            jnp.zeros((), jnp.int32),
            jnp.zeros((), jnp.int32),
            optimizer.init(read(params)),
            jnp.full((), jnp.nan),
        )

    def fit(key: Any, fit_state: FitState, trained: Any) -> tuple[Any, Any, Any]:
        num_filled = jnp.minimum(fit_state.num_seen, buffer_size)

        def one_step(carry, step_key):
            trained, opt_state = carry
            if batch_size is None:
                points, weights = fit_state.points, fit_state.weights
            else:
                index = jax.random.randint(step_key, (batch_size,), 0, num_filled)
                points = jax.tree.map(lambda leaf: leaf[index], fit_state.points)
                weights = fit_state.weights[index]
            loss, grads = jax.value_and_grad(loss_fn)(trained, points, weights)
            if axis_name is not None:
                loss, grads = jax.lax.pmean((loss, grads), axis_name)
            updates, opt_state = optimizer.update(grads, opt_state, trained)
            return (optax.apply_updates(trained, updates), opt_state), loss

        (trained, opt_state), losses = jax.lax.scan(
            one_step,
            (trained, fit_state.opt_state),
            jax.random.split(key, num_fit_steps),
        )
        return trained, opt_state, losses[-1]

    def update(
        state: Any, info: Any, fit_state: FitState, params: Any
    ) -> tuple[FitState, Any, Any]:
        points, weights = data_fn(state, info)
        slots = (fit_state.num_seen + jnp.arange(weights.shape[0])) % buffer_size
        fit_state = fit_state._replace(
            points=jax.tree.map(
                lambda buffer, new: buffer.at[slots].set(new), fit_state.points, points
            ),
            weights=fit_state.weights.at[slots].set(weights),
            num_seen=fit_state.num_seen + weights.shape[0],
            num_updates=fit_state.num_updates + 1,
        )
        # Wait for a full batch: fitting a flow to a few copies of the
        # starting point collapses it onto that point.
        do_fit = (fit_state.num_updates % every == 0) & (
            fit_state.num_seen >= (batch_size or 1)
        )
        key = jax.random.fold_in(jax.random.key(seed), fit_state.num_updates)
        trained, opt_state, loss = jax.lax.cond(
            do_fit,
            lambda: fit(key, fit_state, read(params)),
            lambda: (read(params), fit_state.opt_state, fit_state.loss),
        )
        fit_state = fit_state._replace(opt_state=opt_state, loss=loss)
        return fit_state, write(params, trained), loss

    def finalize(fit_state: FitState, params: Any) -> tuple[Any, Any]:
        return params, fit_state.loss

    return Adaptor(init, update, finalize)

probjax.inference.maximum_likelihood

maximum_likelihood(density)

Weighted negative log-likelihood of density, for :func:fit_adaptor.

Source code in probjax/inference/adaptation.py
def maximum_likelihood(density: Density) -> Callable[[Any, Any, Any], Any]:
    """Weighted negative log-likelihood of ``density``, for :func:`fit_adaptor`."""

    def loss(params: Any, points: Any, weights: Any) -> Any:
        log_q = jax.vmap(density.logpdf, in_axes=(0, None))(points, params)
        # Empty buffer slots carry weight zero; keep their -inf out of the sum.
        weighted = jnp.where(weights > 0, weights * log_q, 0.0)
        return -jnp.sum(weighted) / jnp.maximum(jnp.sum(weights), 1e-12)

    return loss

Convergence diagnostics

probjax.inference.effective_sample_size

effective_sample_size(input_array, chain_axis=0, sample_axis=1)

Compute estimate of the effective sample size (ess).

Parameters

input_array: An array representing multiple chains of MCMC samples. The array must contains a chain dimension and a sample dimension. chain_axis The axis indicating the multiple chains. Default to 0. sample_axis The axis indicating a single chain of MCMC samples. Default to 1.

Returns

NDArray of the resulting statistics (ess), with the chain and sample dimensions squeezed. Variables whose within-chain variance is numerically zero have an effective sample size of zero. Variables containing a NaN draw are undefined and report NaN; the reduction is per variable, so an independent finite variable is unaffected. Infinities are ordered values and are not treated as missing.

Notes

The basic ess (:math:N_{\mathit{eff}}) diagnostic is computed by:

.. math:: \hat{N}_{\mathit{eff}} = \frac{MN}{\hat{\tau}}

.. math:: \hat{\tau} = -1 + 2 \sum_{t'=0}^K \hat{P}_{t'}

where :math:M is the number of chains, :math:N the number of draws, :math:\hat{\rho}_t is the estimated _autocorrelation at lag :math:t, and :math:K is the last integer for which :math:\hat{P}_{K} = \hat{\rho}_{2K} + \hat{\rho}_{2K+1} is still positive :cite:p:stan_ess,gelman1995bayesian.

The current implementation is similar to Stan, which uses Geyer's initial monotone sequence criterion :cite:p:geyer1992practical,geyer2011introduction.

Source code in blackjax/diagnostics.py
def effective_sample_size(
    input_array: ArrayLike, chain_axis: int = 0, sample_axis: int = 1
) -> Array:
    """Compute estimate of the effective sample size (ess).

    Parameters
    ----------
    input_array:
        An array representing multiple chains of MCMC samples. The array must
        contains a chain dimension and a sample dimension.
    chain_axis
        The axis indicating the multiple chains. Default to 0.
    sample_axis
        The axis indicating a single chain of MCMC samples. Default to 1.

    Returns
    -------
    NDArray of the resulting statistics (ess), with the chain and sample dimensions squeezed.
    Variables whose within-chain variance is numerically zero have an effective
    sample size of zero.  Variables containing a ``NaN`` draw are undefined and
    report ``NaN``; the reduction is per variable, so an independent finite
    variable is unaffected.  Infinities are ordered values and are not treated
    as missing.

    Notes
    -----
    The basic ess (:math:`N_{\\mathit{eff}}`) diagnostic is computed by:

    .. math:: \\hat{N}_{\\mathit{eff}} = \\frac{MN}{\\hat{\\tau}}

    .. math:: \\hat{\\tau} = -1 + 2 \\sum_{t'=0}^K \\hat{P}_{t'}

    where :math:`M` is the number of chains, :math:`N` the number of draws,
    :math:`\\hat{\\rho}_t` is the estimated _autocorrelation at lag :math:`t`, and
    :math:`K` is the last integer for which :math:`\\hat{P}_{K} = \\hat{\\rho}_{2K} +
    \\hat{\\rho}_{2K+1}` is still positive :cite:p:`stan_ess,gelman1995bayesian`.

    The current implementation is similar to Stan, which uses Geyer's initial monotone sequence
    criterion :cite:p:`geyer1992practical,geyer2011introduction`.

    """
    input_shape = input_array.shape
    sample_axis = sample_axis if sample_axis >= 0 else len(input_shape) + sample_axis
    num_chains = input_shape[chain_axis]
    num_samples = input_shape[sample_axis]
    assert (
        num_samples > 1
    ), f"The input array must have at least 2 samples, got only {num_samples}."

    first_sample = jnp.take(input_array, jnp.array([0]), axis=sample_axis)
    has_within_chain_variation = jnp.any(
        input_array != first_sample,
        axis=(chain_axis, sample_axis),
        keepdims=True,
    )

    mean_across_chain = input_array.mean(axis=sample_axis, keepdims=True)
    # Compute autocovariance estimates for every lag for the input array using FFT.
    centered_array = input_array - mean_across_chain
    m = next_fast_len(2 * num_samples)
    ifft_ary = jnp.fft.rfft(centered_array, n=m, axis=sample_axis)
    ifft_ary *= jnp.conjugate(ifft_ary)
    autocov_value = jnp.fft.irfft(ifft_ary, n=m, axis=sample_axis)
    autocov_value = (
        jnp.take(autocov_value, jnp.arange(num_samples), axis=sample_axis) / num_samples
    )
    mean_autocov_var = autocov_value.mean(chain_axis, keepdims=True)
    mean_var0 = (
        jnp.take(mean_autocov_var, jnp.array([0]), axis=sample_axis)
        * num_samples
        / (num_samples - 1.0)
    )
    is_numerically_degenerate = jnp.isfinite(mean_var0) & (
        ~has_within_chain_variation | (mean_var0 <= 0.0)
    )
    weighted_var = mean_var0 * (num_samples - 1.0) / num_samples
    weighted_var = jax.lax.cond(
        num_chains > 1,
        lambda mean_across_chain: weighted_var
        + mean_across_chain.var(axis=chain_axis, ddof=1, keepdims=True),
        lambda _: weighted_var,
        operand=mean_across_chain,
    )
    weighted_var = jnp.where(
        is_numerically_degenerate, jnp.ones_like(weighted_var), weighted_var
    )

    # Geyer's initial positive sequence
    num_samples_even = num_samples - num_samples % 2
    mean_autocov_var_tp1 = jnp.take(
        mean_autocov_var, jnp.arange(1, num_samples_even), axis=sample_axis
    )
    rho_hat = jnp.concatenate(
        [
            jnp.ones_like(mean_var0),
            1.0 - (mean_var0 - mean_autocov_var_tp1) / weighted_var,
        ],
        axis=sample_axis,
    )

    rho_hat = jnp.moveaxis(rho_hat, sample_axis, 0)
    rho_hat_even = rho_hat[0::2]
    rho_hat_odd = rho_hat[1::2]

    mask0 = (rho_hat_even + rho_hat_odd) > 0.0
    carry_cond = jnp.ones_like(mask0[0])
    max_t = jnp.zeros_like(mask0[0], dtype=int)

    def positive_sequence_body_fn(state, mask_t):
        t, carry_cond, max_t = state
        next_mask = carry_cond & mask_t
        next_max_t = jnp.where(next_mask, jnp.ones_like(max_t) * t, max_t)
        return (t + 1, next_mask, next_max_t), next_mask

    (*_, max_t_next), mask = jax.lax.scan(
        positive_sequence_body_fn, (0, carry_cond, max_t), mask0
    )
    indices = jnp.indices(max_t_next.shape)
    indices = tuple([max_t_next + 1] + [indices[i] for i in range(max_t_next.ndim)])
    rho_hat_odd = jnp.where(mask, rho_hat_odd, jnp.zeros_like(rho_hat_odd))
    # improve estimation
    mask_even = mask.at[indices].set(rho_hat_even[indices] > 0)
    rho_hat_even = jnp.where(mask_even, rho_hat_even, jnp.zeros_like(rho_hat_even))

    # Geyer's initial monotone sequence
    def monotone_sequence_body_fn(rho_hat_sum_tm1, rho_hat_sum_t):
        update_mask = rho_hat_sum_t > rho_hat_sum_tm1
        next_rho_hat_sum_t = jnp.where(update_mask, rho_hat_sum_tm1, rho_hat_sum_t)
        return next_rho_hat_sum_t, (update_mask, next_rho_hat_sum_t)

    rho_hat_sum = rho_hat_even + rho_hat_odd
    _, (update_mask, update_value) = jax.lax.scan(
        monotone_sequence_body_fn, rho_hat_sum[0], rho_hat_sum
    )

    rho_hat_even_final = jnp.where(update_mask, update_value / 2.0, rho_hat_even)
    rho_hat_odd_final = jnp.where(update_mask, update_value / 2.0, rho_hat_odd)

    # compute effective sample size
    ess_raw = num_chains * num_samples
    tau_hat = (
        -1.0
        + 2.0 * jnp.sum(rho_hat_even_final + rho_hat_odd_final, axis=0)
        - rho_hat_even_final[indices]
    )

    tau_hat = jnp.maximum(tau_hat, 1 / np.log10(ess_raw))
    ess = ess_raw / tau_hat
    ess = jnp.where(is_numerically_degenerate.squeeze(), 0.0, ess.squeeze())

    # A NaN draw is a missing observation, not a value the estimator can use.
    # Geyer's truncation above gates on partial sums being > 0, and every such
    # comparison is False for NaN, so the truncation collapses and tau_hat
    # falls back to its 1 / log10(MN) floor — manufacturing a large, entirely
    # fictitious sample size.  Report the component as undefined instead.  The
    # reduction runs over the chain and draw axes only, so a contaminated
    # component never poisons an independent finite one, and ``.squeeze()``
    # mirrors the squeeze applied to ``ess`` just above.
    has_nan = jnp.any(jnp.isnan(input_array), axis=(chain_axis, sample_axis))
    ess = jnp.where(jnp.squeeze(has_nan), jnp.nan, ess)

    return ess

probjax.inference.ess_bulk

ess_bulk(input_array, chain_axis=0, sample_axis=1)

Bulk effective sample size (rank-normalized split-chain ESS).

Computes the bulk ESS from Vehtari et al. (2021): rank-normalizes draws after splitting each chain in half, then applies the standard autocorrelation-based :func:effective_sample_size estimator. This diagnostic is robust to non-stationarity and multimodality.

Parameters

input_array An array representing multiple chains of MCMC samples. The array must contain a chain dimension and a sample dimension. chain_axis The axis indicating the multiple chains. Default 0. sample_axis The axis indicating a single chain of MCMC samples. Default 1.

Returns

NDArray of the resulting bulk-ESS, with chain and sample dimensions squeezed.

Notes

Algorithm:

  1. Split each chain in half → 2× chains.
  2. Pool all draws and rank-normalize with :math:z_r = \Phi^{-1}((r-3/8)/(n+1/4)).
  3. Apply :func:effective_sample_size to the rank-normalized draws.

References

.. cite:p:vehtari2021rank

Source code in blackjax/diagnostics.py
def ess_bulk(
    input_array: ArrayLike, chain_axis: int = 0, sample_axis: int = 1
) -> Array:
    """Bulk effective sample size (rank-normalized split-chain ESS).

    Computes the bulk ESS from Vehtari et al. (2021): rank-normalizes draws
    after splitting each chain in half, then applies the standard
    autocorrelation-based :func:`effective_sample_size` estimator.  This
    diagnostic is robust to non-stationarity and multimodality.

    Parameters
    ----------
    input_array
        An array representing multiple chains of MCMC samples. The array must
        contain a chain dimension and a sample dimension.
    chain_axis
        The axis indicating the multiple chains. Default 0.
    sample_axis
        The axis indicating a single chain of MCMC samples. Default 1.

    Returns
    -------
    NDArray of the resulting bulk-ESS, with chain and sample dimensions squeezed.

    Notes
    -----
    Algorithm:

    1. Split each chain in half → 2× chains.
    2. Pool all draws and rank-normalize with :math:`z_r = \\Phi^{-1}((r-3/8)/(n+1/4))`.
    3. Apply :func:`effective_sample_size` to the rank-normalized draws.

    References
    ----------
    .. cite:p:`vehtari2021rank`
    """
    x = _to_standard_axes(jnp.asarray(input_array), chain_axis, sample_axis)
    x_split = _split_chains(x)
    x_rn = _rank_normalize(x_split)
    # A contaminated component is already all-NaN after _rank_normalize, and
    # effective_sample_size propagates that on its own.
    return effective_sample_size(x_rn)

probjax.inference.ess_tail

ess_tail(input_array, chain_axis=0, sample_axis=1, prob=0.9)

Tail effective sample size.

Computes the tail ESS from Vehtari et al. (2021) as the minimum of the ESS of the lower- and upper-tail indicator functions applied to split-chain draws.

The tail quantiles are determined by prob: the lower tail uses the (1 - prob) / 2 quantile and the upper tail uses the (1 + prob) / 2 quantile. The default prob=0.90 corresponds to the 5th/95th percentiles, matching ArviZ's default prob=(0.05, 0.95).

.. warning::

The agreement with az.ess(method="tail") holds for continuous draws only. The upper-tail indicator here is :math:\mathbf{1}(x \ge q_{\text{high}}), whereas Vehtari et al. and ArviZ use :math:\mathbf{1}(x \le q) for both tails. On continuous draws the two are exact complements and the ESS is identical, but on tied draws they are not: for iid Bernoulli(0.1), :math:P(x \ge q_{95}) is 0.097 rather than 0.05, and for a 5-level grid it is 0.21. This is a separate known defect in the tail estimator's tie handling, tracked independently of the rank-normalization fix; note that simply switching to <= does not resolve it, since that indicator is identically 1 on such draws and would be reported as degenerate.

Parameters

input_array An array representing multiple chains of MCMC samples. The array must contain a chain dimension and a sample dimension. chain_axis The axis indicating the multiple chains. Default 0. sample_axis The axis indicating a single chain of MCMC samples. Default 1. prob Central-interval probability that determines the tail quantiles. Lower quantile: (1 - prob) / 2; upper quantile: (1 + prob) / 2. Default 0.90 gives the 5th/95th-percentile tail, matching az.ess(method="tail") (ArviZ default).

Returns

NDArray of the resulting tail-ESS, with chain and sample dimensions squeezed.

Notes

Algorithm:

  1. Split each chain in half → 2× chains.
  2. Compute pooled lower/upper quantiles (at (1-prob)/2 and (1+prob)/2) across all split chains and draws.
  3. Form indicator series :math:\mathbf{1}(x \le q_{\text{low}}) and :math:\mathbf{1}(x \ge q_{\text{high}}).
  4. Compute :func:effective_sample_size for each indicator.
  5. Return :math:\min(\text{ESS}_\text{lower}, \text{ESS}_\text{upper}).

References

.. cite:p:vehtari2021rank

Source code in blackjax/diagnostics.py
def ess_tail(
    input_array: ArrayLike,
    chain_axis: int = 0,
    sample_axis: int = 1,
    prob: float = 0.90,
) -> Array:
    """Tail effective sample size.

    Computes the tail ESS from Vehtari et al. (2021) as the minimum of the
    ESS of the lower- and upper-tail indicator functions applied to split-chain
    draws.

    The tail quantiles are determined by ``prob``: the lower tail uses the
    ``(1 - prob) / 2`` quantile and the upper tail uses the
    ``(1 + prob) / 2`` quantile.  The default ``prob=0.90`` corresponds to
    the 5th/95th percentiles, matching ArviZ's default ``prob=(0.05, 0.95)``.

    .. warning::

       The agreement with ``az.ess(method="tail")`` holds for **continuous**
       draws only.  The upper-tail indicator here is
       :math:`\\mathbf{1}(x \\ge q_{\\text{high}})`, whereas Vehtari et al.
       and ArviZ use :math:`\\mathbf{1}(x \\le q)` for both tails.  On
       continuous draws the two are exact complements and the ESS is
       identical, but on tied draws they are not: for iid Bernoulli(0.1),
       :math:`P(x \\ge q_{95})` is 0.097 rather than 0.05, and for a 5-level
       grid it is 0.21.  This is a separate known defect in the tail
       estimator's tie handling, tracked independently of the
       rank-normalization fix; note that simply switching to ``<=`` does not
       resolve it, since that indicator is identically 1 on such draws and
       would be reported as degenerate.

    Parameters
    ----------
    input_array
        An array representing multiple chains of MCMC samples. The array must
        contain a chain dimension and a sample dimension.
    chain_axis
        The axis indicating the multiple chains. Default 0.
    sample_axis
        The axis indicating a single chain of MCMC samples. Default 1.
    prob
        Central-interval probability that determines the tail quantiles.
        Lower quantile: ``(1 - prob) / 2``; upper quantile:
        ``(1 + prob) / 2``.  Default ``0.90`` gives the 5th/95th-percentile
        tail, matching ``az.ess(method="tail")`` (ArviZ default).

    Returns
    -------
    NDArray of the resulting tail-ESS, with chain and sample dimensions squeezed.

    Notes
    -----
    Algorithm:

    1. Split each chain in half → 2× chains.
    2. Compute pooled lower/upper quantiles (at ``(1-prob)/2`` and
       ``(1+prob)/2``) across all split chains and draws.
    3. Form indicator series :math:`\\mathbf{1}(x \\le q_{\\text{low}})` and
       :math:`\\mathbf{1}(x \\ge q_{\\text{high}})`.
    4. Compute :func:`effective_sample_size` for each indicator.
    5. Return :math:`\\min(\\text{ESS}_\\text{lower}, \\text{ESS}_\\text{upper})`.

    References
    ----------
    .. cite:p:`vehtari2021rank`
    """
    x = _to_standard_axes(jnp.asarray(input_array), chain_axis, sample_axis)
    x_split = _split_chains(x)

    nchains, nsamples = x_split.shape[0], x_split.shape[1]
    extra_shape = x_split.shape[2:]

    # Tail quantiles derived from the central-interval probability.
    q_low = (1.0 - prob) / 2.0
    q_high = (1.0 + prob) / 2.0

    # Pooled quantiles over all chains and draws, per trailing dimension.
    x_flat = x_split.reshape(nchains * nsamples, *extra_shape)
    q_lo = jnp.quantile(x_flat, q_low, axis=0)
    q_hi = jnp.quantile(x_flat, q_high, axis=0)

    # Indicator series (float for ESS computation).
    # Broadcast q_lo/q_hi over the (nchains, nsamples) leading axes.
    I_lower = (x_split <= q_lo[None, None]).astype(float)
    I_upper = (x_split >= q_hi[None, None]).astype(float)

    ess_lower = effective_sample_size(I_lower)
    ess_upper = effective_sample_size(I_upper)

    # A NaN draw makes the pooled quantiles NaN, every indicator comparison
    # False, and the resulting all-zero series degenerate — which would report
    # 0 rather than "undefined".  Guard once on the reduced value: the
    # indicators themselves are never NaN, so the estimator cannot propagate
    # this on its own the way it does for ess_bulk.
    return _propagate_nan_components(jnp.minimum(ess_lower, ess_upper), x_split)

probjax.inference.potential_scale_reduction

potential_scale_reduction(input_array, chain_axis=0, sample_axis=1)

Gelman and Rubin (1992)'s potential scale reduction for computing multiple MCMC chain convergence.

Parameters

input_array: An array representing multiple chains of MCMC samples. The array must contains a chain dimension and a sample dimension. chain_axis The axis indicating the multiple chains. Default to 0. sample_axis The axis indicating a single chain of MCMC samples. Default to 1.

Returns

NDArray of the resulting statistics (r-hat), with the chain and sample dimensions squeezed.

Notes

The diagnostic is computed by:

.. math:: \hat{R} = \sqrt{\frac{\hat{V}}{W}}

where :math:W is the within-chain variance and :math:\hat{V} is the posterior variance estimate for the pooled traces. This is the potential scale reduction factor, which converges to unity when each of the traces is a sample from the target posterior. Values greater than one indicate that one or more chains have not yet converged :cite:p:stan_rhat,gelman1992inference.

Source code in blackjax/diagnostics.py
def potential_scale_reduction(
    input_array: ArrayLike, chain_axis: int = 0, sample_axis: int = 1
) -> Array:
    """Gelman and Rubin (1992)'s potential scale reduction for computing multiple MCMC chain convergence.

    Parameters
    ----------
    input_array:
        An array representing multiple chains of MCMC samples. The array must
        contains a chain dimension and a sample dimension.
    chain_axis
        The axis indicating the multiple chains. Default to 0.
    sample_axis
        The axis indicating a single chain of MCMC samples. Default to 1.

    Returns
    -------
    NDArray of the resulting statistics (r-hat), with the chain and sample dimensions squeezed.

    Notes
    -----
    The diagnostic is computed by:

    .. math:: \\hat{R} = \\sqrt{\\frac{\\hat{V}}{W}}

    where :math:`W` is the within-chain variance and :math:`\\hat{V}` is the posterior variance
    estimate for the pooled traces. This is the potential scale reduction factor, which
    converges to unity when each of the traces is a sample from the target posterior. Values
    greater than one indicate that one or more chains have not yet converged :cite:p:`stan_rhat,gelman1992inference`.

    """
    assert (
        input_array.shape[chain_axis] > 1
    ), "potential_scale_reduction as implemented only works for two or more chains."

    num_samples = input_array.shape[sample_axis]
    # Compute stats for each chain
    per_chain_mean = input_array.mean(axis=sample_axis, keepdims=True)
    per_chain_var = input_array.var(axis=sample_axis, ddof=1, keepdims=True)
    # Compute between-chain stats
    between_chain_variance = num_samples * per_chain_mean.var(
        axis=chain_axis, ddof=1, keepdims=True
    )
    # Compute within-chain stats
    within_chain_variance = per_chain_var.mean(axis=chain_axis, keepdims=True)
    # Estimate of marginal posterior variance
    rhat_value = jnp.sqrt(
        (between_chain_variance / within_chain_variance + num_samples - 1)
        / (num_samples)
    )
    return rhat_value.squeeze()

probjax.inference.rhat

rhat(input_array, chain_axis=0, sample_axis=1)

Rank-normalized split-RÌ‚ (Vehtari et al. 2021).

The modern improved R̂ diagnostic. Combines two split-chain R̂ values — one on rank-normalized draws and one on rank-normalized folded draws — and returns the maximum. The folded component catches scale/variance non-convergence that the bulk component can miss.

This matches the default az.rhat(method="rank") convention in ArviZ.

Parameters

input_array An array representing multiple chains of MCMC samples. The array must contain a chain dimension and a sample dimension. At least 2 chains and at least 4 draws per chain are required. chain_axis The axis indicating the multiple chains. Default 0. sample_axis The axis indicating a single chain of MCMC samples. Default 1.

Returns

NDArray of the resulting RÌ‚ values, with chain and sample dimensions squeezed. Values close to 1.0 indicate convergence; values above 1.01 suggest chains have not converged.

Notes

Algorithm (Vehtari et al. 2021, § 4):

  1. Split each chain in half → 2× chains.
  2. Rank-normalize with the Blom plotting position :math:z_r = \Phi^{-1}((r - 3/8) / (n + 1/4)) over the joint pool.
  3. Compute the standard split-RÌ‚ on the rank-normalized draws (bulk).
  4. Compute the folded draws :math:|x - \mathrm{median}(x)|, rank-normalize them, and compute split-RÌ‚ again (tail).
  5. Return :math:\max(\hat{R}_{\text{bulk}}, \hat{R}_{\text{tail}}).

.. warning::

NaN from this function does not uniquely mean "the draws contained a missing observation". The folded component subtracts the pooled median, so when at least half of a component's pooled draws are +inf (or -inf) the median is infinite and the fold evaluates inf - inf, which is NaN. Such a component returns NaN from :func:rhat while containing no NaN at all — and while :func:ess_bulk and :func:ess_tail still return finite values for it. Infinity handling is a separate open question from the missing-data contract; this is documented, not yet decided.

References

.. cite:p:vehtari2021rank

Source code in blackjax/diagnostics.py
def rhat(input_array: ArrayLike, chain_axis: int = 0, sample_axis: int = 1) -> Array:
    """Rank-normalized split-RÌ‚ (Vehtari et al. 2021).

    The modern improved R̂ diagnostic.  Combines two split-chain R̂ values —
    one on rank-normalized draws and one on rank-normalized *folded* draws —
    and returns the maximum.  The folded component catches scale/variance
    non-convergence that the bulk component can miss.

    This matches the default ``az.rhat(method="rank")`` convention in ArviZ.

    Parameters
    ----------
    input_array
        An array representing multiple chains of MCMC samples. The array must
        contain a chain dimension and a sample dimension.  At least 2 chains
        and at least 4 draws per chain are required.
    chain_axis
        The axis indicating the multiple chains. Default 0.
    sample_axis
        The axis indicating a single chain of MCMC samples. Default 1.

    Returns
    -------
    NDArray of the resulting RÌ‚ values, with chain and sample dimensions
    squeezed.  Values close to 1.0 indicate convergence; values above 1.01
    suggest chains have not converged.

    Notes
    -----
    Algorithm (Vehtari et al. 2021, § 4):

    1. Split each chain in half → 2× chains.
    2. Rank-normalize with the Blom plotting position
       :math:`z_r = \\Phi^{-1}((r - 3/8) / (n + 1/4))` over the joint pool.
    3. Compute the standard split-RÌ‚ on the rank-normalized draws (**bulk**).
    4. Compute the folded draws :math:`|x - \\mathrm{median}(x)|`, rank-normalize
       them, and compute split-RÌ‚ again (**tail**).
    5. Return :math:`\\max(\\hat{R}_{\\text{bulk}}, \\hat{R}_{\\text{tail}})`.

    .. warning::

       ``NaN`` from this function does **not** uniquely mean "the draws
       contained a missing observation".  The folded component subtracts the
       pooled median, so when at least half of a component's pooled draws are
       ``+inf`` (or ``-inf``) the median is infinite and the fold evaluates
       ``inf - inf``, which is ``NaN``.  Such a component returns ``NaN`` from
       :func:`rhat` while containing no ``NaN`` at all — and while
       :func:`ess_bulk` and :func:`ess_tail` still return finite values for it.
       Infinity handling is a separate open question from the missing-data
       contract; this is documented, not yet decided.

    References
    ----------
    .. cite:p:`vehtari2021rank`
    """
    x = _to_standard_axes(jnp.asarray(input_array), chain_axis, sample_axis)
    # Split each chain in half → (2*nchains, nsamples//2, …).
    x_split = _split_chains(x)

    # Bulk: rank-normalize split draws, then compute split-RÌ‚.
    x_rn = _rank_normalize(x_split)
    rhat_bulk = potential_scale_reduction(x_rn, chain_axis=0, sample_axis=1)

    # Tail: fold the split draws about their joint median, rank-normalize,
    # then compute split-RÌ‚.  Catches variance non-convergence.
    nchains_split = x_split.shape[0]
    nsamples_split = x_split.shape[1]
    extra_shape = x_split.shape[2:]
    x_flat = x_split.reshape(nchains_split * nsamples_split, *extra_shape)
    # Global median per trailing dimension (scalar when extra_shape is empty).
    x_folded = jnp.abs(x_split - jnp.median(x_flat, axis=0))
    x_folded_rn = _rank_normalize(x_folded)
    rhat_tail = potential_scale_reduction(x_folded_rn, chain_axis=0, sample_axis=1)

    return jnp.maximum(rhat_bulk, rhat_tail)

probjax.inference.pareto_khat

pareto_khat(x, tail='both', tail_frac=0.1)

Pareto shape parameter k̂ for tail diagnosis.

Fits a Generalised Pareto Distribution (GPD) to the upper and/or lower tail of a 1-D sample and returns the estimated shape parameter k̂.

Parameters

x 1-D array of draws (or any array; it is ravelled before use). tail Which tail to fit: "upper", "lower", or "both" (default). When "both", returns the maximum of the two k̂ estimates. tail_frac Fraction of samples used as the tail. Default 0.10 (10 %). A minimum of 5 tail samples is always enforced.

Returns

Scalar Array: the Pareto shape estimate k̂. Values below 0.5 indicate reliable tail estimates; 0.5–0.7 are moderate; above 0.7 may be unreliable.

Notes

Uses the Zhang & Stephens (2009) empirical-Bayes estimator implemented in the internal :func:_gpdfit. The upper tail is modelled directly; the lower tail is reflected and modelled as an upper tail.

Source code in blackjax/diagnostics.py
def pareto_khat(x: ArrayLike, tail: str = "both", tail_frac: float = 0.10) -> Array:
    """Pareto shape parameter k̂ for tail diagnosis.

    Fits a Generalised Pareto Distribution (GPD) to the upper and/or lower
    tail of a 1-D sample and returns the estimated shape parameter k̂.

    Parameters
    ----------
    x
        1-D array of draws (or any array; it is ravelled before use).
    tail
        Which tail to fit: ``"upper"``, ``"lower"``, or ``"both"`` (default).
        When ``"both"``, returns the maximum of the two k̂ estimates.
    tail_frac
        Fraction of samples used as the tail. Default 0.10 (10 %).
        A minimum of 5 tail samples is always enforced.

    Returns
    -------
    Scalar Array: the Pareto shape estimate k̂.  Values below 0.5 indicate
    reliable tail estimates; 0.5–0.7 are moderate; above 0.7 may be
    unreliable.

    Notes
    -----
    Uses the Zhang & Stephens (2009) empirical-Bayes estimator implemented
    in the internal :func:`_gpdfit`.  The upper tail is modelled directly;
    the lower tail is reflected and modelled as an upper tail.
    """
    x_flat = jnp.asarray(x).ravel()
    n = x_flat.shape[0]
    tail_size = max(int(tail_frac * n), 5)

    x_sorted = jnp.sort(x_flat)  # ascending

    if tail in ("upper", "both"):
        upper_tail = x_sorted[n - tail_size :]
        threshold_upper = x_sorted[n - tail_size - 1]
        exc_upper = upper_tail - threshold_upper  # >= 0, ascending
        k_upper, _ = _gpdfit(exc_upper)

    if tail in ("lower", "both"):
        # Reflect the lower tail so it becomes an upper-tail problem.
        lower_tail_reflected = -x_sorted[:tail_size][::-1]  # ascending
        threshold_lower = -x_sorted[tail_size]
        exc_lower = lower_tail_reflected - threshold_lower  # >= 0, ascending
        k_lower, _ = _gpdfit(exc_lower)

    if tail == "upper":
        return k_upper
    if tail == "lower":
        return k_lower
    return jnp.maximum(k_upper, k_lower)

probjax.inference.kernelized_stein_discrepancy

kernelized_stein_discrepancy(samples, grad_logdensity_fn, kernel, *, statistic='v')

Estimate squared kernelized Stein discrepancy with a V- or U-statistic.

Parameters

samples PyTree (an array, or a dict/tuple/NamedTuple of per-field arrays) whose leaves share a leading num_samples axis. Chain dimensions must be pooled explicitly before calling this function. grad_logdensity_fn Target score function, mapping one sample (one leading-axis slice of samples, with the same PyTree structure) to the gradient of its log density. For example, jax.grad(logdensity_fn). The normalizing constant is not required. kernel Scalar-valued, twice differentiable positive semidefinite kernel taking two flattened sample vectors. Bind kernel parameters such as bandwidth with functools.partial. The kernel must satisfy the target's Stein boundary conditions; choosing an appropriate kernel is the caller's responsibility.

statistic "v" includes diagonal pairs; "u" excludes them and requires at least two draws. The U-statistic can be negative, even in exact arithmetic.

Returns

Estimate of squared KSD, not a calibrated p-value or convergence certificate. The V-statistic has a sample-dependent diagonal bias of order 1/n.

Notes

Complements rather than replaces R-hat. Approximately independent draws are assumed; autocorrelation and an unvisited mode can make the result misleading. A positive semidefinite kernel alone does not identify every distribution: this requires additional conditions, including integral strict positive definiteness :cite:p:liu2016kernelized. Gaussian RBF kernels do not in general detect non-convergence in dimension three or higher. IMQ kernels have convergence guarantees for suitable targets, but targets with bounded scores, including some heavy-tailed targets, remain a gap :cite:p:gorham2017kernels.

Uses the score/kernel derivative expression in :cite:p:liu2016kernelized. Rows are processed sequentially and columns are vectorized. The computation has quadratic cost in sample count and mixed Hessians with d² entries; temporary pairwise storage scales as O(n*d²) rather than O(n²*d²). The supplied functions must support JAX differentiation and transformations. No clipping is applied to either statistic.

Vendored from unreleased BlackJAX main; see the module docstring.

Source code in probjax/inference/diagnostics.py
def kernelized_stein_discrepancy(
    samples: ArrayLikeTree,
    grad_logdensity_fn: Callable,
    kernel: Callable,
    *,
    statistic: str = "v",
) -> Array:
    """Estimate squared kernelized Stein discrepancy with a V- or U-statistic.

    Parameters
    ----------
    samples
        PyTree (an array, or a dict/tuple/NamedTuple of per-field arrays) whose
        leaves share a leading ``num_samples`` axis. Chain dimensions must be
        pooled explicitly before calling this function.
    grad_logdensity_fn
        Target score function, mapping one sample (one leading-axis slice of
        ``samples``, with the same PyTree structure) to the gradient of its log
        density. For example, ``jax.grad(logdensity_fn)``. The normalizing
        constant is not required.
    kernel
        Scalar-valued, twice differentiable positive semidefinite kernel taking
        two flattened sample vectors. Bind kernel parameters such as bandwidth
        with ``functools.partial``. The kernel must satisfy the target's Stein
        boundary conditions; choosing an appropriate kernel is the caller's
        responsibility.

    statistic
        ``"v"`` includes diagonal pairs; ``"u"`` excludes them and requires at
        least two draws. The U-statistic can be negative, even in exact arithmetic.

    Returns
    -------
    Estimate of squared KSD, not a calibrated p-value or convergence certificate.
    The V-statistic has a sample-dependent diagonal bias of order ``1/n``.

    Notes
    -----
    Complements rather than replaces R-hat. Approximately independent draws are
    assumed; autocorrelation and an unvisited mode can make the result misleading.
    A positive semidefinite kernel alone does not identify every distribution:
    this requires additional conditions, including integral strict positive
    definiteness :cite:p:`liu2016kernelized`. Gaussian RBF kernels do not in
    general detect non-convergence in dimension three or higher. IMQ kernels
    have convergence guarantees for suitable targets, but targets with bounded
    scores, including some heavy-tailed targets, remain a gap
    :cite:p:`gorham2017kernels`.

    Uses the score/kernel derivative expression in :cite:p:`liu2016kernelized`.
    Rows are processed sequentially and columns are vectorized. The computation
    has quadratic cost in sample count and mixed Hessians with ``d²`` entries;
    temporary pairwise storage scales as ``O(n*d²)`` rather than ``O(n²*d²)``.
    The supplied functions must support JAX differentiation and transformations.
    No clipping is applied to either statistic.

    Vendored from unreleased BlackJAX main; see the module docstring.
    """
    if statistic not in ("v", "u"):
        raise ValueError("statistic must be v or u")
    flat = jax.vmap(lambda s: ravel_pytree(s)[0])(samples)
    scores = jax.vmap(lambda s: ravel_pytree(grad_logdensity_fn(s))[0])(samples)
    kernel_value_and_grad = jax.value_and_grad(kernel, argnums=(0, 1))
    mixed_derivative = jax.jacfwd(jax.grad(kernel, argnums=0), argnums=1)

    def stein_kernel(x, y, score_x, score_y):
        value, (grad_x, grad_y) = kernel_value_and_grad(x, y)
        return (
            value * jnp.dot(score_x, score_y)
            + jnp.dot(score_x, grad_y)
            + jnp.dot(grad_x, score_y)
            + jnp.trace(mixed_derivative(x, y))
        )

    def row_sum(args):
        x, score_x = args
        return jax.vmap(stein_kernel, in_axes=(None, 0, None, 0))(
            x, flat, score_x, scores
        ).sum()

    n = flat.shape[0]
    total = jax.lax.map(row_sum, (flat, scores)).sum()
    if statistic == "v":
        return total / n**2
    diagonal = jax.vmap(stein_kernel)(flat, flat, scores, scores).sum()
    return (total - diagonal) / (n * (n - 1))

probjax.inference.imq_kernel

imq_kernel(x, y, c=1.0, beta=-0.5)

Inverse multiquadric kernel (c² + ||x-y||²)**beta.

Use c > 0 and -1 < beta < 0. Bind non-default parameters with functools.partial. Convergence guarantees additionally require target conditions; see :cite:p:gorham2017kernels.

Vendored from unreleased BlackJAX main; see the module docstring.

Source code in probjax/inference/diagnostics.py
def imq_kernel(x: Array, y: Array, c: float = 1.0, beta: float = -0.5) -> Array:
    """Inverse multiquadric kernel ``(c² + ||x-y||²)**beta``.

    Use ``c > 0`` and ``-1 < beta < 0``. Bind non-default parameters with
    ``functools.partial``. Convergence guarantees additionally require target
    conditions; see :cite:p:`gorham2017kernels`.

    Vendored from unreleased BlackJAX main; see the module docstring.
    """
    return (c**2 + jnp.sum((x - y) ** 2)) ** beta

probjax.inference.divergence_concentration

divergence_concentration(is_divergent, *, rate_threshold=0.02)

Flag a run where sampling-phase divergences concentrate on a minority of chains.

Parameters

is_divergent Per-draw divergence flags (bool or 0/1) for the sampling (post-warmup) phase only, shape (n_chains, n_draws). rate_threshold Minimum per-chain divergence rate for chain k to count as flagged. Default 0.02 (2%).

Returns

:class:DivergenceConcentrationReport. warn is true iff between 1 and max(1, n_chains // 4) chains are flagged -- a minority-outlier trigger; an ensemble where most/all chains cross the threshold returns populated fields but no warning.

Notes

  • Counting is sampling-phase only; warmup divergences are out of scope.
  • Concatenating warmup draws in front of sampling draws dilutes a real signal below threshold rather than raising a false alarm -- slice to sampling draws only before calling this.
  • multinomial_p_value is context only; it never decides warn.
Source code in blackjax/diagnostics.py
def divergence_concentration(
    is_divergent: ArrayLike,
    *,
    rate_threshold: float = 0.02,
) -> DivergenceConcentrationReport:
    """Flag a run where sampling-phase divergences concentrate on a minority of chains.

    Parameters
    ----------
    is_divergent
        Per-draw divergence flags (bool or 0/1) for the sampling
        (post-warmup) phase only, shape ``(n_chains, n_draws)``.
    rate_threshold
        Minimum per-chain divergence rate for chain ``k`` to count as
        flagged. Default ``0.02`` (2%).

    Returns
    -------
    :class:`DivergenceConcentrationReport`. ``warn`` is true iff between
    1 and ``max(1, n_chains // 4)`` chains are flagged -- a
    minority-outlier trigger; an ensemble where most/all chains cross the
    threshold returns populated fields but no warning.

    Notes
    -----
    - Counting is sampling-phase only; warmup divergences are out of scope.
    - Concatenating warmup draws in front of sampling draws dilutes a real
      signal below threshold rather than raising a false alarm -- slice to
      sampling draws only before calling this.
    - ``multinomial_p_value`` is context only; it never decides ``warn``.
    """
    # NaN must flag, not silently pass.
    is_divergent = jnp.asarray(is_divergent).astype(bool)
    n_draws = is_divergent.shape[-1]
    num_chains = is_divergent.shape[-2]
    counts = jnp.sum(is_divergent, axis=-1)

    quarter = max(n_draws // 4, 1)
    early_rate = jnp.sum(is_divergent[:, :quarter], axis=-1) / quarter
    late_rate = jnp.sum(is_divergent[:, n_draws - quarter :], axis=-1) / quarter

    return _divergence_concentration_core(
        counts,
        n_draws,
        num_chains,
        early_rate,
        late_rate,
        rate_threshold=rate_threshold,
    )

probjax.inference.divergence_concentration_from_counts

divergence_concentration_from_counts(chain_divergence_counts, n_draws, *, rate_threshold=0.02)

Same as :func:divergence_concentration, from precomputed per-chain counts.

Use when per-draw flags are not retained but per-chain totals are. early_rate / late_rate are NaN (no per-draw resolution for a quarter profile); :func:format_divergence_warning omits that part of the message rather than printing "nan%".

Parameters

chain_divergence_counts Per-chain divergence counts for the sampling phase, shape (n_chains,). n_draws Number of sampling draws per chain. rate_threshold See :func:divergence_concentration.

Returns

:class:DivergenceConcentrationReport

Source code in blackjax/diagnostics.py
def divergence_concentration_from_counts(
    chain_divergence_counts: ArrayLike,
    n_draws: int,
    *,
    rate_threshold: float = 0.02,
) -> DivergenceConcentrationReport:
    """Same as :func:`divergence_concentration`, from precomputed per-chain counts.

    Use when per-draw flags are not retained but per-chain totals are.
    ``early_rate`` / ``late_rate`` are ``NaN`` (no per-draw resolution for
    a quarter profile); :func:`format_divergence_warning` omits that part
    of the message rather than printing "nan%".

    Parameters
    ----------
    chain_divergence_counts
        Per-chain divergence counts for the sampling phase, shape
        ``(n_chains,)``.
    n_draws
        Number of sampling draws per chain.
    rate_threshold
        See :func:`divergence_concentration`.

    Returns
    -------
    :class:`DivergenceConcentrationReport`
    """
    # NaN must flag (as fully divergent), not silently pass.
    counts = jnp.asarray(chain_divergence_counts).astype(float)
    counts = jnp.clip(jnp.nan_to_num(counts, nan=float(n_draws)), 0.0, float(n_draws))
    num_chains = counts.shape[-1]
    nan_quarters = jnp.full((num_chains,), jnp.nan)
    return _divergence_concentration_core(
        counts,
        n_draws,
        num_chains,
        nan_quarters,
        nan_quarters,
        rate_threshold=rate_threshold,
    )

probjax.inference.format_divergence_warning

format_divergence_warning(report)

Render a :class:DivergenceConcentrationReport as a human-readable message.

Returns "" when report.warn is false. Otherwise returns one sentence per flagged chain (newline-separated when more than one chain is flagged). Not JIT-compatible by design — call it on concrete report values (e.g. after a sampling run has completed), not inside traced code.

Parameters

report A report produced by :func:divergence_concentration or :func:divergence_concentration_from_counts.

Returns

str, empty when there is nothing to warn about.

Source code in blackjax/diagnostics.py
def format_divergence_warning(report: DivergenceConcentrationReport) -> str:
    """Render a :class:`DivergenceConcentrationReport` as a human-readable message.

    Returns ``""`` when ``report.warn`` is false. Otherwise returns one
    sentence per flagged chain (newline-separated when more than one
    chain is flagged). Not JIT-compatible by design — call it on concrete
    report values (e.g. after a sampling run has completed), not inside
    traced code.

    Parameters
    ----------
    report
        A report produced by :func:`divergence_concentration` or
        :func:`divergence_concentration_from_counts`.

    Returns
    -------
    ``str``, empty when there is nothing to warn about.
    """
    if not bool(report.warn):
        return ""

    lines = []
    flagged = np.asarray(report.flagged)
    for k in np.flatnonzero(flagged):
        k = int(k)
        pct = "%.1f" % (float(report.rates[k]) * 100.0)
        median = "%.1f" % (float(report.median_other_rate[k]) * 100.0)
        early = float(report.early_rate[k])
        late = float(report.late_rate[k])
        if np.isfinite(early) and np.isfinite(late):
            early_str = "%.1f" % (early * 100.0)
            late_str = "%.1f" % (late * 100.0)
            quarters = " ({}% in the first quarter, {}% in the last)".format(
                early_str,
                late_str,
            )
        else:
            quarters = ""
        lines.append(
            f"chain {k}: {pct}% of its sampling draws diverged{quarters} — "
            f"significantly more than the other chains ({median}% median)"
            "; draws from this chain, especially early ones, may be "
            "untrustworthy."
        )
    return "\n".join(lines)

probjax.inference.DivergenceConcentrationReport

Bases: NamedTuple

Per-run divergence-concentration diagnostic report.

All fields are plain JAX numerics — no strings — so the statistic itself stays JIT-compilable. Pass the report to :func:format_divergence_warning to render a human-readable message from concrete (non-traced) values.

Attributes

warn Bool. Whether a minority of chains (see the module notes on :func:divergence_concentration) crossed rate_threshold. flagged Bool array, shape (n_chains,). Which chains crossed rate_threshold, independent of whether warn ends up true. num_flagged Number of flagged chains, flagged.sum(). rates Per-chain sampling-phase divergence rate, shape (n_chains,). early_rate, late_rate Per-chain divergence rate in the first and last quarter of the sampling draws, shape (n_chains,). NaN when computed from :func:divergence_concentration_from_counts (no per-draw resolution available). median_other_rate For each chain, the median rates value across the other n_chains - 1 chains, shape (n_chains,). NaN when n_chains <= 1. total_divergences Total divergence count D summed over all chains. num_chains Number of chains M. rate_threshold The threshold that was applied (echoed back for the message / for callers that only keep the report). multinomial_p_value Bonferroni-corrected exchangeable-null tail probability for the single worst chain, min(1, M * P(Binomial(D, 1/M) >= d_max)). Context only; never drives warn. NaN when D=0.

Source code in blackjax/diagnostics.py
class DivergenceConcentrationReport(NamedTuple):
    """Per-run divergence-concentration diagnostic report.

    All fields are plain JAX numerics — no strings — so the statistic
    itself stays JIT-compilable.  Pass the report to
    :func:`format_divergence_warning` to render a human-readable message
    from concrete (non-traced) values.

    Attributes
    ----------
    warn
        Bool. Whether a minority of chains (see the module notes on
        :func:`divergence_concentration`) crossed ``rate_threshold``.
    flagged
        Bool array, shape ``(n_chains,)``. Which chains crossed
        ``rate_threshold``, independent of whether ``warn`` ends up true.
    num_flagged
        Number of flagged chains, ``flagged.sum()``.
    rates
        Per-chain sampling-phase divergence rate, shape ``(n_chains,)``.
    early_rate, late_rate
        Per-chain divergence rate in the first and last quarter of the
        sampling draws, shape ``(n_chains,)``. ``NaN`` when computed from
        :func:`divergence_concentration_from_counts` (no per-draw
        resolution available).
    median_other_rate
        For each chain, the median ``rates`` value across the *other*
        ``n_chains - 1`` chains, shape ``(n_chains,)``. ``NaN`` when
        ``n_chains <= 1``.
    total_divergences
        Total divergence count ``D`` summed over all chains.
    num_chains
        Number of chains ``M``.
    rate_threshold
        The threshold that was applied (echoed back for the message /
        for callers that only keep the report).
    multinomial_p_value
        Bonferroni-corrected exchangeable-null tail probability for the
        single worst chain, ``min(1, M * P(Binomial(D, 1/M) >= d_max))``.
        Context only; never drives ``warn``. ``NaN`` when ``D=0``.
    """

    warn: Array
    flagged: Array
    num_flagged: Array
    rates: Array
    early_rate: Array
    late_rate: Array
    median_other_rate: Array
    total_divergences: Array
    num_chains: Array
    rate_threshold: Array
    multinomial_p_value: Array

Sequential Monte Carlo

See the SMC guide.

probjax.inference.smc

smc(logprior_fn, loglikelihood_fn, *, move, num_mcmc_steps=10, target_ess=None, waste_free=None, batch_size=0)

Tempered SMC with MCMC moves.

With target_ess=None the schedule is fixed and each step takes the next temperature as its argument, so run is called with args=(schedule,). With a target_ess each step picks the largest temperature that keeps that fraction of effective particles; once the temperature reaches 1 further steps leave the state unchanged, so num_steps is a cap.

Parameters:

Name Type Description Default
logprior_fn Callable

Log prior of one particle.

required
loglikelihood_fn Callable

Log likelihood of one particle; a scalar, or one term per observation for data tempering.

required
move Any

MCMC kernel moving the particles, an API class such as hmc or a configured one such as hmc(num_integration_steps=5). The SMC kernel's params are this kernel's params, shared by all particles.

required
num_mcmc_steps int

MCMC steps per particle and SMC step.

10
target_ess float | None

Fraction of effective particles each adaptive step keeps.

None
waste_free int | None

Keep every state of the move chains instead of only the last (Dau and Chopin, 2022): each step resamples num_particles // waste_free particles and runs waste_free - 1 moves from each. Replaces num_mcmc_steps.

None
batch_size int

Move this many particles at a time to bound memory; 0 moves them all at once.

0

Returns:

Name Type Description
A Kernel

class:Kernel with init(key, particles, temperature=0.0),

Kernel

init_params(state, **move_params) and a default_adaptation

Kernel

that fits the move's geometry to the population.

Source code in probjax/inference/smc/tempering.py
def smc(
    logprior_fn: Callable,
    loglikelihood_fn: Callable,
    *,
    move: Any,
    num_mcmc_steps: int = 10,
    target_ess: float | None = None,
    waste_free: int | None = None,
    batch_size: int = 0,
) -> Kernel:
    """Tempered SMC with MCMC moves.

    With ``target_ess=None`` the schedule is fixed and each step takes the next
    temperature as its argument, so ``run`` is called with
    ``args=(schedule,)``. With a ``target_ess`` each step picks the largest
    temperature that keeps that fraction of effective particles; once the
    temperature reaches 1 further steps leave the state unchanged, so
    ``num_steps`` is a cap.

    Args:
        logprior_fn: Log prior of one particle.
        loglikelihood_fn: Log likelihood of one particle; a scalar, or one term
            per observation for data tempering.
        move: MCMC kernel moving the particles, an API class such as ``hmc``
            or a configured one such as ``hmc(num_integration_steps=5)``. The
            SMC kernel's ``params`` are this kernel's params, shared by all
            particles.
        num_mcmc_steps: MCMC steps per particle and SMC step.
        target_ess: Fraction of effective particles each adaptive step keeps.
        waste_free: Keep every state of the move chains instead of only the
            last (Dau and Chopin, 2022): each step resamples
            ``num_particles // waste_free`` particles and runs
            ``waste_free - 1`` moves from each. Replaces ``num_mcmc_steps``.
        batch_size: Move this many particles at a time to bound memory;
            ``0`` moves them all at once.

    Returns:
        A :class:`Kernel` with ``init(key, particles, temperature=0.0)``,
        ``init_params(state, **move_params)`` and a ``default_adaptation``
        that fits the move's geometry to the population.
    """
    move = as_configured(move)
    if waste_free is not None and waste_free < 2:
        raise ValueError("waste_free must keep at least two states per chain.")

    def tempered(temperature: Array) -> Callable:
        def logdensity(x: Any) -> Array:
            return logprior_fn(x) + jnp.sum(temperature * loglikelihood_fn(x))

        return logdensity

    def init(key: Any, particles: PyTree[Array], temperature: Any = 0.0) -> SMCState:
        del key  # the population is given; the signature matches every kernel
        return SMCState(particles, jnp.asarray(temperature, dtype=float), jnp.zeros(()))

    def init_params(state: SMCState, **move_params: Any) -> Any:
        move_kernel = move(tempered(state.temperature))
        first = jax.tree.map(lambda leaf: leaf[0], state.particles)
        move_state = move_kernel.init(jax.random.key(0), first)
        return move_kernel.init_params(move_state, **move_params)

    def chain(key, position, params, temperature, num_steps):
        """Run ``num_steps`` moves from one particle; return all positions."""
        move_kernel = move(tempered(temperature))
        init_key, key = jax.random.split(key)

        def one_move(move_state, step_key):
            move_state, info = move_kernel.step(step_key, move_state, params)
            rate = getattr(info, "acceptance_rate", jnp.nan)
            return move_state, (move_state.position, rate)

        move_state = move_kernel.init(init_key, position)
        keys = jax.random.split(key, num_steps)
        _, (positions, rates) = jax.lax.scan(one_move, move_state, keys)
        return positions, jnp.mean(rates)

    def move_all(key, seeds, params, temperature, num_steps):
        keys = jax.random.split(key, leading_dim(seeds))

        def one(key_and_seed):
            key, seed = key_and_seed
            return chain(key, seed, params, temperature, num_steps)

        if batch_size:
            return jax.lax.map(one, (keys, seeds), batch_size=batch_size)
        return jax.vmap(one)((keys, seeds))

    def advance(key, state, params, temperature, loglik):
        num_particles = loglik.shape[0]
        event_axes = tuple(range(1, loglik.ndim))
        delta = temperature - state.temperature
        log_increments = jnp.sum(delta * loglik, axis=event_axes)
        log_increments = nan_to_neg_inf(log_increments)
        # The population is equally weighted, so the evidence factor is the
        # mean incremental weight.
        log_norm = logsumexp(log_increments)
        log_evidence_increment = log_norm - jnp.log(num_particles)
        log_weights = log_increments - log_norm

        resample_key, move_key = jax.random.split(key)
        weights = jnp.exp(log_weights)
        if waste_free is None:
            ancestors = systematic(resample_key, weights, num_particles)
            parents = ancestors
            seeds = take_batch(state.particles, ancestors)
            positions, rates = move_all(
                move_key, seeds, params, temperature, num_mcmc_steps
            )
            particles = jax.tree.map(lambda leaf: leaf[:, -1], positions)
        else:
            if num_particles % waste_free:
                raise ValueError(
                    f"waste_free={waste_free} must divide the {num_particles} "
                    "particles."
                )
            num_chains = num_particles // waste_free
            parents = systematic(resample_key, weights, num_chains)
            seeds = take_batch(state.particles, parents)
            positions, rates = move_all(
                move_key, seeds, params, temperature, waste_free - 1
            )

            def keep_all(seed, chain_positions):
                stacked = jnp.concatenate([seed[:, None], chain_positions], axis=1)
                return stacked.reshape((num_chains * waste_free,) + seed.shape[1:])

            particles = jax.tree.map(keep_all, seeds, positions)
            ancestors = jnp.repeat(parents, waste_free)

        state = SMCState(
            particles, temperature, state.log_evidence + log_evidence_increment
        )
        info = SMCInfo(
            jnp.exp(log_ess(log_weights)) / num_particles,
            log_evidence_increment,
            ancestors,
            # Waste-free keeps several states per parent; count parents once.
            unique_fraction(parents),
            jnp.mean(rates),
        )
        return state, info

    def step(key, state: SMCState, params: Any, *args: Any):
        loglik = jax.vmap(loglikelihood_fn)(state.particles)
        if target_ess is None:
            (temperature,) = args
            temperature = jnp.asarray(temperature, state.temperature.dtype)
            return advance(key, state, params, temperature, loglik)
        if state.temperature.ndim:
            raise ValueError("Adaptive tempering needs a scalar temperature.")
        ess_fraction = float(target_ess)

        def finished(_):
            info = SMCInfo(
                jnp.ones(()),
                jnp.zeros(()),
                jnp.arange(loglik.shape[0]),
                jnp.ones(()),
                jnp.full((), jnp.nan),
            )
            return state, info

        def tempering(key):
            temperature = _solve_temperature(loglik, state.temperature, ess_fraction)
            return advance(key, state, params, temperature, loglik)

        return jax.lax.cond(state.temperature >= 1.0, finished, tempering, key)

    def default_adaptation():
        # Fit the fields the move has: HMC's mass matrix and step size, say.
        fields = inspect.signature(move.api.init_params).parameters
        adaptors = {}
        if "inverse_mass_matrix" in fields:
            adaptors["geometry"] = particle_adaptor()
        if "step_size" in fields:
            adaptors["step_size"] = acceptance_rate_adaptor()
        if not adaptors:
            raise ValueError(
                f"{move.api.__name__} has no step_size or inverse_mass_matrix "
                "to adapt; pass an adaptor explicitly."
            )
        return compose_adaptors(**adaptors)

    return Kernel(init, step, init_params, default_adaptation)

probjax.inference.SMCState

Bases: NamedTuple

An equally weighted population at one temperature.

Attributes:

Name Type Description
particles PyTree[Array]

pytree with a leading particle axis.

temperature Array

the current temperature, a scalar or one per observation.

log_evidence Array

running estimate of log Z of the current target.

Source code in probjax/inference/smc/tempering.py
class SMCState(NamedTuple):
    """An equally weighted population at one temperature.

    Attributes:
        particles: pytree with a leading particle axis.
        temperature: the current temperature, a scalar or one per observation.
        log_evidence: running estimate of ``log Z`` of the current target.
    """

    particles: PyTree[Array]
    temperature: Array
    log_evidence: Array

probjax.inference.SMCInfo

Bases: NamedTuple

Diagnostics of one SMC step.

Attributes:

Name Type Description
ess Array

effective sample size of the reweighted population, as a fraction of the particles.

log_evidence_increment Array

this step's factor of the evidence estimate.

ancestors Array

index of each particle's parent.

unique_fraction Array

fraction of distinct parents among the resampled particles; 1 / N means the population collapsed onto one.

acceptance_rate Array

mean acceptance rate of the MCMC moves (NaN if the move reports none).

Source code in probjax/inference/smc/tempering.py
class SMCInfo(NamedTuple):
    """Diagnostics of one SMC step.

    Attributes:
        ess: effective sample size of the reweighted population, as a fraction
            of the particles.
        log_evidence_increment: this step's factor of the evidence estimate.
        ancestors: index of each particle's parent.
        unique_fraction: fraction of distinct parents among the resampled
            particles; ``1 / N`` means the population collapsed onto one.
        acceptance_rate: mean acceptance rate of the MCMC moves (NaN if the
            move reports none).
    """

    ess: Array
    log_evidence_increment: Array
    ancestors: Array
    unique_fraction: Array
    acceptance_rate: Array

probjax.inference.particle_adaptor

particle_adaptor(field='inverse_mass_matrix')

Set params[field] to the covariance of the particles.

A one-dimensional field gets the variances, a two-dimensional one the full covariance, matching what HMC-style moves expect of an inverse mass matrix.

Source code in probjax/inference/smc/adaptation.py
def particle_adaptor(field: str = "inverse_mass_matrix") -> Adaptor:
    """Set ``params[field]`` to the covariance of the particles.

    A one-dimensional field gets the variances, a two-dimensional one the full
    covariance, matching what HMC-style moves expect of an inverse mass matrix.
    """

    def tune(state: Any, _info: Any, params: Any) -> Any:
        flat = jax.vmap(lambda x: ravel_pytree(x)[0])(state.particles)
        current = jnp.asarray(get_param(params, field))
        if current.ndim == 2:
            value = jnp.atleast_2d(jnp.cov(flat, rowvar=False))
        else:
            value = jnp.var(flat, axis=0)
        return replace_params(params, **{field: value.astype(current.dtype)})

    return _stateless(tune)

probjax.inference.acceptance_rate_adaptor

acceptance_rate_adaptor(target=0.65, field='step_size')

Scale params[field] by exp(acceptance_rate - target).

Too many rejections shrink the step, too few grow it. Steps that report no acceptance rate (NaN) leave it unchanged.

Source code in probjax/inference/smc/adaptation.py
def acceptance_rate_adaptor(target: float = 0.65, field: str = "step_size") -> Adaptor:
    """Scale ``params[field]`` by ``exp(acceptance_rate - target)``.

    Too many rejections shrink the step, too few grow it. Steps that report no
    acceptance rate (NaN) leave it unchanged.
    """

    def tune(_state: Any, info: Any, params: Any) -> Any:
        rate = info.acceptance_rate
        factor = jnp.where(jnp.isnan(rate), 1.0, jnp.exp(rate - target))
        return replace_params(params, **{field: get_param(params, field) * factor})

    return _stateless(tune)

probjax.inference.population_diagnostics

population_diagnostics(particles, log_weights, ancestors=None)

Weight ESS and exact population/ancestral diversity (O(N log N) sorting).

Diagnostics do not imply independent posterior samples. ancestors may be composed origin labels for measuring collapse over many resampling steps.

Source code in probjax/inference/smc/temporal_utils.py
def population_diagnostics(
    particles: Any, log_weights: Array, ancestors: Array | None = None
) -> PopulationDiagnostics:
    """Weight ESS and exact population/ancestral diversity (O(N log N) sorting).

    Diagnostics do not imply independent posterior samples. ancestors may be
    composed origin labels for measuring collapse over many resampling steps.
    """
    x = population_matrix(particles)
    order = jnp.lexsort(x.T[::-1])
    unique = 1 + jnp.sum(jnp.any(jnp.diff(x[order], axis=0) != 0, axis=1))
    if ancestors is None:
        unique_ancestors = jnp.array(-1)  # unavailable
    else:
        labels = jnp.sort(ancestors)
        unique_ancestors = 1 + jnp.sum(jnp.diff(labels) != 0)
    return PopulationDiagnostics(
        ess(log_weights), unique, unique_ancestors, jax.nn.softmax(log_weights).max()
    )

probjax.inference.summarize_replicates

summarize_replicates(estimates)

Mean and Monte Carlo standard error across independent RUN estimates.

Supply posterior expectation estimates (or evidence estimates on the desired scale), not individual correlated particles from one run.

Source code in probjax/inference/smc/temporal_utils.py
def summarize_replicates(estimates: Any) -> ReplicateDiagnostics:
    """Mean and Monte Carlo standard error across independent RUN estimates.

    Supply posterior expectation estimates (or evidence estimates on the desired
    scale), not individual correlated particles from one run.
    """
    values = jnp.asarray(estimates)
    if values.ndim < 1 or values.shape[0] < 2:
        raise ValueError("At least two independent run estimates are required.")
    return ReplicateDiagnostics(
        values.mean(0),
        values.std(0, ddof=1) / jnp.sqrt(values.shape[0]),
        jnp.array(values.shape[0]),
    )

Twisted SMC for diffusion models

probjax.inference.diffusion_smc

diffusion_smc(guide, times, *, ess_threshold=0.5)

Twisted SMC that follows guide's reverse diffusion through times.

Run it for len(times) - 1 steps; further steps leave the state unchanged.

Parameters:

Name Type Description Default
guide Guide

the reverse SDE and twist, e.g. from :func:~probjax.inference.neural.diffusion_guide or :func:score_guide.

required
times Array

the time grid; sorted from the noisy end to the clean one.

required
ess_threshold float

resample when the ESS fraction falls below this.

0.5

Returns:

Name Type Description
A Kernel

class:Kernel with init(key, num_particles, params=None); its

Kernel

params are the guide's.

Source code in probjax/inference/smc/diffusion.py
def diffusion_smc(guide: Guide, times: Array, *, ess_threshold: float = 0.5) -> Kernel:
    """Twisted SMC that follows ``guide``'s reverse diffusion through ``times``.

    Run it for ``len(times) - 1`` steps; further steps leave the state
    unchanged.

    Args:
        guide: the reverse SDE and twist, e.g. from
            :func:`~probjax.inference.neural.diffusion_guide` or
            :func:`score_guide`.
        times: the time grid; sorted from the noisy end to the clean one.
        ess_threshold: resample when the ESS fraction falls below this.

    Returns:
        A :class:`Kernel` with ``init(key, num_particles, params=None)``; its
        ``params`` are the guide's.
    """
    if not 0.0 < ess_threshold <= 1.0:
        raise ValueError("ess_threshold must be in (0, 1].")
    times = jnp.sort(jnp.asarray(times))[::-1]
    if times.shape[0] < 2:
        raise ValueError("times needs at least two points.")
    last = times.shape[0] - 1

    def evaluate(key, t, particles, params):
        log_twist, *fields = guide.evaluate(key, t, particles, params)
        # Schedules may return a diffusion shared by all particles; give every
        # particle its own so resampling can gather it.
        drift, diffusion, guidance = (
            jax.tree.map(lambda v, x: jnp.broadcast_to(v, x.shape), value, particles)
            for value in fields
        )
        return log_twist, drift, diffusion, guidance

    def init(key: Any, num_particles: int, params: Any = None) -> DiffusionSMCState:
        base_key, evaluate_key = jax.random.split(key)
        particles, log_weights = guide.sample_base(
            base_key, times[0], num_particles, params
        )
        log_twist, drift, diffusion, guidance = evaluate(
            evaluate_key, times[0], particles, params
        )
        log_weights = log_weights + log_twist
        log_norm = logsumexp(log_weights)
        return DiffusionSMCState(
            particles,
            log_weights - log_norm,
            log_twist,
            drift,
            diffusion,
            guidance,
            jnp.zeros((), jnp.int32),
            log_norm - jnp.log(num_particles),
        )

    def advance(key, state: DiffusionSMCState, params: Any):
        noise_key, evaluate_key, resample_key = jax.random.split(key, 3)
        index = state.time_index
        step_size = times[index] - times[index + 1]
        leaves, treedef = jax.tree.flatten(state.particles)
        noise_keys = jax.random.split(noise_key, len(leaves))
        noise = treedef.unflatten([
            jax.random.normal(k, leaf.shape, leaf.dtype)
            for k, leaf in zip(noise_keys, leaves, strict=True)
        ])
        # The steered step shifts the mean by g^2 u h; the log ratio of the
        # unsteered to the steered Gaussian transition at the drawn point is
        # -(g sqrt(h) eps u + g^2 u^2 h / 2), summed over coordinates.
        particles = jax.tree.map(
            lambda x, drift, g, u, eps: (
                x
                - drift * step_size
                + g**2 * u * step_size
                + g * jnp.sqrt(step_size) * eps
            ),
            state.particles,
            state.drift,
            state.diffusion,
            state.guidance,
            noise,
        )
        if guide.log_transition is None:
            log_transition_ratio = _per_particle_sum(
                jax.tree.map(
                    lambda g, u, eps: (
                        -g * jnp.sqrt(step_size) * eps * u
                        - 0.5 * (g * u) ** 2 * step_size
                    ),
                    state.diffusion,
                    state.guidance,
                    noise,
                )
            )
        else:
            log_proposal = _per_particle_sum(
                jax.tree.map(
                    lambda g, eps: (
                        -0.5 * eps**2
                        - jnp.log(g * jnp.sqrt(step_size))
                        - 0.5 * _LOG_2PI
                    ),
                    state.diffusion,
                    noise,
                )
            )
            log_reference = guide.log_transition(
                times[index], state.particles, times[index + 1], particles, params
            )
            log_transition_ratio = log_reference - log_proposal

        def terminal(_):
            # The last twist is the target itself, which keeps the sampler exact.
            zeros = jax.tree.map(
                jnp.zeros_like, (state.drift, state.diffusion, state.guidance)
            )
            return (guide.log_target(particles, params), *zeros)

        def intermediate(key):
            return evaluate(key, times[index + 1], particles, params)

        log_twist, drift, diffusion, guidance = jax.lax.cond(
            index + 1 == last, terminal, intermediate, evaluate_key
        )

        log_increments = nan_to_neg_inf(
            log_twist - state.log_twist + log_transition_ratio
        )
        increment = logsumexp(state.log_weights + log_increments)
        log_weights = state.log_weights + log_increments - increment
        num_particles = log_weights.shape[0]
        ess = jnp.exp(log_ess(log_weights)) / num_particles
        resampled = ess < ess_threshold
        ancestors = jnp.where(
            resampled,
            systematic(resample_key, jnp.exp(log_weights), num_particles),
            jnp.arange(num_particles),
        )
        particles, log_twist, drift, diffusion, guidance = take_batch(
            (particles, log_twist, drift, diffusion, guidance), ancestors
        )
        log_weights = jnp.where(resampled, -jnp.log(num_particles), log_weights)
        state = DiffusionSMCState(
            particles,
            log_weights,
            log_twist,
            drift,
            diffusion,
            guidance,
            index + 1,
            state.log_evidence + increment,
        )
        info = DiffusionSMCInfo(
            ess, resampled, ancestors, unique_fraction(ancestors), increment
        )
        return state, info

    def step(key, state: DiffusionSMCState, params: Any, *args: Any):
        def finished(_):
            num_particles = state.log_weights.shape[0]
            info = DiffusionSMCInfo(
                jnp.exp(log_ess(state.log_weights)) / num_particles,
                jnp.zeros((), bool),
                jnp.arange(num_particles),
                jnp.ones(()),
                jnp.zeros(()),
            )
            return state, info

        return jax.lax.cond(
            state.time_index >= last,
            finished,
            lambda key: advance(key, state, params),
            key,
        )

    return Kernel(init, step, lambda state, params=None: params)

probjax.inference.Guide

Bases: NamedTuple

The reverse diffusion and twist :func:diffusion_smc follows.

All functions act on a batch of particles and take explicit params.

Attributes:

Name Type Description
sample_base Callable

(key, t, num_particles, params) -> (particles, log_weights), the starting particles at time t and their log weights relative to the reverse process (zero when the base is the process's own start).

evaluate Callable

(key, t, particles, params) -> (log_twist, drift, diffusion, guidance) at time t. guidance steers the proposal (usually the gradient of log_twist); zeros follow the reverse process unchanged.

log_target Callable

(particles, params) -> log target, the exact twist at the last time.

log_transition Callable | None

(t, particles, t_next, next_particles, params) -> log density of the reference transition the twist is relative to. None (the default) refers the twist to the reverse SDE itself, as for a pretrained model; a guide whose twist estimates the noised target's marginal passes the forward noising kernel from t_next back to t instead.

Source code in probjax/inference/smc/diffusion.py
class Guide(NamedTuple):
    """The reverse diffusion and twist :func:`diffusion_smc` follows.

    All functions act on a batch of particles and take explicit ``params``.

    Attributes:
        sample_base: ``(key, t, num_particles, params) -> (particles,
            log_weights)``, the starting particles at time ``t`` and their log
            weights relative to the reverse process (zero when the base is the
            process's own start).
        evaluate: ``(key, t, particles, params) -> (log_twist, drift,
            diffusion, guidance)`` at time ``t``. ``guidance`` steers the
            proposal (usually the gradient of ``log_twist``); zeros follow
            the reverse process unchanged.
        log_target: ``(particles, params) -> log target``, the exact twist at
            the last time.
        log_transition: ``(t, particles, t_next, next_particles, params) ->
            log density`` of the reference transition the twist is relative
            to. ``None`` (the default) refers the twist to the reverse SDE
            itself, as for a pretrained model; a guide whose twist estimates
            the noised target's marginal passes the forward noising kernel
            from ``t_next`` back to ``t`` instead.
    """

    sample_base: Callable
    evaluate: Callable
    log_target: Callable
    log_transition: Callable | None = None

probjax.inference.score_guide

score_guide(log_target_fn, schedule, event_shape, *, num_inner=64, max_score_norm=50.0)

A training-free guide for an unnormalised target (reverse diffusion SMC).

At every particle it estimates the score with num_inner importance samples of the target, and uses the same samples' marginal estimate as the twist, relative to the forward noising kernel. That costs num_inner target evaluations per particle and step.

Parameters:

Name Type Description Default
log_target_fn Callable[[Array], Array]

unnormalised log density of one point.

required
schedule Any

the forward noising, with scale(t), std(t), drift(t, x) and diffusion(t, x), e.g. :class:~probjax.nn.generative.diffusion.config.VPNoiseSchedule.

required
event_shape tuple[int, ...]

shape of one point.

required
num_inner int

importance samples per score estimate.

64
max_score_norm float | None

clip the estimated score to this norm; noisy estimates at small times can otherwise throw particles far away.

50.0

Returns:

Name Type Description
A Guide

class:Guide without params; start from a standard normal at a

Guide

time where the noised target is close to one.

Source code in probjax/inference/smc/diffusion.py
def score_guide(
    log_target_fn: Callable[[Array], Array],
    schedule: Any,
    event_shape: tuple[int, ...],
    *,
    num_inner: int = 64,
    max_score_norm: float | None = 50.0,
) -> Guide:
    """A training-free guide for an unnormalised target (reverse diffusion SMC).

    At every particle it estimates the score with ``num_inner`` importance
    samples of the target, and uses the same samples' marginal estimate as the
    twist, relative to the forward noising kernel. That costs ``num_inner``
    target evaluations per particle and step.

    Args:
        log_target_fn: unnormalised log density of one point.
        schedule: the forward noising, with ``scale(t)``, ``std(t)``,
            ``drift(t, x)`` and ``diffusion(t, x)``, e.g.
            :class:`~probjax.nn.generative.diffusion.config.VPNoiseSchedule`.
        event_shape: shape of one point.
        num_inner: importance samples per score estimate.
        max_score_norm: clip the estimated score to this norm; noisy estimates
            at small times can otherwise throw particles far away.

    Returns:
        A :class:`Guide` without params; start from a standard normal at a
        time where the noised target is close to one.
    """

    def sample_base(key, t, num_particles, params):
        del t, params
        particles = jax.random.normal(key, (num_particles,) + tuple(event_shape))
        # The base is a proposal for the noised target, which the twist
        # carries in full, so the weights correct for the base density.
        log_weights = -jax.vmap(
            lambda x: _gaussian_logpdf(x, jnp.zeros_like(x), jnp.ones(()))
        )(particles)
        return particles, log_weights

    def evaluate(key, t, particles, params):
        del params
        alpha, sigma = schedule.scale(t), schedule.std(t)
        keys = jax.random.split(key, particles.shape[0])
        score, log_marginal = jax.vmap(
            lambda k, x: _score_and_marginal(
                k, x, log_target_fn, alpha, sigma, num_inner, max_score_norm
            )
        )(keys, particles)
        diffusion = schedule.diffusion(t, particles)
        drift = schedule.drift(t, particles) - diffusion**2 * score
        return log_marginal, drift, diffusion, jnp.zeros_like(particles)

    def log_target(particles, params):
        del params
        return jax.vmap(log_target_fn)(particles)

    def log_transition(t, particles, t_next, next_particles, params):
        # The forward noising from t_next to t: x_t = a x_next + s eps.
        del params
        a = schedule.scale(t) / schedule.scale(t_next)
        s = jnp.sqrt(schedule.std(t) ** 2 - (a * schedule.std(t_next)) ** 2)
        return jax.vmap(_gaussian_logpdf, in_axes=(0, 0, None))(
            particles, a * next_particles, s
        )

    return Guide(sample_base, evaluate, log_target, log_transition)

probjax.inference.neural.diffusion_guide

diffusion_guide(model, log_potential_fn, *, event_spec=None, twist=True, inputs=None)

Steer a pretrained diffusion model towards prior(x) * exp(V(x)).

The guide for :func:~probjax.inference.diffusion_smc, after the twisted diffusion sampler (Wu et al. 2023). The twist is V at the model's denoised estimate of the clean point, faded in as the noise goes, and its gradient steers the model's reverse SDE. The twist is a heuristic: it decides how many particles are needed, never what they converge to.

Parameters:

Name Type Description Default
model Any

a trained diffusion model with a solver_cfg, e.g. a :class:~probjax.nn.generative.diffusion.model.DiffusionDenoiser.

required
log_potential_fn Callable[[Any], Any]

V for one clean point; scale it to temper.

required
event_spec Any

shape (or pytree of shapes) of one point; defaults to the model's.

None
twist bool

False neither looks ahead nor steers: it simulates the prior and weights by the final potential, a baseline.

True
inputs dict[str, Any] | None

named model inputs such as context.

None

Returns:

Type Description
tuple[Guide, Any]

(guide, params); the params are the model's weights.

Source code in probjax/inference/neural.py
def diffusion_guide(
    model: Any,
    log_potential_fn: Callable[[Any], Any],
    *,
    event_spec: Any = None,
    twist: bool = True,
    inputs: dict[str, Any] | None = None,
) -> tuple[Guide, Any]:
    """Steer a pretrained diffusion model towards ``prior(x) * exp(V(x))``.

    The guide for :func:`~probjax.inference.diffusion_smc`, after the twisted
    diffusion sampler (Wu et al. 2023). The twist is ``V`` at the model's
    denoised estimate of the clean point, faded in as the noise goes, and its
    gradient steers the model's reverse SDE. The twist is a heuristic: it
    decides how many particles are needed, never what they converge to.

    Args:
        model: a trained diffusion model with a ``solver_cfg``, e.g. a
            :class:`~probjax.nn.generative.diffusion.model.DiffusionDenoiser`.
        log_potential_fn: ``V`` for one clean point; scale it to temper.
        event_spec: shape (or pytree of shapes) of one point; defaults to the
            model's.
        twist: ``False`` neither looks ahead nor steers: it simulates the
            prior and weights by the final potential, a baseline.
        inputs: named model inputs such as ``context``.

    Returns:
        ``(guide, params)``; the params are the model's weights.
    """
    inputs = dict(inputs or {})
    event_spec = model.event_spec if event_spec is None else event_spec
    if event_spec is None:
        raise TypeError("Pass event_spec, or set one on the model.")
    spec = model._normalize_event_spec(event_spec)
    graphdef, params, rest = _split(model)
    potential = jax.vmap(log_potential_fn)

    def sample_base(key: Any, t: Any, num_particles: int, params: Any) -> Any:
        merged = nnx.merge(graphdef, params, rest)
        particles = merged.sample_base(key, (num_particles,), spec, t_max=t)
        return particles, jnp.zeros(num_particles)

    def evaluate(key: Any, t: Any, particles: Any, params: Any) -> Any:
        del key
        merged = nnx.merge(graphdef, params, rest)
        drift_fn, diffusion_fn = merged._build_sde_drift_and_diffusion(inputs)
        alpha, sigma = merged.scale_fn(t), merged.std_fn(t)
        # Fade the twist in with the signal left at time t: at high noise the
        # denoised point is a poor stand-in for the clean one, and the full
        # potential there collapses the weights. The square fades in later
        # than the plain fraction, which measured better on sharp potentials.
        fade = (alpha**2 / (alpha**2 + sigma**2)) ** 2

        def log_twist_fn(particles):
            return fade * potential(merged.denoise(t, particles, **inputs))

        if twist:
            log_twist, pullback = jax.vjp(log_twist_fn, particles)
            (guidance,) = pullback(jnp.ones_like(log_twist))
        else:
            log_twist = jnp.zeros(jax.tree.leaves(particles)[0].shape[0])
            guidance = jax.tree.map(jnp.zeros_like, particles)
        return (
            log_twist,
            drift_fn(t, particles),
            diffusion_fn(t, particles),
            guidance,
        )

    def log_target(particles: Any, params: Any) -> Any:
        del params
        return potential(particles)

    return Guide(sample_base, evaluate, log_target), params

probjax.inference.DiffusionSMCState

Bases: NamedTuple

A weighted population at one time of the reverse diffusion.

Attributes:

Name Type Description
particles PyTree[Array]

pytree with a leading particle axis.

log_weights Array

normalised log weights.

log_twist Array

twist of each particle at the current time.

drift, diffusion

the reverse SDE at each particle.

guidance PyTree[Array]

the proposal's steer at each particle.

time_index Array

index of the current time in times.

log_evidence Array

running estimate of log Z.

Source code in probjax/inference/smc/diffusion.py
class DiffusionSMCState(NamedTuple):
    """A weighted population at one time of the reverse diffusion.

    Attributes:
        particles: pytree with a leading particle axis.
        log_weights: normalised log weights.
        log_twist: twist of each particle at the current time.
        drift, diffusion: the reverse SDE at each particle.
        guidance: the proposal's steer at each particle.
        time_index: index of the current time in ``times``.
        log_evidence: running estimate of ``log Z``.
    """

    particles: PyTree[Array]
    log_weights: Array
    log_twist: Array
    drift: PyTree[Array]
    diffusion: PyTree[Array]
    guidance: PyTree[Array]
    time_index: Array
    log_evidence: Array

probjax.inference.DiffusionSMCInfo

Bases: NamedTuple

Diagnostics of one reverse step.

Attributes:

Name Type Description
ess Array

effective sample size after reweighting, as a fraction.

resampled Array

whether the population was resampled.

ancestors Array

index of each particle's parent.

unique_fraction Array

fraction of distinct parents; 1 / N means the population collapsed onto one.

log_evidence_increment Array

this step's factor of the evidence estimate.

Source code in probjax/inference/smc/diffusion.py
class DiffusionSMCInfo(NamedTuple):
    """Diagnostics of one reverse step.

    Attributes:
        ess: effective sample size after reweighting, as a fraction.
        resampled: whether the population was resampled.
        ancestors: index of each particle's parent.
        unique_fraction: fraction of distinct parents; ``1 / N`` means the
            population collapsed onto one.
        log_evidence_increment: this step's factor of the evidence estimate.
    """

    ess: Array
    resampled: Array
    ancestors: Array
    unique_fraction: Array
    log_evidence_increment: Array

Filtering and smoothing

Every filter is a kernel built from a model, with the model parameters theta as its params. See the filtering guide.

probjax.inference.LinearGaussianModel

Bases: NamedTuple

A linear-Gaussian state-space model, filtered exactly by a Kalman filter.

Attributes:

Name Type Description
initial Callable

theta -> (mean, covariance) of the state at t0.

transition Callable

(theta, t_previous, t_next) -> (Phi, Q): the state moves to Phi x plus noise of covariance Q. Either may be a :mod:~probjax.utils.linear_operator operator.

observation Callable

(theta, t) -> (C, R): y is C x plus noise of covariance R.

Source code in probjax/inference/filtering/state_space.py
class LinearGaussianModel(NamedTuple):
    """A linear-Gaussian state-space model, filtered exactly by a Kalman filter.

    Attributes:
        initial: ``theta -> (mean, covariance)`` of the state at ``t0``.
        transition: ``(theta, t_previous, t_next) -> (Phi, Q)``: the state moves
            to ``Phi x`` plus noise of covariance ``Q``. Either may be a
            :mod:`~probjax.utils.linear_operator` operator.
        observation: ``(theta, t) -> (C, R)``: ``y`` is ``C x`` plus noise of
            covariance ``R``.
    """

    initial: Callable
    transition: Callable
    observation: Callable

probjax.inference.GaussianModel

Bases: NamedTuple

A nonlinear model with additive Gaussian noise, for the EKF and the UKF.

Attributes:

Name Type Description
initial Callable

theta -> (mean, covariance) of the state at t0.

transition Callable

(theta, x, t_previous, t_next) -> (f(x), Q).

observation Callable

(theta, x, t) -> (h(x), R).

Source code in probjax/inference/filtering/state_space.py
class GaussianModel(NamedTuple):
    """A nonlinear model with additive Gaussian noise, for the EKF and the UKF.

    Attributes:
        initial: ``theta -> (mean, covariance)`` of the state at ``t0``.
        transition: ``(theta, x, t_previous, t_next) -> (f(x), Q)``.
        observation: ``(theta, x, t) -> (h(x), R)``.
    """

    initial: Callable
    transition: Callable
    observation: Callable

probjax.inference.StateSpaceModel

Bases: NamedTuple

A state-space model written for a single particle.

All functions act on one latent state x, which may be any pytree; the particle filter vectorizes them over particles.

Attributes:

Name Type Description
init Callable

(key, theta) -> x, a draw from the initial law at t0.

step Callable

(key, theta, x, t_previous, t_next) -> x, a draw from the transition over one interval.

log_likelihood Callable

(theta, x, y, t) -> scalar, the normalized observation log density.

transition_logdensity Optional[Callable]

(theta, x_next, x_previous, t_previous, t_next) -> scalar. Optional; needed by proposals, backward path sampling and particle Gibbs.

Source code in probjax/inference/filtering/state_space.py
class StateSpaceModel(NamedTuple):
    """A state-space model written for a single particle.

    All functions act on one latent state ``x``, which may be any pytree; the
    particle filter vectorizes them over particles.

    Attributes:
        init: ``(key, theta) -> x``, a draw from the initial law at ``t0``.
        step: ``(key, theta, x, t_previous, t_next) -> x``, a draw from the
            transition over one interval.
        log_likelihood: ``(theta, x, y, t) -> scalar``, the normalized
            observation log density.
        transition_logdensity: ``(theta, x_next, x_previous, t_previous, t_next)
            -> scalar``. Optional; needed by proposals, backward path sampling
            and particle Gibbs.
    """

    init: Callable
    step: Callable
    log_likelihood: Callable
    transition_logdensity: Optional[Callable] = None

probjax.inference.kalman_filter

kalman_filter(model, *, linear_solve=None, logdet_fn=None)

Kalman filter for a linear-Gaussian state-space model.

\[x_t = \Phi x_{t-1} + w_t,\ w_t \sim \mathcal N(0, Q) \qquad y_t = C x_t + v_t,\ v_t \sim \mathcal N(0, R)\]

The transition and observation matrices may be :mod:~probjax.utils.linear_operator operators: large innovation systems are then solved with CG and their log-determinant estimated by Lanczos.

Parameters:

Name Type Description Default
model LinearGaussianModel

The model, as functions of the parameters theta.

required
linear_solve Optional[Callable]

(S, rhs) -> S^{-1} rhs for the innovation covariance; defaults to dense Cholesky or CG by size.

None
logdet_fn Optional[Callable]

S -> log det S; defaults to slogdet or Lanczos.

None

Returns:

Name Type Description
A Kernel

class:~probjax.inference.Kernel with init(key, theta, t0=0.0)

Kernel

and step(key, state, theta, t, y, observed=True); theta is its

Kernel

params. state.log_likelihood is the exact log-likelihood of the

Kernel

observations so far.

Source code in probjax/inference/filtering/kalman_filter.py
def kalman_filter(
    model: LinearGaussianModel,
    *,
    linear_solve: Optional[Callable] = None,
    logdet_fn: Optional[Callable] = None,
) -> Kernel:
    r"""Kalman filter for a linear-Gaussian state-space model.

    $$x_t = \Phi x_{t-1} + w_t,\ w_t \sim \mathcal N(0, Q) \qquad
    y_t = C x_t + v_t,\ v_t \sim \mathcal N(0, R)$$

    The transition and observation matrices may be
    :mod:`~probjax.utils.linear_operator` operators: large innovation systems
    are then solved with CG and their log-determinant estimated by Lanczos.

    Args:
        model: The model, as functions of the parameters ``theta``.
        linear_solve: ``(S, rhs) -> S^{-1} rhs`` for the innovation covariance;
            defaults to dense Cholesky or CG by size.
        logdet_fn: ``S -> log det S``; defaults to ``slogdet`` or Lanczos.

    Returns:
        A :class:`~probjax.inference.Kernel` with ``init(key, theta, t0=0.0)``
        and ``step(key, state, theta, t, y, observed=True)``; ``theta`` is its
        params. ``state.log_likelihood`` is the exact log-likelihood of the
        observations so far.
    """

    def predict(theta, mean, cov, t_previous, t):
        Phi, Q = model.transition(theta, t_previous, t)
        return Phi @ mean, Phi @ cov @ Phi.T + Q

    def observe(theta, mean_pred, cov_pred, t):
        C, R = model.observation(theta, t)
        return C @ mean_pred, C, R

    return gaussian_filter(model.initial, predict, observe, linear_solve, logdet_fn)

probjax.inference.extended_kalman_filter

extended_kalman_filter(model, *, matrix_free=False, linear_solve=None, logdet_fn=None)

Extended Kalman filter for a nonlinear model with Gaussian noise.

\[x_t = f(x_{t-1}) + w_t \qquad y_t = h(x_t) + v_t\]

Each step linearises f at the filtered mean and h at the predicted mean, then applies the Kalman update. Its log-likelihood is that of the linearised model, an approximation.

Parameters:

Name Type Description Default
model GaussianModel

The model, as functions of the parameters theta.

required
matrix_free bool

Use the Jacobians as matrix-free operators instead of materialising them; pays off for large states.

False
linear_solve, logdet_fn

As for :func:kalman_filter.

required

Returns:

Name Type Description
A Kernel

class:~probjax.inference.Kernel like :func:kalman_filter's.

Source code in probjax/inference/filtering/extended_kalman_filter.py
def extended_kalman_filter(
    model: GaussianModel,
    *,
    matrix_free: bool = False,
    linear_solve: Optional[Callable] = None,
    logdet_fn: Optional[Callable] = None,
) -> Kernel:
    r"""Extended Kalman filter for a nonlinear model with Gaussian noise.

    $$x_t = f(x_{t-1}) + w_t \qquad y_t = h(x_t) + v_t$$

    Each step linearises ``f`` at the filtered mean and ``h`` at the predicted
    mean, then applies the Kalman update. Its log-likelihood is that of the
    linearised model, an approximation.

    Args:
        model: The model, as functions of the parameters ``theta``.
        matrix_free: Use the Jacobians as matrix-free operators instead of
            materialising them; pays off for large states.
        linear_solve, logdet_fn: As for :func:`kalman_filter`.

    Returns:
        A :class:`~probjax.inference.Kernel` like :func:`kalman_filter`'s.
    """

    transition_mean = partial(_transition_mean, model)
    observation_mean = partial(_observation_mean, model)

    def predict(theta, mean, cov, t_previous, t):
        mean_pred, Phi = _linearize(
            transition_mean, mean, (theta, t_previous, t), matrix_free
        )
        Q = model.transition(theta, mean, t_previous, t)[1]
        return mean_pred, Phi @ cov @ Phi.T + Q

    def observe(theta, mean_pred, cov_pred, t):
        y_pred, C = _linearize(
            observation_mean, mean_pred, (theta, t), matrix_free
        )
        R = model.observation(theta, mean_pred, t)[1]
        return y_pred, C, R

    return gaussian_filter(
        model.initial, predict, observe, linear_solve, logdet_fn, symmetrize=True
    )

probjax.inference.unscented_kalman_filter

unscented_kalman_filter(model, *, sigma_points=merwe_sigma_point)

Unscented Kalman filter for a nonlinear model with Gaussian noise.

Propagates sigma points through the transition and the observation instead of linearising them, so it needs no Jacobians.

Parameters:

Name Type Description Default
model GaussianModel

The model, as functions of the parameters theta.

required
sigma_points Callable

(mean, cov) -> (points, weights_mean, weights_cov), e.g. :func:merwe_sigma_point, :func:julier_uhlmann_sigma_points or :func:spherical_simplex_sigma_points.

merwe_sigma_point

Returns:

Name Type Description
A Kernel

class:~probjax.inference.Kernel like :func:kalman_filter's.

Source code in probjax/inference/filtering/unscented_kalman_filter.py
def unscented_kalman_filter(
    model: GaussianModel, *, sigma_points: Callable = merwe_sigma_point
) -> Kernel:
    """Unscented Kalman filter for a nonlinear model with Gaussian noise.

    Propagates sigma points through the transition and the observation
    instead of linearising them, so it needs no Jacobians.

    Args:
        model: The model, as functions of the parameters ``theta``.
        sigma_points: ``(mean, cov) -> (points, weights_mean, weights_cov)``,
            e.g. :func:`merwe_sigma_point`, :func:`julier_uhlmann_sigma_points`
            or :func:`spherical_simplex_sigma_points`.

    Returns:
        A :class:`~probjax.inference.Kernel` like :func:`kalman_filter`'s.
    """

    def init(key: Any, theta: Any, t0: ArrayLike = 0.0) -> GaussianFilterState:
        del key
        mean, cov = model.initial(theta)
        mean = jnp.asarray(mean)
        return GaussianFilterState(
            mean, jnp.asarray(cov), as_time(t0), jnp.zeros((), mean.dtype)
        )

    def step(
        key: Any,
        state: GaussianFilterState,
        theta: Any,
        t: ArrayLike,
        y: ArrayLike,
        observed: Any = True,
    ) -> Tuple[GaussianFilterState, GaussianFilterInfo]:
        del key
        t = jnp.asarray(t, state.t.dtype)
        points, weights_mean, weights_cov = sigma_points(state.mean, state.cov)
        moved = jax.vmap(lambda x: model.transition(theta, x, state.t, t)[0])(points)
        Q = model.transition(theta, state.mean, state.t, t)[1]
        mean_pred, cov_pred = unscented_transform(
            moved, weights_mean, weights_cov, noise_cov=Q
        )
        y = unobserved(observed, jnp.asarray(y))

        def update(_):
            # Fresh sigma points carry the process noise into the observation.
            points, weights_mean, weights_cov = sigma_points(mean_pred, cov_pred)
            observed_points = jax.vmap(lambda x: model.observation(theta, x, t)[0])(
                points
            )
            R = model.observation(theta, mean_pred, t)[1]
            mean_y, cov_y = unscented_transform(
                observed_points, weights_mean, weights_cov, noise_cov=R
            )
            cross_cov = jnp.dot(
                weights_cov * (points - mean_pred).T, observed_points - mean_y
            )
            gain = jnp.linalg.solve(cov_y.T, cross_cov.T).T
            residual = y - mean_y
            increment = innovation_loglik(
                residual,
                cov_y,
                solve_fn=lambda S, res: jnp.linalg.solve(S, res),
                logdet_fn=lambda S: jnp.linalg.slogdet(S)[1],
            )
            return (
                mean_pred + gain @ residual,
                cov_pred - gain @ cov_y @ gain.T,
                increment.astype(state.log_likelihood.dtype),
            )

        def predict_only(_):
            return mean_pred, cov_pred, jnp.zeros_like(state.log_likelihood)

        mean, cov, increment = jax.lax.cond(observed, update, predict_only, None)
        new_state = GaussianFilterState(mean, cov, t, state.log_likelihood + increment)
        return new_state, GaussianFilterInfo(mean_pred, cov_pred, increment)

    return Kernel(init, step, lambda state, theta=None: theta)

probjax.inference.square_root_kalman_filter

square_root_kalman_filter(model)

Square-root Kalman filter for a linear-Gaussian model.

Takes the same model as :func:kalman_filter, with covariances; it factors them itself. The state carries std with cov = std @ std.T.

Returns:

Name Type Description
A Kernel

class:~probjax.inference.Kernel like :func:kalman_filter's.

Source code in probjax/inference/filtering/square_root_kf.py
def square_root_kalman_filter(model: LinearGaussianModel) -> Kernel:
    """Square-root Kalman filter for a linear-Gaussian model.

    Takes the same model as :func:`kalman_filter`, with covariances; it factors
    them itself. The state carries ``std`` with ``cov = std @ std.T``.

    Returns:
        A :class:`~probjax.inference.Kernel` like :func:`kalman_filter`'s.
    """

    def init(key: Any, theta: Any, t0: ArrayLike = 0.0) -> SquareRootFilterState:
        del key
        mean, cov = model.initial(theta)
        mean = jnp.asarray(mean)
        return SquareRootFilterState(
            mean,
            jnp.linalg.cholesky(as_array(cov)),
            as_time(t0),
            jnp.zeros((), mean.dtype),
        )

    def step(
        key: Any,
        state: SquareRootFilterState,
        theta: Any,
        t: ArrayLike,
        y: ArrayLike,
        observed: Any = True,
    ) -> Tuple[SquareRootFilterState, SquareRootFilterInfo]:
        del key
        t = jnp.asarray(t, state.t.dtype)
        Phi, Q = model.transition(theta, state.t, t)
        mean_pred, std_pred = sqrt_kf_predict(
            state.mean,
            state.std,
            as_array(Phi),
            jnp.linalg.cholesky(as_array(Q)),
        )
        y = unobserved(observed, jnp.asarray(y))

        def update(_):
            C, R = model.observation(theta, t)
            mean, std, increment = sqrt_kf_correct(
                mean_pred,
                std_pred,
                as_array(C),
                jnp.linalg.cholesky(as_array(R)),
                y,
            )
            return mean, std, increment.astype(state.log_likelihood.dtype)

        def predict_only(_):
            return mean_pred, std_pred, jnp.zeros_like(state.log_likelihood)

        mean, std, increment = jax.lax.cond(observed, update, predict_only, None)
        new_state = SquareRootFilterState(
            mean, std, t, state.log_likelihood + increment
        )
        return new_state, SquareRootFilterInfo(mean_pred, std_pred, increment)

    return Kernel(init, step, lambda state, theta=None: theta)

probjax.inference.rank_reduced_kalman_filter

rank_reduced_kalman_filter(model, rank, *, linear_solve=None, min_eig=1e-09, process_noise_rank=None, energy_threshold=None, min_rank=1)

Kalman filter with the covariance kept at low rank, P ~= U S U^T.

The predict step combines the propagated covariance and the process noise in factored form and truncates to rank; the update works in the current subspace. Costs scale with rank instead of the state size.

Parameters:

Name Type Description Default
model LinearGaussianModel

As for :func:kalman_filter. The initial covariance and Q may also be given as a factor L (L L^T) or a pair (U, S) (U S U^T, S a matrix or its diagonal).

required
rank int

Rank r of the covariance factor.

required
linear_solve Optional[Callable]

Solver for the innovation covariance.

None
min_eig float

Floor for the kept eigenvalues.

1e-09
process_noise_rank Optional[int]

Rank kept of Q; defaults to rank.

None
energy_threshold Optional[float]

Keep only the leading directions holding this fraction of the variance (at least min_rank).

None
min_rank int

Smallest effective rank under energy_threshold.

1

Returns:

Name Type Description
A Kernel

class:~probjax.inference.Kernel like :func:kalman_filter's;

Kernel

its state carries cov_factor and cov_core.

Source code in probjax/inference/filtering/rank_reduced_kalman_filter.py
def rank_reduced_kalman_filter(
    model: LinearGaussianModel,
    rank: int,
    *,
    linear_solve: Optional[Callable] = None,
    min_eig: float = 1e-9,
    process_noise_rank: Optional[int] = None,
    energy_threshold: Optional[float] = None,
    min_rank: int = 1,
) -> Kernel:
    """Kalman filter with the covariance kept at low rank, ``P ~= U S U^T``.

    The predict step combines the propagated covariance and the process noise
    in factored form and truncates to ``rank``; the update works in the
    current subspace. Costs scale with ``rank`` instead of the state size.

    Args:
        model: As for :func:`kalman_filter`. The initial covariance and ``Q``
            may also be given as a factor ``L`` (``L L^T``) or a pair
            ``(U, S)`` (``U S U^T``, ``S`` a matrix or its diagonal).
        rank: Rank ``r`` of the covariance factor.
        linear_solve: Solver for the innovation covariance.
        min_eig: Floor for the kept eigenvalues.
        process_noise_rank: Rank kept of ``Q``; defaults to ``rank``.
        energy_threshold: Keep only the leading directions holding this
            fraction of the variance (at least ``min_rank``).
        min_rank: Smallest effective rank under ``energy_threshold``.

    Returns:
        A :class:`~probjax.inference.Kernel` like :func:`kalman_filter`'s;
        its state carries ``cov_factor`` and ``cov_core``.
    """
    if process_noise_rank is None:
        process_noise_rank = rank
    if energy_threshold is not None and not (0.0 < energy_threshold <= 1.0):
        raise ValueError("energy_threshold must be in (0, 1].")
    min_rank = max(1, min(min_rank, rank))
    solve = default_solve if linear_solve is None else linear_solve

    def truncate(vals_desc):
        return _apply_energy_threshold(
            vals_desc,
            min_eig=min_eig,
            energy_threshold=energy_threshold,
            min_rank=min_rank,
        )

    def init(
        key: Any, theta: Any, t0: ArrayLike = 0.0
    ) -> RankReducedKalmanFilterState:
        del key
        mean, cov = model.initial(theta)
        mean = jnp.asarray(mean)
        factor, core = _parse_process_noise(
            cov, process_noise_rank=rank, min_eig=min_eig
        )
        return RankReducedKalmanFilterState(
            mean, factor, core, as_time(t0), jnp.zeros((), mean.dtype)
        )

    def step(
        key: Any,
        state: RankReducedKalmanFilterState,
        theta: Any,
        t: ArrayLike,
        y: ArrayLike,
        observed: Any = True,
    ) -> Tuple[RankReducedKalmanFilterState, RankReducedKalmanFilterInfo]:
        del key
        t = jnp.asarray(t, state.t.dtype)
        Phi, Q = model.transition(theta, state.t, t)
        Uq, Sq = _parse_process_noise(
            Q, process_noise_rank=process_noise_rank, min_eig=min_eig
        )
        # P_pred = Phi U S U^T Phi^T + Uq Sq Uq^T, compressed to the rank.
        U_pred, S_pred = _sum_lowrank_terms(
            Phi @ state.cov_factor, state.cov_core, Uq, Sq, rank=rank, min_eig=min_eig
        )
        S_pred = jnp.diag(truncate(jnp.diag(S_pred)))
        mean_pred = Phi @ state.mean
        y = unobserved(observed, jnp.asarray(y))

        def update(_):
            C, R = model.observation(theta, t)
            residual = y - C @ mean_pred
            # Innovation S_y = C U S U^T C^T + R and gain K = U S M^T S_y^-1.
            M = C @ U_pred
            innovation = M @ S_pred @ M.T + R
            B_Sinv = solve(innovation, S_pred @ M.T)
            mean = mean_pred + U_pred @ B_Sinv @ residual
            S_post = symmetrize_matrix(S_pred - B_Sinv @ (M @ S_pred))
            # Re-diagonalise in the subspace and truncate again.
            vals, vecs = _eigh_clip(S_post, min_eig=min_eig)
            k = min(rank, vals.shape[0])
            vals, vecs = vals[-k:], vecs[:, -k:]
            vals = truncate(vals[::-1])[::-1]
            increment = innovation_loglik(
                residual,
                innovation,
                solve_fn=solve,
                logdet_fn=lambda S: jnp.linalg.slogdet(jnp.asarray(S))[1],
            )
            return (
                mean,
                U_pred @ vecs,
                jnp.diag(vals),
                increment.astype(state.log_likelihood.dtype),
            )

        def predict_only(_):
            return mean_pred, U_pred, S_pred, jnp.zeros_like(state.log_likelihood)

        mean, U, S, increment = jax.lax.cond(observed, update, predict_only, None)
        new_state = RankReducedKalmanFilterState(
            mean, U, S, t, state.log_likelihood + increment
        )
        info = RankReducedKalmanFilterInfo(mean_pred, U_pred, S_pred, increment)
        return new_state, info

    return Kernel(init, step, lambda state, theta=None: theta)

probjax.inference.particle_filter

particle_filter(model, num_particles, *, ess_threshold=0.5, proposal=None, unbiased_gradients=False)

Particle filter for a state-space model.

Each step moves the particles through the transition (or proposal), weights them by the observation likelihood and resamples when the ESS falls below ess_threshold. state.log_likelihood is an unbiased estimate of the likelihood (on the log scale), so the filter can drive a pseudo-marginal sampler over theta.

Parameters:

Name Type Description Default
model StateSpaceModel

The model, written for one particle.

required
num_particles int

Number of particles.

required
ess_threshold float

Resample when the ESS falls below this fraction.

0.5
proposal Optional[Callable]

(key, theta, x, y, t_previous, t) -> (x_next, log_q), a proposal that can look at the observation; needs model.transition_logdensity. On steps without an observation the transition is used.

None
unbiased_gradients bool

Pass gradients of the weights through resampling (a stop-gradient correction), for fitting theta by gradient.

False

Returns:

Name Type Description
A Kernel

class:~probjax.inference.Kernel with init(key, theta, t0=0.0)

Kernel

and step(key, state, theta, t, y, observed=True).

Source code in probjax/inference/filtering/particle_filter.py
def particle_filter(
    model: StateSpaceModel,
    num_particles: int,
    *,
    ess_threshold: float = 0.5,
    proposal: Optional[Callable] = None,
    unbiased_gradients: bool = False,
) -> Kernel:
    """Particle filter for a state-space model.

    Each step moves the particles through the transition (or ``proposal``),
    weights them by the observation likelihood and resamples when the ESS
    falls below ``ess_threshold``. ``state.log_likelihood`` is an unbiased
    estimate of the likelihood (on the log scale), so the filter can drive a
    pseudo-marginal sampler over ``theta``.

    Args:
        model: The model, written for one particle.
        num_particles: Number of particles.
        ess_threshold: Resample when the ESS falls below this fraction.
        proposal: ``(key, theta, x, y, t_previous, t) -> (x_next, log_q)``,
            a proposal that can look at the observation; needs
            ``model.transition_logdensity``. On steps without an observation
            the transition is used.
        unbiased_gradients: Pass gradients of the weights through resampling
            (a stop-gradient correction), for fitting ``theta`` by gradient.

    Returns:
        A :class:`~probjax.inference.Kernel` with ``init(key, theta, t0=0.0)``
        and ``step(key, state, theta, t, y, observed=True)``.
    """
    if not 0 <= ess_threshold <= 1:
        raise ValueError("ess_threshold must lie in [0, 1].")
    if proposal is not None and model.transition_logdensity is None:
        raise ValueError("A proposal needs model.transition_logdensity.")
    transition = jax.vmap(model.step, (0, None, 0, None, None))
    likelihood = jax.vmap(model.log_likelihood, (None, 0, None, None))

    def init(key: Any, theta: Any, t0: ArrayLike = 0.0) -> ParticleFilterState:
        keys = jax.random.split(key, num_particles)
        particles = jax.vmap(model.init, (0, None))(keys, theta)
        dtype = jnp.result_type(jax.tree.leaves(particles)[0], float)
        return ParticleFilterState(
            particles,
            jnp.full((num_particles,), -jnp.log(num_particles), dtype),
            as_time(t0),
            jnp.zeros((), dtype),
        )

    def propose(key, theta, particles, y, t_previous, t, observed):
        keys = jax.random.split(key, num_particles)
        moved = transition(keys, theta, particles, t_previous, t)
        if proposal is None:
            return moved, jnp.zeros(num_particles)
        guided, log_q = jax.vmap(proposal, (0, None, 0, None, None, None))(
            keys, theta, particles, y, t_previous, t
        )
        log_p = jax.vmap(model.transition_logdensity, (None, 0, 0, None, None))(
            theta, guided, particles, t_previous, t
        )
        particles = jax.tree.map(lambda a, b: jnp.where(observed, a, b), guided, moved)
        return particles, jnp.where(observed, log_p - log_q, 0.0)

    def step(
        key: Any,
        state: ParticleFilterState,
        theta: Any,
        t: ArrayLike,
        y: ArrayLike,
        observed: Any = True,
    ) -> Tuple[ParticleFilterState, ParticleFilterInfo]:
        move_key, resample_key = jax.random.split(key)
        t = jnp.asarray(t, state.t.dtype)
        y = unobserved(observed, jnp.asarray(y))
        particles, log_correction = propose(
            move_key, theta, state.particles, y, state.t, t, observed
        )
        log_increments = log_correction + jnp.where(
            observed, likelihood(theta, particles, y, t), 0.0
        )
        log_weights = state.log_weights + log_increments
        # The incoming weights are normalised, so this estimates p(y_t | y_<t).
        increment = logsumexp(log_weights)
        log_weights = log_weights - increment
        effective = ess(log_weights) / num_particles

        def resample(_):
            ancestors = systematic(resample_key, jnp.exp(log_weights), num_particles)
            new_log_weights = jnp.full_like(log_weights, -jnp.log(num_particles))
            if unbiased_gradients:
                kept = log_weights[ancestors]
                new_log_weights = new_log_weights + kept - jax.lax.stop_gradient(kept)
            return ancestors, new_log_weights

        def keep(_):
            return jnp.arange(num_particles, dtype=jnp.int32), log_weights

        ancestors, log_weights = jax.lax.cond(
            effective < ess_threshold, resample, keep, None
        )
        particles = take_batch(particles, ancestors)
        increment = increment.astype(state.log_likelihood.dtype)
        new_state = ParticleFilterState(
            particles,
            log_weights.astype(state.log_weights.dtype),
            t,
            state.log_likelihood + increment,
        )
        info = ParticleFilterInfo(
            ancestors, increment, effective, unique_fraction(ancestors)
        )
        return new_state, info

    return Kernel(init, step, lambda state, theta=None: theta)

probjax.inference.filtering.GaussianFilterState

Bases: NamedTuple

The filtering distribution N(mean, cov) at time t.

log_likelihood accumulates log p(y_1:t | theta) over the steps.

Source code in probjax/inference/filtering/gaussian.py
class GaussianFilterState(NamedTuple):
    """The filtering distribution ``N(mean, cov)`` at time ``t``.

    ``log_likelihood`` accumulates ``log p(y_1:t | theta)`` over the steps.
    """

    mean: Array
    cov: Array
    t: Array
    log_likelihood: Array

probjax.inference.filtering.GaussianFilterInfo

Bases: NamedTuple

The one-step prediction and this step's log-likelihood term.

Source code in probjax/inference/filtering/gaussian.py
class GaussianFilterInfo(NamedTuple):
    """The one-step prediction and this step's log-likelihood term."""

    mean_pred: Array
    cov_pred: Array
    log_likelihood_increment: Array

probjax.inference.filtering.ParticleFilterState

Bases: NamedTuple

A weighted particle approximation of the filtering distribution.

log_likelihood accumulates an unbiased estimate of p(y_1:t | theta) on the log scale.

Source code in probjax/inference/filtering/particle_filter.py
class ParticleFilterState(NamedTuple):
    """A weighted particle approximation of the filtering distribution.

    ``log_likelihood`` accumulates an unbiased estimate of ``p(y_1:t | theta)``
    on the log scale.
    """

    particles: Any
    log_weights: Array
    t: Array
    log_likelihood: Array

probjax.inference.filtering.ParticleFilterInfo

Bases: NamedTuple

Diagnostics of one filter step.

Attributes:

Name Type Description
ancestors Array

index of each particle's parent (itself without resampling).

log_likelihood_increment Array

this step's term of the log-likelihood.

ess Array

effective sample size of the weights, as a fraction.

unique_fraction Array

fraction of distinct parents; 1 / N means the population collapsed onto one, which the ESS does not show.

Source code in probjax/inference/filtering/particle_filter.py
class ParticleFilterInfo(NamedTuple):
    """Diagnostics of one filter step.

    Attributes:
        ancestors: index of each particle's parent (itself without resampling).
        log_likelihood_increment: this step's term of the log-likelihood.
        ess: effective sample size of the weights, as a fraction.
        unique_fraction: fraction of distinct parents; ``1 / N`` means the
            population collapsed onto one, which the ESS does not show.
    """

    ancestors: Array
    log_likelihood_increment: Array
    ess: Array
    unique_fraction: Array

probjax.inference.filtering.merwe_sigma_point

merwe_sigma_point(mu0, cov0, alpha=1.0, beta=2.0, kappa=0.0)

Generate sigma points for unscented Kalman filter, see [1].

Parameters:

Name Type Description Default
mu0 ArrayLike

Mean of the state

required
cov0 ArrayLike

Covariance of the state

required
alpha ArrayLike

Determines the spread around the mean (small values lead to large weight which require high precission (64 bit)). Large values will spread the sigma points further, typically letting to an overestimation of the covariance, small values will lead to an underestimation. Literature suggests 1e-3 but this requires 64 bit precision to work at all... . Defaults to 1..

1.0
beta ArrayLike

Prior on covariance. Defaults to 2..

2.0
kappa ArrayLike

Additional parameter. Defaults to 0..

0.0

Returns:

Type Description
Tuple[Array, Array, Array]

Tuple[NDArray, NDArray, NDArray]: Sigma points, weights_mean, weights_cov

References

[1] R. Van der Merwe "Sigma-Point Kalman Filters for Probabilitic Inference in Dynamic State-Space Models" (Doctoral dissertation)

Source code in probjax/inference/filtering/unscented_kalman_filter.py
def merwe_sigma_point(
    mu0: Array,
    cov0: Array,
    alpha: ArrayLike = 1.0,
    beta: ArrayLike = 2.0,
    kappa: ArrayLike = 0.0,
) -> Tuple[Array, Array, Array]:
    """Generate sigma points for unscented Kalman filter, see [1].

    Args:
        mu0 (ArrayLike): Mean of the state
        cov0 (ArrayLike): Covariance of the state
        alpha (ArrayLike, optional): Determines the spread around the mean
            (small values lead to large weight which require high precission (64 bit)).
            Large values will spread the sigma points further, typically letting
            to an overestimation of the covariance, small values will lead to an
            underestimation. Literature suggests 1e-3 but this requires 64 bit
            precision to work at all... . Defaults to 1..
        beta (ArrayLike, optional): Prior on covariance. Defaults to 2..
        kappa (ArrayLike, optional): Additional parameter. Defaults to 0..

    Returns:
        Tuple[NDArray, NDArray, NDArray]: Sigma points, weights_mean, weights_cov

    References:
        [1] R. Van der Merwe "Sigma-Point Kalman Filters for Probabilitic
            Inference in Dynamic State-Space Models" (Doctoral dissertation)
    """

    with jax.ensure_compile_time_eval():
        D = mu0.shape[0]
        lambda_ = alpha**2 * (D + kappa) - D
        weights_mean_0 = jnp.atleast_1d(lambda_ / (D + lambda_))
        weights_cov_0 = jnp.atleast_1d(weights_mean_0 + (1 - alpha**2 + beta))
        weights_mean_cov_1_L = 1 / (2 * (D + lambda_)) * jnp.ones(2 * D)

        weights_mean = jnp.concatenate([weights_mean_0, weights_mean_cov_1_L], axis=0)
        weights_cov = jnp.concatenate([weights_cov_0, weights_mean_cov_1_L], axis=0)

    sqrt_cov = jnp.linalg.cholesky((D + lambda_) * cov0)
    sigma_points_0 = mu0[None, :]
    sigma_points_1_L = mu0 + sqrt_cov.T
    sigma_points_L_2L = mu0 - sqrt_cov.T
    sigma_points = jnp.concatenate(
        [sigma_points_0, sigma_points_1_L, sigma_points_L_2L], axis=0
    )

    return sigma_points, weights_mean, weights_cov

probjax.inference.filtering.julier_uhlmann_sigma_points

julier_uhlmann_sigma_points(mu0, cov0, kappa=0.0)

Generate Julier-Uhlmann sigma points.

Parameters:

Name Type Description Default
mu0 ndarray

Mean of the state (D, )

required
cov0 ndarray

Covariance of the state (D, D)

required
kappa float

Spread parameter. Often chosen as kappa = 3 - D for state dimension D.

0.0

Returns:

Type Description
Tuple[Array, Array, Array]

Tuple[sigma_points, weights_mean, weights_cov]: sigma_points: (2D+1, D) weights_mean: (2D+1,) weights_cov: (2D+1,)

Source code in probjax/inference/filtering/unscented_kalman_filter.py
def julier_uhlmann_sigma_points(
    mu0: Array,
    cov0: Array,
    kappa: float = 0.0,
) -> Tuple[Array, Array, Array]:
    """Generate Julier-Uhlmann sigma points.

    Args:
        mu0 (jnp.ndarray): Mean of the state (D, )
        cov0 (jnp.ndarray): Covariance of the state (D, D)
        kappa (float): Spread parameter.
            Often chosen as kappa = 3 - D for state dimension D.

    Returns:
        Tuple[sigma_points, weights_mean, weights_cov]:
            sigma_points: (2D+1, D)
            weights_mean: (2D+1,)
            weights_cov: (2D+1,)
    """
    D = mu0.shape[0]
    lambda_ = D + kappa
    # Compute sqrt of scaled covariance
    sqrt_cov = jnp.linalg.cholesky(lambda_ * cov0)

    # Sigma points
    sigma_points_0 = mu0[None, :]
    sigma_points_pos = mu0 + sqrt_cov.T
    sigma_points_neg = mu0 - sqrt_cov.T
    sigma_points = jnp.concatenate(
        [sigma_points_0, sigma_points_pos, sigma_points_neg], axis=0
    )

    # Weights
    w0 = kappa / (D + kappa)
    wi = 1.0 / (2.0 * (D + kappa)) * jnp.ones(2 * D)
    weights_mean = jnp.concatenate([jnp.array([w0]), wi], axis=0)
    weights_cov = jnp.concatenate([jnp.array([w0]), wi], axis=0)

    return sigma_points, weights_mean, weights_cov

probjax.inference.filtering.spherical_simplex_sigma_points

spherical_simplex_sigma_points(mu0, cov0)

Generate spherical simplex sigma points without explicit Python loops.

This creates a (D+1) x D array of points that form a regular simplex. The steps are: 1. Construct an initial (D+1, D) array corresponding to a simplex. 2. Center the points so they have zero mean. 3. Scale the points to achieve unit covariance. 4. Apply the square root of the covariance (via Cholesky) and then add mu0.

Parameters:

Name Type Description Default
mu0 ndarray

Mean of the state, shape (D,)

required
cov0 ndarray

Covariance of the state, shape (D,D)

required

Returns:

Name Type Description
sigma_points ndarray

(D+1, D)

weights_mean ndarray

(D+1,)

weights_cov ndarray

(D+1,)

Source code in probjax/inference/filtering/unscented_kalman_filter.py
def spherical_simplex_sigma_points(
    mu0: Array, cov0: Array
) -> Tuple[Array, Array, Array]:
    """
    Generate spherical simplex sigma points without explicit Python loops.

    This creates a (D+1) x D array of points that form a regular simplex.
    The steps are:
    1. Construct an initial (D+1, D) array corresponding to a simplex.
    2. Center the points so they have zero mean.
    3. Scale the points to achieve unit covariance.
    4. Apply the square root of the covariance (via Cholesky) and then add mu0.

    Args:
        mu0 (jnp.ndarray): Mean of the state, shape (D,)
        cov0 (jnp.ndarray): Covariance of the state, shape (D,D)

    Returns:
        sigma_points (jnp.ndarray): (D+1, D)
        weights_mean (jnp.ndarray): (D+1,)
        weights_cov (jnp.ndarray): (D+1,)
    """
    D = mu0.shape[0]
    # A Helmert basis spans the subspace orthogonal to the all-ones
    # vector. Its columns are orthonormal, so equal weights recover I.
    rows = jnp.arange(D + 1)[:, None]
    cols = jnp.arange(D)[None, :]
    denominator = jnp.sqrt((cols + 1) * (cols + 2))
    basis = jnp.where(rows <= cols, 1.0, jnp.where(rows == cols + 1, -(cols + 1), 0.0))
    points = jnp.sqrt(D + 1) * basis / denominator
    sigma_points = mu0[None, :] + points @ jnp.linalg.cholesky(cov0).T

    # Equal weights for mean and covariance
    weights_mean = jnp.ones(D + 1) / (D + 1)
    weights_cov = jnp.ones(D + 1) / (D + 1)

    return sigma_points, weights_mean, weights_cov

Smoothing and trajectories

probjax.inference.smooth

smooth(model, theta, initial, states, infos)

Rauch--Tung--Striebel smoothed means and covariances of a Gaussian filter.

Exact for a :class:LinearGaussianModel filtered by a Kalman filter; for a :class:GaussianModel it linearises the transition at the filtered means (the extended RTS smoother). Both outputs include the initial time, so they have len(states.t) + 1 entries.

Source code in probjax/inference/filtering/trajectory.py
def smooth(
    model: Any, theta: Any, initial: Any, states: Any, infos: Any
) -> tuple[Array, Array]:
    """Rauch--Tung--Striebel smoothed means and covariances of a Gaussian filter.

    Exact for a :class:`LinearGaussianModel` filtered by a Kalman filter; for
    a :class:`GaussianModel` it linearises the transition at the filtered
    means (the extended RTS smoother). Both outputs include the initial time,
    so they have ``len(states.t) + 1`` entries.
    """
    means, covs, pred_means, pred_covs, gains = _gaussian_backward_data(
        model, theta, initial, states, infos
    )

    def backward(carry: Any, data: Any) -> Any:
        next_mean, next_cov = carry
        mean, cov, pred_mean, pred_cov, gain = data
        mean = mean + gain @ (next_mean - pred_mean)
        cov = symmetrize_matrix(cov + gain @ (next_cov - pred_cov) @ gain.T)
        return (mean, cov), (mean, cov)

    _, (smoothed_means, smoothed_covs) = jax.lax.scan(
        backward,
        (means[-1], covs[-1]),
        (means[:-1], covs[:-1], pred_means, pred_covs, gains),
        reverse=True,
    )
    return (
        jnp.concatenate((smoothed_means, means[-1:])),
        jnp.concatenate((smoothed_covs, covs[-1:])),
    )

probjax.inference.sample_paths

sample_paths(rng, model, theta, initial, states, infos, *, num_samples=1, method=None)

Draw joint latent paths from a filter's output at fixed theta.

Paths have shape (len(states.t) + 1, num_samples, D), including the initial state. A Gaussian filter's output is sampled backward from the RTS conditionals: exactly for a :class:LinearGaussianModel, linearised for a :class:GaussianModel. A particle filter's output uses method='backward' (FFBSi, cost O(T N M), needs model.transition_logdensity) or 'ancestry' (the stored parents, O(T M), prone to path collapse). By default backward sampling is used whenever the model has a transition density.

Source code in probjax/inference/filtering/trajectory.py
def sample_paths(
    rng,
    model: Any,
    theta: Any,
    initial: Any,
    states: Any,
    infos: Any,
    *,
    num_samples: int = 1,
    method: str | None = None,
) -> Array:
    """Draw joint latent paths from a filter's output at fixed ``theta``.

    Paths have shape ``(len(states.t) + 1, num_samples, D)``, including the
    initial state. A Gaussian filter's output is sampled backward from the
    RTS conditionals: exactly for a :class:`LinearGaussianModel`, linearised
    for a :class:`GaussianModel`. A particle filter's output uses
    ``method='backward'`` (FFBSi, cost ``O(T N M)``, needs
    ``model.transition_logdensity``) or ``'ancestry'`` (the stored parents,
    ``O(T M)``, prone to path collapse). By default backward sampling is used
    whenever the model has a transition density.
    """
    if num_samples < 1:
        raise ValueError("num_samples must be positive.")
    if not hasattr(states, "particles"):
        return _sample_gaussian_paths(
            rng, model, theta, initial, states, infos, num_samples
        )
    if method is None:
        method = "ancestry" if model.transition_logdensity is None else "backward"
    states = prepend_to_trace(initial, states)
    if method == "ancestry":
        return _genealogies(
            rng,
            states.particles,
            states.log_weights[-1],
            infos.ancestors,
            num_samples,
        )
    if method != "backward" or model.transition_logdensity is None:
        raise ValueError("Choose 'ancestry', or 'backward' with a transition density.")
    return _backward_simulation(rng, model, theta, states, num_samples)

probjax.inference.particle_gibbs

particle_gibbs(rng, reference_path, model, theta, ts, observations, *, t0=0.0, num_particles=32, observed=None, ancestor_sampling=True)

One conditional-SMC update (PGAS by default), returning a new joint path.

reference_path has shape (len(ts) + 1, D), including x(t0), and model is a :class:StateSpaceModel with array states. Ancestor sampling needs model.transition_logdensity. Multinomial resampling at every step is intentional: pinning one index in systematic resampling is not a valid conditional resampler.

The update preserves the smoothing law at fixed theta. Repeated calls form an MCMC chain; one call is not an independent posterior draw.

Source code in probjax/inference/filtering/trajectory.py
def particle_gibbs(
    rng,
    reference_path: Array,
    model: Any,
    theta: Any,
    ts: Any,
    observations: Any,
    *,
    t0: Any = 0.0,
    num_particles: int = 32,
    observed: Any = None,
    ancestor_sampling: bool = True,
) -> Array:
    """One conditional-SMC update (PGAS by default), returning a new joint path.

    ``reference_path`` has shape ``(len(ts) + 1, D)``, including ``x(t0)``, and
    ``model`` is a :class:`StateSpaceModel` with array states. Ancestor
    sampling needs ``model.transition_logdensity``. Multinomial resampling at
    every step is intentional: pinning one index in systematic resampling is
    not a valid conditional resampler.

    The update preserves the smoothing law at fixed ``theta``. Repeated calls
    form an MCMC chain; one call is not an independent posterior draw.
    """
    ts, observations, observed = validate_observations(ts, observations, observed)
    if num_particles < 2:
        raise ValueError("Conditional SMC requires at least two particles.")
    if reference_path.ndim != 2 or reference_path.shape[0] != ts.shape[0] + 1:
        raise ValueError("reference_path must have shape (len(ts)+1, D).")
    if ancestor_sampling and model.transition_logdensity is None:
        raise ValueError("Ancestor sampling needs model.transition_logdensity.")
    key, initial_key = jax.random.split(rng)
    particles = jax.vmap(model.init, (0, None))(
        jax.random.split(initial_key, num_particles), theta
    )
    particles = particles.at[0].set(reference_path[0])
    initial_particles = particles
    weights = jnp.full((num_particles,), -jnp.log(num_particles))
    transition = jax.vmap(model.step, (0, None, 0, None, None))
    likelihood = jax.vmap(model.log_likelihood, (None, 0, None, None))

    def step(carry: Any, data: Any) -> Any:
        key, particles, weights, previous = carry
        key, resample_key, ancestor_key, predict_key = jax.random.split(key, 4)
        t, y, mask, reference = data
        parents = jax.random.categorical(resample_key, weights, shape=(num_particles,))
        if ancestor_sampling:
            ancestor_weights = weights + jax.vmap(
                lambda x: model.transition_logdensity(theta, reference, x, previous, t)
            )(particles)
            reference_parent = jax.random.categorical(ancestor_key, ancestor_weights)
        else:
            reference_parent = 0
        parents = parents.at[0].set(reference_parent)
        keys = jax.random.split(predict_key, num_particles)
        particles = transition(keys, theta, particles[parents], previous, t)
        particles = particles.at[0].set(reference)
        weights = jax.lax.cond(
            mask,
            lambda: likelihood(theta, particles, y, t),
            lambda: jnp.zeros_like(weights),
        )
        weights = normalize_log_weights(weights)
        return (key, particles, weights, t), (particles, parents)

    (key, _, weights, _), (populations, ancestors) = jax.lax.scan(
        step,
        (key, particles, weights, jnp.asarray(t0, dtype=ts.dtype)),
        (ts, observations, observed, reference_path[1:]),
    )
    populations = jnp.concatenate((initial_particles[None], populations))
    return _genealogies(key, populations, weights, ancestors, 1)[:, 0]

Temporal SMC

probjax.inference.temporal_smc

temporal_smc(filter, logprior_fn, *, max_observations, ess_threshold=0.5, num_moves=1, proposal_fn=None, batch_size=0)

SMC over parameters that assimilates one observation per step.

Each parameter particle carries its own filter, built for one theta: a :func:~probjax.inference.particle_filter gives SMC² (unbiased likelihoods), a :func:~probjax.inference.kalman_filter exact parameter SMC. After every observation the outer weights are multiplied by the filters' likelihood increments. When the ESS drops below ess_threshold, parameters are resampled together with their filters and moved by num_moves pseudo-marginal Metropolis-Hastings steps, which re-filter the observations seen so far for each proposal.

The kernel steps like a filter, step(key, state, None, t, y, observed=True), so run(key, kernel, state, None, len(ts), args=(ts, ys)) assimilates a dataset, and running more steps continues it.

Parameters:

Name Type Description Default
filter Kernel

Filter kernel for one theta (its params).

required
logprior_fn Callable

theta -> scalar log prior. Initial parameters passed to init must be draws from this prior.

required
max_observations int

Capacity of the buffer of assimilated observations; steps beyond it are rejected (info.valid is false).

required
ess_threshold float

Resample when the ESS falls below this fraction.

0.5
num_moves int

MH moves after each resampling; 0 disables them.

1
proposal_fn Callable | None

(key, theta) -> (theta', log_q_reverse_minus_forward). Defaults to a Gaussian random walk with the weighted population covariance and a scale tuned towards 23.4% acceptance.

None
batch_size int

Parameters processed at once; 0 vectorizes all.

0

Returns:

Name Type Description
A Kernel

class:~probjax.inference.Kernel with ``init(key, parameters,

Kernel

observation_like, t0=0.0);observation_like`` is any observation,

Kernel

which sets the buffer's shape.

Source code in probjax/inference/smc/temporal.py
def temporal_smc(
    filter: Kernel,
    logprior_fn: Callable,
    *,
    max_observations: int,
    ess_threshold: float = 0.5,
    num_moves: int = 1,
    proposal_fn: Callable | None = None,
    batch_size: int = 0,
) -> Kernel:
    """SMC over parameters that assimilates one observation per step.

    Each parameter particle carries its own filter, built for one ``theta``:
    a :func:`~probjax.inference.particle_filter` gives SMC² (unbiased
    likelihoods), a :func:`~probjax.inference.kalman_filter` exact parameter
    SMC. After every observation the outer weights are multiplied by the
    filters' likelihood increments. When the ESS drops below
    ``ess_threshold``, parameters are resampled together with their filters
    and moved by ``num_moves`` pseudo-marginal Metropolis-Hastings steps,
    which re-filter the observations seen so far for each proposal.

    The kernel steps like a filter, ``step(key, state, None, t, y,
    observed=True)``, so ``run(key, kernel, state, None, len(ts), args=(ts,
    ys))`` assimilates a dataset, and running more steps continues it.

    Args:
        filter: Filter kernel for one ``theta`` (its params).
        logprior_fn: ``theta -> scalar`` log prior. Initial parameters passed
            to ``init`` must be draws from this prior.
        max_observations: Capacity of the buffer of assimilated observations;
            steps beyond it are rejected (``info.valid`` is false).
        ess_threshold: Resample when the ESS falls below this fraction.
        num_moves: MH moves after each resampling; ``0`` disables them.
        proposal_fn: ``(key, theta) -> (theta', log_q_reverse_minus_forward)``.
            Defaults to a Gaussian random walk with the weighted population
            covariance and a scale tuned towards 23.4% acceptance.
        batch_size: Parameters processed at once; ``0`` vectorizes all.

    Returns:
        A :class:`~probjax.inference.Kernel` with ``init(key, parameters,
        observation_like, t0=0.0)``; ``observation_like`` is any observation,
        which sets the buffer's shape.
    """
    if not 0 <= ess_threshold <= 1:
        raise ValueError("ess_threshold must lie in [0, 1].")
    ensure_int("num_moves", num_moves, min_value=0)
    ensure_int("max_observations", max_observations, min_value=1)
    adaptive = proposal_fn is None

    def init(
        key, parameters: Any, observation_like: Any, t0: Any = 0.0
    ) -> TemporalSMCState:
        leaves = jax.tree.leaves(parameters)
        if not leaves or any(x.ndim < 1 for x in leaves):
            raise ValueError("Parameter leaves must have a leading particle axis.")
        n = leaves[0].shape[0]
        if n < 1 or any(x.shape[0] != n for x in leaves):
            raise ValueError("All parameter leaves must have the same nonempty batch.")
        t0 = jnp.asarray(t0, dtype=jnp.result_type(t0, float))
        filter_states = _map(
            lambda a: filter.init(a[0], a[1], t0),
            (jax.random.split(key, n), parameters),
            batch_size,
        )
        log_priors = _map(logprior_fn, parameters, batch_size)
        if log_priors.shape != (n,):
            raise ValueError("logprior_fn must return a scalar per parameter.")
        dtype = filter_states.log_likelihood.dtype
        observation_like = jnp.asarray(observation_like)
        data = (
            jnp.zeros((max_observations,), t0.dtype),
            jnp.zeros(
                (max_observations,) + observation_like.shape, observation_like.dtype
            ),
            jnp.zeros((max_observations,), bool),
        )
        return TemporalSMCState(
            parameters,
            filter_states,
            jnp.full((n,), -jnp.log(n), dtype),
            jnp.zeros((), dtype),
            t0,
            t0,
            jnp.array(0, jnp.int32),
            jnp.ones((), dtype),
            data,
        )

    def rejuvenate(key, state: TemporalSMCState, parameters, filter_states, data):
        count = state.num_observations + 1  # includes the new observation
        # Only the adaptive proposal uses the factor; the placeholder keeps
        # its type concrete.
        factor: Array = jnp.zeros((0, 0))
        if adaptive:
            covariance = population_covariance(state.parameters, state.log_weights)
            factor = jnp.linalg.cholesky(covariance)

        def propose(key, theta: Any) -> tuple[Any, Any]:
            if not adaptive:
                assert proposal_fn is not None
                return proposal_fn(key, theta)
            flat, unravel = ravel_pytree(theta)
            noise = factor @ jax.random.normal(key, flat.shape)
            scale = state.proposal_scale * _SCALE_FACTOR / jnp.sqrt(flat.size)
            return unravel(flat + scale * noise), 0.0

        def move(_, carry: Any) -> Any:
            key, theta, filter_state, accepted = carry
            key, proposal_key, replay_key, accept_key = jax.random.split(key, 4)
            proposed, correction = propose(proposal_key, theta)
            proposed_state = replay_filter(
                filter, replay_key, proposed, state.t0, data, count
            )
            log_ratio = (
                logprior_fn(proposed)
                - logprior_fn(theta)
                + proposed_state.log_likelihood
                - filter_state.log_likelihood
                + correction
            )
            # The current estimate is kept on rejection: refreshing it would
            # change the pseudo-marginal target.
            log_p_accept: Any = jnp.where(jnp.isnan(log_ratio), -jnp.inf, log_ratio)
            current, (accept, _, _) = static_binomial_sampling(
                accept_key,
                log_p_accept,
                (theta, filter_state),
                (proposed, proposed_state),
            )
            return (key, *current, accepted + accept)

        def moves(args: Any) -> Any:
            key, theta, filter_state = args
            carry = (key, theta, filter_state, jnp.array(0.0))
            _, theta, filter_state, accepted = jax.lax.fori_loop(
                0, num_moves, move, carry
            )
            return theta, filter_state, accepted / num_moves

        n = state.log_weights.shape[0]
        parameters, filter_states, rates = _map(
            moves, (jax.random.split(key, n), parameters, filter_states), batch_size
        )
        return parameters, filter_states, rates.mean()

    def step(
        key,
        state: TemporalSMCState,
        params: Any,
        t: Any,
        y: Any,
        observed: Any = True,
    ) -> tuple[TemporalSMCState, TemporalSMCInfo]:
        del params
        t = jnp.asarray(t, state.t.dtype)
        count = state.num_observations
        n = state.log_weights.shape[0]
        filter_key, resample_key, move_key = jax.random.split(key, 3)
        ts, ys, mask = state.data
        slot = jnp.minimum(count, max_observations - 1)
        data = (ts.at[slot].set(t), ys.at[slot].set(y), mask.at[slot].set(observed))

        filter_states, infos = _map(
            lambda a: filter.step(a[0], a[1], a[2], t, y, observed),
            (jax.random.split(filter_key, n), state.filter_states, state.parameters),
            batch_size,
        )
        # A NaN increment (e.g. a simulator diverging at extreme parameters)
        # gives that particle zero weight instead of poisoning the population.
        increments = nan_to_neg_inf(infos.log_likelihood_increment)
        log_weights = state.log_weights + increments
        increment = jax.scipy.special.logsumexp(log_weights)
        valid = (count < max_observations) & (t > state.t) & jnp.isfinite(increment)
        log_weights = log_weights - increment
        ess = effective_sample_size(log_weights) / n
        resampled = valid & (ess < ess_threshold)

        ancestors = jax.lax.cond(
            resampled,
            lambda: systematic(resample_key, jnp.exp(log_weights), n),
            lambda: jnp.arange(n),
        ).astype(jnp.int32)
        parameters, filter_states = take_batch(
            (state.parameters, filter_states), ancestors
        )
        weighted = state._replace(log_weights=log_weights)
        log_weights = jnp.where(resampled, -jnp.log(n), log_weights)
        acceptance_rate = jnp.zeros((), log_weights.dtype)
        if num_moves > 0:
            parameters, filter_states, acceptance_rate = jax.lax.cond(
                resampled,
                lambda: rejuvenate(move_key, weighted, parameters, filter_states, data),
                lambda: (parameters, filter_states, acceptance_rate),
            )
        proposal_scale = state.proposal_scale
        if adaptive and num_moves > 0:
            # Multiplicative Robbins-Monro step towards the target acceptance.
            tuned = proposal_scale * jnp.exp(acceptance_rate - _TARGET_ACCEPTANCE)
            proposal_scale = jnp.where(
                resampled, jnp.clip(tuned, 1e-3, 1e3), proposal_scale
            )

        next_state = TemporalSMCState(
            parameters,
            filter_states,
            log_weights,
            state.log_evidence + increment,
            state.t0,
            t,
            count + 1,
            proposal_scale,
            data,
        )
        # Atomic: an invalid step (impossible observation, a full buffer, a
        # time that does not advance) leaves the state unchanged.
        next_state = jax.lax.cond(valid, lambda: next_state, lambda: state)
        info = TemporalSMCInfo(
            jnp.where(valid, increment, 0.0),
            jnp.where(valid, ess, 0.0),
            resampled,
            ancestors,
            unique_fraction(ancestors),
            acceptance_rate,
            valid,
        )
        return next_state, info

    return Kernel(init, step, lambda state, params=None: params)

probjax.inference.TemporalSMCState

Bases: NamedTuple

A weighted population of parameters, each with its own filter.

Attributes:

Name Type Description
parameters Any

pytree with a leading particle axis.

filter_states Any

each parameter's filter state; their log_likelihood is log p(y_1:t | theta) (or its estimate).

log_weights Any

normalised log weights.

log_evidence Any

running estimate of log p(y_1:t).

t0, t

initial time and time of the last assimilated observation.

num_observations Any

observations assimilated so far.

proposal_scale Any

scale of the adaptive random-walk move.

data Any

(ts, ys, observed) buffers of the assimilated observations, which the moves replay.

Source code in probjax/inference/smc/temporal.py
class TemporalSMCState(NamedTuple):
    """A weighted population of parameters, each with its own filter.

    Attributes:
        parameters: pytree with a leading particle axis.
        filter_states: each parameter's filter state; their
            ``log_likelihood`` is ``log p(y_1:t | theta)`` (or its estimate).
        log_weights: normalised log weights.
        log_evidence: running estimate of ``log p(y_1:t)``.
        t0, t: initial time and time of the last assimilated observation.
        num_observations: observations assimilated so far.
        proposal_scale: scale of the adaptive random-walk move.
        data: ``(ts, ys, observed)`` buffers of the assimilated observations,
            which the moves replay.
    """

    parameters: Any
    filter_states: Any
    log_weights: Any
    log_evidence: Any
    t0: Any
    t: Any
    num_observations: Any
    proposal_scale: Any
    data: Any

    def posterior_samples(self, rng, num_samples: int) -> Any:
        """Draw ``num_samples`` parameters from the weighted population."""
        idx = jax.random.categorical(rng, self.log_weights, shape=(num_samples,))
        return take_batch(self.parameters, idx)

posterior_samples

posterior_samples(rng, num_samples)

Draw num_samples parameters from the weighted population.

Source code in probjax/inference/smc/temporal.py
def posterior_samples(self, rng, num_samples: int) -> Any:
    """Draw ``num_samples`` parameters from the weighted population."""
    idx = jax.random.categorical(rng, self.log_weights, shape=(num_samples,))
    return take_batch(self.parameters, idx)

probjax.inference.TemporalSMCInfo

Bases: NamedTuple

Diagnostics of one assimilated observation.

Attributes:

Name Type Description
log_evidence_increment Any

this observation's term of the evidence.

ess Any

effective sample size before resampling, as a fraction.

resampled Any

whether the parameters were resampled.

ancestors Any

index of each particle's parent.

unique_fraction Any

fraction of distinct parents; 1 / N means the population collapsed onto one, which the ESS does not show.

acceptance_rate Any

mean acceptance of the moves (0 without moves).

valid Any

false if the step was rejected and the state left unchanged.

Source code in probjax/inference/smc/temporal.py
class TemporalSMCInfo(NamedTuple):
    """Diagnostics of one assimilated observation.

    Attributes:
        log_evidence_increment: this observation's term of the evidence.
        ess: effective sample size before resampling, as a fraction.
        resampled: whether the parameters were resampled.
        ancestors: index of each particle's parent.
        unique_fraction: fraction of distinct parents; ``1 / N`` means the
            population collapsed onto one, which the ESS does not show.
        acceptance_rate: mean acceptance of the moves (0 without moves).
        valid: false if the step was rejected and the state left unchanged.
    """

    log_evidence_increment: Any
    ess: Any
    resampled: Any
    ancestors: Any
    unique_fraction: Any
    acceptance_rate: Any
    valid: Any

probjax.inference.sample_joint_paths

sample_joint_paths(rng, state, filter, model, *, num_samples=100, method=None, batch_size=0)

Draw parameters from a temporal SMC state, then a path for each.

Each drawn theta re-runs filter over the observations the state has assimilated (it keeps them), then draws one path with :func:sample_paths. Paths have shape (num_samples, num_observations + 1, D). Gaussian paths are exact given each parameter; particle paths carry the filter's finite-particle error. Must be called outside jit, since the number of observations sets the shapes.

Source code in probjax/inference/filtering/joint.py
def sample_joint_paths(
    rng,
    state: Any,
    filter: Any,
    model: Any,
    *,
    num_samples: int = 100,
    method: str | None = None,
    batch_size: int = 0,
) -> JointPathSamples:
    """Draw parameters from a temporal SMC state, then a path for each.

    Each drawn ``theta`` re-runs ``filter`` over the observations the state
    has assimilated (it keeps them), then draws one path with
    :func:`sample_paths`. Paths have shape ``(num_samples,
    num_observations + 1, D)``. Gaussian paths are exact given each
    parameter; particle paths carry the filter's finite-particle error. Must
    be called outside ``jit``, since the number of observations sets the
    shapes.
    """
    if num_samples < 1:
        raise ValueError("num_samples must be positive.")
    if is_tracing(state.num_observations):
        raise ValueError("sample_joint_paths must be called outside jit.")
    count = int(state.num_observations)
    data = jax.tree.map(lambda buffer: buffer[:count], state.data)
    select_key, path_key = jax.random.split(rng)
    indices = jax.random.categorical(
        select_key, state.log_weights, shape=(num_samples,)
    )
    parameters = take_batch(state.parameters, indices)

    def draw(args: Any) -> Any:
        key, theta = args
        init_key, filter_key, sample_key = jax.random.split(key, 3)
        initial = filter.init(init_key, theta, state.t0)
        trace = run(
            filter_key,
            filter,
            initial,
            theta,
            count,
            args=data,
            collect=("state", "info"),
        ).trace
        paths = sample_paths(
            sample_key,
            model,
            theta,
            initial,
            trace["state"],
            trace["info"],
            method=method,
        )
        return paths[:, 0]

    keys = jax.random.split(path_key, num_samples)
    if batch_size:
        paths = jax.lax.map(draw, (keys, parameters), batch_size=batch_size)
    else:
        paths = jax.vmap(draw)((keys, parameters))
    return JointPathSamples(parameters, paths, indices)

Learned components

probjax.inference.Density

Bases: NamedTuple

A parameterised distribution, as inference needs it.

The parameters are an explicit argument, never closure state, so a kernel can carry them in its params and an adaptor can train them inside a compiled scan. :mod:probjax.inference.neural builds one from a :mod:probjax.nn model.

Attributes:

Name Type Description
sample Callable[[Any, Any], Any]

(key, params) -> x, one draw. Differentiable in params when the model is reparameterisable (flows are).

logpdf Callable[[Any, Any], Any]

(x, params) -> log q(x) for one point.

Source code in probjax/inference/base.py
class Density(NamedTuple):
    """A parameterised distribution, as inference needs it.

    The parameters are an explicit argument, never closure state, so a kernel
    can carry them in its ``params`` and an adaptor can train them inside a
    compiled scan. :mod:`probjax.inference.neural` builds one from a
    :mod:`probjax.nn` model.

    Attributes:
        sample: ``(key, params) -> x``, one draw. Differentiable in ``params``
            when the model is reparameterisable (flows are).
        logpdf: ``(x, params) -> log q(x)`` for one point.
    """

    sample: Callable[[Any, Any], Any]
    logpdf: Callable[[Any, Any], Any]

probjax.inference.neural.density

density(model)

Split a tractable-density model into a :class:Density and its params.

Parameters:

Name Type Description Default
model Any

a :mod:probjax.nn model with a log-density -- a normalizing flow (maf, nsf, ...) or an autoregressive model.

required

Returns:

Type Description
Density

(density, params). Flow samples are reparameterised, so

Any

density.sample is differentiable in params.

Source code in probjax/inference/neural.py
def density(model: Any) -> tuple[Density, Any]:
    """Split a tractable-density model into a :class:`Density` and its params.

    Args:
        model: a :mod:`probjax.nn` model with a log-density -- a normalizing
            flow (``maf``, ``nsf``, ...) or an autoregressive model.

    Returns:
        ``(density, params)``. Flow samples are reparameterised, so
        ``density.sample`` is differentiable in ``params``.
    """
    if not hasattr(model, "logpdf"):
        raise TypeError(f"{type(model).__name__} has no tractable log-density.")
    spec = model.as_dist().event_spec
    graphdef, params, rest = _split(model)

    def logpdf(x: Any, params: Any) -> Any:
        merged = nnx.merge(graphdef, params, rest)
        return merged.logpdf(_batch_of_one(x))[0]

    def sample(key: Any, params: Any) -> Any:
        merged = nnx.merge(graphdef, params, rest)
        # One key for both: a flow draws from the noise and ignores the key,
        # an autoregressive model draws from the key and ignores the noise.
        noise = merged.sample_base(key, (1,), spec)
        return _first(merged.sample_from_noise(noise, key))

    return Density(sample, logpdf), params

probjax.inference.neural.bijection

bijection(flow)

Split a normalizing flow into its map z -> (x, log|det J|) and params.

This is what :func:~probjax.inference.neutra needs.

Source code in probjax/inference/neural.py
def bijection(flow: Any) -> tuple[Callable[[Any, Any], tuple[Any, Any]], Any]:
    """Split a normalizing flow into its map ``z -> (x, log|det J|)`` and params.

    This is what :func:`~probjax.inference.neutra` needs.
    """
    graphdef, params, rest = _split(flow)

    def forward(z: Any, params: Any) -> tuple[Any, Any]:
        merged = nnx.merge(graphdef, params, rest)
        x = merged.transform(z)
        # The flow does not expose its Jacobian, but it does not have to:
        # log q(T(z)) = log base(z) - log|det J_T(z)|, which is exact and costs
        # one density call.
        log_q = merged.logpdf(_batch_of_one(x))[0]
        log_det = jnp.sum(merged.base_dist.logpdf(z)) - log_q
        return x, log_det

    return forward, params

probjax.inference.mcmc.imh.imh_data

imh_data(state, info)

Rao-Blackwellised training data for :func:fit_adaptor.

The next position is the proposal with probability acceptance_rate and the previous position otherwise, so both are stored with those weights. Proposals the chain rejects still teach the proposal where the target is not.

Source code in probjax/inference/mcmc/imh.py
def imh_data(state: RWState, info: IMHInfo) -> tuple[Any, Array]:
    """Rao-Blackwellised training data for :func:`fit_adaptor`.

    The next position is the proposal with probability ``acceptance_rate`` and
    the previous position otherwise, so both are stored with those weights.
    Proposals the chain rejects still teach the proposal where the target is
    not.
    """
    del state
    points = jax.tree.map(
        lambda previous, proposed: jnp.stack((previous, proposed)),
        info.previous_position,
        info.proposal,
    )
    weights = jnp.stack((1.0 - info.acceptance_rate, info.acceptance_rate))
    return points, weights

Variational inference

probjax.inference.flow_vi

Variational inference with a learned density as the variational family.

The objective is the reparameterised reverse KL, mean(log q(x) - log p(x)) over draws x from q. Each kernel step is one optimizer step, so the fit runs through :func:~probjax.inference.run like any sampler:

family, params = density(maf(dim, 4, rngs=nnx.Rngs(0))) kernel = flow_vi(logdensity_fn, family, optax.adam(1e-3)) result = run(key, kernel, kernel.init(params), None, 2000, collect=("loss",)) fitted = result.state.params

Reverse KL is mode-seeking. A flow fitted this way tends to under-cover: on Neal's funnel it concentrates in the neck and reports a smaller variance than the truth. That is a property of the objective, not of this implementation, and it is the reason :func:probjax.inference.neutra exists -- running MCMC in the flow's latent space stays asymptotically exact however imperfect the flow is, so the flow only has to be helpful, not correct.

FlowVIState

Bases: NamedTuple

Variational parameters and the optimizer state that drives them.

Source code in probjax/inference/vi/flow_vi.py
class FlowVIState(NamedTuple):
    """Variational parameters and the optimizer state that drives them."""

    params: Any
    opt_state: Any

FlowVIInfo

Bases: NamedTuple

Per-step diagnostics.

Attributes:

Name Type Description
loss Array

mean(log q - log p), the negative ELBO shifted by the target's unknown log-normaliser. It should go down; its absolute value is not interpretable.

Source code in probjax/inference/vi/flow_vi.py
class FlowVIInfo(NamedTuple):
    """Per-step diagnostics.

    Attributes:
        loss: ``mean(log q - log p)``, the negative ELBO shifted by the
            target's unknown log-normaliser. It should go down; its absolute
            value is not interpretable.
    """

    loss: Array

flow_vi

flow_vi(logdensity_fn, family, optimizer, *, num_samples=100, stl_estimator=True)

Fit family to logdensity_fn by reverse KL.

Parameters:

Name Type Description Default
logdensity_fn Callable

the unnormalized target log-density, taking one position.

required
family Density

the variational family; its sample must be reparameterised (flows from :func:probjax.inference.neural.density are).

required
optimizer GradientTransformation

an optax GradientTransformation.

required
num_samples int

draws used to estimate the objective at each step.

100
stl_estimator bool

use the sticking-the-landing gradient estimator, which lowers gradient variance at no cost in bias.

True

Returns:

Name Type Description
A Kernel

class:~probjax.inference.Kernel. kernel.init(params) starts

Kernel

from the family's params; the step takes no kernel params, so pass

Kernel

None to the runner.

Source code in probjax/inference/vi/flow_vi.py
def flow_vi(
    logdensity_fn: Callable,
    family: Density,
    optimizer: optax.GradientTransformation,
    *,
    num_samples: int = 100,
    stl_estimator: bool = True,
) -> Kernel:
    """Fit ``family`` to ``logdensity_fn`` by reverse KL.

    Args:
        logdensity_fn: the unnormalized target log-density, taking one position.
        family: the variational family; its ``sample`` must be reparameterised
            (flows from :func:`probjax.inference.neural.density` are).
        optimizer: an optax ``GradientTransformation``.
        num_samples: draws used to estimate the objective at each step.
        stl_estimator: use the sticking-the-landing gradient estimator, which
            lowers gradient variance at no cost in bias.

    Returns:
        A :class:`~probjax.inference.Kernel`. ``kernel.init(params)`` starts
        from the family's params; the step takes no kernel params, so pass
        ``None`` to the runner.
    """

    def init(params: Any) -> FlowVIState:
        return FlowVIState(params, optimizer.init(params))

    def step(rng, state: FlowVIState, params: Any = None, *args: Any):
        del params, args
        keys = jax.random.split(rng, num_samples)

        def objective(variational_params: Any) -> Array:
            draws = jax.vmap(family.sample, (0, None))(keys, variational_params)
            # Sticking the landing: with the entropy term's parameters
            # detached the score-function part of the gradient drops out, and
            # it is exactly zero in expectation -- so this removes variance
            # without adding bias.
            entropy_params = (
                jax.lax.stop_gradient(variational_params)
                if stl_estimator
                else variational_params
            )
            log_q = jax.vmap(family.logpdf, (0, None))(draws, entropy_params)
            log_p = jax.vmap(logdensity_fn)(draws)
            return jnp.mean(log_q - log_p)

        loss, gradient = jax.value_and_grad(objective)(state.params)
        updates, opt_state = optimizer.update(gradient, state.opt_state, state.params)
        new_params = optax.apply_updates(state.params, updates)
        return FlowVIState(new_params, opt_state), FlowVIInfo(loss)

    def init_params(state: FlowVIState) -> None:
        return None

    return Kernel(init, step, init_params)

probjax.inference.neutra

neutra(logdensity_fn, bijection, params)

Reparameterise logdensity_fn through a bijection T.

Parameters:

Name Type Description Default
logdensity_fn Callable

the unnormalized target log-density, taking one position.

required
bijection Callable[[Any, Any], tuple[Any, Any]]

(z, params) -> (T(z), log|det J_T(z)|). :func:probjax.inference.neural.bijection builds one from a flow, typically fitted with :func:probjax.inference.flow_vi; a flow trained on samples from a previous run is equally valid.

required
params Any

the bijection's parameters.

required

Returns:

Name Type Description
A NeuTraTransform

class:NeuTraTransform.

Source code in probjax/inference/vi/neutra.py
def neutra(
    logdensity_fn: Callable,
    bijection: Callable[[Any, Any], tuple[Any, Any]],
    params: Any,
) -> NeuTraTransform:
    """Reparameterise ``logdensity_fn`` through a bijection ``T``.

    Args:
        logdensity_fn: the unnormalized target log-density, taking one position.
        bijection: ``(z, params) -> (T(z), log|det J_T(z)|)``.
            :func:`probjax.inference.neural.bijection` builds one from a
            flow, typically fitted with :func:`probjax.inference.flow_vi`; a
            flow trained on samples from a previous run is equally valid.
        params: the bijection's parameters.

    Returns:
        A :class:`NeuTraTransform`.
    """

    def transformed_logdensity(position: Array) -> Array:
        target_position, log_det = bijection(position, params)
        return logdensity_fn(target_position) + log_det

    def forward(positions: Array) -> Array:
        positions = jnp.asarray(positions)
        flat = positions.reshape(-1, positions.shape[-1])
        mapped = jax.vmap(lambda z: bijection(z, params)[0])(flat)
        return mapped.reshape(*positions.shape[:-1], mapped.shape[-1])

    return NeuTraTransform(transformed_logdensity, forward)

probjax.inference.FlowVIState

Bases: NamedTuple

Variational parameters and the optimizer state that drives them.

Source code in probjax/inference/vi/flow_vi.py
class FlowVIState(NamedTuple):
    """Variational parameters and the optimizer state that drives them."""

    params: Any
    opt_state: Any

probjax.inference.FlowVIInfo

Bases: NamedTuple

Per-step diagnostics.

Attributes:

Name Type Description
loss Array

mean(log q - log p), the negative ELBO shifted by the target's unknown log-normaliser. It should go down; its absolute value is not interpretable.

Source code in probjax/inference/vi/flow_vi.py
class FlowVIInfo(NamedTuple):
    """Per-step diagnostics.

    Attributes:
        loss: ``mean(log q - log p)``, the negative ELBO shifted by the
            target's unknown log-normaliser. It should go down; its absolute
            value is not interpretable.
    """

    loss: Array

probjax.inference.NeuTraTransform

Bases: NamedTuple

A target rewritten in a flow's latent coordinates.

Attributes:

Name Type Description
logdensity Callable

the pulled-back log-density, to hand to any kernel.

forward Callable

maps latent draws back to the target's space. Accepts a single position or a leading batch of them.

Source code in probjax/inference/vi/neutra.py
class NeuTraTransform(NamedTuple):
    """A target rewritten in a flow's latent coordinates.

    Attributes:
        logdensity: the pulled-back log-density, to hand to any kernel.
        forward: maps latent draws back to the target's space. Accepts a single
            position or a leading batch of them.
    """

    logdensity: Callable
    forward: Callable

States and results

The types returned by the runners and kernels. Kernel State and Params are blackjax's own types, re-exported for convenience and documented there.

probjax.inference.RunResult

Bases: NamedTuple

The outcome of :func:~probjax.inference.run or a warmup.

trace maps each collected name to its values stacked over the kept steps. A warmup policy stores its own diagnostics there instead.

Source code in probjax/inference/base.py
class RunResult(NamedTuple):
    """The outcome of :func:`~probjax.inference.run` or a warmup.

    ``trace`` maps each collected name to its values stacked over the kept
    steps. A warmup policy stores its own diagnostics there instead.
    """

    state: Any
    params: Any
    trace: Any = None

probjax.inference.Kernel

Bases: NamedTuple

A pure transition and the functions needed to initialize it.

default_adaptation is an optional factory returning the kernel's recommended adaptation policy (an Adaptor or a Warmup). Runners use it when no explicit policy is passed; an explicitly passed policy always wins.

Source code in probjax/inference/base.py
class Kernel(NamedTuple):
    """A pure transition and the functions needed to initialize it.

    ``default_adaptation`` is an optional factory returning the kernel's
    recommended adaptation policy (an ``Adaptor`` or a ``Warmup``).
    Runners use it when no explicit policy is passed; an explicitly
    passed policy always wins.
    """

    init: Callable[..., Any]
    step: StepFn
    init_params: Callable[..., Any]
    default_adaptation: Callable[..., Any] | None = None

    def __call__(
        self,
        rng,
        state: Any,
        params: Any = None,
        *args: Any,
        **kwargs: Any,
    ) -> tuple[Any, Any]:
        return self.step(rng, state, params, *args, **kwargs)

probjax.inference.Warmup

Bases: NamedTuple

A finite initialization policy for a kernel and its parameters.

run(key, kernel, state, params, num_steps) returns a :class:RunResult whose trace holds the policy's diagnostics.

Source code in probjax/inference/base.py
class Warmup(NamedTuple):
    """A finite initialization policy for a kernel and its parameters.

    ``run(key, kernel, state, params, num_steps)`` returns a
    :class:`RunResult` whose ``trace`` holds the policy's diagnostics.
    """

    run: Callable[..., "RunResult"]

probjax.inference.Adaptor

Bases: NamedTuple

A local parameter-adaptation state machine.

init receives (state, params). update consumes one completed transition and can therefore be used during warmup or regular sampling.

Source code in probjax/inference/base.py
class Adaptor(NamedTuple):
    """A local parameter-adaptation state machine.

    ``init`` receives ``(state, params)``. ``update`` consumes one completed
    transition and can therefore be used during warmup or regular sampling.
    """

    init: Callable[[Any, Any], Any]
    update: Callable[[Any, Any, Any, Any], tuple[Any, Any, Any]]
    finalize: Callable[[Any, Any], tuple[Any, Any]]