Skip to content

chc.plan

Stability tier: stable.

The one-call spine: plan a control sequence and get its guarantees attached to it.

Everything here already existed -- constrained OC (chc.control), the offline pessimism and uncertainty penalties (chc.support, chc.uncertainty), and the certified Gronwall error tube with its safe horizon. What was missing was a single object that carries a plan together with the evidence about where it may be trusted, so a caller cannot walk away with the actions and leave the certificate behind.

plan = causal_plan(model, x0, cost, dt=0.1, horizon=20, u_lo=-5.0, u_hi=5.0,
                   lipschitz=0.8, model_error=0.05, tolerance=0.5)
plan.actions              # the full sequence
plan.certified_actions    # only the prefix whose error tube is inside tolerance
plan.certified_horizon    # where that prefix ends
plan.shadow_prices()      # what each constraint row is worth to the plan

With no safety arguments this is exactly chc.control.projected_gradient_control with the trajectory and cost packaged; each safety argument switches on one existing layer, so the defaults promise nothing that was not asked for.

Three modes, deliberately named apart, because "safety" alone does not say which one you get:

  • plan -- causal_plan. Box constraints in the solve -- optionally intersected with linear rows over the whole sequence, such as a budget or a rate limit -- plus an a-priori Gronwall error tube that says how far ahead the plan may be trusted. The tube is computed from lipschitz and model_error, and from the plant's own one-step noise where it is given; it does not enter the objective and does not move a single action. A BarrierConstraint does: passed as barrier=, the audit's own condition is held in the solve by augmented-Lagrangian rounds (docs/adr/0002-barrier-in-the-solve.md).
  • audit -- certify_safety. Given a barrier and a sensitivity level Gamma, it prices a finished plan against §40: where along it the safety guarantee survives unmeasured confounding, and the largest Gamma the whole plan tolerates. Read-only by construction.
  • filter -- chc.barrier.robust_safety_filter. It clips one nominal action into the certified interval at one state, online, scalar control.

A held barrier binds the answer, not every iterate as the box and the rows do, and the solver's word is not taken for it: a solve stopped by its budget, or a condition no admissible action can meet, comes back short of the condition, and CausalPlan.safety -- the audit, run on the finished plan -- says so. The tube is still never imposed inside the optimisation, so a plan can leave tolerance at step 3; that is why CausalPlan.certified_actions exists.

DiagnosticStatus = Literal['within', 'beyond', 'not_finite', 'not_evaluated'] module-attribute

How a plan's stationarity or constraint violation reads against its tolerance: within it, beyond it, not_finite where the number is nan or infinite, as where the rollout left the float range, and not_evaluated where it was not read: a plan built by hand, or the stationarity of a plan held under a barrier, whose condition's multipliers it would need.

CostBiasDirection = Literal['optimistic', 'pessimistic', 'unbiased'] module-attribute

Which way a planned cost reads against what the plan pays the plant, in expectation: optimistic where it reads low, a positive bias, and pessimistic where it reads high.

Barrier = Callable[[Array], Array] | tuple[Callable[[Array], Array], ...] module-attribute

h, safe where h >= 0; or a tuple of them, safe where each is, each its own condition.

ModulusSource = Literal['supplied', 'measured'] module-attribute

Where PlanRegretBound.modulus comes from: the caller, or J's Hessian. Measured, it is the one Hessian of an objective the model's structure makes quadratic, which is the whole box's, or the least eigenvalue at the plan and at random points of the box, a sample that cannot bound the box's least; PlanRegretBound.status tells the two apart.

RegretStatus = Literal['certified', 'diagnostic', 'refused'] module-attribute

What PlanRegretBound.bound is. certified on a supplied modulus or a quadratic objective's; diagnostic on a sampled modulus, which a pocket of negative curvature between the samples breaks; refused, the bound inf, where the modulus came out negative or not a finite number, or the bound itself did not come out finite.

CausalPlan dataclass

A control sequence together with the certificate that says how far to trust it.

None in the certificate fields means no error model was supplied, which is a different statement from a certificate that came back empty: the first is "unknown", the second is "checked, and nothing holds". Earlier versions returned an all-zero tube and a full certified_horizon in the first case, which reads as a proof of safety over the whole plan when nothing was proved at all. Read certificate_status before either field.

solver_status answers a different question from certificate_status, and the two are independent. The certificate is about the model: how far the plan can be trusted given the error budget. The solver status is about the optimisation: whether the descent reached its own stopping rule or merely ran out of budget. A fully certified plan built on an unfinished solve is a trustworthy tube around a suboptimal action, and nothing in the tube says so.

shadow_prices answers a third: what each constraint row costs the plan, read off the problem the plan was solved for.

certificate_status property

Whether the tube was evaluated at all, and if so how much of the plan it covers.

Derived rather than stored: a status field that can disagree with the horizon it summarises is a worse footgun than the one this replaces.

stationarity_status property

How stationarity reads against stationarity_tolerance.

stationarity is the plan's KKT residual (ADR 0097). The descent steps in v = sigma * u (ADR 0059), where sigma_kj^2 is the cost's curvature along lever j at step k over |J| at the guess, zeros clipped to the box: a pure number in any units of the lever and the cost. There the gradient is read as a share of |J|, each row is divided by its norm, and the residual is the least norm of the gradient plus a push of the binding rows and box sides with the signs their bounds allow, over max(1, |gradient on the free actions|): zero exactly at a KKT point, positive where a multiplier has the wrong sign. Off the constraints its square is about twice the share of |J| that one more Newton step along each action alone would buy. What binds is shadow_prices' rule, read in v.

The tolerance is what the descent can see: it stops where no step buys more than max(1e-14, 4 eps) of |J|, eps the actions' precision, which leaves a residual of about the root of twice that; ten times it allows for a curvature off one along the residual. Where the curvature is further off, as under a steep penalty, the descent reads this residual itself and stops only within the tolerance, or where the gradients at the ends of every trial read no fall either. beyond it, the descent stopped on its budget or short of the optimum, and the plan, its prices and its regret are a stopped solve's. Not read (not_evaluated) on a plan held under a barrier, whose condition's multipliers the residual would need.

violation_status property

How constraint_violation reads against violation_tolerance.

constraint_violation is the largest distance in v, the variables of stationarity_status, from the plan to a side of its box or to a bound of one of its constraints rows that it is past: a row's excess over the row's norm in v. Zero where the plan meets every one. Its tolerance is the one the descent's projection holds each side and row to, 256 eps (1 + max|v|), times four for the plan's own rounding and a projection started from another guess. The barrier's condition is not among them: safety audits it.

certified_actions property

The prefix of the plan the tube still covers -- what a cautious caller should execute.

Raises:

Type Description
ValueError

if no error model was supplied. Slicing by None would hand back the whole sequence, and an empty one would claim the plan had been checked and failed; neither is true, so the question has no answer to return.

shadow_prices(tolerance=None)

What each row of the plan's constraints is worth: its KKT multiplier at this plan.

The gradient is of the objective the solve minimised -- the task cost, plus the pessimism penalties when a support model was given -- at the returned actions. A row binds within tolerance * (1 + |bound|) of its bound, and so does a box bound. The box's multipliers absorb every action at a bound, so the rows' multipliers solve the stationarity conditions on the free actions alone, by least squares. Where the binding rows are dependent there, the multiplier is not unique, and two linear programs give each row's range instead.

Read residual and dual_feasible before the prices. A solve stopped by its budget is not at a KKT point, and its prices answer a question that has none. A price is the marginal fall in the optimum per unit its row is relaxed: of the optimum, on a plan convex in its actions -- a linear model and a quadratic cost -- and of the local optimum the solve found otherwise. These are the stated rows' multipliers, not the barrier rounds' that chc.mpc.RecedingHorizon carries between steps.

Computed when asked: one gradient, a least-squares solve and, for a degenerate row, two linear programs. tolerance defaults to max(1e-6, 1e3 * eps) of the actions' dtype.

Raises:

Type Description
ValueError

on a plan held under a barrier, whose condition is a constraint of its own, with multipliers that would enter every row's price; or on a plan that carries no problem, as one built by hand does.

decision_weight(tolerance=None)

What an error in the one-step channel costs this plan, to second order. Experimental.

If the plant's one-step map is the planning model's plus E u, each entry of the channel off by E as chc.gate.channel_drift_evalues watches them, the plan loses vec(E)' W vec(E) / 2 against the plan that knew E, up to a term cubic in E. By the envelope theorem W = J_Eu M^-1 J_uE, with M the Hessian of the task cost in the directions the plan may move and J_uE its mixed derivative in those directions and E. The directions are the actions off the box, along the binding rows, with shadow_prices' rule for what binds. u is the action the field takes: a lever chc.decision.prescribe keeps on the log's rule of the state enters at its rule's level, not at its column of actions.

W is the regret's curvature at E = 0, and three things bound how far it reaches:

  • The active set. W holds while the bounds and rows that bind stay binding. At one met with a zero multiplier, counted in weakly_active, the regret is piecewise quadratic, and W, with the bound held, is its lower branch: freeing a direction can only raise it.
  • The cubic term. On the scalar plan of validation/plan_decision_weight.mac, whose one-step channel is 0.117, the regret's third derivative in E is -280 where W is 9.2, and the quadratic is within 10% of the exact regret only for E in [-0.0096, 0.0102], about 8% of the channel.
  • A stationary plan. residual is the stationarity miss along the free directions. A solve stopped by its budget carries a first-order term that W does not.

W vanishes where the plan's response to the channel is flat, as on one step at r = gamma^2 qf, and the regret there is fourth order, not nil.

Computed when asked, in the plan's precision: one Hessian of the task cost in the actions, (H m)^2 entries, and one mixed derivative. tolerance is shadow_prices'.

Raises:

Type Description
ValueError

on a plan held under a barrier, whose condition moves with the channel; on one solved with pessimism penalties, whose regret on the task cost has a first-order term; on one that carries no problem, as one built by hand does; or where M is not positive definite, so the plan is not a strict local minimum and its regret is not quadratic in E; or where the task cost's derivatives at the plan are not finite.

planned_cost_bias(covariance, *, error_mean=None, tolerance=None)

How far the plan's own cost reads from what it costs the plant, in expectation over the estimate's error, and the cost corrected for it. Experimental.

The plan was made on an estimate of its one-step channel, off the plant's by e with covariance = S = E[e e'], e the estimate less the plant's channel, raveled as decision_weight ravels it. Its planned cost, the task cost on the estimate, misses what the plan pays on the plant by

E[J_true - J_model] = tr(W S) - tr(J_ee S) / 2 - J_e . E[e]

to second order (validation/planned_cost_bias.mac), with W the decision weight, J_ee the task cost's Hessian in the channel at the fixed plan and J_e its gradient. The first term is the optimiser's curse and is never negative: the plan fits its estimate's error. The second is the curvature of the planned cost at the fixed plan, never positive where that cost is convex in the channel. So the sum has no fixed sign. On one step of x1 = a x0 + b u at the cost q x1^2 + r u^2 it is a^2 q^2 x0^2 (2 r^2 - 5 b^2 q r + b^4 q^2) / (r + b^2 q)^3 per unit of S, which is negative, a planned cost that reads high, for q b^2 / r between (5 - sqrt 17)/2 = 0.438 and (5 + sqrt 17)/2 = 4.562, and positive outside. The third term is the estimate's own bias, error_mean, 0 where it is not given; a cross-fitted channel's is of the order of S.

Each term is read at the plan and the estimate, not at the plant's channel, which the expansion is about: the difference is of higher order. It holds while the plan's active set does not move with the error, so a plan that meets a bound or a row with a zero multiplier is refused; and as far as W does, for an error small against the channel (see decision_weight). residual is the stationarity miss along the free directions: a solve stopped by its budget carries a first-order term that W does not.

chc.misspecification.planned_cost_bias reads S off a fitted channel's influence, over every parameter the fit holds, the drift's included.

Raises:

Type Description
ValueError

on a covariance that is missing, not a finite (n m, n m) matrix, or not symmetric positive semidefinite; an error_mean that is not a finite (n, m) matrix; a plan that meets a bound or a row with a zero multiplier; and whatever decision_weight refuses.

RowPrice dataclass

What one constraint row is worth to the plan: how much its objective falls per unit the row is relaxed -- an upper bound raised, a lower bound lowered.

status says how far price can be read:

  • exact -- the row binds, and the binding rows are independent on the actions the box leaves free (LICQ), so the multiplier is unique.
  • weakly_active -- the row sits at its bound with a multiplier of zero: relaxing it buys nothing to first order, and tightening it costs nothing to first order.
  • degenerate -- the binding rows are dependent on the free actions, so the multiplier is a set: price is None and interval its range. Relaxing the bound buys interval[0] a unit and tightening it costs interval[1], both widened by the stationarity slack.
  • inactive -- the row has room, and is worth nothing at the margin.

An equality row's price takes either sign: the fall per unit its value rises.

ShadowPrices dataclass

The rows' prices at a plan, with what it takes to trust them.

See CausalPlan.shadow_prices.

DecisionWeight dataclass

What an error in the one-step channel costs a plan, to second order. Experimental.

See CausalPlan.decision_weight.

regret(change)

vec(E)' W vec(E) / 2 at change = E: what the plan loses, to second order, against the plan that knew its one-step channel was off by E.

Raises:

Type Description
ValueError

on a change that is not a finite channel_shape matrix.

PlannedCostBias dataclass

A plan's planned cost, its bias against what the plan pays the plant, with its sign, and the cost corrected for it, to second order in the estimate's error. Experimental.

See CausalPlan.planned_cost_bias.

bias property

E[J_true - J_model]: what the plan pays the plant less what it reads, in expectation; positive where the planned cost reads low.

corrected_cost property

The planned cost plus its bias: what the plan pays the plant, in expectation.

direction property

Which way the planned cost reads (CostBiasDirection).

BarrierConstraint dataclass

grad h . xdot >= -alpha * h at every planned step, against every effect the data allow.

The condition certify_safety audits, handed to causal_plan to hold in the solve rather than to price after it. barrier is h -- safe where h >= 0, a JAX-traceable scalar function; alpha is the class-K gain; gamma and cvar_gap set the identification radius (gamma-1)/(gamma+1) * cvar_gap * ||B_hat(x0)||_2 on the effect, cvar_gap a share of the channel's operator norm at the plan's start, so the condition is the worst case over the identified set and gamma = 1 is the ordinary CBF condition. The defaults and the calibration burden on cvar_gap are certify_safety's.

Several bounds are a tuple of barriers, safe where every one is >= 0, and each holds its own condition at every step. Their least is no substitute: its condition jumps where two tie, reading on each side that side's gradient, so a solve that ends on the tie cannot step off it, and the condition it holds asks nothing of a bound until that bound is the nearest.

u_max, one number per lever, each lever's largest action in its own units, reads the condition in units of each lever's own largest action, as certify_safety does with the same numbers, and the audit after the solve reads them; None reads it in the levers' own units, one ball for all of them, as before. chc.prescribe passes its box's. A lever whose number is 0 must have a box that holds it at 0.

Raises:

Type Description
ValueError

on a gamma below 1 or not finite, which is not a sensitivity level, a cvar_gap not positive or not finite, which scales no radius, a u_max with an entry negative or nan, or none positive, which is no action's size, or an empty tuple of barriers, which holds nothing -- here, rather than after the solve that would have used them.

SafetyCertificate dataclass

§40 evaluated along a finished plan: how much confounding its safety guarantee survives.

Two questions that must not be conflated, so both are reported:

  • planned_certified -- does the action the planner actually chose still clear the barrier once the adversary moves the effect inside the identified set? This is about this plan.
  • gamma_star -- does any admissible action clear it, and up to which sensitivity level? This is about the problem, and is the number an operator can act on. A plan can fail the first while the second is comfortable, which says the planner, not the confounding, is at fault; the reverse says no controller would have helped.

PlanRegretBound dataclass

How far a finished plan can be from the best one the same box allows (Result 69, L3.2).

Result 6's self-certifying bound J(U) - J* <= |grad J|^2/(2 mu) needs no optimum, which is what makes it a certificate rather than a diagnostic. It does not survive a box: at a lever the gradient holds against its own bound grad J is nonzero while the true regret is zero, so that bound charges regret to a coordinate that cannot move. Result 13 read the same fact from the other side -- an active cap freezes the control and the regret's curvature collapses.

What replaces it is the same quantity maximised over the feasible moves only. Per coordinate the certified gain of a move d is -g d - (mu/2) d^2, and its maximum over [lo - U, hi - U] is the unconstrained one minus a perfect square (validation/constrained_plan_regret.mac STEP 3a): never larger, exactly zero at a lever the gradient pins, and still finite as mu -> 0, where it degrades to frank_wolfe_gap rather than to infinity.

The moves are measured lever by lever, each in its own scale (STEP 8 of the same file): the modulus is the curvature in the norm |D^-1 d|, D one scale per lever that moves with that lever's units, and a move's curvature along lever j is mu / s_j^2. In the plain norm the modulus is the cheapest lever's curvature, which charges a stiff lever's moves at a cheap one's, and the bound read the levers' units: lever u1 in units 1e3 times its own took one plan's bound from 0.0266 to 0.0576, where it now reads 0.0214 in both. Measured, the scales come from the Hessian's diagonal (_objective_modulus); a supplied modulus is the plain norm's, every scale 1.

The bound is on the planning objective, which is the question the solver was asked. How far the planning model itself is from the plant is the error tube's question (CausalPlan.uncertainty_tube), and the two must not be added.

It is a certificate only where the modulus is one for the whole box: supplied, or read off an objective the model's structure makes quadratic (status). A modulus sampled at the plan and a few points of the box is not: a pocket of negative curvature between the samples can hide a plan much cheaper than this one, and the bound then reads small with nothing behind it.

causal_plan(model, x0, cost, dt, horizon, u_lo, u_hi, *, support=None, lam_supp=0.0, uncertainty=None, lam_unc=0.0, lipschitz=0.0, model_error=0.0, noise=0.0, noise_persistence=0.0, tolerance=float('inf'), steps=10000, constraints=(), barrier=None, warm_start=None)

Plan under box constraints, optional offline pessimism, and a certified error tube.

Parameters:

Name Type Description Default
support SupportModel | None

offline (x, u) support model; supplying it switches the solve from plain projected-gradient OC to chc.support.pessimistic_control.

None
uncertainty PenaltyModel | None

a PenaltyModel (ensemble, Wasserstein, confounding radius) weighted by lam_unc. Requires support -- the pessimistic solver evaluates both terms.

None
lipschitz, model_error

feed the error tube of the plan's RK4 rollout, the field off by at most model_error wherever a step reads it. lipschitz is either a norm-Lipschitz bound L >= 0 on the field in the state, for the recursion e_{k+1} = e_k + dt (L e_k + eps) phi(L dt), phi(z) = 1 + z/2 + z^2/6 + z^3/24 (chc.uncertainty.time_varying_rollout_bound), or, for a field affine in the state, its (n, n) state matrix A, whose RK4 propagators carry the tube (chc.uncertainty.linear_rollout_bound) and shrink it where A contracts. model_error or noise is what switches certification on: with neither the tube would be identically zero, so the plan reports certificate_status == "not_evaluated" and both certificate fields come back None rather than a vacuous full-horizon pass.

required
noise, noise_persistence

the plant's own error at each step, which no model error covers: w_k = x_{k+1} - step(x_k, u_k), the plant's next state less the plan's RK4 step of the model from the plant's state, Gaussian, AR(1) from one step to the next, Cov(w_i, w_j) = noise_persistence^|i - j| noise. noise is its covariance in the state's units, a number for each state's variance with none shared, or an (n, n) matrix; noise_persistence its lag-1 correlation, in [-1, 1]. The tube adds to the model error's radius the one within which the noise's part of the gap stays at each step with probability 0.95, carried through the same lipschitz: through R(dt A) on a state matrix, exactly to first order, and on a norm bound L through R(L dt), which bounds it (validation/predictive_tube.mac). On a state matrix the gap's covariance is exact, and the radius is the exact 0.95 quantile of the gap's distance from the plan, a sum of the covariance's eigenvalues times independent chi2_1 (Imhof 1961). On a norm bound it is that quantile at the first step, where the covariance is noise itself, and past it sqrt(chi2_n(0.95)) times the root of the bound on the largest variance along any direction, which holds with probability 0.95 on one state and more on several. Up to 0.17.0 that was the radius on a state matrix too, up to sqrt(chi2_n(0.95) / chi2_1(0.95)) too wide where one direction's variance dominates, 1.249 times on two states. Zero by default, which leaves the tube the model error's alone, as before.

required
tolerance float

tube radius above which the plan stops being certified.

float('inf')
constraints Sequence[LinearConstraint]

linear rows over the whole action sequence (LinearConstraint), held by every iterate of either solver, not only by the answer.

()
barrier BarrierConstraint | None

a condition to hold at every planned step (BarrierConstraint), or one per barrier of a tuple, by augmented-Lagrangian rounds around the same descent. Unlike constraints it binds the answer, not every iterate, and the plan comes back with CausalPlan.safety -- certify_safety run on the finished plan, the verdict to read. Its gamma_star is priced at the largest magnitude any lever may take, the budget chc.prescribe uses. A barrier the unconstrained plan already clears changes no action, even when that solve stopped on its budget. steps then caps each descent, of which there are at most 1 + _BARRIER_ROUNDS.

None
warm_start Array | None

the (horizon, m) actions the descent starts from, zeros when omitted -- typically the last plan shifted one step, which chc.mpc.RecedingHorizon passes along with the barrier's multipliers. It is projected onto the box and the rows before the first step, so it need not be admissible. It moves where the solve starts, and so what a solve stopped by steps returns; a solve that converges on a problem with one minimiser lands on it from any start. Omitted on a model holding a chc.response.Saturation that starts convex, the start is instead the plan of the same problem on each such curve's concave envelope (chc.response.relax), and the plan reports that problem's cost as CausalPlan.relaxed_cost: such a curve has no slope at zero spend, so zeros are a stationary point the descent would stop at, reporting convergence.

None

Raises:

Type Description
ValueError

if an uncertainty penalty is given without a support model, which would silently drop it -- the pessimistic solver is the only consumer of that argument; if model_error is negative, infinite or nan, which is not an error budget; if lipschitz is a negative, infinite or nan number, which no norm of a field's slope is, or neither a number nor a square matrix of the state's size; if noise is not finite, not a number or a matrix of the state's size, or not a covariance, which is symmetric with no negative eigenvalue, or noise_persistence is not in [-1, 1]; if tolerance is negative or nan, or dt is not a finite positive step; if a barrier is given with a box that admits no action but zero, which leaves nothing to price it with; or if warm_start is not a finite (horizon, m) array.

certify_safety(plan, model, barrier, dt, *, alpha=1.0, gamma=1.0, cvar_gap=1.0, u_max=None)

Price a plan's barrier guarantee against a partially identified control effect (§40).

The plan was made under a point estimate of the effect. If that estimate came from confounded logs the effect matrix is only set-identified, and given an operator-norm identification radius ||B_hat - B||_op <= Delta the channel w = B^T grad h is pinned within d = Delta * ||grad h||, so the guaranteed barrier derivative at action u drops to a + <w, u> - d*||u|| (chc.barrier). This walks the nominal trajectory and reports where that guarantee survives.

Parameters:

Name Type Description Default
barrier Barrier

h(x), safe where h >= 0. Differentiated with jax.grad, so it must be a JAX-traceable scalar function. A tuple of them is audited each on its own and read as one: a step is certified where every barrier's condition holds there, and the step's barrier_values are the least barrier's, its guaranteed_derivative and required the pair of the barrier with the least room, and its step_gamma_star the least of theirs -- an upper bound rather than the step's own ceiling, since each is reached by its own best action and no one action need reach the least.

required
alpha float

the class-K gain in grad h . xdot >= -alpha*h.

1.0
gamma, cvar_gap

the marginal sensitivity model's level and the gap it scales, combined into Delta = (gamma-1)/(gamma+1) * cvar_gap * ||B_hat(x0)||_2: cvar_gap is a share of the model's channel, its operator norm at the plan's first state, so the radius is in the channel's units and gamma_star reads the same in any units of the state or of the levers. The default, 1, is the channel's own size, the calibration cvar_gap := b_hat of chc.sensitivity. gamma = 1 is exact identification -- zero radius, and this degenerates to the ordinary CBF check. NOTE the calibration burden: Delta is used here as an operator-norm radius on the effect matrix, and §32's scalar CVaR gap does not by itself establish one. The share is the caller's externally calibrated input, exactly as gamma is; an absolute radius r is the share r / ||B_hat(x0)||_2.

required
u_max ArrayLike | None

actuation limit, used for gamma_star -- that is the best admissible action question, which needs a budget the plan's own choice does not define. Defaults to the largest action norm the plan actually uses, answering the narrower "how much confounding does this plan's own authority tolerate". One number per lever, each lever's largest action in its own units, reads the whole audit in units of each lever's own largest action: the action set is the ellipsoid ||D^+ u|| <= U whose semi-axes are those numbers, U the largest and D each over U, the radius Delta is on (B_hat - B) D and sized by ||B_hat(x0) D||_2, and an action's norm is ||D^+ u||. That reads the same in any units of each lever, where one number reads the levers' units as one: on a log of one state and two levers in [-2, 2], either lever logged in units 1e3 or 1e-3 times its own, its box with it, the barrier's certified steps fell from 3 to 0. One lever, or levers whose numbers are equal, read the ball of radius U they read with u_max = U, bit for bit. A lever whose number is 0 has no action in the set, and the plan must not move it. A number inf, a free side's, leaves no units to read that lever by, and the audit reads the levers' own units, as u_max = inf does.

None

Assumes the plant is control-affine and the uncertainty set isotropic (see chc.barrier; d*||u|| is exact for a Euclidean ball, an outer bound otherwise), and audits the model's trajectory pointwise. Forward invariance (Nagumo/Brezis) is assumed as throughout the CBF literature; what is certified is the pointwise condition.

Raises:

Type Description
ValueError

if cvar_gap is not positive or not finite, which would make the radius meaningless and chc.barrier.barrier_gamma_star uninvertible, or the channel at the plan's first state is not a finite number; if gamma is below 1 or not finite, which is not a sensitivity level; if the actuation budget backing gamma_star is non-positive, which asks how much confounding a controller with no authority tolerates; or if u_max per lever has an entry negative or nan, none positive, not one per lever, or a 0 at a lever the plan moves.

plan_regret_bound(plan, model, x0, cost, dt, u_lo, u_hi, *, modulus=None, probes=16, seed=0)

RESULT 69 (L3.2) -- the optimality gap of a finished plan, certified from its own gradient.

Derived in validation/constrained_plan_regret.mac, proved in proofs/constrained_plan_regret.v. Reads the plan's actions, not the solver's internals, so it prices a plan that came from anywhere -- including one an operator edited by hand.

Parameters:

Name Type Description Default
modulus float | None

the strong-convexity modulus of J over the box, which makes the bound a certificate. None reads it off J's Hessian (see _objective_modulus): once at the plan where the model is linear, the Hessian then being one matrix, and otherwise at the plan and probes random points, a sample that cannot certify. A smaller modulus gives a larger bound and is never invalid (STEP 4d), so a conservative one is the safe input; for a linear plant with positive semidefinite Q and Qf, lambda_min(R) is always valid and needs no eigenvalue solve on J. A supplied modulus is the plain norm's, in the levers' own units, and the bound then reads those units; read off the Hessian, each lever's moves are in its own scale.

None
probes int

random feasible points added to the curvature sample when modulus is sampled.

16

The three numbers to read together: bound is what the box certifies, unconstrained_bound is what Result 6 would have reported at the same plan, and frank_wolfe_gap is what survives if the modulus goes to zero. status says what the bound is: certified on a supplied modulus or a linear model's, diagnostic on a sampled one, and refused -- the bound inf -- when the modulus is negative, because then no convexity argument applies and a finite number would be a fabrication, and when the modulus or the bound is not a finite number.

Raises:

Type Description
ValueError

if a supplied modulus is negative, infinite or nan, which is not a curvature.