Skip to content

Replace L1 stability anchoring with exactly calibrated PMP market-response curves (supply + demand) - #48

Open
koen-vg wants to merge 31 commits into
mainfrom
pmp-demand-response
Open

Replace L1 stability anchoring with exactly calibrated PMP market-response curves (supply + demand)#48
koen-vg wants to merge 31 commits into
mainfrom
pmp-demand-response

Conversation

@koen-vg

@koen-vg koen-vg commented Jul 31, 2026

Copy link
Copy Markdown
Member

Folds in #46 (formerly stacked on it; that PR is closed and its full diff is included here).

What

Production and consumption anchoring is now done by convex piecewise-linear marginal-cost / marginal-utility curves in the positive-mathematical-programming tradition (Howitt 1995), calibrated exactly by the standard two-phase procedure, replacing the L1 deviation_penalty and its Broyden stability calibration as the default mechanism.

Supply side

  • Phase 1 (new market_response step in tools/calibrate, replacing stability in the default chain): one solve with every curve group held at its observed activity behind an elastic pin. The pinning duals -- per-group wedges between marginal value and accounting cost -- land in data/curated/calibration/<source>/market_response.csv (both tracked sets fit and committed).
  • Phase 2: market_response.intercepts: "calibrated" feeds the wedges back as curve intercepts, so each group's marginal cost at the baseline equals the marginal value there and the observed allocation is the exact optimum -- no hard constraints, no tuned deviation target. The configured elasticity (crops 0.5, grassland 0.3, animals 0.4; literature-grounded in the configuration reference) governs only the response away from the calibrated point.
  • Granularity is configurable: link (default -- prices spatial reallocation itself, so no deviation penalty is needed), region, or country. Multi-cropping links carry curves as their own component, per (combination, country) bundle.

Demand side

  • Demand component: every (food, country) consumption link gets a concave marginal-utility curve through its observed intake -- the same convex deviation-cost form, since a convex deviation cost is a diminishing marginal utility. The intercept is minus the fitted willingness to pay; the slope is stated at a fitted reference price over per-food-group demand elasticities (Green et al. 2013). Ordinary solves previously had no demand-side valuation at all (consumption was a passive sink); the component supersedes the consumer-values / food_utility_piecewise pattern and is mutually exclusive with it.
  • Sequential calibration. Jointly pinning both sides closes every commodity chain and leaves the producer/consumer split of each chain's wedge undetermined (duals park at the pin slack bound). Instead the demand pin runs against the calibrated, elastic production curves; the slope basis comes from the same pin against zero-intercept curves (accounting-cost-chain prices); market_response.calibration.sweeps (default 3) Gauss-Seidel passes contract cross-side drift ~4x per sweep.

Formulation and performance

The curves enter the LP as bounded deviation-segment variables (chord slopes as objective coefficients, merit-order fill by convexity) rather than epigraph rows; at link granularity the row-based form put ~1.5M rows (half the model) into the LP. Full-model deployed solves: HiGHS 133 s / Gurobi 40 s at the default GHG price (187 s HiGHS unpriced), making the open-source solver fully viable for demand-enabled configs.

Verification (default config, unpriced reference solve, nothing pinned)

component deviation from observed 2020 baseline
food consumption 0.003 %
crop area 0.14 %
pasture area 0.01 %
animal feed use 0.31 %

Fitted reference prices are economically sensible: beef ~9.6, sheep ~8.5, chicken/pork ~2.9, dairy ~0.8, eggs ~1.4, tomato ~0.6, rice ~0.3 USD/kg. The priced default solve (GHG 200 USD/t) then shows the pure elasticity-governed response.

Key design points

  • The pin is elastic (pin_slack_cost 100): reference data is never perfectly consistent with land/water/feed constraints, and a hard pin is infeasible. Censored duals surface inconsistencies instead of hiding them (the extractor and the calibration log now report duals parked at the bound explicitly).
  • Pin duals are identified at capacity-saturated links. A quarter of crop links sit exactly at baseline == p_nom_max (the suitable-area clip), where the pin is redundant with the capacity bound and its dual degenerate -- the solver parked 26% of crop intercepts at the slack bound, which at deployment froze 229 Mha of cropland against contraction. Phase-1 pins now lift the capacity bounds of pinned links (before the frozen linopy model is created), so those wedges are the true local rent gradient; censoring collapsed to 0.6% of crop groups.
  • Demand curves satiate. A demand group whose marginal utility is still positive at the outer end of the sampled range no longer extrapolates it forever: the unbounded expansion tail is floored at zero marginal utility.
  • Zero baselines are fixed, disclosed. The curves are an intensive-margin model: observed-zero groups get no curve and are fixed to zero (at the default link granularity that closes the extensive margin per link, mirroring the growth caps' country-level contract); the fixed share per component is logged and documented.
  • Wedges are regime-specific: phase 1 runs under the base config's own operating regime with policy dials at neutral (documented in docs/calibration.rst).
  • Slope at accounting cost (Howitt's form, not Merel-Bucaram): under the reference regime the wedges trace the Ricardian rent gradient and sub-marginal groups have c + lambda <= 0, where a calibrated-cost slope is undefined. A group with wedge lambda realises elasticity eta * (c + lambda) / c.
  • Level/slope decoupling on the demand side: quantities alone never identify the producer/consumer split of a chain's wedge; exactness only needs consistency, while the slope needs a real price scale -- hence the separate slope-basis solve. Anchoring wedge levels to observed producer prices is a possible future refinement.

Demand-side regime is validated, not documented

With market_response enabled, schema validation requires components.demand and validation.enforce_baseline_diet to be exact opposites: the demand side is either elastic or held at the observed baseline. Both reproduce the calibration's fitted point (the final production pin runs with the demand curves active); pairing the production intercepts with any other demand valuation silently breaks baseline reproduction, and the component switches are solve-time keys that provenance checking cannot see. Consequently gsa.yaml and doc_figures.yaml are migrated onto the demand component here (piecewise utility off; gsa's consumer-values baseline scenario is gone), and gsa_fixed_diet.yaml keeps demand off with the diet enforced.

Calibration chain

The default chain (tools/calibrate, watched by --check/--record) is now feed, food_waste, food_demand, market_response. The legacy anchoring steps -- cost and stability -- sit outside it and are run explicitly (in that order) for sets consumed by configs that anchor with them; --check/--record still cover them whenever their artefacts are present. Fresh --base sets are no longer seeded: each step consumes only its predecessors' artefacts, so the chain regenerates a new set from scratch and legacy artefacts appear only when their steps are run. Both shipped sets carry freshly refit curves, current cost corrections, and a re-calibrated deviation_penalty.yaml, all checking green.

Migration

  • The deviation_penalty machinery remains available; the tests and tutorials keep the L1 explicitly (with cost_calibration: {enabled: true}, since PMP and the cost corrections are mutually exclusive).
  • Link-granularity intercepts are keyed by link name, so a structurally different config fails loudly on missing group keys and needs tools/calibrate --base <config> market_response.
  • Local configs not in this repo inheriting the new defaults need either their own artefact set or market_response: {enabled: false} + cost_calibration: {enabled: true} + deviation_penalty: {enabled: true}.

koen-vg added 17 commits July 30, 2026 13:49
Adds a positive-mathematical-programming supply curve alongside the existing
deviation penalty, off by default. Each production group -- (crop, country),
grassland by country, (animal product, country) -- gets a convex marginal-cost
curve through its observed activity whose slope reproduces a configured supply
elasticity, so response to a shock is graduated instead of the dead band and
jump an L1 kink produces, and the curvature comes from an exogenous elasticity
rather than a fitted deviation target.

The quadratic deviation cost is approximated by tranches priced at the slope
times their midpoint deviation. Prices increase with distance from the
baseline, so a cost-minimising solution fills the near tranches first and
convexity needs no ordering constraints. The outermost tranche in each
direction is unbounded, which extrapolates the curve linearly instead of
imposing a bound that could conflict with the growth caps.

Curves are deliberately per group, not per link: they price how much of a
commodity a country produces, not where within the country it sits. The
per-link deviation penalty keeps carrying that spatial inertia, so the two
mechanisms are complementary rather than alternatives.
Drives a one-group LP through a real solve at a range of output prices and
checks the arc elasticity comes out as configured, which is the property the
slope formula exists to deliver. Also covers symmetry about the baseline, the
elasticity_factor scan dial, monotone response, feasibility beyond the
expansion range via the unbounded outermost tranche, and the guards for
zero-baseline groups and non-positive marginal cost.
…akdown

The curves add their deviation cost on auxiliary tranche variables, which
PyPSA statistics cannot see, so without this the objective-breakdown identity
check fails on any run with curves enabled -- the same trap the multi-cropping
bounded subsidy hit.

The evaluation recovers the term from the solved tranche variables and the
prices recorded when the curves were built, and reports it as its own category
rather than folding it into production stability: the two anchor production by
different mechanisms and a run may carry either, both, or neither. For the same
reason it sits outside the deviation-penalty gate, which a curves-only run
disables.
Activity moves in whole tranches, so with equal widths the curve cannot express
a move smaller than expansion_range / n_blocks of baseline. At the previous
default that was 12.5%, against typical group moves of a few percent: measured
on a full-resolution solve, 97% of moving groups in the curves-plus-penalty
regime had their entire move inside the first tranche, so the curve was acting
as a flat-rate group penalty and not as a curve at all.

width_growth narrows the near tranches so the resolution sits where groups
actually are, while the outer tranches still resolve large moves. Six tranches
growing by a factor of two span the same range with a finest tranche of 0.8% of
baseline, where equal widths would need dozens. Shrinking expansion_range is not
equivalent: it would resolve small moves and degenerate for large ones, and
large moves are the interesting regime under carbon pricing.

Prices remain the slope times each tranche's midpoint deviation, now measured
from the accumulated width, so the piecewise cost still traces the quadratic and
stays monotone in the tranche index -- convexity needs no ordering rows. At
width_growth 1.0 the construction reduces to the previous equal widths exactly,
which a test pins.
`granularity` chooses whether a curve prices a country's total output of a
commodity, its output per optimisation region, or each production link
separately. Finer resolution prices reallocation that a coarser setting leaves
to the per-link deviation penalty, so the two mechanisms can be traded off
against each other rather than assumed.

The elasticity is invariant to the choice: each group's slope is
`cost / (elasticity * baseline)`, so a link priced on its own carries the
configured elasticity with respect to its own margin, and the aggregate carries
the same one wherever costs within a group are similar. Cost is 2 * n_blocks
variables per group, so link granularity is the expensive end.
Standard two-phase PMP: supply_response.pin_baseline holds every group
at its observed activity behind an elastic pin (pin_slack_cost) and the
solve writes the pinning duals -- the per-group price wedges -- next to
the solved network; supply_response.intercepts feeds them back so each
curve's marginal cost at the baseline equals the marginal value there,
making the observed allocation the exact optimum of the unpinned model.
The slope becomes (cost + intercept) / (elasticity * baseline), stating
the elasticity at the calibrated marginal cost (Merel and Bucaram 2010).

The pin is elastic because the reference data is never perfectly
consistent with every hard constraint: validation-mode pinning only
stays feasible through land and water slack channels enabled by
use_actual_production, and a hard equality pin is infeasible without
them. Groups that use pin slack get their intercept censored at the
slack price, are flagged in the log, and carry a slack column in the
intercepts file for diagnosis.
Under a pinned diet the fitted wedges trace the Ricardian rent
gradient, so sub-marginal groups at the extensive margin have
c + lambda <= 0, where a calibrated-marginal-cost slope (Merel and
Bucaram 2010) is undefined -- about a third of link-level crop groups
in practice. Exactness only needs the intercept, so the slope reverts
to Howitt's accounting-cost form c / (eta * b); a group with wedge
lambda then realises elasticity eta * (c + lambda) / c with respect to
its market price.
… multi-cropping

The hand-rolled tranche formulation (paired bounded variables per
direction, midpoint prices, a balance row per group, and a module-level
price stash for the post-solve cost evaluation) is replaced by linopy's
piecewise machinery: the deviation cost is sampled at the breakpoint
grid and one free variable per group is bounded below by the chords
from linopy.piecewise.tangent_lines. Minimisation presses the variable
onto the chords' upper envelope -- the same piecewise-linear function
the tranches encoded -- and its solution is the objective term, so the
cost evaluation reads it directly. Beyond the outermost breakpoints the
envelope extrapolates at the end chords' slopes, preserving the
no-hidden-bounds property.

Multi-cropping links now carry curves as their own component
(multi_crops, grouped per (combination, country) bundle): at any
granularity the bundle is priced as a unit, which sidesteps the
attribution question that had left them uncovered.
The curves replace the L1 deviation penalty as the default anchoring
mechanism, at link granularity (country and region remain available via
supply_response.granularity). A new supply_response step in
tools/calibrate -- replacing the legacy stability step in the default
chain -- runs one baseline-pinned solve and writes each group's price
wedge to data/curated/calibration/<source>/supply_response.csv, which
the new intercepts: "calibrated" sentinel resolves against at solve
time; both tracked artefact sets are fit and committed.

The pinned solve runs under the base config's own operating regime with
the policy dials at neutral: no GHG price, demand left to whatever
drives it in ordinary solves. Wedges are regime-specific -- a first fit
under an enforced baseline diet made a plain default solve walk 20-50%
off baseline, because the free-demand regime values production
differently. Fit under its own regime, the unpriced default solve
reproduces observed 2020 production to 0.3% on crops and under 0.02% on
grassland and animals, with the residual confined to groups whose
intercepts censor at pin_slack_cost (flagged in the artefact's slack
column); a priced solve then shows the pure elasticity-governed
response.

Configs whose builds differ structurally from the default (tests,
tutorials) or that pin production outright (validation) keep the L1
penalty and disable the curves explicitly.
…ection

The correction generator was sized on the intersection of harvested-area
and land_use-supply bus indexes, silently dropping buses with baseline
crop area but no land-cover cropland supply at all. Those 16 buses
(0.016 Mha, all in irrigation-heavy or data-poor regions) were the only
physically infeasible spots in the supply-response pin solve. They now
receive a correction generator sized to their full baseline demand, like
every other deficit bus.
At pin_slack_cost 10, 28 Mha of crop baseline (8372 links) had its
wedge censored: horticulture production costs are derived as a share of
producer revenue (13-30+ bnUSD/Mha for fruit and vegetables), and the
neutral fit regime values output only through nutrition demand, so the
genuine wedge -- the consumer price premium -- routinely exceeds 10.
The censored links produced nothing in the fit and only ~85% of their
baseline in deployed solves, accounting for essentially the whole 0.31%
crop residual.

At 100, censoring collapses to 0.037 Mha (153 links, the greenhouse-
cost tail above 100 bnUSD/Mha), and together with the land-correction
fix the unpriced default solve reproduces observed production at 0.002%
crops / 0.000% multi-crops / 0.000% grassland / 0.000% animals.

Both artefact sets refit; interior wedges are unchanged. Fingerprints
restamped: the remaining staleness causes are solve-path modules the
earlier steps disable, plus the 1e-5-relative land-correction change.
The extractor writes value_bnusd_per_mt = +mu_p_set and keeps negative
duals; the flooring described here was reverted in edd3383.
…curves

A new demand component in the supply-response machinery gives every
(food, country) consumption link a concave marginal-utility curve
through its observed intake -- the same convex deviation-cost form as
the production curves, whose intercept is minus the fitted willingness
to pay and whose slope is stated at a fitted reference price
(slope_basis column) over the own-price food demand elasticity.
Ordinary solves previously had no demand-side valuation at all; the
component supersedes the consumer-values / piecewise-utility pattern
and is mutually exclusive with it.

The fit is sequential rather than joint: pinning both sides at once
closes every commodity chain and leaves the producer/consumer split of
each chain's wedge undetermined (the duals park at the pin slack
bound). Instead the demand pin runs against the calibrated, elastic
production curves (unique food-bus prices, measured against exactly the
deployed supply side), the slope basis comes from the same pin against
zero-intercept curves (accounting-cost-chain prices), and a configurable
number of refinement sweeps re-pins each side against the other's
fitted curves; the cross-side drift contracts roughly 4x per sweep.
pin_baseline accordingly accepts a component list.

Verified on the default config (unpriced): demand reproduces observed
intake at 0.004%; production at 0.41% crops / 0.75% multi-crops /
0.01% grassland / 0.19% animals after two sweeps, converging further
with the default three. gsa, gsa_fixed_diet and doc_figures keep their
existing demand mechanisms via supply_response.components.demand: false.
Fitted reference prices are economically sensible (beef ~9.6, chicken
~2.9, dairy ~0.8, tomato ~0.6 USD/kg); elasticity defaults are grounded
in the empirical literature in the configuration reference.
The mechanism now calibrates both sides of each market, so the config
section, module, functions, rule, calibration step, artefact and group
key (mr_group) drop the supply-only name. The per-side cost categories
keep their names: supply_response and demand_response remain the two
sides in the objective breakdown and network metadata.
data/curated/calibration/default/market_response.csv now carries the
sequentially fitted demand rows (intercept, slack, slope_basis) next to
the production wedges; the gbd-anchored set stays production-only since
its consumers keep the consumer-values mechanism. Verified on the
default config (unpriced): demand 0.004%, crops 0.200%, multi-crops
0.404%, grassland 0.008%, animals 0.096% deviation from the observed
baseline with the default three calibration sweeps.
…demand by food group

market_response.elasticities.demand becomes a per-food-group mapping.
The verified meta-regression evidence (Green et al., BMJ 2013) puts
own-price food demand magnitudes at 0.43-0.78 depending on food group
and income tier -- a flat 0.4 understates everything except rich-country
staples -- so the defaults now carry the middle-income column as global
central values (staple-like groups 0.55, meat and dairy 0.72, eggs 0.54,
fruit and vegetables 0.65, fats and oils 0.5, discretionary 0.74), with
documented analogs for groups the meta-regression does not cover.
Elasticities enter only at solve time, so the fitted artefacts are
unchanged; the deployed verification is unaffected (demand 0.010%,
crops 0.201%).

Supply-side values survive review and keep their citations honest:
crops 0.5 sits mid-band of the long-run Nerlovian range 0.3-1.2 (Rao
1989); Roberts & Schlenker 2013's ~0.1 is explicitly a short-run,
weather-identified response; livestock 0.4 is conservative against
long-run cattle estimates of 0.3-2.9 and dairy 0.47-0.65 (with Jarvis
1974's negative short-run herd dynamics noted); grassland 0.3 is
documented as an assumption by analogy to cropland area elasticities
(Iqbal & Babcock 2018), since no dedicated estimate exists.
@koen-vg

koen-vg commented Jul 31, 2026

Copy link
Copy Markdown
Member Author

Two additions since opening:

  • Per-food-group demand elasticities, literature-grounded. A three-way literature verification (crop supply / livestock+grassland / food demand) confirmed the supply-side defaults (crops 0.5 mid-band of the long-run Nerlovian range 0.3-1.2 per Rao 1989; livestock 0.4 conservative against long-run cattle estimates; grassland 0.3 now honestly documented as an assumption by analogy, since no dedicated estimate exists) but showed a flat demand elasticity of 0.4 understates everything except rich-country staples. market_response.elasticities.demand is now a per-food-group mapping carrying the middle-income column of Green et al. (BMJ 2013) as global central values (staples 0.55, meat/dairy 0.72, fruit & veg 0.65, discretionary 0.74). Elasticities enter only at solve time, so the artefacts are unchanged and the deployed verification is unaffected (demand 0.010%, crops 0.201%).
  • HiGHS performance caveat. The demand-curve LP is hard for HiGHS 1.12's default settings: the unpriced default solve did not finish IPM crossover/simplex cleanup within 15 minutes, vs ~2.5 minutes for Gurobi. Configs using the demand component currently want Gurobi; HiGHS option tuning (e.g. running IPM without crossover where duals-at-a-vertex are not needed) is left as a follow-up.

koen-vg added 2 commits July 31, 2026 00:09
The epigraph-over-chords formulation put 2 * n_blocks inequality rows per
curve group into the model -- at link granularity about 1.5M rows, half
of all rows -- and the near-ties PMP creates at the calibrated optimum
made those rows expensive for simplex and barrier alike. The deviation
from baseline now splits into bounded segment variables whose objective
coefficients are the chord slopes; convexity fills them in merit order,
so the formulation is LP-identical (A/B objectives agree to 1e-7
relative, baseline reproduction unchanged) while the chords become
variable bounds instead of rows. Full-model solves speed up 7-9x under
HiGHS (922 s -> 133 s at the default GHG price, 1646 s -> 187 s
unpriced) and 2.4x under Gurobi (96 s -> 40 s single-threaded).
@koen-vg

koen-vg commented Jul 31, 2026

Copy link
Copy Markdown
Member Author

Performance follow-up, benchmarked on the full default config:

Formulation (5597235): the curves now enter the LP as bounded deviation-segment variables instead of an epigraph over chord rows. LP-identical by construction (A/B objectives agree to ~1e-7 relative; baseline reproduction unchanged at crops 0.201% / animals 0.096% / demand 0.003%), but the old formulation's ~1.5M chord rows (half of all model rows) become variable bounds. Deployed solve times:

solve HiGHS Gurobi (1 thread)
GHG 200 (default) 922 s -> 133 s 96 s -> 40 s
unpriced reference 1646 s -> 187 s -

This removes the practical need for Gurobi on demand-component configs.

Solver tuning (measured, mostly negative results worth recording): solving.threads: 4 gives a further ~2x under Gurobi barrier on priced solves (but slows the near-degenerate unpriced case); Crossover: 0 is 7x slower plus 2-3x memory on this model (degenerate optimal face), BarConvTol loosening backfires, dual simplex and concurrent LP lose outright; under HiGHS the default choose beats ipm/pdlp, and market_response.n_blocks: 3 (solve-time key, no refit) remains a 2x escape hatch at ~0.5% objective cost.

@koen-vg koen-vg changed the title Calibrate the demand side with sequential PMP marginal-utility curves Replace L1 stability anchoring with exactly calibrated PMP market-response curves (supply + demand) Jul 31, 2026
@koen-vg
koen-vg changed the base branch from pmp-supply-response to main July 31, 2026 16:58
koen-vg added 3 commits July 31, 2026 11:11
…al base config

Calibrating against config/gsa.yaml coupled the shared artefact set to
one consumer config: any structural override added there would silently
redefine the set, and the fingerprints recorded the whole GSA config as
a calibration input. The new config/gbd_anchored.yaml is the default
config with the GBD-anchored baseline diet as its only structural
change, which is verifiably (via the structural snapshot) what the set
was fit against all along.
Full-chain refit after the market-response hardening: the diet
preparation and zero-baseline handling changed, and the gbd-anchored set
now carries demand curves like the default set (fit against its new
minimal base, so the two sets differ only in the baseline diet). Demand
censoring is minimal in both (12 groups, mostly sugar in small
countries, total slack 0.09 Mt).
@koen-vg

koen-vg commented Jul 31, 2026

Copy link
Copy Markdown
Member Author

Recalibrated both artefact sets after d9f3a13 (4839b18), and gave the gbd-anchored set a dedicated minimal calibration base (517aede, config/gbd_anchored.yaml = default + GBD-anchored diet, nothing else — verified via the structural snapshot to be exactly what the set was implicitly fit against before, without the coupling to config/gsa.yaml). Consequence: both sets now carry demand curves and differ only in the baseline diet; the earlier "gbd-anchored covers production only" caveat is gone. Demand censoring is minimal in both sets (12 groups, mostly sugar in small countries, |slack| 0.09 Mt). The previously pathological demand-fit stage on the gbd-anchored structure (24+ min per solve) is also gone — all stages now run at 20-31 s.

koen-vg added 8 commits July 31, 2026 13:11
…satiation

The equality pin of a phase-1 market-response fit is redundant with the
capacity bound wherever baseline_area_mha equals p_nom_max (the
suitable-area clip makes this the case for a quarter of crop links), so
the pin dual is degenerate and the solver reports the slack-bound
extreme: 26% of crop and 15% of grassland intercepts sat at exactly
-pin_slack_cost, which at deployment priced contraction of 229 Mha of
cropland at 100 bnUSD/Mha and effectively froze it. Capacity bounds of
pinned links are now lifted (before the frozen linopy model is created)
so the pin is the unique binding constraint and its dual is the true
wedge, scarcity rent included.

Demand groups whose marginal utility is still positive at the outer end
of the sampled range no longer extrapolate that utility forever on the
unbounded tail: the tail's cost slope is floored at zero marginal
utility, so consumption cannot run away when supply becomes very cheap
while nutrition constraints can still push intake up at zero utility.

Also: production links now fail loudly on a missing baseline like
demand links do (instead of silently becoming a zero-fixed group),
grouping columns reject missing values, the intercept extractor and the
calibration driver report duals censored at the pin slack bound, the
zero-baseline log states the fixed share per component, and solves with
no production anchoring at all warn (new use_actual_production solve
param, mirrored in both rule harnesses and the cluster manifest).
…ion chain

The default chain run by tools/calibrate (and watched by --check and
--record) is now feed, food_waste, food_demand, market_response: with
cost_calibration disabled by default and mutually exclusive with
market_response, the cost artefacts feed nothing on the default path,
so cost joins stability as an explicitly-run legacy step (in that
order; stability consumes the cost corrections). --check/--record still
cover a legacy step whenever its artefacts are present in the set.

Seeding of fresh --base artefact sets is removed: each step consumes
only its predecessors' artefacts, so the chain regenerates a new set
from scratch in order, single steps require their predecessors on disk,
and legacy artefacts appear only when their steps are run. This also
stops a fresh set from silently carrying the default set's
deviation_penalty.yaml under a provenance stamp claiming it was fit
against the new base.
With market_response enabled, schema validation now requires
components.demand and validation.enforce_baseline_diet to be exact
opposites: the demand side is either elastic or held at the observed
baseline. Both regimes reproduce the fitted point of the sequential
calibration (the final production pin runs with the demand curves
active), whereas pairing the production intercepts with any other
demand valuation silently breaks baseline reproduction -- which
gsa.yaml and doc_figures.yaml did by substituting piecewise food
utility. Both are migrated onto the demand component; gsa also drops
its consumer-values baseline scenario (nothing consumes it with
piecewise utility off) and doc_figures keeps its baseline scenario,
demand-off, for the consumer-values figure.

Also: the test config restores the legacy cost corrections alongside
its L1 anchoring (the cost_calibration default flip had silently left
it solving against uncalibrated costs), gsa.yaml points its regenerate
hint at config/gbd_anchored.yaml instead of overwriting the shared set
with a gsa-specific fit, and tests cover the new schema rule, the
supply/demand response breakdown categories, and the GDD unit-map
completeness guard.
…nism

Fixes the drift a full review of the branch surfaced: the last
surviving description of the removed per-component suppression gate
(costs.rst), stale claims that the GSA configs anchor with the
calibrated L1 penalty (sensitivity_analysis.rst, consumer_values.rst),
the objective-breakdown column table missing the supply_response and
demand_response categories along with several older ones
(analysis.rst), the market-response calibration section not listing
market_response.csv among the diet-sensitive artefacts nor among the
consumed calibration outputs, missing config-reference entries for
pin_slack_cost and the calibration subsection, and the calibration
rule's docstring describing a single pinned solve. Also states the
contracts the review pinned down: the configured elasticities act per
relative change of the reference price (accounting cost / slope basis),
not as arc elasticities at the curve's equilibrium wedge; the
zero-baseline closure magnitudes; the capacity lift during phase-1
pins; and the demand satiation floor.
Recording only ever updated its own step entry, so the pre-rename
supply_response block survived in both committed fingerprint files as a
dead, uncheckable entry pointing at an artefact that no longer exists.
…ibration

Both artefact sets are refit with the pinned-solve capacity lift: the
mass of intercepts censored at the pin slack bound collapses from 26%
to 0.6% of crop groups (15% to 4% of grassland), so the wedges of
capacity-saturated links are now the true local rent gradient instead
of a degeneracy artifact that froze 229 Mha of cropland against
contraction. The legacy deviation_penalty.yaml in both sets is
re-calibrated against the current cost corrections (default: cropland
1.38, grassland 0.23, feed 0.063; gbd-anchored: 1.52/0.24/0.056), the
fingerprints cover the legacy cost and stability steps and drop the
dead pre-rename supply_response blocks, and both sets check green.

Verification (unpriced default reference solve, nothing pinned):
crop area 0.14%, pasture 0.01%, animal feed 0.31%, food consumption
0.003% off the observed 2020 baseline.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant