Pearl Layer 3 — Imagining

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.

L3

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.

CF typewhich structural mechanism is being counterfactually changed: Substitution (sub_elasticity / sub_cap), Fringe (capacity_share / entry_price), or Trajectory (override the per-year shock signals)

e.g. Substitution → "what if non-dominant suppliers had responded faster?"

factual line (gray) vs counterfactual (indigo)factual = the scenario as-written, fully simulated; counterfactual = same exogenous noise but with the intervention applied

e.g. If the gray line peaked at 3× and the indigo line peaked at 1.7×, the CF would have halved the spike

ATE per outcomemean of (counterfactual − factual) over the horizon. Sign indicates direction.

e.g. ATE(shortage) = −0.4 → the CF would have alleviated 0.4 units of shortage on average per year

ATE colors (green/red)green = the CF improved the outcome (lower price, lower shortage, higher supply); red = worsened it. Direction matters here — read the chart, not just the sign.
description texthuman-readable summary of the CF query returned by the backend (e.g. "what if substitution_elasticity had been 0.8?")

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.

L3

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()

Counterfactual parameters

0.0 = no substitution · 0.8 = strong substitution · controls how fast non-dominant suppliers fill the gap when export_restriction > 0

Maximum fraction of restricted volume that can ever be substituted