1"""Tables and generated numbers for the paper.23Writes paper/tables/numbers.tex (macros \numXXX used in the text), model_a_baseline.tex, model_b_scenarios.tex,4model_b_decomposition.tex, model_c_factorial.tex, model_c_stress.tex, llm_lab.tex, notation.tex, novelty.tex,5compute_cost.tex. Every number comes from results/ files.6"""7from __future__ import annotations89import json10import platform11import subprocess12import sys13from pathlib import Path1415import numpy as np16import pandas as pd1718ROOT = Path(__file__).resolve().parents[1]19RA, RB, RC, RL = [ROOT / 'results' / d for d in ('model_a', 'model_b', 'model_c', 'llm_lab')]20TAB = ROOT / 'paper' / 'tables'21TAB.mkdir(parents=True, exist_ok=True)22nums = {}232425def macro(name, val, fmt='{:.3f}'):26 nums[name] = fmt.format(val) if not isinstance(val, str) else val272829def esc(s):30 return str(s).replace('_', '\\_').replace('%', '\\%').replace('&', '\\&')313233# ------------------------------------------------------------------ Model A34sumA = json.load(open(RA / 'summary.json'))35ss, pf, sav = sumA['steady_state'], sumA['portfolio_map'], sumA['savings_channel']36macro('kappaSav', sav['kappa0'], '{:.4f}'); macro('kappaFullSav', sav['kappa_full'], '{:.4f}'); macro('mpc', sav['MPC'], '{:.3f}')37macro('kappaPort', pf['kappa0'], '{:.1f}'); macro('alphaPort', pf['alpha'], '{:.2f}')38g = pd.read_csv(RA / 'game_moments.csv')39base = g[(g.M == 1) & (np.isclose(g.sig_A, 0.04))]40macro('pvZero', 100 * base[base.D == 0].PV_goods.iloc[0] / ss['C'], '{:.1f}')41macro('pvOne', 100 * base[np.isclose(base.D, 1)].PV_goods.iloc[0] / ss['C'], '{:.1f}')42macro('wageSdOneOne', 100 * np.sqrt(0.33 ** 2 * base[np.isclose(base.D, 1)].V_noise.iloc[0]), '{:.1f}')43g8 = g[(g.M == 8) & (np.isclose(g.sig_A, 0.04)) & np.isclose(g.D, 1)]44macro('wageSdOneEight', 100 * np.sqrt(0.33 ** 2 * g8.V_noise.iloc[0]), '{:.1f}')45macro('maxWedge', g.wedge.abs().max(), '{:.1e}')46dD = pd.read_csv(RA / 'delegation_share.csv')47macro('Dstar', dD[(np.isclose(dD.sig_A, 0.04, atol=0.003)) & (dD.pQ == 0.6) & (dD.qbar == 0.002)].D.iloc[0], '{:.2f}')48wb = pd.read_csv(RA / 'wedge_hand_to_mouth.csv')49macro('taxFeeMone', wb[(wb.M == 1) & (wb.htm == 0.5) & np.isclose(wb.D, 1.0, atol=0.03)].tax_over_fee.iloc[0], '{:.1f}')50macro('taxFeeMeight', wb[(wb.M == 8) & (wb.htm == 0.5) & np.isclose(wb.D, 1.0, atol=0.03)].tax_over_fee.iloc[0], '{:.1f}')51cm = pd.read_csv(RA / 'compute_market.csv')52sr = cm[(cm.regime.str.startswith('short')) & (cm.KQ == 0.02)].sort_values('qbar')53imax = int(sr.share_L.values.argmax())54macro('shareLqzero', sr.share_L.iloc[0], '{:.3f}'); macro('shareLqmax', sr.share_L.iloc[imax], '{:.3f}'); macro('shareLgainpp', 100 * (sr.share_L.iloc[imax] - sr.share_L.iloc[0]), '{:.2f}')55macro('qbarmax', sr.qbar.iloc[imax], '{:.3f}'); macro('Dqmax', sr.D.iloc[imax], '{:.2f}'); macro('infshare', 100 * sr.qbar.iloc[imax] * sr.D.iloc[imax] / 0.02, '{:.0f}')56macro('iotaqzero', sr.iota.iloc[0], '{:.3f}'); macro('iotaqmax', sr.iota.iloc[imax], '{:.3f}'); macro('pQqzero', sr.pQ.iloc[0], '{:.2f}'); macro('pQqmax', sr.pQ.iloc[imax], '{:.2f}')57with open(TAB / 'model_a_baseline.tex', 'w') as f:58 f.write("\\begin{table}[htbp]\\centering\\small\n\\caption{Model A: baseline steady state and game parameters (portfolio mapping)}\\label{tab:a_baseline}\n")59 f.write("\\begin{tabular}{lS[table-format=2.3]lS[table-format=2.3]}\\toprule\nSteady state & {value} & Game & {value}\\\\\\midrule\n")60 rows = [('Output $Y$', ss['Y'], '$\\kappa_0=1/(\\gamma\\sigma_R^2)$', pf['kappa0']), ('Capital $K$', ss['K'], '$\\alpha_s=-(1-\\alpha_K)R^K\\kappa_0$', pf['alpha']),61 ('Consumption $C$', ss['C'], '$|U_{xx}|$ (goods)', pf['Uxx_goods']), ('Wage $w$', ss['w'], 'Savings-mapping $\\kappa_0$', sav['kappa0']),62 ('Gross return $R$', ss['R'], 'Savings-mapping $\\kappa_{full}$', sav['kappa_full']), ('Labour share', ss['share_L'], 'MPC (windfall)', sav['MPC'])]63 for a, b, c, d in rows:64 f.write(f"{a} & {b:.3f} & {c} & {d:.4f}\\\\\n")65 f.write("\\bottomrule\\end{tabular}\n\\begin{tablenotes}\\footnotesize\\item Zero-AI steady state ($K_Q=0$). Source: \\texttt{results/model\\_a/summary.json}.\\end{tablenotes}\\end{table}\n")6667# ------------------------------------------------------------------ Model B68scen = {f.stem: json.load(open(f)) for f in sorted(RB.glob('*.json'))}69order = [s for s in ['no_ai', 'production_only', 'delegation_only', 'full_broad', 'full_concentrated', 'scarce_broad', 'scarce_concentrated',70 'full_broad_tight_credit', 'full_broad_cheap_fee', 'full_broad_no_mistakes'] if s in scen]71labels = {'no_ai': 'No AI', 'production_only': 'AI as production input', 'delegation_only': 'Delegation only', 'full_broad': 'Both, broad ownership',72 'full_concentrated': 'Both, concentrated (top 10\\%)', 'scarce_broad': 'Scarce compute, broad', 'scarce_concentrated': 'Scarce compute, concentrated',73 'full_broad_tight_credit': 'Both, broad, no borrowing', 'full_broad_cheap_fee': 'Both, broad, cheap fee', 'full_broad_no_mistakes': 'Both, broad, no human mistakes'}74if scen:75 with open(TAB / 'model_b_scenarios.tex', 'w') as f:76 f.write("\\begin{table}[htbp]\\centering\\footnotesize\n\\caption{Model B: stationary equilibria across scenarios}\\label{tab:b_scenarios}\n")77 f.write("\\begin{tabular}{lrrrrrrrrr}\\toprule\nScenario & $r$ (\\%) & $w$ & $Y$ & $D$ & $\\iota$ & labour share & Gini & top 10\\% & top 1\\%\\\\\\midrule\n")78 for s in order:79 r = scen[s]80 f.write(f"{labels[s]} & {100*r['r']:.2f} & {r['w']:.3f} & {r['Y']:.3f} & {r['D']:.2f} & {r['iota']:.3f} & {r['shares']['labor']:.3f} & {r['gini']:.3f} & {r['top10']:.3f} & {r['top1']:.3f}\\\\\n")81 f.write("\\bottomrule\\end{tabular}\n\\begin{tablenotes}\\footnotesize\\item $D$: delegation share; $\\iota$: automated task share. Concentrated ownership adds a measure-0.10 owner class holding the capitalised scarcity rents. Source: \\texttt{scripts/20\\_model\\_b.py}.\\end{tablenotes}\\end{table}\n")82 # decomposition table: differences relative to no_ai83 with open(TAB / 'model_b_decomposition.tex', 'w') as f:84 f.write("\\begin{table}[htbp]\\centering\\footnotesize\n\\caption{Model B: channel decomposition (changes relative to the no-AI economy)}\\label{tab:b_decomp}\n")85 f.write("\\begin{tabular}{lrrrrr}\\toprule\nChannel (scenario) & $\\Delta Y$ (\\%) & $\\Delta$ labour share & $\\Delta$ Gini & $\\Delta$ top 10\\% & $\\Delta r$ (pp)\\\\\\midrule\n")86 b0 = scen.get('no_ai')87 if b0:88 for s in order[1:]:89 r = scen[s]90 f.write(f"{labels[s]} & {100*(r['Y']/b0['Y']-1):.1f} & {r['shares']['labor']-b0['shares']['labor']:+.3f} & {r['gini']-b0['gini']:+.3f} & {r['top10']-b0['top10']:+.3f} & {100*(r['r']-b0['r']):+.2f}\\\\\n")91 f.write("\\bottomrule\\end{tabular}\\end{table}\n")92 if 'no_ai' in scen:93 for s in ['delegation_only', 'full_broad', 'full_concentrated', 'scarce_concentrated']:94 if s in scen:95 key = s.replace('_', '')96 macro(f'gini{key}', scen[s]['gini'], '{:.3f}'); macro(f'topten{key}', scen[s]['top10'], '{:.3f}'); macro(f'D{key}', scen[s]['D'], '{:.2f}')97 macro(f'dY{key}', 100 * (scen[s]['Y'] / scen['no_ai']['Y'] - 1), '{:.0f}')98 macro('gininoai', scen['no_ai']['gini'], '{:.3f}'); macro('rnoai', 100 * scen['no_ai']['r'], '{:.1f}'); macro('toptennoai', scen['no_ai']['top10'], '{:.3f}')99100# ------------------------------------------------------------------ Model C101if (RC / 'summary.csv').exists():102 d = pd.read_csv(RC / 'summary.csv')103 with open(TAB / 'model_c_factorial.tex', 'w') as f:104 f.write("\\begin{table}[htbp]\\centering\\footnotesize\n\\caption{Model C (Bayesian control): factorial results, no leverage, means with bootstrap 95\\% intervals}\\label{tab:c_factorial}\n")105 f.write("\\begin{tabular}{llrrrrr}\\toprule\nDesign & $(D,M)$ & corr.\\ (all) & corr.\\ (delegators) & volatility (\\%) & $R^2$ common & trades/period\\\\\\midrule\n")106 for design in ['walrasian', 'cda', 'bilateral']:107 for (D, M) in [(0, 1), (0.5, 1), (0.5, 4), (1, 1), (1, 4), (1, 8)]:108 r = d[(d.design == design) & (d.D == D) & (d.M == M) & (d.leverage_cap == 0)]109 if len(r) == 0:110 continue111 r = r.iloc[0]112 cd = f"{r.of_corr_deleg:.2f}" if not np.isnan(r.of_corr_deleg) else '--'113 f.write(f"{design} & ({D},{M}) & {r.of_corr:.2f} [{r.of_corr_lo:.2f},{r.of_corr_hi:.2f}] & {cd} & {100*r.vol:.2f} & {r.r2_common:.2f} & {r.n_trades:.1f}\\\\\n")114 f.write("\\bottomrule\\end{tabular}\n\\begin{tablenotes}\\footnotesize\\item Order-flow correlation is computed after residualising each agent's flow on public information. Source: \\texttt{scripts/30\\_model\\_c.py}.\\end{tablenotes}\\end{table}\n")115 for design in ['walrasian', 'cda']:116 for (D, M), tag in [((0, 1), 'zero'), ((1, 1), 'oneone'), ((1, 4), 'onefour'), ((1, 8), 'oneeight')]:117 r = d[(d.design == design) & (d.D == D) & (d.M == M) & (d.leverage_cap == 0)]118 if len(r):119 macro(f'ofc{design}{tag}', r.iloc[0].of_corr, '{:.2f}'); macro(f'vol{design}{tag}', 100 * r.iloc[0].vol, '{:.2f}'); macro(f'trades{design}{tag}', r.iloc[0].n_trades, '{:.1f}')120 lev = d[(d.design == 'cda') & (d.leverage_cap == 0.5)]121 macro('defaultsLev', lev.defaults.mean(), '{:.1f}'); macro('defaultsNoLev', d[(d.design == 'cda') & (d.leverage_cap == 0)].defaults.mean(), '{:.1f}')122if (RC / 'stress_summary.csv').exists():123 s = pd.read_csv(RC / 'stress_summary.csv')124 s = s[~s.control.astype(bool)]125 with open(TAB / 'model_c_stress.tex', 'w') as f:126 f.write("\\begin{table}[htbp]\\centering\\footnotesize\n\\caption{Model C: stress test -- a $-10$ s.d.\\ common model error, all agents delegating, thin liquidity ladder}\\label{tab:c_stress}\n")127 f.write("\\begin{tabular}{llllrrrr}\\toprule\nDesign & $M$ & persist. & margin / preload & cum.\\ return (\\%) & trough (\\%) & forced sales & leverage\\\\\\midrule\n")128 for _, r in s.sort_values(['design', 'M', 'persist', 'margin', 'preload']).iterrows():129 f.write(f"{r.design} & {int(r.M)} & {int(r.persist)} & {r.margin} / {r.preload} & {100*r.shock_return:.1f} [{100*r.shock_return_lo:.1f},{100*r.shock_return_hi:.1f}] & {100*r.shock_trough:.1f} & {r.shock_forced:.2f} & {r.leverage:.2f}\\\\\n")130 f.write("\\bottomrule\\end{tabular}\n\\begin{tablenotes}\\footnotesize\\item Cumulative return over the shock window (persistence + 2 periods), bootstrap 95\\% intervals over 20 seeds; trough = minimum price within the window relative to the pre-shock price; preload = share of margin capacity borrowed before the shock. Source: \\texttt{scripts/30\\_model\\_c.py}.\\end{tablenotes}\\end{table}\n")131 def _g(design, M, persist, margin, pre):132 return s[(s.design == design) & (s.M == M) & (s.persist == persist) & (s.margin == margin) & (s.preload == pre)].iloc[0]133 for (design, M, persist, margin, pre), tag in [(('walrasian', 1, 1, 0.5, 0.0), 'WunlevOne'), (('walrasian', 1, 1, 0.2, 0.95), 'WlevTwenty'), (('walrasian', 1, 1, 0.5, 0.95), 'WlevFifty'),134 (('walrasian', 4, 1, 0.2, 0.95), 'WlevTwentyMfour'), (('cda', 1, 1, 0.5, 0.0), 'CunlevOne'), (('cda', 1, 1, 0.2, 0.95), 'ClevTwenty'),135 (('walrasian', 1, 3, 0.2, 0.95), 'WlevTwentyPersist')]:136 r = _g(design, M, persist, margin, pre)137 macro('stressRet' + tag, 100 * r.shock_return, '{:.1f}'); macro('stressTrough' + tag, 100 * r.shock_trough, '{:.1f}'); macro('stressForced' + tag, r.shock_forced, '{:.1f}')138 macro('stressAmp', _g('walrasian', 1, 1, 0.2, 0.95).shock_return / _g('walrasian', 1, 1, 0.5, 0.0).shock_return, '{:.1f}')139 macro('stressAmpFifty', _g('walrasian', 1, 1, 0.5, 0.95).shock_return / _g('walrasian', 1, 1, 0.5, 0.0).shock_return, '{:.1f}')140141# ------------------------------------------------------------------ LLM lab142if (RL / 'cells.csv').exists():143 c = pd.read_csv(RL / 'cells.csv')144 g = c.groupby(['policy', 'D', 'M', 'temperature']).agg(of=('of_corr', 'mean'), ofs=('of_corr', 'std'), raw=('raw_of_corr', 'mean'), vol=('vol', 'mean'),145 trades=('n_trades', 'mean'), inv=('invalid', 'mean'), cl=('clipped', 'mean'), rt=('runtime_s', 'mean'), n=('seed', 'count')).reset_index()146 names = {'bayesian': 'Bayesian control', 'surrogate': 'surrogate (rule, LLM interface)', 'llm_shared': 'one LLM (Qwen2.5-7B)', 'llm_diverse': 'three LLMs (Qwen/Mistral/Llama)',147 'llm_shared_varied_prompts': 'one LLM, five prompt variants', 'llm_shared_memory': 'one LLM, memory'}148 with open(TAB / 'llm_lab.tex', 'w') as f:149 f.write("\\begin{table}[htbp]\\centering\\footnotesize\n\\caption{LLM laboratory (CDA): policies $\\times$ signal structure}\\label{tab:llm}\n")150 f.write("\\begin{tabular}{llrrrrrrr}\\toprule\nPolicy & $(D,M)$, temp. & corr.\\ $|$ public & raw corr. & vol.\\ (\\%) & trades & invalid & clipped & s/run\\\\\\midrule\n")151 for _, r in g.iterrows():152 f.write(f"{names.get(r.policy, r.policy)} & ({r.D:.0f},{int(r.M)}), {r.temperature} & {r.of:.2f} ({r.ofs:.2f}) & {r.raw:.2f} & {100*r.vol:.2f} & {r.trades:.1f} & {r.inv:.0f} & {r.cl:.0f} & {r.rt:.0f}\\\\\n")153 f.write("\\bottomrule\\end{tabular}\n\\begin{tablenotes}\\footnotesize\\item Means over seeds (s.d.\\ in parentheses). Invalid: LLM replies rejected by the schema after one repair attempt (treated as hold); clipped: actions reduced by cash/holdings/margin limits. Source: \\texttt{scripts/35\\_llm\\_lab.py}; audit logs in \\texttt{results/llm\\_audit}.\\end{tablenotes}\\end{table}\n")154 st = json.load(open(RL / 'audit_stats.json')) if (RL / 'audit_stats.json').exists() else {}155 macro('llmCalls', sum(v['calls'] for v in st.values()), '{:d}'); macro('llmTokens', sum(v['tokens'] for v in st.values()), '{:,d}')156 macro('llmInvalid', sum(v['invalid'] for v in st.values()), '{:d}')157 for p, tag in [('llm_shared', 'shared'), ('llm_diverse', 'diverse'), ('bayesian', 'bayes')]:158 for (D, M), t2 in [((0, 1), 'zero'), ((1, 1), 'oneone'), ((1, 3), 'onethree')]:159 r = g[(g.policy == p) & (g.D == D) & (g.M == M) & (g.temperature == 0)]160 if len(r):161 macro(f'llm{tag}{t2}', r.iloc[0].of, '{:.2f}'); macro(f'llm{tag}{t2}trades', r.iloc[0].trades, '{:.1f}')162163# ------------------------------------------------------------------ notation, novelty, computational cost164with open(TAB / 'notation.tex', 'w') as f:165 f.write("\\begin{table}[htbp]\\centering\\footnotesize\n\\caption{Notation}\\label{tab:notation}\n\\begin{tabular}{ll}\\toprule\nSymbol & Meaning\\\\\\midrule\n")166 for s, m in [('$z_t,\\ \\theta_t=\\varepsilon_{t+1}$', 'log TFP; next innovation (the news)'), ('$s_{it}, s_{mt}$', 'private signal of household $i$; signal of model $m$'),167 ('$\\tau_\\theta,\\tau_H,\\tau_A$', 'prior, human and model precisions; $\\beta_H,\\beta_A$ Bayesian weights'), ('$D,\\ M$', 'delegation share; effective number of independent models'),168 ('$\\iota,\\ \\bar\\iota,\\ \\psi(x)$', 'automated task share, frontier, compute productivity on task $x$'), ('$K,K_Q,Q^P,\\bar q$', 'capital, compute stock, production compute, inference compute per decision'),169 ('$w,R^K,p_Q,p_E$', 'wage, capital rental, compute rental, energy price'), ('$\\phi,\\kappa_i,\\tau_s$', 'delegation fee, disutility, supervision time'),170 ('$x_i,X$', 'individual and aggregate action (log deviations)'), ('$\\kappa_0,\\alpha,\\upsilon$', 'PE response, strategic complementarity, curvature ratio $U_{XX}=-\\upsilon U_{xX}$'),171 ('$a_H,a_A,\\xi,\\bar A$', 'equilibrium weights, damping factor, aggregate loading'), ('$\\PV,\\ \\Delta$', 'private value of delegation; private--social wedge'),172 ('$m,\\ k_B,\\ \\lambda_r,\\ \\mu$', 'margin, bank capital ratio, price impact, margin multiplier'), ('$\\nu^r, m_H,\\sigma_{rH},\\sigma_{rA}$', 'decision-quality return term (Model B)')]:173 f.write(f"{s} & {m}\\\\\n")174 f.write("\\bottomrule\\end{tabular}\\end{table}\n")175with open(TAB / 'novelty.tex', 'w') as f:176 f.write("\\begin{table}[htbp]\\centering\\footnotesize\n\\caption{Novelty matrix: what is old, what is new}\\label{tab:novelty}\n\\begin{tabularx}{\\textwidth}{Xll}\\toprule\nIngredient & Closest prior work & Status here\\\\\\midrule\n")177 for a, b, c in [('Common-noise aggregate fluctuations', 'Lorenzoni 2009; Angeletos--La\'O 2013', 'inherited'), ('Efficiency of the use of information under CRS', 'Angeletos--Pavan 2007', 'inherited, applied'),178 ('Endogenous purchase of the same information', 'Veldkamp 2006; Hellwig--Veldkamp 2009; Myatt--Wallace 2012', 'inherited'),179 ('Private gain / common exposure externality', 'Stein 2009; Wagner 2011', 'inherited (portfolios)'),180 ('AI adoption with correlated signals in a Kyle market', 'Meng--Chen 2026', 'closest competitor'),181 ('Delegation market sets the error-covariance of a production economy; closed-form $D$ and $1/M$ loadings', '--', 'new (Prop.~2)'),182 ('Private value of delegation = social marginal value under CRS; wedges under three frictions, closed form', '--', 'new (Props.~3--4)'),183 ('One compute market clears inference and automation; crowding-out of automation by delegation', '--', 'new (Prop.~5)'),184 ('Factorial agentic-market experiments over $(D,M)$ with three designs and live LLM agents under hard constraints', 'Gode--Sunder 1993 (discipline)', 'new')]:185 f.write(f"{a} & {b} & {c}\\\\\n")186 f.write("\\bottomrule\\end{tabularx}\\end{table}\n")187try:188 rev = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD'], cwd=ROOT, text=True).strip()189except Exception:190 rev = 'uncommitted'191with open(TAB / 'compute_cost.tex', 'w') as f:192 f.write("\\begin{table}[htbp]\\centering\\footnotesize\n\\caption{Computational cost and provenance}\\label{tab:cost}\n\\begin{tabular}{lll}\\toprule\nComponent & Typical cost & Hardware / software\\\\\\midrule\n")193 rows = [('Model A (all figures)', '$<1$ min', 'laptop, numpy/scipy'), ('Model B (one equilibrium)', '4--30 s; 10 scenarios $\\approx$ 5 min', 'numba kernels'),194 ('Model C factorial (20 seeds)', '$\\approx$ 10 min', 'pure python'), ('LLM lab (live)', '$\\approx$ 2--3 min per 8-agent $\\times$ 24-period run', 'mlx\\_lm.server, Apple M3 Ultra nodes')]195 for a, b, c in rows:196 f.write(f"{a} & {b} & {c}\\\\\n")197 f.write(f"\\midrule Git revision & \\texttt{{{rev}}} & Python {platform.python_version()}, {esc(platform.platform())}\\\\\n")198 f.write("\\bottomrule\\end{tabular}\\end{table}\n")199200with open(TAB / 'numbers.tex', 'w') as f:201 for k, v in nums.items():202 f.write(f"\\newcommand{{\\num{k}}}{{{v}}}\n")203# fallbacks: any \numXXX used in the sections but not produced yet renders as a visible placeholder204import re205used = set()206for p in list((ROOT / 'paper' / 'sections').glob('*.tex')) + list((ROOT / 'paper' / 'appendices').glob('*.tex')):207 used |= set(re.findall(r'\\num([A-Za-z]+)', p.read_text()))208with open(TAB / 'fallbacks.tex', 'w') as f:209 for k in sorted(used - set(nums)):210 f.write(f"\\providecommand{{\\num{k}}}{{\\textbf{{[n/a]}}}}\n")211print('missing macros:', sorted(used - set(nums)))212print(len(nums), 'macros;', json.dumps(nums)[:1500])213