1"""Figures from solved scenarios (results/scenarios/<name>_paths.npz). Skips scenarios not yet solved.2Outputs results/figures/fig03..fig09 (pdf + png)."""3import glob4import json5import os6import sys78import matplotlib9import numpy as np1011matplotlib.use("Agg")12import matplotlib.pyplot as plt1314sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))15from agi_ge.analysis import campbell_shiller_decomposition, valuation_ratios # noqa: E40216from agi_ge.solve import load_paths # noqa: E4021718ROOT = os.path.join(os.path.dirname(__file__), "..")19SC = os.path.join(ROOT, "results", "scenarios")20FIG = os.path.join(ROOT, "results", "figures")21os.makedirs(FIG, exist_ok=True)22plt.rcParams.update({"font.size": 9, "axes.spines.top": False, "axes.spines.right": False, "legend.frameon": False})23C = {"baseline": "#1f3b73", "no_ai": "#7f8c8d", "rapid_agi": "#c0392b", "fast_moore_cheap_energy": "#e67e22",24 "fast_moore_energy_constrained": "#8e44ad", "complements_leontief_like": "#16a085", "complements_cobb_douglas": "#2980b9",25 "substitutes": "#d35400", "ai_monopoly": "#2c3e50", "citizen_fund_20": "#27ae60", "land_tax_50": "#f39c12",26 "compute_tax_30": "#c0392b", "capital_tax_20": "#8e44ad", "foreign_ai_50": "#7f8c8d", "low_risk_aversion": "#e67e22",27 "high_risk_aversion": "#16a085", "scarce_land": "#8e44ad", "severe_chip_depreciation": "#2c3e50",28 "reinstatement": "#27ae60", "moore_ends_2040": "#f39c12", "moore_never_ends": "#c0392b"}29LAB = {"baseline": "baseline", "no_ai": "AI frozen (2026 capability)", "rapid_agi": "rapid AGI (2032)",30 "fast_moore_cheap_energy": "fast Moore, cheap energy", "fast_moore_energy_constrained": "fast Moore, energy-constrained",31 "complements_leontief_like": r"complements ($\sigma=0.4$)", "complements_cobb_douglas": r"Cobb-Douglas ($\sigma=1$)",32 "substitutes": r"substitutes ($\sigma=1.5$)", "ai_monopoly": "AI monopoly (markup 1.5)", "citizen_fund_20": "citizen fund 20%",33 "land_tax_50": "land tax 50%", "compute_tax_30": "compute tax 30%", "capital_tax_20": "capital-income tax 20%",34 "foreign_ai_50": "50% foreign-owned compute", "low_risk_aversion": r"$\gamma=1$", "high_risk_aversion": r"$\gamma=4$",35 "scarce_land": "scarce land", "severe_chip_depreciation": r"$\delta_Q=0.45$", "reinstatement": "task 4 never automated",36 "moore_ends_2040": "Moore's law ends 2040", "moore_never_ends": "Moore's law never ends"}373839def load_all():40 out = {}41 for f in sorted(glob.glob(os.path.join(SC, "*_paths.npz"))):42 name = os.path.basename(f).replace("_paths.npz", "")43 out[name] = load_paths(f)44 return out454647def save(fig, name):48 for ext in ("pdf", "png"):49 fig.savefig(os.path.join(FIG, f"{name}.{ext}"), dpi=200, bbox_inches="tight")50 plt.close(fig)515253def yr_mask(P, y0=2026, y1=2100):54 return (P["years"] >= y0) & (P["years"] <= y1)555657def fig_baseline(P, P0=None):58 m = yr_mask(P); y = P["years"][m]59 fig, ax = plt.subplots(3, 3, figsize=(10.5, 8))60 gf = P["gf"]61 ax[0, 0].plot(y, (P["GDP"] / P["GDP"][0])[m], color=C["baseline"], label="baseline")62 if P0 is not None:63 ax[0, 0].plot(y, (P0["GDP"] / P0["GDP"][0])[yr_mask(P0)], color=C["no_ai"], ls="--", label="AI frozen")64 ax[0, 0].set_yscale("log"); ax[0, 0].set_title("GDP (2026 = 1)"); ax[0, 0].legend()65 ax[0, 1].plot(y, P["labor_share"][m], color="#c0392b", label="labor")66 ax[0, 1].plot(y, (P["R_Q"] * P["K_Q"][:-1] / P["GDP"])[m], color=C["baseline"], label="compute")67 ax[0, 1].plot(y, (P["R_N"] * P["K_N"][:-1] / P["GDP"])[m], color="#7f8c8d", label="structures")68 ax[0, 1].plot(y, ((P["RTp"] + P["RTHp"]) / P["GDP"])[m], color="#27ae60", label="land")69 ax[0, 1].plot(y, (P["PE"] * P["E"] / P["GDP"])[m], color="#e67e22", label="energy")70 ax[0, 1].set_title("Income shares of GDP"); ax[0, 1].legend(fontsize=7)71 ax[0, 2].plot(y, (P["wL"] * gf / (P["wL"][0]))[m], color="#c0392b", label="$w_L$ per worker")72 ax[0, 2].plot(y, (P["wH"] * gf / (P["wH"][0]))[m], color=C["baseline"], label="$w_H$ per worker")73 ax[0, 2].plot(y, gf[m], color="#7f8c8d", ls=":", label="trend $(1+g_Z)^t$")74 ax[0, 2].set_title("Real wages (2026 = 1)"); ax[0, 2].legend(fontsize=7)75 ax[1, 0].plot(y, 100 * P["r"][1:][m], color=C["baseline"])76 ax[1, 0].set_title("Real return on wealth $r_{t+1}$ (%)")77 ax[1, 1].plot(y, P["K_Q"][:-1][m], color=C["baseline"], label="$K^Q$ (compute units)")78 ax[1, 1].set_yscale("log"); ax[1, 1].set_title("Compute capacity (log)")79 ax2 = ax[1, 1].twinx(); ax2.plot(y, P["A"][m], color="#c0392b", ls="--"); ax2.set_yscale("log"); ax2.set_ylabel("price of compute $A$ (dashed)", color="#c0392b")80 ax[1, 2].plot(y, (P["EQ"] / P["E"])[m], color="#e67e22", label="share of energy to compute")81 ax[1, 2].plot(y, P["util"][m], color="#7f8c8d", ls="--", label="utilization")82 ax[1, 2].plot(y, (P["PE"] / P["PE"][0])[m], color=C["baseline"], ls=":", label="energy price (2026=1)")83 ax[1, 2].set_title("Energy"); ax[1, 2].legend(fontsize=7)84 Li = P["Li"]; Qi = P["Qi"]; al = P["alpha"]85 comp_share_task = (Qi / al) / (Li + Qi / al + 1e-300)86 for i, nm in enumerate(["routine", "cognitive-L", "cognitive-H", "expert"]):87 ax[2, 0].plot(y, comp_share_task[m, i], label=nm)88 ax[2, 0].set_title("Compute share of task input"); ax[2, 0].legend(fontsize=7)89 ax[2, 1].plot(y, (P["inv_goods"] / P["GDP"])[m], color=C["baseline"], label="total investment / GDP")90 ax[2, 1].plot(y, ((P["pQ"] * P["I_Q"] + P["adj_Q"]) / P["GDP"])[m], color="#c0392b", label="compute investment / GDP")91 ax[2, 1].set_title("Investment"); ax[2, 1].legend(fontsize=7)92 ax[2, 2].plot(y, (P["C_W"] / P["C_W"][0] / gf)[m], color="#c0392b", label="workers (detrended)")93 ax[2, 2].plot(y, (P["C_K"] / P["C_K"][0] / gf)[m], color=C["baseline"], label="capitalists (detrended)")94 ax[2, 2].set_yscale("log"); ax[2, 2].set_title("Consumption by class (2026 = 1)"); ax[2, 2].legend(fontsize=7)95 for a in ax.ravel():96 a.set_xlim(2026, 2100)97 fig.tight_layout()98 save(fig, "fig03_baseline")99100101def fig_valuation(P, P0=None):102 m = yr_mask(P); y = P["years"][m]103 v = valuation_ratios(P)104 fig, ax = plt.subplots(2, 3, figsize=(10.5, 5.6))105 for k, lab, col in [("N", "structures", "#7f8c8d"), ("Q", "compute", C["baseline"]), ("E", "energy capital", "#e67e22"), ("H", "housing structures", "#16a085")]:106 ax[0, 0].plot(y, v[f"V_over_GDP_{k}"][m], color=col, label=lab)107 ax[0, 0].plot(y, ((P["PT"] + P["PTH"]) / P["GDP"])[m], color="#27ae60", label="land (both)")108 ax[0, 0].set_title("Market value of claims / GDP"); ax[0, 0].legend(fontsize=7)109 ax[0, 1].plot(y, v["W_over_GDP"][m], color=C["baseline"], label="baseline")110 if P0 is not None:111 ax[0, 1].plot(y, valuation_ratios(P0)["W_over_GDP"][yr_mask(P0)], color=C["no_ai"], ls="--", label="AI frozen")112 ax[0, 1].set_title("Aggregate wealth / GDP"); ax[0, 1].legend(fontsize=7)113 ax[0, 2].plot(y, v["PE_Q"][m], color=C["baseline"], label="compute")114 ax[0, 2].plot(y, v["PE_N"][m], color="#7f8c8d", label="structures")115 ax[0, 2].set_title("Price-earnings ratios"); ax[0, 2].legend(fontsize=7)116 ax[1, 0].plot(y, v["land_PR"][m], color="#27ae60", label="production land")117 ax[1, 0].plot(y, v["house_PR"][m], color="#16a085", label="house (structure + land)")118 ax[1, 0].set_title("Price-rent ratios"); ax[1, 0].legend(fontsize=7)119 ax[1, 1].plot(y, v["q_over_p_N"][m], color="#7f8c8d", label="structures")120 ax[1, 1].plot(y, v["q_over_p_Q"][m], color=C["baseline"], label="compute")121 ax[1, 1].plot(y, v["q_over_p_H"][m], color="#16a085", label="housing")122 ax[1, 1].set_title("Tobin's $q$ (market / replacement cost)"); ax[1, 1].legend(fontsize=7)123 ax[1, 2].plot(y, v["house_land_share"][m], color="#27ae60", label="land share of house value")124 ax[1, 2].plot(y, v["wealth_comp"]["land"][m], color="#7f8c8d", ls="--", label="land share of wealth")125 ax[1, 2].plot(y, v["wealth_comp"]["Q"][m], color=C["baseline"], ls=":", label="compute share of wealth")126 ax[1, 2].set_title("Composition"); ax[1, 2].legend(fontsize=7)127 for a in ax.ravel():128 a.set_xlim(2026, 2100)129 fig.tight_layout()130 save(fig, "fig04_valuation")131132133def fig_scenarios(allP, names, fname, title):134 names = [n for n in names if n in allP]135 if len(names) < 2:136 return137 fig, ax = plt.subplots(2, 3, figsize=(10.5, 5.6))138 for n in names:139 P = allP[n]; m = yr_mask(P); y = P["years"][m]; v = valuation_ratios(P); gf = P["gf"]140 kw = dict(color=C.get(n, "k"), label=LAB.get(n, n), lw=1.3 if n == "baseline" else 1.0)141 ax[0, 0].plot(y, (P["GDP"] / P["GDP"][0] / gf)[m], **kw)142 ax[0, 1].plot(y, P["labor_share"][m], **kw)143 ax[0, 2].plot(y, (P["wL"] * gf / P["wL"][0])[m], **kw)144 ax[1, 0].plot(y, 100 * P["r"][1:][m], **kw)145 ax[1, 1].plot(y, v["W_over_GDP"][m], **kw)146 ax[1, 2].plot(y, v["land_PR"][m], **kw)147 ax[0, 0].set_title("GDP / trend (2026 = 1)"); ax[0, 0].set_yscale("log")148 ax[0, 1].set_title("Labor share")149 ax[0, 2].set_title("Wage of skill L (2026 = 1)"); ax[0, 2].set_yscale("log")150 ax[1, 0].set_title("Real return (%)")151 ax[1, 1].set_title("Wealth / GDP")152 ax[1, 2].set_title("Land price-rent ratio")153 ax[0, 0].legend(fontsize=7)154 for a in ax.ravel():155 a.set_xlim(2026, 2100)156 fig.suptitle(title, fontsize=10)157 fig.tight_layout()158 save(fig, fname)159160161def fig_decomposition(allP):162 if "baseline" not in allP or "no_ai" not in allP:163 return164 P1, P0 = allP["baseline"], allP["no_ai"]165 claims = [("land", "production land"), ("housing_land", "residential land"), ("N", "structures"), ("Q", "compute"), ("H", "housing structures")]166 rows = []167 for c, lab in claims:168 try:169 d = campbell_shiller_decomposition(P1, P0, claim=c)170 rows.append((lab, d["cash_flow"], d["discount_rate"], d["total"]))171 except Exception as e: # noqa: BLE001172 print("decomp failed", c, e)173 if not rows:174 return175 fig, ax = plt.subplots(figsize=(6.5, 3.2))176 x = np.arange(len(rows)); w = 0.35177 ax.bar(x - w / 2, [r[1] for r in rows], w, color="#27ae60", label="cash-flow channel")178 ax.bar(x + w / 2, [r[2] for r in rows], w, color="#c0392b", label="discount-rate channel")179 ax.plot(x, [r[3] for r in rows], "ko", label="total log change in value (2026)")180 ax.axhline(0, color="k", lw=0.5)181 ax.set_xticks(x); ax.set_xticklabels([r[0] for r in rows], fontsize=8)182 ax.set_ylabel("log(value, baseline / AI frozen)")183 ax.legend(fontsize=7)184 fig.tight_layout()185 save(fig, "fig07_decomposition")186 with open(os.path.join(ROOT, "results", "data", "decomposition_2026.json"), "w") as f:187 json.dump([dict(claim=r[0], cash_flow=r[1], discount_rate=r[2], total=r[3]) for r in rows], f, indent=1)188189190if __name__ == "__main__":191 allP = load_all()192 print("scenarios available:", list(allP))193 if "baseline" in allP:194 fig_baseline(allP["baseline"], allP.get("no_ai"))195 fig_valuation(allP["baseline"], allP.get("no_ai"))196 fig_scenarios(allP, ["baseline", "no_ai", "rapid_agi", "fast_moore_cheap_energy", "fast_moore_energy_constrained", "moore_never_ends"],197 "fig05_technology_scenarios", "Technology scenarios")198 fig_scenarios(allP, ["baseline", "complements_leontief_like", "complements_cobb_douglas", "substitutes", "scarce_land", "severe_chip_depreciation"],199 "fig06_structure_scenarios", "Substitutability and scarcity")200 fig_decomposition(allP)201 fig_scenarios(allP, ["baseline", "citizen_fund_20", "land_tax_50", "compute_tax_30", "capital_tax_20", "ai_monopoly", "foreign_ai_50"],202 "fig08_policy_scenarios", "Ownership, competition and policy")203 fig_scenarios(allP, ["baseline", "low_risk_aversion", "high_risk_aversion", "reinstatement", "moore_ends_2040"],204 "fig09_preference_scenarios", "Preferences, reinstatement and the end of Moore's law")205 print("done")206