Skip to content

Utilities

Numerical solvers, linear-algebra helpers and shared type aliases.

Solvers

See ODE and SDE integration for adaptive controllers, gradients and inverse log-determinants.

probjax.utils.odeint

odeint

odeint(drift, y0, ts, *args, solver='rk4', method=None, dtype=float32, filter_state=None, collect_trace=True, check_points=None, step_size_adaptor=None, logdet_rng=None, trace_estimator='exact', num_samples=1, sample_dist='rademacher')

Solve an ordinary differential equation dy/dt = drift(t, y, *args).

drift may be any of:

  • a plain Python callable drift(t, y, *args) — it is automatically wrapped in :class:probjax.utils.functions.generic_drift so it rides as a pytree through jax.jit and the custom_inverse primitive;
  • a registered JAX pytree node (eqx.Module, flax.struct.PyTreeNode, :class:~probjax.utils.functions.Drift subclass, ...) — its array leaves participate in transformations natively, non-array fields ride as aux.

Drift keyword arguments are no longer supported. Pass parameters positionally via *args, or bind them up-front with functools.partial (or drift.bind_args(...) on a :class:~probjax.utils.functions.Drift).

Parameters:

Name Type Description Default
drift Callable[..., PyTree[Array]]

The drift function f(t, y, *args).

required
y0 PyTree[Array]

Initial state. Single array or pytree of arrays.

required
ts Array

Time points at which to evaluate the solution.

required
*args Any

Positional arguments forwarded to drift.

()
solver Union[str, type, ConfiguredSolver]

Integration method: a registered name ("rk4", "dopri5", "bdf2", ...), a solver class, or a configured solver such as bdf2(newton_iters=3, jacobian="matrix_free").

'rk4'
method Optional[str]

Deprecated alias of solver.

None
dtype Optional[dtype]

Computation dtype (default float32).

float32
filter_state Optional[Callable[[PyTree[Array]], Optional[PyTree[Array]]]]

Optional function to filter the state during integration.

None
collect_trace bool

If True (default) return the filtered state at every time point; otherwise return only the filtered terminal state.

True
check_points Optional[Sequence[int]]

Optional index sequence for checkpointed grid integration.

None
step_size_adaptor Optional[StepSizeAdaptor]

Step-size controller for adaptive solver methods ("dopri5", "dopri8", "bosh3", …). Pass an instance of :class:~probjax.utils.odeutil.adaptive.StepSizeAdaptor (or a subclass) to tune rtol / atol / clip bounds / controller behavior. Ignored entirely by fixed-step methods ("rk4", "euler", "midpoint", …). Defaults to :class:StepSizeAdaptor() with the standard Hairer–Wanner controller.

None
logdet_rng Optional[Array]

RNG key for stochastic log-determinant estimators. Required when trace_estimator="hutchinson". Ignored on the forward path; only consumed by :func:probjax.core.inverse_and_logabsdet.

None
trace_estimator TraceEstimator

Log-det trace estimator used when the function is inverted via inverse_and_logabsdet. One of:

  • "exact" (default): full Jacobian per step (O(d²) cost).
  • "hutchinson": FFJORD-style stochastic estimator tr(J) ≈ mean_k vᵀ_k J v_k via one JVP per probe vector; probe vectors are fixed across the trajectory so ∫tr(J)dt stays unbiased. Requires logdet_rng.
  • a callable trace_fn(drift_flat, t, x_flat, args) -> scalar for custom structured-Jacobian strategies.
'exact'
num_samples int

Hutchinson probe-vector count per trajectory. Higher values reduce variance linearly in cost.

1
sample_dist SampleDist

Hutchinson probe distribution — "rademacher" (default, minimum-variance for general matrices) or "normal".

'rademacher'

Returns:

Type Description
Optional[PyTree[Array]]

Pytree containing either the time-series trace (when

Optional[PyTree[Array]]

collect_trace=True) or the filtered terminal state.

Example

import jax.numpy as jnp from probjax.utils.odeint import odeint

def lotka_volterra(t, y, alpha, beta, delta, gamma): ... prey, predator = y ... dprey = alpha * prey - beta * prey * predator ... dpredator = delta * prey * predator - gamma * predator ... return jnp.array([dprey, dpredator])

y0 = jnp.array([40.0, 9.0]) ts = jnp.linspace(0, 10, 100) ys = odeint(lotka_volterra, y0, ts, 1.0, 0.1, 0.075, 0.5, solver="dopri5")

Source code in probjax/utils/odeint.py
def odeint(
    drift: Callable[..., PyTree[Array]],
    y0: PyTree[Array],
    ts: Array,
    *args: Any,
    solver: Union[str, type, ConfiguredSolver] = "rk4",
    method: Optional[str] = None,
    dtype: Optional[jnp.dtype] = jnp.float32,
    filter_state: Optional[Callable[[PyTree[Array]], Optional[PyTree[Array]]]] = None,
    collect_trace: bool = True,
    check_points: Optional[Sequence[int]] = None,
    step_size_adaptor: Optional[StepSizeAdaptor] = None,
    logdet_rng: Optional[Array] = None,
    trace_estimator: TraceEstimator = "exact",
    num_samples: int = 1,
    sample_dist: SampleDist = "rademacher",
) -> Optional[PyTree[Array]]:
    """Solve an ordinary differential equation ``dy/dt = drift(t, y, *args)``.

    ``drift`` may be any of:

    - a plain Python callable ``drift(t, y, *args)`` — it is automatically
      wrapped in :class:`probjax.utils.functions.generic_drift` so it rides
      as a pytree through ``jax.jit`` and the ``custom_inverse`` primitive;
    - a registered JAX pytree node (``eqx.Module``, ``flax.struct.PyTreeNode``,
      :class:`~probjax.utils.functions.Drift` subclass, ...) — its array
      leaves participate in transformations natively, non-array fields ride
      as aux.

    Drift keyword arguments are no longer supported. Pass parameters
    positionally via ``*args``, or bind them up-front with ``functools.partial``
    (or ``drift.bind_args(...)`` on a :class:`~probjax.utils.functions.Drift`).

    Args:
        drift: The drift function ``f(t, y, *args)``.
        y0: Initial state. Single array or pytree of arrays.
        ts: Time points at which to evaluate the solution.
        *args: Positional arguments forwarded to ``drift``.
        solver: Integration method: a registered name (``"rk4"``,
            ``"dopri5"``, ``"bdf2"``, ...), a solver class, or a configured
            solver such as ``bdf2(newton_iters=3, jacobian="matrix_free")``.
        method: Deprecated alias of ``solver``.
        dtype: Computation dtype (default ``float32``).
        filter_state: Optional function to filter the state during integration.
        collect_trace: If ``True`` (default) return the filtered state at
            every time point; otherwise return only the filtered terminal state.
        check_points: Optional index sequence for checkpointed grid integration.
        step_size_adaptor: Step-size controller for **adaptive** solver
            methods (``"dopri5"``, ``"dopri8"``, ``"bosh3"``, …). Pass an
            instance of :class:`~probjax.utils.odeutil.adaptive.StepSizeAdaptor`
            (or a subclass) to tune ``rtol`` / ``atol`` / clip bounds /
            controller behavior. Ignored entirely by fixed-step methods
            (``"rk4"``, ``"euler"``, ``"midpoint"``, …). Defaults to
            :class:`StepSizeAdaptor()` with the standard Hairer–Wanner
            controller.
        logdet_rng: RNG key for stochastic log-determinant estimators.
            Required when ``trace_estimator="hutchinson"``. Ignored on the
            forward path; only consumed by
            :func:`probjax.core.inverse_and_logabsdet`.
        trace_estimator: Log-det trace estimator used when the function is
            inverted via ``inverse_and_logabsdet``. One of:

            - ``"exact"`` (default): full Jacobian per step (O(d²) cost).
            - ``"hutchinson"``: FFJORD-style stochastic estimator
              ``tr(J) ≈ mean_k vᵀ_k J v_k`` via one JVP per probe vector;
              probe vectors are fixed across the trajectory so
              ``∫tr(J)dt`` stays unbiased. Requires ``logdet_rng``.
            - a callable ``trace_fn(drift_flat, t, x_flat, args) -> scalar``
              for custom structured-Jacobian strategies.
        num_samples: Hutchinson probe-vector count per trajectory. Higher
            values reduce variance linearly in cost.
        sample_dist: Hutchinson probe distribution — ``"rademacher"``
            (default, minimum-variance for general matrices) or
            ``"normal"``.

    Returns:
        Pytree containing either the time-series trace (when
        ``collect_trace=True``) or the filtered terminal state.

    Example:
        >>> import jax.numpy as jnp
        >>> from probjax.utils.odeint import odeint
        >>>
        >>> def lotka_volterra(t, y, alpha, beta, delta, gamma):
        ...     prey, predator = y
        ...     dprey = alpha * prey - beta * prey * predator
        ...     dpredator = delta * prey * predator - gamma * predator
        ...     return jnp.array([dprey, dpredator])
        >>>
        >>> y0 = jnp.array([40.0, 9.0])
        >>> ts = jnp.linspace(0, 10, 100)
        >>> ys = odeint(lotka_volterra, y0, ts, 1.0, 0.1, 0.075, 0.5, solver="dopri5")
    """
    solver = solver_or_legacy_method(solver, method, "odeint", default="rk4")
    drift = wrap_if_plain_callable(drift)
    return _odeint_custom(
        y0,
        drift,
        ts,
        tuple(args),
        logdet_rng,
        method=solver,
        dtype=dtype,
        filter_state=filter_state,
        collect_trace=collect_trace,
        check_points=check_points,
        step_size_adaptor=step_size_adaptor,
        trace_estimator=trace_estimator,
        num_samples=num_samples,
        sample_dist=sample_dist,
    )

probjax.utils.sdeint

sdeint

sdeint(rng, drift, diffusion, y0, ts, *args, solver='euler_maruyama', method=None, dtype=float32, sde_type='ito', return_brownian=False, return_state=False, filter_state=None, collect_trace=True, check_points=None, step_size_adaptor=None)

Solve a stochastic differential equation dy = drift(t, y, *args) dt + diffusion(t, y, *args) dW.

drift and diffusion may be plain Python callables (automatically wrapped as :class:~probjax.utils.functions.generic_drift) or any pytree-registered callable (marker :class:~probjax.utils.functions.Drift subclass, eqx.Module, etc.).

Keyword arguments to drift/diffusion are no longer supported. Pass parameters positionally via *args, or bind them with functools.partial / drift.bind_args(...).

Parameters:

Name Type Description Default
rng Key

Random number generator key.

required
drift Callable[..., PyTree[Array]]

Drift function f(t, y, *args) — deterministic part of dy = f(t, y, *args) dt + g(t, y, *args) dW_t.

required
diffusion Callable[..., PyTree[Array]]

Diffusion function g(t, y, *args) — stochastic part.

required
y0 PyTree[Array]

Initial state. Single array or pytree of arrays.

required
ts Array

Strictly increasing 1D array of time points.

required
*args Any

Positional arguments forwarded to both drift and diffusion.

()
solver Union[str, type, ConfiguredSolver]

Integration method: a registered name, a solver class or a configured solver (milstein(...)). Registered names include:

  • "euler_maruyama": Euler-Maruyama (order 0.5)
  • "exp_euler_maruyama": Exponential Euler-Maruyama (requires :class:~probjax.utils.functions.split_drift)
  • "milstein": Milstein (order 1.0)
  • "sri1", "sri2": Stochastic Runge-Kutta methods (strong order 1.0); "srk" is an alias of "sri1"
  • "linear_exact_sde": exact transition sampling for linear SDEs (requires :class:~probjax.utils.functions.linear_drift and :class:~probjax.utils.functions.const_diffusion)
'euler_maruyama'
method Optional[str]

Deprecated alias of solver.

None
dtype Optional[dtype]

Computation dtype (default float32).

float32
sde_type str

"ito" (default) or "stratonovich". Noise layout is inferred from the diffusion output shape — scalar/vector outputs are diagonal, matrix outputs are full (including rectangular).

'ito'
return_brownian bool

Whether to return Brownian paths (requires collect_trace=True).

False
return_state bool

Whether to return solver state.

False
filter_state Optional[Callable[[PyTree[Array]], Optional[PyTree[Array]]]]

Optional state filter; returning None disables tracing entirely.

None
collect_trace bool

Record the filtered quantity at every time step (True, default) or return only the filtered terminal state (False). Must be True when returning Brownian paths.

True
check_points Optional[Sequence[int]]

Optional index sequence for checkpointed grid integration.

None
step_size_adaptor Optional[SDEStepSizeAdaptor]

When provided, switches integration to an adaptive step-doubling Euler-Maruyama scheme driven by the given controller. Pass :class:~probjax.utils.sdeutil.adaptive.WeakStepSizeAdaptor for distributional / weak quantities (fresh noise from the key) or :class:~probjax.utils.sdeutil.adaptive.StrongStepSizeAdaptor for per-path consistency (increments from a single virtual Brownian tree, so runs at different tolerances are comparable per path). Steps are never rejected on their own noise; the local error only sets the next step size. Currently restricted to diagonal noise and ignores solver (always uses Euler-Maruyama with step-doubling error estimation). Defaults to None (fixed-step integration via solver).

None

Returns:

Type Description
Union[Optional[PyTree[Array]], Tuple[Any, Optional[PyTree[Array]]], Tuple[Optional[PyTree[Array]], Optional[PyTree[Array]]], Tuple[Any, Tuple[Optional[PyTree[Array]], Optional[PyTree[Array]]]]]

When return_brownian=False: the filtered trajectory (if

Union[Optional[PyTree[Array]], Tuple[Any, Optional[PyTree[Array]]], Tuple[Optional[PyTree[Array]], Optional[PyTree[Array]]], Tuple[Any, Tuple[Optional[PyTree[Array]], Optional[PyTree[Array]]]]]

collect_trace=True) or the filtered terminal state. When

Union[Optional[PyTree[Array]], Tuple[Any, Optional[PyTree[Array]]], Tuple[Optional[PyTree[Array]], Optional[PyTree[Array]]], Tuple[Any, Tuple[Optional[PyTree[Array]], Optional[PyTree[Array]]]]]

return_brownian=True: (state_trace, brownian_trace). If

Union[Optional[PyTree[Array]], Tuple[Any, Optional[PyTree[Array]]], Tuple[Optional[PyTree[Array]], Optional[PyTree[Array]]], Tuple[Any, Tuple[Optional[PyTree[Array]], Optional[PyTree[Array]]]]]

return_state=True, the solver state is prepended.

Example

import jax.numpy as jnp from probjax.utils.sdeint import sdeint from jax import random

def drift(t, state, mu, sigma): ... return {"price": mu * state["price"]} def diffusion(t, state, mu, sigma): ... return {"price": sigma * state["price"]}

rng = random.PRNGKey(0) y0 = {"price": jnp.array([1.0])} ts = jnp.linspace(0, 1, 100) ys = sdeint(rng, drift, diffusion, y0, ts, 0.1, 0.2)

Source code in probjax/utils/sdeint.py
def sdeint(
    rng: Key,
    drift: Callable[..., PyTree[Array]],
    diffusion: Callable[..., PyTree[Array]],
    y0: PyTree[Array],
    ts: Array,
    *args: Any,
    solver: Union[str, type, ConfiguredSolver] = "euler_maruyama",
    method: Optional[str] = None,
    dtype: Optional[jnp.dtype] = jnp.float32,
    sde_type: str = "ito",
    return_brownian: bool = False,
    return_state: bool = False,
    filter_state: Optional[Callable[[PyTree[Array]], Optional[PyTree[Array]]]] = None,
    collect_trace: bool = True,
    check_points: Optional[Sequence[int]] = None,
    step_size_adaptor: Optional[SDEStepSizeAdaptor] = None,
) -> Union[
    Optional[PyTree[Array]],
    Tuple[Any, Optional[PyTree[Array]]],
    Tuple[
        Optional[PyTree[Array]],
        Optional[PyTree[Array]],
    ],
    Tuple[
        Any,
        Tuple[Optional[PyTree[Array]], Optional[PyTree[Array]]],
    ],
]:
    """Solve a stochastic differential equation
    ``dy = drift(t, y, *args) dt + diffusion(t, y, *args) dW``.

    ``drift`` and ``diffusion`` may be plain Python callables (automatically
    wrapped as :class:`~probjax.utils.functions.generic_drift`) or any
    pytree-registered callable (marker
    :class:`~probjax.utils.functions.Drift` subclass, ``eqx.Module``, etc.).

    Keyword arguments to drift/diffusion are no longer supported. Pass
    parameters positionally via ``*args``, or bind them with
    ``functools.partial`` / ``drift.bind_args(...)``.

    Args:
        rng: Random number generator key.
        drift: Drift function ``f(t, y, *args)`` — deterministic part of
            ``dy = f(t, y, *args) dt + g(t, y, *args) dW_t``.
        diffusion: Diffusion function ``g(t, y, *args)`` — stochastic part.
        y0: Initial state. Single array or pytree of arrays.
        ts: Strictly increasing 1D array of time points.
        *args: Positional arguments forwarded to both drift and diffusion.
        solver: Integration method: a registered name, a solver class or a
            configured solver (``milstein(...)``). Registered names include:

            - ``"euler_maruyama"``: Euler-Maruyama (order 0.5)
            - ``"exp_euler_maruyama"``: Exponential Euler-Maruyama
              (requires :class:`~probjax.utils.functions.split_drift`)
            - ``"milstein"``: Milstein (order 1.0)
            - ``"sri1"``, ``"sri2"``: Stochastic Runge-Kutta methods
              (strong order 1.0); ``"srk"`` is an alias of ``"sri1"``
            - ``"linear_exact_sde"``: exact transition sampling for linear SDEs
              (requires :class:`~probjax.utils.functions.linear_drift` and
              :class:`~probjax.utils.functions.const_diffusion`)
        method: Deprecated alias of ``solver``.
        dtype: Computation dtype (default ``float32``).
        sde_type: ``"ito"`` (default) or ``"stratonovich"``. Noise layout
            is inferred from the diffusion output shape — scalar/vector
            outputs are diagonal, matrix outputs are full (including
            rectangular).
        return_brownian: Whether to return Brownian paths (requires
            ``collect_trace=True``).
        return_state: Whether to return solver state.
        filter_state: Optional state filter; returning ``None`` disables
            tracing entirely.
        collect_trace: Record the filtered quantity at every time step
            (``True``, default) or return only the filtered terminal state
            (``False``). Must be ``True`` when returning Brownian paths.
        check_points: Optional index sequence for checkpointed grid
            integration.
        step_size_adaptor: When provided, switches integration to an
            adaptive step-doubling Euler-Maruyama scheme driven by the
            given controller. Pass
            :class:`~probjax.utils.sdeutil.adaptive.WeakStepSizeAdaptor`
            for distributional / weak quantities (fresh noise from the key)
            or :class:`~probjax.utils.sdeutil.adaptive.StrongStepSizeAdaptor`
            for per-path consistency (increments from a single virtual
            Brownian tree, so runs at different tolerances are comparable
            per path). Steps are never rejected on their own noise; the
            local error only sets the next step size. Currently restricted to
            **diagonal noise** and ignores ``solver`` (always uses Euler-Maruyama with
            step-doubling error estimation). Defaults to ``None``
            (fixed-step integration via ``solver``).

    Returns:
        When ``return_brownian=False``: the filtered trajectory (if
        ``collect_trace=True``) or the filtered terminal state. When
        ``return_brownian=True``: ``(state_trace, brownian_trace)``. If
        ``return_state=True``, the solver state is prepended.

    Example:
        >>> import jax.numpy as jnp
        >>> from probjax.utils.sdeint import sdeint
        >>> from jax import random
        >>>
        >>> def drift(t, state, mu, sigma):
        ...     return {"price": mu * state["price"]}
        >>> def diffusion(t, state, mu, sigma):
        ...     return {"price": sigma * state["price"]}
        >>>
        >>> rng = random.PRNGKey(0)
        >>> y0 = {"price": jnp.array([1.0])}
        >>> ts = jnp.linspace(0, 1, 100)
        >>> ys = sdeint(rng, drift, diffusion, y0, ts, 0.1, 0.2)
    """
    solver = solver_or_legacy_method(solver, method, "sdeint", default="euler_maruyama")
    drift = wrap_if_plain_callable(drift)
    diffusion = wrap_if_plain_callable(diffusion)
    result = _sdeint(
        rng,
        drift,
        diffusion,
        y0,
        ts,
        tuple(args),
        method=solver,
        dtype=dtype,
        sde_type=sde_type,
        return_brownian=return_brownian,
        return_state=return_state,
        filter_state=filter_state,
        collect_trace=collect_trace,
        check_points=check_points,
        step_size_adaptor=step_size_adaptor,
    )
    # ``_sdeint`` appends an adaptive-diagnostic hit count as the last
    # element of its return so we can warn host-side (no per-vmap-element
    # callback overhead). Strip it before returning to the user; warn iff
    # the controller ran out of budget on a meaningful fraction of the
    # output segments.
    if return_state:
        state_obj, payload, diag_hits = result
        out = (state_obj, payload)
    else:
        payload, diag_hits = result
        out = payload
    # Host-side boundary warning. Skip when a tracer flows through (under
    # ``jax.vmap`` / ``jax.jit`` of ``sdeint`` itself); the warning will
    # surface from the outermost concrete invocation. ``jax.core.Tracer``
    # check costs nothing under vmap and avoids the per-element callback
    # dispatch we'd pay if the warn lived inside the JIT graph.
    if step_size_adaptor is not None and not is_tracing(diag_hits):
        n_segments = int(jnp.atleast_1d(jnp.asarray(ts)).shape[0]) - 1
        warn_boundary_hits(step_size_adaptor, diag_hits, n_segments)
    return out

probjax.utils.root

root(fun, x0, args=(), method='newton-raphson', tol=0.001, max_iter=20, **newton_kwargs)

Find a root of fun(x, *args).

Parameters:

Name Type Description Default
fun Callable

Function to find root of.

required
x0 Array

Initial value.

required
args tuple

Extra arguments to pass to function. Defaults to ().

()
method str

Method to use. Only "newton-raphson".

'newton-raphson'
tol float

Tolerance, floored at machine epsilon. Defaults to 1e-3.

0.001
max_iter int

Maximum number of iterations. Defaults to 20.

20
**newton_kwargs

jacobian/linear_solver for :func:newton_raphson.

{}

Returns:

Type Description

(x, converged).

Source code in probjax/utils/solver.py
def root(
    fun,
    x0,
    args=(),
    method="newton-raphson",
    tol=1e-3,
    max_iter=20,
    **newton_kwargs,
):
    """Find a root of ``fun(x, *args)``.

    Args:
        fun (Callable): Function to find root of.
        x0 (Array): Initial value.
        args (tuple, optional): Extra arguments to pass to function. Defaults to ().
        method (str, optional): Method to use. Only ``"newton-raphson"``.
        tol (float, optional): Tolerance, floored at machine epsilon. Defaults to 1e-3.
        max_iter (int, optional): Maximum number of iterations. Defaults to 20.
        **newton_kwargs: ``jacobian``/``linear_solver`` for :func:`newton_raphson`.

    Returns:
        ``(x, converged)``.
    """

    # Dtype constraints on tolerance
    dtype = x0.dtype
    tol = max(tol, jnp.finfo(dtype).eps)

    _f = lambda x: fun(x, *args)

    if method == "newton-raphson":
        return newton_raphson(_f, x0, tol=tol, max_iter=max_iter, **newton_kwargs)
    else:
        raise NotImplementedError(f"Method {method} not implemented.")

probjax.utils.newton_raphson

newton_raphson(f, x0, tol=1e-06, max_iter=50, *, jacobian='dense', linear_solver='auto')

Newton-Raphson root finding for f(x) = 0 with f(x).shape == x.shape.

Gradients of the returned root use the implicit function theorem (jax.lax.custom_root) instead of differentiating through the iterations, so they cost one extra linear solve. The gradient is only valid when the solve converged: always check the returned flag before differentiating (callers that keep stepping, like the implicit ODE solvers, record it in their info).

Parameters:

Name Type Description Default
f

Function whose root is sought.

required
x0

Initial guess.

required
tol

Stop once a Newton step is shorter than this (in 2-norm), floored at machine epsilon.

1e-06
max_iter

Maximum number of Newton steps (at least 1).

50
jacobian str

"dense" materializes the Jacobian with forward mode, which is best for small states. "matrix_free" applies it with one JVP per product, for large states with an iterative solver.

'dense'
linear_solver Any

Solver for each Newton step, passed to :func:probjax.utils.linear_operator.solve: "auto" (LU for dense, GMRES for matrix-free Jacobians), another solver name or a lineax solver. Inner solves use :func:solve defaults. Newton Jacobians are generally nonsymmetric, so "cg"/"cholesky" are opt-in at the caller's risk.

'auto'

Returns:

Type Description

(x, converged): the root estimate and whether a step was shorter

than tol (a boolean array).

Source code in probjax/utils/solver.py
def newton_raphson(
    f,
    x0,
    tol=1e-6,
    max_iter=50,
    *,
    jacobian: str = "dense",
    linear_solver: Any = "auto",
):
    """Newton-Raphson root finding for ``f(x) = 0`` with ``f(x).shape == x.shape``.

    Gradients of the returned root use the implicit function theorem
    (``jax.lax.custom_root``) instead of differentiating through the
    iterations, so they cost one extra linear solve. The gradient is only
    valid when the solve converged: always check the returned flag before
    differentiating (callers that keep stepping, like the implicit ODE
    solvers, record it in their info).

    Args:
        f: Function whose root is sought.
        x0: Initial guess.
        tol: Stop once a Newton step is shorter than this (in 2-norm),
            floored at machine epsilon.
        max_iter: Maximum number of Newton steps (at least 1).
        jacobian: ``"dense"`` materializes the Jacobian with forward mode,
            which is best for small states. ``"matrix_free"`` applies it with
            one JVP per product, for large states with an iterative solver.
        linear_solver: Solver for each Newton step, passed to
            :func:`probjax.utils.linear_operator.solve`: ``"auto"`` (LU for
            dense, GMRES for matrix-free Jacobians), another solver name or a
            lineax solver. Inner solves use :func:`solve` defaults. Newton
            Jacobians are generally nonsymmetric, so ``"cg"``/``"cholesky"``
            are opt-in at the caller's risk.

    Returns:
        ``(x, converged)``: the root estimate and whether a step was shorter
        than ``tol`` (a boolean array).
    """
    if jacobian not in ("dense", "matrix_free"):
        raise ValueError(f"jacobian must be 'dense' or 'matrix_free', got {jacobian!r}")
    if max_iter < 1:
        raise ValueError(f"max_iter must be at least 1, got {max_iter}.")
    x0 = jnp.asarray(x0)
    tol = max(tol, jnp.finfo(x0.dtype).eps)
    shape = x0.shape

    def flat_f(x):
        return f(x.reshape(shape)).ravel()

    def jacobian_at(g, x):
        return Dense.jacfwd(g, x) if jacobian == "dense" else Jacobian(g, x)

    def newton_solve(g, x):
        def scan_fn(carry, _):
            converged, x = carry

            def newton_step(x):
                delta = solve(jacobian_at(g, x), -g(x), linear_solver)
                return x + delta, jnp.linalg.norm(delta)

            x, step_size = jax.lax.cond(converged, lambda x: (x, 0.0), newton_step, x)
            return (converged | (step_size < tol), x), None

        (converged, x), _ = jax.lax.scan(
            scan_fn, (jnp.asarray(False), x), None, length=max_iter
        )
        # Aux crosses custom_root as a float: JAX < 0.10 builds bool aux
        # tangents as bool arrays instead of zero tangents, which fails
        # custom_jvp validation under grad. The comparison below restores
        # the boolean flag (a no-op for differentiation: aux tangents are
        # zero on every supported version).
        return x, jnp.asarray(converged, dtype=jnp.float32)

    def tangent_solve(g, y):
        # g is the Jacobian-vector product of f at the root.
        op = Function(g, y.shape, dtype=y.dtype)
        return solve(op.as_dense() if jacobian == "dense" else op, y, linear_solver)

    x, converged = jax.lax.custom_root(
        flat_f, x0.ravel(), newton_solve, tangent_solve, has_aux=True
    )
    return x.reshape(shape), converged > 0.5

Special functions

probjax.utils.betaincinv

Regularized beta inverses with cached solvers and implicit reverse AD.

betaincinv

betaincinv(a, b, p, *, max_halley_steps=6, max_bisection_steps=15)

Invert regularized lower beta for finite positive shapes.

Probabilities are clipped to [0, 1]; invalid shapes return NaN. The total safeguarded Halley/bisection budget is 2 plus both step settings. Reverse AD uses the inverse density and central differences for shapes; forward AD and higher shape derivatives are not supported.

Source code in probjax/utils/special/betaincinv.py
def betaincinv(a, b, p, *, max_halley_steps=6, max_bisection_steps=15):
    """Invert regularized lower beta for finite positive shapes.

    Probabilities are clipped to [0, 1]; invalid shapes return NaN. The total
    safeguarded Halley/bisection budget is 2 plus both step settings.
    Reverse AD uses the inverse density and central differences for shapes;
    forward AD and higher shape derivatives are not supported.
    """
    return _inverse(a, b, p, max_halley_steps, max_bisection_steps, False)

betainccinv

betainccinv(a, b, q, *, max_halley_steps=6, max_bisection_steps=15)

Invert regularized upper beta directly, preserving tiny q.

q=0 returns one; q=1 returns zero. Domain, step settings and differentiation have the same semantics as betaincinv.

Source code in probjax/utils/special/betaincinv.py
def betainccinv(a, b, q, *, max_halley_steps=6, max_bisection_steps=15):
    """Invert regularized upper beta directly, preserving tiny q.

    q=0 returns one; q=1 returns zero. Domain, step settings and differentiation
    have the same semantics as betaincinv.
    """
    return _inverse(a, b, q, max_halley_steps, max_bisection_steps, True)

probjax.utils.betainccinv

betainccinv(a, b, q, *, max_halley_steps=6, max_bisection_steps=15)

Invert regularized upper beta directly, preserving tiny q.

q=0 returns one; q=1 returns zero. Domain, step settings and differentiation have the same semantics as betaincinv.

Source code in probjax/utils/special/betaincinv.py
def betainccinv(a, b, q, *, max_halley_steps=6, max_bisection_steps=15):
    """Invert regularized upper beta directly, preserving tiny q.

    q=0 returns one; q=1 returns zero. Domain, step settings and differentiation
    have the same semantics as betaincinv.
    """
    return _inverse(a, b, q, max_halley_steps, max_bisection_steps, True)

probjax.utils.gammaincinv

Regularized gamma inverses with implicit differentiation.

gammaincinv

gammaincinv(a, p)

Invert regularized lower gamma; finite a > 0, p clipped to [0, 1].

Source code in probjax/utils/special/gammaincinv.py
@jax.jit
def gammaincinv(a, p):
    """Invert regularized lower gamma; finite a > 0, p clipped to [0, 1]."""
    return _inverse(a, p, False)

gammainccinv

gammainccinv(a, q)

Invert regularized upper gamma directly, preserving tiny q.

Finite a > 0 is required. Probabilities are clipped to [0, 1]; q=0 returns infinity and q=1 returns zero. Invalid shapes return NaN. Supports implicit forward and reverse differentiation.

Source code in probjax/utils/special/gammaincinv.py
@jax.jit
def gammainccinv(a, q):
    """Invert regularized upper gamma directly, preserving tiny q.

    Finite a > 0 is required. Probabilities are clipped to [0, 1]; q=0
    returns infinity and q=1 returns zero. Invalid shapes return NaN.
    Supports implicit forward and reverse differentiation.
    """
    return _inverse(a, q, True)

probjax.utils.gammainccinv

gammainccinv(a, q)

Invert regularized upper gamma directly, preserving tiny q.

Finite a > 0 is required. Probabilities are clipped to [0, 1]; q=0 returns infinity and q=1 returns zero. Invalid shapes return NaN. Supports implicit forward and reverse differentiation.

Source code in probjax/utils/special/gammaincinv.py
@jax.jit
def gammainccinv(a, q):
    """Invert regularized upper gamma directly, preserving tiny q.

    Finite a > 0 is required. Probabilities are clipped to [0, 1]; q=0
    returns infinity and q=1 returns zero. Invalid shapes return NaN.
    Supports implicit forward and reverse differentiation.
    """
    return _inverse(a, q, True)

probjax.utils.log1mexp

log1mexp(x)

Compute log(1-exp(x)) for x <= 0; positive inputs return NaN.

Zero returns -inf and -inf returns zero. Inactive branch arguments are guarded to keep interior gradients finite near zero.

Source code in probjax/utils/special/logspace.py
@jax.jit
def log1mexp(x):
    """Compute log(1-exp(x)) for x <= 0; positive inputs return NaN.

    Zero returns -inf and -inf returns zero. Inactive branch arguments are
    guarded to keep interior gradients finite near zero.
    """
    x = jnp.asarray(x, dtype=jnp.result_type(x, 1.0))
    left = x < -jnp.log(2.0)
    return jnp.where(
        left,
        jnp.log1p(-jnp.exp(jnp.where(left, x, -1.0))),
        jnp.log(-jnp.expm1(jnp.where(left, -1.0, x))),
    )

probjax.utils.logdiffexp

logdiffexp(a, b)

Compute log(exp(a)-exp(b)), broadcasting a >= b.

Equal finite inputs and (-inf, -inf) return -inf. Invalid domains, including (+inf, +inf), return NaN. Gradients require finite a > b.

Source code in probjax/utils/special/logspace.py
@jax.jit
def logdiffexp(a, b):
    """Compute log(exp(a)-exp(b)), broadcasting a >= b.

    Equal finite inputs and (-inf, -inf) return -inf. Invalid domains,
    including (+inf, +inf), return NaN. Gradients require finite a > b.
    """
    dtype = jnp.result_type(a, b, 1.0)
    a, b = jnp.broadcast_arrays(jnp.asarray(a, dtype), jnp.asarray(b, dtype))
    both_zero = jnp.isneginf(a) & jnp.isneginf(b)
    difference = jnp.where(both_zero, -1.0, b - a)
    return jnp.where(both_zero, -jnp.inf, a + log1mexp(difference))

probjax.utils.digammainv

digammainv

digammainv(y, maxiter=5, tol=1e-14)

Inverse of the digamma function using Newton's method with asymptotic approximations.

Implementation follows the approach described in the literature: For Ψ(x) = y, use Newton's method with the update: x_new = x_old - (Ψ(x) - y) / Ψ'(x)

NOTE: Digamm is only invertible for x > 0. and this function assumes that y is in the domain of invertibility.

Parameters:

Name Type Description Default
y Union[float, ndarray]

The value to find the inverse digamma for

required
maxiter int

Maximum number of Newton iterations (5 iterations typically sufficient for 14 digits)

5
tol float

Tolerance for convergence

1e-14

Returns:

Type Description
Union[float, ndarray]

x such that digamma(x) = y

Source code in probjax/utils/special/digammainv.py
def digammainv(
    y: Union[float, jnp.ndarray], maxiter: int = 5, tol: float = 1e-14
) -> Union[float, jnp.ndarray]:
    """
    Inverse of the digamma function using Newton's method with asymptotic approximations.

    Implementation follows the approach described in the literature:
    For Ψ(x) = y, use Newton's method with the update:
    x_new = x_old - (Ψ(x) - y) / Ψ'(x)

    NOTE: Digamm is only invertible for x > 0. and this function assumes that y is in the domain of invertibility.

    Args:
        y: The value to find the inverse digamma for
        maxiter: Maximum number of Newton iterations (5 iterations typically sufficient for 14 digits)
        tol: Tolerance for convergence

    Returns:
        x such that digamma(x) = y
    """

    # Initial guess based on asymptotic formulas (eq. 149 in the paper)
    def initial_guess(y):
        gamma = jnp.euler_gamma  # Euler-Mascheroni constant

        # Use asymptotic approximations
        # For y ≥ -2.22: x ≈ exp(y) + 1/2
        # For y < -2.22: x ≈ -1/(y + γ)
        x_init = jnp.where(y >= -2.22, jnp.exp(y) + 0.5, -1.0 / (y + gamma))

        return x_init

    # Define Newton step function (eq. 146 in the paper)
    def newton_step(x_old):
        # Calculate Ψ(x) - y and Ψ'(x)
        psi_x = digamma(x_old)
        psi_prime_x = polygamma(1, x_old)  # First derivative of digamma

        # Newton update
        x_new = x_old - (psi_x - y) / psi_prime_x

        return x_new

    # Initialize with the asymptotic approximation
    x = initial_guess(y)

    # Run fixed number of Newton iterations using lax.fori_loop
    def body_fun(i, x):
        return newton_step(x)

    # The paper states 5 iterations are sufficient for 14 digits of precision
    x = lax.fori_loop(0, maxiter, body_fun, x)

    return jnp.nan_to_num(x, nan=0.0, posinf=0.0, neginf=0.0)

Interpolation and linear algebra

probjax.utils.linear_interpolation

linear_interpolation(ts, ys)

Linear interpolation function for a given set of points (ts, ys). Here ts must be a one dimensional sorted array and ys can be any array with the same length as ts on axis 0. Outside of the data range, the function returns the value of the nearest data point.

Parameters:

Name Type Description Default
ts Array

Time points

required
ys Array

Values at time points

required

Returns:

Type Description
Callable[[Float], Array]

Callable[[Float], Array]: Interpolation function that can be evaluated at any

Callable[[Float], Array]

time point.

Source code in probjax/utils/interpolation.py
def linear_interpolation(ts: Array, ys: Array) -> Callable[[Float], Array]:
    """Linear interpolation function for a given set of points (ts, ys). Here ts must be
    a one dimensional sorted array and ys can be any array with the same length as ts on
    axis 0. Outside of the data range, the function returns the value of the nearest
    data point.

    Args:
        ts (Array): Time points
        ys (Array): Values at time points

    Returns:
        Callable[[Float], Array]: Interpolation function that can be evaluated at any
        time point.
    """

    shape = ys.shape
    event_shape = ys.shape[1:]
    ys = ys.reshape(shape[0], -1)

    def interpolate(t: Float) -> Array:
        return jax.vmap(jnp.interp, in_axes=(None, None, -1))(t, ts, ys).reshape(
            event_shape
        )

    return interpolate

probjax.utils.polynomial_interpolation

polynomial_interpolation(ts, ys, degree=3, window=None)

Polynomial interpolation function for a given set of points (ts, ys). Here ts must be a one dimensional sorted array and ys can be any array with the same length as ts on axis 0.

The interpolation is done using a polynomial of degree 'degree'. The window parameter can be used to limit the range of data points used for interpolation. If window is None, the interpolation is done using all the data points.

Outside of the data range, the function does return the value of the

interpolant.

Parameters:

Name Type Description Default
ts Array

description

required
ys Array

description

required
degree Int

description. Defaults to 3.

3
window Optional[Int]

description. Defaults to None.

None

Returns:

Name Type Description
_type_ Callable[[Float], Array]

description

Source code in probjax/utils/interpolation.py
def polynomial_interpolation(
    ts: Array, ys: Array, degree: Int = 3, window: Int = None
) -> Callable[[Float], Array]:
    """Polynomial interpolation function for a given set of points (ts, ys). Here ts
    must be a one dimensional sorted array and ys can be any array with the same length
    as ts on axis 0.

    The interpolation is done using a polynomial of degree 'degree'. The window
    parameter can be used to limit the range of data points used for interpolation.
    If window is None, the interpolation is done using all the data points.

    Note: Outside of the data range, the function does return the value of the
        interpolant.

    Args:
        ts (Array): _description_
        ys (Array): _description_
        degree (Int, optional): _description_. Defaults to 3.
        window (Optional[Int], optional): _description_. Defaults to None.

    Returns:
        _type_: _description_
    """
    shape = ys.shape
    event_shape = ys.shape[1:]
    ys = ys.reshape(shape[0], -1)

    window = degree // 2 if window is None else window

    def interpolate(t: Float) -> Array:
        index = jnp.searchsorted(ts, t)
        index = lax.cond(index - window < 0, lambda: window, lambda: index)
        index = lax.cond(
            index + window > len(ts), lambda: len(ts) - window, lambda: index
        )
        indices = index + jnp.arange(-window, window + 1)
        data_x = ts[indices]
        data_y = ys[indices, :]
        p = jnp.polyfit(data_x, data_y, degree)
        return jnp.polyval(p, t).reshape(event_shape)

    return interpolate

probjax.utils.cholesky_update

cholesky_update(L, u)

Update the Cholesky decomposition of a matrix after a rank-1 update i.e.

C = L @ L.T + multiplier * u @ u.T

Args: L: A [D, D] lower triangular matrix, the Cholesky factor of the original matrix. u: A [D,] vector, the update vector.

Returns: The updated [D, D] lower triangular matrix.

Source code in probjax/utils/linalg.py
def cholesky_update(L, u):
    """
    Update the Cholesky decomposition of a matrix after a rank-1 update i.e.

    C = L @ L.T + multiplier * u @ u.T

    Args:
    L: A [D, D] lower triangular matrix, the Cholesky factor of the original matrix.
    u: A [D,] vector, the update vector.

    Returns:
    The updated [D, D] lower triangular matrix.
    """
    D = L.shape[0]
    indices = jnp.arange(D)

    def body_fun(i, vals):
        L, u = vals
        r = jnp.sqrt(L[i, i] ** 2 + u[i] ** 2)
        c = r / L[i, i]
        s = u[i] / L[i, i]
        L = L.at[i, i].set(r)

        mask = indices > i
        col_update = (L[:, i] + s * u) / c
        col_update = jnp.where(mask, col_update, L[:, i])
        L = L.at[:, i].set(col_update)
        u_update = c * u - s * L[:, i]
        u = jnp.where(mask, u_update, u)

        return (L, u)

    L, u = jax.lax.fori_loop(0, D, body_fun, (L, u))

    return L

probjax.utils.mv_diag_or_dense

mv_diag_or_dense(A_diag_or_dense, b, precission=DEFAULT)

Dot product of a diagonal matrix and a dense matrix

Parameters:

Name Type Description Default
A Array

Diagonal matrix

required
B Array

Dense matrix

required

Returns:

Name Type Description
Array Array

Dot product

Source code in probjax/utils/linalg.py
@partial(jax.jit, static_argnames=("precission",), inline=True)
def mv_diag_or_dense(
    A_diag_or_dense: Array, b: Array, precission=jax.lax.Precision.DEFAULT
) -> Array:
    """Dot product of a diagonal matrix and a dense matrix

    Args:
        A (Array): Diagonal matrix
        B (Array): Dense matrix

    Returns:
        Array: Dot product
    """
    A_diag_or_dense = jnp.asarray(A_diag_or_dense)
    dtype = jnp.result_type(A_diag_or_dense.dtype, b.dtype)
    A_diag_or_dense = A_diag_or_dense.astype(dtype)
    b = b.astype(dtype)
    ndim = A_diag_or_dense.ndim

    if ndim <= 1:
        return jax.lax.mul(A_diag_or_dense, b)
    else:
        return jax.lax.dot(
            A_diag_or_dense, b, precision=precission, preferred_element_type=dtype
        )

Linear operators

probjax.utils.linear_operator.Diagonal dataclass

Bases: LinearOperator

Elementwise x -> diag * x; a 0-d diag is diag * I.

Source code in probjax/utils/linear_operator.py
@jax.tree_util.register_dataclass
@dataclass(frozen=True)
class Diagonal(LinearOperator):
    """Elementwise ``x -> diag * x``; a 0-d ``diag`` is ``diag * I``."""

    diag: jax.Array
    in_shape: Shape = _static(default=None)

    def __post_init__(self):
        in_shape = jnp.shape(self.diag) if self.in_shape is None else self.in_shape
        object.__setattr__(self, "in_shape", _as_shape(in_shape))

    @property
    def out_shape(self) -> Shape:
        return self.in_shape

    @property
    def dtype(self):
        return jnp.result_type(self.diag)

    @property
    def T(self) -> "Diagonal":
        return self

    def _mv(self, x):
        return self.diag * x

    def _full_diag(self) -> jax.Array:
        return jnp.broadcast_to(self.diag, self.in_shape).ravel()

    def as_array(self):
        return jnp.diag(self._full_diag())

    def expm(self, t=1.0):
        return Diagonal(jnp.exp(t * self.diag), self.in_shape)

    def phi(self, t=1.0, k=1):
        z = t * jnp.asarray(self.diag)
        if k == 1:
            values = [jnp.exp(z), phi1_scalar(z)]
        else:
            # Higher phi_k by a tiny block expm per entry; the scalar
            # recursion phi_{k+1} = (phi_k - 1/k!) / z cancels badly near 0.
            per_entry = jax.vmap(
                lambda zi: jnp.stack(phi_matrices(zi.reshape(1, 1), k)).ravel()
            )
            values = [v.reshape(z.shape) for v in per_entry(z.ravel()).T]
        return [Diagonal(v, self.in_shape) for v in values]

probjax.utils.linear_operator.Dense dataclass

Bases: LinearOperator

A materialized (out_size, in_size) matrix acting on shaped arrays.

Source code in probjax/utils/linear_operator.py
@jax.tree_util.register_dataclass
@dataclass(frozen=True)
class Dense(LinearOperator):
    """A materialized ``(out_size, in_size)`` matrix acting on shaped arrays."""

    matrix: jax.Array
    in_shape: Shape = _static(default=None)
    out_shape: Shape = _static(default=None)

    def __post_init__(self):
        rows, cols = jnp.shape(self.matrix)
        in_shape = (cols,) if self.in_shape is None else self.in_shape
        out_shape = (rows,) if self.out_shape is None else self.out_shape
        object.__setattr__(self, "in_shape", _as_shape(in_shape))
        object.__setattr__(self, "out_shape", _as_shape(out_shape))

    @classmethod
    def jacfwd(cls, fn: Callable, x: ArrayLike) -> "Dense":
        """Materialized Jacobian of ``fn`` at ``x``."""
        x = jnp.asarray(x)
        jac = jax.jacfwd(fn)(x)
        out_shape = jac.shape[: jac.ndim - x.ndim]
        return cls(jac.reshape(math.prod(out_shape), x.size), x.shape, out_shape)

    @property
    def dtype(self):
        return self.matrix.dtype

    @property
    def T(self) -> "Dense":
        return Dense(self.matrix.T, self.out_shape, self.in_shape)

    def _mv(self, x):
        return (self.matrix @ x.ravel()).reshape(self.out_shape)

    def as_array(self):
        return self.matrix

    def expm(self, t=1.0):
        return Dense(
            jax.scipy.linalg.expm(t * self.matrix), self.in_shape, self.in_shape
        )

    def phi(self, t=1.0, k=1):
        return [
            Dense(m, self.in_shape, self.in_shape)
            for m in phi_matrices(t * self.matrix, k)
        ]

jacfwd classmethod

jacfwd(fn, x)

Materialized Jacobian of fn at x.

Source code in probjax/utils/linear_operator.py
@classmethod
def jacfwd(cls, fn: Callable, x: ArrayLike) -> "Dense":
    """Materialized Jacobian of ``fn`` at ``x``."""
    x = jnp.asarray(x)
    jac = jax.jacfwd(fn)(x)
    out_shape = jac.shape[: jac.ndim - x.ndim]
    return cls(jac.reshape(math.prod(out_shape), x.size), x.shape, out_shape)

probjax.utils.linear_operator.Function dataclass

Bases: LinearOperator

Matrix-free operator given by a linear matvec on in_shape arrays.

out_shape/dtype are inferred with jax.eval_shape (no FLOPs) when omitted. matvec is static pytree data, so build Function operators inside the traced function that uses them rather than passing them across jit/scan boundaries.

Source code in probjax/utils/linear_operator.py
@jax.tree_util.register_dataclass
@dataclass(frozen=True)
class Function(LinearOperator):
    """Matrix-free operator given by a linear ``matvec`` on ``in_shape`` arrays.

    ``out_shape``/``dtype`` are inferred with ``jax.eval_shape`` (no FLOPs)
    when omitted. ``matvec`` is static pytree data, so build Function
    operators inside the traced function that uses them rather than passing
    them across ``jit``/``scan`` boundaries.
    """

    matvec: Callable = _static()
    in_shape: Shape = _static()
    out_shape: Shape = _static(default=None)
    dtype: Any = _static(default=None)

    def __post_init__(self):
        in_shape = _as_shape(self.in_shape)
        object.__setattr__(self, "in_shape", in_shape)
        if self.out_shape is not None and self.dtype is not None:
            object.__setattr__(self, "out_shape", _as_shape(self.out_shape))
            return
        probe_dtype = jnp.result_type(float) if self.dtype is None else self.dtype
        out = jax.eval_shape(self.matvec, jax.ShapeDtypeStruct(in_shape, probe_dtype))
        if self.out_shape is not None and _as_shape(self.out_shape) != out.shape:
            raise ValueError(
                f"matvec returns shape {out.shape}, expected {_as_shape(self.out_shape)}"
            )
        object.__setattr__(self, "out_shape", out.shape)
        if self.dtype is None:
            object.__setattr__(self, "dtype", out.dtype)

    @property
    def T(self) -> "LinearOperator":
        return _Transpose(self)

    def _mv(self, x):
        return self.matvec(x)

probjax.utils.linear_operator.Jacobian dataclass

Bases: LinearOperator

Matrix-free Jacobian of fn(x, *args) with respect to x.

Each application costs one JVP. x and args are pytree leaves and fn is static, so rebuilding the Jacobian at a new point inside a jitted function does not retrace.

Source code in probjax/utils/linear_operator.py
@jax.tree_util.register_dataclass
@dataclass(frozen=True)
class Jacobian(LinearOperator):
    """Matrix-free Jacobian of ``fn(x, *args)`` with respect to ``x``.

    Each application costs one JVP. ``x`` and ``args`` are pytree leaves and
    ``fn`` is static, so rebuilding the Jacobian at a new point inside a
    jitted function does not retrace.
    """

    fn: Callable = _static()
    x: jax.Array
    args: tuple = ()
    out_shape: Shape = _static(default=None)
    dtype: Any = _static(default=None)

    def __post_init__(self):
        if self.out_shape is None or self.dtype is None:
            out = jax.eval_shape(self.fn, self.x, *self.args)
            object.__setattr__(self, "out_shape", out.shape)
            object.__setattr__(self, "dtype", out.dtype)

    @property
    def in_shape(self) -> Shape:
        return jnp.shape(self.x)

    @property
    def T(self) -> "LinearOperator":
        return _Transpose(self)

    def _mv(self, v):
        return jax.jvp(lambda x: self.fn(x, *self.args), (self.x,), (v,))[1]

probjax.utils.linear_operator.solve

solve(op, b, solver='auto', *, psd=False, tol=1e-05, maxiter=None)

Solve op @ x = b for x.

Parameters:

Name Type Description Default
op LinearOperator

Square operator (in_shape == out_shape).

required
b ArrayLike

Right-hand side of out_shape, or out_shape + (k,) for k right-hand sides.

required
solver Any

"auto", "lu", "cholesky", "cg", "gmres", "bicgstab" or any lineax solver instance. "auto" picks by structure: elementwise for Diagonal, Cholesky (psd) or LU for Dense, CG (psd) or GMRES for Function. "cholesky" and "cg" assume a symmetric positive-definite operator.

'auto'
psd bool

Whether op is symmetric positive-definite.

False
tol float

Relative tolerance of the iterative solvers.

1e-05
maxiter int | None

Iteration cap of the iterative solvers.

None

Returns:

Type Description
Array

Solution of in_shape (+ (k,)).

Source code in probjax/utils/linear_operator.py
def solve(
    op: LinearOperator,
    b: ArrayLike,
    solver: Any = "auto",
    *,
    psd: bool = False,
    tol: float = 1e-5,
    maxiter: int | None = None,
) -> jax.Array:
    """Solve ``op @ x = b`` for ``x``.

    Args:
        op: Square operator (``in_shape == out_shape``).
        b: Right-hand side of ``out_shape``, or ``out_shape + (k,)`` for k
            right-hand sides.
        solver: ``"auto"``, ``"lu"``, ``"cholesky"``, ``"cg"``, ``"gmres"``,
            ``"bicgstab"`` or any lineax solver instance. ``"auto"`` picks by
            structure: elementwise for Diagonal, Cholesky (``psd``) or LU for
            Dense, CG (``psd``) or GMRES for Function. ``"cholesky"`` and
            ``"cg"`` assume a symmetric positive-definite operator.
        psd: Whether ``op`` is symmetric positive-definite.
        tol: Relative tolerance of the iterative solvers.
        maxiter: Iteration cap of the iterative solvers.

    Returns:
        Solution of ``in_shape`` (``+ (k,)``).
    """
    _check_square(op)
    b = jnp.asarray(b)
    batch_shape = b.shape[len(op.out_shape) :]
    if b.shape[: len(op.out_shape)] != op.out_shape or len(batch_shape) > 1:
        raise ValueError(
            f"Expected rhs of shape {op.out_shape} (+ (k,)), got {b.shape}"
        )
    rhs = b.reshape(op.out_size, -1)

    if not isinstance(solver, str):
        lx = require_lineax()
        lx_op = to_lineax(op, psd)
        x = jax.vmap(
            lambda column: lx.linear_solve(lx_op, column, solver).value,
            in_axes=1,
            out_axes=1,
        )(rhs)
        return x.reshape(op.in_shape + batch_shape)

    if solver == "auto":
        if isinstance(op, Diagonal):
            solver = "diagonal"
        elif isinstance(op, Dense):
            solver = "cholesky" if psd else "lu"
        else:
            solver = "cg" if psd else "gmres"

    if solver == "diagonal":
        x = rhs / op._full_diag()[:, None]
    elif solver == "lu":
        x = jnp.linalg.solve(op.as_array(), rhs)
    elif solver == "cholesky":
        factor = jax.scipy.linalg.cho_factor(_symmetric_part(op.as_array()))
        x = jax.scipy.linalg.cho_solve(factor, rhs)
    elif solver in _ITERATIVE_SOLVERS:
        method = _ITERATIVE_SOLVERS[solver]

        def unit_scale_solve(matvec, column):
            # Krylov methods square residual norms, which over/underflows
            # float32 for extreme |b|; solve at unit scale instead.
            scale = jnp.max(jnp.abs(column))
            scale = jnp.where(scale > 0, scale, 1)
            return method(matvec, column / scale, tol=tol, maxiter=maxiter)[0] * scale

        def solve_column(column):
            # The rescaling is nonlinear in b; declaring the solve linear keeps
            # it transposable (e.g. inside lax.custom_root tangent solves).
            return jax.lax.custom_linear_solve(
                op._mv,
                column,
                unit_scale_solve,
                transpose_solve=unit_scale_solve,
                symmetric=solver == "cg",
            )

        # The jax.scipy solvers accept shaped vectors, so no flattening is needed.
        columns = rhs.reshape(op.out_shape + (-1,))
        x = jax.vmap(solve_column, in_axes=-1, out_axes=-1)(columns)
    else:
        raise ValueError(f"Unknown solver {solver!r}")
    return x.reshape(op.in_shape + batch_shape)

probjax.utils.linear_operator.logdet

logdet(op, *, psd=False, **lanczos_kwargs)

log |det op| of a square operator.

Function operators need psd=True and use Lanczos quadrature (:func:probjax.utils.linalg.lanczos_logdet, which receives lanczos_kwargs); call as_dense() first for an exact result.

Source code in probjax/utils/linear_operator.py
def logdet(op: LinearOperator, *, psd: bool = False, **lanczos_kwargs) -> jax.Array:
    """``log |det op|`` of a square operator.

    Function operators need ``psd=True`` and use Lanczos quadrature
    (:func:`probjax.utils.linalg.lanczos_logdet`, which receives
    ``lanczos_kwargs``); call ``as_dense()`` first for an exact result.
    """
    _check_square(op)
    if isinstance(op, Diagonal):
        return jnp.sum(jnp.log(jnp.abs(op._full_diag())))
    if isinstance(op, Dense):
        if psd:
            factor = jnp.linalg.cholesky(_symmetric_part(op.matrix))
            return 2 * jnp.sum(jnp.log(jnp.diagonal(factor)))
        return jnp.linalg.slogdet(op.matrix).logabsdet
    if not psd:
        raise ValueError(
            "Matrix-free logdet requires psd=True; call as_dense() otherwise"
        )
    flat = Function(op._flat_mv, (op.in_size,), (op.out_size,), op.dtype)
    return lanczos_logdet(flat, **lanczos_kwargs)

probjax.utils.linear_operator.as_operator

as_operator(A, shape)

Interpret A as a square operator on arrays of shape.

Operators pass through (their domain must equal shape); a 0-d array or 1-d array broadcastable to shape is a diagonal, and a square 2-d array a dense matrix on vector states of matching dimension. Anything else raises a clear error: the exact solvers need a flattened square system, so dense drifts support vector states only (evaluate the drift itself for matmul-broadcast behavior on shaped states).

Source code in probjax/utils/linear_operator.py
def as_operator(A: LinearOperator | ArrayLike, shape: Shape) -> LinearOperator:
    """Interpret ``A`` as a square operator on arrays of ``shape``.

    Operators pass through (their domain must equal ``shape``); a 0-d array
    or 1-d array broadcastable to ``shape`` is a diagonal, and a square 2-d
    array a dense matrix on vector states of matching dimension. Anything
    else raises a clear error: the exact solvers need a flattened square
    system, so dense drifts support vector states only (evaluate the drift
    itself for matmul-broadcast behavior on shaped states).
    """
    shape = _as_shape(shape)
    if isinstance(A, LinearOperator):
        if tuple(A.in_shape) != tuple(shape) or tuple(A.out_shape) != tuple(shape):
            raise ValueError(
                f"Operator with domain {A.in_shape} -> {A.out_shape} cannot act"
                f" on states of shape {shape}."
            )
        return A
    A = jnp.asarray(A)
    if A.ndim <= 1:
        try:
            jnp.broadcast_shapes(A.shape, shape)
        except ValueError:
            raise ValueError(
                f"Diagonal of shape {A.shape} cannot broadcast to state"
                f" shape {shape}."
            ) from None
        return Diagonal(A, shape)
    if A.ndim == 2:
        if A.shape[0] != A.shape[1]:
            raise ValueError(f"Dense drift must be square, got shape {A.shape}.")
        if tuple(shape) != (A.shape[0],):
            raise ValueError(
                f"Dense drift of shape {A.shape} needs vector states of shape"
                f" ({A.shape[0]},), got {shape}."
            )
        return Dense(A, shape, shape)
    raise ValueError(f"Expected a 0-d, 1-d or 2-d array, got shape {A.shape}")

probjax.utils.linear_operator.phi_matrices

phi_matrices(z, k)

[phi_0(Z), ..., phi_k(Z)] from one block matrix exponential.

expm of the (k+1)-block-bidiagonal augmentation (Z in the first block, identities on the first superdiagonal) has phi_j(Z) in its j-th top-row block.

Source code in probjax/utils/linear_operator.py
def phi_matrices(z: jax.Array, k: int) -> list[jax.Array]:
    """``[phi_0(Z), ..., phi_k(Z)]`` from one block matrix exponential.

    expm of the ``(k+1)``-block-bidiagonal augmentation (``Z`` in the first
    block, identities on the first superdiagonal) has ``phi_j(Z)`` in its
    j-th top-row block.
    """
    n = z.shape[0]
    aug = jnp.zeros((n * (k + 1), n * (k + 1)), dtype=z.dtype)
    aug = aug.at[:n, :n].set(z)
    aug = aug.at[: n * k, n:].add(jnp.eye(n * k, dtype=z.dtype))
    top = jax.scipy.linalg.expm(aug)[:n]
    return [top[:, j * n : (j + 1) * n] for j in range(k + 1)]

ODE integration internals

probjax.utils.odeutil.ODEInfo module-attribute

ODEInfo = NamedTuple

probjax.utils.odeutil.ODESolver

Bases: NamedTuple

This is a NamedTuple that represents a ODE kernel. given by the drift.

Source code in probjax/utils/odeutil/solvers/base.py
class ODESolver(NamedTuple):
    """This is a NamedTuple that represents a ODE kernel.
    given by the drift.
    """

    init: Callable
    step: Callable

    def __call__(self, state: ODEState, dt: ArrayLike) -> tuple[ODEState, ODEInfo]:
        return self.step(state, dt)

probjax.utils.odeutil.ODEState module-attribute

ODEState = NamedTuple

probjax.utils.odeutil.get_method

get_method(solver)

(solver_factory, info) for a name, solver class or configured solver.

Source code in probjax/utils/odeutil/solvers/base.py
def get_method(solver) -> tuple[Any, dict]:
    """``(solver_factory, info)`` for a name, solver class or configured solver."""
    return resolve_solver(solver, METHOD_STEP_FN, METHOD_INFO)

probjax.utils.odeutil.get_methods

get_methods()
Source code in probjax/utils/odeutil/solvers/base.py
def get_methods():
    return list(METHOD_STEP_FN.keys())

probjax.utils.odeutil.StepSizeAdaptor

Standardized step-size controller for adaptive ODE solvers.

Constructor arguments are the classic adaptive-RK tuning knobs:

Parameters:

Name Type Description Default
rtol float

Relative tolerance (per-component).

0.0001
atol float

Absolute tolerance (per-component).

1e-05
mxstep int

Hard cap on inner adaptive iterations between two output grid points. Defaults to jnp.inf (no cap).

inf
dtmin float

Minimum admissible step size; the controller clips below this and accepts the step regardless of error.

0.0
dtmax float

Maximum admissible step size.

inf
maxerror float

Accept-step threshold on the normalized error ratio.

1.2
safety, ifactor, dfactor

Hairer–Wanner safety factor and grow/shrink limits applied around the optimal step.

required
error_norm float

Order of the error norm (2 = RMS).

2
order int

Local-error order. Set automatically by the integrator to match the chosen method, so there is rarely a reason to set this at construction time.

5

Subclass to customize the strategy:

>>> class HairerOnly(StepSizeAdaptor):
...     def accept_step(self, error_ratio, dt):
...         return error_ratio <= self.maxerror
Note

This object is only consulted by adaptive solver methods ("dopri5", "dopri8", "bosh3", etc.). Fixed-step methods ("euler", "rk4", "midpoint", ...) ignore it entirely.

Source code in probjax/utils/odeutil/adaptive.py
class StepSizeAdaptor:
    """Standardized step-size controller for adaptive ODE solvers.

    Constructor arguments are the classic adaptive-RK tuning knobs:

    Args:
        rtol: Relative tolerance (per-component).
        atol: Absolute tolerance (per-component).
        mxstep: Hard cap on inner adaptive iterations between two output
            grid points. Defaults to ``jnp.inf`` (no cap).
        dtmin: Minimum admissible step size; the controller clips below
            this and accepts the step regardless of error.
        dtmax: Maximum admissible step size.
        maxerror: Accept-step threshold on the normalized error ratio.
        safety, ifactor, dfactor: Hairer–Wanner safety factor and
            grow/shrink limits applied around the optimal step.
        error_norm: Order of the error norm (``2`` = RMS).
        order: Local-error order. Set automatically by the integrator to
            match the chosen method, so there is rarely a reason to set
            this at construction time.

    Subclass to customize the strategy:

        >>> class HairerOnly(StepSizeAdaptor):
        ...     def accept_step(self, error_ratio, dt):
        ...         return error_ratio <= self.maxerror

    Note:
        This object is only consulted by **adaptive** solver methods
        (``"dopri5"``, ``"dopri8"``, ``"bosh3"``, etc.). Fixed-step methods
        (``"euler"``, ``"rk4"``, ``"midpoint"``, ...) ignore it entirely.
    """

    def __init__(
        self,
        rtol: float = 1e-4,
        atol: float = 1e-5,
        mxstep: int = jnp.inf,
        dtmin: float = 0.0,
        dtmax: float = jnp.inf,
        maxerror: float = 1.2,
        safety: float = 0.95,
        ifactor: float = 10.0,
        dfactor: float = 0.1,
        error_norm: float = 2,
        order: int = 5,
    ) -> None:
        self.rtol = rtol
        self.atol = atol
        self.mxstep = mxstep
        self.dtmin = dtmin
        self.dtmax = dtmax
        self.maxerror = maxerror
        self.safety = safety
        self.ifactor = ifactor
        self.dfactor = dfactor
        self.error_norm = error_norm
        self.order = order

    def with_order(self, order: int) -> "StepSizeAdaptor":
        """Return a shallow copy with the local-error ``order`` set.

        The adaptive integrator calls this once after resolving the method's
        local order, so subclasses do not need to track ``order`` themselves.
        """
        new = copy.copy(self)
        new.order = order
        return new

    def initial_step_size(self, drift, args, t0, y0, f0):
        """Pick the initial step ``dt0`` from drift / state magnitudes."""
        return jnp.clip(
            _initial_step_size(
                drift, args, t0, y0, f0, self.order, self.rtol, self.atol
            ),
            min=0.0,
            max=jnp.inf,
        )

    def error_ratio(self, error, y, y_next):
        """Compute ``||error / (atol + rtol·max(|y|,|y_next|))||_p``."""
        return mean_error_ratio(error, y, y_next, self.rtol, self.atol, self.error_norm)

    def next_step_size(self, dt, error_ratio):
        """Hairer–Wanner optimal step, clipped to ``[dtmin, dtmax]``."""
        return jnp.clip(
            optimal_step_size(
                dt,
                error_ratio,
                maxerror=self.maxerror,
                safety=self.safety,
                ifactor=self.ifactor,
                dfactor=self.dfactor,
                order=self.order,
            ),
            self.dtmin,
            self.dtmax,
        )

    def accept_step(self, error_ratio, dt):
        """Accept iff the error is below ``maxerror`` or ``dt`` hit a clip."""
        return (error_ratio <= self.maxerror) | (dt == self.dtmin) | (dt == self.dtmax)

with_order

with_order(order)

Return a shallow copy with the local-error order set.

The adaptive integrator calls this once after resolving the method's local order, so subclasses do not need to track order themselves.

Source code in probjax/utils/odeutil/adaptive.py
def with_order(self, order: int) -> "StepSizeAdaptor":
    """Return a shallow copy with the local-error ``order`` set.

    The adaptive integrator calls this once after resolving the method's
    local order, so subclasses do not need to track ``order`` themselves.
    """
    new = copy.copy(self)
    new.order = order
    return new

initial_step_size

initial_step_size(drift, args, t0, y0, f0)

Pick the initial step dt0 from drift / state magnitudes.

Source code in probjax/utils/odeutil/adaptive.py
def initial_step_size(self, drift, args, t0, y0, f0):
    """Pick the initial step ``dt0`` from drift / state magnitudes."""
    return jnp.clip(
        _initial_step_size(
            drift, args, t0, y0, f0, self.order, self.rtol, self.atol
        ),
        min=0.0,
        max=jnp.inf,
    )

error_ratio

error_ratio(error, y, y_next)

Compute ||error / (atol + rtol·max(|y|,|y_next|))||_p.

Source code in probjax/utils/odeutil/adaptive.py
def error_ratio(self, error, y, y_next):
    """Compute ``||error / (atol + rtol·max(|y|,|y_next|))||_p``."""
    return mean_error_ratio(error, y, y_next, self.rtol, self.atol, self.error_norm)

next_step_size

next_step_size(dt, error_ratio)

Hairer–Wanner optimal step, clipped to [dtmin, dtmax].

Source code in probjax/utils/odeutil/adaptive.py
def next_step_size(self, dt, error_ratio):
    """Hairer–Wanner optimal step, clipped to ``[dtmin, dtmax]``."""
    return jnp.clip(
        optimal_step_size(
            dt,
            error_ratio,
            maxerror=self.maxerror,
            safety=self.safety,
            ifactor=self.ifactor,
            dfactor=self.dfactor,
            order=self.order,
        ),
        self.dtmin,
        self.dtmax,
    )

accept_step

accept_step(error_ratio, dt)

Accept iff the error is below maxerror or dt hit a clip.

Source code in probjax/utils/odeutil/adaptive.py
def accept_step(self, error_ratio, dt):
    """Accept iff the error is below ``maxerror`` or ``dt`` hit a clip."""
    return (error_ratio <= self.maxerror) | (dt == self.dtmin) | (dt == self.dtmax)

probjax.utils.odeutil.euler

Bases: ODESolverAPI

Source code in probjax/utils/odeutil/solvers/rk_explicit.py
class euler(ODESolverAPI):
    init = init_euler
    build_step = build_euler_step

probjax.utils.odeutil.implicit_euler

Bases: ODESolverAPI

Source code in probjax/utils/odeutil/solvers/rk_implicit.py
class implicit_euler(ODESolverAPI):
    init = init_implicit_euler
    build_step = build_implicit_euler_step

probjax.utils.odeutil.TraceFilter

Bases: Protocol

Protocol for specifying which parts of the state to trace.

A filter receives a PyTree with the same structure as the solver state and should return either a PyTree selecting the components that should be recorded over time or None to disable tracing entirely.

Source code in probjax/utils/odeutil/filters.py
@runtime_checkable
class TraceFilter(Protocol):
    """Protocol for specifying which parts of the state to trace.

    A filter receives a PyTree with the same structure as the solver state and
    should return either a PyTree selecting the components that should be
    recorded over time or ``None`` to disable tracing entirely.
    """

    def __call__(self, state: PyTree[Array]) -> Optional[PyTree[Array]]: ...

probjax.utils.odeutil.TraceEverything

Filter that records the entire solver state.

Source code in probjax/utils/odeutil/filters.py
class TraceEverything:
    """Filter that records the entire solver state."""

    def __call__(self, state: PyTree[Array]) -> PyTree[Array]:
        return state

probjax.utils.odeutil.TraceNothing

Filter that disables tracing to minimize memory usage.

Source code in probjax/utils/odeutil/filters.py
class TraceNothing:
    """Filter that disables tracing to minimize memory usage."""

    def __call__(self, state: PyTree[Array]) -> None:  # type: ignore[override]
        return None

SDE integration internals

probjax.utils.sdeutil.SDEStepSizeAdaptor

Bases: StepSizeAdaptor

Base class for SDE step-size controllers.

Extends the ODE :class:StepSizeAdaptor with a noise-sampling protocol:

  • :meth:init_brownian constructs the per-trajectory sampling state.
  • :meth:propose_increments produces the increment for a proposed dt plus the bridge-consistent halves used by step doubling.
  • :meth:on_reject updates the sampling state when a step is rejected before the next attempt.

Subclasses set :attr:is_path_consistent so downstream code can decide whether per-path quantities are meaningful.

Source code in probjax/utils/sdeutil/adaptive.py
class SDEStepSizeAdaptor(StepSizeAdaptor):
    """Base class for SDE step-size controllers.

    Extends the ODE :class:`StepSizeAdaptor` with a noise-sampling protocol:

    - :meth:`init_brownian` constructs the per-trajectory sampling state.
    - :meth:`propose_increments` produces the increment for a proposed
      ``dt`` plus the bridge-consistent halves used by step doubling.
    - :meth:`on_reject` updates the sampling state when a step is rejected
      before the next attempt.

    Subclasses set :attr:`is_path_consistent` so downstream code can decide
    whether per-path quantities are meaningful.
    """

    is_path_consistent: bool = False

    def __init__(
        self,
        *,
        max_inner_steps: int = 64,
        unroll: int = 2,
        warn_on_boundary: bool = True,
        **kwargs,
    ):
        """
        Args:
            max_inner_steps: Static cap on inner adaptive iterations between
                two output points. Drives the bounded :func:`jax.lax.scan`
                length so the loop is reverse-mode differentiable. The
                scan **always** runs this many iterations (no-op past
                completion), so larger values cost proportionally more
                wall-clock. Default ``64`` is enough for typical
                ``rtol/atol ≈ 1e-2``; raise it for tight tolerances or
                stiff problems and a :class:`RuntimeWarning` will fire
                when it's the wrong call.
            unroll: Static unroll factor for the inner scan. Higher values
                let XLA fuse more work per loop iteration at the cost of
                larger compiled code; default ``2`` is a small sweet spot.
                Try ``4`` or ``8`` if the inner step is small relative to
                scheduling overhead, or drop to ``1`` to minimize compile
                time.
            warn_on_boundary: If ``True`` (default), emit a host-side
                ``RuntimeWarning`` via ``jax.debug.callback`` whenever
                ``max_inner_steps`` is exhausted before reaching the next
                output time — that's a sign the budget is too small or
                tolerances are too tight.
        """
        super().__init__(**kwargs)
        self.max_inner_steps = int(max_inner_steps)
        self.unroll = int(unroll)
        self.warn_on_boundary = bool(warn_on_boundary)

    def bind_interval(self, t0, t_end) -> "SDEStepSizeAdaptor":
        """Return the adaptor to use for integrating over ``[t0, t_end]``."""
        del t0, t_end
        return self

    def accept_step(self, error_ratio, dt):
        """Always accept: the error only steers the next step (see module docs)."""
        del dt
        return jnp.ones_like(error_ratio, dtype=bool)

    def init_brownian(self, rng, t0, t_end, noise_shape: Tuple[int, ...]):
        raise NotImplementedError

    def propose_increments(self, state, t_curr, dt, noise_shape: Tuple[int, ...]):
        """Return ``(committed_state, dW_full, dW_first_half, dW_second_half)``.

        The two halves satisfy ``dW_first_half + dW_second_half == dW_full``
        within the proposed step (Brownian-bridge consistent), enabling an
        unbiased step-doubling local error estimate.

        ``committed_state`` is the new sampling state to keep on accept; the
        integrator passes it through :meth:`on_reject` instead on rejection.
        """
        raise NotImplementedError

    def on_reject(self, state):
        """Sampling state to use for the next retry after a rejected step."""
        return state

bind_interval

bind_interval(t0, t_end)

Return the adaptor to use for integrating over [t0, t_end].

Source code in probjax/utils/sdeutil/adaptive.py
def bind_interval(self, t0, t_end) -> "SDEStepSizeAdaptor":
    """Return the adaptor to use for integrating over ``[t0, t_end]``."""
    del t0, t_end
    return self

accept_step

accept_step(error_ratio, dt)

Always accept: the error only steers the next step (see module docs).

Source code in probjax/utils/sdeutil/adaptive.py
def accept_step(self, error_ratio, dt):
    """Always accept: the error only steers the next step (see module docs)."""
    del dt
    return jnp.ones_like(error_ratio, dtype=bool)

propose_increments

propose_increments(state, t_curr, dt, noise_shape)

Return (committed_state, dW_full, dW_first_half, dW_second_half).

The two halves satisfy dW_first_half + dW_second_half == dW_full within the proposed step (Brownian-bridge consistent), enabling an unbiased step-doubling local error estimate.

committed_state is the new sampling state to keep on accept; the integrator passes it through :meth:on_reject instead on rejection.

Source code in probjax/utils/sdeutil/adaptive.py
def propose_increments(self, state, t_curr, dt, noise_shape: Tuple[int, ...]):
    """Return ``(committed_state, dW_full, dW_first_half, dW_second_half)``.

    The two halves satisfy ``dW_first_half + dW_second_half == dW_full``
    within the proposed step (Brownian-bridge consistent), enabling an
    unbiased step-doubling local error estimate.

    ``committed_state`` is the new sampling state to keep on accept; the
    integrator passes it through :meth:`on_reject` instead on rejection.
    """
    raise NotImplementedError

on_reject

on_reject(state)

Sampling state to use for the next retry after a rejected step.

Source code in probjax/utils/sdeutil/adaptive.py
def on_reject(self, state):
    """Sampling state to use for the next retry after a rejected step."""
    return state

probjax.utils.sdeutil.StrongStepSizeAdaptor

Bases: SDEStepSizeAdaptor

Path-consistent sampling via a virtual Brownian tree.

All increments come from a single keyed path, so the integrator's output is a sample from one consistent W. Use this whenever per-path quantities matter (state estimation, log-likelihood under a given path, gradient methods that assume realized-path consistency).

Parameters:

Name Type Description Default
tree_depth int

Static refinement depth for the underlying virtual Brownian tree. Per-query cost is O(tree_depth). Default 20 ≈ 1e-6 leaf width on a unit interval. Inside a leaf the path is interpolated smoothly and carries no quadratic variation, so the controller step is kept at least MIN_STEP_IN_LEAVES leaf widths: below that the integrator would see too little noise and lose the Ito correction. Raise it if the tolerances need smaller steps (the time dtype's resolution is the practical limit).

20
tree_tol Optional[float]

Backward-compatibility alias; converted to ceil(-log2(tree_tol)) at construction time. Mutually exclusive with tree_depth.

None
Source code in probjax/utils/sdeutil/adaptive.py
class StrongStepSizeAdaptor(SDEStepSizeAdaptor):
    """Path-consistent sampling via a virtual Brownian tree.

    All increments come from a single keyed path, so the integrator's output
    is a sample from one consistent ``W``. Use this whenever per-path quantities matter (state
    estimation, log-likelihood under a given path, gradient methods that
    assume realized-path consistency).

    Args:
        tree_depth: Static refinement depth for the underlying virtual
            Brownian tree. Per-query cost is ``O(tree_depth)``. Default
            ``20`` ≈ 1e-6 leaf width on a unit interval. Inside a leaf the
            path is interpolated smoothly and carries no quadratic variation,
            so the controller step is kept at least ``MIN_STEP_IN_LEAVES``
            leaf widths: below that the integrator would see too little noise
            and lose the Ito correction. Raise it if the tolerances need
            smaller steps (the time dtype's resolution is the practical
            limit).
        tree_tol: Backward-compatibility alias; converted to
            ``ceil(-log2(tree_tol))`` at construction time. Mutually
            exclusive with ``tree_depth``.
    """

    is_path_consistent = True
    MIN_STEP_IN_LEAVES = 64

    def __init__(
        self,
        *,
        tree_depth: int = 20,
        tree_tol: Optional[float] = None,
        **kwargs,
    ):
        super().__init__(**kwargs)
        if tree_tol is not None:
            tree_depth = max(1, int(jnp.ceil(-jnp.log2(jnp.asarray(tree_tol)))))
        self.tree_depth = int(tree_depth)

    def bind_interval(self, t0, t_end) -> "StrongStepSizeAdaptor":
        leaf_width = (t_end - t0) * 2.0 ** (-self.tree_depth)
        bound = copy.copy(self)
        bound.dtmin = jnp.maximum(self.dtmin, self.MIN_STEP_IN_LEAVES * leaf_width)
        return bound

    def init_brownian(
        self, rng, t0, t_end, noise_shape: Tuple[int, ...]
    ) -> StrongBrownianState:
        zeros = jnp.zeros(noise_shape)
        return StrongBrownianState(
            key=rng,
            t0=jnp.asarray(t0),
            t_end=jnp.asarray(t_end),
            w0=zeros,
            t_curr=jnp.asarray(t0),
            w_curr=zeros,
        )

    def propose_increments(
        self,
        state: StrongBrownianState,
        t_curr,
        dt,
        noise_shape: Tuple[int, ...],
    ):
        del noise_shape  # tree query infers it from ``state.w0``
        t_half = t_curr + 0.5 * dt
        t_full = t_curr + dt
        w_half = brownian_tree(
            state.key,
            t_half,
            state.t0,
            state.t_end,
            state.w0,
            depth=self.tree_depth,
        )
        w_full = brownian_tree(
            state.key,
            t_full,
            state.t0,
            state.t_end,
            state.w0,
            depth=self.tree_depth,
        )
        dW_full = w_full - state.w_curr
        dW1 = w_half - state.w_curr
        dW2 = w_full - w_half
        committed = state._replace(t_curr=t_full, w_curr=w_full)
        return committed, dW_full, dW1, dW2

probjax.utils.sdeutil.WeakStepSizeAdaptor

Bases: SDEStepSizeAdaptor

Fresh PRNG increments — valid for distributional quantities only.

The trajectory is not a sample from a single Brownian path. Suitable for diffusion-model sampling or any setting where you only care about moments / densities of the terminal state. Use :class:StrongStepSizeAdaptor for per-path quantities.

Source code in probjax/utils/sdeutil/adaptive.py
class WeakStepSizeAdaptor(SDEStepSizeAdaptor):
    """Fresh PRNG increments — valid for distributional quantities only.

    The trajectory is *not* a sample from a single Brownian path. Suitable for
    diffusion-model sampling or any setting where you only care about moments /
    densities of the terminal state. Use :class:`StrongStepSizeAdaptor` for
    per-path quantities.
    """

    is_path_consistent = False

    def init_brownian(
        self, rng, t0, t_end, noise_shape: Tuple[int, ...]
    ) -> WeakBrownianState:
        del t0, t_end, noise_shape
        return WeakBrownianState(key=rng)

    def propose_increments(
        self,
        state: WeakBrownianState,
        t_curr,
        dt,
        noise_shape: Tuple[int, ...],
    ):
        k_full, k_bridge, k_next = jax.random.split(state.key, 3)
        sqrt_dt = jnp.sqrt(jnp.abs(dt))
        dW_full = jax.random.normal(k_full, noise_shape) * sqrt_dt
        # Brownian bridge midpoint of *this* step:
        # dW_first_half = 0.5 dW_full + 0.5 sqrt(dt) Z, var = dt/2 ✓
        z = jax.random.normal(k_bridge, noise_shape)
        dW1 = 0.5 * dW_full + 0.5 * sqrt_dt * z
        dW2 = dW_full - dW1
        committed = WeakBrownianState(key=k_next)
        return committed, dW_full, dW1, dW2

probjax.utils.sdeutil.SDEInfo module-attribute

SDEInfo = NamedTuple

probjax.utils.sdeutil.SDESolver

Bases: NamedTuple

This is a NamedTuple that represents a ODE kernel. given by the drift.

Source code in probjax/utils/sdeutil/base.py
class SDESolver(NamedTuple):
    """This is a NamedTuple that represents a ODE kernel.
    given by the drift.
    """

    init: Callable
    step: Callable

    def __call__(self, key, state: SDEState, dt: ArrayLike) -> Tuple[SDEState, SDEInfo]:
        return self.step(key, state, dt)

probjax.utils.sdeutil.SDEState module-attribute

SDEState = NamedTuple

probjax.utils.sdeutil.get_method

get_method(solver)

(solver_factory, info) for a name, solver class or configured solver.

Source code in probjax/utils/sdeutil/base.py
def get_method(solver):
    """``(solver_factory, info)`` for a name, solver class or configured solver."""
    return resolve_solver(solver, METHOD_STEP_FN, METHOD_INFO)

probjax.utils.sdeutil.get_methods

get_methods()
Source code in probjax/utils/sdeutil/base.py
def get_methods():
    return list(METHOD_STEP_FN.keys())

probjax.utils.sdeutil.euler_maruyama

Bases: SDESolverAPI

Source code in probjax/utils/sdeutil/solver/em.py
class euler_maruyama(SDESolverAPI):
    init = init_state
    build_step = build_em_step

probjax.utils.sdeutil.exp_euler_maruyama

Bases: SDESolverAPI

Source code in probjax/utils/sdeutil/solver/exponential.py
class exp_euler_maruyama(SDESolverAPI):
    init = staticmethod(init_state)
    build_step = staticmethod(build_exp_euler_maruyama_step)

probjax.utils.sdeutil.milstein

Bases: SDESolverAPI

Source code in probjax/utils/sdeutil/solver/milstein.py
class milstein(SDESolverAPI):
    init = init_state
    build_step = build_milstein_step

probjax.utils.sdeutil.sri1

Bases: SDESolverAPI

SRI1 method - Strong order 1.0 stochastic Runge-Kutta method.

Source code in probjax/utils/sdeutil/solver/srk_explicit.py
class sri1(SDESolverAPI):
    """SRI1 method - Strong order 1.0 stochastic Runge-Kutta method."""

    init = init_state
    build_step = partial(build_srk_step, build_coefficients=build_sri1_coefficients)

probjax.utils.sdeutil.sri2

Bases: SDESolverAPI

SRI2 method - Strong order 1.0 stochastic Runge-Kutta method.

Source code in probjax/utils/sdeutil/solver/srk_explicit.py
class sri2(SDESolverAPI):
    """SRI2 method - Strong order 1.0 stochastic Runge-Kutta method."""

    init = init_state
    build_step = partial(build_srk_step, build_coefficients=build_sri2_coefficients)

probjax.utils.sdeutil.linear_exact_sde

Bases: SDESolverAPI

Source code in probjax/utils/sdeutil/solver/linear_exact.py
class linear_exact_sde(SDESolverAPI):
    init = staticmethod(init_state)
    build_step = staticmethod(build_linear_exact_sde_step)

Special functions (subpackage)

probjax.utils.special.betaincinv

Regularized beta inverses with cached solvers and implicit reverse AD.

betaincinv

betaincinv(a, b, p, *, max_halley_steps=6, max_bisection_steps=15)

Invert regularized lower beta for finite positive shapes.

Probabilities are clipped to [0, 1]; invalid shapes return NaN. The total safeguarded Halley/bisection budget is 2 plus both step settings. Reverse AD uses the inverse density and central differences for shapes; forward AD and higher shape derivatives are not supported.

Source code in probjax/utils/special/betaincinv.py
def betaincinv(a, b, p, *, max_halley_steps=6, max_bisection_steps=15):
    """Invert regularized lower beta for finite positive shapes.

    Probabilities are clipped to [0, 1]; invalid shapes return NaN. The total
    safeguarded Halley/bisection budget is 2 plus both step settings.
    Reverse AD uses the inverse density and central differences for shapes;
    forward AD and higher shape derivatives are not supported.
    """
    return _inverse(a, b, p, max_halley_steps, max_bisection_steps, False)

betainccinv

betainccinv(a, b, q, *, max_halley_steps=6, max_bisection_steps=15)

Invert regularized upper beta directly, preserving tiny q.

q=0 returns one; q=1 returns zero. Domain, step settings and differentiation have the same semantics as betaincinv.

Source code in probjax/utils/special/betaincinv.py
def betainccinv(a, b, q, *, max_halley_steps=6, max_bisection_steps=15):
    """Invert regularized upper beta directly, preserving tiny q.

    q=0 returns one; q=1 returns zero. Domain, step settings and differentiation
    have the same semantics as betaincinv.
    """
    return _inverse(a, b, q, max_halley_steps, max_bisection_steps, True)

probjax.utils.special.betainccinv

betainccinv(a, b, q, *, max_halley_steps=6, max_bisection_steps=15)

Invert regularized upper beta directly, preserving tiny q.

q=0 returns one; q=1 returns zero. Domain, step settings and differentiation have the same semantics as betaincinv.

Source code in probjax/utils/special/betaincinv.py
def betainccinv(a, b, q, *, max_halley_steps=6, max_bisection_steps=15):
    """Invert regularized upper beta directly, preserving tiny q.

    q=0 returns one; q=1 returns zero. Domain, step settings and differentiation
    have the same semantics as betaincinv.
    """
    return _inverse(a, b, q, max_halley_steps, max_bisection_steps, True)

probjax.utils.special.gammaincinv

Regularized gamma inverses with implicit differentiation.

gammaincinv

gammaincinv(a, p)

Invert regularized lower gamma; finite a > 0, p clipped to [0, 1].

Source code in probjax/utils/special/gammaincinv.py
@jax.jit
def gammaincinv(a, p):
    """Invert regularized lower gamma; finite a > 0, p clipped to [0, 1]."""
    return _inverse(a, p, False)

gammainccinv

gammainccinv(a, q)

Invert regularized upper gamma directly, preserving tiny q.

Finite a > 0 is required. Probabilities are clipped to [0, 1]; q=0 returns infinity and q=1 returns zero. Invalid shapes return NaN. Supports implicit forward and reverse differentiation.

Source code in probjax/utils/special/gammaincinv.py
@jax.jit
def gammainccinv(a, q):
    """Invert regularized upper gamma directly, preserving tiny q.

    Finite a > 0 is required. Probabilities are clipped to [0, 1]; q=0
    returns infinity and q=1 returns zero. Invalid shapes return NaN.
    Supports implicit forward and reverse differentiation.
    """
    return _inverse(a, q, True)

probjax.utils.special.gammainccinv

gammainccinv(a, q)

Invert regularized upper gamma directly, preserving tiny q.

Finite a > 0 is required. Probabilities are clipped to [0, 1]; q=0 returns infinity and q=1 returns zero. Invalid shapes return NaN. Supports implicit forward and reverse differentiation.

Source code in probjax/utils/special/gammaincinv.py
@jax.jit
def gammainccinv(a, q):
    """Invert regularized upper gamma directly, preserving tiny q.

    Finite a > 0 is required. Probabilities are clipped to [0, 1]; q=0
    returns infinity and q=1 returns zero. Invalid shapes return NaN.
    Supports implicit forward and reverse differentiation.
    """
    return _inverse(a, q, True)

probjax.utils.special.digammainv

digammainv

digammainv(y, maxiter=5, tol=1e-14)

Inverse of the digamma function using Newton's method with asymptotic approximations.

Implementation follows the approach described in the literature: For Ψ(x) = y, use Newton's method with the update: x_new = x_old - (Ψ(x) - y) / Ψ'(x)

NOTE: Digamm is only invertible for x > 0. and this function assumes that y is in the domain of invertibility.

Parameters:

Name Type Description Default
y Union[float, ndarray]

The value to find the inverse digamma for

required
maxiter int

Maximum number of Newton iterations (5 iterations typically sufficient for 14 digits)

5
tol float

Tolerance for convergence

1e-14

Returns:

Type Description
Union[float, ndarray]

x such that digamma(x) = y

Source code in probjax/utils/special/digammainv.py
def digammainv(
    y: Union[float, jnp.ndarray], maxiter: int = 5, tol: float = 1e-14
) -> Union[float, jnp.ndarray]:
    """
    Inverse of the digamma function using Newton's method with asymptotic approximations.

    Implementation follows the approach described in the literature:
    For Ψ(x) = y, use Newton's method with the update:
    x_new = x_old - (Ψ(x) - y) / Ψ'(x)

    NOTE: Digamm is only invertible for x > 0. and this function assumes that y is in the domain of invertibility.

    Args:
        y: The value to find the inverse digamma for
        maxiter: Maximum number of Newton iterations (5 iterations typically sufficient for 14 digits)
        tol: Tolerance for convergence

    Returns:
        x such that digamma(x) = y
    """

    # Initial guess based on asymptotic formulas (eq. 149 in the paper)
    def initial_guess(y):
        gamma = jnp.euler_gamma  # Euler-Mascheroni constant

        # Use asymptotic approximations
        # For y ≥ -2.22: x ≈ exp(y) + 1/2
        # For y < -2.22: x ≈ -1/(y + γ)
        x_init = jnp.where(y >= -2.22, jnp.exp(y) + 0.5, -1.0 / (y + gamma))

        return x_init

    # Define Newton step function (eq. 146 in the paper)
    def newton_step(x_old):
        # Calculate Ψ(x) - y and Ψ'(x)
        psi_x = digamma(x_old)
        psi_prime_x = polygamma(1, x_old)  # First derivative of digamma

        # Newton update
        x_new = x_old - (psi_x - y) / psi_prime_x

        return x_new

    # Initialize with the asymptotic approximation
    x = initial_guess(y)

    # Run fixed number of Newton iterations using lax.fori_loop
    def body_fun(i, x):
        return newton_step(x)

    # The paper states 5 iterations are sufficient for 14 digits of precision
    x = lax.fori_loop(0, maxiter, body_fun, x)

    return jnp.nan_to_num(x, nan=0.0, posinf=0.0, neginf=0.0)

probjax.utils.special.log1mexp

log1mexp(x)

Compute log(1-exp(x)) for x <= 0; positive inputs return NaN.

Zero returns -inf and -inf returns zero. Inactive branch arguments are guarded to keep interior gradients finite near zero.

Source code in probjax/utils/special/logspace.py
@jax.jit
def log1mexp(x):
    """Compute log(1-exp(x)) for x <= 0; positive inputs return NaN.

    Zero returns -inf and -inf returns zero. Inactive branch arguments are
    guarded to keep interior gradients finite near zero.
    """
    x = jnp.asarray(x, dtype=jnp.result_type(x, 1.0))
    left = x < -jnp.log(2.0)
    return jnp.where(
        left,
        jnp.log1p(-jnp.exp(jnp.where(left, x, -1.0))),
        jnp.log(-jnp.expm1(jnp.where(left, -1.0, x))),
    )

probjax.utils.special.logdiffexp

logdiffexp(a, b)

Compute log(exp(a)-exp(b)), broadcasting a >= b.

Equal finite inputs and (-inf, -inf) return -inf. Invalid domains, including (+inf, +inf), return NaN. Gradients require finite a > b.

Source code in probjax/utils/special/logspace.py
@jax.jit
def logdiffexp(a, b):
    """Compute log(exp(a)-exp(b)), broadcasting a >= b.

    Equal finite inputs and (-inf, -inf) return -inf. Invalid domains,
    including (+inf, +inf), return NaN. Gradients require finite a > b.
    """
    dtype = jnp.result_type(a, b, 1.0)
    a, b = jnp.broadcast_arrays(jnp.asarray(a, dtype), jnp.asarray(b, dtype))
    both_zero = jnp.isneginf(a) & jnp.isneginf(b)
    difference = jnp.where(both_zero, -1.0, b - a)
    return jnp.where(both_zero, -jnp.inf, a + log1mexp(difference))