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
106 lines
5.4 KiB
Python
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()
|