L07: Earth's Thermostat
Real data meets the model. We take a two-box climate model, fit it to 146 years of NASA temperature records with Markov chain Monte Carlo, and ask how much the past really tells us about the future. The model is small enough to run a million times. The past record, it turns out, pins down one number well (the transient response) and leaves the one people care about most (equilibrium sensitivity) mostly to the priors.

Movie: output/mcmc_walkers.mp4 (1280×600, 8 s, H.264). 64 emcee walkers start from random prior draws and climb onto the posterior. Left: each walker's (ECS, aerosol scale), with the final posterior as white contours. Right: each walker's simulated climate against NASA GISTEMP.
1. The question
Doubling CO₂ adds about 3.9 W m⁻² of energy to the climate system. How much warmer does the planet get? Two numbers summarise the answer:
- ECS (equilibrium climate sensitivity): the warming once the deep ocean has caught up, centuries later.
- TCR (transient climate response): the warming at the moment of doubling when CO₂ rises 1 %/yr. It is roughly what we live through this century.
IPCC AR6 (2021) assessed ECS at 3 K (likely 2.5 to 4 K) and TCR at 1.8 K (likely 1.4 to 2.2 K), combining process understanding, paleoclimate, the historical record and models. This level uses only the historical record plus physically motivated priors, and turns the question into a calibration problem:
- Which parameter values of a simple energy-balance model are consistent with observed warming, given realistic, autocorrelated year-to-year noise?
- How do we know the sampler and the likelihood are working? (diagnostics, posterior predictive checks, synthetic-truth recovery, coverage)
- Which parameters actually drive the uncertainty, in the past and in 2100? (Sobol vs one-at-a-time)
- What does the calibrated model project for the five IPCC SSP scenarios, and what can such a model not say?
2. The model
Two-layer energy balance (Held et al. 2010; Geoffroy et al. 2013a). A mixed layer (atmosphere, land and upper ocean) with temperature $T$ sits on a deep ocean with temperature $T_d$:
$$C\,\frac{dT}{dt} = F(t) - \lambda T - \gamma\,(T - T_d), \qquad C_d\,\frac{dT_d}{dt} = \gamma\,(T - T_d)$$
$$F(t) = F_{\text{non-aer}}(t) + \alpha_{aer}\,F_{aer}(t)$$
| Symbol | Meaning | Units |
|---|---|---|
| $\lambda$ | climate feedback parameter (how fast extra heat radiates away) | W m⁻² K⁻¹ |
| $\gamma$ | heat exchange between the layers | W m⁻² K⁻¹ |
| $C,\ C_d$ | heat capacities of the mixed layer and deep ocean | W yr m⁻² K⁻¹ (1 unit ≈ 7.7 m of seawater over the whole globe) |
| $\alpha_{aer}$ | scale factor on the IPCC aerosol forcing (the most uncertain forcing) | 1 |
Derived quantities: $\text{ECS} = F_{2\times}/\lambda$ with $F_{2\times}=3.93$ W m⁻² (AR6). TCR is the mean warming over years 61 to 80 of a 1 %/yr CO₂ ramp. The Earth's energy imbalance is $N = F - \lambda T$. The system has two e-folding modes: a fast one (years, the mixed layer) and a slow one (centuries, the deep ocean).
Exact integration. The model is linear, $\dot{\mathbf x} = A\mathbf x + B F$. With annual forcing held constant over each year, the one-year map $\mathbf x_{n+1} = e^{A}\mathbf x_n + A^{-1}(e^{A}-I)B\,F_n$ is exact. $e^A$ comes from Sylvester's formula for a 2×2 matrix with real eigenvalues, written out in closed form inside a Numba kernel. There is no time-step error, and one run of 1750 to 2100 costs a few microseconds per parameter set.
Statistical model. Observed anomaly = model + baseline offset + AR(1) noise:
$$y_t = T_t(\theta) + T_0 + r_t, \qquad r_t = \rho\, r_{t-1} + e_t,\quad e_t\sim\mathcal N(0,\sigma^2)$$
The exact AR(1) likelihood draws $r_1$ from the stationary law $\mathcal N(0,\sigma^2/(1-\rho^2))$ and whitens the rest: $e_t = r_t - \rho r_{t-1}$. AR(1) "red noise" is Hasselmann's (1976) picture of weather forcing a slow ocean. It lumps internal variability (ENSO and the rest), measurement error and model error into one process.
Priors (8 parameters):
| Parameter | Prior | Why |
|---|---|---|
| $\lambda$ | Uniform(0.2, 4) | broad; implies an ECS prior of 1 to 20 K, heavy toward low ECS (∝ 1/ECS²) |
| $\gamma,\ C,\ C_d$ | lognormal centred on 35 CMIP6 two-layer fits (AR6 Ch. 7 data), log-spread ×2 | medians 0.60, 8.1, 99; physics from climate models, widened because an ensemble shares biases |
| $\alpha_{aer}$ | Normal(1, 0.35), truncated to [0, 3] | AR6 aerosol ERF 1750-2019 is −1.1 [−1.7, −0.4] W m⁻² |
| $T_0$ | Uniform(−1, 1) K | baseline offset between GISTEMP (1951-1980) and the model's 1750 zero |
| $\sigma$ | Uniform(0.01, 0.3) K | innovation noise |
| $\rho$ | Uniform(0, 0.95) | lag-1 autocorrelation |
Data (downloaded with requests, cached in data/, parsed with pandas):
| Source | What | Used for |
|---|---|---|
NASA GISTEMP v4 GLB.Ts+dSST.csv |
global land+ocean annual anomaly, 1880-2025 | the likelihood |
IPCC AR6 WG1 Ch. 7 AR6_ERF_1750-2019.csv |
assessed effective radiative forcing by agent | model input 1750-2019 |
NOAA Mauna Loa co2_annmean_mlo.csv |
annual CO₂, 1959-2025 | checking AR6 CO₂ forcing against measurements, and extending it to 2020-2025 |
IPCC Ch. 7 SSPs/ERF_ssp*_1750-2500.csv |
forcing for SSP1-1.9, 1-2.6, 2-4.5, 3-7.0, 5-8.5 | non-CO₂ forcing 2020-2025 (SSP2-4.5), and projections |
IPCC Ch. 7 cmip6_twolayer_tuning_params.csv |
two-layer fits to 35 CMIP6 models | priors on $\gamma, C, C_d$, and the CMIP6 ECS spread |
Assumptions. Global means only. Constant feedback $\lambda$ (no pattern effect, no deep-ocean efficacy). Forcing is prescribed (no carbon cycle). Forcing uncertainty enters only through $\alpha_{aer}$. The model starts in equilibrium in 1750.
3. M&S concepts you'll learn here
- Calibration as inference. Parameters are not "fitted". They get a posterior distribution $p(\theta\mid y)\propto p(y\mid\theta)\,p(\theta)$, and every downstream number carries that uncertainty.
- The likelihood is a noise model. Annual temperature residuals are strongly autocorrelated (ρ ≈ 0.56). Assuming independent residuals shrinks the TCR interval by ~40%, which is overconfidence you can measure.
- Ensemble MCMC (emcee). Affine-invariant ensembles need no hand-tuned proposal. Differential-evolution moves beat the default stretch move here.
- MCMC diagnostics. Look at trace plots, compute the integrated autocorrelation time τ (and require N > 50τ), check acceptance, and hunt for walkers parked in side modes. Burn-in is a diagnosis, not a constant.
- Posterior predictive checks. Replicate the data from the fitted model and ask whether the real record looks like a typical draw: band coverage, test statistics with Bayesian p-values, whitened-residual autocorrelation.
- Out-of-sample validation. Earth's energy imbalance is never used in the fit, but the model predicts it. That tests the physics, not just the curve fit.
- Synthetic-truth recovery and coverage. Generate fake Earths with known parameters and check that 90% intervals contain the truth about 90% of the time (simulation-based calibration, lite).
- Identifiability. A parameter can be "recovered" only because the posterior equals the prior. The ratio sd(post)/sd(prior) shows what the data actually learned.
- Degeneracy. Strong aerosol cooling plus high sensitivity fits the same past as weak aerosols plus low sensitivity, but gives different futures.
- Global sensitivity analysis. Sobol indices decompose output variance over the whole input space (S₁ = main effect, S_T = with interactions). One-at-a-time answers depend on where you stand.
- Projections vs predictions. Scenario projections are conditional on a forcing pathway and a model structure. The fan chart is honest only about the uncertainty that was modelled.
4. Tech stack
| Tool | Role here |
|---|---|
| Python 3.12, NumPy | everything numeric; vectorised likelihood across walkers |
Matplotlib (+ ffmpeg via imageio-ffmpeg) |
all figures (handmade corner plot, trace plots, fan charts) and the MP4 |
| SciPy | linalg.expm as the independent check of the integrator; ndimage.gaussian_filter to smooth corner-plot contours |
| Numba | the model kernel: @njit(parallel=True), one prange lane per parameter set, 3 threads |
| ★ pandas | parsing four CSV formats (GISTEMP's *** gaps, NOAA's # comments, IPCC's per-agent tables), splicing forcing series by year index, the posterior summary CSV |
| ★ requests | live downloads from NASA, NOAA and GitHub with timeouts and raise_for_status, cached to data/ so reruns work offline |
| ★ emcee | affine-invariant ensemble MCMC with vectorize=True (one Numba call per half-ensemble) and DEMove + DESnookerMove; get_autocorr_time, emcee.autocorr.integrated_time |
| ★ SALib | Saltelli/Sobol sampling (SALib.sample.sobol) and variance-based indices with bootstrap CIs (SALib.analyze.sobol); the bundled Ishigami function for verification |
SimPy, Mesa and NetworkX from earlier levels are still in the environment but are not needed here. pandas and requests turn "a model" into "a model that eats live data". emcee and SALib turn "a run" into "a distribution of runs you can reason about".
5. Industry & scientific tools
| Tool | What it is | Who uses it | How this project maps to it |
|---|---|---|---|
| FaIR (Finite-amplitude Impulse Response) | open-source Python emulator: emissions → concentrations → forcing → a 3-layer EBM | IPCC AR6 WG1 and WG3 (scenario warming classification), many research groups | our ebm is FaIR's temperature module with 2 boxes instead of 3. FaIR v2 adds the carbon cycle and calibrates with a constrained ensemble, which is the same Bayesian idea |
| MAGICC | reduced-complexity climate model, probabilistic set-up | IPCC WG3, IAM teams (IIASA, PIK), NGFS finance scenarios | same role: emulate ESMs cheaply and propagate parameter uncertainty to SSP projections |
| CMIP6 ESMs (CESM2, E3SM, UKESM, …) | 3-D coupled Earth system models on supercomputers | national labs, the CMIP6 archive behind every IPCC figure | our priors come from two-layer fits to these models; the white ticks in the ECS figure are 35 of them |
| PyMC / Stan | probabilistic programming with gradient-based HMC/NUTS | statistics, epidemiology, pharma, finance | the same posterior written as a model block; NUTS needs gradients (JAX/Stan autodiff through the time loop) but mixes far better in high dimensions |
| ArviZ | diagnostics and plots for Bayesian inference | PyMC/Stan/emcee users | R-hat, ESS, trace, pair and PPC plots; we hand-roll the emcee-native equivalents (τ, N/50τ, corner, PPC) |
| Dakota (Sandia) | optimisation, UQ and sensitivity toolkit that wraps any simulation code | DOE labs, aerospace, nuclear | sampling, sobol and Bayesian calibration (QUESO/DREAM) methods do what our SALib + emcee pipeline does, for expensive black-box models |
| UQLab (ETH Zürich) | MATLAB UQ framework: surrogates, Sobol, Bayesian inversion | engineering UQ | Sobol via polynomial chaos surrogates: the move when each model run costs hours, not microseconds |
| OpenTURNS | open-source C++/Python UQ library (EDF, Airbus, Phimeca) | energy and aerospace industry | probabilistic input models, Sobol/Shapley indices, calibration; supports dependent inputs, which our Sobol box ignores |
6. Results
All numbers below are printed by sim.py (seed 2021, deterministic).
The data, and a real-data sanity check on the forcing

- (a) Total forcing reached ~2.8 W m⁻² in 2019. Aerosols (blue, −1.06 W m⁻²) mask more than a quarter of the ~3.8 W m⁻² greenhouse-gas and ozone forcing, and that mask is the biggest source of uncertainty in the whole problem. Volcanic eruptions (grey spikes, clipped) are brief but large.
- (b) NOAA's measured CO₂ against IPCC's CO₂ forcing. A log law $F = k\ln(C/278.3)$ fitted through the origin gives k = 5.493 W m⁻², R² = 0.99985. That is close to the classic Myhre et al. (1998) 5.35; AR6's value is higher because it includes tropospheric adjustments. The implied $F_{2\times} = k\ln 2 = $ 3.81 W m⁻², inside AR6's 3.93 [3.46, 4.40]. It sits slightly low because the AR6 formula is not purely logarithmic.
- The same fit extends CO₂ forcing to 2020-2025 from measurements. That comes out 0.03 to 0.06 W m⁻² below what the SSP2-4.5 scenario assumed. Other agents for 2020-2025 come from SSP2-4.5, which is a scenario, not an observation (see Simplifications).
- (c) GISTEMP v4, 1880-2025 (complete calendar years only).
MCMC: 64 walkers × 30 000 steps in 23 s

- Acceptance 0.131 (every walker 0.11 to 0.14). The textbook 0.2 to 0.5 target is for the stretch move. Here we use differential-evolution moves (
DEMove80%,DESnookerMove20%). In a development test (12 000 steps, earlier and wider prior) they had lower acceptance (0.14 vs 0.40) but shorter autocorrelation times (τ_max 112 vs 169 steps). The quantity that matters is effective samples per second, not acceptance. - Autocorrelation times τ (steps): λ 67, γ 103, C 64, C_d 177, α 59, T0 61, σ 59, ρ 60. The post-burn-in chain is 113 τ_max long (the rule of thumb is > 50), and the τ-vs-N panel shows the estimates flattening once N > 50τ. We keep every 88th step: 14 528 samples.
- A walker in a side mode. Walkers start from prior draws on purpose. One of them (walker 33) sat until step 7 364 in a spurious mode with ρ ≈ 0.94, T0 ≈ +0.5 K and λ ≈ 3.9: red noise with near-unit memory "explains" the warming, so the physics does not have to. Its log-probability was far below the ensemble's. With the first burn-in of 4 000 steps it leaked into the posterior as a small bump at ECS ≈ 1 K. The burn-in is now 10 000, and a check asserts that every walker's post-burn-in median log-probability is within 5 units of the ensemble (worst: 0.43).
Posterior (median [5 %, 95 %]; the full table is in output/posterior_summary.csv):
| λ [W m⁻²K⁻¹] | γ | C [W yr m⁻²K⁻¹] | C_d | α_aer | T0 [K] | σ [K] | ρ |
|---|---|---|---|---|---|---|---|
| 1.81 [1.29, 2.22] | 0.70 [0.33, 1.48] | 9.3 [6.4, 13.5] | 82 [21, 338] | 0.54 [0.31, 0.77] | −0.40 [−0.46, −0.33] | 0.098 [0.089, 0.109] | 0.56 [0.44, 0.69] |
Derived: ECS 2.17 [1.77, 3.04] K, TCR 1.60 [1.42, 1.80] K. The fast mode is 3.6 [2.2, 5.7] yr and the slow mode 169 [38, 822] yr (Geoffroy et al. 2013a found ~4 and ~200+ yr across CMIP5). The implied aerosol forcing in 2019 is −0.58 [−0.82, −0.33] W m⁻², about half of AR6's central −1.1 W m⁻² and in the weak end of its [−1.7, −0.4] range. Simple models fitted to global temperature alone often prefer weak aerosols. With an aerosol-forcing prior this uninformative, the record would rather have a modestly sensitive climate with little masking.

The corner plot (hand-made in matplotlib: 2-D histograms, Gaussian smoothing, 68/95% highest-density contours) shows the two ridges that make climate calibration hard:
- TCR vs aerosols (r = +0.64). More aerosol cooling in the past needs a more responsive climate to produce the same warming.
- ECS vs γ (r = +0.58). A high-sensitivity climate can hide its warming if the deep ocean takes up heat faster. The surface record cannot tell these apart, but 2100 can.
$C_d$ is essentially its prior (compare the dashed line). 146 years of surface temperature say almost nothing about the deep ocean.
Does the noise model matter? AR(1) vs white noise
The same MCMC with ρ fixed at 0 (independent residuals):
| Quantity | AR(1) likelihood | White-noise likelihood | 90% width, white / AR(1) |
|---|---|---|---|
| TCR [K] | 1.60 [1.42, 1.80] | 1.58 [1.47, 1.69] | 0.58 |
| α_aer | 0.54 [0.31, 0.77] | 0.50 [0.36, 0.66] | 0.65 |
| T0 [K] | −0.40 [−0.46, −0.33] | −0.41 [−0.44, −0.37] | 0.58 |
| ECS [K] | 2.17 [1.77, 3.04] | 2.21 [1.86, 3.23] | 1.08 |
| C | 9.3 [6.4, 13.5] | 11.3 [7.9, 16.0] | 1.13 |
With autocorrelated residuals, 146 years hold only about 146·(1−ρ)/(1+ρ) ≈ 41 independent pieces of information. The white-noise fit believes it has 146, so the quantities the data really constrain (TCR, aerosol scale, baseline) come out 35 to 42% too narrow. Quantities whose uncertainty comes from the prior and the degeneracies (ECS, C) do not shrink; they shift. Overconfidence hits exactly the numbers you would be tempted to quote.
Posterior predictive checks and an out-of-sample test

- (a) Coverage. 3 000 replicated records (posterior draw + AR(1) noise). 93.2% of observed years fall inside the 90% predictive band (target 90%). The 10 misses cluster in the 1880s, the unusually cold 1909-1917, the warm early 1940s (a known WWII-era sea-surface-temperature measurement bias plus a strong El Niño) and 2024, the record El Niño year. That year sits above the band because the model's 2020-2025 forcing is a scenario and misses the recent extra warming factors (Simplifications).
- (b) Residual autocorrelation. Raw residuals have lag-1 ACF +0.53. Whitening with the posterior ρ brings it to −0.03. Honest miss: lag 4 keeps +0.25, above the white-noise band (±0.16). That is the ENSO quasi-periodicity (3 to 7 yr) that an AR(1) process cannot represent. An ARMA(2,1) or an explicit ENSO index would fix it.
- (c) Test statistics. The observed 1975-2025 trend (0.207 K/decade) and the detrended lag-1 autocorrelation (0.54) are typical of replicated records: Bayesian p = 0.65 and 0.77 (bad would be < 0.05 or > 0.95).
- (d) Earth's energy imbalance, never used in the fit:
| Period | Posterior $N=F-\lambda T$ [W m⁻²] | AR6 assessed (Table 7.1) |
|---|---|---|
| 1971-2018 | 0.56 [0.36, 0.90] | 0.57 [0.43, 0.72] |
| 2006-2018 | 0.76 [0.47, 1.28] | 0.79 [0.52, 1.06] |
The medians agree to within 0.03 W m⁻², which is independent evidence that the calibrated heat uptake is physical. The posterior is wider than AR6's observational range, so adding ocean heat content to the likelihood would sharpen $\gamma$ and $C_d$ (Go further #2).
- Warming 2011-2020 vs 1850-1900: forced response 1.17 [1.07, 1.26] K. GISTEMP on the model's baseline gives 1.14 K, against AR6's observed 1.09 [0.95, 1.20] K. In 2025, GISTEMP is at 1.49 K and the forced response at 1.37 K.
Synthetic truth: can the pipeline recover a known Earth?

We generate a fake record from the model with known parameters (λ = 1.3 so ECS = 3.02 K, γ = 0.7, C = 8, C_d = 100, α = 1, T0 = −0.35, σ = 0.09, ρ = 0.5), using the same years, forcing and AR(1) noise, then run the identical MCMC.
| ECS | TCR | λ | γ | C | C_d | α | T0 | σ | ρ | |
|---|---|---|---|---|---|---|---|---|---|---|
| truth | 3.02 | 1.98 | 1.30 | 0.70 | 8.0 | 100 | 1.00 | −0.35 | 0.090 | 0.50 |
| posterior median | 2.90 | 2.02 | 1.36 | 0.62 | 7.9 | 94 | 1.03 | −0.34 | 0.091 | 0.51 |
| inside 90% CI | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
| sd(post)/sd(prior) | 0.30 | 0.11 | 0.22 | 1.02 | 0.64 | 0.97 | 0.24 | 0.06 | 0.07 | 0.28 |
All 10 truths lie inside their 90% intervals. The last row is the real lesson. TCR is learned well (the posterior is 9× narrower than the prior), ECS moderately (3×), and γ and C_d are not learned at all (ratio ≈ 1): they are "recovered" only because the posterior is the prior. Even when the model is exactly right, the surface record cannot identify deep-ocean heat uptake.
Coverage test (bottom panel). A single recovery can succeed by luck. So we draw 40 truths from the prior, simulate 40 synthetic Earths and run 40 MCMCs (32 walkers × 5 000 steps each). The 90% intervals contain the truth in 37/40 (ECS), 37/40 (TCR), 36/40 (α), 36/40 (σ) of them, against an expected 36 ± 1.9 (binomial 99% range ≥ 30). The inference is calibrated. This is a small-scale version of simulation-based calibration (Talts et al. 2018).
ECS and TCR vs IPCC AR6

| This posterior: median, 66%, 90% | IPCC AR6: best, likely (66%), very likely (90%) | |
|---|---|---|
| ECS | 2.17 K, [1.91, 2.56], [1.77, 3.04] | 3.0 K, [2.5, 4.0], [2.0, 5.0] |
| TCR | 1.60 K, [1.50, 1.71], [1.42, 1.80] | 1.8 K, [1.4, 2.2], [1.2, 2.4] |
- TCR agrees with AR6: 96% of the posterior lies inside AR6's likely range. The historical record constrains TCR well, because TCR is the historical-type experiment.
- ECS is low: P(ECS < 2 K) = 0.28, P(2.5 ≤ ECS ≤ 4) = 0.19, P(ECS > 4) = 0.009. This is a known result, not a new finding. Historical warming has occurred with a sea-surface-temperature pattern that radiates heat away efficiently. As the pattern evolves, feedbacks weaken, so the effective λ seen so far is larger than the long-term λ (the pattern effect; Armour 2017; AR6 Ch. 7, sections 7.4.4 and 7.5.2). A constant-λ model cannot represent this, so it turns an effective sensitivity into ECS. AR6 corrects historical estimates upward for this effect and combines them with process and paleoclimate evidence. Our number is a historical-record-only effective sensitivity with no such correction.
- The ECS tail is set by the priors, not the data. While developing this level, two summary rows (
Mean,StDev) in the CMIP6 table were accidentally read as models, which made the priors on γ, C and C_d wider. That run gave ECS 2.28 [1.81, 3.51] K but TCR 1.61 [1.42, 1.81] K. The priors moved the ECS 95th percentile by 0.5 K and TCR by 0.01 K. The white ticks are the 35 CMIP6 models' ECS (1.98 to 6.63 K); 26 of them lie above our 95th percentile.
Global sensitivity: Sobol vs one-at-a-time

Inputs are λ ∈ [0.6, 2.4] (ECS 1.6 to 6.6 K), γ ∈ [0.3, 1.2], C ∈ [5, 12], C_d ∈ [40, 200] and α ∈ [0.3, 1.7], treated as independent uniforms. SALib Saltelli sampling uses N = 4096, giving 4096 × (5+2) = 28 672 model runs (about a second with Numba, including the bootstrap confidence intervals).
| Output | S₁: λ | γ | C | C_d | α | S_T: λ | γ | C | C_d | α | Σ S₁ |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 2015-2024 warming | 0.547 | 0.056 | 0.006 | 0.007 | 0.327 | 0.598 | 0.073 | 0.008 | 0.013 | 0.365 | 0.943 |
| 2081-2100 warming, SSP2-4.5 | 0.881 | 0.042 | 0.000 | 0.025 | 0.020 | 0.908 | 0.062 | 0.001 | 0.037 | 0.024 | 0.968 |
- The past and the future care about different things. Today's warming depends on feedback and aerosols (S₁ = 0.55 and 0.33). 2100's depends almost only on feedback (0.88), because aerosol emissions fall in every SSP. This is the structural reason ECS is hard to pin down from history: the parameter that matters for the future shares the historical signal with one that won't.
- Interactions are small but real: Σ S₁ = 0.94 and 0.97, so 3 to 6% of the variance comes only from parameter combinations. S_T − S₁ is largest for λ and α in today's warming (0.05 and 0.04).
- One-at-a-time (panel b) sweeps each parameter over its range with the others frozen. At the box centre (ECS 2.6 K) the swings in 2100 warming are λ 2.73, γ 0.52, C_d 0.43, α 0.35, C 0.04 K, the same ranking as Sobol. Freeze the others at a high-sensitivity point (λ = 0.8) instead and the swings of γ, C_d and α grow 2.5×, 2.3× and 1.9× (1.30, 0.98, 0.66 K). OAT reports the local slope at whatever point you froze, and explores only 5 lines through a 5-D box. Sobol averages over the whole box and reports how much variance is left unexplained by main effects. Here OAT happens to get the ranking right, but not the magnitudes.
- Caveat: Sobol indices describe the box you chose. They assume independent inputs. The posterior is strongly correlated (the ridges above), so a posterior variance decomposition needs Shapley effects or dependent-input methods (OpenTURNS).
Projections: the fan chart, and what it can't say

Warming 2081-2100 relative to 1850-1900 (median [5 %, 95 %]), 3 000 posterior draws per scenario:
| Scenario | This model | IPCC AR6 (SPM Table SPM.1) |
|---|---|---|
| SSP1-1.9 | 1.18 [0.99, 1.51] | 1.4 [1.0, 1.8] |
| SSP1-2.6 | 1.52 [1.29, 1.88] | 1.8 [1.3, 2.4] |
| SSP2-4.5 | 2.42 [2.11, 2.83] | 2.7 [2.1, 3.5] |
| SSP3-7.0 | 3.37 [3.00, 3.80] | 3.6 [2.8, 4.6] |
| SSP5-8.5 | 3.97 [3.52, 4.50] | 4.4 [3.3, 5.7] |
Every median is 0.2 to 0.4 K below AR6 and every range is narrower, especially at the top. Both follow from what the model leaves out:
- Low medians come from the low ECS (the pattern effect above). Over this century, projected warming tracks TCR more than ECS, which is why the gap is only 0.2 to 0.4 K.
- Narrow ranges come from uncertainty that is simply not in the model: CO₂ and other non-aerosol forcing uncertainty ($F_{2\times}$ alone is ±12%), the pattern effect / state-dependent feedbacks, carbon-cycle feedbacks (the forcing is prescribed), and structural uncertainty. AR6's ranges are built from several lines of evidence and emulators calibrated to all of them.
- What this model cannot tell you at all: regional change, extremes, precipitation, sea level, tipping points or abrupt change, anything after 2100 that depends on ice sheets. The thin gold line is "one simulated world" (a posterior draw plus AR(1) weather). The real path will wiggle like that around the fan, with record years and pauses.
So the honest headline is: given this model structure and these priors, the historical record implies roughly the AR6 central warming, somewhat lower, with an uncertainty range we know to be too narrow.
7. Run it
# from SimulationLab/
uv run python L07_earths_thermostat/sim.py
Runtime: 232 s (≈ 4 min wall-clock with uv start-up) on the 8-core dev machine while other agents were running, using 3 Numba threads. Measured breakdown from the section timestamps the script prints: main MCMC 23 s (1.9 M walker-steps), white-noise MCMC 8 s, synthetic recovery 9 s, coverage test 122 s (40 MCMCs, ~3 s each), static figures 14 s, Sobol + OAT ~1 s (28 672 + 210 model runs), projections < 1 s, MP4 51 s. The first run also downloads 1.3 MB (9 files) into data/. Delete a file there to refresh it; otherwise reruns are offline. Everything is seeded (SEED = 2021), and the numbers above reproduce exactly (only the section timings vary).
Outputs in output/: hero.png, projections.png, data_and_forcing.png, mcmc_diagnostics.png, corner.png, posterior_predictive.png, synthetic_truth.png, ecs_tcr_vs_ar6.png, sobol_vs_oat.png, mcmc_walkers.mp4, posterior_summary.csv. The script ends with a PASS/FAIL/REPORT table and raises AssertionError if any check fails:
| Check | Expected | Got | |
|---|---|---|---|
| AR6 CO₂ ERF = k ln(C/C₀) on Mauna Loa CO₂ | R² > 0.99 | R² = 0.99985 | PASS |
| implied F₂ₓ = k ln 2 vs AR6 [3.46, 4.40] | inside | 3.81 W m⁻² | PASS |
exact integrator vs scipy.linalg.expm (abrupt 4×CO₂, 5000 yr) |
< 1e-9 K | 4.4e-14 K | PASS |
| equilibrium after 5000 yr, T / (F/λ) | 1 ± 1e-4 | 1.000000 | PASS |
| SALib Sobol on Ishigami vs analytic S₁, S_T | < 0.02 | 0.0023 | PASS |
| emcee: no stuck walkers (acceptance of every walker) | > 0.05 | min 0.113, mean 0.131 | PASS |
| chain length after burn-in / τ_max | > 50 | 113 | PASS |
| no walker parked in a side mode after burn-in | median log-prob gap < 5 | 0.43 | PASS |
| GISTEMP inside the 90% posterior predictive band | 0.80 to 0.98 | 0.932 | PASS |
| energy imbalance 2006-2018 median vs AR6 [0.52, 1.06] (out of sample) | inside | 0.76 W m⁻² | PASS |
| forced warming 2011-2020 vs AR6 [0.95, 1.20] | inside | 1.17 K | PASS |
| synthetic truth inside 90% CI (10 quantities) | ≥ 8 of 10 | 10 of 10 | PASS |
| synthetic truth inside 99% CI | 10 of 10 | 10 of 10 | PASS |
| 90% CI coverage over 40 prior-drawn worlds (ECS, TCR, α, σ) | ≥ 30/40 each | 37/37/36/36 | PASS |
| TCR median inside AR6 very likely [1.2, 2.4] | inside | 1.60 K | PASS |
| (reported) white-noise likelihood, TCR 90% range | narrower | ×0.58 | REPORT |
| (reported) whitened residuals, max |ACF| lags 1-10 | < 0.16 | 0.25 at lag 4 | miss (ENSO) |
| (reported) ECS median inside AR6 likely [2.5, 4.0] | inside | 2.17 K | miss (pattern effect) |
8. Code tour (sim.py)
| Function | What it does |
|---|---|
fetch, load_data |
cached requests download; pandas parsing of GISTEMP (*** gaps), NOAA (# header), IPCC ERF and CMIP6 tables |
groups, build_forcing, split |
ERF → CO₂ / other / aerosol / natural; Mauna Loa log-law fit and 2020-2025 extension; SSP splicing with a tapered offset; (non-aerosol, aerosol) arrays for the model |
ebm |
the model: Numba prange kernel, exact exponential map per parameter set, annual-mean output |
model, tcr, timescales |
contiguous-array wrapper; 1 %/yr CO₂ experiment; fast/slow mode e-folding times |
cmip6_prior, log_prior, sample_prior |
CMIP6-informed lognormals; hard prior box; rejection sampler (used for walker starts, prior overlays and coverage truths) |
log_like, log_prob |
exact AR(1) Gaussian likelihood; vectorised posterior (only in-prior walkers run the model) |
run_mcmc, posterior |
emcee sampler (DE + snooker moves, seeded, optional ρ = 0); burn-in, τ, thinning |
verify_integrator, verify_sobol |
closed form vs scipy.linalg.expm; SALib vs the analytic Ishigami indices |
posterior_predictive |
replicated records, band coverage, trend and ACF test statistics, whitened residuals, energy imbalance |
synthetic_recovery, coverage_test |
known-truth MCMC with contraction ratios; 40 prior-drawn worlds for interval coverage |
sensitivity, warming |
SALib Sobol for two outputs; one-at-a-time sweeps at two nominal points |
projections |
3 000 posterior draws × 5 SSPs, rebased to 1850-1900; one noisy "simulated world" |
main |
the pipeline, printed numbers and the PASS/FAIL table |
fig_*, make_hero, mp4_walkers, credible_contours, fan, spread |
plotting only |
9. Simplifications & ceilings
- Constant feedback, no efficacy. The biggest structural limit. The pattern effect makes historical-only ECS estimates low (above). Upgrade: a deep-ocean efficacy ε (Geoffroy 2013b; the CMIP6 table already has it), or a time-varying λ.
- Forcing uncertainty = one aerosol scale. CO₂, other GHGs, volcanic and solar forcing are taken as exact and $F_{2\times}$ is fixed at 3.93. The IPCC repo ships
_pc05/_pc95forcing files that could become prior draws. - 2020-2025 forcing is partly a scenario. CO₂ comes from measurements, but everything else follows SSP2-4.5. That misses the 2020 IMO shipping-sulphur cut, the 2022 Hunga Tonga water vapour, the solar-cycle-25 maximum and any aerosol decline beyond the scenario, all candidates for the 2023-2024 warmth that sits above our band.
- SSP splice (marked
ponytail:in the code). The SSPs diverge from 2015, but our 2020-2025 history follows SSP2-4.5, so each scenario is joined to the 2025 state with an offset that tapers linearly to zero by 2050. Per forcing group the offsets reach 0.27 W m⁻² (SSP1-1.9 aerosols); the net offsets are −0.04 to −0.26 W m⁻². 2081-2100 is untouched, but 2025-2050 is approximate. Per-agent harmonisation (as FaIR andanerisdo it) is the upgrade. - AR(1) is the whole error model. Internal variability, GISTEMP measurement uncertainty (larger before 1950) and model structural error are one red-noise process. The lag-4 ENSO signal is left over. A Kennedy & O'Hagan discrepancy term or a state-space (Kalman-filter) EBM (Cummins et al. 2020) separates them.
- Priors are choices. Uniform λ favours low ECS; the CMIP6 widening factor (×2, marked
ponytail:in the code) is a judgement call. We showed that the ECS tail moves by 0.5 K with it. - Coverage with 40 worlds detects gross miscalibration only (it checks 4 quantities at one level). Full SBC rank histograms would need hundreds of MCMCs, which is fine here (each takes ~2.5 s) but was cut to fit the 5-minute budget.
- Sobol over an independent box. The indices describe that box, not the correlated posterior.
- emcee is not the best sampler for this job. In 8-D with ridges it needs τ ≈ 60 to 180 steps. Gradient-based NUTS would mix faster but needs a differentiable model.
10. Go further
- Rebuild it in FaIR.
pip install fair(v2), drive it with the same SSP ERF files, and compare its AR6-calibrated ensemble with our posterior. Then calibrate FaIR's 3-layer EBM with this script's emcee setup. - Add ocean heat to the likelihood. Use NOAA NCEI 0-2000 m ocean heat content, or the von Schuckmann et al. (2023) Earth heat inventory, as a second data stream with its own noise model. Measure how much the posteriors of γ and C_d contract, and whether ECS moves.
- Add the pattern effect. Give the model efficacy ε with a CMIP6-informed prior, or let λ drift over time. How close does ECS get to AR6's 3 K, and how much wider do the projections become?
- Rewrite in PyMC or Stan. Write the time loop as
pytensor.scanor a Stanforloop, sample with NUTS, then use ArviZ for R-hat and ESS. Compare effective samples per second with emcee's. - Emulate an expensive model. Pretend each run costs an hour. Fit a Gaussian-process or polynomial-chaos surrogate on 200 runs (UQLab, OpenTURNS or
scikit-learn), run Sobol and MCMC on the surrogate, and check against the exact numbers here.
11. References
- I. M. Held et al., "Probing the fast and slow components of global warming by returning abruptly to preindustrial forcing," J. Climate 23, 2418 (2010).
- O. Geoffroy et al., "Transient climate response in a two-layer energy-balance model. Part I: Analytical solution and parameter calibration using CMIP5 AOGCM experiments," J. Climate 26, 1841 (2013a); "Part II: Representation of the efficacy of deep-ocean heat uptake and validation for CMIP5 AOGCMs," J. Climate 26, 1859 (2013b).
- P. Forster et al., "The Earth's energy budget, climate feedbacks, and climate sensitivity," Ch. 7 in Climate Change 2021: The Physical Science Basis (IPCC AR6 WG1, 2021), with data at github.com/IPCC-WG1/Chapter-7 (C. Smith et al.).
- IPCC, Summary for Policymakers in Climate Change 2021: The Physical Science Basis (Table SPM.1).
- S. C. Sherwood et al., "An assessment of Earth's climate sensitivity using multiple lines of evidence," Rev. Geophys. 58, e2019RG000678 (2020).
- K. C. Armour, "Energy budget constraints on climate sensitivity in light of inconstant climate feedbacks," Nature Climate Change 7, 331 (2017).
- N. J. L. Lenssen et al., "Improvements in the GISTEMP uncertainty model," J. Geophys. Res. Atmos. 124, 6307 (2019); GISTEMP Team, NASA GISS Surface Temperature Analysis v4.
- G. Myhre et al., "New estimates of radiative forcing due to well mixed greenhouse gases," Geophys. Res. Lett. 25, 2715 (1998).
- K. Hasselmann, "Stochastic climate models. Part I. Theory," Tellus 28, 473 (1976).
- D. P. Cummins, D. B. Stephenson, P. A. Stott, "Optimal estimation of stochastic energy balance model parameters," J. Climate 33, 7909 (2020).
- C. J. Smith et al., "FAIR v1.3: a simple emissions-based impulse response and carbon cycle model," Geosci. Model Dev. 11, 2273 (2018); N. J. Leach et al., "FaIRv2.0.0," GMD 14, 3007 (2021).
- D. Foreman-Mackey et al., "emcee: The MCMC Hammer," PASP 125, 306 (2013); J. Goodman, J. Weare, "Ensemble samplers with affine invariance," CAMCoS 5, 65 (2010).
- C. J. F. ter Braak, J. A. Vrugt, "Differential Evolution Markov Chain with snooker updater and fewer chains," Stat. Comput. 18, 435 (2008).
- I. M. Sobol', "Global sensitivity indices for nonlinear mathematical models and their Monte Carlo estimates," Math. Comput. Simul. 55, 271 (2001).
- A. Saltelli et al., "Variance based sensitivity analysis of model output. Design and estimator for the total sensitivity index," Comput. Phys. Commun. 181, 259 (2010); A. Saltelli, P. Annoni, "How to avoid a perfunctory sensitivity analysis," Environ. Model. Softw. 25, 1508 (2010).
- J. Herman, W. Usher, "SALib: An open-source Python library for sensitivity analysis," JOSS 2, 97 (2017).
- T. Ishigami, T. Homma, "An importance quantification technique in uncertainty analysis for computer models," Proc. ISUMA (1990).
- S. Talts et al., "Validating Bayesian inference algorithms with simulation-based calibration," arXiv:1804.06788 (2018).
- M. C. Kennedy, A. O'Hagan, "Bayesian calibration of computer models," J. R. Stat. Soc. B 63, 425 (2001).
- A. Gelman et al., Bayesian Data Analysis, 3rd ed. (CRC, 2013); A. Gelman et al., "Bayesian workflow," arXiv:2011.01808 (2020).