Counterfactual Analysis
Abduction-Action-Prediction: fix the exogenous noise sequence from the factual run, change one structural mechanism, replay to see what would have been.
How to read this output
The result shows two trajectories per outcome — factual (what actually happened under the scenario) vs counterfactual (what would have happened under your CF intervention, with the same noise) — plus per-outcome ATE tiles.
e.g. Substitution → "what if non-dominant suppliers had responded faster?"
e.g. If the gray line peaked at 3× and the indigo line peaked at 1.7×, the CF would have halved the spike
e.g. ATE(shortage) = −0.4 → the CF would have alleviated 0.4 units of shortage on average per year
Key takeaway: L3's value: it answers retroactive policy questions like 'would building this stockpile have helped in 2023?' — anchored to the specific realised history, not a hypothetical average.
What L3 (Counterfactual) computes
Formal: P(Y_x | X′ = x′, Y′ = y′) — given factual (x′, y′), what would Y have been under do(X = x)?
Three-step recipe (Pearl 2009, Causality §7)
1. ABDUCTION : recover the latent noise / exogenous trajectory u
from the factual run, so that M(u, x′) = y′
(in this codebase: store every per-year shock and
integrator state from the factual simulation)
2. ACTION : modify the SCM by applying do(X = x)
→ mutilated model M_x
3. PREDICTION : re-run M_x forward using the SAME recovered u
→ counterfactual trajectory Y_x | (x′, y′)Twin-network coupling (what makes this NOT just L2)
Factual run : M (u) = (X′, Y′) [observed past, fully fixed] Counterfactual run : M_x (u) = Y_x [same noise u, different mechanism] L2 would re-sample noise → 'a fresh forward simulation'. L3 conditions on the actual realised noise → 'what would HAVE been in THIS world, had the mechanism been different'.
Average Treatment Effect on the treated trajectory
ATE_t (Y) = Y_t^(counterfactual) − Y_t^(factual) mean ATE(Y) = (1/T) · Σ_t ATE_t (Y) [reported per outcome]
Caveat: L3 answers a strictly stronger question than L2: not 'what happens on average if we do(X)' but 'what would have happened in this exact realised history if we had done(X) instead'. Requires the SCM to be invertible to recover u — for this ODE that means storing the factual {K, I, P} per year and the per-year shock signals.
Source: src/minerals/pearl_layers.py — counterfactual_substitution(), counterfactual_fringe(), counterfactual_trajectory()