Skip to content

Solvers

Solver objects configure algorithm choices, tolerances, and iteration limits. Calls use validated defaults unless a solver= is supplied. Diffrax and Optimistix objects retain their native interfaces in simulation and optimization.

import jax
jax.config.update("jax_enable_x64", True)
import jax.numpy as jnp
import contrax as cx

solver = cx.solvers.Lyapunov(method="doubling", rtol=1e-12)
result = cx.dlyap(0.5 * jnp.eye(2), jnp.eye(2), solver=solver)
assert result.successful

Close over solver objects in jax.jit, or pass them through Equinox transforms. Model matrices remain differentiable dynamic data. Numerical result statuses are integer JAX scalars, using SolverStatus codes. throw=False returns failure diagnostics; throw=True checks the result during execution. Invalid matrix shapes or algorithm configuration raise immediately.

DARE configures structured doubling; CARE configures Hamiltonian-subspace design with accepted Newton corrections. lqr and lqi accept the solver for their system's time domain. kalman_gain accepts a DARE object for the dual design equation. All these return .status and .successful in eager and compiled execution. They require float64 mode and use implicit JVP/VJP rules.

Kalman and EKF configure covariance updates. Both default to Joseph form; compact form requires covariance_update="compact". EKF(num_iter=3) enables iterated measurement linearization in online and batch estimators. Filtering supports float32 and float64 inputs and preserves their precision.

SteadyStateKalman(design) explicitly reuses a kalman_gain result. Its gain and covariance are differentiable data. The initial predicted covariance defaults to design.P; a supplied P0 must match it. The batch checks the fixed-point equations for the supplied A, C, Q, R and the estimator's stability. An inconsistent design or a missing observation reports invalid_problem. Use covariance-form filtering for varying covariances or missing observations. Design creation still requires float64 mode.

InformationKalman() accepts diagonal observation variances as R.shape=(p,). It whitens the prior and observations, then factors an n-by-n matrix. It avoids forming the prior inverse or a dense innovation covariance, and is useful when observations outnumber states. Predicted covariances at observed steps must be positive definite and all variances positive. Controls, masks, initialization defaults and differentiation follow ordinary Kalman filtering.

UKF(alpha=1, beta=2, kappa=0) configures the scaled unscented transform. Sigma-point and innovation covariances must be positive definite. Missing observations skip the measurement transform and contribute zero log likelihood. The innovation Cholesky factor is reused for the gain and likelihood.

Lyapunov supports stable doubling, general dense solves, and bounded automatic fallback. See Analysis and Control for equation assumptions, residual criteria and transform behavior.

PolePlacement configures the design-time KNV0/YT method, update limit, determinant-improvement tolerance, achieved-pole tolerances and conditioning limit. fallback_limit=8 bounds SISO Ackermann fallback; zero disables it. Pole matching preserves multiplicities and conjugate pairs. Configuration errors raise immediately; convergence and numerical failures return status diagnostics unless throw=True. Read the control reference for its float64 and transform contract.

contrax.solvers

Configuration objects for numerical solvers.

Solver objects hold static algorithm choices and tolerances. Close over a configuration in jax.jit; model matrices remain dynamic inputs.

CARE

Bases: Module

Configure a Hamiltonian stable-subspace CARE solve with Newton refinement.

Refinement accepts corrections that reduce the residual and retain stable feedback. The Hamiltonian must provide a well-separated stable subspace.

Attributes:

Name Type Description
refinement_steps int

Maximum Newton corrections of the stabilizing candidate.

rtol float

Relative equation residual tolerance.

atol float

Absolute equation residual tolerance.

Source code in contrax/solvers.py
class CARE(eqx.Module):
    """Configure a Hamiltonian stable-subspace CARE solve with Newton refinement.

    Refinement accepts corrections that reduce the residual and retain stable
    feedback. The Hamiltonian must provide a well-separated stable subspace.

    Attributes:
        refinement_steps: Maximum Newton corrections of the stabilizing candidate.
        rtol: Relative equation residual tolerance.
        atol: Absolute equation residual tolerance.
    """

    refinement_steps: int = eqx.field(static=True, default=3)
    rtol: float = eqx.field(static=True, default=1e-10)
    atol: float = eqx.field(static=True, default=1e-12)

    def __check_init__(self):
        Lyapunov(max_steps=self.refinement_steps, rtol=self.rtol, atol=self.atol)

DARE

Bases: Module

Configure structured doubling for the discrete algebraic Riccati equation.

Attributes:

Name Type Description
max_steps int

Maximum structured doubling iterations.

rtol float

Relative iterate and equation residual tolerance.

atol float

Absolute iterate and equation residual tolerance.

Source code in contrax/solvers.py
class DARE(eqx.Module):
    """Configure structured doubling for the discrete algebraic Riccati equation.

    Attributes:
        max_steps: Maximum structured doubling iterations.
        rtol: Relative iterate and equation residual tolerance.
        atol: Absolute iterate and equation residual tolerance.
    """

    max_steps: int = eqx.field(static=True, default=64)
    rtol: float = eqx.field(static=True, default=1e-10)
    atol: float = eqx.field(static=True, default=1e-12)

    def __check_init__(self):
        Lyapunov(max_steps=self.max_steps, rtol=self.rtol, atol=self.atol)

EKF

Bases: Module

Extended Kalman measurement-update configuration.

Attributes:

Name Type Description
covariance_update str

joseph (default) or compact.

num_iter int

Measurement linearization passes; one gives ordinary EKF. The prediction covariance stays fixed during iterated updates.

Source code in contrax/solvers.py
class EKF(eqx.Module):
    """Extended Kalman measurement-update configuration.

    Attributes:
        covariance_update: ``joseph`` (default) or ``compact``.
        num_iter: Measurement linearization passes; one gives ordinary EKF.
            The prediction covariance stays fixed during iterated updates.
    """

    covariance_update: str = eqx.field(static=True, default="joseph")
    num_iter: int = eqx.field(static=True, default=1)

    def __check_init__(self):
        Kalman(covariance_update=self.covariance_update)
        if not isinstance(self.num_iter, int) or self.num_iter < 1:
            raise ValueError("num_iter must be a positive integer")

InformationKalman

Bases: Module

Whitened state-space Kalman update for diagonal observation noise.

Use a positive observation-variance vector R with shape (p,). Prior whitening and state-size Cholesky solves make this formulation useful when observations outnumber states. Predicted covariances must be positive definite; controls and missing observations follow covariance-form Kalman conventions.

Source code in contrax/solvers.py
class InformationKalman(eqx.Module):
    """Whitened state-space Kalman update for diagonal observation noise.

    Use a positive observation-variance vector `R` with shape `(p,)`. Prior
    whitening and state-size Cholesky solves make this formulation useful when
    observations outnumber states. Predicted covariances must be positive
    definite; controls and missing observations follow covariance-form Kalman
    conventions.
    """

Kalman

Bases: Module

Covariance-form linear Kalman filter configuration.

Attributes:

Name Type Description
covariance_update str

joseph (default) or compact. Joseph form retains the measurement-noise term explicitly and better preserves covariance symmetry and positive semidefiniteness under roundoff.

Source code in contrax/solvers.py
class Kalman(eqx.Module):
    """Covariance-form linear Kalman filter configuration.

    Attributes:
        covariance_update: ``joseph`` (default) or ``compact``. Joseph form
            retains the measurement-noise term explicitly and better preserves
            covariance symmetry and positive semidefiniteness under roundoff.
    """

    covariance_update: str = eqx.field(static=True, default="joseph")

    def __check_init__(self):
        if self.covariance_update not in ("joseph", "compact"):
            raise ValueError("covariance_update must be joseph or compact")

Lyapunov

Bases: Module

Configure a continuous- or discrete-time Lyapunov solve.

auto uses squared Smith iteration with dense fallback when the state count is at most dense_limit. doubling applies to stable systems; dense handles any uniquely solvable equation, including unstable A. Explicit dense selection permits O(n^4) storage and O(n^6) work.

Attributes:

Name Type Description
method str

auto, doubling, or dense.

rtol float

Scale-aware equation residual tolerance.

atol float

Absolute equation residual tolerance.

max_steps int

Maximum doubling steps, including residual correction solves.

dense_limit int

Largest state dimension allowed for automatic fallback.

Source code in contrax/solvers.py
class Lyapunov(eqx.Module):
    """Configure a continuous- or discrete-time Lyapunov solve.

    ``auto`` uses squared Smith iteration with dense fallback when the state
    count is at most ``dense_limit``. ``doubling`` applies to stable systems;
    ``dense`` handles any uniquely solvable equation, including unstable A.
    Explicit dense selection permits O(n^4) storage and O(n^6) work.

    Attributes:
        method: ``auto``, ``doubling``, or ``dense``.
        rtol: Scale-aware equation residual tolerance.
        atol: Absolute equation residual tolerance.
        max_steps: Maximum doubling steps, including residual correction solves.
        dense_limit: Largest state dimension allowed for automatic fallback.
    """

    method: str = eqx.field(static=True, default="auto")
    rtol: float = eqx.field(static=True, default=1e-12)
    atol: float = eqx.field(static=True, default=1e-14)
    max_steps: int = eqx.field(static=True, default=64)
    dense_limit: int = eqx.field(static=True, default=8)

    def __check_init__(self):
        if self.method not in ("auto", "doubling", "dense"):
            raise ValueError("Lyapunov method must be auto, doubling, or dense")
        if (
            not math.isfinite(self.rtol)
            or not math.isfinite(self.atol)
            or self.rtol < 0
            or self.atol < 0
            or self.rtol + self.atol == 0
        ):
            raise ValueError(
                "Lyapunov tolerances must be nonnegative and not both zero"
            )
        if not isinstance(self.max_steps, int) or self.max_steps < 0:
            raise ValueError("max_steps must be a nonnegative integer")
        if not isinstance(self.dense_limit, int) or self.dense_limit < 0:
            raise ValueError("dense_limit must be a nonnegative integer")

PolePlacement

Bases: Module

Configure design-time pole assignment and acceptance tolerances.

Attributes:

Name Type Description
method str

auto chooses KNV0 for real poles and YT for complex pairs. Explicit KNV0 and YT selections are also accepted.

rtol float

Relative determinant-improvement convergence tolerance.

max_steps int

Maximum eigenvector-update sweeps.

assignment_rtol float

Scale-aware achieved-pole tolerance.

assignment_atol float

Absolute achieved-pole tolerance.

condition_limit float

Maximum controllability/eigenvector condition estimate.

fallback_limit int

Largest SISO dimension allowed for Ackermann fallback; zero disables fallback. Every fallback is validated independently.

Source code in contrax/solvers.py
class PolePlacement(eqx.Module):
    """Configure design-time pole assignment and acceptance tolerances.

    Attributes:
        method: auto chooses KNV0 for real poles and YT for complex pairs.
            Explicit KNV0 and YT selections are also accepted.
        rtol: Relative determinant-improvement convergence tolerance.
        max_steps: Maximum eigenvector-update sweeps.
        assignment_rtol: Scale-aware achieved-pole tolerance.
        assignment_atol: Absolute achieved-pole tolerance.
        condition_limit: Maximum controllability/eigenvector condition estimate.
        fallback_limit: Largest SISO dimension allowed for Ackermann fallback;
            zero disables fallback. Every fallback is validated independently.
    """

    method: str = eqx.field(static=True, default="auto")
    rtol: float = eqx.field(static=True, default=1e-3)
    max_steps: int = eqx.field(static=True, default=30)
    assignment_rtol: float = eqx.field(static=True, default=1e-6)
    assignment_atol: float = eqx.field(static=True, default=1e-8)
    condition_limit: float = eqx.field(static=True, default=1e12)
    fallback_limit: int = eqx.field(static=True, default=8)

    def __check_init__(self):
        if self.method not in ("auto", "KNV0", "YT"):
            raise ValueError("PolePlacement method must be auto, KNV0, or YT")
        Lyapunov(
            rtol=self.rtol,
            atol=0,
            max_steps=self.max_steps,
            dense_limit=self.fallback_limit,
        )
        Lyapunov(rtol=self.assignment_rtol, atol=self.assignment_atol)
        if not math.isfinite(self.condition_limit) or self.condition_limit < 1:
            raise ValueError("condition_limit must be finite and at least one")

SolverStatus

Bases: IntEnum

JAX-compatible integer status codes shared by numerical results.

Source code in contrax/solvers.py
class SolverStatus(IntEnum):
    """JAX-compatible integer status codes shared by numerical results."""

    successful = 0
    max_steps_reached = 1
    invalid_problem = 2
    numerical_failure = 3

SteadyStateKalman

Bases: Module

Explicitly reuse a kalman_gain design for a fully observed LTI stream.

Uses the gain design as differentiable data for a fully observed LTI stream. Batch filtering defaults P0 to design.P, the predicted covariance before the first update. A different P0, a missing observation, or an inconsistent model/noise pair reports invalid_problem; use covariance-form filtering when those cases are expected.

Attributes:

Name Type Description
design KalmanGainResult

KalmanGainResult returned by kalman_gain. The gain and predicted covariance remain differentiable inputs.

rtol float

Relative tolerance for gain/fixed-point and initialization checks.

atol float

Absolute tolerance for those checks.

Source code in contrax/solvers.py
class SteadyStateKalman(eqx.Module):
    """Explicitly reuse a kalman_gain design for a fully observed LTI stream.

    Uses the gain design as differentiable data for a fully observed LTI stream.
    Batch filtering defaults P0 to design.P, the predicted covariance before
    the first update. A different P0, a missing observation, or an inconsistent
    model/noise pair reports `invalid_problem`; use covariance-form filtering
    when those cases are expected.

    Attributes:
        design: KalmanGainResult returned by `kalman_gain`. The gain and
            predicted covariance remain differentiable inputs.
        rtol: Relative tolerance for gain/fixed-point and initialization checks.
        atol: Absolute tolerance for those checks.
    """

    design: KalmanGainResult
    rtol: float = eqx.field(static=True, default=1e-8)
    atol: float = eqx.field(static=True, default=1e-10)

    def __check_init__(self):
        if not isinstance(self.design, KalmanGainResult):
            raise TypeError("SteadyStateKalman requires a kalman_gain design")
        Lyapunov(rtol=self.rtol, atol=self.atol)

UKF

Bases: Module

Scaled unscented transform for additive-noise filtering.

Attributes:

Name Type Description
alpha float

Positive sigma-point spread; defaults to 1.

beta float

Prior-distribution correction; 2 is standard for Gaussian priors.

kappa float

Secondary scaling; n+kappa must be positive for n states.

Sigma-point covariances and innovation covariances must be positive definite. Supply covariances that satisfy this condition before filtering.

Source code in contrax/solvers.py
class UKF(eqx.Module):
    """Scaled unscented transform for additive-noise filtering.

    Attributes:
        alpha: Positive sigma-point spread; defaults to 1.
        beta: Prior-distribution correction; 2 is standard for Gaussian priors.
        kappa: Secondary scaling; n+kappa must be positive for n states.

    Sigma-point covariances and innovation covariances must be positive
    definite. Supply covariances that satisfy this condition before filtering.
    """

    alpha: float = eqx.field(static=True, default=1.0)
    beta: float = eqx.field(static=True, default=2.0)
    kappa: float = eqx.field(static=True, default=0.0)

    def __check_init__(self):
        if not all(math.isfinite(a) for a in (self.alpha, self.beta, self.kappa)):
            raise ValueError("UKF scaling parameters must be finite")
        if self.alpha <= 0:
            raise ValueError("UKF alpha must be positive")