Analysis¶
Use the analysis namespace to inspect controllability, observability, poles, transfer response, Gramians, Lyapunov solutions, and finite invariant zeros.
The namespace includes:
ctrb()andobsv()for controllability and observability matricespoles()for eigenvalue inspectionevalfr(),freqresp(), anddcgain()for transfer evaluationctrb_gramian()andobsv_gramian()for continuous/discrete, finite/infinite Gramianslyap()anddlyap()for continuous and discrete Lyapunov equation solverszeros()for finite invariant zeros of square or rectangular LTI systemsquadratic_rollout_cost()for exact finite-horizon discrete feedback costs
Minimal Example¶
import jax
jax.config.update("jax_enable_x64", True)
import jax.numpy as jnp
import contrax as cx
sys = cx.ss(
jnp.array([[0.0, 1.0], [-2.0, -0.4]]),
jnp.array([[0.0], [1.0]]),
jnp.array([[1.0, 0.0]]),
jnp.zeros((1, 1)),
)
poles = cx.poles(sys)
dc = cx.dcgain(sys)
Wc = cx.ctrb_gramian(sys, t=2.0).X
Conventions¶
poles(sys)returns the system eigenvaluesevalfr(sys, point)evaluates the transfer map at one complex pointfreqresp(sys, omega)evaluates over a frequency griddcgain(sys)evaluates the zero-frequency or steady-state gain
The transfer helpers evaluate state-space realizations directly.
Transform Behavior¶
Array-based analysis helpers compose with jit and batching when the
underlying linear algebra operation supports those transformations.
Gramian helpers support jit, vmap, JVP and VJP. Discrete finite horizon
counts are static Python integers. Continuous finite horizons use a matrix
exponential; infinite horizons differentiate the Lyapunov equation implicitly.
Gramians and Horizons¶
With no horizon argument, ctrb_gramian(sys) and obsv_gramian(sys) solve the
stable infinite-horizon equation. The default uses Lyapunov doubling. Both
continuous and discrete systems must be strictly stable for an infinite-horizon
Gramian; a marginal or unstable model returns invalid_problem even if a
general dense Lyapunov equation could have a finite solution.
Specify t=... for a continuous integration horizon, or num_steps=... for a
discrete sum over k=0,...,N−1. The arguments are distinct to avoid confusing
seconds and step counts. Finite horizons support marginal and unstable models;
zero returns the zero Gramian. Discrete sums have per-step weighting and do not
include a sample-time factor. Controllability sums use A^k B B.T (A^k).T;
observability sums use (A^k).T C.T C A^k.
Both functions return LyapunovResult: the Gramian is .X, with .status,
.successful and .residual_norm diagnostics. throw=True checks failure in
eager or compiled execution. The finite-horizon residual checks the corresponding
Gramian equation with its terminal forcing term. For finite discrete horizons,
.num_steps is the requested term count; infinite solves report doubling steps.
Select an infinite-horizon equation algorithm using
solver=cx.solvers.Lyapunov(...). Finite discrete horizons use binary
composition; finite continuous horizons use Van Loan's block exponential.
Very long continuous horizons can overflow the block exponential even for a
stable model; use the infinite-horizon solve when that is the intended quantity.
Lyapunov Equations¶
lyap(A, Q) solves the continuous Lyapunov equation \(A X + X A^\top + Q = 0\).
dlyap(A, Q) solves the discrete form \(A X A^\top - X + Q = 0\).
Both return LyapunovResult; use .X for the matrix and .successful or
.status to check the solve. The default uses squared Smith iteration for
stable systems and validates the equation residual. Automatic dense fallback
is restricted to at most eight states. It supports small nonstable equations;
for larger nonstable equations select the dense solver explicitly:
Dense solves need O(n^4) memory and O(n^6) work. The stable doubling route uses
O(n^2) storage and O(n^3) work per step. For continuous equations, a Cayley
transform supplies the discrete equation. method="doubling" requires a
stable state matrix and reports invalid_problem otherwise.
Both equations require float64 mode and real square matrices. The solution is
unique when no eigenvalue sum is zero (continuous) or product is one
(discrete). This condition also permits nonstable systems. Forward and reverse
derivatives solve the implicit matrix equation; they do not differentiate
through the stopping loop. Use vmap for independent equations.
throw=False returns failure diagnostics; throw=True raises through Equinox
checks in eager or compiled execution. Singular equations and nonconvergent
iterations must be handled before using their solutions. The returned residual
is a Frobenius norm; success requires it to be at most
atol + rtol * scale, where scale sums norms of the individual equation
terms. This bounds backward error; it does not bound forward error for an
ill-conditioned equation.
Finite Invariant Zeros¶
zeros(sys) computes finite rank drops of the Rosenbrock system pencil
relative to its normal rank. It accepts square and rectangular realizations,
including zero, singular and full-rank feedthrough matrices. For a minimal
realization these are transmission zeros. Nonminimal realizations can include
decoupling zeros absent from the minimal transfer realization.
For square invertible \(D\), the finite pencil reduces to \(A - B D^{-1} C - \lambda I\).
The general algorithm uses SVD-based orthogonal staircase reductions of the
algebraic output constraints, followed by removal of right singular structure.
The remaining finite pencil has identity coefficient for the spectral
parameter and uses jnp.linalg.eigvals. SciPy and SLICOT are offline references.
The formulation follows the finite-pencil reduction described by
SLICOT AB08ND.
Validation includes its rectangular nonminimal example and the transposed
system, as well as independent generalized eigenvalues for regular square
pencils.
Zero computation uses host-side rank decisions and requires float64 precision;
it cannot be jitted, vmapped, or differentiated. Tolerances determine numerical
rank, so check sensitivity to rtol, atol, and realization scaling near rank
transitions. The result contains finite zeros; it omits infinite zeros and
Kronecker indices.
Quadratic Feedback Costs¶
quadratic_rollout_cost(sys, K, Q, R, x0, num_steps=N) computes
Omit Q_terminal for zero terminal cost. Q and R weight each step
independently of the discrete system's sample time. Binary Gramian
composition uses O(log N) matrix products and O(n²) storage, without retaining
a trajectory. Stability is unnecessary for a finite horizon, but long unstable
horizons may overflow. It returns a scalar for one (n,) initial state;
use vmap for batches:
cost = cx.quadratic_rollout_cost(
sys_d, K, Q, R, x0, num_steps=100, Q_terminal=Q,
)
costs = jax.vmap(lambda x: cx.quadratic_rollout_cost(
sys_d, K, Q, R, x, num_steps=100,
))(initial_states)
The horizon is a static nonnegative Python integer. Zero steps return only
the optional initial-state terminal cost. Float32 and float64 inputs are
supported. The cost composes with jit, vmap, JVP, and VJP. Validation
compares values and gain derivatives with an explicit state/input trajectory
sum.
Numerical Notes¶
Finite-horizon Gramians use a Van Loan block-matrix exponential construction
and require jax_enable_x64=True.
When comparing realizations, prefer invariant checks such as rank, conditioning, pole location, and transfer behavior. Exact matrix equality is only meaningful when the realization basis is intentionally identical.
Related Pages¶
- Systems for the models these helpers inspect
- Simulation for time-domain validation alongside frequency- domain or structural checks
- JAX-native workflows for broader end-to-end usage patterns
contrax.analysis
¶
Structural and transfer analysis helpers.
ctrb(sys: DiscLTI | ContLTI) -> Array
¶
Construct the controllability matrix of a state-space system.
Forms [B, AB, A^2 B, ..., A^{n-1} B] for a continuous or discrete linear
system.
Use the singular values or a tolerance-aware rank of this matrix to assess controllability for a particular model and scaling.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sys
|
DiscLTI | ContLTI
|
Continuous or discrete LTI system. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
Array |
Array
|
Controllability matrix. Shape: |
Source code in contrax/analysis.py
ctrb_gramian(sys: ContLTI | DiscLTI, *, t: float | Array | None = None, num_steps: int | None = None, solver: Lyapunov | None = None, throw: bool = False) -> LyapunovResult
¶
Compute a continuous or discrete controllability Gramian.
With no horizon, solves the infinite-horizon equation for a strictly stable
system. Set t for a continuous finite integral or num_steps for a
discrete finite sum; finite horizons also apply to marginal or unstable
systems. Requires float64 precision. A discrete horizon is a static Python
integer, and each discrete sum term has unit weight.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sys
|
ContLTI | DiscLTI
|
Continuous or discrete LTI realization. |
required |
t
|
float | Array | None
|
Nonnegative continuous-time integration horizon. |
None
|
num_steps
|
int | None
|
Nonnegative number of discrete Gramian terms. |
None
|
solver
|
Lyapunov | None
|
Infinite-horizon Lyapunov configuration; defaults to stable doubling. |
None
|
throw
|
bool
|
Raise on numerical failure or a nonstable infinite-horizon model. |
False
|
Returns:
| Name | Type | Description |
|---|---|---|
LyapunovResult |
LyapunovResult
|
Gramian X, validated status and equation residual. Finite discrete num_steps reports the horizon term count. |
Examples:
>>> import jax
>>> jax.config.update("jax_enable_x64", True)
>>> import contrax as cx
>>> sys = cx.dss([[0.8]], [[1.0]], [[1.0]], [[0.0]], dt=1)
>>> result = ctrb_gramian(sys, num_steps=20)
>>> Wc = result.X
Source code in contrax/_gramians.py
dcgain(sys: DiscLTI | ContLTI) -> Array
¶
Return the zero-frequency or steady-state gain of an LTI system.
For continuous systems this evaluates G(0). For discrete systems this
evaluates G(1).
The corresponding resolvent must be nonsingular at zero frequency.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sys
|
DiscLTI | ContLTI
|
Continuous or discrete LTI system. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
Array |
Array
|
DC gain matrix. Shape: |
Source code in contrax/analysis.py
dlyap(A: Array, Q: Array, *, solver: Lyapunov | None = None, throw: bool = False) -> LyapunovResult
¶
Solve the discrete Lyapunov equation AXA^T - X + Q = 0.
The default uses squared Smith iteration, with residual validation and
dense fallback bounded to eight states. Select Lyapunov(method="dense")
for general uniquely solvable equations, including unstable systems.
Uniqueness requires every eigenvalue product lambda_i * lambda_j to
differ from one. Symmetric Q gives symmetric X.
Requires float64 precision. JAX derivatives follow the implicit equation solve. Automatic dense fallback is limited to eight states; larger systems use the doubling method.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
A
|
Array
|
Real state transition matrix. Shape: |
required |
Q
|
Array
|
Real forcing matrix. Shape: |
required |
solver
|
Lyapunov | None
|
Algorithm, tolerances and iteration limit. |
None
|
throw
|
bool
|
Raise on numerical failure, including under |
False
|
Returns:
| Name | Type | Description |
|---|---|---|
LyapunovResult |
LyapunovResult
|
Solution |
Examples:
>>> import jax
>>> jax.config.update("jax_enable_x64", True)
>>> import contrax as cx
>>> import jax.numpy as jnp
>>> result = cx.dlyap(0.5 * jnp.eye(2), jnp.eye(2))
>>> bool(result.successful)
True
Source code in contrax/analysis.py
evalfr(sys: DiscLTI | ContLTI, point: complex | Array) -> Array
¶
Evaluate the transfer matrix at a complex frequency point.
For continuous systems this computes G(s) = C (sI - A)^-1 B + D. For
discrete systems it computes G(z) = C (zI - A)^-1 B + D.
Use point evaluation for spot checks or as the basis of a frequency-response calculation. The resolvent must be nonsingular; evaluation at a realization pole may return nonfinite values even when a nonminimal realization has a removable cancellation. The matrices and point determine the promoted complex dtype.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sys
|
DiscLTI | ContLTI
|
Continuous or discrete LTI system. |
required |
point
|
complex | Array
|
Complex evaluation point |
required |
Returns:
| Name | Type | Description |
|---|---|---|
Array |
Array
|
Transfer matrix evaluated at |
Source code in contrax/analysis.py
freqresp(sys: DiscLTI | ContLTI, omega: Array) -> Array
¶
Evaluate the frequency response over angular frequencies.
Continuous systems are evaluated on the imaginary axis at s = j omega.
Discrete systems are evaluated on the unit circle at
z = exp(j omega dt).
Returns the complex transfer matrix at each frequency. An empty frequency
vector returns shape (0, p, m).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sys
|
DiscLTI | ContLTI
|
Continuous or discrete LTI system. |
required |
omega
|
Array
|
Angular frequencies in rad/s. Shape: |
required |
Returns:
| Name | Type | Description |
|---|---|---|
Array |
Array
|
Complex frequency response. Shape: |
Source code in contrax/analysis.py
lyap(A: Array, Q: Array, *, solver: Lyapunov | None = None, throw: bool = False) -> LyapunovResult
¶
Solve the continuous Lyapunov equation AX + XA^T + Q = 0.
The default uses squared Smith iteration after a Cayley transform, with
residual validation and dense fallback bounded to eight states. Select
Lyapunov(method="dense") for general uniquely solvable equations,
including unstable systems. Uniqueness requires every eigenvalue sum
lambda_i + lambda_j to be nonzero. Symmetric Q gives symmetric X.
Requires float64 precision. JAX derivatives follow the implicit equation
solve.
Numerical failures return diagnostics unless throw=True.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
A
|
Array
|
Real state matrix. Shape: |
required |
Q
|
Array
|
Real forcing matrix. Shape: |
required |
solver
|
Lyapunov | None
|
Algorithm, tolerances and iteration limit. |
None
|
throw
|
bool
|
Raise on numerical failure, including under |
False
|
Returns:
| Name | Type | Description |
|---|---|---|
LyapunovResult |
LyapunovResult
|
Solution |
Examples:
>>> import jax
>>> jax.config.update("jax_enable_x64", True)
>>> import contrax as cx
>>> import jax.numpy as jnp
>>> result = cx.lyap(-jnp.eye(2), jnp.eye(2))
>>> bool(result.successful)
True
Source code in contrax/analysis.py
obsv(sys: DiscLTI | ContLTI) -> Array
¶
Construct the observability matrix of a state-space system.
Forms [C; CA; CA^2; ...; CA^{n-1}] for a continuous or discrete linear
system.
Use the singular values or a tolerance-aware rank of this matrix to assess observability for a particular model and scaling.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sys
|
DiscLTI | ContLTI
|
Continuous or discrete LTI system. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
Array |
Array
|
Observability matrix. Shape: |
Source code in contrax/analysis.py
obsv_gramian(sys: ContLTI | DiscLTI, *, t: float | Array | None = None, num_steps: int | None = None, solver: Lyapunov | None = None, throw: bool = False) -> LyapunovResult
¶
Compute a continuous or discrete observability Gramian.
Uses the dual equation with forcing C.T @ C and transposed state
dynamics. With no horizon, the system must be strictly stable. Set t for
a finite continuous integral or num_steps for a finite discrete sum; zero
horizons are valid. Requires float64 precision. A discrete horizon is a
static Python integer, and each discrete sum term has unit weight.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sys
|
ContLTI | DiscLTI
|
Continuous or discrete LTI realization. |
required |
t
|
float | Array | None
|
Nonnegative continuous-time integration horizon. |
None
|
num_steps
|
int | None
|
Nonnegative number of discrete Gramian terms. |
None
|
solver
|
Lyapunov | None
|
Infinite-horizon Lyapunov configuration; defaults to stable doubling. |
None
|
throw
|
bool
|
Raise on numerical failure or a nonstable infinite-horizon model. |
False
|
Returns:
| Name | Type | Description |
|---|---|---|
LyapunovResult |
LyapunovResult
|
Gramian X, validated status and equation residual. Finite discrete num_steps reports the horizon term count. |
Examples:
>>> import jax
>>> jax.config.update("jax_enable_x64", True)
>>> import contrax as cx
>>> sys = cx.ss([[-1.0]], [[1.0]], [[1.0]], [[0.0]])
>>> result = obsv_gramian(sys, t=2.0)
>>> Wo = result.X
Source code in contrax/_gramians.py
poles(sys: DiscLTI | ContLTI) -> Array
¶
Return the poles of a state-space system.
The poles are the eigenvalues of the state matrix A. The returned dtype
is generally complex, including for real systems with real poles.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sys
|
DiscLTI | ContLTI
|
Continuous or discrete LTI system. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
Array |
Array
|
System poles. Shape: |
Source code in contrax/analysis.py
quadratic_rollout_cost(sys: DiscLTI, K: Array, Q: Array, R: Array, x0: Array, *, num_steps: int, Q_terminal: Array | None = None) -> Array
¶
Evaluate a finite-horizon discrete LTI feedback cost.
Computes sum(x_k.T @ Q @ x_k + u_k.T @ R @ u_k) for k=0,...,N-1,
with u_k = -K @ x_k, and optionally adds
x_N.T @ Q_terminal @ x_N. Binary Gramian composition uses O(log N)
matrix products and O(n**2) storage. num_steps is a static nonnegative
Python integer; the system matrices, gain, costs, and initial state are JAX
inputs. Long horizons with unstable closed-loop dynamics can overflow.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sys
|
DiscLTI
|
Discrete LTI system with n states and m inputs. |
required |
K
|
Array
|
Feedback gain. Shape: |
required |
Q
|
Array
|
State cost. Shape: |
required |
R
|
Array
|
Input cost. Shape: |
required |
x0
|
Array
|
Initial state. Shape: |
required |
num_steps
|
int
|
Number of cost terms/transitions. Zero is valid. |
required |
Q_terminal
|
Array | None
|
Optional terminal-state cost. Shape: |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
Array |
Array
|
Scalar quadratic cost for the single initial state. |
Raises:
| Type | Description |
|---|---|
TypeError
|
For a continuous model or noninteger horizon. |
ValueError
|
For a negative horizon or inconsistent shapes. |
Examples:
>>> import jax
>>> jax.config.update("jax_enable_x64", True)
>>> import contrax as cx
>>> import jax.numpy as jnp
>>> sys = cx.dss(jnp.eye(1), jnp.eye(1), jnp.eye(1), jnp.zeros((1, 1)), 1)
>>> cx.quadratic_rollout_cost(sys, jnp.zeros((1, 1)), jnp.eye(1),
... jnp.eye(1), jnp.ones(1), num_steps=3)
Array(3., dtype=float64)
Source code in contrax/_quadratic.py
zeros(sys: ContLTI | DiscLTI, *, rtol: float | None = None, atol: float = 0.0) -> Array
¶
Return finite invariant zeros of a real continuous or discrete LTI system.
Computes rank drops of the Rosenbrock pencil relative to its normal rank for square or rectangular systems with any feedthrough rank. For minimal realizations, invariant zeros coincide with transmission zeros. Nonminimal realizations can also have decoupling zeros absent from a minimal transfer realization. The result contains finite zeros; infinite zeros are omitted.
Orthogonal staircase reduction removes output constraints and infinite
structure. A controllability staircase removes remaining right singular
structure, then JAX eigensolvers solve the finite reduced pencil. Rank-
dependent shapes and host-side decisions make this a design-time analysis
routine; it does not support jit, vmap, or differentiation. Requires
float64 mode. Rank decisions depend on scaling, so set tolerances for nearly
rank-deficient realizations and compare transfer behavior after rescaling.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sys
|
ContLTI | DiscLTI
|
Real continuous or discrete LTI realization. |
required |
rtol
|
float | None
|
Relative numerical rank tolerance. Defaults to ten times machine epsilon times the larger Rosenbrock pencil dimension. |
None
|
atol
|
float
|
Absolute rank threshold. Nonnegative. |
0.0
|
Returns:
| Name | Type | Description |
|---|---|---|
Array |
Array
|
Finite invariant zeros including multiplicities. Complex shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
For complex/nonfinite matrices or invalid rank tolerances. |
Examples:
>>> import jax
>>> jax.config.update("jax_enable_x64", True)
>>> import contrax as cx
>>> import jax.numpy as jnp
>>> sys = cx.ss(jnp.array([[-2.]]), jnp.ones((1, 1)),
... jnp.array([[3.]]), jnp.array([[2.]]))
>>> cx.zeros(sys)
Array([-3.5+0.j], dtype=complex128)