chc.uncertainty¶
Stability tier: evolving.
Calibrated pessimism: a deep-ensemble residual whose disagreement, conformalised for coverage,
scores predictive uncertainty for the offline-safety penalty (its U term).
chc.support scores density distance D((x,u),D); this module supplies the complementary
calibrated predictive uncertainty U(x,u). Fit K residuals as a deep ensemble; their member
disagreement is the epistemic uncertainty of the learned dynamics (large where the members, trained
on the same offline data, extrapolate apart), and split conformal turns it into interval widths that
cover with probability at least 1 - alpha on data exchangeable with the calibration split, given
at least (1 - alpha) / alpha calibration transitions. Both plug into the same penalty channel of
chc.support.pessimistic_control via penalty_trajectory(xs, us) -> scalar, so model
exploitation is bounded, not merely discouraged.
A third scorer, WassersteinPenalty, targets the deployment-shift failure mode rather than
in-distribution epistemic spread: a Wasserstein-1 distributionally-robust margin
(radius * Sigma ||d r / d x||) that keeps control where a small shift of the state distribution
cannot move the learned dynamics much.
EnsembleResidual
¶
Bases: Module
Deep ensemble of residuals: K members whose mean is the field, whose spread is uncertainty.
Duck-types the Dynamics protocol -- __call__ returns the member mean, so it drops into
HybridDynamics.residual. disagreement is the total variance across members (epistemic
uncertainty of the learned part), ~0 where members agree and large where they extrapolate apart.
ShardedEnsembleCertificate
dataclass
¶
Evidence that the stacked, sharded ensemble fit is the serial per-member recursion.
EnsembleUncertainty
¶
Bases: Module
Predictive-uncertainty penalty U from an ensemble residual -- its member disagreement.
Mirrors SupportModel.penalty_trajectory so it plugs into the same lam_unc channel of
chc.support.pessimistic_control. Penalises visiting states/actions where the learned
dynamics are uncertain (members disagree) -- exactly where model exploitation happens.
penalty_trajectory(xs, us)
¶
Total member disagreement over the visited (x, u) pairs (xs: (H,n), us: (H,m)).
NestedCVaRPenalty
¶
Bases: Module
Time-consistent (nested) CVaR of the ensemble's disagreement, as a PenaltyModel.
EnsembleUncertainty sums the member variance along the trajectory, which is a risk
neutral aggregation: one very bad step averages away against many quiet ones. This replaces
the aggregation with a nested conditional risk measure, rho_t = c_t + CVaR_alpha[rho_{t+1}]
(Ruszczynski 2010; Shapiro-Dentcheva-Ruszczynski 2021), evaluated on the per-member deviations
||r_k - mean_k r_k||. Same penalty_trajectory(xs, us) -> scalar shape, so it drops into
the lam_unc channel of chc.support.pessimistic_control unchanged.
The distinction that matters is which adversary the number prices, and both are available:
penalty_trajectory(nested) -- the adversary re-picks the worstalphafraction of members at every step, so a model may be wrong here and a different one wrong there.static_penalty_trajectory-- the adversary must commit to one member for the whole horizon, then the tail is taken once over those horizon-summed scenarios.
Nested is therefore never smaller (subadditivity of CVaR,
Sum_t CVaR[c_t] >= CVaR[Sum_t c_t] -- a standard coherence property, not a result of this
repo), and the gap is the price of time consistency. Time consistency is the reason to pay it:
a plan optimal for the static measure at t = 0 need not be the plan you would re-choose at
t = 1 after seeing the first step, so a receding-horizon controller that re-solves -- which
is what chc.mpc.mpc_control does -- can chase its own tail. The nested measure has no
such gap by construction.
HONEST SCOPE: with the members re-evaluated independently at each visited pair, the recursion
collapses to Sum_t CVaR_alpha[c_t]; this is the aggregation rule made risk-averse, not a
dynamic-programming solve of a nested-risk MDP, and it does not certify a bound on the cost.
member_costs(xs, us)
¶
(H, K) per-member deviation from the ensemble mean field at each visited pair.
penalty_trajectory(xs, us)
¶
Nested: the tail is taken per step, so the worst members may differ across steps.
static_penalty_trajectory(xs, us)
¶
Static: one tail over horizon-summed per-member scenarios (time-INCONSISTENT).
NestedRiskCertificate
dataclass
¶
Numeric evidence that the nested measure dominates the static one, and by how much.
SplitConformal
¶
Bases: Module
Split-conformal calibration of the ensemble's next-state uncertainty (coverage guarantee).
Calibrated on a held-out split: the normalised nonconformity score is
s = ||x_next - mean|| / (sigma + eps) with sigma the ensemble predictive std; q_hat
is the k-th smallest of the n scores, k = ceil((n + 1)(1 - alpha)).
interval_width = q_hat*(sigma + eps) has marginal coverage >= 1 - alpha on data
exchangeable with the calibration split (measured by coverage). That needs k <= n, so
n >= (1 - alpha) / alpha: with fewer scores no finite width carries the guarantee, and
calibrate refuses rather than read the largest score, which covers n / (n + 1).
calibrate(model, data, dt, alpha=0.1, eps=1e-06)
classmethod
¶
Calibrate on data's transitions, held out from the ensemble's fit.
Raises:
| Type | Description |
|---|---|
TypeError
|
when |
ValueError
|
when |
interval_width(x, u)
¶
Calibrated prediction-interval half-width q_hat * (sigma(x,u) + eps) (scalar).
coverage(data)
¶
Empirical coverage on data: fraction of true next-states within the interval.
penalty_trajectory(xs, us)
¶
Total calibrated interval width over the visited (x, u) pairs (a penalty).
HorizonConformal
dataclass
¶
One conformal scale for every step of a horizon: a simultaneous band scale * r_k.
Experimental: it may change or be withdrawn in any release. Each calibration window is
scored by max_k ||x_{t+k} - x_hat_{t+k}|| / r_k over the horizon, r_k a fixed profile
such as the plan's tube (CausalPlan.uncertainty_tube[1:]), and scale is the
ceil((n + 1)(1 - delta))-th smallest of the n scores. Cleaveland, Lee, Pappas and
Lindemann (AAAI 2024, Theorem 3 and Corollary 1): on windows drawn i.i.d., a fresh window's
every step lies within scale * r_k with probability at least 1 - delta, and, its
scores tied with probability 0, at most 1 - delta + 1 / (n + 1). The profile and the
model must not be fitted on the calibration windows. Per step at 1 - delta each, the
steps together hold less; split delta / H per step (Bonferroni), they hold more than
needed.
On one series rather than i.i.d. windows, calibrate with mixing states
1 - delta - eta, eta from mixing_penalty, and refuses where eta >= delta.
What it covers: windows of the policy that logged the calibration windows, under that policy's actions. Not the plan's own actions, which move the plant elsewhere; and it moves no number of the tube, which it reads as a profile only.
band
property
¶
scale * r_k at each step: every step's radius at the joint level.
level
property
¶
The joint level the band holds: 1 - delta, less eta on one series.
calibrate(errors, profile, delta=0.05, *, mixing=None, train_lag=None)
classmethod
¶
Calibrate on errors, (n, H, states), one row per window as
horizon_errors returns them, against profile, one positive radius per step.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mixing
|
Callable[[int], float] | None
|
on windows cut from one series, |
None
|
train_lag
|
int | None
|
with |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
when |
covers(errors)
¶
Whether each window of errors, (W, H, states), lies within the band at every
step.
WassersteinPenalty
¶
Bases: Module
Wasserstein-1 distributionally-robust penalty on the learned residual.
Where SupportModel scores in-distribution density distance D and the ensemble scores
epistemic spread U, this scores robustness to a distribution shift of the states.
By Kantorovich-Rubinstein W1 duality the worst-case cost over a Wasserstein-1 ball of radius
radius equals the empirical cost plus radius times the cost's Lipschitz constant
(Gao-Kleywegt; Blanchet-Kang-Murthy; Shafieezadeh-Abadeh-Esfahani-Kuhn). The fragile quantity is
the learned residual r (the known physics f_known is trusted, hence excluded), so the
penalty is radius * Sigma_t ||d r / d x|| -- a gradient-norm margin that steers control away
from states where a small deployment shift moves the learned dynamics a lot. Satisfies the
PenaltyModel protocol, so it drops into the lam_unc channel of
chc.support.pessimistic_control unchanged; radius is the distribution-shift budget.
from_model(model, radius)
classmethod
¶
Penalise the learned residual of a hybrid model (f_known is trusted, so excluded).
local_lipschitz(x, u)
¶
Local Lipschitz constant of the residual in x: Frobenius norm of d r / d x.
penalty_trajectory(xs, us)
¶
W1-DRO margin radius * Sigma_t ||d r/d x|| over the visited (x, u) pairs.
ConfoundingRobustPenalty
¶
Bases: Module
Pessimism against a HIDDEN-CONFOUNDING effect error, for the general OC stack (§34/§38).
The §35 controller robustifies a scalar tracker; this lifts the same sensitivity radius into the
multivariate hybrid solver. Under hidden confounding the control-effect matrix B is only
partially identified: the offline estimate B_hat sits within a sensitivity half-width
radius (the §32 Delta(Gamma)) of the truth. By the §34 dimensional insight a per-step
control u_t then carries a per-step transition error
||Delta_B @ u_t|| <= radius * ||u_t||, and radius * Sigma_t ||u_t|| accumulates that
over a trajectory. Penalising it steers control away from exploiting a partially-identified
effect with large actions -- the confounding analogue of WassersteinPenalty's
deployment-shift margin. Satisfies the PenaltyModel protocol
(penalty_trajectory(xs, us) -> scalar), so it drops into the lam_unc channel of
chc.support.pessimistic_control unchanged.
HONEST SCOPE, and it now depends on which constructor was used. Plain
ConfoundingRobustPenalty(radius=...) is an identification-radius regulariser: the §34
inequality bounds the state-transition error, and converting that into a bound on the
objective needs the sensitivity multiplier Delta J <= Sigma_t L_{V,t+1} * radius * ||u_t||,
which this path does not supply -- so lam_unc absorbs it as an unidentified scale.
certified supplies it -- the adjoint norm ||lambda_{t+1}||, the RK4 injection gain
and a second-order deviation tube -- and on that path lam_unc = 1 is the bound rather than
a knob. confounding_cost_bound_certificate is the gate that can fail it.
Either way the COEFFICIENT is derived from the §32 sensitivity rather than being an arbitrary
actuation budget; Gamma and the CVaR-gap calibration remain the analyst's inputs, and it
does NOT test for confounding.
from_sensitivity(cvar_gap, gamma)
classmethod
¶
Radius = the §32 bounded-density-ratio inflation (Gamma-1)/(Gamma+1) * cvar_gap.
certified(radius, dyn, x0, us_reference, dt, cost)
classmethod
¶
Supply the missing multiplier, so lam_unc = 1 is the bound rather than a free knob.
The §34 inequality bounds the per-step transition error; turning it into a bound on the
objective needs Delta J <= Sigma_t L_{V,t+1} * radius * ||u_t||, and L_{V,t+1} is
locally the adjoint norm ||lambda_{t+1}||. That is a quantity the planner already
computes, so leaving it to be absorbed by lam_unc was throwing away information, not
avoiding an assumption. chc.adjoint.costate_norms returns it along us_reference.
The first-order term alone is not an upper bound, and measuring rather than assuming is
what showed it: at the optimum the Cauchy-Schwarz step is nearly tight, so the dropped
curvature term -- positive, O(radius^2) -- pushes the realised worst case above it at every
radius: an adversary reaches 1.014 of it at radius 0.005 and 1.69 at 0.2 on a two-lever
oscillator, while the weights below hold at 0.995 and 0.953.
chc.adjoint.perturbation_cost_weights therefore carries the deviation tube and the
cost curvature as well, which is why radius is an argument here: a second-order term
cannot be folded into a radius-free weight.
Three things this is and is not:
- Second order, exact for LQ. For a linear plant with a quadratic cost the objective is
exactly quadratic along a perturbation direction, so the expansion closes and the only
slack left is Cauchy-Schwarz. Nonlinear plants keep an
O(radius^3)remainder;confounding_cost_bound_certificatemeasures whether it matters. - At a reference. The weights are frozen at
us_reference, which breaks the circularity of weights that depend on the plan that depends on the weights. Iterating (re-solve, re-weight) is available to the caller and is not done here, because a fixed point is a different claim from a bound at a named trajectory. - Not a confounding test.
radiusstill comes from an assumedGamma.
penalty_trajectory(xs, us)
¶
Confounding pessimism radius * Sigma_t L_t ||u_t|| (xs unused).
L_t is 1 when no weights were supplied -- the identification-radius regulariser -- and
chc.adjoint.perturbation_cost_weights when they were, on which path the sum is a
certified upper bound on the cost gap rather than a scale-free direction.
LipschitzRolloutCertificate
dataclass
¶
Numeric evidence: the measured rollout deviation stays under the certified Gronwall bound.
ContractiveRolloutCertificate
dataclass
¶
Evidence a contractive residual has log-norm mu<0 and a flat (bounded) rollout radius.
TimeVaryingRolloutCertificate
dataclass
¶
Evidence the time-varying tube is a valid, tighter-than-constant envelope with a cutoff.
ConfoundingRobustCertificate
dataclass
¶
Evidence the closed-form MSM worst-case equals the brute-force sharp bound and behaves.
ConfoundingRobustClosedLoopCertificate
dataclass
¶
Evidence confounding widens the closed-loop tube and shortens the certified-safe horizon.
ConfoundingBoundCurve
dataclass
¶
Predicted vs adversarially realised cost gap, radius by radius. ok iff none exceeded.
GammaBenchmark
dataclass
¶
Gamma expressed in units of the confounding the OBSERVED covariates actually carry.
GammaBenchmarkCertificate
dataclass
¶
Falsifiable gates for benchmark_gamma and negative_control_gamma.
fit_ensemble(model, data, dt, n_members=5, width=16, depth=2, bootstrap=True, seed=0, steps=2000, lr=0.01)
¶
Fit K residuals into an EnsembleResidual (split keys + optional bootstrap), in parallel.
Each member is a fresh MLPResidual (dims inferred from data = {x, u, x_next}) trained by
the same Adam recursion chc.train.fit_residual runs. With bootstrap each member sees
a row-resample of the data, so disagreement reflects both init randomness and data variation.
Returns HybridDynamics(model.known, EnsembleResidual) and the per-member loss histories.
The members are trained together: one stacked parameter pytree, jax.vmap over the member
axis, jax.lax.scan over the Adam steps, and the stack sharded over the member axis of
_member_mesh. This is one XLA program rather than K x steps dispatches, and it is
the same recursion -- sharded_ensemble_certificate pins the agreement in ULP.
sharded_ensemble_certificate(seed=0, n_members=8, n_samples=96, parity_steps=60, dt=0.05)
¶
Check fit_ensemble against a serial oracle built from the untouched public trainer.
Two claims, each pinned by the measurement that can falsify it. Numerics: every member of the
stacked fit matches the member chc.train.fit_residual produces from the same key and the
same bootstrap rows, reported in ULP of the working dtype rather than as an absolute -- the
threshold is a precision claim, so it is stated in the unit precision actually has. Layout:
the member axis is genuinely distributed, so a stacked array committed to _member_mesh
and consumed by a jitted vmap comes back spanning the whole mesh; on a one-device host that
is trivially 1, and the subprocess test forces eight.
parity_steps is deliberately short. Adam on this loss is chaotic: past a few hundred
steps a one-ULP difference in reduction order is amplified without bound (measured 6.6 ULP
at 200 float32 steps, 7557 at 400), so a long-horizon parity threshold would be either
vacuous or flaky. The equivalence certified is of the recursion, which a short horizon
tests exactly.
cvar_upper(values, alpha)
¶
Upper CVaR (superquantile) of equally-weighted values at level alpha.
Mean of the worst alpha fraction, with the exact fractional-tail weight rather than a
rounded count, so it agrees with _top_tail_mean's convention and is continuous in
alpha. Differentiable: jnp.sort is a permutation, so this is a fixed linear functional
of the order statistics almost everywhere. alpha = 1 gives the mean, alpha -> 0 the max.
nested_risk_certificate(xs, us, ensemble, alpha=0.2)
¶
Check nested >= static >= risk-neutral, and that alpha = 1 collapses the three.
horizon_errors(model, states, actions, dt, horizon, *, stride=None)
¶
The (W, horizon, n) errors of the model's open loop over one unit's logged windows.
Window i starts at t = i * stride (stride defaults to horizon, so the windows
tile the log and each transition is in one) and runs the model's RK4 steps from the logged
x_t under the logged u_t, ..., u_{t + horizon - 1}. Its row k - 1 is
x_{t + k} - x_hat_{t + k}, the plant's state less the open loop's at step k.
HorizonConformal.calibrate scores them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
model
|
Dynamics
|
the field the plan rolls out, read as |
required |
states
|
ArrayLike
|
|
required |
actions
|
ArrayLike
|
|
required |
dt
|
float
|
the step. |
required |
horizon
|
int
|
the window's number of steps, at least 1. |
required |
stride
|
int | None
|
the steps between window starts, at least 1. |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
on states and actions of lengths that do not pair, a |
mixing_penalty(mixing, calibration, train_lag=None)
¶
eta, the coverage that split conformal pays on one beta-mixing series.
Oliveira, Orenstein, Ramos and Romano (JMLR 2024, Theorem 4): on a stationary beta-mixing
sequence, the score at a test index i falls within the empirical 1 - alpha quantile of
calibration consecutive scores with probability at least 1 - alpha - eta,
eta = eps_cal + eps_train + delta_cal, where eps_train = beta(i - n_train) and
eps_cal = inf over (a, m, r) of sigma(a) sqrt(4 L / (n - r + 1)) + L / (3 m) + (r - 1) / n
with L = log(4 / (delta_cal - 4 (m - 1) beta(a) - beta(r))), 2 m a = n - r + 1,
delta_cal > 4 (m - 1) beta(a) + beta(r) and
sigma(a) = sqrt(1/4 + (2 / a) sum_{j < a} (a - j) beta(j)). The theorem holds at every
delta_cal > 0, so this takes the least eta over every block (a, m, r) and a grid of
delta_cal: any point of it is a valid penalty, the least one the tightest found.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mixing
|
Callable[[int], float]
|
|
required |
calibration
|
int
|
|
required |
train_lag
|
int | None
|
the lag, in scores, from the last score the model or the profile was fitted
on to the test window, which adds |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
on fewer than 2 calibration scores or a |
gaussian_mixing(state_matrix, noise_covariance, *, stride=1, span=0)
¶
A bound on the beta-mixing coefficient of a stable Gaussian VAR(1) s' = F s + w,
w ~ N(0, Q), at a lag of j windows that start stride steps apart and each span
span steps: beta(j * stride - span), or 1 where the windows meet.
For a stationary Markov chain beta(k) is the mean over the stationary law of the total
variation from P^k(s, .) to it. By Pinsker's inequality and Jensen's, it is at most
sqrt(E KL / 2), and for this chain E KL = -log det(I - S^-1 F^k S F^k') / 2, S the
stationary covariance: beta(k) <= sqrt(-log det(I - S^-1 F^k S F^k')) / 2
(validation/tube_levels.mac). On one state of correlation rho it is
sqrt(-log(1 - rho^(2k))) / 2.
s must be the whole state the scores are read off: on a log under a policy with its own
persistence, the policy's state belongs in it.
Raises:
| Type | Description |
|---|---|
ValueError
|
on a matrix that is not square, a covariance not of its size, not symmetric
or not positive definite, a spectral radius of 1 or more, or |
lipschitz_rollout_bound(lipschitz, model_error, dt, horizon)
¶
Certified H-step trajectory-error bound from a Lipschitz field via discrete Gronwall.
Two Euler rollouts of an L-Lipschitz field, one perturbed per step by <= model_error,
deviate by e_k obeying e_{k+1} <= (1 + L*dt)*e_k + dt*model_error, e_0 = 0, so
``e_H <= model_error * ((1 + L*dt)^H - 1) / L`` (``L>0``; the ``L->0`` limit is linear,
``model_error * dt * H``, no blow-up).
Turns the CERTIFIED constant L of LipschitzResidual (plus the trusted
known-field norm) into a certified pessimism radius that propagates a per-step error BUDGET
through time. This COMPLEMENTS (does not replace) chc.support.SupportModel: the support
density-distance scores WHERE the one-step error model_error is large (off-support), and
the bound propagates it (see support_calibrated_error). model_error should be a
one-step bound (a conformal upper quantile or a bounded-disturbance envelope), not a validation
average. Machine-checked in proofs/lipschitz_rollout.v (rollout_error_bound); derived in
validation/lipschitz_rollout.mac. HONEST SCOPE: the bound is exp(L*T) (T = H*dt) --
tight for small L*T (bounded-gain residual, short horizon, safety-critical), loose else; a
contraction metric (ContractiveResidual, log-norm mu<0) removes the
exponential; the open loop assumes a FIXED action sequence -- for re-planning use
closed_loop_rollout_bound; for per-step L_k use time_varying_rollout_bound.
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
lipschitz_rollout_certificate(seed=0, state_dim=2, horizon=8, dt=0.05, model_error=0.1)
¶
Roll two fields differing by a bounded per-step error; confirm the deviation obeys the bound.
The field is f_known + r with f_known a linear map of known spectral norm and r a
LipschitzResidual with certified constant L_r; L = ||A||_2 + L_r
upper-bounds its x-Lipschitz constant. A fixed error of norm model_error is added each
step (the learned part is the fragile one); the Euler deviation from a shared start stays under
lipschitz_rollout_bound(L, model_error, dt, horizon).
contractive_rollout_bound(contraction_rate, lipschitz, model_error, dt, horizon)
¶
UNIFORMLY BOUNDED explicit-Euler rollout radius for a CONTRACTING field (log-norm mu=-c<0).
The discrete Euler contraction factor is q = sqrt(1+2*mu*dt+L^2*dt^2) (NOT 1+mu*dt;
L = the full Lipschitz constant), since two Euler steps deviate by
|d+dt*(f(x)-f(y))|^2 <= (1+2*mu*dt+L^2*dt^2)|d|^2 (Maxima contractive_euler.mac; Rocq
contractive_euler.v). Contraction (q<1) needs the step dt<2c/L^2 (sufficient, from
the worst-case (mu,L) bounds); under it the discrete Gronwall bound
e_H <= model_error*dt*(1 - q^H)/(1 - q) is capped at
model_error*dt/(1-q) for ALL horizons -- no e^{L*T} blow-up -- tends to the continuous
radius model_error/c as dt -> 0. The payoff of a certified negative log-norm
(ContractiveResidual) over the non-negative ||.||-Lipschitz of
LipschitzResidual; the q < 1 cap uses gronwall_bounded. Returns
+inf if not contracting or the step is too large (dt >= 2c/L^2), where Euler overshoots.
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
contractive_rollout_certificate(seed=0, state_dim=3, horizon=60, dt=0.05, model_error=0.1)
¶
Confirm a ContractiveResidual contracts and its radius stays flat.
Checks (i) the empirical one-sided Lipschitz <r(x)-r(y),x-y>/||x-y||^2 <= -rate (certified
contraction), and (ii) the perturbed-rollout deviation stays under the BOUNDED explicit-Euler
radius eps*dt/(1-q) (step dt<2c/L^2) even as the horizon grows -- contrasted with
the e^{L*T} that the same field's norm-Lipschitz L would incur (lipschitz_blowup).
time_varying_rollout_bound(lipschitz, model_error, dt, *, integrator='rk4')
¶
Per-step certified error tube e_0..e_H for time-varying L_k and BUDGET eps_k.
Two rollouts by integrator from one start, of an L_k-Lipschitz field and of the field
off by at most eps_k wherever step k reads it, stay within e_k of each other,
e_0 = 0:
"rk4", the integrator ofchc.integrate.rolloutand so of every plan:e_{k+1} = e_k + dt (L_k e_k + eps_k) phi(L_k dt),phi(z) = 1 + z/2 + z^2/6 + z^3/24, from bounding RK4's four stages one after another (Rocqrk4_rollout_error_bound,validation/rk4_rollout.mac). Its growth1 + z phi(z)is RK4's stability polynomial, and on a linear scalar field with a constant error the tube is the two rollouts' gap exactly;"euler":e_{k+1} = (1 + L_k dt) e_k + dt eps_k(Rocqgronwall_var_comparison), a bound on explicit-Euler rollouts only. On RK4 rollouts it is not one:x' = xagainstx' = x + 0.1atdt = 1reads0.1, 0.3, 0.7where the RK4 gaps are0.1708, 0.6335, 1.8866.
Unlike the constant-L lipschitz_rollout_bound, this exposes WHICH step / channel
drives the growth -- feed certified_horizon for the honest certified_until_step.
eps_k is a budget: it should be a CERTIFIED per-step bound (a conformal quantile, a
bounded-disturbance envelope, or support_calibrated_error combining a base error with
chc.support.SupportModel), not a validation-set average.
Raises:
| Type | Description |
|---|---|
ValueError
|
if an |
certified_horizon(lipschitz, model_error, dt, tolerance, *, integrator='rk4')
¶
The largest step H whose certified error e_H stays within tolerance.
Past it the plan is flagged uncertain; e is monotone, so this is the first crossing. A
radius that overflows to inf is within no tolerance, inf included. The tube is
time_varying_rollout_bound's for integrator, which raises as it does; a
tolerance that is negative or nan raises too.
linear_rollout_bound(state_matrix, model_error, dt)
¶
The RK4 error tube e_0..e_H of a field affine in the state, x' = A x + g(t, u).
Two RK4 rollouts from one start, of the field and of the field off by at most eps_k
wherever step k reads it, under the same actions, stay within
``e_k = sum_{j<k} eps_j g_{k-1-j}``, ``g_m = dt sum_i ||R(dt A)^m c_i(dt A)||_2``,
R(z) = 1 + z + z^2/2 + z^3/6 + z^4/24 being RK4's stability polynomial and c_1..c_4 the
weights its stages put on the field's errors, (1 + z + z^2/2 + z^3/4)/6,
(2 + z + z^2/2)/6, (2 + z)/6 and 1/6, which sum to phi: one step's gap is
exactly R(dt A) e + dt sum_i c_i(dt A) delta_i (validation/rk4_rollout.mac). The
matrices, not a norm of A, carry the growth, so a contracting A shrinks the tube where
a norm-Lipschitz one could only widen it; on a scalar A = L >= 0 it is
time_varying_rollout_bound's RK4 tube.
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
closed_loop_rollout_bound(state_lipschitz, control_lipschitz, policy_lipschitz, model_error, dt, horizon)
¶
Closed-loop rollout radius when the plan is RE-PLANNED each step by an L_pi-Lipschitz pi.
A state error perturbs the action, which perturbs the next state, so the growth rate is
L_x + L_u*L_pi (not L_x alone): e_{k+1} <= (1 + dt(L_x + L_u*L_pi)) e_k + dt*eps.
Reduces to lipschitz_rollout_bound with the combined constant. CAVEAT: for a clipping /
active-set / threshold MPC L_pi may be unbounded (not globally Lipschitz); use this only
where the controller is Lipschitz (a fixed feedback gain, or a smooth relaxation).
Raises:
| Type | Description |
|---|---|
ValueError
|
if a Lipschitz constant is negative, infinite or nan: a negative one lowered
the combined rate below the state's own. Otherwise as |
support_calibrated_error(base_error, scale, support_distances)
¶
Per-step error BUDGET eps_k = base_error + scale * D(x_k, u_k) from a support distance.
This COMBINES (does not replace) chc.support.SupportModel: the certified rollout bound
propagates a per-step budget through time, and the SupportModel says WHERE that budget is large
(off-support, where the one-step model error grows). SupportModel scores the local risk, the
Gronwall tube propagates it.
constraint_margin(nominal_g, lipschitz_g, error_radii)
¶
Tightened constraint margin g(x_hat_k) + L_g * e_k for an L_g-Lipschitz constraint.
By Rocq constraint_tightening, if this margin is <= 0 at step k then the TRUE
trajectory (within the certified radius e_k of the nominal x_hat_k) satisfies
g(x_k) <= 0 -- the tube becomes a robust feasibility check. Admissible while all <= 0.
time_varying_rollout_certificate(seed=0, horizon=20, dt=0.05, tolerance=0.3)
¶
Rising per-step L_k + a bumpy budget: the tube stays under the constant-max-L bound.
Identifies certified_until_step and the safety margin gives a safe_until_step cutoff.
confounding_robust_inflation(cvar_upper, cvar_lower, gamma)
¶
MSM confounding inflation (Gamma-1)/(Gamma+1) * (CVaR_up - CVaR_lo) over a point effect.
Under a BOUNDED DENSITY-RATIO sensitivity model (the weight lies in [1/Gamma, Gamma] with
mean 1 -- a marginal, covariate-unconditional special case of Tan's MSM, whose full form
constrains the treatment-assignment odds with propensity/covariate-dependent bounds) the sharp
worst-case E[wY] puts w=Gamma on the top-tau tail (tau=1/(Gamma+1),
mean-preserving) and w=1/Gamma elsewhere. The gap over the nominal mean is this closed form
(Maxima confounding_robust_cvar.mac; Rocq msm_inflation_*): 0 at Gamma=1 (point
ID), nonnegative, monotone in Gamma. Dorn-Guo (2023); Oprescu et al., B-Learner (2023); Tan
(2006).
For a continuous lever the same closed form is the sharp bound on the mean outcome at one dose
level under the continuous MSM (Jesson et al. 2022; Frauen, Melnychuk and Feuerriegel 2023,
Thm 1). It bounds a level, not a slope: a contrast of two levels is bounded by the upper bound
at one less the lower bound at the other (Frauen et al., Lemma 3). A channel radius spent as
radius * ||u|| (ConfoundingRobustPenalty) is a structural sensitivity model of its
own, not the MSM's bound.
msm_worst_case_mean(outcomes, gamma)
¶
Sharp MSM worst-case (upper) mean of outcomes over the [1/Gamma, Gamma] weight box.
The confounding-robust pessimistic value a controller should plan against when the treated
outcome sample may be confounded up to sensitivity Gamma. Equals mean + inflation with
the CVaR tails of confounding_robust_inflation; reduces to the sample mean at
Gamma=1.
confounding_robust_radius(nominal_radius, outcomes, gamma)
¶
Inflate a pessimism radius by the MSM confounding gap: rho0 + (worst_case - mean).
Rocq robust_radius_ge_nominal / robust_radius_monotone: the returned radius is never
below nominal_radius (pessimism only grows under assumed confounding) and is monotone in
Gamma. Feeds chc.support.pessimistic_control as a widened uncertainty budget.
confounding_robust_certificate(seed=0, n=8, gamma=3.0)
¶
Confirm the CVaR closed form matches the sharp LP worst-case on a mean-preserving grid.
Chooses (n, Gamma) so tau*n = n/(Gamma+1) is an integer, making the sharp box-LP
optimum a pure top-tau*n assignment (weight Gamma on the largest points, 1/Gamma on
the rest) that a brute-force sort computes exactly -- a real check, no LP solver needed.
confounding_robust_closed_loop_bound(state_lip, control_lip, policy_lip, base_error, control_magnitude, cvar_gap, gamma, dt, horizon)
¶
Closed-loop rollout radius (Result 31) with the per-step budget inflated by MSM confounding.
Composes the confounding radius (§32) with the closed-loop replan tube (§31): the growth rate is
L_x + L_u*L_pi (re-planning feeds state error through an L_pi-Lipschitz policy). The §32
half-width Delta_B = confounding_robust_inflation(cvar_gap, 0, gamma) is an effect (B)
error; the per-step state-transition error it induces is ||Delta_B * u_k|| <=
control_magnitude*Delta_B, so the budget is eps_k = base_error + control_magnitude*Delta_B
(the action multiplier makes the units match -- effect error times control is a state error).
gamma=1 recovers the plain closed-loop tube. Rocq closed_loop_confounding_monotone: the
tube is monotone in the confounding at every horizon.
confounding_robust_closed_loop_certificate(gamma=3.0, horizon=25, dt=0.05)
¶
Show the §32-inflated §31 closed-loop tube widens and its safe horizon shrinks with Gamma.
A re-planning controller (L_pi-Lipschitz policy) on an L_x/L_u plant, a nominal
margin to a constraint g <= 0: as the assumed MSM sensitivity Gamma grows the
confounding-inflated tube eats the margin sooner, so the safe planning horizon contracts.
confounding_cost_bound_certificate(radii=(0.005, 0.01, 0.02, 0.05, 0.1, 0.2), horizon=20, dt=0.05, restarts=6, ascent_steps=250, seed=0)
¶
Try to break ConfoundingRobustPenalty.certified, and report where it bends.
A two-lever damped oscillator, planned to optimality, then attacked: the adversary picks the
control-channel error M with ||M||_2 <= radius that moves the objective most, by
projected gradient ascent from several starts on both signs of Delta J. Random sampling was
the first version and it is not good enough -- in four parameters it underestimates the worst
case by enough to hide a violated bound.
Two ratios are reported because they answer different questions. ratio is the certificate:
realised over the full second-order weight, and ok demands every entry stay at or below one.
first_order_ratio is the diagnostic that motivated the second-order term: it exceeds one at
every radius, which is why ||lambda|| alone was a calibrated estimate, not a bound.
benchmark_gamma(treated, covariates, assumed_gamma, names=None, quantile=0.95, ridge=1e-06)
¶
Price an assumed MSM Gamma against the observed covariates, in MSM units.
Gamma is the analyst's unfalsifiable input, and Result 32 says so every time it is used. It
is unfalsifiable; it is not uncalibrated. The MSM bounds the assignment odds ratio between the
true propensity and the modelled one uniformly over units, and dropping an observed covariate
produces exactly such a pair -- so
``Gamma_j = exp( quantile_i | logit e(x_i) - logit e_{-j}(x_i) | )``
is the sensitivity a confounder as strong as covariate j would generate, measured rather
than assumed.
quantile = 1.0 is the MSM's own quantity and it is not the default, because it grows
with the sample. The model bounds the odds ratio uniformly over units, so the sup is the
faithful statistic -- but under an unbounded covariate the sup is an extreme order statistic.
Measured on a standard-normal design, the sup runs 309 -> 382 -> 397 -> 734 as n goes
500 -> 4000 -> 32000 -> 128000, while the 95th percentile sits at 22, 24, 19, 19 and
the median at 2.7, 3.0, 2.7, 2.8. A benchmark that quadruples because more data arrived is
not a benchmark, so the default reports a quantile and says which one; pass quantile=1.0
when the uniform bound is the point and the covariates are bounded.
multiples_of_strongest then reads Gamma as "an unobserved confounder k times as
strong as the strongest thing we did observe", on the log scale odds ratios compose in. A
k far below 1 is an assumption nobody should be impressed by; a k far above 1 is one
the analyst has to defend.
SCOPE. Per-covariate and one-at-a-time: this is the marginal strength of each covariate, not a policy-level aggregation over a set of them, and the two differ whenever covariates are correlated. Dropping a covariate from the propensity is a benchmark, not a claim that the unobserved confounder resembles it. The propensity is logistic-linear in the columns as passed; a benchmark is only as good as that model, which is why the covariates should already carry the basis expansion the analyst believes.
ridge penalises each slope of the propensity in units of its column's variance, so an
implied Gamma reads the same in any units and from any origin of the covariates.
negative_control_gamma(outcomes, tol=1e-09, gamma_max=1000000.0)
¶
Smallest Gamma whose MSM interval covers zero on a KNOWN-NULL outcome.
The other half of calibration, and the half that can refute. On an outcome whose true effect is
zero, any nonzero estimate is confounding, so the smallest Gamma that reconciles the two is
a lower bound on the confounding actually present: assuming less than it is refuted by the
data rather than merely unappealing. That transfers to the outcome of interest only when the
control is U-comparable, confounded by the same unobserved causes (Lipsitch, Tchetgen Tchetgen
and Cohen 2010). A control confounded by more than the outcome overstates the outcome's
Gamma, and one confounded by less understates it (Baitairian et al., arXiv 2510.16560).
Monotonicity of the MSM inflation in Gamma makes the search a bisection, exact to tol.
Returns inf when no Gamma reconciles the null -- the endpoint saturates at the extreme
order statistic as Gamma -> inf, so a sample whose values all share the mean's sign cannot
be pulled across zero by a bounded density ratio at all, and the negative control has refuted
the model class instead of calibrating it.
The interval is NOT symmetric about the mean, and which endpoint has to travel depends on the
sign of the estimate: a positive estimate is reconciled by the lower endpoint reaching zero, a
negative one by the upper. Reflecting the sample by -sign(mean) maps both onto the single
upper-endpoint routine msm_worst_case_mean computes, which is why the sharp CVaR tails
are used in the direction that actually binds rather than the convenient one.
gamma_benchmark_certificate(strengths=(0.25, 0.5, 1.0, 2.0), sizes=(500, 4000, 32000), biases=(0.1, 0.2, 0.4), quantile=0.95, samples=4000, seed=0)
¶
Check that the benchmark measures confounding strength and that the calibration binds.
Five gates, each able to fail:
- Monotone in strength. A covariate whose logit coefficient grows induces a larger implied
Gamma. A benchmark that does not order confounders by strength orders nothing. - The null covariate's score is a root-
nnoise floor. Dropping a coefficient that is truly zero must not manufacture sensitivity beyond sampling noise -- and "beyond sampling noise" is a rate, not a constant, so the gate is thatlog(Gamma_null) * sqrt(n)stays bounded asngrows rather than that a single draw sits below a fixed number. Measured on six seeds per size, that product runs2.1, 4.0, 5.0, 4.4atn = 1000 .. 64000whileGamma_nullitself falls1.069 -> 1.017; a single draw of1.27atn = 4000is inside that distribution, which is exactly why one draw cannot be the gate. - Ranking. On one design carrying strong / weak / null covariates, the implied
Gammaranks them in that order. - The reported quantile is stabler in
nthan the sup. This is the measurement the default rests on, so it is run rather than remembered: the sup is an extreme order statistic under an unbounded covariate and grows with the sample; the quantile should not. - The negative control lands on the endpoint, and grows with the planted bias. At the
returned
Gammathe binding interval endpoint sits at zero to solver tolerance, and a larger planted bias needs a largerGamma-- plusinfwhen the sample cannot be reconciled at all.