assembly-economics/simulations/gadm02/dematerialization.py
tegwick 78d79ac4e3 ASSEMBLY-WP-0018-T01: dematerialization vs. Eurostat resource productivity; operator corrected; decoupling depends on phi_r (EU fit 0.66: none; DE 0.15: holds), BR-011; E5 wired
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 14913@bnt-lap001
Assistant-Session: add7ef69-b901-4756-9a67-34cf1125d96f
2026-10-13 23:26:24 +02:00

106 lines
5.4 KiB
Python

"""Calibrate dematerialization to observed resource productivity (ASSEMBLY-WP-0018-T01).
Observed: EU resource productivity (GDP per kg of domestic material consumption, chain-linked
2015 EUR; Eurostat env_ac_rp) rose 1.85 %/year in 2010-2024 (DE 2.73 %/year). Model operator:
the material draw per unit of output (rows "mat" / Y*) in the first decade. The model reproduces
the trend without an efficiency trend; the data constrain the rival share of organism output
phi_r (with the demand split at its default). The T04 readings are re-run at the fitted phi_r.
Usage: python3 -B simulations/gadm02/dematerialization.py
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
sys.dont_write_bytecode = True
import band_probe as bp # noqa: E402
import demand as dm # noqa: E402
import gadm05 as g5 # noqa: E402
import grounding as gr # noqa: E402
from scenarios import load_all # noqa: E402
HERE = Path(__file__).resolve().parent
OUT = HERE / "results"
OBS = {"EU": -0.0185, "DE": -0.0273} # yearly change of material intensity, 2010-2024
SPLIT = {"dm_lam": 0.05, "dm_eps": 2.0}
def cal_start(jur: str) -> tuple[dict, dict]:
st = json.loads((OUT / "calibrated_starts.json").read_text())[jur]
return {k: v for k, v in st["params"].items() if k in g5.PARAMS}, dict(st["init"])
def trend(p: dict, s: dict, eff: float, years: int = 10) -> float:
"""Yearly change of material draw per unit of output over the first `years`."""
rows = g5.simulate_eco(p, s, eco={"rm": "rest"}, demand={**SPLIT, "eff": eff}, T=years, record_every=10)
a, b = rows[0], rows[-1]
return ((b["mat"] / b["Ystar"]) / (a["mat"] / a["Ystar"])) ** (1 / years) - 1
def mat_trend(p: dict, s: dict, phi: float, demand: dict, years: int = 10) -> float:
rows = g5.simulate_eco({**p, "phi_r": phi}, s, eco={"rm": "rest"}, demand=demand, T=years, record_every=10)
a, b = rows[0], rows[-1]
return ((b["mat"] / b["Ystar"]) / (a["mat"] / a["Ystar"])) ** (1 / years) - 1
def fit_phi(p: dict, s: dict, target: float) -> float:
"""Rival share of organism output phi_r that reproduces the observed material-intensity trend."""
lo, hi = 0.0, 1.0
for _ in range(25):
mid = 0.5 * (lo + hi)
lo, hi = (mid, hi) if mat_trend(p, s, mid, SPLIT) < target else (lo, mid)
return 0.5 * (lo + hi)
def main() -> None:
OUT.mkdir(exist_ok=True)
lines = ["# GADM-0.5-dev: dematerialization against observed resource productivity", "",
"Generated by `simulations/gadm02/dematerialization.py` (ASSEMBLY-WP-0018-T01). Observed: material",
"intensity (inverse of Eurostat resource productivity, env_ac_rp, chain-linked 2015 EUR per kg DMC),",
"2010-2024. Model operator: material draw per unit of output in the first decade from the calibrated",
"start (Calibration v0.5) with ecological classes. Total throughput per output is the wrong operator",
"(it includes compute energy).", "",
"## Fit", "",
"| Jurisdiction | observed | model, no split (φ 0.2) | model, split ε 2 (φ 0.2) | φ_r reproducing the observation (split ε 2) |",
"|---|---|---|---|---|"]
phis = {}
for jur, target in OBS.items():
p, s = cal_start(jur)
phis[jur] = fit_phi(p, s, target)
lines.append(f"| {jur} | {100 * target:+.2f} %/y | {100 * mat_trend(p, s, 0.2, {'dm_lam': 0.0}):+.2f} %/y | "
f"{100 * mat_trend(p, s, 0.2, SPLIT):+.2f} %/y | {phis[jur]:.2f} |")
scs = load_all()
var = {a: gr.variant(gr.BAND[a]) for a in dm.ANCHORS}
lines += ["", "## T04 readings at the prior and at the EU-fitted φ_r (generative regime, material replenishment at rest)",
"", "| φ_r | Anchor | decoupled decades no split → split | sinks breach no split → split | VFAC no split → split |",
"|---|---|---|---|---|"]
res = {}
for phi in (0.2, round(phis["DE"], 2), round(phis["EU"], 2)):
for a in dm.ANCHORS:
out = {}
for cn, cd in (("none", {"dm_lam": 0.0}), ("split", SPLIT)):
rows = g5.simulate_eco({**scs["generative_regime"]["params"], **var[a][0], "phi_r": phi},
{**scs["generative_regime"]["init"], **var[a][1]}, eco={"rm": "rest"}, demand=cd)
out[cn] = {"dec": dm.decoupled_windows(rows), "V": rows[-1]["VFAC"],
"sink": next((r["t"] for r in rows if r["Es"] < g5.ECO_PARAMS["eco_floor"]), None)}
res[(phi, a)] = out
fs = lambda x: f"t {x:.0f}" if x is not None else "none" # noqa: E731
lines.append(f"| {phi} | {a} | {out['none']['dec']:.2f} → {out['split']['dec']:.2f} | "
f"{fs(out['none']['sink'])} → {fs(out['split']['sink'])} | {out['none']['V']:.3f} → {out['split']['V']:.3f} |")
lines += ["", "## Readings and robustness class", "", "| Reading | " + " | ".join(dm.ANCHORS) + " | class |",
"|---|" + "---|" * (len(dm.ANCHORS) + 1)]
for phi in sorted({k[0] for k in res}):
vals = [res[(phi, a)]["split"]["dec"] for a in dm.ANCHORS]
lines.append(f"| decoupled decades with split, φ_r {phi} | " + " | ".join(f"{v:+.2f}" for v in vals)
+ f" | {bp.classify(vals, 'sign')} |")
lines.append("")
(OUT / "dematerialization.md").write_text("\n".join(lines))
print("\n".join(lines))
if __name__ == "__main__":
main()