Skip to content

chc.adjoint

Stability tier: stable.

Control gradient dJ/du: a hand-written discrete adjoint + an adaptive diffrax continuous adjoint.

The discrete adjoint assembles the backward recursion explicitly from autodiff Jacobians -- the object the gradient-check gate verifies against autodiff and finite differences. control_gradient_diffrax is the modern sibling: each interval is integrated by an adaptive (stiff-capable) solver, with the continuous BacksolveAdjoint for O(1)-memory gradients.

control_gradient_adjoint(dyn, x0, us, dt, cost)

Return dJ/du of shape (H, m) via the discrete adjoint.

λ_H = ∇Φ(x_H); λ_k = ∇_x L_k + (∂F/∂x_k)ᵀ λ_{k+1}; ∇_{u_k} J = ∇_u L_k + (∂F/∂u_k)ᵀ λ_{k+1}.

costate_norms(dyn, x0, us, dt, cost)

||lambda_{k+1}|| for k = 0 .. H-1 -- the local Lipschitz constant of the cost-to-go.

The same backward recursion control_gradient_adjoint runs, returning the costates themselves rather than the gradient they contract with. lambda_{k+1} is dJ/dx_{k+1} along this trajectory, so ||lambda_{k+1}|| is exactly the first-order sensitivity of the objective to a perturbation of the state at step k+1 -- which is what turns a bound on a per-step transition error into a bound on the objective (chc.uncertainty.ConfoundingRobustPenalty, Result 38 (b)).

Local by construction, and that is the honest reading: these are derivatives at one trajectory, so the bound they license is a first-order one about a neighbourhood of it, not a global Lipschitz constant of J.

perturbation_cost_weights(dyn, x0, us, dt, cost, radius)

Weights w_k with sum_k w_k * radius * ||u_k|| >= |J(perturbed) - J(reference)|.

The object Result 38 (b) asks for. A support model licenses a bound on the field: ||f_true(x, u) - f_hat(x, u)|| <= radius * ||u||. This turns that into a bound on the objective, at the actions it is handed, in three pieces:

  • injection. One RK4 step maps a field offset g into a state offset dt (I + dtJ/2 + (dtJ)^2/6 + (dtJ)^3/24) g with J = df/dx -- exact for an offset constant over the step, which an unmodelled Delta_B u at fixed u is. The spectral norm of that polynomial falls either side of 1 depending on the plant -- 0.99990 on the certificate's oscillator, so a bare dt happens to be conservative there and need not be elsewhere -- which is reason enough to compute it rather than assume it.
  • first order. ||lambda_{k+1}|| contracted with that injection (costate_norms).
  • second order. The first-order term is asymptotically tight, so it is not an upper bound: the curvature term it drops is positive and O(radius^2). A deviation tube rho_{k+1} = ||dF/dx|| rho_k + eps_k carries the injections forward, and 1/2 sum ||d^2 L/dx^2|| rho^2 closes the Taylor expansion. Exact for a linear plant with a quadratic cost, where J is exactly quadratic along the perturbation; O(radius^3) otherwise.

radius = 0 is meaningful and is used: it zeroes the tube, so the weights collapse to exactly the first-order term, which is how the certificate reports both.

The curvature term does not factor per step, so it is spread across the same radius * ||u_k|| denominator the first-order term uses -- the sum is the bound, and only the sum is claimed. Weights are computed at one reference trajectory, so the guarantee is local to it; chc.uncertainty.confounding_cost_bound_certificate measures where it holds.

total_cost_diffrax(dyn, x0, us, dt, cost, rtol=1e-06, atol=1e-09, adjoint=None)

Discrete control cost J with each interval integrated by an adaptive diffrax solver.

Same objective and zero-order-hold control as chc.cost.total_cost, but each step x_k -> x_{k+1} is an adaptive (stiff-capable) Tsit5 solve of dx/dt = dyn(t, x, u_k), not the fixed-step RK4 of chc.integrate.rollout -- accuracy set by (rtol, atol), and the continuous BacksolveAdjoint available (via control_gradient_diffrax).

control_gradient_diffrax(dyn, x0, us, dt, cost, rtol=1e-06, atol=1e-09, adjoint=None)

dJ/du through the adaptive diffrax solve -- the continuous-adjoint sibling of the discrete control_gradient_adjoint. Pass adjoint=diffrax.BacksolveAdjoint() for the memory-light optimise-then-discretise continuous adjoint; the default backprops the solver.