1"""Tables and LaTeX number macros from solved scenarios.2Outputs: results/tables/tab02_calibration.tex, tab03_baseline.tex, tab04_scenarios.tex, tab05_decomposition.tex,3tab06_policy.tex, tab08_diagnostics.tex, numbers.tex ; results/data/scenario_summary.csv"""4import glob5import json6import os7import re8import sys910import numpy as np11import pandas as pd1213sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))14from agi_ge.analysis import campbell_shiller_decomposition, summary_by_year, valuation_ratios, welfare_cev # noqa: E40215from agi_ge.calibration import load_targets # noqa: E40216from agi_ge.solve import load_paths # noqa: E4021718ROOT = os.path.join(os.path.dirname(__file__), "..")19SC = os.path.join(ROOT, "results", "scenarios")20TAB = os.path.join(ROOT, "results", "tables")21os.makedirs(TAB, exist_ok=True)22LAB = {"baseline": "Baseline", "no_ai": "AI frozen at 2026 capability", "rapid_agi": "Rapid AGI (all tasks in 2032)",23 "fast_moore_cheap_energy": "Fast Moore ($g_{pQ}=0.34$), cheap energy", "fast_moore_energy_constrained": "Fast Moore, no efficiency gains",24 "moore_ends_2040": "Moore's law ends 2040 (floor 0.12)", "moore_floor_1e3": "Compute price floor $10^{-3}$", "moore_never_ends": "Moore's law never ends",25 "slow_alpha": "Slow capability ($g_\\alpha=0.05$)", "fast_alpha": "Fast capability ($g_\\alpha=0.20$)",26 "complements_leontief_like": "Strong complements ($\\sigma_N=\\sigma_T=0.4$)", "complements_cobb_douglas": "Cobb--Douglas ($\\sigma=1$)",27 "substitutes": "Substitutes ($\\sigma=1.5$)", "ai_monopoly": "AI monopoly (markup 1.5)", "citizen_fund_20": "Citizen fund (20\\% of equity)",28 "land_tax_50": "Land tax 50\\%", "compute_tax_30": "Compute tax 30\\%", "capital_tax_20": "Capital-income tax 20\\%",29 "foreign_ai_50": "50\\% foreign-owned compute", "low_risk_aversion": "$\\gamma=1$", "high_risk_aversion": "$\\gamma=4$",30 "scarce_land": "Scarce land (share 8\\%, $\\sigma_T=0.4$)", "severe_chip_depreciation": "Severe chip depreciation ($\\delta_Q=0.45$)",31 "reinstatement": "Expert task never automated"}323334_DIGITS = {"0": "Zero", "1": "One", "2": "Two", "3": "Three", "4": "Four", "5": "Five", "6": "Six", "7": "Seven", "8": "Eight", "9": "Nine"}353637def macro_name(s):38 """LaTeX-safe macro fragment: digits spelled out, non-letters removed."""39 s = "".join(_DIGITS.get(ch, ch) for ch in str(s))40 return re.sub(r"[^A-Za-z]", "", s)414243def fmt(x, d=2):44 if x is None or (isinstance(x, float) and not np.isfinite(x)):45 return "--"46 if abs(x) >= 1000:47 return f"{x:,.0f}".replace(",", "{,}")48 return f"{x:.{d}f}"495051def write_numbers(nums):52 with open(os.path.join(TAB, "numbers.tex"), "w") as f:53 f.write("% generated by scripts/50_tables.py — one macro per number used in the text\n")54 for k, v in nums.items():55 f.write(f"\\newcommand{{\\{k}}}{{{v}}}\n")565758def table_calibration(params, info):59 tg = load_targets()60 rows = []61 src = params.get("source", {})62 def add(name, sym, val, status, source):63 rows.append((name, sym, val, status, source))64 v = {k: t.value for k, t in tg.items()}65 st = {k: t.status for k, t in tg.items()}66 so = {k: t.source for k, t in tg.items()}67 add("Labor share of GDP, 2023", "", fmt(v["labor_share"], 3), st["labor_share"], "PWT via FRED")68 add("Nonresidential fixed assets / GDP (comparison)", "", fmt(v["KN_over_GDP"]), st["KN_over_GDP"], "BEA via FRED")69 add("Structures / GDP implied by income shares", "$K^N_0$", fmt(params["KN0"]), "calibrated", "residual of the income identity")70 add("Residential structures / GDP", "$K^H_0$", fmt(params["KH0"]), st["KH_over_GDP"], "BEA via FRED (derived)")71 add("Energy capital / GDP", "$K^E_0$", fmt(params["KE0"]), st["KE_over_GDP"], "assumption")72 add("Value of compute capital / GDP", "$p^Q_0K^Q_0$", fmt(v["VQ_over_GDP"], 3), st["VQ_over_GDP"], "IPE investment capitalized")73 add("Depreciation: structures, housing, compute, energy", "$\\delta_k$", f"{params['delta']['N']:.3f}, {params['delta']['H']:.3f}, {params['delta']['Q']:.2f}, {params['delta']['E']:.2f}", "data / assumed", "BEA via FRED; accounting lives")74 add("Adjustment costs", "$\\phi_k$", f"{params['phi']['N']:.0f}, {params['phi']['H']:.0f}, {params['phi']['Q']:.0f}, {params['phi']['E']:.0f}", "literature", "Cooper--Haltiwanger (2006) order")75 add("Real return in the frozen BGP", "$r^*$", fmt(v["r_star"], 3), st["r_star"], "between TIPS (1.96\\%) and returns on wealth")76 add("Trend growth per efficiency unit", "$g_Z$", fmt(v["g_Z"], 3), st["g_Z"], "real GDP growth 1990--2025 minus population")77 add("Curvature of utility", "$\\gamma$", fmt(v["gamma_u"], 1), "literature", "")78 add("Discount factor (implied)", "$\\beta$", fmt(params["beta"], 4), "calibrated", "$\\beta=(1+g_Z)^\\gamma/(1+r^*)$")79 add("Housing expenditure share (implied)", "$\\theta_H$", fmt(params["theta_H"], 3), "calibrated", "housing value added 12\\% of GDP")80 add("Land share of housing (implied)", "$\\nu_H$", fmt(params["nu_H"], 3), "calibrated", "housing Euler")81 add("Resource share in energy (implied)", "$\\eta$", fmt(params["eta"], 3), "calibrated", "energy Euler")82 add("Energy sector output / GDP", "", fmt(v["energy_share"], 3), st["energy_share"], "assumption (electricity 1.7\\% + other)")83 add("Data-center share of electricity, 2023", "", fmt(v["datacenter_energy_share"], 3), st["datacenter_energy_share"], "Shehabi et al. (2024)")84 add("Production land rent / GDP", "", fmt(v["land_rent_share"], 3), st["land_rent_share"], "assumption")85 add("Elasticities: tasks; tasks--structures; top nest", "$\\sigma_B,\\sigma_N,\\sigma_T$", f"{v['sigma_B']}, {v['sigma_N']}, {v['sigma_T']}", "literature", "Oberfield--Raval; Chirinko")86 add("Workers of skill $L$; skill premium", "$H_L$; $w_H/w_L$", f"{v['share_L_workers']}; {v['skill_premium']}", "assumed", "")87 add("CES task weights (implied)", "$\\nu_i$", ", ".join(f"{x:.3f}" for x in params["tasks"]["nu"]), "calibrated", "task wage shares")88 add("Compute per worker, task 1, 2026 (implied)", "$\\alpha_{1,0}$", fmt(info["alpha10"], 1), "calibrated", "compute Euler and task-1 tie")89 add("Implied compute share of task-1 input, 2026", "", src.get("compute_in_task1_implied", ""), "implied", "")90 add("Terminal compute per worker by task", "$\\alpha_i^\\infty$", ", ".join(f"{x:g}" for x in params["tasks"]["alpha_inf"]), "scenario", "normalization $\\alpha_1^\\infty=1$")91 add("2026 compute per worker by task (multiples of terminal)", "$\\alpha_{i,0}/\\alpha_i^\\infty$", ", ".join(f"{x:.0f}" for x in params["alpha0_mult"]), "calibrated / scenario", "")92 add("Capability progress; compute price decline; efficiency gains", "$g_\\alpha,g_{pQ},g_\\varepsilon$", f"{params['g_alpha']}, {params['g_pQ']}, {params['g_eps']}", "scenario", "Epoch: 0.34 for GPU FLOP/\\$")93 add("Floors (fraction of 2026): compute price; energy per unit", "$\\underline p/p_0,\\underline\\varepsilon/\\varepsilon_0$", f"{params['pQ_floor_ratio']:g}, {params['eps_floor_ratio']:g}", "scenario", "")94 with open(os.path.join(TAB, "tab02_calibration.tex"), "w") as f:95 f.write("\\begin{tabular}{@{}p{6.3cm}lp{3.2cm}lp{3.8cm}@{}}\n\\toprule\nObject & Symbol & Value & Status & Source\\\\\n\\midrule\n")96 for r in rows:97 f.write(" & ".join(str(x) for x in r) + "\\\\\n")98 f.write("\\bottomrule\n\\end{tabular}\n")99100101def table_baseline(P, years=(2026, 2030, 2040, 2050, 2075, 2100)):102 S = summary_by_year(P, years)103 rows = [("GDP (2026 = 1)", "GDP", 2), ("GDP growth (\\%)", "GDP_growth", 1), ("Labor share", "labor_share", 3),104 ("Compute income share", "compute_income_share", 3), ("Structures share", "structures_share", 3),105 ("Land share (both)", "land_share", 3), ("Energy share", "energy_share", 3),106 ("Wage $L$ per worker (2026 = 1)", "wL", 3), ("Wage $H$ per worker (2026 = 1)", "wH", 3),107 ("Real return $r$ (\\%)", "r", 1), ("Compute capacity (2026 = 1)", "KQ", 1), ("Value of compute / GDP", "VQ_over_GDP", 3),108 ("Wealth / GDP", "W_over_GDP", 2), ("Land price--rent ratio", "land_PR", 1), ("House price--rent ratio", "house_PR", 1),109 ("P/E compute", "PE_Q", 1), ("P/E structures", "PE_N", 1), ("Tobin's $q$ compute", "q_Q", 2),110 ("Share of energy used by compute", "energy_to_compute", 3), ("Investment / GDP", "inv_share", 3),111 ("Workers' consumption / GDP", "CW_over_GDP", 3), ("Tasks using compute (of 4)", "tasks_automated", 0)]112 with open(os.path.join(TAB, "tab03_baseline.tex"), "w") as f:113 f.write("\\begin{tabular}{@{}l" + "r" * len(years) + "@{}}\n\\toprule\n & " + " & ".join(str(y) for y in years) + "\\\\\n\\midrule\n")114 for lab, k, d in rows:115 vals = []116 for y in years:117 x = S[y][k]118 if k in ("GDP_growth", "r"):119 x = 100 * x120 vals.append(fmt(x, d))121 f.write(lab + " & " + " & ".join(vals) + "\\\\\n")122 f.write("\\bottomrule\n\\end{tabular}\n")123 return S124125126def table_scenarios(allP, names, fname, years=(2050, 2100)):127 cols = [("labor_share", "Labor share", 3), ("wL", "$w_L$ (2026=1)", 3), ("GDP", "GDP (2026=1)", 1), ("r", "$r$ (\\%)", 1),128 ("W_over_GDP", "$W$/GDP", 1), ("land_PR", "Land P/R", 1), ("VQ_over_GDP", "$V^Q$/GDP", 3), ("CW_over_GDP", "$C_W$/GDP", 3)]129 recs = []130 with open(os.path.join(TAB, fname), "w") as f:131 f.write("\\begin{tabular}{@{}l" + "r" * (len(cols) * len(years)) + "@{}}\n\\toprule\n")132 f.write(" & " + " & ".join(f"\\multicolumn{{{len(cols)}}}{{c}}{{{y}}}" for y in years) + "\\\\\n")133 f.write("Scenario & " + " & ".join(c[1] for c in cols for _ in years) if False else "Scenario & " + " & ".join(c[1] for _ in years for c in cols) + "\\\\\n\\midrule\n")134 for n in names:135 if n not in allP:136 continue137 S = summary_by_year(allP[n], years)138 vals = []139 for y in years:140 for k, _, d in cols:141 x = S[y][k]142 x = 100 * x if k == "r" else x143 vals.append(fmt(x, d))144 recs.append(dict(scenario=n, year=y, stat=k, value=S[y][k]))145 f.write(LAB.get(n, n) + " & " + " & ".join(vals) + "\\\\\n")146 f.write("\\bottomrule\n\\end{tabular}\n")147 return recs148149150def table_decomposition(allP):151 if "baseline" not in allP or "no_ai" not in allP:152 return {}153 P1, P0 = allP["baseline"], allP["no_ai"]154 out = {}155 with open(os.path.join(TAB, "tab05_decomposition.tex"), "w") as f:156 f.write("\\begin{tabular}{@{}lrrr@{}}\n\\toprule\nClaim & Cash-flow channel & Discount-rate channel & Total\\\\\n\\midrule\n")157 for c, lab in [("land", "Production land"), ("housing_land", "Residential land"), ("N", "Structures"), ("Q", "Compute capital"), ("H", "Housing structures"), ("house", "Houses (structure + land)")]:158 try:159 d = campbell_shiller_decomposition(P1, P0, claim=c)160 f.write(f"{lab} & {fmt(100*d['cash_flow'],1)} & {fmt(100*d['discount_rate'],1)} & {fmt(100*d['total'],1)}\\\\\n")161 out[c] = d162 except Exception as e: # noqa: BLE001163 print("decomp failed", c, e)164 f.write("\\bottomrule\n\\end{tabular}\n")165 return out166167168def table_policy(allP, params):169 names = ["baseline", "citizen_fund_20", "land_tax_50", "compute_tax_30", "capital_tax_20", "ai_monopoly", "foreign_ai_50"]170 if "baseline" not in allP:171 return {}172 P0 = allP["baseline"]173 beta, gamma = params["beta"], params["gamma_u"]174 out = {}175 with open(os.path.join(TAB, "tab06_policy.tex"), "w") as f:176 f.write("\\begin{tabular}{@{}lrrrrrrr@{}}\n\\toprule\nScenario & GDP 2050 & GDP 2100 & $w_L$ 2050 & $C_W$/GDP 2050 & $C_W$/GDP 2100 & CEV workers 2026--65 (\\%) & CEV capitalists 2026--65 (\\%)\\\\\n\\midrule\n")177 for n in names:178 if n not in allP:179 continue180 P = allP[n]181 S = summary_by_year(P, (2050, 2100))182 cevW = welfare_cev(P["X_W"], P["P_idx"], P0["X_W"], P0["P_idx"], beta, gamma)183 cevK = welfare_cev(P["X_K"], P["P_idx"], P0["X_K"], P0["P_idx"], beta, gamma)184 out[n] = dict(cevW=cevW, cevK=cevK, S=S)185 f.write(f"{LAB.get(n, n)} & {fmt(S[2050]['GDP'],2)} & {fmt(S[2100]['GDP'],1)} & {fmt(S[2050]['wL'],3)} & {fmt(S[2050]['CW_over_GDP'],3)} & {fmt(S[2100]['CW_over_GDP'],4)} & {fmt(100*cevW,1)} & {fmt(100*cevK,1)}\\\\\n")186 f.write("\\bottomrule\n\\end{tabular}\n")187 return out188189190def table_diagnostics(names):191 rows = []192 for n in names:193 mf = os.path.join(SC, f"{n}_meta.json")194 pf = os.path.join(SC, f"{n}_paths.npz")195 if not (os.path.exists(mf) and os.path.exists(pf)):196 continue197 meta = json.load(open(mf))198 P = load_paths(pf)199 res = np.abs(P["residuals"]).max()200 inc = np.abs(P["income_residual"] / P["GDP"]).max()201 hc = meta.get("horizon_check") or {}202 rows.append((LAB.get(n, n), meta["converged"], res, inc, hc.get("max_dev_r", np.nan), max(hc.get("max_log_dev_K", {np.nan: np.nan}).values()) if hc else np.nan, meta["seconds"]))203 with open(os.path.join(TAB, "tab08_diagnostics.tex"), "w") as f:204 f.write("\\begin{tabular}{@{}lcrrrrr@{}}\n\\toprule\nScenario & Converged & max $|$residual$|$ & max income-identity gap / GDP & horizon check: $\\Delta r$ & $\\Delta\\log K$ & seconds\\\\\n\\midrule\n")205 for r in rows:206 f.write(f"{r[0]} & {'yes' if r[1] else 'no'} & {r[2]:.1e} & {r[3]:.1e} & {r[4]:.1e} & {r[5]:.1e} & {r[6]:.0f}\\\\\n")207 f.write("\\bottomrule\n\\end{tabular}\n")208 return rows209210211if __name__ == "__main__":212 allP = {}213 for fpath in sorted(glob.glob(os.path.join(SC, "*_paths.npz"))):214 allP[os.path.basename(fpath).replace("_paths.npz", "")] = load_paths(fpath)215 print("scenarios:", list(allP))216 nums = {}217 if "baseline" in allP:218 params = json.load(open(os.path.join(SC, "baseline_params.json")))219 info = json.load(open(os.path.join(SC, "baseline_meta.json")))["calibration_info"]220 table_calibration(params, info)221 S = table_baseline(allP["baseline"])222 vr = valuation_ratios(allP["baseline"])223 P = allP["baseline"]224 nums.update({225 "WoverGDPbase": fmt(vr["W_over_GDP"][0], 1), "LandOverGDPbase": fmt((P["PT"][0] + P["PTH"][0]) / P["GDP"][0], 1),226 "PEstructuresbase": fmt(vr["PE_N"][0], 1), "PEcomputebase": fmt(vr["PE_Q"][0], 1),227 "rBase": fmt(100 * P["r"][1], 1), "AlphaOneBase": fmt(info["alpha10"], 1),228 })229 for y in (2030, 2040, 2050, 2075, 2100):230 nums[f"LS{macro_name(str(y))}"] = fmt(S[y]["labor_share"], 3)231 nums[f"wL{macro_name(str(y))}"] = fmt(S[y]["wL"], 2)232 nums[f"wH{macro_name(str(y))}"] = fmt(S[y]["wH"], 2)233 nums[f"GDP{macro_name(str(y))}"] = fmt(S[y]["GDP"], 1)234 nums[f"WY{macro_name(str(y))}"] = fmt(S[y]["W_over_GDP"], 1)235 nums[f"rr{macro_name(str(y))}"] = fmt(100 * S[y]["r"], 1)236 nums[f"CompShare{macro_name(str(y))}"] = fmt(S[y]["compute_income_share"], 3)237 nums[f"LandShare{macro_name(str(y))}"] = fmt(S[y]["land_share"], 3)238 nums[f"LandPR{macro_name(str(y))}"] = fmt(S[y]["land_PR"], 1)239 nums[f"CWshare{macro_name(str(y))}"] = fmt(S[y]["CW_over_GDP"], 2)240 nums[f"HousePR{macro_name(str(y))}"] = fmt(S[y]["house_PR"], 1)241 # years of automation (first year each task uses compute for > 50% of its input)242 Li, Qi, al = P["Li"], P["Qi"], P["alpha"]243 cs = (Qi / al) / (Li + Qi / al + 1e-300)244 for i, nm in enumerate(["One", "Two", "Three", "Four"]):245 idx = np.where(cs[:, i] > 0.5)[0]246 nums[f"AutoYear{nm}"] = str(int(P["years"][idx[0]])) if len(idx) else "never"247 pk = min(range(len(P["labor_share"])), key=lambda t: P["labor_share"][t] if P["years"][t] <= 2100 else 9)248 gmax = int(np.argmax(np.diff(np.log(P["GDP"]))[:100]))249 nums["GDPgrowthPeak"] = fmt(100 * np.diff(np.log(P["GDP"]))[gmax], 1)250 nums["GDPgrowthPeakYear"] = str(int(P["years"][gmax]))251 nums["rPeak"] = fmt(100 * np.max(P["r"][1:101]), 1)252 nums["rPeakYear"] = str(int(P["years"][int(np.argmax(P["r"][1:101]))]))253 nums["WYmin"] = fmt(np.min(vr["W_over_GDP"][:100]), 1)254 nums["WYminYear"] = str(int(P["years"][int(np.argmin(vr["W_over_GDP"][:100]))]))255 nums["LandPRmin"] = fmt(np.min(vr["land_PR"][:100]), 1)256 recs = table_scenarios(allP, ["baseline", "no_ai", "rapid_agi", "slow_alpha", "fast_alpha", "fast_moore_cheap_energy", "fast_moore_energy_constrained",257 "moore_ends_2040", "moore_floor_1e3", "moore_never_ends", "complements_leontief_like", "complements_cobb_douglas",258 "substitutes", "scarce_land", "severe_chip_depreciation", "reinstatement", "low_risk_aversion", "high_risk_aversion"],259 "tab04_scenarios.tex")260 pd.DataFrame(recs).to_csv(os.path.join(ROOT, "results", "data", "scenario_summary.csv"), index=False)261 for n, key in [("moore_ends_2040", "MooreEnds"), ("moore_floor_1e3", "MooreFloor"), ("alpha_inf_x5", "AlphaInfFive"), ("rapid_agi", "RapidAGI"), ("no_capability", "NoCapability")]:262 if n in allP:263 S = summary_by_year(allP[n], (2050, 2100))264 for y in (2050, 2100):265 nums[f"wL{key}{macro_name(str(y))}"] = fmt(S[y]["wL"], 3)266 nums[f"LS{key}{macro_name(str(y))}"] = fmt(S[y]["labor_share"], 3)267 nums[f"GDP{key}{macro_name(str(y))}"] = fmt(S[y]["GDP"], 1)268 dec = table_decomposition(allP)269 for c, d in dec.items():270 nums[f"Dec{macro_name(c)}CF"] = fmt(100 * d["cash_flow"], 0)271 nums[f"Dec{macro_name(c)}DR"] = fmt(100 * d["discount_rate"], 0)272 nums[f"Dec{macro_name(c)}Tot"] = fmt(100 * d["total"], 0)273 if "baseline" in allP:274 pol = table_policy(allP, params)275 for n, d in pol.items():276 nums[f"CEVW{macro_name(n)}"] = fmt(100 * d["cevW"], 1)277 nums[f"CEVK{macro_name(n)}"] = fmt(100 * d["cevK"], 1)278 rows = table_diagnostics(list(LAB))279 nums["NumScenarios"] = str(len(rows))280 nums["MaxResidualAll"] = f"{max(r[2] for r in rows):.0e}" if rows else "--"281 nums["MaxIncomeGapAll"] = f"{max(r[3] for r in rows):.0e}" if rows else "--"282 # keep previously generated HA numbers if present283 ha = os.path.join(ROOT, "results", "data", "ha_numbers.json")284 if os.path.exists(ha):285 nums.update(json.load(open(ha)))286 write_numbers(nums)287 print("numbers:", len(nums), "tables written")288