Skip to content

chc.koopman

Stability tier: experimental.

Koopman dynamics + LQR: lift a nonlinear system to a global-linear one, then control it.

The Koopman idea: in a lifted feature space phi(x) the dynamics are approximately linear, phi(x') ~ A phi(x) + B u. Fit A, B from transitions by least squares (EDMD); the nonlinear system becomes linear, so control is a fast discrete LQR in the lifted space, not a gradient-descent MPC. The LQR is exact for the linear surrogate, not for the plant: through a nonlinear dictionary the input of even a control-affine plant can enter the lift with a state-dependent gain, so a linear lift is misspecified there in general, and LQR or MPC on it carries no closed-loop guarantee for the plant (Strässer et al. 2026, Annual Reviews in Control 61). This mirrors the CHC hybrid philosophy (a structured global model plus a correction); the modern, Residual-Koopman-MPC-lineage control backend. The dictionary is polynomial; the first state_dim features are the raw state, decoding is a slice.

KoopmanModel dataclass

Koopman model phi(x') ~ A phi(x) + B u fit by least squares on a polynomial lift.

ridge is the Tikhonov term on each coefficient, in units of its column's mean square, so the fit reads the same in any units of the state and the action. Added as a constant, it outweighed an action logged in millionths of its units and set B near zero.

form="bilinear" fits phi(x') ~ A phi(x) + B u + sum_j u_j H_j phi(x). A control-affine plant moves each nonlinear feature of the lift by a term that is the action times a function of the state, so its linear lift is misspecified however rich the dictionary; the bilinear one carries that term (Strässer et al., Annual Reviews in Control 61, 2026). The default form="linear" fits as before, bit for bit.

fit reports closure_residuals: for each feature of the lift, the root mean square of its one-step error on transitions held out of the fit, over the root mean square of the feature at the next state. The transitions are cut into five contiguous blocks, each predicted by a model fit on the other four; the model kept is fit on all of them. A lift whose span the dynamics map into itself reads 0 on noise-free data (ResDMD's residual, Colbrook and Townsend, on the whole dictionary); one whose span they leave reads the share of each feature it does not predict. The ratio is read feature by feature, so it reads the same in any units of the state and the action.

residuals(x, u, x_next)

Each transition's one-step error in the lift, phi(x') - A phi(x) - B u (- ...).

Pass transitions the model was not fit on: on its own the error reads the fit, not the closure.

channel(x)

The lifted model's derivative in the action at state x: B + [H_j phi(x)]_j.

koopman_lqr_gain(model, q_state, r, *, x_target=None, max_residual=0.5)

Infinite-horizon discrete-LQR gain in the lifted space (state cost via the decode slice).

The gain is the lifted model's, and it is refused where the lift does not close: where a feature's held-out closure residual exceeds max_residual, or was not read. The default of 0.5 refuses a lift that predicts less than three quarters of some feature's mean square on transitions it was not fit on. A bilinear model's channel depends on the state: its gain is the LQR of the model linearised at x_target, which it then requires. A linear model's gain does not depend on x_target.

Raises:

Type Description
RuntimeError

before the model is fit.

ValueError

where the lift does not close, or a bilinear model is given no x_target.

koopman_controller(model, gain, x_target)

Feedback law u = -G (phi(x) - phi(x_target)) regulating the system to x_target.