Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions .claude/skills/model-calibration/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: model-calibration
description: Run, refresh, or diagnose the model's calibration pipeline (feed -> food_waste -> food_demand -> cost -> stability) that produces the per-config artefact sets under `data/curated/calibration/<source>/` (the default set is git-tracked). Covers the dependency order, the `tools/calibrate` wrapper, realistic runtime expectations, when each kind of upstream change forces a re-run, and how to diagnose the most common failure mode: a hidden supply/demand mismatch that inflates the production-stability L1 cost. Use whenever calibration is relevant -- the user touches inputs/build logic that feed the calibration solves, calibration artefacts look off, or a refresh of the artefacts is needed after a model/data change.
description: Run, refresh, or diagnose the model's calibration pipeline (feed -> food_waste -> food_demand -> cost -> stability) that produces the per-config artefact sets under `data/curated/calibration/<source>/` (the `default` and `gbd-anchored` sets are git-tracked). Covers the dependency order, the `tools/calibrate` wrapper, realistic runtime expectations, when each kind of upstream change forces a re-run, and how to diagnose the most common failure mode: a hidden supply/demand mismatch that inflates the production-stability L1 cost. Use whenever calibration is relevant -- the user touches inputs/build logic that feed the calibration solves, calibration artefacts look off, or a refresh of the artefacts is needed after a model/data change.
---

<!--
Expand All @@ -13,8 +13,14 @@ SPDX-License-Identifier: CC-BY-4.0

The default workflow consumes five calibration artefact groups organized
in per-config *sets* under `data/curated/calibration/<source>/`, selected
by the `calibration.source` config key (the `default` set is
git-tracked). Each is produced by a dedicated validation-mode solve and
by the `calibration.source` config key. Two sets are git-tracked:
`default` (fit against the anchoring-off baseline diet of the health-off
default config) and `gbd-anchored` (fit against the GBD-anchored diet;
consumed by the health-enabled configs gsa, gsa_fixed_diet, validation
and the doc configs). `tools/calibrate` resolves the base config's
diet.anchor_groups_to_gbd sentinel once and pins it across all five
steps, and provenance stamps record the *resolved* anchoring. Each
artefact group is produced by a dedicated validation-mode solve and
absorbs a specific class of residual mismatch so that ordinary solves
don't have to. Without these files in place, production-stability,
costs, and food/feed accounting drift from observed 2020 reality.
Expand Down Expand Up @@ -168,7 +174,7 @@ sequential (each Broyden iteration depends on the previous solve).

## Output landing zones

- `data/curated/calibration/<source>/*` -- one artefact set per base config, plus its `provenance.yaml` stamp; the `default` set is **git-tracked**. Commit a set together as a refresh; mixed-vintage artefacts are the most common cause of confusing downstream solves.
- `data/curated/calibration/<source>/*` -- one artefact set per base config, plus its `provenance.yaml` stamp; the `default` and `gbd-anchored` sets are **git-tracked**. Commit a set together as a refresh; mixed-vintage artefacts are the most common cause of confusing downstream solves.
- `processing/calibration/*` (or `processing/calibration-<source>/*` for non-default bases) -- shared upstream prep, NOT committed.
- `results/calibration/*` -- per-iteration solve logs, NOT committed.
- `results/calibration/calibration/deviation_penalty_trace.csv` -- per-iter Broyden trace (per-component lambda, achieved deviations, residual norm). Inspect when stability behaves oddly.
Expand Down
9 changes: 6 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ See `workflow/scripts/build_model/__init__.py` for the complete reference.
- Keep code concise: Prefer simple control flow; fail early on invalid external inputs.
- Do your best to avoid over-engineering. If you see possibilities for simplifying, suggest improvements (but let the user approve of such drive-by refactors first).
- Consistent style: Follow existing patterns in nearby files; don’t introduce new paradigms ad hoc.
- Comments (code and config alike) describe the *current* state as helpfully as possible, and only where something is not self-explanatory. Never write comments that justify a change or contrast with previous behaviour ("previously X", "default flipped to Y", "used to be unconditional") — that history belongs in the commit message.
- Reproducibility: Use the Snakemake targets below to validate changes; don’t hand‑run ad hoc pipelines unless necessary.
- No unused imports: The linter removes them automatically; only add imports when adding code that uses them.
- ASCII-only in code, comments, and docstrings: ruff's `RUF001`/`RUF002`/`RUF003` rules flag ambiguous Unicode look-alikes (`×`, `–`, `—`, `’`, `…`, `Σ`, `≈`, non-breaking spaces, etc.) and block pushes via the pre-push hook. Use plain ASCII substitutes: `*` for multiplication, `-` for dashes, `'` for apostrophes, `~=` for approx-equal, spell out Greek letters. Math notation in docstrings is the most common offender — write `sum over i of a_i * b_i`, not `Σ_i a_i × b_i`.
Expand Down Expand Up @@ -327,9 +328,11 @@ pixi run -e dev pytest -v # verbose output

Five calibrations feed the default workflow. Their outputs are organized
in per-config artefact *sets* under `data/curated/calibration/<source>/`
(selected by the `calibration.source` config key; the `default` set is
git-tracked) and builds depend on them. When upstream data or build
logic changes materially, regenerate in this order:
(selected by the `calibration.source` config key; the `default` set --
fit against the anchoring-off baseline diet -- and the `gbd-anchored`
set -- consumed by the health-enabled configs -- are git-tracked) and
builds depend on them. When upstream data or build logic changes
materially, regenerate in this order:

1. **feed** — `config/calibration/feed.yaml` → `grassland_yield.csv`,
`fodder_conversion.csv`, `exogenous_forage.csv`,
Expand Down
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,28 @@ introduce breaking changes to configuration and outputs.

## [Unreleased]

### Changed

- The health module is now **disabled by default** (`health.enabled: false`).
With health off, the workflow no longer requires the manually-downloaded
IHME GBD data and runs end to end without it; a clear startup error is
raised if health (or GBD anchoring) is enabled but the data is absent.

### Added

- New `diet.anchor_groups_to_gbd` option that decouples GBD anchoring of the
baseline diet's risk-factor food groups from the health module. Defaults to
the sentinel `match_health` (follow `health.enabled`); set `true`/`false` to
control it independently. Previously anchoring was unconditional. See
`docs/current_diets.rst` for a quantitative description of the difference and
the refined-grain caveat. The baseline diet feeds calibration, so two
artefact sets are now committed: `default` (recalibrated against the
anchoring-off default diet) and `gbd-anchored` (the previous GBD-anchored
artefacts, consumed by the health-enabled configs via
`calibration.source: gbd-anchored`). Provenance stamps record the *resolved*
anchoring, and `tools/calibrate` pins the base config's resolved anchoring
across all five calibration steps.

## [0.1.0] - 2026-06-15

First public release of GLADE (Global Land, Agriculture, Diet and Emissions),
Expand Down
4 changes: 4 additions & 0 deletions config/calibration/cost.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ validation:
water:
supply_scenario: "current_use"

# NB: diet.anchor_groups_to_gbd is pinned to the base config's resolved value
# by tools/calibrate (via a config overlay), so disabling health here for
# solver performance does not change the baseline diet the artefacts are fit
# against.
health:
enabled: false

Expand Down
2 changes: 1 addition & 1 deletion config/calibration/feed.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,5 @@ emissions:

solving:
io_api: "direct"
solver: "gurobi"
solver: gurobi
threads: 2
2 changes: 1 addition & 1 deletion config/calibration/food_demand.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,5 @@ emissions:

solving:
io_api: "direct"
solver: "gurobi"
solver: gurobi
threads: 2
2 changes: 1 addition & 1 deletion config/calibration/food_waste.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,5 @@ emissions:

solving:
io_api: "direct"
solver: "gurobi"
solver: gurobi
threads: 2
2 changes: 1 addition & 1 deletion config/calibration/stability.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ deviation_penalty:
generate: true # trigger the calibrate_deviation_penalty rule

solving:
solver: "gurobi"
solver: gurobi
io_api: "direct"
# Solves run sequentially in the iteration, so give each solve more
# threads instead of running multiple solves in parallel.
Expand Down
28 changes: 27 additions & 1 deletion config/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,25 @@ weight_conversion:
# --- section: diet ---
diet:
baseline_age: "All ages"
# Whether to anchor the per-country baseline-diet totals of the health
# risk-factor food groups (health.risk_factors, e.g. fruits, vegetables,
# red_meat, ...) to GBD dietary-exposure intake instead of the GDD/FAOSTAT
# estimate. Anchoring aligns the baseline diet with the same intake basis
# the GBD relative-risk curves are calibrated against, so it is required
# for attributable-burden numbers to be comparable to GBD; it is otherwise
# a modest, mostly distributional change to the baseline diet (see
# docs/current_diets.rst). The sentinel "match_health" follows health.enabled, so a
# health run anchors and a no-health run does not (and then needs no IHME
# GBD data on disk). Set explicitly to true/false to decouple the two.
# NOTE: refined "grain" is additionally adjusted by the whole-grain cereal
# residual fix whenever whole_grains is anchored -- see docs/current_diets.rst.
# CALIBRATION COUPLING: this flag changes the baseline diet, which the
# calibration artefacts under data/curated/calibration/ are fit against.
# Changing it (directly or via health.enabled) means the `default` artefact
# set no longer matches; point calibration.source at the `gbd-anchored` set
# (fit with anchoring on) or recalibrate. The provenance check errors on a
# mismatch (see docs/calibration.rst).
anchor_groups_to_gbd: match_health
# Foods whose per-country intake is computed directly from FAOSTAT Food
# Balance Sheet supply rather than disaggregated from GDD/GBD group
# totals. For each listed food the override sets
Expand Down Expand Up @@ -1217,7 +1236,14 @@ commodities:

# --- section: health ---
health:
enabled: true # Whether to include health costs in the objective function
# Whether to include the dietary health-burden module. When false, the
# manually-downloaded IHME GBD data is not needed (see docs/health.rst).
# IMPORTANT: this also drives diet.anchor_groups_to_gbd (sentinel
# "match_health"), so flipping it changes the baseline diet. The git-tracked
# calibration artefacts under data/curated/calibration/ are computed against a
# specific baseline diet, so after changing this (or anchoring) you must rerun
# `tools/calibrate` with matching options (see docs/calibration.rst).
enabled: false
region_clusters: 30
breakpoint_rel_tol: 0.05 # Max Stage 1 PWL deviation as a fraction of each RR curve's amplitude (5%)
log_rr_points: 15
Expand Down
15 changes: 15 additions & 0 deletions config/example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,21 @@ name: "example"
# emissions:
# ghg_price: 200 # USD per tonne CO2-eq

# Enable the dietary health-burden module. This requires the manually-
# downloaded IHME GBD data (see docs/health.rst); when left disabled (the
# default) none of that data is needed. Enabling health also anchors the
# baseline diet to GBD intake exposure (diet.anchor_groups_to_gbd defaults to
# "match_health"); set that key explicitly to decouple the two.
# With anchoring on, also consume the calibration artefacts fit against the
# anchored diet (the default set is fit against the anchoring-off diet).
# health:
# enabled: true
# value_per_yll: 50000 # USD per year of life lost (0 = evaluate post-hoc only)
# diet:
# anchor_groups_to_gbd: true # or false to keep the GDD/FAOSTAT-only diet
# calibration:
# source: "gbd-anchored"

# Restrict to specific countries (ISO 3166-1 alpha-3 codes)
# countries:
# - NOR
Expand Down
10 changes: 10 additions & 0 deletions config/gsa.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@
# live in a companion config, gsa_l1.yaml, on the paper branch.

name: "gsa"

health:
enabled: true

# With health on, the baseline diet is GBD-anchored, so consume the artefact
# set calibrated against that diet (the `default` set is anchoring-off).
# Regenerate with `tools/calibrate --base config/gsa.yaml`.
calibration:
source: "gbd-anchored"

scenarios:
# Baseline scenario for calibrating piecewise food utility blocks. Consumer
# values (dual variables) are taken from this enforced-baseline solve.
Expand Down
14 changes: 12 additions & 2 deletions config/gsa_fixed_diet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,18 @@ sensitivity_analysis:
validation:
enforce_baseline_diet: true

# Keep post-hoc YLL evaluation disabled: diet is fixed, so health outputs
# would be essentially constant across scenarios and just add solve time.
diet:
# Anchor to GBD so the fixed baseline diet matches the health-enabled
# gsa.yaml diet (with health off, the sentinel would resolve to false).
anchor_groups_to_gbd: true

# Anchored baseline diet -> consume the artefact set calibrated against it
# (the `default` set is anchoring-off).
calibration:
source: "gbd-anchored"

# No post-hoc YLL evaluation: diet is fixed, so health outputs would be
# essentially constant across scenarios and just add solve time.
health:
enabled: false

Expand Down
7 changes: 7 additions & 0 deletions config/schemas/config.schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -988,13 +988,20 @@ properties:
type: object
required:
- baseline_age
- anchor_groups_to_gbd
- source_basis
- gdd_ia
additionalProperties: false
properties:
baseline_age:
type: string
description: "Age group for baseline diet (e.g., 'All ages')"
anchor_groups_to_gbd:
oneOf:
- type: boolean
- type: string
enum: ["match_health"]
description: "Anchor risk-factor food-group baseline-diet totals to GBD intake exposure. true/false, or 'match_health' to follow health.enabled."
fbs_override_foods:
type: array
uniqueItems: true
Expand Down
4 changes: 3 additions & 1 deletion config/tutorial/01_ghg_prices.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@ scenarios:
validation:
use_actual_yields: true

# This tutorial does not use health costs.
# Health module on for the GBD-anchored baseline diet and health outputs,
# but with zero weight in the objective (post-hoc YLL evaluation only).
health:
enabled: true
value_per_yll: 0

# Compare all three scenarios in auto-generated plots.
Expand Down
3 changes: 3 additions & 0 deletions config/tutorial/02_consumer_values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,10 @@ scenarios:
validation:
use_actual_yields: true

# Health module on for the GBD-anchored baseline diet and health outputs,
# but with zero weight in the objective (post-hoc YLL evaluation only).
health:
enabled: true
value_per_yll: 0

plotting:
Expand Down
7 changes: 7 additions & 0 deletions config/validation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,16 @@ cost_calibration:
food_demand_calibration:
enabled: false

# Health module on with zero objective weight: post-hoc YLL evaluation only.
health:
enabled: true
value_per_yll: 0

# With health on, the baseline diet is GBD-anchored, so consume the artefact
# set calibrated against that diet (the `default` set is anchoring-off).
calibration:
source: "gbd-anchored"

emissions:
ghg_pricing_enabled: false

Expand Down
Loading
Loading