Skip to content

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:

  1. Calibrating Gamma before spending it (§32 (b)). Gamma cannot be tested against the data, but it can be priced. benchmark_gamma expresses it in units of the confounding the OBSERVED covariates carry -- dropping covariate j from the propensity produces exactly the pair of propensities the MSM bounds -- and reports the EXPONENT log(Gamma)/log(Gamma_strongest), because odds ratios compose multiplicatively. negative_control_gamma inverts a known-null outcome for the smallest Gamma that reconciles it, a lower bound on the confounding present: assuming less is refuted by the data, and inf says the model class is refuted instead.
  2. 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_mean give the radius inflation, confounding_robust_radius widens a pessimism radius (never optimistic, tight at Gamma=1, monotone).
  3. Regret floor (§33). confounding_robust_lq_regret -- the confounding effect-bias becomes a control-regret floor SECOND order in the bias (lq_regret_sensitivity for the toy L_reg; confounding_robust_lq_regret_matrix for the multivariate Frobenius lift).
  4. 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_improvement is the piecewise worst-case-loss gain, worst_case_asymmetric_loss the loss itself.
  5. 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.
  6. Safety (§40). The same radius, spent on a constraint instead of on the objective. robust_barrier_margin is the best barrier derivative guaranteed against every effect in the identified set, robust_safe_action its maximiser -- exactly zero once the radius swallows the control channel -- identification_radius_threshold the sharp radius at which certification dies and barrier_gamma_star the Gamma it 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_safety applies all of that along a finished chc.plan.CausalPlan -- the plan-level Gamma* is the weakest step's, so one uncertifiable step sinks the plan. barrier_reachability_gap prices 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 pointwise certified_but_unreachable says how much the per-step reading over-promises -- a filter, not a proof.
  7. Validation. confounding_robust_control_benchmark grounds it on a synthetic observational confounded marketplace and confounding_robust_tracking_benchmark on a confounded dynamic plant in closed loop; ConfoundingRobustTask carries the same radius into the main chc.benchmark leaderboard (regret vs oracle, multi-seed CIs) through ConfoundingRobustPenalty; the *_certificate functions 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_certificate measures 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. radius still comes from an assumed Gamma.

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 (or nan) -- nothing is certified, not even at Gamma = 1 where the radius is zero. nan, because no Gamma >= 1 works and reporting 1.0 would 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 -- Delta saturates at cvar_gap as Gamma -> 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 and inf says 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 at radius = 0. nan (the module's empty-set convention, as in admissible_action_interval).
  • 0 < D <= u_max * channel -- the case worth having a threshold for: d* lands in [0, channel) and certification holds iff radius <= 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:

  • oracle filters with the true gain and a zero radius -- the best achievable safe tracking;
  • safety_calibrated filters with the biased gain and the §32 radius at gamma;
  • regret_calibrated shrinks its action by the §33 accounting instead -- the radius spent on the objective, which is what this library did everywhere before §40;
  • greedy believes 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

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.

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 chc.plan.certify_safety.

1.0
tolerance float

grid-fraction slack when comparing the tube to {h >= 0}, absorbing the Lax-Friedrichs dissipation that erodes the zero level set by O(dx).

0.02

Raises:

Type Description
ValueError

as backward_reachable_tube does, and on an alpha that is not finite or a tolerance outside [0, 1].

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:

  1. 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.
  2. The null covariate's score is a root-n noise 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 that log(Gamma_null) * sqrt(n) stays bounded as n grows rather than that a single draw sits below a fixed number. Measured on six seeds per size, that product runs 2.1, 4.0, 5.0, 4.4 at n = 1000 .. 64000 while Gamma_null itself falls 1.069 -> 1.017; a single draw of 1.27 at n = 4000 is inside that distribution, which is exactly why one draw cannot be the gate.
  3. Ranking. On one design carrying strong / weak / null covariates, the implied Gamma ranks them in that order.
  4. The reported quantile is stabler in n than 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.
  5. The negative control lands on the endpoint, and grows with the planted bias. At the returned Gamma the binding interval endpoint sits at zero to solver tolerance, and a larger planted bias needs a larger Gamma -- plus inf when 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.