Control¶
Use the control namespace to solve Riccati equations, design LQR/LQI gains, and place closed-loop poles for LTI systems.
The namespace includes:
dare()andcare()for algebraic Riccati solveslqr()andlqi()for regulator designaugment_integrator()for explicit integral-state augmentationplace()for design-time pole placementstate_feedback()for applying a state-feedback gain to an LTI model
Minimal Example¶
import jax
jax.config.update("jax_enable_x64", True)
import jax.numpy as jnp
import contrax as cx
sys = cx.dss(
jnp.array([[1.0, 0.05], [0.0, 1.0]]),
jnp.array([[0.0], [0.05]]),
jnp.eye(2),
jnp.zeros((2, 1)),
dt=0.05,
)
result = cx.lqr(sys, jnp.eye(2), jnp.array([[1.0]]))
closed_loop = cx.state_feedback(sys, result.K)
Conventions¶
Q: state cost matrix with shape(n, n)R: input cost matrix with shape(m, m)LQRResult.K: state-feedback gain with shape(m, n)LQRResult.S: Riccati solution with shape(n, n)LQRResult.poles: closed-loop eigenvaluesLQRResult.residual_norm: JAX scalar Riccati residual diagnostic
place() returns a PlaceResult with
.K (state-feedback gain, shape (m, n)) and .poles (achieved closed-loop
eigenvalues). Check .status, .successful, .assignment_error, conditioning
estimates, and .num_steps before using a candidate. A finite gain can fail
the acceptance checks.
For lqi(), the returned gain acts on the augmented state [x; z], where z
is the integral state added by augment_integrator().
Control Equations¶
LQR designs state feedback for linear systems.
For the discrete-time model
the infinite-horizon quadratic objective used by dare(), lqr(), and
discrete lqi() is
with state-feedback law
For the continuous-time model
the corresponding continuous objective behind care() and continuous lqr()
is
with feedback law
Riccati Equations¶
The stabilizing discrete Riccati equation is
and the continuous Riccati equation is
Given the stabilizing solution S, the corresponding gains are
Closed-Loop Models¶
state_feedback() applies u = v - Kx and retains v as the external input.
feedback() is an alias for this state-feedback operation.
For discrete systems,
and for continuous systems,
The output is y = (C - DK)x + Dv. Direct feedthrough changes the closed-loop
output matrix; B and D remain the external-input maps. These dynamics have the poles in
LQRResult.poles.
Integral Augmentation¶
augment_integrator() and lqi() introduce an integral state z driven by an
output-tracking error. At the level of the augmented state
the returned gain acts as
with \(\bar{K}\) partitioned over the physical and integral states.
Solver Selection and Failure Handling¶
Ordinary calls use cx.lqr(sys, Q, R). Pass solver=cx.solvers.DARE(...)
for discrete systems or solver=cx.solvers.CARE(...) for continuous systems;
lqi passes the same configuration to the augmented design. kalman_gain
accepts the DARE configuration for its dual equation.
- DARE uses structured doubling with scale-aware stopping.
- CARE uses Hamiltonian stable-subspace extraction and accepts Newton corrections only when they reduce the residual and retain stability.
- Both validate cost definiteness, equation residuals and closed-loop stability
in JAX. Inspect
result.successful,result.status,result.residual_normandresult.num_steps. Numerical failure returns diagnostics by default;throw=Trueraises in eager or compiled execution. - Shape errors and incompatible solver objects raise immediately.
Nearly inseparable Hamiltonian subspaces can make CARE inaccurate. Newton
refinement cannot create a stabilizing subspace when one is absent. Read
Riccati solvers for assumptions and conditioning.
Pole placement uses solver=cx.solvers.PolePlacement(...) for method,
convergence, assignment and conditioning limits. auto selects KNV0 for real
poles and YT for conjugate pairs. SVD input subspaces support redundant or
dependent input columns. A bounded small-SISO Ackermann fallback must pass
the same achieved-assignment and conditioning checks as the robust route.
The solver rejects invalid methods and malformed conjugate-pole requests.
placement = cx.place(sys, [0.7, 0.8],
solver=cx.solvers.PolePlacement(method="YT", max_steps=40))
assert placement.successful
Pole placement is a float64 design-time calculation and runs outside jit,
vmap, and differentiation. Model matrices are promoted to float64 when the
global x64 option is enabled. Numerically uncontrollable models report
invalid_problem;
assignment or conditioning breakdown reports numerical_failure. Exhausting
robust update sweeps reports max_steps_reached when a finite, otherwise valid
candidate remains. throw=False returns diagnostics and throw=True raises.
Inputs and incompatible configurations are validated before numerical work.
Transform Behavior¶
dare(), care(), and lqr() are designed to participate in compiled JAX
workflows.
The relevant transform behavior is:
lqr()dispatches onDiscLTIvsContLTIoutside traced runtime values- the Riccati paths differentiate the equation implicitly
state_feedback()is just state-space algebra on the model matrices
These paths allow controller design inside a JIT-compiled objective.
Numerical Notes¶
Riccati solves require jax_enable_x64=True; JAX x64 mode is not changed on
import.
state_feedback(sys, K) applies a gain to an LTI model. Use series() or
parallel() to compose LTI systems.
For integral-action designs, use augment_integrator() to build the augmented
model or lqi() to design the gain in one call.
Related Pages¶
- Systems for model construction before design
- Simulation for validating the designed controller
- Types for
LQRResult - Riccati solvers for solver details
- Tune LQR weights with gradients for a task-oriented recipe
contrax.control
¶
LQR design, pole placement, and closed-loop construction.
augment_integrator(sys: DiscLTI | ContLTI, C_integral: Array | None = None, D_integral: Array | None = None, *, sign: float = 1.0, dt_scale: ArrayLike | None = None) -> DiscLTI | ContLTI
¶
Augment an LTI system with integral-of-output states.
Returns an augmented system for LQI design. Pass it to lqr() with a state
cost that includes the added integral states.
For continuous systems, the added state evolves as
z_dot = sign * (C_integral x + D_integral u). For discrete systems, the
added state evolves as
z[k+1] = z[k] + dt_scale * sign * (C_integral x[k] + D_integral u[k]).
When dt_scale is omitted for a discrete system, sys.dt is used.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sys
|
DiscLTI | ContLTI
|
Continuous or discrete LTI system to augment. |
required |
C_integral
|
Array | None
|
Output map to integrate. Shape: |
None
|
D_integral
|
Array | None
|
Feedthrough map for the integrated output. Shape: |
None
|
sign
|
float
|
Sign applied to the integrated output. Use |
1.0
|
dt_scale
|
ArrayLike | None
|
Discrete integration scale. Defaults to |
None
|
Returns:
| Type | Description |
|---|---|
DiscLTI | ContLTI
|
Source code in contrax/control.py
care(A: Array, B: Array, Q: Array, R: Array, *, solver: CARE | None = None, throw: bool = False) -> LQRResult
¶
Solve the continuous algebraic Riccati equation and design a stabilizing gain.
Solves \(A^T S + S A - S B R^{-1} B^T S + Q = 0\) and computes \(K = R^{-1} B^T S\).
The baseline solver starts from a Hamiltonian stable-subspace candidate and applies residual-decreasing Newton corrections. Poor separation of the stable subspace can make the candidate inaccurate; residual and closed-loop stability checks report such cases as failures. The stabilizing solution requires a stabilizable pair (A, B) and detectable pair (Q**½, A).
Uses real matrices with symmetric positive-semidefinite Q and positive-definite R, and requires float64 precision. JVPs and VJPs use implicit differentiation of the Riccati equation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
A
|
Array
|
State matrix. Shape: |
required |
B
|
Array
|
Input matrix. Shape: |
required |
Q
|
Array
|
State cost matrix. Shape: |
required |
R
|
Array
|
Input cost matrix. Shape: |
required |
solver
|
CARE | None
|
Hamiltonian candidate refinement and residual tolerances. |
None
|
throw
|
bool
|
Raise on failed validation instead of returning failure diagnostics. |
False
|
Returns:
| Name | Type | Description |
|---|---|---|
LQRResult |
LQRResult
|
Gain K, Riccati solution S, poles, status and diagnostics. |
Examples:
>>> import jax
>>> jax.config.update("jax_enable_x64", True)
>>> import contrax as cx
>>> import jax.numpy as jnp
>>> result = cx.care(-jnp.eye(2), jnp.eye(2), jnp.eye(2), jnp.eye(2))
>>> bool(result.successful)
True
Source code in contrax/_care.py
dare(A: Array, B: Array, Q: Array, R: Array, *, solver: DARE | None = None, throw: bool = False) -> LQRResult
¶
Solve the discrete algebraic Riccati equation and design a stabilizing gain.
Solves \(S = A^T S A - A^T S B (R + B^T S B)^{-1} B^T S A + Q\) and computes \(K = (R + B^T S B)^{-1} B^T S A\).
The solver applies structured doubling with scale-aware stopping and validates the equation residual. It is the mature discrete LTI design path; check the returned status and residual for numerical failures. The stabilizing solution requires a stabilizable pair (A, B) and detectable pair (Q**½, A). Inputs are real matrices with symmetric positive-semidefinite Q and positive-definite R. Requires float64 precision.
Gradients use implicit differentiation of the Riccati equation through a residual-validated Lyapunov solve.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
A
|
Array
|
State transition matrix. Shape: |
required |
B
|
Array
|
Input matrix. Shape: |
required |
Q
|
Array
|
State cost matrix. Shape: |
required |
R
|
Array
|
Input cost matrix. Shape: |
required |
solver
|
DARE | None
|
Structured doubling tolerances and iteration limit. |
None
|
throw
|
bool
|
Raise on failed validation instead of returning failure diagnostics. |
False
|
Returns:
| Name | Type | Description |
|---|---|---|
LQRResult |
LQRResult
|
Gain K, Riccati solution S, poles, status and diagnostics. |
Examples:
>>> import jax
>>> jax.config.update("jax_enable_x64", True)
>>> import contrax as cx
>>> import jax.numpy as jnp
>>> result = cx.dare(jnp.eye(2), jnp.eye(2), jnp.eye(2), jnp.eye(2))
>>> bool(result.successful)
True
Source code in contrax/_riccati.py
lqi(sys: DiscLTI | ContLTI, Q: Array, R: Array, C_integral: Array | None = None, D_integral: Array | None = None, *, solver: DARE | CARE | None = None, throw: bool = False, sign: float = 1.0, dt_scale: ArrayLike | None = None) -> LQRResult
¶
Design an LQI gain by augmenting integral states and calling lqr().
Augments the system with integral states and designs a gain for the
resulting state [x; z], where z contains the integrated outputs.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sys
|
DiscLTI | ContLTI
|
Continuous or discrete LTI system. |
required |
Q
|
Array
|
Augmented state cost matrix. Shape: |
required |
R
|
Array
|
Input cost matrix. Shape: |
required |
C_integral
|
Array | None
|
Output map to integrate. Shape: |
None
|
D_integral
|
Array | None
|
Feedthrough map for the integrated output. Shape: |
None
|
solver
|
DARE | CARE | None
|
DARE for discrete systems or CARE for continuous systems. |
None
|
throw
|
bool
|
Raise on numerical failure. |
False
|
sign
|
float
|
Sign applied to the integrated output. |
1.0
|
dt_scale
|
ArrayLike | None
|
Discrete integration scale. Defaults to |
None
|
Returns:
| Type | Description |
|---|---|
LQRResult
|
LQRResult: LQR result for the integrator-augmented design model. |
Source code in contrax/control.py
lqr(sys: DiscLTI | ContLTI, Q: Array, R: Array, *, solver: DARE | CARE | None = None, throw: bool = False) -> LQRResult
¶
Solve the infinite-horizon linear quadratic regulator problem.
Dispatches to dare() for discrete systems and care() for continuous
systems, returning a unified LQRResult.
Both routes validate the equation residual and closed-loop stability and support implicit JVP/VJP differentiation. Type dispatch occurs at trace time; the system's time domain must stay fixed within a compiled workflow.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sys
|
DiscLTI | ContLTI
|
required | |
Q
|
Array
|
State cost matrix. Shape: |
required |
R
|
Array
|
Input cost matrix. Shape: |
required |
solver
|
DARE | CARE | None
|
DARE for discrete systems or CARE for continuous systems. |
None
|
throw
|
bool
|
Raise on numerical failure; otherwise return failure diagnostics. |
False
|
Returns:
| Type | Description |
|---|---|
LQRResult
|
LQRResult: A bundle containing the optimal gain, Riccati solution, and closed-loop poles. |
Raises:
| Type | Description |
|---|---|
TypeError
|
Examples:
>>> import jax
>>> jax.config.update("jax_enable_x64", True)
>>> import jax.numpy as jnp
>>> import contrax as cx
>>> sys = cx.dss(
... jnp.array([[1.0, 0.05], [0.0, 1.0]]),
... jnp.array([[0.0], [0.05]]),
... jnp.eye(2),
... jnp.zeros((2, 1)),
... dt=0.05,
... )
>>> result = cx.lqr(sys, jnp.eye(2), jnp.array([[1.0]]))
Source code in contrax/control.py
place(sys: DiscLTI | ContLTI, poles: ArrayLike, *, solver: PolePlacement | None = None, throw: bool = False) -> PlaceResult
¶
Assign real/conjugate-paired poles and validate the achieved feedback.
Uses native JAX KNV0 (real poles) or YT (real or complex pairs) linear algebra with design-time rank decisions and convergence control. Input subspaces use SVD so dependent and redundant input columns are supported. Small SISO failures may try bounded Ackermann fallback; every candidate must meet assignment and conditioning tolerances.
Rank decisions and host-side matching make this a design-time routine. It requires float64 precision, promotes real model matrices to float64, and cannot be jitted, vmapped, or differentiated.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sys
|
DiscLTI | ContLTI
|
Continuous or discrete real LTI model with |
required |
poles
|
ArrayLike
|
Finite requested poles. Shape: |
required |
solver
|
PolePlacement | None
|
PolePlacement method, stopping/acceptance tolerances and limits.
Multiplicity beyond |
None
|
throw
|
bool
|
Raise on failed assignment, convergence or conditioning checks. |
False
|
Returns:
| Name | Type | Description |
|---|---|---|
PlaceResult |
PlaceResult
|
Pole-placement gain and validation diagnostics. A failed |
PlaceResult
|
result may contain a finite candidate for inspection; check |
|
PlaceResult
|
|
Examples:
>>> import jax
>>> jax.config.update("jax_enable_x64", True)
>>> import contrax as cx
>>> sys = cx.ss([[0., 1.], [0., 0.]], [[0.], [1.]], [[1., 0.]], [[0.]])
>>> result = cx.place(sys, [-1., -2.])
>>> bool(result.successful)
True
Source code in contrax/_place.py
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 | |
state_feedback(sys: DiscLTI | ContLTI, K: Array) -> DiscLTI | ContLTI
¶
Construct the closed-loop system under state feedback.
Applies u = v - Kx, retaining v as the external input. The new matrices are A_cl = A - BK and C_cl = C - DK, with B, D and discrete dt unchanged. Nonzero direct feedthrough therefore contributes to the closed-loop output.
Accepts either DiscLTI or
ContLTI. For external input v, the closed-loop
matrices are A_cl = A - B K and C_cl = C - D K; B, D, and the
discrete sample time are unchanged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sys
|
DiscLTI | ContLTI
|
Continuous or discrete-time system. |
required |
K
|
Array
|
State-feedback gain. Shape: |
required |
Returns:
| Type | Description |
|---|---|
DiscLTI | ContLTI
|
Examples:
>>> import jax
>>> jax.config.update("jax_enable_x64", True)
>>> import jax.numpy as jnp
>>> import contrax as cx
>>> sys = cx.dss(
... jnp.array([[1.0, 0.05], [0.0, 1.0]]),
... jnp.array([[0.0], [0.05]]),
... jnp.eye(2),
... jnp.zeros((2, 1)),
... dt=0.05,
... )
>>> result = cx.lqr(sys, jnp.eye(2), jnp.array([[1.0]]))
>>> closed_loop = cx.state_feedback(sys, result.K)