chc.sensitivity¶
Stability tier: evolving.
Sensitivity-aware robust control under HIDDEN CONFOUNDING -- a facade over the §32-§40 line.
Offline pessimism (chc.support, chc.uncertainty) assumes the observed transitions identify
the causal effect. Under hidden confounding they do not: the effect is only partially identified
in a sensitivity interval. This module gathers the one "sensitivity-aware robust control" story
whose primitives are otherwise split across two modules by their numerics (chc.regret is
NumPy/SciPy, chc.uncertainty is JAX). Nothing is moved -- this is a discovery/API surface only.
The pipeline, estimate -> sensitivity radius -> robust control -> validation:
- Calibrating
Gammabefore spending it (§32 (b)).Gammacannot be tested against the data, but it can be priced.benchmark_gammaexpresses it in units of the confounding the OBSERVED covariates carry -- dropping covariatejfrom the propensity produces exactly the pair of propensities the MSM bounds -- and reports the EXPONENTlog(Gamma)/log(Gamma_strongest), because odds ratios compose multiplicatively.negative_control_gammainverts a known-null outcome for the smallestGammathat reconciles it, a lower bound on the confounding present: assuming less is refuted by the data, andinfsays the model class is refuted instead. - Sensitivity radius (§32, bounded density-ratio / MSM). Given a treated-outcome sample and a
sensitivity
Gamma >= 1(the analyst's unfalsifiable input,[1/Gamma, Gamma]density-ratio box), the sharp worst-case effect is a CVaR mixture;confounding_robust_inflation/msm_worst_case_meangive the radius inflation,confounding_robust_radiuswidens a pessimism radius (never optimistic, tight atGamma=1, monotone). - Regret floor (§33).
confounding_robust_lq_regret-- the confounding effect-bias becomes a control-regret floor SECOND order in the bias (lq_regret_sensitivityfor the toyL_reg;confounding_robust_lq_regret_matrixfor the multivariate Frobenius lift). - Robust controller (§35).
confounding_robust_control-- under an ASYMMETRIC loss (over/under-shoot) the radius shifts the gain (sign dichotomy) and beats certainty-equivalence (certainty_equivalence_control);asymmetric_control_improvementis the piecewise worst-case-loss gain,worst_case_asymmetric_lossthe loss itself. - Closed loop (§34).
confounding_robust_closed_loop_bound-- the radius feeds the replan-tube (L_x + L_u*L_pi), the effect error scaled by the control magnitude. - Safety (§40). The same radius, spent on a constraint instead of on the objective.
robust_barrier_marginis the best barrier derivative guaranteed against every effect in the identified set,robust_safe_actionits maximiser -- exactly zero once the radius swallows the control channel --identification_radius_thresholdthe sharp radius at which certification dies andbarrier_gamma_startheGammait corresponds to -- the largest sensitivity-model level under which the barrier stays certified, not a measured amount of hidden confounding. The accounting differs from step 2 by an order: performance regret is second order in the effect bias, safety margin is first.certify_safetyapplies all of that along a finishedchc.plan.CausalPlan-- the plan-levelGamma*is the weakest step's, so one uncertifiable step sinks the plan.barrier_reachability_gapprices that certificate against the Hamilton-Jacobi answer it approximates: where the condition holds on all of{h >= 0}the reachable tube is that whole set, and where it only holds pointwisecertified_but_unreachablesays how much the per-step reading over-promises -- a filter, not a proof. - Validation.
confounding_robust_control_benchmarkgrounds it on a synthetic observational confounded marketplace andconfounding_robust_tracking_benchmarkon a confounded dynamic plant in closed loop;ConfoundingRobustTaskcarries the same radius into the mainchc.benchmarkleaderboard (regret vs oracle, multi-seed CIs) throughConfoundingRobustPenalty; the*_certificatefunctions are the machine-checked evidence.
Worked example (the calibration is explicit -- NOT baked in)::
from chc.sensitivity import (
confounding_robust_inflation, certainty_equivalence_control, confounding_robust_control,
)
b_hat, target, gamma = 1.3, 1.0, 2.5 # biased effect estimate, service target, sensitivity
cvar_gap = b_hat # <- YOUR effect-scale CVaR-gap calibration; keeps D < b_hat
D = confounding_robust_inflation(cvar_gap, 0.0, gamma) # effect half-width Delta(Gamma)
u_ce = certainty_equivalence_control(b_hat, target) # trusts the biased estimate
u_robust = confounding_robust_control(b_hat, D, target, 1.0, 4.0) # undershoot 4x -> push up
HONEST: Gamma and the CVaR-gap calibration are the analyst's inputs; the sign-identification
constraint b_hat > D > 0 must hold. These robustify pessimism, they do NOT test for confounding.
The scalar closed forms of steps 0-5 are proved in Rocq and derived in Maxima, one pair per step,
each proof's header stating its scope: proofs/<name>.v and validation/<name>.mac for
gamma_benchmark, confounding_robust_cvar, confounding_lq_regret,
confounding_robust_control, lipschitz_rollout and barrier_feasibility, in step order;
step 5's vector form is proved at any dimension in proofs/mathcomp/barrier_feasibility_lift.v.
BarrierConfoundingCurve
dataclass
¶
Measured evidence for §40 over a sensitivity grid on one confounded safety problem.
SafetyFilterBenchmark
dataclass
¶
Closed-loop evidence for the §40 claim that a regret-sized budget is not a safety budget.
ConfoundingRobustTask
dataclass
¶
Control under HIDDEN confounding: the safeguard is a sensitivity radius, not adjustment.
The actuator gain is calibrated from an observational log whose action was driven by a
latent disturbance that also moved the transition. It is never recorded and there is no
instrument, so the gain is only partially identified -- the adjustment fix of PricingTask
is unavailable by construction, which is why this task takes no estimator argument. Here the
confounding attenuates the gain, so the greedy plan believes a weak actuator, over-commands,
overshoots the target on the true plant, and demands actions far outside the logged range. The
safeguard is chc.uncertainty.ConfoundingRobustPenalty in the lam_unc channel: the
partial-identification radius times the action magnitude, bounding the transition error a
confounded gain can inject per step.
HONEST TRAPS. (1) The penalty is ONE-SIDED -- it can only shrink actions, so it rescues an
understated gain and strictly hurts when the confounding inflates it instead (a test pins
this). (2) gamma and the cvar_gap := b_hat calibration are the analyst's inputs, not
learned; the row scores the controller at the assumed sensitivity, not an oracle one.
(3) With no confounding the robust controller pays a strict premium, and it only wins beyond a
problem-dependent confounding threshold -- at half the default confounding it still loses.
(4) This is an OPEN-LOOP plan scored on the plant; the closed-loop counterpart is
chc.sensitivity.confounding_robust_tracking_benchmark.
The oracle is the planner's descent from zero on the true plant. The plant is linear, so the cost is a convex quadratic in the plan and the descent's plan is the best one: the exact minimiser over the box, by bounded-variable least squares (Stark and Parker 1995), is 6.9e-11 below it in float64, and descents from 73 starts end on the same plan.
run(seed_data=0)
¶
Calibrate the gain on a confounded log, then score greedy/robust/oracle on the plant.
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.
BarrierReachabilityGap
dataclass
¶
What a pointwise §40 check does and does not tell you, measured against the true tube.
The CBF theorem is a statement about a set: if the robust barrier condition holds at every
point of {h >= 0}, that set is forward invariant. Checking it at one state -- which is what
chc.plan.certify_safety does per step, and what any online safety filter does -- is a
strictly weaker thing, and these fields separate the two.
ConfoundingRegretFloorCurve
dataclass
¶
Empirical certificate: control regret vs confounding bias, and its log-log slope (~2).
ConfoundingRobustControlCurve
dataclass
¶
Evidence the pessimism radius shifts the gain and strictly beats CE under asymmetric loss.
ConfoundingRobustLQRegretCurve
dataclass
¶
Evidence the confounding regret bound order-doubles effect error and floors at Delta^2.
DynamicConfoundingCurve
dataclass
¶
§35 robust vs CE on realised CLOSED-LOOP cost across a confounding grid (the dynamic §37).
MarketplaceControlCurve
dataclass
¶
§35 robust controller vs CE on realised marketplace cost across a confounding grid.
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.
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.
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.
admissible_action_interval(channel, radius, u_max, drift, alpha_h)
¶
The scalar actions whose guaranteed barrier derivative clears -alpha_h.
channel is the signed grad h . B, unlike the norm taken by
robust_barrier_margin -- which way to push is the whole question here. The guaranteed
derivative drift + channel*u - radius*|u| is concave in u, so the admissible set is an
interval; combined with |u| <= u_max it stays one. Returns an empty interval as
(nan, nan) when no admissible action exists, which the caller must handle rather than
silently clip into.
A non-positive deficit means u = 0 is admissible -- it does not mean every action is.
The control that serves the task is exactly the one that can spend the drift's slack and cross
the boundary, so both ends are computed from the branch slopes in every case.
barrier_confounding_certificate(channel=0.6, drift=-0.9, u_max=2.0, alpha_h=0.5, cvar_gap=1.0, grad_norm=1.0, l_reg=3.0, gammas=(1.0, 1.5, 2.0, 3.0, 5.0, 9.0))
¶
Sweep the sensitivity level and check the threshold, the zero-action rule and the orders.
The defaults are chosen so the sweep crosses every regime rather than sitting inside one: the
drift is in deficit (D = 0.4 > 0) so the controller is load-bearing, Gamma* = 7/3 falls
inside the grid so certification genuinely fails partway, and the radius overtakes the channel
at Gamma = 4 so the tail of the grid exercises the zero-action rule. A configuration with
D <= 0 would certify everything and prove nothing.
barrier_gamma_star(threshold_radius, cvar_gap, grad_norm)
¶
Largest MSM Gamma a safety certificate survives: (gap + c) / (gap - c).
Inverts §32's Delta(Gamma) = (Gamma-1)/(Gamma+1) * cvar_gap at threshold_radius, with
c = threshold_radius / grad_norm the effect-space radius the barrier can absorb. The closed
form is only valid on 0 < c < cvar_gap; outside it the four cases are genuinely different
answers rather than edge-case noise, and the denominator would silently change sign:
c < 0(ornan) -- nothing is certified, not even atGamma = 1where the radius is zero.nan, because noGamma >= 1works and reporting1.0would claim the opposite.c == 0-- exact identification and nothing more:1.0.0 < c < cvar_gap-- the finite ceiling(gap + c) / (gap - c).c >= cvar_gap--Deltasaturates atcvar_gapasGamma -> inf, so the threshold is beyond anything the sensitivity model can produce:inf.
grad_norm == 0 is a state where the barrier is flat, such as the centre of a ball, and there
the radius Delta * grad_norm is zero at every Gamma: the step holds at every level or at
none, inf when threshold_radius >= 0 and nan otherwise.
control_channel(grad_h, b_hat)
¶
||B^T grad h|| -- how strongly the control can move the barrier, in the best direction.
The multivariate stand-in for the scalar |g|. Cauchy-Schwarz is tight at
u = U * w / ||w||, so every bound below is attained rather than conservative.
identification_radius_threshold(drift, channel, u_max, alpha_h)
¶
Largest radius d* whose safety certificate still holds: channel - D / u_max.
D = -alpha_h - drift is the deficit the drift leaves for the controller to cover, and the
answer is piecewise in it -- the formula alone is a trap, because it happily returns a
negative "largest radius" for a problem no radius can save:
D <= 0-- the uncontrolled system already satisfies the barrier. No radius can break it, so the threshold is vacuous andinfsays so rather than inviting a comparison.D > u_max * channel-- nominally infeasible: even a perfectly identified channel at full authority cannot cover the deficit, so nothing is certified, not even atradius = 0.nan(the module's empty-set convention, as inadmissible_action_interval).0 < D <= u_max * channel-- the case worth having a threshold for:d*lands in[0, channel)and certification holds iffradius <= d*.
robust_barrier_margin(drift, channel, radius, u_max)
¶
Best barrier derivative guaranteed against every effect inside the identified set.
drift + (channel - radius)_+ * u_max. The clip at zero is the whole content: past
radius = channel extra actuation buys nothing, because the adversary can cancel it. So the
margin lost relative to a perfectly identified channel is u_max * min(radius, channel) --
first order in the radius until the authority runs out, then constant. Exact for a Euclidean
action ball and an isotropic channel radius (see the module docstring).
robust_safe_action(grad_h, b_hat, radius, u_max)
¶
The action attaining robust_barrier_margin: full authority, or nothing at all.
Returns exactly 0 when radius >= ||B^T grad h||. That is not a numerical artefact and
not conservatism -- it is the optimum: with the channel's sign unidentified, every nonzero
action has a worst case at least as bad as standing still.
robust_safety_filter(u_nominal, channel, radius, u_max, drift, alpha_h)
¶
Least-restrictive admissible action: the nominal, clipped into the certified interval.
Minimal intervention in the exact sense -- among all actions whose guaranteed margin clears, the
one closest to what the task controller asked for. Scalar control only: for m > 1 inputs the
same set is convex but its projection is no longer a clip, and a filter that quietly over-brakes
would misreport the price of safety. Use robust_safe_action there, which maximises the
margin rather than tracking a nominal.
When the interval is empty -- the deficit exceeds what the identified channel can deliver -- the
margin-maximising action sign(channel)*u_max is returned: it does not certify, and the
caller should already know that from identification_radius_threshold.
safety_filter_benchmark(b_true=1.0, kappa=-0.5, confounding=1.0, gamma=3.0, decay=0.3, x0=0.0, x_ref=3.0, x_limit=2.0, alpha=1.0, dt=0.1, n_steps=60, u_max=6.0, n_data=4000, seed=0)
¶
Drive a confounded plant at a reference beyond a safety limit, four ways.
The scalar plant x' = decay*x + b_true*u must track x_ref while respecting
x <= x_limit, and the two are in conflict by construction -- the reference sits past the
limit, so nothing but the constraint stops the controller. The drift is unstable (decay >
0), which is what makes the threshold bite: near the limit the plant pushes outward on its
own, so the controller has a real deficit to cover and Gamma* is finite. The actuator gain
is calibrated from an observational log whose action responded to a latent driver (kappa < 0
attenuates it), so the controller believes a weaker actuator than it has and over-commands.
The four rows are the point:
oraclefilters with the true gain and a zero radius -- the best achievable safe tracking;safety_calibratedfilters with the biased gain and the §32 radius atgamma;regret_calibratedshrinks its action by the §33 accounting instead -- the radius spent on the objective, which is what this library did everywhere before §40;greedybelieves the biased gain and does nothing about it.
Returns violation rate, worst excursion and tracking cost for each. Read the tracking column
with its floor in mind: a controller that respects the limit cannot beat
(x_limit - x_ref)**2 on this plant, so the violators' lower cost is bought with the
excursions in the other two columns and is not evidence that they control better. This is a
demonstration on one synthetic plant, not a theorem: it grounds the order argument rather
than proving it.
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
|
|
required |
alpha
|
float
|
the class-K gain in |
1.0
|
gamma, cvar_gap
|
the marginal sensitivity model's level and the gap it scales, combined into
|
required | |
u_max
|
ArrayLike | None
|
actuation limit, used for |
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 |
barrier_reachability_gap(barrier, drift, b_matrix, *, lower, upper, resolution=(81, 81), horizon=1.0, steps=200, u_max=1.0, radius=0.0, alpha=1.0, tolerance=0.02)
¶
Run the §40 condition and the horizon-T tube on one grid and report where they part.
The barrier condition asks ``max_u [grad h . f + - radius||grad h||||u||]
= -alpha*h
; the tube asks whether *any* controller holdsh >= 0forhorizonseconds against every effect matrix in the identified ball. The implication runs one way and only with a quantifier: if the condition holds at **every** point of{h >= 0}, that set is invariant and the tube must be all of it -- which is whatokchecks. Pointwise the implication is simply false, andcertified_but_unreachableis how much of the grid it is false on: states where the §40 margin is satisfied right now, yet no controller survives the horizon. Relative degree is the usual reason -- whenB^T grad h == 0`` the condition degenerates into a statement about the drift and stops seeing the actuator at all.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
alpha
|
float
|
the class-K gain of the barrier condition, matching |
1.0
|
tolerance
|
float
|
grid-fraction slack when comparing the tube to |
0.02
|
Raises:
| Type | Description |
|---|---|
ValueError
|
as |
asymmetric_control_improvement(effect_estimate, halfwidth, target, overshoot_penalty, undershoot_penalty)
¶
Piecewise worst-case-loss improvement W_CE - W_rob of the §35 robust controller over CE.
W_rob = 2*a*b*target*D/Q (branch-free, Q = (a+b)*b_hat + (a-b)*D), but
W_CE = max(a,b)*target*D/b_hat, so the improvement is PIECEWISE (reviewer-8): the
OVERSHOOT-dominant branch a*target*D*(a-b)*(b_hat+D)/(b_hat*Q) for a >= b and the
UNDERSHOOT-dominant branch b*target*D*(b-a)*(b_hat-D)/(b_hat*Q) for b >= a (Rocq
rob_gap_* / rob_gap_under_*). Both are >= 0 for b_hat > D > 0 and > 0 iff
a != b and D > 0 -- the old single formula only covered a >= b (missing Result 37's
b = 4*a regime). Here a = overshoot_penalty, b = undershoot_penalty.
certainty_equivalence_control(effect_estimate, target)
¶
CE control u = target/b_hat -- centres the outcome b*u at target under b_hat.
confounding_regret_floor_certificate(b_true=1.3, effort=0.4, target=1.0, confounding_levels=(0.02, 0.04, 0.06, 0.08, 0.1), action_noise=0.6, n=200000, seed=0)
¶
Show the §33 Delta^2 regret floor emerges from real confounded data, not just algebra.
A confounder z drives both the action (u = z + noise) and the outcome
(y = b_true*u + gamma*z + noise). Naive OLS of y on u returns an effect biased by
the confounding gamma -- an irreducible Delta that does NOT vanish with n. The CE
controller uses that biased effect on the true plant, and its measured regret scales as
Delta^2 (log-log slope ~2), matching the analytic L_reg*Delta^2 -- the floor.
confounding_robust_control(effect_estimate, halfwidth, target, overshoot_penalty, undershoot_penalty)
¶
Minimax control over the §32 effect interval [b_hat +/- halfwidth] under asymmetric loss.
Loss alpha*(y-target)_+ + beta*(target-y)_+ on the outcome y = b*u; the controller does
not know b in the confounding interval. The minimax u balances the two weighted worst
tails (validation/confounding_robust_control.mac): u = (alpha+beta)*target /
(alpha*(b_hat+D) + beta*(b_hat-D)). Equals u_CE/(1 + kappa*D) with
kappa = (alpha-beta)/((alpha+beta)*b_hat) -- the pessimism radius D SHIFTS the gain, a
sign DICHOTOMY (Rocq): a>=b -> kappa>=0 -> u_rob<=u_CE, conservative
(shift_factor_nonneg / robust_gain_conservative); a<=b -> kappa<=0 ->
u_rob>=u_CE, aggressive (shift_factor_nonpos / robust_gain_aggressive). CE when
symmetric (a=b -> kappa=0). Here a=overshoot_penalty, b=undershoot_penalty.
SCOPE: a minimax STATIC tracking toy, not a general robust controller -- assumes
b_hat > halfwidth > 0 (the effect interval has an identified sign, so the denominator is
positive), target > 0, alpha, beta > 0, and NO effort/control penalty (pure outcome
tracking). The dynamic/effort-penalised case is out of scope.
confounding_robust_control_benchmark(b_true=2.0, target=1.0, overshoot_penalty=1.0, undershoot_penalty=4.0, sensitivity_gamma=2.5, target_confounding=0.8, confounding_levels=(0.0, 0.4, 0.8, 1.2), incentive_demand_corr=1.0, action_noise=0.6, n_markets=300, n_periods=400, seed=0)
¶
Ground §35: does the confounding-robust controller beat CE on a full marketplace pipeline?
Synthetic OBSERVATIONAL confounded marketplace (not a randomised switchback -- the action
follows the demand confounder z, so this is confounded logging, not an experiment): the naive
effect estimate is biased, so the
CE controller (trusting it) under-incentivises and misses completions -- expensive when churn
(undershoot_penalty) dominates budget waste (overshoot_penalty). The §35 controller uses
an assumed sensitivity Gamma and a half-width D. Calibration (named): §32 gives
D = (Gamma-1)/(Gamma+1)*(CVaR_up - CVaR_lo); for THIS synthetic benchmark we calibrate the
sensitivity gap to the estimate, CVaR_up - CVaR_lo := b_hat, so D = (Gamma-1)/(Gamma+1)*
b_hat -- a benchmark assumption, not a general consequence of §32.
Reports realised cost across a confounding sweep: the robust controller bounds the WORST-CASE cost (pessimism) and wins beyond a problem-dependent confounding threshold, while paying a measurable premium near zero confounding (its conservatism then costs more than it saves) -- the honest robustness trade-off, not a universal win.
confounding_robust_control_certificate(effect_estimate=1.3, halfwidth=0.25, target=1.0, overshoot_penalty=3.0, undershoot_penalty=1.0)
¶
Confirm the closed-form minimax control is the numeric argmin and strictly beats CE.
Grid-searches the worst-case loss over u (the analytic u_robust should be its argmin),
then checks the robust control is shifted below CE and its worst-case loss is strictly smaller,
while a symmetric loss reproduces CE exactly.
confounding_robust_lq_regret(regret_sensitivity, stat_error, confounding_halfwidth)
¶
Confounding-robust LQ regret bound L_reg*(eps_stat + Delta)^2 (Rocq cr_regret).
Delta is the confounding half-width on the effect estimate -- the §32 inflation
(chc.uncertainty.confounding_robust_inflation), an irreducible bias floor that does
not vanish with sample size. Pushed through the order-doubling regret map it becomes a
control-regret floor L_reg*Delta^2: the LOCAL LQ control regret is SECOND-order in the
confounding-induced effect bias (Rocq floor_below_linear: L_reg*Delta^2<=L_reg*Delta on
[0,1]) -- a local-toy statement, not a general robustness claim. Delta=0 (Gamma=1,
point identification) recovers the statistical regret L_reg*eps^2.
confounding_robust_lq_regret_matrix(curvature, stat_error, confounding)
¶
Matrix confounding-robust LQ regret tr(E^T H E), E = stat + confounding.
The Frobenius lift of the scalar bound via the §21 matrix order-doubling (Rocq
regret_order_2p): for a positive-semidefinite LQ regret curvature H and effect-error
matrix E, the regret is the quadratic form tr(E^T H E) = O(||E||_F^2). The confounding
floor (stat = 0) is tr(Delta^T H Delta) -- SECOND order in the Frobenius norm of the
matrix confounding half-width, the same second-order robustness as the scalar §33. Reduces to
confounding_robust_lq_regret in the 1x1 case.
confounding_robust_tracking_benchmark(a=0.6, b_true=2.0, x_target=1.0, overshoot_penalty=1.0, undershoot_penalty=4.0, sensitivity_gamma=2.5, target_confounding=0.8, confounding_levels=(0.0, 0.4, 0.8, 1.2), incentive_demand_corr=1.0, action_noise=0.6, n_steps=30, noise_scale=0.05, n_markets=200, n_periods=400, seed=0)
¶
The DYNAMIC §37: does the confounding-robust controller beat CE in CLOSED LOOP over time?
Same honest story as confounding_robust_control_benchmark, now closed-loop over
n_steps on the confounded plant x' = a*x + b_true*u + noise: a demand confounder biases
the offline b_hat, the CE controller under-actuates and undershoots x_target (expensive
when churn = undershoot_penalty dominates budget waste), the §35 controller uses the §32
radius (calibrated D = (Gamma-1)/(Gamma+1)*b_hat, a benchmark assumption) to hedge. Reports
mean realised closed-loop cost across a confounding sweep: robust bounds the worst case and wins
beyond a problem-dependent threshold, paying a bounded premium near zero confounding.
confounding_robust_tracking_loop(a, b_true, b_hat, halfwidth, x_target, overshoot_penalty, undershoot_penalty, x0=0.0, n_steps=30, noise_scale=0.05, seed=0)
¶
Roll the confounded scalar plant x' = a*x + b_true*u + noise under the §35 controller.
Lifts the STATIC §35 minimax into a receding-horizon CLOSED LOOP (the reviewers' recurring
critique): each step aims the one-step target tau_t = x_target - a*x_t with
confounding_robust_control trusting the biased offline b_hat while the TRUE plant
uses b_true (the model/plant split of chc.flagship.closed_loop, in NumPy to keep
this module x64-flag-independent). halfwidth = 0 recovers certainty-equivalence exactly (the
§35 formula collapses to tau/b_hat); halfwidth > 0 (the §32 radius) is the robust
controller. Returns the realised trajectory, the applied controls, and the accumulated
ASYMMETRIC stage cost Sum_t [alpha*(x_t - x_target)_+ + beta*(x_target - x_t)_+].
lq_regret_sensitivity(effect, effort, target)
¶
Second-order regret sensitivity L_reg = target^2*(r-b^2)^2/(b^2+r)^3 of the LQ toy.
The leading coefficient of the certainty-equivalence regret in the effect error: applying the
optimum for a wrong effect b_hat = b + delta costs L_reg*delta^2 + O(delta^3) on the
true plant (order-doubling -- linear effect error, quadratic control regret). Verified against
the exact regret in validation/confounding_lq_regret.mac / the certificate below.
Nonnegative; zero at the degenerate r = b^2 (where u* is locally flat in the effect).
worst_case_asymmetric_loss(control, effect_estimate, halfwidth, target, overshoot_penalty, undershoot_penalty)
¶
Worst-case asymmetric loss of applying control over the §32 effect interval.
For u > 0 the outcome is monotone in b, so the worst overshoot is at b_hat+D and the
worst undershoot at b_hat-D; the worst-case loss is the max of the two weighted tails.
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.
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_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.
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.
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.
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.
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.