Lightweight climate model evaluation framework supporting multiple high-resolution model sets.
Feather compares high-resolution climate models against observations (ERA5, CERES, EN4, ESA-CCI, HadISST, OSI-SAF, PIOMAS/GIOMAS, MSWEP, Berkeley Earth) and CMIP6/HighResMIP multi-model means. It generates diagnostic figures, LLM-analyzed results, a LaTeX report, and a static web dashboard.
| Model set | Models | Resolution | Grid | Data backend |
|---|---|---|---|---|
| DestinE | IFS-FESOM, IFS-NEMO, ICON | ~5 km | HEALPix | intake catalogs |
| EERIE Ensemble | IFS-FESOM2-SR, IFS-NEMO-ER, ICON-ESM-ER, HadGEM3-GC5 | ~10-20 km atm, ~5-10 km ocean | 0.25° lat/lon | CMOR directory tree |
| EERIE multi-member | IFS-FESOM2-SR (r1–r3), IFS-NEMO-ER (r1–r3), ICON-ESM-ER, HadGEM3-GC5 | ~10-20 km atm, ~5-10 km ocean | 0.25° lat/lon | CMOR + kerchunk parquet |
| IFS-FESOM T319 | IFS-FESOM (T319) | ~60 km | HEALPix | per-year NetCDF |
| DestinE GRIB | IFS-FESOM TCO399/TCO319 | ~25-35 km | lat/lon | GRIB files |
| TerraDT | IFS-FESOM, IFS-NEMO, ICON | ~5 km | HEALPix | intake catalogs |
| Combined | TCO2999 + TCO399 + TCO319 | mixed | mixed | multi-source |
The framework is grid-agnostic: diagnostics automatically dispatch between HEALPix and regular lat/lon grids based on per-model configuration. The CompositeModelLoader enables mixing models from different data backends in the same evaluation run.
# Create the environment with all dependencies
conda env create -f environment.yml
# Or using mamba/micromamba (faster)
mamba env create -f environment.yml
# Activate and install feather in editable mode
conda activate feather
pip install -e .pip install -e .See environment.yml for the full list of dependencies. Key packages that are best installed via conda-forge: cartopy, healpy, netcdf4, eccodes, cfgrib.
nereus must be installed directly from GitHub (not available on PyPI/conda-forge):
pip install git+https://github.com/koldunovn/nereus.git@0.4.1After installation, the feather command is available.
Feather has four pipeline stages that can be run individually or together:
diagnostics → analyze → report → website
(compute+plot) (Gemini) (OpenAI) (HTML)
# DestinE (default config)
feather --config configs/default.yaml -v
# EERIE Ensemble (4 models)
feather --config configs/eerie.yaml -v
# EERIE — 3 IFS-NEMO-ER members + IFS-FESOM2-SR + ICON-ESM-ER + HadGEM3-GC5
feather --config configs/eerie_ifsnemo_members.yaml -v
# EERIE — all 8 members (3×IFS-FESOM2-SR + 3×IFS-NEMO-ER + ICON-ESM-ER + HadGEM3-GC5)
feather --config configs/eerie_all_members.yaml -v
# EERIE Tropical Nights climate change signal (SSP2-4.5)
feather --config configs/eerie_climchange_tn.yaml \
--steps diagnostics \
--diagnostics tropical_nights_change -v
# EERIE Heatwave indices climate change signal (SSP2-4.5)
feather --config configs/eerie_climchange_hw.yaml \
--steps diagnostics \
--diagnostics heatwave_change -v
# EERIE Heatwave hotspots — extreme-heat tail-widening vs ERA5 (PNAS Fig 2–4)
feather --config configs/eerie_all_members_extremes.yaml \
--steps diagnostics \
--diagnostics heatwave_hotspots --variables tasmax -v
# TerraDT baseline evaluation
feather --config configs/terradt.yaml -v
# DestinE Added Value 1990–2025 (hist + ssp3-7.0 stitched; timeseries to 2049/2044)
feather --config configs/destine_added_value.yaml -v
# Multi-resolution IFS-FESOM comparison (mixed data sources)
feather --config configs/ifs_fesom_combined.yaml -v# Generate diagnostic figures (requires compute node for real data)
feather --steps diagnostics -v
# LLM analysis of figures (Gemini)
feather --steps analyze --api-key $GEMINI_API_KEY -v
# Generate LaTeX report (OpenAI)
feather --steps report --openai-api-key $OPENAI_API_KEY -v
# Build static website
feather --steps website -v
# Combine steps
feather --steps analyze report website -v# Filter diagnostics by name
feather --diagnostics global_biases timeseries -v
# Filter by variable (CMOR canonical names)
feather --variables tas pr -v
# Include individual CMIP6 model lines/biases alongside MMM
feather --cmip6-individual -v
# Figures-only website (no LLM analysis)
feather --no-llm -v
# Custom output directory
feather --output ./my_output -v
# Re-run everything (ignore cached results)
feather --no-skip-existing -v
# Also write per-source diagnostic fields to NetCDF (obs, each model,
# CMIP6/HighResMIP MMM). Filenames include the analysis period and
# already-present files are skipped, so it is safe to re-run.
feather --save-netcdf --diagnostics global_biases temperature_berkeley -v
# Custom time period
feather --period 1990 2014 -v
# Compile LaTeX report to PDF
feather --steps report --compile-pdf -v
# Also works as a module
python -m feather --config configs/eerie.yaml -vWith --save-netcdf, diagnostics also write the per-source fields underlying
their figures — the observations, each evaluated model, and each benchmark MMM
(CMIP6, HighResMIP) — to NetCDF under {output_dir}/netcdf/{diagnostic}/. Each
filename carries the analysis period (e.g. tas_annual_1980-2014.nc), and files
already present are skipped, so the flag is incremental and safe to re-run.
Supported by global_biases, temperature_berkeley, precipitation_mswep,
global_trends, climate_variability, ocean_sst, sst_hadisst, ocean_en4,
radiation_budget, sea_ice, timeseries, seasonal_cycle, and
teleconnections. The obs-comparison and extremes/classification diagnostics
write NetCDF as part of their normal operation. A new diagnostic can opt in by
following the template in NEW_DIAGNOSTIC_SPEC.md.
Ocean Added Value chain. With --save-netcdf (and benchmarks configured),
the ocean diagnostics also write model/ensemble/benchmark bias NetCDFs in
the shared bias-map schema (ocean_sst→tos vs ESA-CCI, sst_hadisst→tos
vs HadISST, ocean_en4→thetao/so vs EN4, sea_ice→siconc vs OSI-SAF),
all regridded to a common grid (nereus.resolution). The added_value
diagnostic then reuses these to build the Ocean Added Value page — so run
the ocean diagnostics first, then added_value last:
CFG=configs/eerie_6_members_cmip6_highresmip.yaml
# 1) Ocean diagnostics write the benchmark-bias NetCDFs
feather --config $CFG --steps diagnostics --save-netcdf -v \
--diagnostics ocean_sst sst_hadisst ocean_en4 sea_ice
# 2) Added Value reuses them (also runs the atmospheric AV)
feather --config $CFG --steps diagnostics --save-netcdf -v --diagnostics added_valueBias-map diagnostics attach two area-weighted significance tests to each
evaluated source (every model, the CMIP6 MMM, and — with --cmip6-individual —
each individual CMIP6 model), comparing its spatial field against the
observations:
- Paired t-test — treats each grid point as a paired sample and tests
whether the mean bias (model − obs) differs significantly from zero. Cell
areas are used as weights so the test matches the area-weighted global-mean
bias and does not overrepresent polar cells; the weighted degrees of freedom
use Kish's effective sample size. Reported as
t_test_statistic/t_test_p_value. - Variance-ratio F-test — computes
F = Var(model) / Var(obs)across grid points (population variance,ddof=0, area-weighted) to test whether the model reproduces the observed spatial variability.F = 1means equal variability,F > 1the model is too noisy,F < 1too smooth; the p-value is two-sided. Reported asvariance_ratio/variance_ratio_p_value.
Both statistics are written into each figure's sidecar JSON metadata and
surfaced on the dashboard (the website's format_stat filter renders very small
p-values in scientific notation). The underlying helpers are spatial_ttest()
and spatial_variance_ratio() in feather/util/spatial.py. Currently emitted by
global_biases; other bias-map diagnostics can reuse the same helpers.
from feather import FeatherConfig, run_pipeline
# Load any config
cfg = FeatherConfig.from_yaml("configs/eerie.yaml")
# Full pipeline
result = run_pipeline(cfg, steps="all", api_key="...", openai_api_key="...")
# Individual steps
result = run_pipeline(cfg, steps=["diagnostics"], diagnostics=["global_biases"])
result = run_pipeline(cfg, steps=["report"], openai_api_key="...")
# Result dict
print(result)
# {"figures": 42, "analyses": 42, "syntheses": 12, "report": Path(...), "site_dir": Path(...)}Feather provides 26 registered diagnostics across atmosphere, ocean, cryosphere, extremes, cross-domain evaluation, and model intercomparison:
| Diagnostic | Class | Observation | What it produces |
|---|---|---|---|
global_biases |
GlobalBiases |
ERA5 | Multi-panel bias maps (obs + all model biases), annual/DJF/JJA |
timeseries |
TimeseriesDiag |
ERA5 | Global-mean time series, all models + obs overlaid |
seasonal_cycle |
SeasonalCycleDiag |
ERA5 | 12-month climatological cycle, all models + obs overlaid |
global_trends |
GlobalTrends |
ERA5 | Per-grid-point linear trends over the evaluation period |
climate_variability |
ClimateVariability |
ERA5 | STD maps of deseasonalised, detrended monthly fields |
radiation_budget |
RadiationBudget |
CERES EBAF | TOA/surface radiation budget bars, Gregory plot, imbalance time series, bias maps |
| Diagnostic | Class | Observation | What it produces |
|---|---|---|---|
ocean_sst |
OceanSST |
ESA-CCI | SST bias maps, time series, seasonal cycle, zonal mean |
sst_hadisst |
SSTHadISST |
HadISST | SST evaluation vs HadISST (1°, full 1980–2014 record vs ESA-CCI's 1990–2014): bias maps, time series, seasonal cycle, zonal mean, warming-trend maps (global + Arctic/Antarctic), and a Taylor diagram, with CMIP6/HighResMIP overlays |
ocean_en4 |
OceanEN4 |
EN4 v4.2.2 | 3D ocean T/S: surface bias maps, Hovmoller diagrams, depth-layer time series |
| Diagnostic | Class | Observation | What it produces |
|---|---|---|---|
sea_ice |
SeaIceDiag |
OSI-SAF, PIOMAS/GIOMAS | Sea ice area/extent/volume time series, seasonal cycles, trends, polar spatial maps |
| Diagnostic | Class | Observation | What it produces |
|---|---|---|---|
tropical_nights |
TropicalNightsDiag |
Berkeley Earth Land TMIN | Annual tropical nights count (TN > 20 °C) per grid point; land-only bias map vs Berkeley Earth |
tropical_nights_change |
TropicalNightsChangeDiag |
Berkeley Earth Land TMIN | Climate change signal in TN under SSP2-4.5: [Reference | Future | Change] maps per model, land-mean time series hist+SSP stitched, mean Tmin bias map |
heatwave |
HeatwaveDiag |
Berkeley Earth Land TMAX | Five TX90 heatwave indices (HWN, HWF, HWD, HWM, HWA): climatological maps + annual time series; TMAX bias map |
heatwave_change |
HeatwaveChangeDiag |
Berkeley Earth Land TMAX | Climate change signal in five TX90 heatwave indices under SSP2-4.5: [Reference | Future | Change] maps per model per index, land-mean time series hist+SSP stitched, mean Tmax bias map |
heatwave_hotspots |
HeatwaveHotspotsDiag |
ERA5 (derived daily tasmax) | Extreme-heat tail-widening (trend in yearly 99th − 87.5th percentile of daily tasmax, °C/decade): global trend maps per model with significance stippling and region boxes, regional box-and-whisker, PDF/CDF discrepancy panels (PNAS Fig 2–4) |
temp_extremes_change |
TempExtremesChangeDiag |
— (model change signal) | Climate change signal in mean daily tasmin and tasmax under SSP2-4.5, annual + four seasons (DJF/MAM/JJA/SON): [Reference | Future | Change] maps per model + stitched global-mean time series, per variable |
The Tropical Nights Index (TN20) counts nights per year where daily minimum temperature exceeds 20 °C. Requires daily tasmin (CMOR) or kerchunk-parquet mn2t24 store. Per-model NC checkpoints are written to {output_dir}/tropical_nights/.
The Tropical Nights Climate Change Signal (tropical_nights_change) compares mean annual TN between a historical reference period (default 1981–2000) and a future period (default 2031–2050) under SSP2-4.5. It is purpose-built for experiments that span multiple data backends (e.g. CMOR r1 + kerchunk-native r2/r3):
- Group A — Combined map: one row per model × three columns [Reference climatology | Future climatology | Change (Future − Reference)]. Sequential YlOrRd colormap for absolute counts; diverging RdYlBu_r for the change column. Models whose SSP run ends before the future period show a placeholder in columns 2–3.
- Group B — Stitched land-mean time series: historical segment (hist-1950) joined to SSP2-4.5 at 2015 with a dashed vertical line; reference and future windows highlighted; dashed black line shows Berkeley Earth approximate observed TN (months with mean TMIN > 20 °C weighted by days-in-month).
- Group C — Mean daily Tmin bias map vs Berkeley Earth Land TMIN over the reference period (skipped when BE data is unavailable).
Configuration lives under project.climate_change (see configs/eerie_climchange_tn.yaml). Each model declares its hist/future data source (cmor or kerchunk_native) and optionally a future_only_to year. Land masking is applied per-model using the Berkeley Earth land mask (NaN over ocean). Per-model NC checkpoints written to {output_dir}/tropical_nights_change/.
The Heatwave diagnostic computes five interconnected indices using the TX90 method (tasmax > DOY-specific 90th-percentile threshold, runs ≥ 3 consecutive days):
- HWN — heatwave number (events per year)
- HWF — heatwave frequency (heatwave days per year)
- HWD — heatwave duration (length of longest event, days)
- HWM — heatwave magnitude (mean tasmax on heatwave days, °C)
- HWA — heatwave amplitude (peak tasmax on heatwave days, °C)
Summer windows are hemisphere-aware (NH: May–Sep; SH: Nov–Mar). Requires daily tasmax (CMOR day/tasmax table or kerchunk-parquet mx2t24 store). Per-model NC checkpoints stored in {output_dir}/heatwave/.
The Heatwave Climate Change Signal (heatwave_change) compares all five TX90 indices between a historical reference period (default 1981–2000) and a future period (default 2031–2050) under SSP2-4.5. The T90 threshold is computed once from the reference period and applied to both periods. One figure set is produced per index:
- Group A — Combined change map: one row per model × three columns [Reference climatology | Future climatology | Change (Future − Reference)]. Sequential colormap for absolute values; diverging RdYlBu_r for the change column. Models whose SSP run ends before the future period show a placeholder in columns 2–3.
- Group B — Stitched land-mean time series: historical segment joined to SSP2-4.5 at 2015 with a dashed vertical line; reference and future windows highlighted.
- Group C — Mean daily Tmax bias map vs Berkeley Earth Land TMAX over the reference period (skipped when BE data is unavailable).
Configuration lives under project.climate_change (see configs/eerie_climchange_hw.yaml). Each model declares its hist/future data source (cmor or kerchunk_native). Land masking is applied per-model using the Berkeley Earth land mask. Per-model NC checkpoints written to {output_dir}/heatwave_change/.
The Daily Tmin/Tmax Climate Change Signal (temp_extremes_change) compares the mean of daily minimum (tasmin) and maximum (tasmax) temperature between a historical reference period (default project.period) and a future period (default 2031–2050) under SSP2-4.5. Unlike the index-count *_change diagnostics it maps the raw Tmin/Tmax fields themselves, globally (land + ocean), for the annual mean and each of the four seasons. Per variable it produces six figures:
- Change map panels (
{var}_{season}_change_panels, ×5: annual + DJF/MAM/JJA/SON) — one row per model × three columns [Reference | Future | Change (Future − Reference)].cmocean.thermalfor the absolute °C climatology columns; divergingRdBu_rfor the change column (ΔK ≡ Δ°C). Models with no SSP data in the future window show a placeholder. - Global-mean time series (
{var}_change_timeseries) — historical global-mean annual series joined to SSP2-4.5 at 2015, with reference and future windows shaded and the 2015 boundary marked.
It reuses the multi-backend loader factory of the other *_change diagnostics, so a model can mix CMOR and kerchunk-native segments. To avoid re-reading the daily archive, each model persists its annual- and seasonal-mean series (year, lat, lon) as NetCDF checkpoints {output_dir}/temp_extremes_change/{model}_{var}_{hist,ssp}_{start}_{end}.nc. Configuration lives under project.climate_change (see configs/eerie_all_members_indices.yaml).
The Heatwave Hotspots diagnostic (heatwave_hotspots) reproduces Figures 2–4 of Sambartusek, Kornhuber et al. (2024, PNAS, doi:10.1073/pnas.2411258121) — "A global emergence of regional heatwave hotspots". The core metric is tail-widening: at each land grid point and each year it computes the 99th and 87.5th percentiles of daily tasmax, forms the yearly tail width D = P99 − P87.5, and takes the linear trend of D over the analysis period (°C/decade). A positive trend means the hot tail is widening faster than the bulk of the warm-season distribution — the heatwave-hotspot signature. All fields are regridded to a common 0.25° grid (per-grid interpolator cache), land-masked (>25 %), and masked grey where the P87.5 trend is negative. Five figures are produced:
- Trend map (
heatwave_hotspots_trend_map) — globalD-trend map per model on Robinson projection, with parametric OLS slope t-test stippling for significance and the ten study-region boxes overlaid. - Region trend maps (
heatwave_hotspots_regions) — zoomed PlateCarree panels for the ten heatwave-hotspot regions (Europe, China, Argentina, Oman, Australia, Japan, Arctic, NW Canada, N Africa, Siberia). - Box-and-whisker (
heatwave_hotspots_boxwhisker) — regionalD-trend distributions per region with bootstrap (10 000-sample) confidence intervals on the regional area-weighted mean. - Discrepancy (
heatwave_hotspots_discrepancy) — model-minus-reference cumulative-difference panels highlighting where each model over/under-represents the tail-widening signal. - PDF (
heatwave_hotspots_pdf) — area-weighted probability density of theD-trend across land, model vs ERA5.
The reference is ERA5 (the derived daily tasmax CMOR tree); the model ensemble is the EERIE high-resolution members. CMIP6 is not included in this first pass — daily tasmax is only on the DKRZ pool DRS (/pool/data/CMIP6/.../day/tasmax/), not in the zarr cache — but the config carries a stub for wiring in a yearly-percentile cache later. Per-model yearly-percentile fields are checkpointed to NetCDF in {output_dir}/heatwave_hotspots/ so re-runs skip the heavy daily-percentile pass. Configuration lives in configs/eerie_all_members_extremes.yaml.
| Diagnostic | Class | Observation | What it produces |
|---|---|---|---|
climate_classification |
KTClimateClassification |
ERA5, Berkeley Earth HR + MSWEP | Köppen–Trewartha (KT14) land climate type per grid cell; discrete classification maps, % land-area bar chart, and summary table per source |
The Köppen–Trewartha Climate Classification (climate_classification) classifies the monthly climatology of tas and pr into the 14 KT types (Ar, Aw, As, BW, BS, Cs, Cw, Cf, Do, Dc, Eo, Ec, Ft, Fi) using the Trewartha dryness threshold R = 2.3·Tann − 0.64·Pw + 41 (cm) and the F→E→B→D→C→A priority order. The classification is restricted to land via the Berkeley Earth land mask and computed on a common 0.25° grid so per-type area fractions are directly comparable. Each model is classified from its own tas+pr; observations come from ERA5 (t2m+tp) and a Berkeley Earth HR + MSWEP combination; the CMIP6 multi-model mean is included when enabled. Four figures are produced:
- Classification maps — one discrete KT panel per source, ordered ERA5, Berkeley Earth HR + MSWEP, the EERIE models, then the CMIP6 multi-model mean.
- Ensemble comparison — the observational datasets alongside the mean and median of each model family. Families are defined by a per-model
ensemblelabel in the config (e.g.EERIE, orCORDEX/CMIP5/CMIP6for the Africa config), and each ensemble is classified from the per-cell, per-month mean/median of its members'tasandprclimatologies (not by averaging KT codes). - Bar chart and summary table — % land area per KT type per source, including every family's ensemble mean/median and the CMIP6 MMM (each source sums to 100 %).
Regional mode (Africa). Setting project.region (e.g. africa) restricts the classification to a regional bounding box, builds the common grid on that box (−180..180 longitude convention so western Africa is not dropped), handles rotated-pole (2-D lat/lon) sources, and draws zoomed PlateCarree maps. configs/africa_climate.yaml classifies, on a common 0.25° Africa grid for the 1981–2010 reference: ERA5/obs, CORDEX-CORE AFR-22 (10 GCM-driven members + 4 ERAINT evaluation runs; CORDEXLoader stitches historical 1981–2005 + rcp85 2006–2010 and falls back from mon to day-resampled-to-monthly), CMIP5 (5 driving GCMs, 1st member, historical+rcp85 stitch via CMIP5Loader), and CMIP6 (5 driving-GCM successors, historical, via the NetCDF-tree CMIP6NCLoader). Coarse GCMs use a 250 km regrid influence-radius floor.
Climate shifts at a warming level (climate_shifts, Phase 2). Compares the reference classification with the classification at a target global warming level (default +2 °C above the 1850–1900 pre-industrial baseline, configured under project.gwl). The 30-year window is found per GCM from its own global-mean temperature (feather/util/gwl.py; CMIP5→rcp85, CMIP6→ssp585), and CORDEX regional models inherit the window of their driving GCM (driving_gcm in the config). Per model family it produces: shift maps [reference | future | change], a reference-vs-future area bar, a net gain/loss summary (Δ % land area per type), and a transition matrix (reference type → warming-level type, area-weighted). Reference and future KT-code NetCDFs (per source and per family ensemble mean) and an area-shift CSV are written to {output_dir}/climate_shifts/. Run with --diagnostics climate_shifts on the same africa_climate.yaml.
Outputs written to {output_dir}/climate_classification/ for later regional analysis: a {source}_clim_{period}.nc per source with the monthly tas (°C) and pr (mm/month) climatology — both a full-global field and a land-only version, on the common grid — for ERA5, Berkeley Earth HR, MSWEP, every EERIE model, every CMIP6 model, the EERIE ensemble mean/median, and the CMIP6 MMM; a {source}_kt_{period}.nc with the integer kt_code field per classified source; and a kt_area_percent CSV. See feather/util/koeppen_trewartha.py for the pure, vectorised classifier.
| Diagnostic | Class | Observation | What it produces |
|---|---|---|---|
precipitation_mswep |
PrecipitationMSWEP |
MSWEP v2.8 | Precipitation bias maps (absolute + relative), time series, seasonal cycle, zonal mean, intensity PDF |
temperature_berkeley |
TemperatureBerkeley |
Berkeley Earth | T2m bias maps, warming trend maps (global + polar), Taylor diagram |
teleconnections |
TeleconnectionDiag |
ERA5 | Climate variability modes (ENSO, NAO, SAM, AO, IOD, PDO, QBO): index time series, spatial patterns, power spectra, seasonal variance |
obs_comparison |
ObsComparisonDiag |
ERA5 + Berkeley Earth | ERA5 vs Berkeley Earth T2m trend and bias comparison across two periods (1980–2014, 1980–2024) |
precip_obs_comparison |
PrecipObsComparisonDiag |
ERA5 + MSWEP v2.8 | ERA5 vs MSWEP precipitation trend and bias comparison across two periods (1980–2014, 1980–2023) |
| Diagnostic | Class | Observation | What it produces |
|---|---|---|---|
added_value |
AddedValueDiag |
ERA5 / Berkeley Earth / MSWEP (atmos); ESA-CCI + HadISST / EN4 / OSI-SAF (ocean) | Dosio et al. (2015) Added Value: EERIE ensemble vs CMIP6/HighResMIP MMM — ensemble summary maps + per-model panels. Includes a dedicated Ocean Added Value page (tos vs ESA-CCI and HadISST, thetao/so vs EN4, siconc vs OSI-SAF) that reuses the ocean diagnostics' benchmark-bias NetCDFs |
Added Value (AV) quantifies where the EERIE ensemble outperforms the CMIP6 multi-model mean relative to observations. AV ∈ [-1, 1]: AV > 0 means EERIE reduces squared error vs CMIP6 MMM at that grid point. Two figures per period (annual, DJF, JJA): ensemble mean/median summary and one panel per individual EERIE and CMIP6 model.
All diagnostics support:
variables=["tas", ...]— filter which variables to evaluate (CMOR canonical names)experiment="hist-1950"— experiment identifier (from config by default)period=("1980", "2014")— time period for climatologies (from config by default)cmip6_individual=True— individual CMIP6 model lines/biases alongside MMM
Feather uses YAML configuration files. Fourteen configs are provided:
| Config | Model set | Data source | Comparison type |
|---|---|---|---|
configs/default.yaml |
DestinE (3 models) | intake catalogs | multi_model |
configs/eerie.yaml |
EERIE Ensemble (4 models) | CMOR directory tree | multi_model |
configs/eerie_ifsnemo_members.yaml |
EERIE — 3 IFS-NEMO-ER + 3 other models | CMOR (hist-1950 + hist-1975) | multi_model |
configs/eerie_all_members.yaml |
EERIE — 8 models (3×FESOM2 + 3×NEMO + 2) | CMOR + kerchunk parquet | multi_model |
configs/eerie_climchange_tn.yaml |
EERIE — IFS-FESOM2-SR (r1–r3) + ICON-ESM-ER, SSP2-4.5 TN signal | CMOR + kerchunk native | multi_model |
configs/eerie_climchange_hw.yaml |
EERIE — IFS-FESOM2-SR (r1–r3) + ICON-ESM-ER, SSP2-4.5 heatwave signal | CMOR + kerchunk native | multi_model |
configs/eerie_all_members_extremes.yaml |
EERIE — ERA5 + 7 members (3×FESOM2 + 3×NEMO + ICON; HadGEM3 has no daily tasmax), heatwave-hotspot tail-widening (PNAS Fig 2–4) | CMOR + kerchunk parquet (ERA5 derived daily + EERIE day table) | multi_model |
configs/himansu_319.yaml |
IFS-FESOM T319 | per-year NetCDF | single_model |
configs/tco_grib.yaml |
IFS-FESOM TCO399/TCO319 | GRIB files | resolution_sensitivity |
configs/destine_ifs_fesom.yaml |
IFS-FESOM only | intake catalogs | single_model |
configs/destine_ifs_nemo.yaml |
IFS-NEMO only | intake catalogs | single_model |
configs/destine_icon.yaml |
ICON only | intake catalogs | single_model |
configs/terradt.yaml |
TerraDT (3 models) | intake catalogs | baseline_evaluation |
configs/destine_added_value.yaml |
DestinE (3 models), 1990–2025 | intake catalogs (hist + ssp3-7.0 stitched) | multi_model |
configs/ifs_fesom_combined.yaml |
IFS-FESOM multi-res | mixed (catalog + GRIB) | resolution_sensitivity |
The models key determines the config format:
- List (
models: [ifs-fesom, ifs-nemo, icon]) — legacy DestinE format, auto-populates HEALPix grid defaults - Dict (
models: {ModelA: {institution: ..., grids: ...}}) — structured format with per-model configuration
data_source.type selects the loading backend: "cmor", "destine_catalog", "netcdf_healpix", "grib", or "kerchunk_parquet". Individual models can override the global backend with a per-model data_source_type key (used by eerie_all_members.yaml to mix CMOR and kerchunk parquet in the same run).
project:
name: "EERIE"
description: "EERIE Ensemble evaluation"
experiment: "hist-1950"
period: ["1980", "2014"]
resolution: "high-resolution (~10-20 km atm, ~5-10 km ocean)"
comparison_type: "multi_model" # multi_model | resolution_sensitivity | single_model | baseline_evaluation
comparison_description: "" # optional free-text for LLM prompt framing
data_source:
type: "cmor" # cmor | destine_catalog | netcdf_healpix | grib | kerchunk_parquet
root: "/path/to/CMOR/tree"
models:
IFS-FESOM2-SR:
institution: AWI
experiment: hist-1950
variant: r1i1p1f1
grids:
sfc: latlon # "healpix" or "latlon" per domain
o2d: latlon
o3d: latlon
color: "#1f77b4"For evaluations mixing data backends, set per-model data_source_type:
data_source:
type: "destine_catalog" # default backend
models:
TCO2999:
data_source_type: "destine_catalog" # per-model override (optional, uses default if omitted)
catalog_key: "ifs-fesom" # catalog entry name (if different from model name)
member: 1 # ensemble member index
grids: {sfc: healpix, o2d: healpix, o3d: healpix}
TCO399:
data_source_type: "grib" # different backend
grids: {sfc: latlon, o2d: latlon, o3d: latlon}The same pattern is used in eerie_all_members.yaml to mix CMOR (r1 members) with kerchunk parquet (r2/r3 members):
data_source:
type: "cmor"
root: "/path/to/CMOR/EERIE/HighResMIP"
models:
IFS-FESOM2-SR: # r1: standard CMOR path
institution: AWI
variant: r1i1p1f1
...
IFS-FESOM2-SR-r2: # r2: kerchunk parquet reference store
institution: AWI
variant: r2i1p1f1
data_source_type: "kerchunk_parquet"
data_root: "/path/to/kerchunks/IFS-FESOM2/hist-1950"
...ModelConfig supports:
data_root— custom data path (e.g., HadGEM3 outside shared CMOR root)grid_label— CMOR grid label override (default"gr")variable_aliases— CMOR var → on-disk name mapping (e.g.,thetao: thetao-con)scale_factors— per-variable multiplier (e.g.,clt: 100for fraction → %)absolute_salinity: true— enable SA→SP salinity conversion (NEMO-based models)
Some evaluation windows are not covered by a single experiment. For DestinE, the historical baseline_hist run ends in 2014 while the projections_ssp3-7.0 run continues to 2049 (IFS-FESOM / IFS-NEMO) or 2044 (ICON). To analyse e.g. 1990–2025, list the experiments to concatenate along the time axis:
project:
period: ["1990", "2025"]
# Stitched along time: historical baseline + ssp3-7.0 projection.
experiments: ["baseline_hist", "projections_ssp3-7.0"]
# Optional: let the timeseries diagnostic extend to each model's native end
# (IFS-FESOM/IFS-NEMO → 2049, ICON → 2044) while every other diagnostic
# uses `period` above. Defaults to `period` when omitted.
timeseries_period: ["1990", "2050"]
cmip6:
# Stitch CMIP6 symmetrically so added-value / bias diagnostics cover the
# same window. Models lacking ssp370 for their variant fall back to
# historical-only (end at 2014).
experiments: ["historical", "ssp370"]Segments are concatenated, sorted, and de-duplicated on overlapping months (the first experiment in the list wins). Models that did not run a listed experiment are skipped gracefully. Period slicing happens after concatenation, so a single stitched series serves both the analysis window and the extended timeseries. Omitting experiments preserves the legacy single-experiment behaviour, so existing configs are unaffected. See configs/destine_added_value.yaml for a complete example.
Note: an ssp370 variant label may differ from its historical counterpart (e.g. CanESM5 ssp370 is
r1i1p2f1, notr1i1p1f1); the configured variant must exist for both experiments, or the model reverts to historical-only.
The comparison_type field controls how LLM prompts frame the analysis:
| Type | Focus |
|---|---|
multi_model |
Inter-model differences and common biases across a multi-model ensemble |
resolution_sensitivity |
How model skill scales with resolution (same model at different grid spacings) |
single_model |
Detailed evaluation of one model against observations and CMIP6 |
baseline_evaluation |
Whether a model is adequate as a starting point for a downstream project |
All configs share:
- Observations: ERA5, CERES, EN4, ESA-CCI, OSI-SAF, MSWEP, Berkeley Earth
- CMIP6: 12 models from zarr archives (configurable ensemble mode)
- LLM analysis: Gemini (
GEMINI_API_KEY/VERTEX_API_KEY) - Report generation: OpenAI (
OPENAI_API_KEY) - nereus: Interpolation settings (
method,resolution,influence_radius)
Feather can overlay two reference benchmark ensembles as multi-model means
(MMM): CMIP6 historical and HighResMIP hist-1950. Each is built once from
the DKRZ pool into a per-variable zarr cache (scripts/convert_pool_cmip6.py, SLURM
wrapper scripts/convert_pool_cmip6.sh) and configured with models: auto, so
ensemble membership is discovered at runtime from the cache — see
configs/eerie_all_members_cmip6_highresmip.yaml.
Daily cache (day table). The monthly converter deliberately skips the huge
day table. A companion pair — scripts/convert_pool_cmip6_daily.py (SLURM wrapper
scripts/convert_pool_cmip6_daily.sh) — builds a daily zarr cache of tas,
tasmax, tasmin and pr (+ areacella) for the daily extremes / Tmin/Tmax-change
diagnostics. It reuses the monthly converter's I/O helpers (atomic, resumable,
calendar-aware) and writes stores named {model}_{experiment}_{member}_day_{var}.zarr
into a dedicated cache dir (default /work/bk1580/cmip6_zarr); the experiment is
encoded in the filename so historical (reference) and ssp245 (SSP2-4.5 future)
coexist. Run sbatch scripts/convert_pool_cmip6_daily.sh for both experiments (or
… historical / … ssp245), and preview with bash scripts/convert_pool_cmip6_daily.sh both --dry-run.
Selection rule. A model contributes to a benchmark MMM for a variable only when its converted store:
- is non-empty (the conversion actually wrote the field);
- is on a regriddable grid — rectilinear, curvilinear (2-D lat/lon), or unstructured with 1-D lat/lon cell-centre coords (e.g. ICON's triangular grid, regridded as scattered points like HEALPix); and
- covers the full analysis period 1980–2014 (
require_full_coverage, default on; opt out withrequire_full_coverage: falsein the benchmark config block).
Membership grows automatically as more stores are converted. The lists below reflect
the current cache for atmospheric tas; per-variable membership varies with which
stores converted.
CMIP6 historical — 55 of 63 candidate models included:
ACCESS-CM2, ACCESS-ESM1-5, AWI-CM-1-1-MR, AWI-ESM-1-1-LR, BCC-CSM2-MR, BCC-ESM1, CAMS-CSM1-0, CESM2, CESM2-FV2, CESM2-WACCM, CESM2-WACCM-FV2, CIESM, CMCC-CM2-HR4, CMCC-CM2-SR5, CMCC-ESM2, CNRM-CM6-1, CNRM-ESM2-1, CanESM5, CanESM5-CanOE, E3SM-1-0, E3SM-1-1, E3SM-1-1-ECA, EC-Earth3, EC-Earth3-AerChem, EC-Earth3-CC, EC-Earth3-Veg, EC-Earth3-Veg-LR, FGOALS-f3-L, FGOALS-g3, FIO-ESM-2-0, GFDL-CM4, GFDL-ESM4, GISS-E2-1-G, GISS-E2-1-G-CC, GISS-E2-1-H, HadGEM3-GC31-LL, ICON-ESM-LR, IITM-ESM, INM-CM4-8, INM-CM5-0, IPSL-CM5A2-INCA, KACE-1-0-G, KIOST-ESM, MCM-UA-1-0, MIROC-ES2L, MIROC6, MPI-ESM1-2-HR, MRI-ESM2-0, NESM3, NorCPM1, NorESM2-LM, NorESM2-MM, SAM0-UNICON, TaiESM1, UKESM1-0-LL
Excluded (8) — pending (re)conversion: AWI-ESM-1-REcoM, CAS-ESM2-0, CNRM-CM6-1-HR,
HadGEM3-GC31-MM, IPSL-CM6A-LR, IPSL-CM6A-LR-INCA, MPI-ESM-1-2-HAM, MPI-ESM1-2-LR —
currently missing a usable tas store (empty or not yet converted). They rejoin the
ensemble automatically once their stores are rebuilt.
HighResMIP hist-1950 — 17 of 22 candidate models included:
BCC-CSM2-HR, CESM1-CAM5-SE-LR, CMCC-CM2-HR4, CMCC-CM2-VHR4, CNRM-CM6-1, CNRM-CM6-1-HR, EC-Earth3P, EC-Earth3P-HR, ECMWF-IFS-HR, ECMWF-IFS-LR, ECMWF-IFS-MR, HadGEM3-GC31-HH, HadGEM3-GC31-HM, HadGEM3-GC31-LL, HadGEM3-GC31-MM, MPI-ESM1-2-HR, MPI-ESM1-2-XR
Excluded (5):
- AWI-CM-1-1-HR, AWI-CM-1-1-LR, CESM1-CAM5-SE-HR, GFDL-CM4C192,
INM-CM5-H — no usable
tasstore yet (empty / not yet converted); rejoin once rebuilt.
Re-converting only the missing stores: the converter's
skip_existingtreats an empty store as "done", so delete the empties first — the verification sweep writes their paths to…/cmip6_empty_stores.txtand…/highresmip_empty_stores.txt— or pass--no-skip.
Feather supports six data loading backends:
| Backend | Class | Config type | Grid |
|---|---|---|---|
| DestinE intake catalogs | MultiCatalogLoader |
destine_catalog |
HEALPix (zarr) |
| CMOR directory tree | CMORLoader |
cmor |
regular lat/lon (NetCDF) |
| Per-year NetCDF | NetCDFLoader |
netcdf_healpix |
HEALPix (NetCDF) |
| GRIB files | GRIBLoader |
grib |
regular lat/lon (GRIB) |
| Kerchunk parquet reference stores | KerchunkParquetLoader |
kerchunk_parquet |
regular lat/lon (via fsspec) |
| Multi-source | CompositeModelLoader |
mixed | per-model |
KerchunkParquetLoader reads IFS-FESOM2 ensemble members stored as kerchunk parquet reference files pointing to raw GRIB/FESOM output. Two store layouts are supported:
- Regridded 0.25° (
gr025):{root}/{variant}/atmos/gr025/*.parq— atmosphere 2D/3D monthly;ocean/gr025/2D_daily_avg_*.parqocean 2D daily. Used byeerie_all_members.yaml. - Native grid (
native):{root}/{variant}/atmos/native/2D_daily_native_atmos_min.parq/…_max.parq— daily min/max on the native HEALPix grid. Selected viadata_source_type: kerchunk_nativein the model config; used byeerie_climchange_tn.yamlfor thetropical_nights_changediagnostic.
Requires kerchunk, fastparquet, and fsspec in the Python environment.
The CompositeModelLoader automatically routes load_var() calls to the correct backend per model based on data_source_type in the model config.
- Create a config YAML using the structured format (see
configs/eerie.yamlas template) - Set
data_source.typeto one of:"cmor","destine_catalog","netcdf_healpix","grib" - Define
modelsas a dict with per-modelinstitution,experiment,variant,grids,color - Set
project.name,project.period,project.experiment,project.resolution - Optionally set
project.comparison_typefor LLM prompt framing - For mixed data sources, set per-model
data_source_typeand useCompositeModelLoader - Run:
feather --config configs/my_project.yaml -v
If your data format is not supported, implement a new loader class (see CMORLoader or GRIBLoader as templates) and add a dispatch case in feather/run.py:_create_model_loader().
output/
figures/ # Diagnostic figures + JSON metadata
global_biases/
tas_annual_bias_combined.png
tas_annual_bias_combined.json
...
timeseries/
seasonal_cycle/
radiation_budget/
sea_ice/
ocean_sst/
ocean_en4/
global_trends/
climate_variability/
precipitation_mswep/
temperature_berkeley/
teleconnections/
obs_comparison/
precip_obs_comparison/
added_value/
tropical_nights/
tropical_nights_change/
heatwave/
heatwave_change/
heatwave_hotspots/
temp_extremes_change/
tropical_nights/ # NC checkpoints (outside figures tree)
{model}_tropical_nights_tn20_{start}_{end}.nc
tropical_nights_change/ # NC checkpoints (outside figures tree)
{model}_tn_hist_{start}_{end}.nc
{model}_tn_ssp_{start}_{end}.nc
heatwave/ # NC checkpoints (outside figures tree)
{model}_heatwave_tx90_{start}_{end}.nc
heatwave_change/ # NC checkpoints (outside figures tree)
{model}_hw_hist_{start}_{end}.nc
{model}_hw_ssp_{start}_{end}.nc
heatwave_hotspots/ # NC checkpoints (outside figures tree)
{model}_percs_{start}_{end}.nc
temp_extremes_change/ # NC checkpoints (outside figures tree)
{model}_tasmin_hist_{start}_{end}.nc
{model}_tasmin_ssp_{start}_{end}.nc
{model}_tasmax_hist_{start}_{end}.nc
{model}_tasmax_ssp_{start}_{end}.nc
analysis/ # LLM analysis (Gemini)
global_biases/
tas_annual_bias_combined_analysis.json
synthesis.json
...
publication/ # LaTeX report (OpenAI)
structure.json # Stage 1: editorial curation
sections/ # Stage 2: per-section prose
figures/ # Copied figures for LaTeX
report.tex # Final LaTeX document
report.pdf # (if --compile-pdf)
site/ # Static HTML dashboard
index.html
global_biases.html
...
feather/
cli.py # CLI entry point (feather command)
config.py # FeatherConfig + ModelConfig dataclasses
run.py # Pipeline orchestration (run_pipeline)
__main__.py # python -m feather support
data/
loader.py # MultiCatalogLoader (DestinE intake catalogs)
cmor_loader.py # CMORLoader (CMOR directory tree, e.g. EERIE)
netcdf_loader.py # NetCDFLoader (per-year NetCDF on HEALPix grid)
grib_loader.py # GRIBLoader (GRIB files on regular lat/lon grid)
kerchunk_loader.py # KerchunkParquetLoader (kerchunk parquet reference stores)
composite_loader.py # CompositeModelLoader (multi-source routing)
obs.py # ObsLoader (observations from config)
cmip6.py # CMIP6Loader (multi-model mean from zarr)
variables.py # VARIABLE_REGISTRY (36 vars, CMOR canonical names)
util/
spatial.py # Zonal/global means, regridding, latlon areas
temporal.py # Climatology, anomaly, deseason, detrend
eof.py # EOF computation via SVD (teleconnections)
spectrum.py # Power spectrum via Welch (teleconnections)
units.py # Unit conversions
plot/
maps.py # Bias maps (nereus), combined multi-panel maps
lines.py # Time series, seasonal cycle, zonal profiles, Taylor diagram
styles.py # Model colors, plot defaults
diag/
base.py # DiagnosticBase ABC + grid-agnostic helpers
registry.py # @register decorator, diagnostic discovery
figure_meta.py # Figure metadata sidecar system
global_biases.py # Climatology bias maps (18 variables)
timeseries.py # Global-mean time series
seasonal_cycle.py # Monthly climatological cycle
radiation_budget.py # Radiation budget analysis (CERES EBAF)
sea_ice.py # Sea ice evaluation (OSI-SAF, PIOMAS/GIOMAS)
ocean_sst.py # SST evaluation (ESA-CCI)
ocean_en4.py # 3D ocean T/S (EN4 v4.2.2)
global_trends.py # Per-grid-point linear trends
climate_variability.py # STD of deseasonalised, detrended fields
precipitation_mswep.py # Precipitation evaluation (MSWEP v2.8)
temperature_berkeley.py # T2m evaluation (Berkeley Earth)
teleconnections.py # Variability modes (ENSO, NAO, SAM, AO, IOD, PDO, QBO)
obs_comparison.py # ERA5 vs Berkeley Earth T2m trends and biases
precip_obs_comparison.py # ERA5 vs MSWEP precipitation trends and biases
added_value.py # Added Value: EERIE ensemble vs CMIP6 MMM (Dosio et al. 2015)
tropical_nights.py # Tropical Nights Index (TN > 20 °C, daily tasmin)
tropical_nights_change.py # Tropical Nights climate change signal (SSP2-4.5)
heatwave.py # Heatwave Indices (TX90: HWN, HWF, HWD, HWM, HWA)
heatwave_change.py # Heatwave climate change signal (SSP2-4.5)
heatwave_hotspots.py # Heatwave hotspots — extreme-heat tail-widening (PNAS Fig 2–4)
temp_extremes_change.py # Daily Tmin/Tmax climate change signal (SSP2-4.5, annual + seasons)
llm/
analyzer.py # FigureAnalyzer (Gemini, comparison-type aware)
schemas.py # FigureAnalysis, DiagnosticSynthesis (Pydantic)
prompts.py # Gemini prompts (composable, comparison-type aware)
export/
report.py # ReportGenerator (OpenAI, 3-stage)
schemas.py # ReportStructure, WrittenSection
prompts.py # OpenAI prompts (composable, comparison-type aware)
openai_client.py # Thin OpenAI wrapper with retry
latex_builder.py # LaTeX escaping, template rendering, PDF
templates/ # Jinja2 LaTeX template
website/
generator.py # SiteGenerator (static HTML dashboard)
templates/ # Jinja2 HTML templates
static/ # Dark-theme CSS
Use CMOR canonical variable names and grid-agnostic base class helpers:
from feather.diag.base import DiagnosticBase
from feather.diag.registry import register
@register
class MyDiagnostic(DiagnosticBase):
name = "my_diagnostic"
title = "My Diagnostic"
domain = "sfc"
variables = ["tas"] # CMOR canonical names
group = "temperature"
def compute(self):
for model in self.config.models:
mdata = self._load_model_var(model, "tas") # Grid-agnostic
ts = self._model_global_mean(mdata.data, model) # HEALPix or latlon
# ...
obs = self._load_obs_var("tas") # Sign-convention aware
return {"model_ts": ..., "obs_ts": ...}
def plot(self, results):
fig, ax = plt.subplots()
meta = self._build_metadata(title="...", figure_id="...", models=[...])
return [(fig, meta)]Then import the module in feather/diag/__init__.py so @register fires. See NEW_DIAGNOSTIC_SPEC.md for the full implementation guide.
conda activate feather
# Unit tests (synthetic data, no real data needed)
pytest tests/ -v -m "not integration"
# Integration tests (requires data access on DKRZ Levante)
pytest tests/ -v -m "integration"
# All tests
pytest tests/ -v1880 tests across 41 test files.
Python 3.10+. See environment.yml for the full dependency list, or install via conda env create -f environment.yml.
Key dependencies: xarray, dask, numpy, scipy, matplotlib, cartopy, healpy, nereus, gsw, cfgrib, pydantic, google-genai, openai.