Skip to content

chc.support

Stability tier: evolving.

Support / pessimism: keep offline-trained control inside the region the data justifies.

SupportModel scores how far a state-action pair (x, u) sits from the offline data cloud (squared Mahalanobis distance D); pessimistic_control penalises leaving that support, so the controller does not exploit the model where it was never trained. This is the offline-safety layer: the objective is J_task + λ_unc·Σ U + λ_supp·Σ D, where the calibrated predictive-uncertainty term U comes from chc.uncertainty (deep ensemble / conformal) and this module supplies D and the controller that combines them through the PenaltyModel.

PenaltyModel

Bases: Protocol

Anything that scores a trajectory penalty penalty_trajectory(xs, us) -> scalar.

Both SupportModel (density distance D) and the chc.uncertainty scorers (calibrated predictive uncertainty U) satisfy it, so they are interchangeable penalty channels.

SupportModel

Bases: Module

Gaussian support of the offline (x, u) cloud; scores squared Mahalanobis distance.

fit(xs, us, ridge=0.001) classmethod

The log's mean and the inverse of its covariance, each variance raised by ridge of itself.

Raising each variance by a share of itself shrinks the correlations and counts the distance in each coordinate's own standard deviations: a point k of them off the log, along a coordinate the log moved apart from the others, scores about k**2 / (1 + ridge) in any units and from any origin, and coordinates the log moved in step keep a finite distance across their line. A ridge added in the caller's units outweighed a coordinate logged in small ones: at 1e-3 of its units, a point 4 standard deviations off scored 0.016 where it scored 16.0.

Raises:

Type Description
ValueError

on a coordinate the log never moved, or moved by rounding alone (a spread about its mean of at most 64 eps of its root mean square, eps its precision's), whose distance is unbounded in any units: a finite stand-in would be set by the units it was logged in, or by its rounding. And on a coordinate whose spread the precision cannot hold in the caller's units, where the inverse of its square overflows or underflows the precision's dtype and every point would read as off the support, or on it.

penalty_trajectory(xs, us)

Total off-support penalty over the visited (x, u) pairs (xs: (H,n), us: (H,m)).

pessimistic_solve(model, x0, us0, dt, cost, support, lam_supp, u_lo, u_hi, steps=10000, lr0=1.0, tol=1e-14, uncertainty=None, lam_unc=0.0, *, constraints=())

pessimistic_control, returning why it stopped as well as where.

The stationarity residual is of the augmented objective, not the task cost: the penalties are what the descent actually minimised, so a residual measured on the task alone would be non-zero at the very point the solver was right to stop. With constraints it projects onto the box and the rows together, as chc.control.projected_gradient_solve does. Under the confounding radius with no constraints it is ||u - prox(u - grad f)||, f the augmented cost less the radius' norm and prox the norm's proximal map with the box: zero exactly where the unsmoothed problem is stationary, where the smoothed norm's gradient reads zero at an action on the kink whatever the cost's gradient there. The residual is in the caller's units, so it is zero at a KKT point in any units but its size is not.

pessimistic_control(model, x0, us0, dt, cost, support, lam_supp, u_lo, u_hi, steps=10000, lr0=1.0, tol=1e-14, uncertainty=None, lam_unc=0.0, *, constraints=())

Projected-gradient OC with an offline-safety penalty λ_supp·Σ D + λ_unc·Σ U.

support supplies the density-distance term D; the optional uncertainty scorer gives the calibrated predictive-uncertainty term U (a chc.uncertainty ensemble/conformal). Uses autodiff for the augmented-objective gradient (validated equal to the discrete adjoint in 01 §4.1). Returns the optimised controls and the task-cost history (penalties excluded, so runs at different weights are comparable).

u_lo / u_hi take the same scalar, per-lever or full-schedule forms, and constraints the same linear rows, that chc.control.projected_gradient_control accepts.

The descent is the planner's, so a problem is planned the same in any units of each lever and of the cost and penalties together. It runs in v_kj = sigma_kj u_kj: sigma_kj^2 starts as the cost's Gauss-Newton curvature along lever j at step k alone, over the augmented cost at the guess clipped to the box, and after each accepted step it is raised to the secant of the augmented cost's curvature along that action over the step: a penalty's curvature is measured where the plan is, not where the guess was.

The confounding radius (chc.uncertainty.ConfoundingRobustPenalty) has a kink at every u_t = 0, where a plan holds the actions it does not use. Its norm is not differentiated: the descent minimises it unsmoothed, read in the cost's precision, and takes it by its proximal map with the box at each step, a forward-backward step whose line search asks a third of the fall the smooth part's gradient and the norm predict together. A plan holds an unused action at exactly zero, and from a zero guess the search finds a step wherever the proximal map moves it and the step lowers the cost by more than tol asks; in float32 the descent no longer stops at the kink's rounding (docs/adr/0078-the-radius-norm-is-taken-by-its-proximal-map.md). Under constraints the rows' projection is not the norm's proximal map, and the descent takes the smoothed norm's gradient as before: a zero guess on the kink may take no step there.

  • lr0 is the first step the line search tries, in v: 1 is the Newton step along each action alone. It is halved until the augmented cost falls by a third of what the gradient predicts for the step; where no halving does, the longest step that lowers it is taken.
  • tol is relative: a step counts where it lowers the augmented cost by more than tol times its value at the clipped guess, and from a guess where it is zero any decrease counts.

Both used to be absolute, in the caller's units: lr0 a step of 0.2 action units per unit of gradient and tol a fall of 1e-9 in the cost's own units, so with a lever in units a thousand times its own the descent ran out of steps, and in a million times took none (docs/adr/0073-the-penalised-descent-reads-its-penalty-as-it-goes.md). A fall eps in the cost's own units is tol = eps / |F|, with F the augmented cost of the guess clipped to the box.