Statistical analysis (primarily linear regression) of climate model output from the CESM model (currently CESM1.2 NAHosMIP runs; formerly CESM2), with a focus on the Atlantic Meridional Overturning Circulation (AMOC) and its relationships to other climate variables.
- Extract AMOC-relevant diagnostics from CESM output (e.g. AMOC streamfunction strength, North Atlantic surface temperature/salinity, surface fluxes).
- Use linear regression to characterize relationships between AMOC strength and other climate variables and forcings.
- Quantify trends, sensitivities, and their statistical uncertainty.
- Produce publication-quality figures summarizing the results.
Developed with Python 3.10. Dependency versions in requirements.txt are
unpinned; the analysis is deterministic (no random components).
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtKey dependencies:
- numpy, pandas — numerical arrays and tabular data
- xarray, netCDF4, dask — reading CESM NetCDF output
- scipy, statsmodels — linear regression and statistics
(closed-form vectorized OLS for the per-grid-cell maps;
statsmodelsvalidates it and is available for single fits) - matplotlib, cartopy — figures and coastline maps (Cartopy downloads Natural Earth coastline data on first use, which needs network access)
amoc_analysis/
├── data/
│ ├── input/ # CESM model output (read-only — do not modify)
│ └── output/ # generated results, tables, figures (git-ignored)
├── src/ # analysis modules (data loading, regression, plotting)
├── scripts/ # thin command-line wrappers around src modules
├── requirements.txt
├── README.md
└── CLAUDE.md # coding conventions for this project
Model output lives in ./data/input/ (NetCDF). Treat this directory as read-only
reference data (see CLAUDE.md). The files are too large for git and are not
tracked, so a colleague has to obtain them separately and put them in
data/input/ under the exact names below. The names are hard-coded in
src/data_loader.py (INPUT_FILES, AMOC_FILE).
Model version. These runs come from CESM1.2, not CESM2. The files show CESM1: the
B1850CNcompset, thef19g16grid, a 26-level CAM initial file (cami_0000-01-01_1.9x2.5_L26), and the upstream path…/CESM1_AMOC_Data/…in each file'shistory. They do not record the minor version (theVersionattribute is an unfilled$Name$); 1.2 is per the data provider. Processed files are taggedCESM1(data_loader.SOURCE_ID). The earlier CESM2 inputs (monthly CMORized and CAM files) are kept for reference indata/input/old_data/, and the code no longer reads them.
TODO (data source): record where these files come from (archive/DOI/URL or internal path) so that the inputs can be reproduced. Per their
historyattribute, they were extracted with CDO from/oak/stanford/groups/sjdavis/Mahendra/CESM1_AMOC_Data/Output_Extracted/.
There is one file per simulation, named <CESM case>_annual_mean.nc
(netCDF 64-bit offset, CF-1.0, written by CDO 2.1.1):
- Annual means only. The upstream processing already averaged the fields to
one value per year, so no monthly data is available.
cdo seltimesteptrimmed each run to its first 100/105/301 years. - Dimensions are
time(unlimited, 1 step per year) ×lat= 96 ×lon= 144. This is the CAM finite-volume 1.9° × 2.5° grid.latruns from −90 to 90 and includes the half-width polar cells;lonruns from 0 to 357.5 °E. All fields arefloat32(time, lat, lon). None containsNaN. - Time axis.
timeisdoublewithunits = "years since YYYY-7-2 00:00:00"andcalendar = "365_day", values 0, 1, 2, ….cdo settaxiswrote these labels (mid-year stamps); they are not the model's own dates. CFyearsunits cannot be decoded by xarray/cftime, so the loader opens files withdecode_times=Falseand convertstimeinto an integer calendaryearequal to the reference year plus the offset. - Variables. Every file holds the same 39 raw CAM history fields, all
cell_methods = "time: mean", with native CAM names and units:
| Group | Variables | Units |
|---|---|---|
| Near-surface state | TREFHT (2 m temperature), TREFMNAV/TREFMXAV (mean daily min/max of TREFHT), QREFHT (specific humidity), RHREFHT (relative humidity) |
K; kg/kg; fraction |
| Precipitation | PRECT (total), PRECC (convective), PRECL (large-scale), PRECSH (shallow convective), PRECSC/PRECSL (convective / large-scale snow, water equiv.) |
m/s |
| Surface water & heat fluxes | QFLX (surface water flux), LHFLX (latent), SHFLX (sensible) |
kg/m²/s; W/m² |
| Surface radiation | FSDS, FSDSC, FSNS, FSNSC, FLDS, FLDSC, FLNS, FLNSC, SRFRAD |
W/m² |
| TOA radiation | SOLIN, FSNT, FSNTC, FLNT, FLNTC, FLUT, FLUTC |
W/m² |
| Clouds | CLDTOT, CLDLOW, CLDMED, CLDHGH (vertically integrated cloud fraction); SWCF, LWCF (cloud forcing) |
fraction; W/m² |
| Other | TMQ (precipitable water), SNOWHICE/SNOWHLND (snow depth over ice / land, water equiv.) |
kg/m²; m |
(Suffix C = clear-sky. S = surface and T = top of model. N = net,
D = downwelling and U = upwelling.)
AMOC is not in these files. It comes from separate CSV files (see below).
Nine runs: a 3 × 3 matrix of CO₂ level (1×, 2×, 4× preindustrial) × North
Atlantic freshwater hosing (NAHosMIP protocol: −0.3, 0, +0.3 Sv). Each run's
case name is built from those two forcings (data_loader.case_name):
[124]xCO2for runs without hosing, e.g.2xCO2.1xCO2is the preindustrial control.[124]xCO2_[pm]03Svfor hosed runs, wherep= +0.3 Sv (freshwater added) andm= −0.3 Sv (freshwater removed), e.g.4xCO2_m03Sv.
The case name labels the run everywhere in the code and in processed file names. The table follows the case-grid layout, with CO₂ level as rows and hosing as columns:
| Case name | File | CO₂ | Hosing | Years (n) |
|---|---|---|---|---|
1xCO2_m03Sv |
B1850CN_neghos0p3Sv_f19g16_NAHosMIP_v5_annual_mean.nc |
1× | −0.3 Sv | 2051–2155 (105) |
1xCO2 |
B1850CN_f19g16_GCC_piCtrl300yr_annual_mean.nc |
1× | 0 | 1850–2150 (301) |
1xCO2_p03Sv |
B1850CN_hos0p3Sv_f19g16_NAHosMIP_v5_annual_mean.nc |
1× | +0.3 Sv | 2051–2155 (105) |
2xCO2_m03Sv |
B1850CN_2xCO2_neghos0p3Sv_f19g16_NAHosMIP_v5_annual_mean.nc |
2× | −0.3 Sv | 2051–2155 (105) |
2xCO2 |
B1850CN_2xCO2_noh_f19g16_yr200_annual_mean.nc |
2× | 0 | 2051–2150 (100) |
2xCO2_p03Sv |
B1850CN_2xCO2_hos0p3Sv_f19g16_NAHosMIP_v5_annual_mean.nc |
2× | +0.3 Sv | 2051–2155 (105) |
4xCO2_m03Sv |
B1850CN_4xCO2_neghos0p3Sv_f19g16_NAHosMIP_v5_annual_mean.nc |
4× | −0.3 Sv | 2051–2155 (105) |
4xCO2 |
B1850CN_4xCO2_noh_f19g16_yr200_annual_mean.nc |
4× | 0 | 2051–2150 (100) |
4xCO2_p03Sv |
B1850CN_4xCO2_hos0p3Sv_f19g16_NAHosMIP_v5_annual_mean.nc |
4× | +0.3 Sv | 2051–2150 (100) |
Figures that map several cases use this grid, available as
data_loader.CASE_GRID: rows are CO₂ = 1, 2, 4× (top to bottom) and columns are
hosing = m03Sv, 0 Sv, p03Sv (left to right).
Caveats (inferred from the files; please confirm):
- Forcing values come from the CESM case names in the file names.
hos0p3Svis read as +0.3 Sv of freshwater added to the North Atlantic,neghos0p3Svas an equal amount removed (−0.3 Sv), andnohas no hosing. - Branch point. Every run except the
1xCO2control is labeled from 2051, which is control year 201 on the control's 1850-based labels. This matches theyr200in the no-hosing file names. The year labels come fromcdo settaxis, so an experiment's year and the control's year only correspond if the runs really branched there. - Unequal lengths. Run lengths differ: 100 years for
2xCO2,4xCO2and4xCO2_p03Sv, and 105 years for the other hosing runs. Pooled regressions use whatever years each run has. - Mislabeled
caseattribute. The2xCO2file's globalcaseattribute readsB1850CN_f19g16_GCC_piCtrl300yr, but its data warm steadily (global-meanTREFHT287.3 → 288.8 K over 100 years), unlike the control (~286.9 K). The file name, not the attribute, identifies the run.
data_loader.VARIABLES defines 46 analysis variables. Each is available for
all nine runs, and each is a regression predictand. Every variable has a
definition (a formula in CAM field names), units and a long_name, and each
loaded field records these, plus its source file, as attributes.
- CMIP names (3):
tas=TREFHT(K);prc=PRECC(convective precipitation);pr=PRECT(total precipitation,PRECC+PRECL), both in mm day⁻¹. - CAM names (36): every other field in the files keeps its CAM name (table
above). Water fluxes (
PRECL,PRECSH,PRECSC,PRECSL,QFLX) are converted to mm day⁻¹, likeprandprc. All other fields keep their native units. - Derived (7):
| Variable | Definition | Units | Meaning |
|---|---|---|---|
pr_minus_evap |
PRECT × 8.64e7 − QFLX × 86400 |
mm day⁻¹ | precipitation minus evaporation (P − E) |
prsn |
(PRECSC + PRECSL) × 8.64e7 |
mm day⁻¹ | snowfall (water equivalent) |
toa_net_down |
FSNT − FLNT |
W m⁻² | net downward radiation at top of model |
sfc_net_energy_down |
FSNS − FLNS − LHFLX − SHFLX − 3.337e8 × (PRECSC + PRECSL) |
W m⁻² | net downward surface energy flux (radiative + turbulent + melting of snowfall; see below) |
cloud_radiative_effect |
SWCF + LWCF |
W m⁻² | net cloud radiative effect at top of model |
diurnal_temperature_range |
TREFMXAV − TREFMNAV |
K | mean diurnal temperature range |
planetary_albedo |
1 − FSNT / SOLIN |
1 | planetary albedo (from annual-mean fluxes) |
As checks, the control's global means are P − E ≈ 0 (−0.0006 mm day⁻¹), and over 1850–2150 a top-of-model imbalance of −0.13 W m⁻² and a surface energy flux of −0.16 W m⁻², which agree as they must when the atmosphere stores no energy.
Surface energy flux and snowfall. CAM's LHFLX includes only the latent heat of
vaporization, so sfc_net_energy_down also subtracts the latent heat of fusion
needed to melt the snow that falls: L_f ρ_w (PRECSC + PRECSL), with the CESM
constants L_f = 3.337×10⁵ J kg⁻¹ and ρ_w = 1000 kg m⁻³
(data_loader.SNOW_MELT_ENERGY_PER_M). The term is about 0.5–0.7 W m⁻² in the
global mean and concentrated where snow falls. Without it the surface flux runs
0.5–0.7 W m⁻² above the top-of-model flux; with it the two agree to within
0.03 W m⁻² in the 2101–2150 means of the 1×, 2× and 4×CO₂ runs.
Water-flux units. All water fluxes (precipitation, snowfall, evaporation,
P − E) are in mm day⁻¹ of liquid water (data_loader.WATER_FLUX_UNITS). CAM
precipitation is a liquid-water-equivalent rate, and in these files the m/s label
is correct: m s⁻¹ × 1000 mm m⁻¹ × 86400 s day⁻¹ = × 8.64e7. QFLX is a mass flux in
kg m⁻² s⁻¹, and 1 kg m⁻² of water is a 1 mm layer, so it needs only × 86400. As a
check, the control's global-mean pr (2.877 mm day⁻¹) equals its global-mean
QFLX.
Corrected source-attribute errors:
RHREFHTis labeledfraction, but its values run from about 20 to 110, so it is in %.SRFRADis labeled "Net radiative flux at surface", but it equalsFSNS+FLDSto within 0.04 W m⁻², i.e. absorbed shortwave plus downwelling longwave, which is not a net flux. The net surface radiation isFSNS−FLNS.
None of the 39 fields contains missing values. Four fields have cells whose value never changes in any run over 2051–2150:
SNOWHICE(9740 cells): no sea ice ever forms there.SNOWHLND(9450 cells): ocean, plus ice-sheet cells held at the land model's 1 m snow cap.PRECSC(5530 cells, 46°S–37°N): no convective snow.CLDLOW(356 cells): probably high terrain such as Tibet and Antarctica, where CAM defines no low cloud (not checked cell by cell).
At those cells the fit is exact, so every slope coefficient and its standard error are 0, and the t- and p-values are undefined (NaN). They show as unstippled white on the maps.
Loader entry points:
open_experiment(case)returns the raw Dataset, with all 39 CAM fields, on an integeryeardimension. Use it for any field not inVARIABLES.load_annual_field(case, var)returns a(year, lat, lon)DataArray of anyVARIABLESentry, in the analysis units. Its attributes areunits,long_name,definitionandsource_file.amoc_strength_on_years(case, years)returns the AMOC series aligned toyears(NaN where not covered).
Two CSV files in data/input/ hold AMOC strength (Sv) at 26.5°N, the
latitude of the RAPID array. They have one column per run, labeled as below.
The mapping from column label to case name is INPUT_FILES[...]["amoc_column"]
in src/data_loader.py.
amoc_timeseries_26p5N_9experiments_v5_annual.csv(data_loader.AMOC_FILE) contains the annual-mean series used in the analysis. It has ayearcolumn (2051–2150, 100 years, no gaps) and one column per run, with no missing values.amoc_timeseries_26p5N_9experiments_v5_summary.csvis a reference file with one row per run: the CESM case name (experiment),label,mean_Sv,std_Sv(population, ddof = 0),yr1_Sv,yr100_Svandtrend_Sv_per_century(OLS). The code does not read it. The annual file reproduces all of its values, the year-100 values to within 0.001 Sv.
| Case name | CSV column label | mean ± std (Sv) | year 1 → year 100 (Sv) | trend (Sv/century) |
|---|---|---|---|---|
1xCO2_m03Sv |
1x CO2, -0.3 Sv |
25.8 ± 1.8 | 21.6 → 25.3 | +3.4 |
1xCO2 |
piControl |
19.8 ± 0.7 | 21.6 → 20.3 | −0.1 |
1xCO2_p03Sv |
1x CO2, +0.3 Sv |
9.0 ± 4.5 | 21.8 → 5.2 | −13.8 |
2xCO2_m03Sv |
2x CO2, -0.3 Sv |
23.1 ± 1.0 | 22.2 → 23.3 | +1.9 |
2xCO2 |
2x CO2, no hosing |
15.9 ± 1.7 | 22.6 → 14.7 | −4.6 |
2xCO2_p03Sv |
2x CO2, +0.3 Sv |
9.2 ± 5.1 | 22.5 → 5.1 | −13.5 |
4xCO2_m03Sv |
4x CO2, -0.3 Sv |
20.1 ± 0.7 | 22.4 → 20.1 | +0.9 |
4xCO2 |
4x CO2, no hosing |
11.8 ± 2.9 | 22.3 → 10.3 | −7.7 |
4xCO2_p03Sv |
4x CO2, +0.3 Sv |
8.1 ± 4.1 | 21.9 → 5.9 | −10.8 |
Transient AMOC recovery in 2xCO2_p03Sv. In this run AMOC jumps from
9.5 Sv (2077) to 26.8 Sv (2082) and falls back to about 6 Sv by 2086. The same
run's gridded subpolar North Atlantic temperature (45–65°N, 60–10°W) warms by
about 4 K over 2077–2081. The two datasets agree, so this looks like a genuine
model event, not a data error, and it is kept. It dominates that run's
2071–2080 and 2081–2090 decadal blocks.
How AMOC years line up with the gridded years. The AMOC years use the same
labels as the gridded files, and amoc_strength_on_years aligns the two by year.
AMOC covers the first 100 years of every perturbation run. The runs lasting 105
years get NaN AMOC for 2151–2155. The control's AMOC covers only 2051–2150 of its
1850–2150 gridded record. Regressions use complete-case deletion (see
CLAUDE.md), so every run contributes 100 years (900 pooled; 90 decadal
blocks). The best evidence that the control's AMOC years are the same years as
its gridded years is year 1: the control's 2051 value (21.63 Sv) almost equals
the first-year values of the 1×CO₂ hosing runs (21.64 and 21.75 Sv). That is
expected if 2051 is where they branched from the control. Correlating the
control's AMOC with North Atlantic temperature gives no clear answer, because
the control's AMOC variability is small.
scripts/make_scalar_timeseries.py writes one scalars_annual_CESM1_{case}.nc per
run. Each holds these series on the run's year axis:
amoc_strength(Sv), fromAMOC_FILEtas_global_mean(K), the area-weighted global annual-mean temperaturetas_interhemispheric_diff(K), the area-weighted NH mean minus the SH meanprecip_centroid_lat_20,precip_centroid_lat_30(°N), the precipitation-mass centroid latitude (an ITCZ-position index) over 20°S–20°N and 30°S–30°N. It is the area- and precipitation-weighted mean latitude of the zonal-mean total precipitation (pr),Σ φ·P·a / Σ P·a. Unlike an argmax, the centroid covers both branches of a double ITCZ, so it changes continuously instead of jumping between them.
Area weighting uses exact zonal-band weights, sin(edge_N) − sin(edge_S), which
handle the FV grid's half-width polar cells at ±90°.
Status of this section (CESM1.2 data). Every analysis below (gridded regressions, EOFs, scenario prediction, ITCZ regressions) has been run on the nine CESM1.2 runs, and all numbers quoted are CESM1.2 results, with water fluxes in mm day⁻¹. The earlier CESM2 regression outputs are kept for reference in
data/output/old_cesm2/regression/.
scripts/run_regressions.py regresses a gridded annual-mean predictand (one
time series per grid cell) on the scalar indices tas_global_mean (Tglob, K),
tas_interhemispheric_diff (dT_NS, K) and amoc_strength (AMOC at 26.5°N, Sv).
It runs for every analysis variable (46 predictands, see
Analysis variables). Each
predictand's field is read directly from the input files.
The years of all nine CESM1.2 runs (the 3 × 3 CO₂ × hosing matrix) are pooled into one fit per grid cell, with a single common intercept and no per-run fixed effects. The design largely decouples the two main predictors, because CO₂ sets global temperature while hosing sets AMOC. Across the pooled decadal samples, Tglob spans 285.0–291.6 K and AMOC 5.4–27.0 Sv, with corr(Tglob, AMOC) = 0.01. All sets use one common sample: the years where every predictor is present, which are the AMOC years 2051–2150. That is 900 annual rows (100 per run). The control's gridded years before 2051 and the 105-year runs' years 2151–2155 have no AMOC and are dropped (complete-case deletion).
Ten predictor sets are defined (one multi-panel coefficient map per set, per
predictand). By default only sets 5 & 10 are produced; pass --all-sets to any
of the regression scripts to produce all ten:
| Set | Predictors |
|---|---|
| 1–3 | each index alone: Tglob; dT_NS; AMOC |
| 4–6 | combinations: Tglob+dT_NS; Tglob+AMOC; Tglob+dT_NS+AMOC |
| 7 | orthogonalized, order tas→NS→AMOC: Tglob, dT_NS⊥Tglob, AMOC⊥(Tglob,dT_NS) |
| 8 | orthogonalized, order tas→AMOC→NS: Tglob, AMOC⊥Tglob, dT_NS⊥(Tglob,AMOC) |
| 9 | full quadratic (centered): Tglob, Tglob², AMOC, AMOC², dT_NS, dT_NS², Tglob·AMOC, Tglob·dT_NS, AMOC·dT_NS |
| 10 | Tglob × AMOC interaction (centered): Tglob, AMOC, Tglob·AMOC |
- Sets 4–6 use full multiple OLS, so each map is that predictor's partial coefficient (its effect with the other predictors held fixed).
- Sets 7–8 are Gram–Schmidt orthogonalizations (
add_orthogonalized_columns). Each residual column is the index with the earlier ones regressed out. The columns are therefore mutually orthogonal (VIF = 1) and give a hierarchical decomposition, whose attribution depends on the chosen order (compare 7 vs 8). - Set 9 is the full quadratic response surface (
add_quadratic_columns). The three base indices are centered on their pooled means before squares and products are formed. Its 9 term coefficients are mapped on a 3×3 grid. - Set 10 is the global-temperature × AMOC interaction model: Tglob, AMOC and
Tglob·AMOC, reusing the centered
add_quadratic_columnsterms. Because the main effects are centered, each main-effect coefficient is the response at the other index's pooled mean. The CESM1.2 centering means are Tglob = 288.48 K and AMOC = 15.86 Sv, stored ascentering_mean_*attributes in the NetCDF. The interaction coefficient is the same as in the uncentered form.
Coefficient maps (src/output.py) use the Equal Earth projection (longitudes
relabeled to −180…180 via output.centered_lon, so the grid's wrap point falls on
the map edge, not at 0°) and a diverging
colormap with symmetric bounds (white = 0). Water-related fields use RdBu, so
wetter or moister is blue (regression.WET_IS_BLUE). All other fields use
RdBu_r, so positive is red. The maps stipple cells where p > 0.05.
Case styling. Every figure that distinguishes cases uses one convention. Hosing
sets the color: −0.3, 0, +0.3 Sv = red, black, blue. CO₂ sets the line style (1×,
2×, 4× = solid, dashed, dotted; output.case_line_style) or, in scatter plots, the
marker (circle, triangle, square; output.case_marker_style). Scatter markers are
filled when each simulation contributes ≤ 10 points (e.g. decadal means) and open
outlines when it contributes more (e.g. ~100 annual values).
Shared axis ranges. Unless a plot specifies otherwise, panels of a multi-panel
figure whose axes carry the same units share the same axis range on those axes, so
they compare directly by eye: e.g. the per-simulation time-series panels (one K
range and one Sv range throughout), the per-mode PC-regression bars, and the ITCZ
panels (one latitude range). Temperature differences and anomalies (ΔT) count as a
different unit from absolute temperatures, so an absolute global-mean temperature
axis (~287 K) and an interhemispheric-difference axis (a few K) keep their own
ranges, as in predictor_scatter.pdf and the ITCZ scatter page.
Shared color scales. The same rule applies to color: map panels of one figure that show the same quantity in the same units share one color scale (one symmetric bound over all of them for diverging maps). This covers the nine cases of a case grid, the EOF patterns of one field, and coefficient maps with the same units; panels in different units (per-K vs per-Sv coefficients) keep their own scales.
Drawing maps. Gridded fields are drawn with output.draw_field, which projects
the cell corners itself. Cartopy's pcolormesh(transform=…) spent ~1.3 s per map
checking for cells that wrap the map edge; draw_field takes ~0.01 s. PDF books
are kept to one per variable, because matplotlib's PdfPages holds every page
in memory until the book is closed.
Outputs.
Flat in data/output/regression/, with a shared caveats README.txt:
<var>_coef.pdf— one PDF book per predictand, one page per predictor set.<var>_coef.nc— the coefficient/SE/t/p/R² fields, one NetCDF group per predictor set (set5,set10, …;regression.set_group). Read a set withxr.open_dataset(path, group="set10"); set 10's group attributes carry the centering means for its centered (q_) terms.scripts/plot_predictor_scatter.py,scripts/plot_scalar_timeseries.pyandscripts/plot_tglob_vs_amoc.pywrite the predictor scatter, time-series and AMOC-vs-Tglob plots todata/output/regression/.
Collinearity. The design is well conditioned for the default sets: VIF = 1.0002 for set 5 (Tglob, AMOC) and ≤ 1.19 for set 10. The three-index union is more collinear (decadal VIF: Tglob 9.3, AMOC 13.2, dT_NS 21.8), because dT_NS is largely a linear function of Tglob and AMOC. The partial coefficients of set 6 (Tglob + dT_NS + AMOC) are therefore weakly constrained.
Caveats: p-values are nominal OLS. Fits are validated against statsmodels
(agreement < 1e-6). Two consistency checks come out exact. Global-mean tas is
Tglob, so in any set containing Tglob the global mean of the tas-on-Tglob
coefficient is exactly 1. Every other tas coefficient then has an area-mean of
exactly 0, which also makes its NH and SH means equal and opposite.
Pooled n = 90 decadal blocks (10 per run); df = 87 for set 5 and 86 for set 10. In the table, "global" is the area-weighted global mean of the coefficient map. "Typical SE" is the area mean of the per-cell standard error. "p < 0.05" is the fraction of global area where the coefficient is significant (nominal). Regions are area-weighted boxes: subpolar North Atlantic = 45–65°N, 60–10°W; Sahel = 10–20°N, 20°W–40°E. Precipitation is in mm day⁻¹ throughout.
Set 5: predictand ~ Tglob + AMOC
| Predictand | Coefficient | Global | Typical SE | NH / SH | Regional | p < 0.05 | R² (area mean) |
|---|---|---|---|---|---|---|---|
tas |
Tglob (K K⁻¹) | 1.000 (exact) | 0.018 | 1.23 / 0.77 | subpolar N Atl. 1.36; 0–10°N 0.71 | 100 % | 0.97 |
tas |
AMOC (K Sv⁻¹) | 0 (exact) | 0.005 | +0.071 / −0.071 | subpolar N Atl. +0.36 | 97 % | |
prc |
Tglob (mm day⁻¹ K⁻¹) | +0.033 | 0.008 | +0.044 / +0.022 | Sahel +0.079; 0–10°N +0.076 | 92 % | 0.62 |
prc |
AMOC (mm day⁻¹ Sv⁻¹) | +0.002 | 0.002 | +0.011 / −0.008 | 0–10°S −0.017; Sahel +0.018 | 83 % | |
pr |
Tglob (mm day⁻¹ K⁻¹) | +0.040 | 0.011 | +0.053 / +0.028 | Sahel +0.091; 0–10°N +0.076 | 91 % | 0.58 |
pr |
AMOC (mm day⁻¹ Sv⁻¹) | +0.001 | 0.003 | +0.012 / −0.010 | 0–10°S −0.020; Sahel +0.027 | 77 % |
Set 10: predictand ~ Tglob + AMOC + Tglob·AMOC (centered)
The main effects hardly change from set 5 (for example, tas/AMOC in the
subpolar North Atlantic is +0.35 K Sv⁻¹), and area-mean R² rises only slightly
(tas 0.98, prc 0.64, pr 0.59). The interaction term:
| Predictand | Tglob·AMOC, global | Typical SE | Regional | p < 0.05 |
|---|---|---|---|---|
tas (K K⁻¹ Sv⁻¹) |
0 (exact) | 0.003 | subpolar N Atl. −0.022 | 48 % |
prc (mm day⁻¹ K⁻¹ Sv⁻¹) |
0.000 | 0.001 | subpolar N Atl. +0.001 | 32 % |
pr (mm day⁻¹ K⁻¹ Sv⁻¹) |
−0.000 | 0.002 | subpolar N Atl. −0.002 | 26 % |
Interpretation.
- Temperature. With global temperature held fixed, AMOC mostly moves heat around rather than changing the global mean. A 1 Sv stronger AMOC warms the subpolar North Atlantic box by about 0.36 K on average. The largest effect, 0.90 K, is at 65°N, 10°W near Iceland, and values above 0.6 K Sv⁻¹ extend from 52°N to 79°N, reaching northeast toward the Barents Sea. It warms the NH by 0.07 K on average and cools the SH by the same amount. Per kelvin of global warming, the NH warms 1.23 K and the SH 0.77 K. North of 70°N the coefficient averages 2.7 K K⁻¹, peaking at 3.6 (polar amplification).
- Interaction. The negative
tasinteraction in the subpolar North Atlantic means AMOC's local warming effect weakens as the climate warms, by about 6 % of its value per K of global warming (−0.022 / 0.354). - Precipitation. A stronger AMOC shifts tropical rain northward. Rainfall
increases north of the equator and over the Sahel (+0.027 mm day⁻¹ Sv⁻¹ for
pr) and decreases in the 0–10°S band (−0.020 mm day⁻¹ Sv⁻¹). This is the ITCZ moving toward the hemisphere that AMOC warms. Global-mean total precipitation increases by 0.040 mm day⁻¹ K⁻¹, about 1.4 % K⁻¹ of the 2.88 mm day⁻¹ control mean. The Tglob maps show the familiar tropical wet-get-wetter pattern. - Interaction significance. The Tglob·AMOC interaction is significant (nominal p < 0.05) over a quarter to half of the globe. Its magnitude is small, however, so the additive set 5 already captures most of the response.
Every regression analysis (gridded, EOF, ITCZ, scenarios) uses decadal means,
to characterize variability slower than interannual. The low-pass is
non-overlapping 10-year block means (regression.DECADAL_BLOCK,
data_loader.block_average_on_years). It is applied per run, per
contiguous segment, to both the predictors and the predictand inside
regression.build_pooled(block=regression.DECADAL_BLOCK), before pooling. The same filter therefore
acts on the dependent and independent variables, and every downstream step (the
orthogonalized and quadratic columns, the grid OLS, the EOFs) inherits it. Blocks
never span a run boundary or a gap within a run. Each block's timestamp is its
midpoint year. For CESM1.2, each run's AMOC years 2051–2150 form one contiguous
segment of exactly ten blocks.
Block averaging is a decimation, not a running mean. It collapses each decade to one roughly independent sample (pooled n = 90, 10 per run), so the nominal OLS degrees of freedom are far more honest than with annual data (successive decades of a run still drift together, so p-values remain somewhat optimistic). Quadratic and product terms are formed from the filtered bases (filter, then square), which gives the genuinely low-frequency response surface.
scripts/run_eof_regressions.py is an additive companion to the direct
per-grid-point maps (it does not replace run_regressions.py). It decomposes each
gridded field into empirical orthogonal functions (EOFs) and examines how the
leading principal-component (PC) time series — the EOF weightings over time —
behave and relate to the predictors. It works on decadal means only (10-year
block means per run, pooled n = 90), and like the other per-variable scripts takes
--variables.
Method (src/eof.py):
- Anomalies are taken about each cell's grand temporal mean over all pooled samples (not per-run means) — this retains the between-run forced variability the predictors are meant to explain.
- Area-weighted covariance EOF: anomalies are multiplied by √(zonal-band area weight) before an economy SVD and the patterns divided by it afterward. No per-cell standard-deviation normalization.
- Normalization: each EOF is a dimensionless pattern scaled to an area-weighted RMS of 1 over the grid, so its values (order 1) do not depend on model resolution. The PCs carry the field's units: |PCₖ(t)| is the area-weighted RMS anomaly (e.g. K) that mode k contributes at time t, and PCₖ × EOFₖ is that mode's anomaly field. The pattern maps of one field share a color scale; the PC-regression bars are standardized and so dimensionless.
- Truncation: two rules combined, the more restrictive winning — keep leading modes until cumulative variance reaches ≥ 95 %, but never keep a mode that individually explains < 1 % of variance (the per-mode floor drops the long low-variance noise tail). Counts are reported per field. The patterns page maps up to the leading 9 modes.
- PC regression: the retained PCs are regressed on the selected predictor sets
(5 & 10 by default; all ten with
--all-sets, including the orthogonalized, quadratic, and interaction columns) with an intercept; the PC-space coefficients (coef/SE/t/p) are saved.
Outputs are flat in data/output/eof/, two files per variable plus a shared
caveats README.txt:
<var>_pc.nc— OLS of the PCs on every predictor set fit, in raw PC units, one NetCDF group per set (set5,set10, …):coef,se,tstat,pvalueon(param, mode)andr2onmode.<var>_pc.pdf, in page order:- the leading EOF spatial patterns + a variance scree;
- the PC-on-scalar regression — the EOF analog of the 2D coefficient maps, with
the discrete EOF-mode index replacing the (lat, lon) grid. One page per
predictor set; each page has one panel per retained EOF mode, with a bar per
predictor showing the standardized coefficient β·σ(xⱼ)/σ(PCₘ) (z-scoring
predictors and the PC, so bars are comparable across modes — raw coefficients
scale with each PC's amplitude) and a ±SE whisker. Non-significant bars
(p > 0.05) are faded; the panel title reports R² and the mode's variance share.
t/p are scale-invariant and match
<var>_pc.nc; - one page per richer set among the 3-index set 6, the quadratic set 9, and
the interaction set 10 that was actually fit (only set 10 by default; all three
with
--all-sets): the fitted X·β overlaid on the actual PC over time, one panel per simulation — a direct view of how well the scalars predict each EOF weighting.
Results (CESM1.2, decadal means, n = 90).
| Field | Modes kept (cum. var.) | Leading modes (% var.) | Set 5 R² of PC1, PC2 |
|---|---|---|---|
tas |
2 (97.9 %) | 81.9, 15.9 | 1.00, 0.95 |
prc |
7 (87.0 %) | 47.4, 21.6, 9.5, 4.2, 1.8, 1.4 | 0.98, 0.84 |
pr |
7 (84.5 %) | 46.5, 19.3, 9.7, 4.5, 1.8, 1.5 | 0.98, 0.79 |
tasis almost exactly two-dimensional. The 95 % rule binds at two modes. PC1 is essentially Tglob (set 5 t = +197 for Tglob, +39 for AMOC). PC2 is the AMOC pattern (t = −38 for AMOC, +8.5 for Tglob).- Precipitation is not low-rank, so the 1 % floor binds at seven modes. As for
tas, PC1 is the warming mode (t_Tglob ≈ +60) and PC2 the AMOC mode (t_AMOC ≈ +18 to +21). Together they explain two thirds of the variance. The scalar predictors explain almost none of PCs 3–7 (R² ≤ 0.09), which are unforced decadal variability.
The spatial fingerprint maps (Σₖ βₖ·EOFₖ, the PC regression projected back to
the grid) are intentionally not generated — the EOFs and PC weightings are the
wanted deliverables. The capability remains in eof.reconstruct_fingerprint
(verified: with all modes retained it reproduces the direct field regression's
coefficient and p-value to Δcoef ~ 1e-10, Δp ~ 1e-8) should maps be wanted later.
scripts/predict_scenarios.py uses the decadal set 5 (Tglob + AMOC) and set 10
(Tglob + AMOC + Tglob·AMOC) coefficient maps to predict field changes for 3 K of
global warming relative to the 1×CO₂ control, with and without an AMOC decline from
20 to 6 Sv. It addresses: where does AMOC decline exacerbate the response to warming,
and where does it ameliorate it? Four states form a 2 × 2 factorial:
| state | Tglob (K) | AMOC (Sv) |
|---|---|---|
| reference | T0 = 286.91 (1×CO₂ control, mean over 2051–2150) | 20 |
| weak | T0 | 6 |
| warm | T0 + 3 | 20 |
| warm-weak | T0 + 3 | 6 |
Each output is one page with a 3 × 3 grid of maps. The corners are the four states' changes from the reference; each edge is the difference of its two neighbouring corners:
| 20 Sv | AMOC 20 → 6 Sv effect | 6 Sv | |
|---|---|---|---|
| +0 K | reference (≡ 0) | weak − reference | weak − reference |
| warming effect | warm − reference | interaction (set 10) | warm-weak − weak |
| +3 K | warm − reference | warm-weak − warm | warm-weak − reference |
The centre is the interaction: the AMOC effect at +3 K minus that at +0 K
(equivalently, the warming effect at 6 Sv minus that at 20 Sv). It is identically
zero for the additive set 5, so set 5 leaves it blank. For tas in set 10 it
weakens the AMOC-decline cooling of the subpolar North Atlantic by about 0.9 K
(−0.022 K K⁻¹ Sv⁻¹ × 3 K × −14 Sv) out of about 5 K.
Global-mean tas is fixed along each row, so the AMOC effects are pure spatial
redistributions of tas; the global means of prc and pr can still shift. All four
states lie inside the sampled predictor space (between the ~20 Sv runs and the
+0.3 Sv runs at ~5.5 Sv), so the predictions are interpolations. The predicted change
between two states is coef · (predictor(X) − predictor(R)); the intercept cancels,
and set 10 evaluates its centered columns and interaction with the fit's centering
means.
Outputs: data/output/scenarios/<predictand>_scenarios.pdf, one page per set
(5, then 10). All panels for a predictand, in both sets, share one symmetric color scale
(99th percentile of |change|), so set 5 and set 10 compare directly. Each panel title
gives its area-weighted global mean.
Results (set 5; set 10 differs by < 0.003 mm day⁻¹ in the global means). Regional means use the boxes of the regression results above:
| Region | tas: warming only (K) | tas: AMOC effect (K) | pr: warming only (mm day⁻¹) | pr: AMOC effect (mm day⁻¹) |
|---|---|---|---|---|
| global | +3.00 | 0.00 (exact) | +0.121 | −0.015 |
| subpolar N Atlantic | +4.09 | −5.09 | +0.032 | −0.250 |
| Sahel | +2.82 | +0.66 | +0.274 | −0.372 |
| 0–10°N | +2.14 | +0.65 | +0.227 | −0.085 |
| 0–10°S | +2.14 | +0.69 | +0.048 | +0.285 |
- Temperature. The AMOC decline more than cancels the local warming in the subpolar North Atlantic. The box mean goes from +4.1 K to −1.0 K, and the strongest cell cools by 12.6 K relative to the warming-only case. Because the global mean is held at +3 K, most of the rest of the world warms somewhat more, including the tropics (about +0.7 K).
- Precipitation. The decline shifts tropical rain south. It reverses the Sahel's warming-driven wetting (+0.27 → −0.10 mm day⁻¹), wets 0–10°S, and dries the subpolar North Atlantic. Globally it takes back 12 % of the warming-driven increase in total precipitation (−0.015 of +0.121 mm day⁻¹) and 23 % of the increase in convective precipitation (−0.022 of +0.098 mm day⁻¹).
scripts/run_itcz_regressions.py regresses the scalar ITCZ index — the
precipitation-mass centroid latitude, for two tropical bands
(precip_centroid_lat_20, precip_centroid_lat_30) — on the same scalar indices
(Tglob, dT_NS, AMOC), using the same predictor sets (5 & 10 by default, all ten with
--all-sets) and the same decadal-mean pooling as the gridded regressions. Because the response is a single series per simulation-year
(not a gridded field or PCs), it uses regression.build_pooled_scalar and
regression.fit_scalar_ols (a 1-D OLS with the same normal-equations math as the
gridded fit, validated against statsmodels, plus 95 % confidence intervals). The
pooled common sample is the same AMOC-complete 900 years (90 decadal blocks).
Outputs are flat in data/output/itcz/, one pair per band (band20, band30),
with a shared caveats README.txt:
<band>_coef_table.csv— coef, SE, t, p, 95 % CI per parameter, with R² and n, for every set (the scalar analog of the gridded coefficient maps).<band>_itcz.pdf(scripts/plot_itcz_regressions.py), in page order:- the centroid latitude per simulation, annual with the decadal means overlaid;
- ITCZ latitude vs each single predictor with the OLS line, 95 % CI band, and slope ± SE / R² / p annotated;
- predicted vs observed centroid latitude for the multi-predictor sets (5 & 10
by default; 5, 6, 10 with
--all-sets), with the 1:1 line and R² (shows how well the joint regression reproduces the ITCZ across runs); - partial-slope (coef ± SE) bar charts for the same sets, blue/red by sign and hatched where not significant.
Results (CESM1.2; total-pr centroid). R² by predictor set, band20 /
band30:
| Set | Predictors | Annual (n = 900) | Decadal (n = 90) |
|---|---|---|---|
| 1 | Tglob | 0.03 / 0.06 | 0.05 / 0.07 |
| 2 | dT_NS | 0.54 / 0.66 | 0.75 / 0.79 |
| 3 | AMOC | 0.55 / 0.65 | 0.78 / 0.80 |
| 5 | Tglob + AMOC | 0.58 / 0.70 | 0.83 / 0.86 |
| 6 | Tglob + dT_NS + AMOC | 0.67 / 0.78 | 0.92 / 0.94 |
| 9 | full quadratic | 0.70 / 0.81 | 0.96 / 0.97 |
| 10 | Tglob × AMOC | 0.58 / 0.70 | 0.83 / 0.86 |
- AMOC and the interhemispheric temperature difference are about equally strong single predictors of the ITCZ position (R² within 0.03 of each other). Global temperature alone explains almost nothing (R² ≤ 0.07).
- Set 5 slopes (decadal): AMOC +0.045 ± 0.002 ° Sv⁻¹ (band20) and +0.052 ± 0.002 ° Sv⁻¹ (band30); Tglob +0.042 ± 0.009 ° K⁻¹ and +0.057 ± 0.009 ° K⁻¹. A stronger AMOC moves the ITCZ north, toward the hemisphere it warms.
- Scenario scale: an AMOC decline from 20 to 6 Sv moves the centroid about 0.6° (band20) to 0.7° (band30) south. That is four to five times the northward shift from 3 K of warming (+0.13° / +0.17°).
- No interaction: the set-10 Tglob·AMOC term is not significant (p = 0.62 / 0.24), so R² is unchanged from set 5.
- Total vs convective precipitation. The centroid used to be computed from
convective
prc, a holdover from the CESM2 data, where totalprwas missing for some runs. Withprthe fits are slightly weaker (set 5 decadal R² 0.83 / 0.86, against 0.83 / 0.87 withprc), and the AMOC slope in the wider band is smaller (0.052, against 0.059 ° Sv⁻¹).
After placing the input files in data/input/ (see Data) and installing
dependencies, run, in order:
python scripts/make_scalar_timeseries.py # data/processed/scalars_annual_CESM1_*.nc
python scripts/run_regressions.py # data/output/regression/<var>_coef.{pdf,nc}
python scripts/plot_predictor_scatter.py # data/output/regression/predictor_scatter.pdf
python scripts/plot_scalar_timeseries.py # data/output/regression/predictor_timeseries.pdf
python scripts/plot_tglob_vs_amoc.py # data/output/regression/tglob_vs_amoc.pdf (AMOC vs Tglob, 9 cases)
python scripts/plot_case_grid_book.py # data/output/case_grid/<var>_2101-2150.pdf (3x3 case maps, one book per variable)
python scripts/run_eof_regressions.py # data/output/eof/<var>_pc.{pdf,nc}
python scripts/predict_scenarios.py # data/output/scenarios/<var>_scenarios.pdf
python scripts/run_itcz_regressions.py # data/output/itcz/<band>_coef_table.csv
python scripts/plot_itcz_regressions.py # data/output/itcz/<band>_itcz.pdfThe four set-fitting scripts (run_regressions.py, run_eof_regressions.py,
run_itcz_regressions.py, plot_itcz_regressions.py) default to only sets 5 & 10
on decadal means; add --all-sets to produce all ten sets. predict_scenarios.py
uses sets 5 & 10, so it needs only the default run.
Variable sets (--variables). Every script that makes per-variable output --
run_regressions.py, plot_case_grid_book.py, run_eof_regressions.py,
predict_scenarios.py -- takes --variables with one or
more named sets (data_loader.VARIABLE_SETS) and/or variable names, default all.
run_eof_regressions.py and predict_scenarios.py read the matching run_regressions.py output, so run that with the same set first.
The ITCZ scripts work on the scalar precipitation-centroid latitude, not per-variable
fields, so they have no --variables.
| Set | Variables | run_regressions.py / plot_case_grid_book.py time |
|---|---|---|
minimal |
tas, pr |
~20 s / ~10 s |
key |
13: tas, diurnal_temperature_range, pr, prc, pr_minus_evap, prsn, RHREFHT, TMQ, CLDTOT, cloud_radiative_effect, toa_net_down, sfc_net_energy_down, planetary_albedo |
~2 min / ~40 s |
all (default) |
all 46 | ~6 min / ~2 min |
python scripts/run_regressions.py --variables minimal
python scripts/plot_case_grid_book.py --variables key SHFLX # sets and names mixEvery output is per variable, so a subset run simply rewrites that subset's files and leaves the others alone. A full run of everything on all 46 variables takes roughly 20 minutes, mostly figure rendering (the fits take ~1 s per variable), with peak memory under 2 GB.
Each script is a thin wrapper over src/ and prints what it writes. All outputs
land under data/ (git-ignored) and are fully regenerable from the inputs.
Utility: python scripts/split_pdf_into_pages.py <file.pdf> splits a multi-page
figure PDF into one file per page (<file>_page01.pdf, _page02.pdf, … in the
same directory) for pasting individual panels into LaTeX. Add --png (optionally
--dpi N, default 300) to rasterize the pages to high-resolution PNGs instead —
much lighter for a LaTeX engine to load than the vector PDFs.
Preprocessing (scripts/make_scalar_timeseries.py), pooled per-grid-point
regression analysis (scripts/run_regressions.py, sets 1–10 for all 46 analysis
variables), predictor scatter
and time series (scripts/plot_predictor_scatter.py,
scripts/plot_scalar_timeseries.py), the additive EOF / principal-component path
(scripts/run_eof_regressions.py, built on src/eof.py), the decadal
scenario-prediction maps (scripts/predict_scenarios.py), and the ITCZ-centroid
scalar regressions (scripts/run_itcz_regressions.py,
scripts/plot_itcz_regressions.py), all built on src/data_loader.py,
src/regression.py, and src/output.py. The regression, EOF, and ITCZ analyses all
use decadal means (10-year block means), and only sets 5 & 10 are fit unless
--all-sets is given.
src/amoc_cesm/ and its scripts come from the former amoc-cesm repository
(CESM1 CO₂ × hosing factorial); the data they target is outmoded. See
src/amoc_cesm/README.md.