Economic Scenarios for Transformative AI

An independent reproduction of Korinek, Jones, Sacher, Cotter and McCrory (2026), The Anthropic Institute Working Paper No. 2026-02

Published

September 23, 2026

Introduction

An independent reproduction of Economic Scenarios for Transformative AI, following the Open Policy Analysis framework:

  • Open Output: the explorer, 4374 precomputed combinations of the seven inputs
  • Open Analysis: this document, and the slide deck
  • Open Materials: the repository, the model, its tests and every exported CSV

No replication package was published with the paper, so every number below is computed from the equations printed in it: Proposition 1 (p. 15), which is the closed form for a frictionless economy where labor reallocates instantly and which Section 2.1.5 states and derives; the 44-equation monthly system of Table A.1 (pp. 42-43); the simulation procedure of Appendix A (pp. 40-41); the innovation block of Appendix C (pp. 49-51); and the parameters of Tables 1 and A.2 (pp. 23-25, 43-45). The implementation is the aiscen package beside this file; this document calls it in the order the paper develops the model and puts each reproduced quantity next to the published one.

The numbered sections below start at 2 and skip nothing: they are the paper’s own section numbers, kept so a reader can hold the two documents side by side. The paper’s Section 1 is its introduction, which states no equations, so there is nothing here to reproduce against it.

Two conventions, both from the note to Table 3: level differences are percent deviations from the no-AI path, \(e^{\Delta \ln x} - 1\), and growth rates are log changes over the preceding twelve months.

A third convention concerns the equations. Every numbered equation here carries the paper’s own number, so (17) below is (17) in the paper, and the numbers therefore do not run in order: the document follows the order in which the paper develops the model, not the order in which it numbers it. One equation, one number, one anchor, so any number can be linked to directly. Equations that appear inside a refresher callout, or as an intermediate algebraic step, are deliberately left unnumbered because they are not in the paper.

Notation, in one place

Every quantity the model reports is a gap against a counterfactual without AI, not a level: \(\Delta \ln x_t \equiv \ln x_t - \ln \bar x_t\), where the bar is the no-AI path along which the economy grows at \(g + n\). So “GDP is 8.3 percent above the no-AI path” says nothing about whether GDP is high or low in absolute terms.

Four more conventions worth holding onto:

  • Head counts are shares of the labor force. \(\ell_{C,t} + \ell_{N,t} + U_{C,t} + U_{N,t} = 1\) at every date, so employment and unemployment are fractions, not millions of people. The subscript \(C\) is the paper’s own “cognitive” occupations (\(N\) is everyone else), which this document keeps, because it follows the paper line by line. The slide deck, the explorer and the landing page relabel the same group “AI-sensitive occupations” in prose while keeping the same subscript and the same underlying data, so a reader moving between them and this report isn’t hit with a silent, unexplained vocabulary swap.
  • The final good is the numeraire, \(P_t \equiv 1\). “The wage” is therefore a real wage, and the price index is a constraint rather than an output: if AI lowers the cost of some tasks, some other price has to rise to hold \(P_t\) at one. That constraint is what forces the productivity gains into factor prices.
  • A tilde means deflated by the ideas stock, \(\tilde w = w / A_t\). The labor-augmenting ideas stock \(A_t\) raises the productivity of every worker, so unit costs depend on \(w/A\) rather than \(w\) alone.
  • The grid is monthly, but dates are written in calendar years: \(t = 2026.5\) is mid-2026, and one period is \(h = 1/12\).
setup: imports, scenarios, simulated paths, plotting style
# `import X` makes module X's contents available as `X.thing`; `from X import a, b`
# pulls specific names straight into this file's namespace so you can write `a`.
import math                     # standard library: exp, log, sqrt

import matplotlib as mpl        # plotting; `as mpl` is just a shorter alias
import matplotlib.pyplot as plt
import numpy as np              # arrays and NaN
import pandas as pd             # DataFrames: tables with named columns
from IPython.display import Markdown, display

# `aiscen` is the package sitting next to this document (the aiscen/ folder). Python
# finds it because Quarto runs this document from that folder.
from aiscen import Fixed, SCENARIOS, simulate, statics, steady
from aiscen.params import SURVEY_MEDIAN
from aiscen.paths import Paths, research_share
from aiscen.report import PUBLISHED, ROW_ORDER, build_table3, pct, table3_column

# Fixed() creates one "parameter object". It is a dataclass, meaning its fields are
# declared once and accessed by name: F.sigma, F.s_L0, F.eps. Nothing is a loose
# global variable, so a typo like F.sigmaa raises an error instead of silently
# returning something wrong.
F = Fixed()                                   # Table 1, panels A and D
SS = steady.solve(F)                          # Table A.1, panel E

NAMES = ["modest", "substantial", "extreme"]  # a list: ordered, written with []

# A "dict comprehension": {key: value for item in iterable}. This one runs the model
# once per scenario and stores the results under the scenario's name, so RUNS is a
# dictionary you index by string: RUNS["extreme"]. Written out longhand it would be
#     RUNS = {}
#     for n in NAMES:
#         RUNS[n] = simulate.run(F, SCENARIOS[n])
# The comprehension is the idiomatic one-line version of exactly that loop.
P = {n: Paths.build(F, SCENARIOS[n]) for n in NAMES}
RUNS = {n: simulate.run(F, SCENARIOS[n]) for n in NAMES}
T3 = build_table3(F)                          # {row name: (no-AI, modest, subst, extreme)}

# Categorical palette slots 1-3, validated for CVD separation and lightness; the
# contrast warning on slot 3 is relieved by direct end-labels, which is also the
# paper's own figure convention.
COL = {"modest": "#2a78d6", "substantial": "#eb6834", "extreme": "#1baf7a"}
INK, INK2, GRID = "#0b0b0b", "#52514e", "#d9d8d2"
mpl.rcParams.update({
    "figure.dpi": 150, "savefig.dpi": 150, "savefig.bbox": "tight",
    "font.size": 8.0, "axes.titlesize": 8.5, "axes.labelsize": 7.5,
    "axes.spines.top": False, "axes.spines.right": False,
    "axes.edgecolor": GRID, "axes.labelcolor": INK2, "text.color": INK,
    "xtick.color": INK2, "ytick.color": INK2, "xtick.labelsize": 7,
    "ytick.labelsize": 7, "grid.color": GRID, "grid.linewidth": 0.6,
    "axes.grid": True, "axes.axisbelow": True, "legend.frameon": False,
    "figure.facecolor": "white", "axes.facecolor": "white", "lines.linewidth": 2.0,
})


def frame(res):
    """One tidy monthly frame per scenario, in the paper's reporting units.

    A docstring (the triple-quoted text right under `def`) is Python's built-in way
    to document a function; `help(frame)` prints it. `res` is the object
    simulate.run() returned: it holds the fixed parameters and a list of monthly
    records.
    """
    f = res.fixed
    # Tuple unpacking: the right side builds a pair, the left side names both halves
    # in one line. res.at(date) returns the monthly record closest to that date.
    l_C_2026, l_N_2026 = res.at(f.t_anchor).l_C, res.at(f.t_anchor).l_N

    rows = []                       # collect one dict per month, then build a table
    # enumerate() yields (position, item) pairs, so `i` counts months from 0 while
    # `m` is the record itself. We need `i` to look 12 months back for growth rates.
    for i, m in enumerate(res.months):
        # A "conditional expression": VALUE_IF_TRUE if CONDITION else VALUE_IF_FALSE.
        # For the first 12 months there is no year-ago record, so `back` is None and
        # the growth columns below get NaN (numpy's missing-value marker).
        back = res.months[i - 12] if i >= 12 else None
        rows.append({
            "t": m.t, "m": m.m, "d": m.d, "md": m.m * m.d, "a": m.a,
            "gdp": pct(m.dlnY),
            "gdp_growth": 100 * (f.g + f.n + m.dlnY - back.dlnY) if back else np.nan,
            "wage": pct(m.dlnw_avg), "wC": pct(m.dlnw_C_paid), "wN": pct(m.dlnw_N),
            "net_r": 100 * (f.r_bar * math.exp(m.dlnr) - f.delta),
            "capital": pct(m.dlnK), "labor_share": 100 * m.s_L,
            "cog_emp": 100 * (m.l_C / l_C_2026 - 1.0),
            "oth_emp": 100 * (m.l_N / l_N_2026 - 1.0),
            "u_C": 100 * m.u_rate_C, "u_all": 100 * m.u_rate,
            "u_excess": 100 * m.u_excess, "tfp": pct(m.dln_tfp),
            "tfp_growth": 100 * (f.s_L0 * f.g + m.dln_tfp - back.dln_tfp) if back else np.nan,
            "ideas": pct(m.dlnA),
            "ideas_growth": 100 * (f.g + m.dlnA - back.dlnA) if back else np.nan,
            "dlnR": m.dlnR, "realloc": 100 * m.reallocation,
        })
    return pd.DataFrame(rows)


D = {n: frame(RUNS[n]) for n in NAMES}


def v(scen, col, d=1, t=2030.0):
    """Inline value: column `col` of scenario `scen` at date `t`.

    `d=1` and `t=2030.0` are default arguments: callers may omit them. So
    v("extreme", "gdp") means v("extreme", "gdp", d=1, t=2030.0).
    """
    df = D[scen]
    # Read this inside out: (df["t"] - t) is a column of differences, .abs() makes
    # them positive, .idxmin() returns the row label of the smallest, and .iloc[...]
    # fetches that row. In short: the row whose date is closest to t.
    row = df.iloc[(df["t"] - t).abs().idxmin()]
    return f"{row[col]:,.{d}f}"


def num(x, d=1):
    """Format a number for prose: 1 decimal by default, with thousands separators.

    This is an f-string, Python's string interpolation: anything inside {} is
    evaluated. The part after the colon is a format spec, where "," asks for
    thousands separators and ".1f" means fixed-point with one decimal. The nested
    {d} makes the number of decimals itself a variable.
    """
    return f"{x:,.{d}f}"


def show(df, **kw):
    """Render a DataFrame as a markdown table that Quarto will style.

    `**kw` collects any extra keyword arguments and forwards them to to_markdown(),
    so show(df, floatfmt=".3f") works without naming floatfmt here.
    """
    return Markdown(df.to_markdown(index=False, **kw))

2 The model

2.1.1 Technology and factor markets

Output is a CES aggregate not over capital and labor but over task instances: a task is a type of work (reviewing a contract), an instance is one performance of it (this contract).

\[ Y_t = \Big[ \sum_i \omega_i^{1/\sigma}\, y_{i,t}^{(\sigma-1)/\sigma} \Big]^{\sigma/(\sigma-1)} \tag{1} \]

This is the CES function from first-year graduate macro, with the inputs reinterpreted. The weights \(\omega_i\) are base-period expenditure shares and sum to one, and \(\sigma\) is the elasticity of substitution between any two instances, whether they belong to the same task or different ones.

The single fact to carry forward is that \(\sigma =\) 0.5 is below one, so instances are gross complements: the economy needs some of everything, and whatever stays expensive becomes the bottleneck. In the Cobb-Douglas case \(\sigma = 1\) expenditure shares are constant and nothing interesting happens to the labor share. Below one, a task whose cost falls sees its expenditure share fall, because quantity demanded rises less than proportionally to the price cut. That one inequality drives most of the paper’s distributional results.

Each instance can be produced by a worker or by a machine, and within an instance the two are perfect substitutes:

\[ y_{i,t} = A_t\, \alpha_{L,i,t}\, \ell_{i,t} + \alpha_{K,i,t}\, k_{i,t} \tag{2} \]

\[ p_{i,t} = c_{i,t} = \min\Big\{ \frac{w_t}{A_t \alpha_{L,i,t}},\ \frac{r_t}{\alpha_{K,i,t}} \Big\} \tag{3} \]

Equation (2) says one contract review is one contract review, whoever does it: a worker delivering \(A_t \alpha_L\) units and a machine delivering \(\alpha_K\) units are interchangeable for that instance. \(A_t\) is the stock of ideas and augments labor only.

Competition then prices each instance at unit cost, and cost minimization assigns it to whichever factor is cheaper, which is Equation (3). Rank instances by capital’s comparative advantage \(\alpha_{K,i}/\alpha_{L,i}\) and the assignment is a threshold rule: everything above the cutoff is done by machines, everything below by people, and the cutoff moves with the factor price ratio \(r_t/w_t\). This is the task-based setup of Zeira (1998) and Acemoglu and Restrepo (2018), and it is where the model departs from the aggregate production function of undergraduate macro: automation is not a shift in an exponent, it is a set of tasks changing hands.

In this paper “automation” means AI raises \(\alpha_{K,i}\) on an instance far enough that capital takes it over at the pre-AI rental rate; “augmentation” means AI raises \(\alpha_{L,i}\) so the worker keeps the instance and does it faster. Both cut the instance’s unit cost by the same \(a_{i,t}\) log points. What differs is who is holding it afterwards, which is why \(\psi_t\) moves the labor share without moving TFP.

\[ s_{i,t} \equiv \frac{p_{i,t} y_{i,t}}{P_t Y_t} = \omega_i \Big(\frac{c_{i,t}}{P_t}\Big)^{1-\sigma}, \qquad P_t = \Big[\sum_j \omega_j c_{j,t}^{1-\sigma}\Big]^{1/(1-\sigma)} \equiv 1 \tag{4} \]

Two steps, both standard. Cost minimization on (1) gives the CES demand curve \(y_{i} = \omega_i (c_{i}/P)^{-\sigma} Y\). Multiply by the price \(c_i\) and divide by \(PY\) to turn a quantity into an expenditure share, and the exponent goes from \(-\sigma\) to \(1-\sigma\): the price enters once through quantity demanded and once through revenue per unit.

The sign is what trips people up. With \(\sigma < 1\) the exponent \(1 - \sigma\) is positive, so a cheaper instance takes a smaller share of spending. Concretely, at \(\sigma = 0.5\) an instance whose unit cost falls by 0.45 log points, a factor of \(e^{-0.45} =\) 0.64, has its expenditure share multiplied by 0.80: down about a fifth, even though more of it is produced. Set \(\sigma > 1\) and every result below flips sign.

The normalization \(P_t \equiv 1\) is not cosmetic. Since the price index is a weighted average of unit costs, holding it at one while AI cuts some costs forces the remaining prices, the wage and the rental rate, to rise. That is the mechanical channel through which productivity gains become factor income.

\[ s_{L,t} = \sum_{i \in \mathcal{L}_t} s_{i,t} = \frac{w_t L_t}{P_t Y_t}, \qquad \Delta \ln w_t = \Delta \ln (Y_t / L_t) + \Delta \ln s_{L,t} \tag{5} \]

The labor share is the sum of the expenditure shares of the instances labor still performs, which by definition equals the wage bill over GDP. Take logs of \(s_L = wL/(PY)\) and rearrange: \(\ln w = \ln(Y/L) + \ln s_L\). Nothing behavioural has been assumed, so it holds exactly at every date.

Its use is that any two of the three pin the third. The paper’s headline that wages rise much less than GDP is the same statement as the labor share falling. Check it in the extreme scenario at 2030, at the frictionless targets of Proposition 1: output per worker is 36.5 log points above the no-AI path, the labor share change is -29.8 log points, and they sum to 6.7, the common wage gap. (The simulated economy, with its unemployment, reports an average wage 9.7 percent above the no-AI path.)

\[ \Delta \ln K_t = \varepsilon\, \Delta \ln r_t \tag{6} \]

Rather than solve a household savings problem, the paper posits an upward-sloping supply curve: capital is \(\varepsilon\) percent higher for every percent the rental rate exceeds its no-AI level \(\bar r\). It is the reduced form of Moll, Rachel and Restrepo (2022), whose households hold more wealth when the return is higher.

The two limits are the intuition. At \(\varepsilon = 0\) the capital stock cannot respond within the horizon, so any extra demand for machines shows up entirely in the rental rate. At \(\varepsilon = \infty\) the rental rate is pegged at \(\bar r\), the small-open-economy case, and any amount of capital arrives at an unchanged return; then the whole productivity gain goes to labor, which is the Caselli and Manning (2019) result. The baseline \(\varepsilon =\) 3 sits between them. It comes from converting Moll et al.’s semi-elasticity of 50 in the net return: \(50 \bar r =\) 6 in log-rental units (footnote 1, p. 9), halved because their economy takes decades to get there. Section 4.5 reruns everything at \(\varepsilon = 1, 3, 6, \infty\), and this is the single parameter that decides whether the average wage rises or falls.

Tasks are gross complements at \(\sigma =\) 0.5, labor earns 60 percent of income before AI, and the capital supply schedule (6) has elasticity \(\varepsilon =\) 3.

base-period technology and factor markets
# A list of tuples. Each tuple is (label, value computed here, value printed in the
# paper), so the table can put them side by side. Tuples are written with () and are
# fixed-length; lists use [] and can grow.
base = [
    ("sigma, elasticity of substitution across tasks", F.sigma, "0.5"),
    ("s_L,t0, base-period labor share", F.s_L0, "0.60"),
    ("s_K,t0 = 1 - s_L,t0", F.s_K0, "0.40"),
    ("s_C,t0 / s_L,t0, cognitive share of employment", F.cog_share, "0.624"),
    ("s_N,t0 / s_C,t0, relative size of the two groups", (1 - F.cog_share) / F.cog_share, "0.603"),
    ("eps, elasticity of capital supply", F.eps, "3"),
    ("net return r_bar - delta, per year", F.r_bar - F.delta, "0.065"),
    ("capital-output ratio s_K,t0 / r_bar", F.s_K0 / F.r_bar, "3.5"),
    ("g + n, no-AI GDP growth, per year", F.g + F.n, "0.02"),
    ("g_A = s_L,t0 g, no-AI TFP growth, per year", F.s_L0 * F.g, "0.010"),
    ("l_C,t0, base cognitive employment, share of L", F.l_C0, "0.600"),
    ("l_N,t0, base all-other employment, share of L", F.l_N0, "0.362"),
]
# pd.DataFrame({...}) builds a table from a dict of {column name: list of values}.
# Each list here is a "list comprehension": [EXPRESSION for item in iterable], which
# builds a new list by evaluating EXPRESSION once per item. r[0] is the first element
# of each tuple, r[1] the second, and ".4g" formats to 4 significant digits.
show(pd.DataFrame({"object": [r[0] for r in base],
                   "this reproduction": [f"{r[1]:.4g}" for r in base],
                   "paper": [r[2] for r in base]}))
object this reproduction paper
sigma, elasticity of substitution across tasks 0.5 0.5
s_L,t0, base-period labor share 0.6 0.6
s_K,t0 = 1 - s_L,t0 0.4 0.4
s_C,t0 / s_L,t0, cognitive share of employment 0.624 0.624
s_N,t0 / s_C,t0, relative size of the two groups 0.6026 0.603
eps, elasticity of capital supply 3 3
net return r_bar - delta, per year 0.065 0.065
capital-output ratio s_K,t0 / r_bar 3.478 3.5
g + n, no-AI GDP growth, per year 0.02 0.02
g_A = s_L,t0 g, no-AI TFP growth, per year 0.01002 0.01
l_C,t0, base cognitive employment, share of L 0.6003 0.6
l_N,t0, base all-other employment, share of L 0.3617 0.362

2.1.2 AI scenarios

The five exogenous objects are the affected mass \(m_t\), the diffusion share \(d_t\), the log gain per instance \(a_t\), the automation share \(\psi_t\) and the reinstatement ratio \(\rho\). All three scenarios share the mid-2026 anchors \(m = 0.14\) and \(d = 0.10\), so they coincide there and separate only afterwards; each logistic’s slope follows from the anchor, the 2030 value and the ceiling by Equation (8’).

Equation (8’): logistic slopes and the 2030 values
rows = []                            # start empty, append one dict per scenario
for n in NAMES:
    p, s = P[n], SCENARIOS[n]        # p: the fitted paths; s: the scenario's parameters
    # .append() adds one element to the end of a list. Each element is a dict
    # {"column name": value}, and pandas turns a list of such dicts into a table
    # whose columns are the dict keys.
    rows.append({
        "scenario": n, "kappa_m": p.kappa_m, "kappa_d": p.kappa_d,
        "t_mid m": p.t_mid_m, "t_mid d": p.t_mid_d,
        "m 2030": p.m(2030.0), "d 2030": p.d(2030.0), "a 2030": p.a(2030.0),
        "m d 2030": p.m(2030.0) * p.d(2030.0), "psi": s.psi, "rho": s.rho,
    })
show(pd.DataFrame(rows), floatfmt=".3f")
scenario kappa_m kappa_d t_mid m t_mid d m 2030 d 2030 a 2030 m d 2030 psi rho
modest 0.140 0.232 2035.378 2035.983 0.200 0.200 0.300 0.040 0.500 0.500
substantial 0.332 0.512 2030.232 2030.792 0.300 0.400 0.448 0.120 0.750 0.250
extreme 0.753 0.744 2028.148 2029.455 0.500 0.600 0.800 0.300 0.900 0.000

\[ m_t = \frac{\bar m}{1 + e^{-\kappa_m (t - t_m)}}, \qquad d_t = \frac{\bar d}{1 + e^{-\kappa_d (t - t_d)}}, \qquad a_t = a_0 + g_a (t - t_0) \tag{8} \]

\[ \kappa_m = \frac{1}{3.5} \ln\Big( \frac{\bar m - m_{2026}}{m_{2026}} \cdot \frac{m_{2030}}{\bar m - m_{2030}} \Big) \tag{8'} \]

Technology diffusion is S-shaped in the historical record (Griliches on hybrid corn, David on electricity), so reach and use follow logistics while the productivity gain per instance is allowed to drift linearly.

Equation (8’) looks worse than it is. A logistic is exactly the curve whose log odds are linear in time: rearranging (8) gives \(\ln\!\big(m_t / (\bar m - m_t)\big) = \kappa_m (t - t_m)\). So the slope is just the change in log odds between the two dates the paper pins down, divided by the 3.5 years between them,

\[ \kappa_m = \frac{1}{3.5}\Big[ \ln \frac{m_{2030}}{\bar m - m_{2030}} - \ln \frac{m_{2026}}{\bar m - m_{2026}} \Big], \]

which is (8’) with the two logs combined. The ceiling \(\bar m = s_{C,t0}/s_{L,t0} =\) 0.624 is all cognitive work: in the long run AI reaches all of it, and the scenarios differ only in how much it reaches by 2030. Since all three share the mid-2026 anchor, the scenarios are a fan opening from a single point, which is why the paper stresses that almost all divergence happens after 2027.

For the substantial scenario the paper states those slopes as \(\kappa_m = 0.33\) and \(\kappa_d = 0.51\) (Table A.2, p. 45); here they are 0.33 and 0.51. Because the paths start near zero, the largest GDP gap the simulation shows at the 2024 base period is 0.25 percent, matching the “at most a quarter of a percent” of Appendix A.

By 2030 AI performs 4, 12 and 30 percent of the economy’s task instances, the 4, 12 and 30 percent quoted in Sections 4.1 to 4.3. In the extreme scenario the gain per instance reaches 0.80 log points, more than doubling productivity on the tasks it touches: \(e^{0.8} =\) 2.2.

the exogenous paths
fig, axes = plt.subplots(1, 2, figsize=(6.6, 2.3))
for n in NAMES:
    df = D[n][D[n].t >= 2024.0]
    axes[0].plot(df.t, 100 * df.md, color=COL[n], label=n.capitalize())
    axes[1].plot(df.t, df.a, color=COL[n], label=n.capitalize())
    axes[0].annotate(f"{100 * df.md.iloc[-1]:.0f}", (df.t.iloc[-1], 100 * df.md.iloc[-1]),
                     xytext=(4, 0), textcoords="offset points", fontsize=7,
                     color=INK2, va="center")
    axes[1].annotate(f"{df.a.iloc[-1]:.2f}", (df.t.iloc[-1], df.a.iloc[-1]),
                     xytext=(4, 0), textcoords="offset points", fontsize=7,
                     color=INK2, va="center")
axes[0].set_title("Task instances performed with AI, $m_t d_t$", loc="left")
axes[0].set_ylabel("percent of all instances")
axes[1].set_title("Log gain per AI-performed instance, $a_t$", loc="left")
axes[1].set_ylabel("log points")
for ax in axes:
    ax.set_xlim(2024, 2030.8)
    ax.axvline(F.t_anchor, color=GRID, lw=0.8, zorder=0)
axes[0].legend(loc="upper left", fontsize=7)
plt.show()
Figure 1: The exogenous paths behind the paper’s Figures 2 to 4: the share of task instances performed with AI and the log gain on each. The vertical rule is the mid-2026 anchor, where the three scenarios coincide by construction. Not a figure in the paper.

2.1.3 Measured TFP and the factor-price frontier

\[ \Delta \ln \mathrm{TFP}_t \approx \sum_i \omega_i d_{i,t} a_{i,t} = s_{L,t0} \sum_i m_i d_{i,t} a_{i,t} = s_{L,t0}\, m_t d_t a_t \tag{9} \]

Hulten (1978) says that to a first order, the aggregate productivity gain is the sum of the microeconomic cost reductions, each weighted by its Domar weight: the producer’s sales divided by GDP. The useful part is that you do not need to know how the production network is wired, only how big each piece is and how much cheaper it got.

Here each task instance sells into final output, so its Domar weight is just its expenditure share \(\omega_i\), and the cost decline is \(a_{i,t}\) on the fraction \(d_{i,t}\) of instances that use AI. Hence the first expression.

Why does \(s_{L,t0}\) appear in the last one? Purely as a change of units, not as a behavioural claim. The affected tasks are all tasks labor was doing, and the paper measures their size as a fraction of the wage bill, \(m_i \equiv \omega_i/s_{L,t0}\), so that \(\sum_i m_i = 1\) across labor’s tasks. Converting back to a fraction of GDP multiplies by the labor share. So “AI touches 12 percent of the economy’s task instances”, the paper’s phrase, counts instances at their pre-AI wage-bill weights, as Table A.2 defines it; measured in GDP it is \(0.6 \times 0.12\), about 7 percent, because capital’s pre-AI instances are left out.

Notice what is absent: the automation share \(\psi_t\). For aggregate productivity it is irrelevant whether the cheaper instance ends up in a machine’s hands or a worker’s, because the unit cost falls by \(a_{i,t}\) either way. \(\psi_t\) matters enormously for the distribution of the gain, and not at all for its size.

\[ s_{L,t0}\, \Delta \ln w_t + s_{K,t0}\, \Delta \ln r_t \approx s_{L,t0}\, m_t d_t a_t \tag{10} \]

The Solow residual you computed in graduate macro as \(d\ln \mathrm{TFP} = d\ln Y - s_L d\ln L - s_K d\ln K\) has a dual: under constant returns and competitive pricing, factor payments exhaust output, and substituting the first-order conditions turns the same object into the share-weighted growth of factor prices,

\[ d\ln \mathrm{TFP}_t = s_{L,t}\, d\ln w_t + s_{K,t}\, d\ln r_t . \]

Read from left to right it is a measurement identity. Read from right to left, as the paper does, it is a budget constraint on the gains: whatever productivity AI creates has to leave the model as higher wages or a higher return, in some combination, because there is nowhere else for it to go.

That yields the paper’s cleanest piece of intuition. If capital is elastic enough that the rental rate does not move, labor gets the entire gain and \(\Delta \ln w \approx m d a\). Every percent the rental rate does rise costs the wage \(s_{K,t0}/s_{L,t0} =\) 0.67 percent. The wage can even fall below its no-AI path, which happens exactly when \(s_{K,t0} \Delta \ln r_t > s_{L,t0} m_t d_t a_t\): capital’s claim exceeds the gain being distributed.

The paper illustrates the mechanics with the substantial scenario’s 2030 values, \(m = 0.30\), \(d = 0.40\), \(a = 0.45\) (pp. 12-13). That passage, reproduced:

the worked example of Section 2.1.3
# The paper's illustration uses round numbers rather than the fitted 2030 path
# values, so they are written literally here. md_x is m times d; a_x is the gain.
md_x, a_x = 0.30 * 0.40, 0.45

# frictionless() solves Proposition 1: it finds the rental-rate gap that clears the
# capital market, then returns every other variable implied by it. The arguments are
# positional, in the order the function defines them: (params, m*d, a, psi, rho).
ex = statics.frictionless(F, md_x, a_x, 0.75, 0.25)
worked = [
    ("m d, instances performed with AI", md_x, "0.12"),
    ("m d a, the wage gain at a pegged rental rate", md_x * a_x, "0.054"),
    ("first-order TFP gain, s_L,t0 m d a (26)", F.s_L0 * md_x * a_x, "0.032"),
    ("exact TFP gain (45)", ex.dln_tfp, "0.029"),
    ("rental-rate gap at eps = 3", ex.dlnr, "0.046"),
    ("capital's term in the frontier, s_K,t0 dln r", F.s_K0 * ex.dlnr, "0.018"),
    ("wage gap at eps = 3", ex.dlnw, "0.019"),
    ("s_K,t0 / s_L,t0, wage cost per percent of rental rate", F.s_K0 / F.s_L0, "2/3"),
]
show(pd.DataFrame({"object": [r[0] for r in worked],
                   "this reproduction": [f"{r[1]:.4f}" for r in worked],
                   "paper": [r[2] for r in worked]}))
object this reproduction paper
m d, instances performed with AI 0.12 0.12
m d a, the wage gain at a pegged rental rate 0.054 0.054
first-order TFP gain, s_L,t0 m d a (26) 0.0324 0.032
exact TFP gain (45) 0.0292 0.029
rental-rate gap at eps = 3 0.0447 0.046
capital’s term in the frontier, s_K,t0 dln r 0.0179 0.018
wage gap at eps = 3 0.0172 0.019
s_K,t0 / s_L,t0, wage cost per percent of rental rate 0.6667 2/3

AI raises measured TFP by 3.2 percent to first order and 2.9 percent exactly. At a pegged rental rate the entire gain would accrue to labor and the wage would sit 5.4 percent above its no-AI path. With capital supplied at \(\varepsilon = 3\) the rental rate instead rises 4.5 percent, capital’s term absorbs 1.8 of the frontier, and the wage gains 1.7 percent rather than 5.4.

Two rows differ from the paper’s: its rental-rate and wage figures include the substantial run’s 2030 ideas gap, \(\Delta \ln A \approx\) 0.002. Passing it, statics.frictionless(F, md_x, a_x, 0.75, 0.25, dlnA=...), gives 4.5 and 1.9 percent, against the paper’s 4.6 and 1.9. The paper’s own next step uses 0.045 for that rental-rate gap (its capital term is \(0.4 \times 0.045 = 0.018\)), so the printed 4.6 looks like a rounding of the same number rather than a different value.

2.1.4 First-order solutions against the exact model

\[ \begin{aligned} \Delta \ln w_t &\approx m_t d_t a_t - \frac{s_{K,t0}}{s_{L,t0}} \Delta \ln r_t \\[2pt] \Delta \ln r_t &\approx \frac{1}{\varepsilon + \sigma / s_{L,t0}} \Big[ m_t d_t a_t + \big((1-\rho) - (1-\sigma) a_t\big) \frac{\psi_t m_t d_t}{s_{K,t0}} + \Delta \ln A_t \Big] \\[2pt] \Delta \ln s_{L,t} &\approx -\underbrace{(1-\rho)\psi_t m_t d_t}_{\text{displacement}} + \underbrace{(1-\sigma)\psi_t m_t d_t a_t}_{\text{weak-link effect}} - \underbrace{(1-\sigma)\frac{s_{K,t0}}{s_{L,t0}}\Delta \ln r_t}_{\text{dearer capital}} \\[2pt] \Delta \ln (Y_t/L_t) &\approx m_t d_t a_t + (1-\rho)\psi_t m_t d_t - (1-\sigma)\psi_t m_t d_t a_t - \sigma \frac{s_{K,t0}}{s_{L,t0}} \Delta \ln r_t \end{aligned} \tag{11} \]

The wage line is just the frontier (10) rearranged: labor gets the productivity gain less whatever the rental rate takes. Note what is not there: net displacement \((1-\rho)\psi m d\) does not appear. At a fixed rental rate the wage tracks productivity, not the number of tasks labor lost. Displacement reaches the wage only indirectly, by pushing up the rental rate.

The rental line is a supply-equals-demand solution. The bracket is the extra capital AI calls for at an unchanged rental price, from two sources: capital keeps pace with the output the gains themselves create (\(m d a\)), and each automated instance moves its wage bill onto capital (\(\psi m d / s_{K,t0}\), adjusted for reinstatement \(\rho\) and for the substitution the cost saving sends back toward labor). The denominator is the pair of elasticities that reconcile it: supply rises with the rental rate at \(\varepsilon\), demand falls at \(\sigma/s_{L,t0}\).

The labor-share line has the three channels the paper names. Automation moves the entire wage bill of an instance to capital however small the cost saving, which is \(-(1-\rho)\psi m d\) net of reinstated tasks. Then, because the instances labor keeps did not get cheaper while the automated ones did, labor’s instances become the expensive ones, and with \(\sigma < 1\) spending tilts toward what is expensive: that adds back \((1-\sigma)\psi m d a\). Finally the same complementarity works against labor when capital itself gets dearer.

The output line is then the wage line minus the labor-share line, which is just identity (5) again.

The same algebra answers a question worth asking directly: when does AI leave workers worse off than no AI at all? Substituting the first line into the second, the wage rises if and only if

\[ \varepsilon > \varepsilon^*_t \approx \frac{1}{s_{L,t0}} \Big[ s_{K,t0} - \sigma + \psi_t \Big( \frac{1-\rho}{a_t} - (1-\sigma) \Big) \Big] \tag{12} \]

The threshold rises with the automation share \(\psi\) and with \((1-\rho)/a\), the displacement per unit of cost saving. That ratio is the formal version of Acemoglu and Restrepo’s “so-so automation”: technology that takes tasks away from labor while saving very little on each. When the saving per automated task is small, capital’s claim on output is large relative to the gain available to distribute, so the rental rate has to rise a lot to justify the transfer, and the wage ends up below its no-AI path.

Since \(\varepsilon^*\) is a first-order object, it is worth checking against the exact model, which the chunk below does by finding the elasticity at which the simulated 2030 average wage actually crosses zero.

the wage-sign threshold: first-order (12) against the exact model
# For each scenario, compare the paper's first-order threshold with the elasticity at
# which the *simulated* average wage actually changes sign.
rows = []
for n in ["substantial", "extreme"]:
    x = P[n].at(2030.0)                       # the 2030 values of the AI objects
    psi, rho, a = x["psi"], x["rho"], x["a"]

    # Equation (12), evaluated at those values.
    eps_star = (F.s_K0 - F.sigma + psi * ((1 - rho) / a - (1 - F.sigma))) / F.s_L0

    # The exact threshold: bisect on eps until the 2030 average wage gap is zero.
    # Each evaluation is a full simulation, so keep the iteration count small.
    def wage_gap(eps):
        return table3_column(simulate.run(Fixed(eps=eps), SCENARIOS[n]))[
            "Average wage, pct above no-AI"]

    lo, hi = 0.3, 6.0                          # wage is negative at lo, positive at hi
    for _ in range(12):                        # 12 halvings: enough for 2 decimals
        mid = 0.5 * (lo + hi)
        if wage_gap(mid) < 0:
            lo = mid
        else:
            hi = mid
    rows.append({"scenario": n, "first-order eps* (12)": eps_star,
                 "exact sign flip": 0.5 * (lo + hi),
                 "wage at eps = 1, pct": wage_gap(1.0)})
show(pd.DataFrame(rows), floatfmt=".2f")
scenario first-order eps* (12) exact sign flip wage at eps = 1, pct
substantial 1.30 1.53 -1.56
extreme 0.96 1.67 -9.22

The approximation locates the threshold well when the shock is small and poorly when it is large, which is the honest summary of every first-order row in the paper: in the substantial scenario \(\varepsilon^*\) misses the true crossing by about a fifth, in the extreme scenario it is off by a factor of nearly two. Table A.1 carries an exact and a first-order row for each variable and notes that the simulation runs the exact set. The spread between them is that same second-order error: negligible in the modest scenario, material in the extreme one.

Equations (11) and (19) against Proposition 1, at the 2030 values
rows = []
for n in NAMES:
    # .at(2030.0) returns a dict of the five exogenous objects at that date, so
    # x["md"] is m*d, x["a"] is the gain, and so on.
    x = P[n].at(2030.0)
    e = statics.frictionless(F, x["md"], x["a"], x["psi"], x["rho"])   # exact
    o = statics.first_order(F, x["md"], x["a"], x["psi"], x["rho"])    # approximate

    # Looping over a list of tuples and unpacking each into three names at once.
    # `label` is the row name, `ev` the exact value, `ov` the first-order one.
    for label, ev, ov in [("dln r", e.dlnr, o.dlnr), ("dln s_L", e.dln_sL, o.dln_sL),
                          ("dln w", e.dlnw, o.dlnw), ("dln Y/L", e.dlnYL, o.dlnYL),
                          ("ell~_N", e.ell_N_tilde, o.ell_N_tilde),
                          ("dln TFP", e.dln_tfp, o.dln_tfp)]:
        rows.append({"scenario": n, "variable": label, "exact": ev, "first order": ov,
                     "gap, pct of exact": 100 * (ov - ev) / ev})
fo_df = pd.DataFrame(rows)
show(fo_df, floatfmt=(".0f", "", "", ".4f", ".4f", ".1f"))
scenario variable exact first order gap, pct of exact
modest dln r 0.007618679857637288 0.0077 1.0103
modest dln s_L -0.009834294795568467 -0.0096 -2.7361
modest dln w 0.0060679760357280135 0.0069 13.2102
modest dln Y/L 0.01590227083129648 0.0164 3.3487
modest ell~_N 0.012868282813432474 0.0130 1.0236
modest dln TFP 0.00669721780747177 0.0072 7.5073
substantial dln r 0.04468398457115712 0.0449 0.4795
substantial dln s_L -0.06786954634209984 -0.0623 -8.1973
substantial dln w 0.01698772611578145 0.0238 40.2650
substantial dln Y/L 0.08485727245788129 0.0861 1.5045
substantial ell~_N 0.07636340939999056 0.0742 -2.8069
substantial dln TFP 0.029109436445869115 0.0323 10.8094
extreme dln r 0.1714362107536842 0.1683 -1.8522
extreme dln s_L -0.2968582475677361 -0.2181 -26.5350
extreme dln w 0.06298721369770377 0.1278 102.9397
extreme dln Y/L 0.35984546126543987 0.3459 -3.8718
extreme ell~_N 0.328351854416588 0.2820 -14.1165
extreme dln TFP 0.1223521300579683 0.1440 17.6931

2.1.5 Employment in the two groups

\[ \tilde \ell_{N,t} \equiv \ln \frac{\ell^*_{N,t}}{\ell_{N,t0}} = \Delta \ln (Y_t/L_t) - \sigma \Delta \ln w_t, \qquad \frac{\ell_{C,t0} - \ell^*_{C,t}}{\ell_{C,t0}} = \frac{s_{N,t0}}{s_{C,t0}} \cdot \frac{\ell^*_{N,t} - \ell_{N,t0}}{\ell_{N,t0}} \tag{13} \]

The appealing move here is to solve for the group that is unaffected. AI does nothing to the productivity of home health aides or electricians, so the cost of those tasks depends only on the wage, and demand for that labor is an ordinary CES demand curve for a good priced at \(w_t\): it rises one for one with output and falls with its own price at elasticity \(\sigma\). That is the first equation, derived in two lines from \(\ell_N = s_N Y / w\) with \(\Delta \ln s_N = (1-\sigma)\Delta \ln w\).

The second equation is pure adding up. The labor force is fixed, so every worker the “all other” group gains is one the cognitive group loses. Converting a proportional change in one group into a proportional change in the other requires the size ratio \(s_{N,t0}/s_{C,t0} =\) 0.60, which is below one because cognitive occupations are the larger group. So cognitive employment falls proportionally by less than all-other employment rises, and, since output rises by more still, by much less than GDP.

One wrinkle, worth flagging because it is the kind of thing that silently breaks a reimplementation. Once the ideas stock is endogenous, (13) as printed acquires a spurious term: \(A_t\) raises the wage and output one for one, so \(\Delta \ln(Y/L) - \sigma \Delta \ln w\) picks up an extra \((1-\sigma)\Delta \ln A_t\) that does not belong. Deriving \(\ell_N = s_N Y / w\) from scratch with \(A_t\) present gives \(\Delta \ln \ell_N = \Delta \ln Y - \sigma \Delta \ln w - (1-\sigma)\Delta \ln A\), and the exact row the simulation actually uses, Equation (15) below, has no ideas term at all. This code uses (15).

Proposition 1 collects the exact solution, replacing every \(\approx\) above with an equality:

\[ s_{L,t} = 1 - \underbrace{\Big[ s_{K,t0} + s_{L,t0}\, \psi_t m_t d_t \big( e^{-(1-\sigma)a_t} - \rho \big) \Big]}_{\textstyle B_t} \, e^{(1-\sigma)\Delta \ln r_t} \tag{14} \]

\[ \tilde \ell_{N,t} = -\ln \Big( 1 - m_t d_t \big[ 1 - \rho \psi_t - (1-\psi_t) e^{-(1-\sigma)a_t} \big] \Big) \tag{15} \]

\[ \Delta \ln w_t = \frac{\Delta \ln s_{L,t} + \tilde \ell_{N,t}}{1-\sigma} + \Delta \ln A_t, \qquad \Delta \ln (Y_t/L_t) = \Delta \ln w_t - \Delta \ln s_{L,t} \tag{16} \]

\[ \Delta \ln K_t = \ln \frac{1 - s_{L,t}}{s_{K,t0}} + \Delta \ln (Y_t/L_t) - \Delta \ln r_t \tag{17} \]

\[ \varepsilon \Delta \ln r_t = \Delta \ln K_t \tag{18} \]

(14), the labor share. Work with the capital share and subtract, since capital’s expenditure share is easier to track. At an unchanged rental rate it is the bracket \(B_t\): the base-period capital share, plus the wage bill of the instances automation just moved onto capital. That transferred bill is \(\psi_t m_t d_t\) of labor’s tasks, scaled by \(e^{-(1-\sigma)a_t}\) because those instances are now cheaper and so command a smaller share, and reduced by \(\rho\) for the new tasks handed back to labor. The factor \(e^{(1-\sigma)\Delta \ln r_t}\) then applies the same expenditure-share logic as Equation (4) to capital itself: when capital gets dearer and \(\sigma < 1\), spending tilts toward it.

(15), the shift in demand for unaffected labor. Everything sits in the bracket, which is the net fraction of affected instances that leave labor’s hands. Start from all of them, 1. Subtract \(\rho\psi_t\), the automated ones handed back as new labor tasks. Subtract \((1-\psi_t)e^{-(1-\sigma)a_t}\), the augmented ones: they stay with labor, but each needs \(a_t\) log points less labor time, and the \(e^{-(1-\sigma)a_t}\) records that only the fraction \(1-\sigma\) of the saved labor is actually released, since the cheaper task also gets demanded more. The outer \(-\ln(1-\cdot)\) converts that fraction into the log increase in demand for the other group’s labor.

(16), the wage. This is the one line that is not obvious, and it is two substitutions. Write (13) in ideas-deflated terms, \(\tilde \ell_{N} = \Delta\ln(Y/L) - \Delta \ln A - \sigma \Delta \ln \tilde w\), then replace \(\Delta \ln(Y/L)\) using the wage identity (5), \(\Delta\ln(Y/L) = \Delta \ln \tilde w + \Delta \ln A - \Delta \ln s_L\). The ideas terms cancel and what is left is \(\tilde \ell_{N} = (1-\sigma)\Delta \ln \tilde w - \Delta \ln s_L\). Solve for the wage and add \(\Delta \ln A\) back to get from the deflated wage to the actual one.

(17) and (18), the capital market. Capital demand is just \(K = s_{K,t} Y / r\) written in gaps. Setting it equal to the supply schedule (6) leaves one equation in one unknown, \(\Delta \ln r_t\), whose right side is strictly decreasing in the rental rate: demand for capital falls as capital gets more expensive. A monotone function with a sign change has exactly one root, which is why the code can solve it by bisection rather than anything cleverer, and why the paper can assert the equilibrium is unique. At \(\varepsilon = \infty\) the root is zero by construction.

Everything else follows in order once \(\Delta \ln r_t\) is known, which is what makes the model recursive within a month: no simultaneous system, just a sequence.

Expanding (15) to first order separates the two forces that push workers out of cognitive jobs:

\[ \tilde \ell_{N,t} \approx \underbrace{(1-\rho)\psi_t m_t d_t}_{\text{net displacement}} + \underbrace{(1-\sigma)(1-\psi_t) m_t d_t a_t}_{\text{labor released by the gains}} \tag{19} \]

The first term is tasks changing hands: automation moves them to capital, and reinstatement gives a fraction \(\rho\) back.

The second is subtler and easy to miss. Even a task that stays entirely with workers sheds labor if AI makes it faster: each augmented instance needs \(a_t\) log points less time. But because tasks are gross complements, the cheaper task is also demanded more, by \(\sigma a_t\), so only the fraction \(1 - \sigma\) of the saved time is actually released. At \(\sigma =\) 0.5, half of every efficiency gain is spent on doing more of the same task and half shows up as labor no longer needed there. Set \(\sigma = 1\) and this channel vanishes entirely.

Target employment in the unaffected group follows from Equation (15) and the cognitive target from the adding-up rule (13). Since \(s_{N} / s_{C}\) is 0.60, below one, cognitive employment falls proportionally by less than all-other employment rises, and therefore by less than output rises.

Equations (13), (15) and (19): the group targets in 2030
rows = []
for n in NAMES:
    x = P[n].at(2030.0)
    e = statics.frictionless(F, x["md"], x["a"], x["psi"], x["rho"])
    o = statics.first_order(F, x["md"], x["a"], x["psi"], x["rho"])
    rows.append({
        "scenario": n,
        "ell~_N, exact (15)": e.ell_N_tilde,
        "ell~_N, first order (19)": o.ell_N_tilde,
        "of which net displacement": (1 - x["rho"]) * x["psi"] * x["md"],
        "of which labor released by gains": (1 - F.sigma) * (1 - x["psi"]) * x["md"] * x["a"],
        "ell~_C, exact": math.log(e.l_C_star / F.l_C0),
        "ell~_C approx -(s_N/s_C) ell~_N": -(1 - F.cog_share) / F.cog_share * e.ell_N_tilde,
    })
show(pd.DataFrame(rows), floatfmt=".4f")
scenario ell~_N, exact (15) ell~_N, first order (19) of which net displacement of which labor released by gains ell~_C, exact ell~_C approx -(s_N/s_C) ell~_N
modest 0.0129 0.0130 0.0100 0.0030 -0.0078 -0.0078
substantial 0.0764 0.0742 0.0675 0.0067 -0.0490 -0.0460
extreme 0.3284 0.2820 0.2700 0.0120 -0.2668 -0.1979

2.2 Innovation

So far \(A_t\) has been held on its no-AI path. This section makes it semi-endogenous: AI raises GDP, a fixed share of GDP is spent on research, so AI buys more research.

\[ \dot A_t = \nu R_t^{\lambda} A_t^{\phi_R}, \qquad g_{A,t} = \nu R_t^{\lambda} A_t^{\phi_R - 1} \tag{20} \]

\[ Y_t = C_t + I_t + R_t, \quad R_t = \iota_{R,t} Y_t \tag{21} \]

This is the Romer (1990) idea-production function with the Jones (1995) correction, and the two exponents are the whole content.

\(\lambda \le 1\) governs returns to research input: below one, doubling the research effort less than doubles the flow of new ideas, because researchers duplicate each other. The paper sets \(\lambda =\) 1, no duplication.

\(\phi_R < 1\) is fishing out: the more ideas already found, the harder the next one is. This is the parameter that decides what kind of growth model you are in. At \(\phi_R = 1\) a constant research effort sustains constant growth forever, which is fully endogenous growth, and policy that permanently raises research permanently raises the growth rate. At \(\phi_R < 1\), holding growth constant requires research input to grow, and a one-off increase in research raises the level of the idea stock but not its long-run growth rate. That is the semi-endogenous world, and it is why AI shows up here as a level effect that fades from the growth rate.

Equation (21) is the “lab equipment” specification of Rivera-Batiz and Romer (1991): research uses final output rather than a separate stock of scientists. That choice is what makes the AI channel mechanical. AI raises \(Y\), the research budget is a fixed share of \(Y\), so the research budget rises without anyone deciding anything.

\[ \Delta \ln R_t = \Delta \ln Y_t \tag{22} \]

\[ \Delta g_t = g \Big[ e^{\lambda \Delta \ln R_t - (1-\phi_R)\Delta \ln A_t} - 1 \Big] \tag{42} \]

\[ \Delta \ln A_{t+1} \approx \Delta \ln A_t + h \Delta g_t \]

The last line is the monthly Euler step the simulation takes, not an equation of the paper: Table A.1 gives it as the ideas-stock row and attributes it to (42) together with the level effect it approximates. That level effect has a closed form, which Appendix C.2 states and Section 5 checks the monthly step against:

\[ \Delta \ln A_t = \frac{1}{1 - \phi_R} \ln \Big[ e^{-(1-\phi_R) g (t - t_0)} + (1-\phi_R)\, g \int_{t_0}^{t} e^{-(1-\phi_R) g (t-s)} e^{\lambda \Delta \ln R_s}\, ds \Big] \tag{43} \]

Equation (22) is where the research share cancels. Both the AI and the no-AI path spend the same fraction \(\iota_{R,t}\) of GDP on research, so in the gap between them only the GDP gap survives.

Equation (42) is (20) log-differenced against the no-AI path, and the thing to notice is the leading \(g\). The bracket is a proportional gap in the growth rate, and that rate is only 1.67 percent a year to begin with. So a 5 percent GDP gap does not add 5 points to growth; it adds roughly 5 percent of 1.67 percent, which is under a tenth of a point. The subtracted term \((1-\phi_R)\Delta \ln A_t\) is the drag: as the AI path accumulates ideas, the fishing out that (20) already contains makes the next ones harder, so the growth gap erodes as the level gap builds.

In the extreme scenario this channel moves idea growth from 1.67 to 2.03 percent a year, and the ideas stock ends 0.61 percent above the no-AI path after six years. Compare that with the level channel of Section 2.1, worth 32.4 percent of GDP. Over a six-year horizon the innovation block is a rounding error; over fifty years it would not be.

\[ \Delta \ln A^* = \gamma\, \Delta \ln R, \qquad \gamma \equiv \frac{\lambda}{1 - \phi_R} \tag{24} \]

\[ 1 - \phi_R = s_{L,t0}(1 - \phi) + \lambda \tag{40} \]

For \(\gamma\), set \(g_{A,t} = g\) in (20) and solve for the stock: \(A^* = (\nu R^{\lambda}/g)^{1/(1-\phi_R)}\), so \(A^* \propto R^{\lambda/(1-\phi_R)}\). The elasticity of the long-run level of ideas to a permanent research uplift is therefore \(\gamma = \lambda/(1-\phi_R) =\) 0.35. A permanent 1 percent more research eventually buys 0.35 percent more ideas, and “eventually” means decades.

Equation (40) is the bridge from Bloom et al. (2020), who estimate fishing out of \(1 - \phi = 3.1\), to the \(2.86\) this model needs. Two conversions, each worth following slowly because they are pure bookkeeping and easy to get backwards:

  1. Ideas units. Bloom et al. measure idea output in TFP units. Here \(A_t\) is labor-augmenting, and on a balanced path \(\ln \mathrm{TFP} = s_{L,t0} \ln A\), so measuring ideas by their effect on labor productivity instead of on TFP multiplies the exponent by \(s_{L,t0} =\) 0.60. That gives \(0.6 \times 3.1 =\) 1.86.
  2. Research units. They measure research input in researcher-equivalents (R&D spending deflated by the skilled wage); here \(R_t\) is goods. As the economy grows, a unit of goods buys fewer researcher-hours, and correcting for that adds \(\lambda\).

Sum: 1.86 + 1 = 2.86. The consequence is that keeping idea growth at \(g\) needs research input growing at \((1-\phi_R) g/\lambda =\) 4.8 percent a year while GDP grows at 2, so the research share of GDP has to climb, with or without AI, at

\[ g_\iota = g_R - (g + n) = \frac{(1-\phi_R)\,g}{\lambda} - (g + n) \tag{41} \]

which is 2.8 percent a year.

A permanent one percent uplift in research input raises the ideas stock by \(\gamma = \lambda / (1 - \phi_R)\), which is 1 / 2.86 = 0.35 percent, in the long run (Equation 24). Holding idea growth at \(g\) requires research input to grow at 4.8 percent a year against GDP growth of 2 percent, so the research share rises at 2.8 percent a year, from 3.5 percent of GDP in 2024 to 4.1 percent in 2030 (Appendix C.2).

The channel is small over this horizon. On the extreme path the research uplift reaches 0.28 log points by 2030, which points to an ideas stock 10 percent above the no-AI path in the long run, but the realized 2030 gap is only 0.61 percent because the cumulation takes decades.

Equations (22), (42) and (43): the ideas block in 2030
rows = []
for n in NAMES:
    res = RUNS[n]                 # the whole simulation for this scenario
    m = res.at(2030.0)            # one month's record, as an object with named fields
    rows.append({
        "scenario": n,
        "research uplift dln R": m.dlnR,
        "long-run target, gamma dln R": F.lam / F.fishing_out_R * m.dlnR,
        "realized dln A, monthly step (42)": m.dlnA,
        "realized dln A, closed form (43)": simulate.ideas_closed_form(res, 2030.0),
        "ideas growth, pct per year": D[n].iloc[-1].ideas_growth,
    })
show(pd.DataFrame(rows), floatfmt=".5f")
scenario research uplift dln R long-run target, gamma dln R realized dln A, monthly step (42) realized dln A, closed form (43) ideas growth, pct per year
modest 0.01593 0.00557 0.00065 0.00066 1.69037
substantial 0.07958 0.02783 0.00198 0.00203 1.76654
extreme 0.28082 0.09819 0.00606 0.00625 2.02712

2.2.2 Measured TFP

What an econometrician would measure is not \(A_t\) but the Solow residual, which mixes the ideas channel with the level channel of Section 2.1.

\[ g_{\mathrm{TFP},t} = s_{L,t} g_{w,t} + s_{K,t} g_{r,t} \tag{25} \]

\[ \Delta \ln \mathrm{TFP}_t \approx s_{L,t0} \big( \Delta \ln A_t + m_t d_t a_t \big) \tag{26} \]

\[ \Delta \ln \mathrm{TFP}_t \approx -\frac{1}{1-\sigma} \ln \Big[ s_{K,t0} + s_{L,t0} \big( 1 - m_t d_t (1 - e^{-(1-\sigma)a_t}) \big) e^{-(1-\sigma)\Delta \ln A_t} \Big] \tag{45} \]

Equation (25) is the dual from Section 2.1.3 again. Equation (26) is its first-order solution: both channels enter with the labor share as their Domar weight, because the ideas stock lowers the cost of every labor instance and the level channel lowers the cost of the affected ones.

Equation (45) is what the simulation actually evaluates. It is the CES price index of Equation (4) rebuilt at base-period factor prices: hold \(w\) and \(r\) at their no-AI values, let only the technology change, and ask how much cheaper the basket of tasks becomes. The \(-\frac{1}{1-\sigma}\ln[\cdot]\) out front is just inverting the CES aggregator. Inside, \(m_t d_t (1 - e^{-(1-\sigma)a_t})\) is the share of labor’s base-period cost the AI gains remove, and \(e^{-(1-\sigma)\Delta \ln A_t}\) scales every labor instance for the ideas stock.

The three agree to first order and diverge beyond it, because (45) is a base-weighted index while (25) chains its weights month by month. The verification section below quantifies that wedge: it is 0.001 percentage points in the modest scenario and grows with the size of the shock, exactly as an index-number problem should.

2.3 Unemployment

Sections 2.1 and 2.2 describe where employment should end up. This section is about the fact that getting there takes time, and that the transition is what unemployment is. The whole block runs on a monthly grid.

\[ q_{o,t} = q^X_o + q^T_o \frac{f_{o,t-1}}{\bar f_o} \tag{27} \]

\[ G_{C,t} = \max\{0, \ln \ell_{C,t} - \ln \ell^*_{C,t+1}\}, \qquad B_{N,t} = \max\{0, \ln \ell^*_{N,t+1} - \ln \ell_{N,t}\} \tag{28} \]

\[ N_{C,t} = \ell_{C,t} + \max\{0, U_{C,t} - \bar U_C\} \tag{29} \]

\[ \frac{w_{C,t}}{w_t} = \Big(\frac{w_{C,t-1}}{w_{t-1}}\Big)^{\xi_m} \Big(\frac{w^c_{C,t}}{w_t}\Big)^{1-\xi_m}, \quad \xi_m = \xi^{1/12} \tag{30} \]

\[ D_{C,t} = \max\{0, E_t - q_{C,t}\ell_{C,t}\}, \qquad E_t = \max\{0, \ell_{C,t} - \ell^d_{C,t}\} \tag{31} \]

(27) Quits. Workers quit at a base rate plus a part that moves with how good prospects look, proxied by last month’s job-finding rate relative to normal. In normal times \(f = \bar f\) and the quit rate is just \(\bar q_o\). The paper splits it 45/55 using the JOLTS-to-CPS elasticity. This is a small piece of realism, not a driver: quits matter because they let a shrinking group shed workers without laying anyone off.

(28) The gaps. Each month firms compare where they are with next month’s target from Equation (13). On these scenario paths the cognitive group is always above its target (an overhang, \(G_C\)) and the other group always below (a shortfall, \(B_N\)), so the two are tracked separately and the maxima never bind on the other side.

(29) The attached force. A cognitive worker laid off last month is still a cognitive worker looking for cognitive work, so the group’s effective labor force is its employment plus its own unemployed in excess of the normal pool. This is the quantity the group’s wage has to clear.

(30) The rigid wage, in the Blanchard and Galí (2007) form. In logs this is partial adjustment: the gap between the wage actually paid and the wage that would clear the group closes by a fraction \(1 - \xi\) each year, and by \(1 - \xi^{1/12}\) each month, so \(\xi^{1/12}\) of the gap carries over from one month to the next. Two details are easy to misread. First, what is rigid is the cognitive discount to the common wage, \(\ln(w_{C,t}/w_t)\), not the wage level, so the cognitive wage still inherits economy-wide productivity growth. Second, \(\xi\) is a persistence, so higher means more rigid: at \(\xi = 0\) the group clears every month, and as \(\xi \to 1\) the cognitive wage never separates from the common wage at all. The baseline \(\xi =\) 0.5 is a half-life of one year.

(31) Layoffs. Here is the piece that a pure Mortensen-Pissarides model does not have. With the wage stuck above its clearing level, firms want fewer cognitive workers than are attached to the group. Employment is predetermined within the month, so they get there by separating: quits from surplus positions go unreplaced, and layoffs \(D_{C,t}\) remove the rest. This is job rationing in the sense of Michaillat (2012): the unemployment is not only frictional, some of it exists because the wage is too high and the jobs simply are not offered. It is also the mechanism behind the Table 6 trade-off: rigid wages convert the shock into unemployment, flexible wages convert it into wage declines.

\[ v_{C,t} = \frac{\max\{0, q_{C,t}\ell_{C,t} - E_t\} + \theta_H Z_t}{\bar \pi_C}, \qquad v_{N,t} = \frac{(q_{N,t} + \theta_H B_{N,t})\, \ell_{N,t}}{\bar \pi_N} \tag{32} \]

\[ S_{C,t} = U_{C,t} + \mu U_{N,t}, \qquad S_{N,t} = \mu U_{C,t} + U_{N,t} \tag{33} \]

\[ H_{j,t} = \chi \frac{S_{j,t} v_{j,t}}{\big(S_{j,t}^{\iota} + v_{j,t}^{\iota}\big)^{1/\iota}}, \qquad \pi_{j,t} = \frac{H_{j,t}}{v_{j,t}} = \chi \big(1 + \theta_{j,t}^{\iota}\big)^{-1/\iota}, \quad \theta_{j,t} \equiv \frac{v_{j,t}}{S_{j,t}} \tag{34} \]

\[ f_{C,t} = \frac{H_{C,t}}{S_{C,t}} + \mu \frac{H_{N,t}}{S_{N,t}}, \qquad f_{N,t} = \mu \frac{H_{C,t}}{S_{C,t}} + \frac{H_{N,t}}{S_{N,t}} \tag{35} \]

(32) Postings. Firms post to replace quits and to close a fraction \(\theta_H\) of their shortfall. The division by the normal filling rate \(\bar \pi_o\) is a unit conversion that is easy to skip past: a firm that wants ten hires this month must post more than ten vacancies, because only \(\bar\pi_o \approx\) 0.65 of postings fill within the month. \(\theta_H\) is the recruiting-and-training speed limit: at \(\theta_H = 1\) the expanding group posts its entire shortfall at once, at 0.10 it closes a tenth of the gap per month.

(33) Effective search, and where occupational specificity lives. A worker supplies one unit of search in their own group and only \(\mu \le 1\) units in the other. This one parameter carries all of the occupational human capital in the model: a laid-off paralegal is not instantly an electrician (Kambourov and Manovskii, 2009). Normal times give \(\bar\mu =\) 0.17, estimated from CPS switching, and the disruptive scenarios lower it to 0.08 and 0.04 on the reasoning that crossing is harder when a whole occupation tries at once.

(34) The matching function. The workhorse Cobb-Douglas matching function \(H = \chi S^{\alpha} v^{1-\alpha}\) has a defect that matters here: for extreme ratios of searchers to vacancies it can return more hires than there are searchers or vacancies, so it has to be capped, and the cap puts kinks in simulated paths. The den Haan, Ramey and Watson (2000) form used here is a CES-style aggregator that satisfies \(H \le \min\{S, v\}\) automatically, with \(\iota =\) 1.27 controlling the curvature and \(\iota \to \infty\) giving exactly \(\chi \min\{S, v\}\). It has constant returns, so the filling rate depends only on tightness \(\theta = v/S\), the standard result. This matters in the extreme scenario precisely because the ratios do get extreme.

(35) Finding rates. A group hires \(H_j/S_j\) per unit of effective search directed at it, and a worker supplies 1 unit at home and \(\mu\) away, so their finding rate is the weighted sum. This is why a displaced cognitive worker’s prospects depend on how hard the other group is hiring, and why \(\mu\) and \(\theta_H\) are the two parameters that decide how much unemployment a given amount of reallocation causes.

\[ \ell_{o,t+1} = (1 - q_{o,t})\ell_{o,t} - D_{o,t} + H_{o,t} \tag{36} \]

\[ U_{o,t+1} = U_{o,t} + q_{o,t}\ell_{o,t} + D_{o,t} - f_{o,t}U_{o,t} \tag{37} \]

\[ \bar H_o = \bar q_o \ell_{o,t0} = \bar f_o \bar U_o, \qquad \bar f_o = \sum_j \bar\mu_{oj} \frac{\bar H_j}{\bar S_j}, \qquad \bar \pi_o = \chi \Big[ 1 - \Big(\frac{\bar H_o}{\chi \bar S_o}\Big)^{\iota}\Big]^{1/\iota} \tag{38} \]

(36) and (37) are bookkeeping: employment loses quits and layoffs and gains hires; the pool gains the separations and loses the job-finders. Every separation enters the pool and every hire leaves it, so \(\ell_C + \ell_N + U_C + U_N\) is constant. The verification section checks that this holds to machine precision, which is a genuine test of the code rather than of the model.

(38) solves for the labor market before AI arrives, and it is worth seeing what does the pinning. Hiring replaces quits, so \(\bar H_o = \bar q_o \ell_{o,t0}\). Each origin’s pool is then whatever makes its inflow equal its outflow, \(\bar f_o \bar U_o = \bar H_o\), with the finding rates themselves depending on the pools through effective search: one equation in the split of the pool, plus adding up. Inverting the matching function at that point gives the normal filling rates that the posting rule (32) needs, and the one free parameter \(\chi\) is set so the employment-weighted mean filling rate matches the JOLTS-based 0.65.

Nothing in this block is fitted to anything about AI, so the numbers it produces are a clean test of the transcription. The steady state gives filling rates of 0.659 and 0.635 against the paper’s 0.66 and 0.64 (the second reaches 0.64 at \(\bar U = 0.0384\), the pool rounding discussed with Table 3), matching efficiency 0.76, aggregate finding rate 0.23, and the calibration target that one job-finder in seven changes group, here 0.144.

Finally, the model has to report GDP, wages and the labor share at the employment the flow block actually delivers, which is not the frictionless allocation of Proposition 1. That is the system (39):

\[ \begin{aligned} s_{L,t0}\Lambda_{C,t} e^{(1-\sigma)\Delta \ln w_{C,t}} &+ s_{N,t0} e^{(1-\sigma)\Delta \ln w_{N,t}} + B_t e^{(1-\sigma)\Delta \ln r_t} = 1 \\[2pt] \frac{\ell_{C,t}}{\ell_{C,t0}} &= \frac{\Lambda_{C,t}}{s_{C,t0}/s_{L,t0}} e^{\Delta \ln (Y_t/\bar L) - \sigma \Delta \ln w_{C,t}}, \qquad \frac{\ell_{N,t}}{\ell_{N,t0}} = e^{\Delta \ln (Y_t/\bar L) - \sigma \Delta \ln w_{N,t}} \\[2pt] \Delta \ln K_t &= \varepsilon \Delta \ln r_t \end{aligned} \tag{39} \]

The first row is the price index of Equation (4) with the tasks grouped three ways: surviving cognitive instances, all-other instances, and capital’s instances. \(\Lambda_{C,t}\) is the surviving cognitive mass, the same bracket as Equation (15) subtracted from the group’s base share. The next two rows are the CES labor demands of Equation (13), one per group. The last is the capital supply schedule.

The logic runs backwards from the usual direction. Ordinarily you would set prices and solve for quantities. Here employment is a state variable handed over by the flow block, so the system is inverted: find the prices at which the observed \((\ell_{C,t}, \ell_{N,t})\) is exactly what firms want to hire, and GDP follows. Running the demand system backwards this way is legitimate because the same equations hold either way; here the quantities are the given and the prices the unknown. That has a useful consequence for solving it: the capital row can be substituted out in closed form, leaving one monotone equation in \(\Delta \ln r_t\), which is why a bisection is all the code needs. When the cognitive wage is rationed the price the system returns is the marginal product rather than the wage paid, and the difference is cognitive firms’ profit, which the paper bounds at half a percent of GDP.

The check that this reading is right: evaluated at the targets rather than at realized employment, the system must collapse back to Proposition 1, with both group wages equal to the common wage. It does, to machine precision, in every scenario and with or without an ideas gap; the verification section below reports the largest violation across all the variables.

The flow block starts from the steady state of Equation (38), evaluated at the normal-times search discount in every scenario. Nothing in it is fitted to the AI paths, so it is an independent check on the transcription: the paper reports filling rates of 0.66 and 0.64, a matching efficiency of 0.76, an aggregate finding rate of 0.23 a month, and a pool splitting into 1.76 and 2.08 percent of the labor force.

Table A.1 panel E: the normal-times steady state
# Fixed(U_bar=0.0384) makes a *copy* of the parameter set with one field changed;
# the original F is untouched, because the dataclass is frozen (immutable). This is
# the safe way to ask "what if this one number were different?".
ss_alt = steady.solve(Fixed(U_bar=0.0384))     # the pool the p. 26 derivation uses
rows = [
    ("quit rate, cognitive, pct per month", 100 * SS.q_C, "0.63"),
    ("quit rate, all other, pct per month", 100 * SS.q_N, "1.40"),
    ("pool of cognitive origin, pct of L", 100 * ss_alt.U_C, "1.76"),
    ("pool of all-other origin, pct of L", 100 * ss_alt.U_N, "2.08"),
    ("unemployment rate, cognitive group, pct", 100 * ss_alt.U_C / (ss_alt.U_C + F.l_C0), "2.9"),
    ("unemployment rate, all-other group, pct", 100 * ss_alt.U_N / (ss_alt.U_N + F.l_N0), "5.4"),
    ("aggregate finding rate, per month", SS.f_agg, "0.23"),
    ("finding rate, cognitive origin", SS.f_C, "not reported"),
    ("finding rate, all-other origin", SS.f_N, "not reported"),
    ("filling rate, cognitive", SS.pi_C, "0.66"),
    ("filling rate, all other", SS.pi_N, "0.64"),
    ("matching efficiency chi", SS.chi, "0.76"),
    ("share of job-finders changing group", SS.switch_share, "1/7 = 0.143"),
]
show(pd.DataFrame({"object": [r[0] for r in rows],
                   "this reproduction": [f"{r[1]:.4g}" for r in rows],
                   "paper": [r[2] for r in rows]}))
object this reproduction paper
quit rate, cognitive, pct per month 0.6345 0.63
quit rate, all other, pct per month 1.403 1.40
pool of cognitive origin, pct of L 1.759 1.76
pool of all-other origin, pct of L 2.081 2.08
unemployment rate, cognitive group, pct 2.847 2.9
unemployment rate, all-other group, pct 5.44 5.4
aggregate finding rate, per month 0.2338 0.23
finding rate, cognitive origin 0.2188 not reported
finding rate, all-other origin 0.2465 not reported
filling rate, cognitive 0.6591 0.66
filling rate, all other 0.6349 0.64
matching efficiency chi 0.7585 0.76
share of job-finders changing group 0.1436 1/7 = 0.143

Two calibration steps behind that table reproduce directly. The quit rate comes from the pool and the CPS finding rate: \(0.219 \times 3.84 / 96.16 =\) 0.875 percent a month, or 0.105 a year, which the paper rounds to 0.11. And the search discount is the geometric mean of the two CPS switching odds, \(\sqrt{(19/81)(11/89)} =\) 0.17, the \(\bar\mu = 0.17\) of Table 1.

3 Calibration

Table 1 of the paper, rebuilt from the parameter objects the simulation actually consumes. The scenarios differ in how fast AI advances (panel B) and in how disruptive a given amount of AI is (panel C).

Table 1: parameters and scenario values
S = SCENARIOS
t1 = [
    ("A", "sigma, elasticity across tasks", F.sigma, F.sigma, F.sigma),
    ("A", "s_L,t0, base-period labor share", F.s_L0, F.s_L0, F.s_L0),
    ("A", "s_C,t0/s_L,t0, cognitive employment share", F.cog_share, F.cog_share, F.cog_share),
    ("A", "eps, elasticity of capital supply", F.eps, F.eps, F.eps),
    ("A", "lambda, returns to research input", F.lam, F.lam, F.lam),
    ("A", "1 - phi_R, fishing out", F.fishing_out_R, F.fishing_out_R, F.fishing_out_R),
    ("A", "g, no-AI growth of the ideas stock", F.g, F.g, F.g),
    ("A", "n, labor-force growth", F.n, F.n, F.n),
    ("A", "iota_R,t0; g_iota, research share and its trend", F.iota_R0, F.g_iota, float("nan")),
    # The * before a list "unpacks" it: *[a, b, c] passes three separate arguments
    # rather than one list, so each scenario's value lands in its own column.
    ("B", "m, affected mass, mid-2026", *[S[n].m_anchor for n in NAMES]),
    ("B", "m_2030, affected mass", *[S[n].m_2030 for n in NAMES]),
    ("B", "m_bar, ceiling on affected mass", *[F.cog_share] * 3),
    ("B", "d, diffusion share, mid-2026", *[S[n].d_anchor for n in NAMES]),
    ("B", "d_2030, diffusion share", *[S[n].d_2030 for n in NAMES]),
    ("B", "a, log gain, mid-2026", *[S[n].a_anchor for n in NAMES]),
    ("B", "g_a, slope of the gain, per year", *[S[n].g_a for n in NAMES]),
    ("B", "a_2030, implied log gain", *[P[n].a(2030.0) for n in NAMES]),
    ("C", "psi, automation share", *[S[n].psi for n in NAMES]),
    ("C", "rho, reinstatement ratio", *[S[n].rho for n in NAMES]),
    ("C", "mu, search discount on the path", *[S[n].mu for n in NAMES]),
    ("C", "theta_H, posting speed, per month", *[S[n].theta_H for n in NAMES]),
    ("C", "xi, rigidity of the cognitive wage", F.xi, F.xi, F.xi),
    ("D", "q_bar, normal quit rate, per year", *[F.q_bar_ann] * 3),
    ("D", "q^T_o / q_bar_o, responsive share of quits", *[F.q_resp_share] * 3),
    ("D", "q_bar_C / q_bar; q_bar_N / q_bar", F.q_rel_C, F.q_rel_N, float("nan")),
    ("D", "U_bar, normal pool, share of L", *[F.U_bar] * 3),
    ("D", "mu_bar, normal-times search discount", *[F.mu_bar] * 3),
    ("D", "iota, matching curvature", *[F.iota_match] * 3),
    ("D", "mean filling rate, per month", *[F.fill_bar] * 3),
]
t1df = pd.DataFrame(t1, columns=["panel", "symbol and meaning", "modest", "substantial", "extreme"])

# .map() applies a function to every cell of a column. `lambda x: ...` is an
# anonymous one-line function: here it returns "" for missing values and a formatted
# number otherwise, so the two rows that span the scenario columns print blanks
# rather than the string "nan".
for c in ["modest", "substantial", "extreme"]:
    t1df[c] = t1df[c].map(lambda x: "" if pd.isna(x) else f"{x:.4g}")
show(t1df)
panel symbol and meaning modest substantial extreme
A sigma, elasticity across tasks 0.5 0.5 0.5
A s_L,t0, base-period labor share 0.6 0.6 0.6
A s_C,t0/s_L,t0, cognitive employment share 0.624 0.624 0.624
A eps, elasticity of capital supply 3 3 3
A lambda, returns to research input 1 1 1
A 1 - phi_R, fishing out 2.86 2.86 2.86
A g, no-AI growth of the ideas stock 0.0167 0.0167 0.0167
A n, labor-force growth 0.0033 0.0033 0.0033
A iota_R,t0; g_iota, research share and its trend 0.035 0.028
B m, affected mass, mid-2026 0.14 0.14 0.14
B m_2030, affected mass 0.2 0.3 0.5
B m_bar, ceiling on affected mass 0.624 0.624 0.624
B d, diffusion share, mid-2026 0.1 0.1 0.1
B d_2030, diffusion share 0.2 0.4 0.6
B a, log gain, mid-2026 0.3 0.35 0.45
B g_a, slope of the gain, per year 0 0.028 0.1
B a_2030, implied log gain 0.3 0.448 0.8
C psi, automation share 0.5 0.75 0.9
C rho, reinstatement ratio 0.5 0.25 0
C mu, search discount on the path 0.17 0.08 0.04
C theta_H, posting speed, per month 0.1 0.25 0.5
C xi, rigidity of the cognitive wage 0.5 0.5 0.5
D q_bar, normal quit rate, per year 0.11 0.11 0.11
D q^T_o / q_bar_o, responsive share of quits 0.55 0.55 0.55
D q_bar_C / q_bar; q_bar_N / q_bar 0.69 1.52
D U_bar, normal pool, share of L 0.038 0.038 0.038
D mu_bar, normal-times search discount 0.17 0.17 0.17
D iota, matching curvature 1.27 1.27 1.27
D mean filling rate, per month 0.65 0.65 0.65

Note on the two rows that carry two numbers rather than three: the research share and its trend, and the two groups’ relative separation rates, are single values in the paper spanning the scenario columns.

Every input, by origin

Table 1 says where each number comes from in prose, scattered down a “source or basis” column. This section is not in the paper: it re-sorts the same information by how much evidence stands behind each input, which is the question a reader who wants to argue with the results has to answer first. The three labels are the ones the Open Policy Analysis guidelines use.

  • data: read off a public dataset, and in principle recomputable from it.
  • research: an estimate taken from a published paper, or a received conventional value.
  • guesswork: set by assumption. Some are bounded by evidence, and the basis column says so, but no dataset or estimate pins the value itself.

Two further labels are needed for things that are not inputs at all: derived, for values another row implies, and convention, for the dates and the period length.

The headline is short. All seven scenario dials (everything that differs between modest, substantial and extreme, and therefore everything that produces the spread in the results) are guesswork. Apart from the gain’s mid-2026 anchor, which also differs by scenario, and two further assumptions (the returns to research \(\lambda\) and labor-force growth \(n\)), everything they are measured against is data or research.

every model input, classified by the evidence behind it
# Values come from the parameter objects, so this table cannot drift from the model;
# the origin, basis and where columns are editorial, read off Table 1, Table A.2 and
# Section 3 of the paper. "where" says where in the paper each value is stated and, for
# a data row, where the underlying series lives; "Table 1 cites X without a location"
# means the paper names the source paper but not a table or page in it.
def three(attr):
    """The three scenarios' values for one Scenario field, as 'a / b / c'."""
    return " / ".join(f"{getattr(SCENARIOS[n], attr):g}" for n in NAMES)

SET = "Set by the authors, September 2026: "
origins = [
    # --- the seven dials: everything that separates the scenarios
    ("m_2030", "affected mass in 2030", three("m_2030"), "guesswork",
     "Scenario values. The range spans the low end to near the high end of the task "
     "feasibility ratings of Eloundou et al. (2024); the points inside it are chosen.",
     SET + "Table 1 panel B, p. 23; Section 3.3, p. 27. Table 1 cites Eloundou et al. "
     "(2024) without a location."),
    ("d_2030", "diffusion share in 2030", three("d_2030"), "guesswork",
     "Scenario values. Table 1 gives no basis beyond 'scenario assumptions'.",
     SET + "Table 1 panel B, p. 24; Section 3.3, p. 27. No basis given."),
    ("a_2030", "log gain per AI-performed instance, 2030",
     " / ".join(f"{P[n].a(F.t_target):.2f}" for n in NAMES), "guesswork",
     "Scenario values, implied by the mid-2026 anchor and the slope g_a. Informed by "
     "measured trial gains (Brynjolfsson et al. 2025; Huang et al. 2025; Demirer et al. 2026).",
     SET + "Table 1 panel B (the g_a row), p. 24; Section 3.3, p. 27. Table 1 cites the "
     "three trial papers without a location."),
    ("psi", "automation share of AI use", three("psi"), "guesswork",
     "Scenario values. The low end is similar to the observed automation-like share of "
     "use (Appel et al. 2025); 0.75 and 0.90 are assumed.",
     SET + "Table 1 panel C, p. 24; Section 3.4, pp. 27-28. Table 1 cites Appel et al. "
     "(2025) without a location."),
    ("rho", "reinstatement ratio", three("rho"), "guesswork",
     "Scenario values. Acemoglu and Restrepo (2019) estimate 0.5 for past technologies, "
     "which is the modest value; nothing measures it for AI.",
     SET + "Table 1 panel C, p. 24; Section 3.4, p. 28. Table 1 cites Acemoglu and "
     "Restrepo (2019) without a location."),
    ("mu", "cross-group search discount on the path", three("mu"), "guesswork",
     "Scenario values. Only the normal-times value below is measured; how much harder "
     "reallocation becomes under the shock is assumed.",
     SET + "Table 1 panel C, p. 24; Section 3.4, p. 28 (the 0.09 fitted to managers "
     "and professionals is the stated reason for 0.08 and 0.04)."),
    ("theta_H", "share of the hiring shortfall posted per month", three("theta_H"),
     "guesswork", "Scenario values. Table 1 gives no basis.",
     SET + "Table 1 panel C, p. 24; Section 3.4, p. 28. No basis given."),
    # --- the anchors the dials start from
    ("m_2026", "affected mass, mid-2026 anchor", f"{SCENARIOS['substantial'].m_anchor:g}", "research",
     "Observed-exposure measure of Massenkoff and McCrory (2026): occupations scored by "
     "the share of task time on tasks with significant work-related Claude use. Common "
     "to the three scenarios.",
     "Table 1 panel B, p. 23; Section 3.1, p. 25, and Section 3.3, p. 27 (0.22 in the "
     "AI-sensitive group, 0.01 in the other). Table 1 cites Massenkoff and McCrory (2026) "
     "without a location."),
    ("d_2026", "diffusion share, mid-2026 anchor", f"{SCENARIOS['substantial'].d_anchor:g}", "data",
     "Census Business Trends and Outlook Survey and its 2026 AI supplement "
     "(https://www.census.gov/hfp/btos/). Common to the three scenarios.",
     "Table 1 panel B, p. 24; Section 3.3, p. 27 (18 percent of firms, 32 percent "
     "employment-weighted, set to 0.10). Series: BTOS AI supplement, "
     "https://www.census.gov/hfp/btos/."),
    ("a_2026", "log gain, mid-2026 anchor", three("a_anchor"), "guesswork",
     "Scenario values: unlike m and d, the gain's anchor already differs by scenario.",
     SET + "Table 1 panel B, p. 24; Section 3.3, p. 27. Table 1 cites the three trial "
     "papers without a location."),
    # --- technology and factor markets
    ("sigma", "elasticity of substitution across tasks", f"{F.sigma:g}", "research",
     "Gross complements: Acemoglu and Restrepo (2022), Humlum (2019), Jones and Tonetti (2026).",
     "Table 1 panel A, p. 23; Section 3.1, p. 25. Table 1 cites the three papers without "
     "a location."),
    ("s_L,t0", "base-period labor share", f"{F.s_L0:g}", "research",
     "Conventional US value; Table 1 cites no series.",
     "Table 1 panel A, p. 23; Section 3.1, p. 25. No source named."),
    ("s_C,t0/s_L,t0", "AI-sensitive share of employment", f"{F.cog_share:g}", "data",
     "CPS 2025 annual averages (https://www.bls.gov/cps/): employees in SOC major "
     "groups 11-29, 41 and 43. This is also the ceiling m_bar on the affected mass.",
     "Table 1 panel A, p. 23; Section 3.1, p. 25, footnote 10 (the SOC list); Table A.2 "
     "panel B, p. 44. Series: CPS 2025 annual averages, Table 11, "
     "https://www.bls.gov/cps/."),
    ("eps", "elasticity of capital supply", f"{F.eps:g}", "research",
     "Half the long-run wealth elasticity of Moll, Rachel and Restrepo (2022); the "
     "halving is the authors' judgement about the horizon. Varied over 1 / 3 / 6 / inf "
     "in Table 5, which is where its weight shows.",
     "Table 1 panel A, p. 23; Section 3.1, p. 26 (the reasoning for a high value). "
     "Table 1 cites Moll et al. (2022) without a location."),
    ("r_bar, delta", "no-AI rental rate and depreciation",
     f"{F.r_bar:g}, {F.delta:g}", "research",
     "Moll et al. (2022): a 6.5 percent net return at a capital-output ratio of 3.5.",
     "Table 1 panel A, p. 23; Table A.2 panel A, p. 44. Table 1 cites Moll et al. (2022) "
     "without a location."),
    ("xi", "rigidity of the AI-sensitive wage, per year", f"{F.xi:g}", "research",
     "Wage-setting evidence, Section 3.4. Varied over 0 / 0.5 / 0.75 / 0.9 in Table 6.",
     "Table 1 panel C, p. 24; Section 3.4, p. 28 (the wage-setting evidence and the "
     "half-life argument); Table A.2 panel D, p. 44."),
    # --- ideas production
    ("lambda", "returns to research input", f"{F.lam:g}", "guesswork",
     "Set to 1, no duplication in research. A normalization rather than an estimate.",
     SET + "Table 1 panel A, p. 23; Section 3.1, p. 26; Table A.2 panel C, p. 44."),
    ("1 - phi_R", "fishing out, labor-augmenting units", f"{F.fishing_out_R:g}", "derived",
     "s_L,t0 (1 - phi) + lambda, Equation (40), from the TFP-units value 3.1 of "
     "Bloom et al. (2020), which is itself research.",
     "Table 1 panel A, p. 23 (both rows, Equation (40)); Section 3.1, p. 26; Table A.2 "
     "panel C, p. 44. Table 1 cites Bloom et al. (2020) without a location."),
    ("g", "no-AI growth of the ideas stock, per year", f"{F.g:g}", "data",
     "Postwar US TFP growth, implying measured TFP growth of s_L,t0 g = 1 percent. "
     "Table 1 cites no series.",
     "Table 1 panel A, p. 23; Section 3.1, p. 26; Table A.2 panel C, p. 44. No series "
     "named."),
    ("n", "labor-force growth, per year", f"{F.n:g}", "guesswork",
     "Chosen so that GDP grows at 2 percent a year without AI. Table 1 notes BLS "
     "projections put labor-force growth near 0.4 percent, so the value is close to "
     "measured but is set by the target.",
     SET + "Table 1 panel A, p. 23; Table A.2 panel C, p. 44. The BLS projection is "
     "cited without a location."),
    ("iota_R,t0", "research share of GDP in 2024", f"{F.iota_R0:g}", "data",
     "R&D share in the US national accounts.",
     "Table 1 panel A, p. 23; Section 3.1, p. 26; Table A.2 panel C, p. 44. No series "
     "named."),
    ("g_iota", "growth of the research share, per year", f"{F.g_iota:g}", "derived",
     "Equation (41), set so the ideas stock grows at g without AI (iota_R,2030 = 0.041).",
     "Table 1 panel A, p. 23 (Equation (41), Appendix C); Table A.2 panel C, p. 44."),
    # --- the labor market in normal times
    ("U_bar", "normal search pool, share of the labor force", f"{F.U_bar:g}", "data",
     "CPS 2025. See the rounding discrepancy in Sections 2.3 and 4: the p. 26 derivation and the "
     "published pool split both imply 0.0384 rather than Table 1's 0.038.",
     "Table 1 panel D, p. 25; Section 3.2, p. 26 (3.84 in the quit-rate derivation); "
     "Table A.2 panel D, p. 44. Series: CPS 2025 annual averages, Table 25b, "
     "https://www.bls.gov/cps/, and UNEMPLOY on FRED, https://fred.stlouisfed.org/."),
    ("q_bar", "normal quit rate, per year", f"{F.q_bar_ann:g}", "data",
     "Implied by the pool and the CPS finding rate, rounded (Section 3.2).",
     "Table 1 panel D, p. 25; Section 3.2, p. 26 (0.219 x 3.84/96.16 = 0.875 percent a "
     "month, rounded to 0.11 a year); Table A.2 panel D, p. 44. Series: the CPS finding "
     "rate from the IPUMS-CPS matched files 2010-19, https://cps.ipums.org/cps/."),
    ("q^T_o / q_bar_o", "share of quits responding to job prospects",
     f"{F.q_resp_share:g}", "research",
     "Elasticity of JOLTS quits to the CPS finding rate, estimated at 0.53 and rounded "
     "to 0.55 (Section 3.2).",
     "Table 1 panel D, p. 25; Section 3.2, p. 27 (the 0.53 elasticity over 2001-19). "
     "Series: JOLTS quits rate JTSQUR on FRED, https://fred.stlouisfed.org/, and the CPS "
     "finding rate."),
    ("q_bar_C/q_bar, q_bar_N/q_bar", "relative separation rates by group",
     f"{F.q_rel_C:g}, {F.q_rel_N:g}", "data",
     "IPUMS-CPS matched monthly files 2010-19 (https://cps.ipums.org/cps/).",
     "Table 1 panel D, p. 25; Section 3.2, p. 26 (0.84 and 1.84 percent a month become "
     "unemployed; the calibration uses their ratio). Series: IPUMS-CPS matched monthly "
     "files 2010-19, https://cps.ipums.org/cps/."),
    ("mu_bar", "cross-group search discount in normal times", f"{F.mu_bar:g}", "data",
     "CPS 2010-19 occupational switching matrix from the Carrillo-Tudela and Visschers "
     "(2023) replication files, corrected by their method (Section 3.2).",
     "Table 1 panel D, p. 25; Section 3.2, pp. 26-27 (19 and 11 percent cross, "
     "sqrt(0.23 x 0.12) = 0.17); Table A.2 panel D, p. 44. Files: Carrillo-Tudela and "
     "Visschers (2023), Econometrica 91(3), supplementary material at "
     "https://doi.org/10.3982/ECTA12498."),
    ("iota", "matching curvature", f"{F.iota_match:g}", "research",
     "den Haan, Ramey and Watson (2000).",
     "Table 1 panel D, p. 25; Table A.2 panel D, p. 44. Table 1 cites den Haan, Ramey and "
     "Watson (2000) without a location."),
    ("mean filling rate", "employment-weighted filling rate, per month",
     f"{F.fill_bar:g}", "data",
     "JOLTS 2010-19 (https://www.bls.gov/jlt/), by the method of Davis et al. (2013). "
     "The matching efficiency chi is then calibrated to hit it, so chi is derived.",
     "Table 1 panel D, p. 25; Section 3.2, p. 27; Table A.2 panel D, p. 44. Series: JOLTS "
     "2010-19, https://www.bls.gov/jlt/. Table 1 cites Davis et al. (2013) without a "
     "location."),
    # --- conventions
    ("t0, t_anchor, t_target", "base period, anchor, scenario date",
     f"{F.t0:g}, {F.t_anchor:g}, {F.t_target:g}", "convention",
     "Dates, not estimates.",
     "Table 1 panel A, p. 23; Table A.2 panel A, p. 44."),
    ("h", "period length, years", "1/12", "convention", "A monthly grid, Appendix A.",
     "Table 1 panel A, p. 23; Table A.2 panel A, p. 44."),
]
odf = pd.DataFrame(origins, columns=["symbol", "what it is", "modest / substantial / extreme",
                                     "origin", "basis", "where"])
show(odf)
symbol what it is modest / substantial / extreme origin basis where
m_2030 affected mass in 2030 0.2 / 0.3 / 0.5 guesswork Scenario values. The range spans the low end to near the high end of the task feasibility ratings of Eloundou et al. (2024); the points inside it are chosen. Set by the authors, September 2026: Table 1 panel B, p. 23; Section 3.3, p. 27. Table 1 cites Eloundou et al. (2024) without a location.
d_2030 diffusion share in 2030 0.2 / 0.4 / 0.6 guesswork Scenario values. Table 1 gives no basis beyond ‘scenario assumptions’. Set by the authors, September 2026: Table 1 panel B, p. 24; Section 3.3, p. 27. No basis given.
a_2030 log gain per AI-performed instance, 2030 0.30 / 0.45 / 0.80 guesswork Scenario values, implied by the mid-2026 anchor and the slope g_a. Informed by measured trial gains (Brynjolfsson et al. 2025; Huang et al. 2025; Demirer et al. 2026). Set by the authors, September 2026: Table 1 panel B (the g_a row), p. 24; Section 3.3, p. 27. Table 1 cites the three trial papers without a location.
psi automation share of AI use 0.5 / 0.75 / 0.9 guesswork Scenario values. The low end is similar to the observed automation-like share of use (Appel et al. 2025); 0.75 and 0.90 are assumed. Set by the authors, September 2026: Table 1 panel C, p. 24; Section 3.4, pp. 27-28. Table 1 cites Appel et al. (2025) without a location.
rho reinstatement ratio 0.5 / 0.25 / 0 guesswork Scenario values. Acemoglu and Restrepo (2019) estimate 0.5 for past technologies, which is the modest value; nothing measures it for AI. Set by the authors, September 2026: Table 1 panel C, p. 24; Section 3.4, p. 28. Table 1 cites Acemoglu and Restrepo (2019) without a location.
mu cross-group search discount on the path 0.17 / 0.08 / 0.04 guesswork Scenario values. Only the normal-times value below is measured; how much harder reallocation becomes under the shock is assumed. Set by the authors, September 2026: Table 1 panel C, p. 24; Section 3.4, p. 28 (the 0.09 fitted to managers and professionals is the stated reason for 0.08 and 0.04).
theta_H share of the hiring shortfall posted per month 0.1 / 0.25 / 0.5 guesswork Scenario values. Table 1 gives no basis. Set by the authors, September 2026: Table 1 panel C, p. 24; Section 3.4, p. 28. No basis given.
m_2026 affected mass, mid-2026 anchor 0.14 research Observed-exposure measure of Massenkoff and McCrory (2026): occupations scored by the share of task time on tasks with significant work-related Claude use. Common to the three scenarios. Table 1 panel B, p. 23; Section 3.1, p. 25, and Section 3.3, p. 27 (0.22 in the AI-sensitive group, 0.01 in the other). Table 1 cites Massenkoff and McCrory (2026) without a location.
d_2026 diffusion share, mid-2026 anchor 0.1 data Census Business Trends and Outlook Survey and its 2026 AI supplement (https://www.census.gov/hfp/btos/). Common to the three scenarios. Table 1 panel B, p. 24; Section 3.3, p. 27 (18 percent of firms, 32 percent employment-weighted, set to 0.10). Series: BTOS AI supplement, https://www.census.gov/hfp/btos/.
a_2026 log gain, mid-2026 anchor 0.3 / 0.35 / 0.45 guesswork Scenario values: unlike m and d, the gain’s anchor already differs by scenario. Set by the authors, September 2026: Table 1 panel B, p. 24; Section 3.3, p. 27. Table 1 cites the three trial papers without a location.
sigma elasticity of substitution across tasks 0.5 research Gross complements: Acemoglu and Restrepo (2022), Humlum (2019), Jones and Tonetti (2026). Table 1 panel A, p. 23; Section 3.1, p. 25. Table 1 cites the three papers without a location.
s_L,t0 base-period labor share 0.6 research Conventional US value; Table 1 cites no series. Table 1 panel A, p. 23; Section 3.1, p. 25. No source named.
s_C,t0/s_L,t0 AI-sensitive share of employment 0.624 data CPS 2025 annual averages (https://www.bls.gov/cps/): employees in SOC major groups 11-29, 41 and 43. This is also the ceiling m_bar on the affected mass. Table 1 panel A, p. 23; Section 3.1, p. 25, footnote 10 (the SOC list); Table A.2 panel B, p. 44. Series: CPS 2025 annual averages, Table 11, https://www.bls.gov/cps/.
eps elasticity of capital supply 3 research Half the long-run wealth elasticity of Moll, Rachel and Restrepo (2022); the halving is the authors’ judgement about the horizon. Varied over 1 / 3 / 6 / inf in Table 5, which is where its weight shows. Table 1 panel A, p. 23; Section 3.1, p. 26 (the reasoning for a high value). Table 1 cites Moll et al. (2022) without a location.
r_bar, delta no-AI rental rate and depreciation 0.115, 0.05 research Moll et al. (2022): a 6.5 percent net return at a capital-output ratio of 3.5. Table 1 panel A, p. 23; Table A.2 panel A, p. 44. Table 1 cites Moll et al. (2022) without a location.
xi rigidity of the AI-sensitive wage, per year 0.5 research Wage-setting evidence, Section 3.4. Varied over 0 / 0.5 / 0.75 / 0.9 in Table 6. Table 1 panel C, p. 24; Section 3.4, p. 28 (the wage-setting evidence and the half-life argument); Table A.2 panel D, p. 44.
lambda returns to research input 1 guesswork Set to 1, no duplication in research. A normalization rather than an estimate. Set by the authors, September 2026: Table 1 panel A, p. 23; Section 3.1, p. 26; Table A.2 panel C, p. 44.
1 - phi_R fishing out, labor-augmenting units 2.86 derived s_L,t0 (1 - phi) + lambda, Equation (40), from the TFP-units value 3.1 of Bloom et al. (2020), which is itself research. Table 1 panel A, p. 23 (both rows, Equation (40)); Section 3.1, p. 26; Table A.2 panel C, p. 44. Table 1 cites Bloom et al. (2020) without a location.
g no-AI growth of the ideas stock, per year 0.0167 data Postwar US TFP growth, implying measured TFP growth of s_L,t0 g = 1 percent. Table 1 cites no series. Table 1 panel A, p. 23; Section 3.1, p. 26; Table A.2 panel C, p. 44. No series named.
n labor-force growth, per year 0.0033 guesswork Chosen so that GDP grows at 2 percent a year without AI. Table 1 notes BLS projections put labor-force growth near 0.4 percent, so the value is close to measured but is set by the target. Set by the authors, September 2026: Table 1 panel A, p. 23; Table A.2 panel C, p. 44. The BLS projection is cited without a location.
iota_R,t0 research share of GDP in 2024 0.035 data R&D share in the US national accounts. Table 1 panel A, p. 23; Section 3.1, p. 26; Table A.2 panel C, p. 44. No series named.
g_iota growth of the research share, per year 0.028 derived Equation (41), set so the ideas stock grows at g without AI (iota_R,2030 = 0.041). Table 1 panel A, p. 23 (Equation (41), Appendix C); Table A.2 panel C, p. 44.
U_bar normal search pool, share of the labor force 0.038 data CPS 2025. See the rounding discrepancy in Sections 2.3 and 4: the p. 26 derivation and the published pool split both imply 0.0384 rather than Table 1’s 0.038. Table 1 panel D, p. 25; Section 3.2, p. 26 (3.84 in the quit-rate derivation); Table A.2 panel D, p. 44. Series: CPS 2025 annual averages, Table 25b, https://www.bls.gov/cps/, and UNEMPLOY on FRED, https://fred.stlouisfed.org/.
q_bar normal quit rate, per year 0.11 data Implied by the pool and the CPS finding rate, rounded (Section 3.2). Table 1 panel D, p. 25; Section 3.2, p. 26 (0.219 x 3.84/96.16 = 0.875 percent a month, rounded to 0.11 a year); Table A.2 panel D, p. 44. Series: the CPS finding rate from the IPUMS-CPS matched files 2010-19, https://cps.ipums.org/cps/.
q^T_o / q_bar_o share of quits responding to job prospects 0.55 research Elasticity of JOLTS quits to the CPS finding rate, estimated at 0.53 and rounded to 0.55 (Section 3.2). Table 1 panel D, p. 25; Section 3.2, p. 27 (the 0.53 elasticity over 2001-19). Series: JOLTS quits rate JTSQUR on FRED, https://fred.stlouisfed.org/, and the CPS finding rate.
q_bar_C/q_bar, q_bar_N/q_bar relative separation rates by group 0.69, 1.52 data IPUMS-CPS matched monthly files 2010-19 (https://cps.ipums.org/cps/). Table 1 panel D, p. 25; Section 3.2, p. 26 (0.84 and 1.84 percent a month become unemployed; the calibration uses their ratio). Series: IPUMS-CPS matched monthly files 2010-19, https://cps.ipums.org/cps/.
mu_bar cross-group search discount in normal times 0.17 data CPS 2010-19 occupational switching matrix from the Carrillo-Tudela and Visschers (2023) replication files, corrected by their method (Section 3.2). Table 1 panel D, p. 25; Section 3.2, pp. 26-27 (19 and 11 percent cross, sqrt(0.23 x 0.12) = 0.17); Table A.2 panel D, p. 44. Files: Carrillo-Tudela and Visschers (2023), Econometrica 91(3), supplementary material at https://doi.org/10.3982/ECTA12498.
iota matching curvature 1.27 research den Haan, Ramey and Watson (2000). Table 1 panel D, p. 25; Table A.2 panel D, p. 44. Table 1 cites den Haan, Ramey and Watson (2000) without a location.
mean filling rate employment-weighted filling rate, per month 0.65 data JOLTS 2010-19 (https://www.bls.gov/jlt/), by the method of Davis et al. (2013). The matching efficiency chi is then calibrated to hit it, so chi is derived. Table 1 panel D, p. 25; Section 3.2, p. 27; Table A.2 panel D, p. 44. Series: JOLTS 2010-19, https://www.bls.gov/jlt/. Table 1 cites Davis et al. (2013) without a location.
t0, t_anchor, t_target base period, anchor, scenario date 2024, 2026.5, 2030 convention Dates, not estimates. Table 1 panel A, p. 23; Table A.2 panel A, p. 44.
h period length, years 1/12 convention A monthly grid, Appendix A. Table 1 panel A, p. 23; Table A.2 panel A, p. 44.
how many inputs of each kind
# value_counts() tallies a column; reindex() fixes the row order so the table reads
# from most evidence to least rather than by whichever label happens to be commonest.
counts = (odf["origin"].value_counts()
          .reindex(["data", "research", "guesswork", "derived", "convention"])
          .rename_axis("origin").reset_index(name="rows"))
show(counts)
origin rows
data 9
research 8
guesswork 10
derived 2
convention 2

Read the two tables together and the shape of the paper’s argument is visible. The labor market of a normal year is pinned down hard: the pool, the quit rate, the separation rates by group, the switching matrix and the filling rate are all data, and the matching curvature is an estimate. The production side rests on received values. What AI does is guesswork, by construction and by the paper’s own account: the seven dials are scenario assumptions, and Section 4 is a statement about what follows from them rather than a forecast.

That is not a criticism of the calibration; it is what a scenario exercise is. It does mean that the only honest way to read the results is as a map from the seven guesses to outcomes, which is what the scenario explorer makes clickable.

4 Results

Table 3 of the paper, with the published figure beside each reproduced one. The last column is the largest absolute deviation in the row.

Table 3: the three scenarios in 2030
cols = ["No AI", "modest", "substantial", "extreme"]
rows = []
for row in ROW_ORDER:                      # ROW_ORDER keeps the paper's row order
    got, want = T3[row], PUBLISHED[row]    # two 4-tuples: simulated, published

    # zip() walks several sequences in step, yielding tuples: here (column name,
    # simulated value, published value). The dict comprehension then builds one
    # "8.28  (8.30)" string per column.
    cells = {c: f"{g:,.2f}  ({w:,.2f})" for c, g, w in zip(cols, got, want)}

    # **cells splices that dict's key/value pairs into this new dict, so the four
    # scenario columns sit beside "row" and "max diff". max(... for ...) is a
    # generator expression: it computes the values one at a time and keeps the
    # largest, without building the intermediate list.
    rows.append({"row": row, **cells,
                 "max diff": max(abs(g - w) for g, w in zip(got, want))})
t3_df = pd.DataFrame(rows)
show(t3_df, floatfmt=".3f")
row No AI modest substantial extreme max diff
GDP, pct above no-AI 0.00 (0.00) 1.61 (1.60) 8.28 (8.30) 32.42 (32.40) 0.022
GDP, index 2024 = 100 112.75 (112.70) 114.56 (114.50) 122.09 (122.10) 149.31 (149.30) 0.060
GDP growth, pct per year 2.00 (2.00) 2.40 (2.40) 5.37 (5.40) 15.46 (15.40) 0.062
Average wage, pct above no-AI 0.00 (0.00) 0.69 (0.70) 2.14 (2.10) 9.65 (9.70) 0.049
cognitive occupations w_C 0.00 (0.00) 0.41 (0.40) -0.33 (-0.30) -11.49 (-11.50) 0.031
all other occupations w_N 0.00 (0.00) 1.14 (1.10) 5.86 (5.90) 33.61 (33.60) 0.043
Net return r - delta, pct per year 6.50 (6.50) 6.59 (6.60) 7.01 (7.00) 8.35 (8.30) 0.046
Capital stock, pct above no-AI 0.00 (0.00) 2.31 (2.30) 13.83 (13.80) 56.30 (56.30) 0.029
Labor share, pct of income 60.00 (60.00) 59.41 (59.40) 56.10 (56.10) 45.21 (45.20) 0.013
Capital share, pct of income 40.00 (40.00) 40.59 (40.60) 43.90 (43.90) 54.79 (54.80) 0.013
Labor income, pct above no-AI 0.00 (0.00) 0.63 (0.60) 1.36 (1.40) 0.46 (0.50) 0.040
wage bill of cognitive occs 0.00 (0.00) -0.29 (-0.30) -4.63 (-4.60) -30.96 (-31.00) 0.040
Capital income, pct above no-AI 0.00 (0.00) 3.10 (3.10) 18.85 (18.90) 81.38 (81.40) 0.048
Cognitive employment, pct since mid-2026 0.00 (0.00) -0.46 (-0.50) -3.87 (-3.90) -21.44 (-21.50) 0.055
Unemployment rate, cognitive, pct 2.82 (2.90) 2.91 (2.90) 4.50 (4.50) 17.89 (17.90) 0.082
Unemployment rate, all workers, pct 3.80 (3.80) 3.86 (3.90) 4.53 (4.60) 11.86 (11.90) 0.071
Measured TFP, pct above no-AI 0.00 (0.00) 0.71 (0.70) 3.07 (3.10) 13.41 (13.40) 0.025
Measured TFP growth, pct per year 1.00 (1.00) 1.18 (1.20) 2.35 (2.30) 7.32 (7.30) 0.048
Ideas stock A, pct above no-AI 0.00 (0.00) 0.07 (0.07) 0.20 (0.20) 0.61 (0.61) 0.005
Growth of the ideas stock, pct per year 1.67 (1.67) 1.69 (1.69) 1.77 (1.76) 2.03 (2.02) 0.007

Every cell is inside the tolerance the test suite applies: within 0.05 percentage points of the published value for Table 3 (0.10 where the published value is 10 or larger, and 0.09 for two unemployment cells explained below), and within the larger of 0.12 points and 0.4 percent of the published value for Tables 5 and 6. Those two unemployment cells are the only ones outside Table 3’s standard band. They are not the worst fits: inside the looser floor of Tables 5 and 6, the same rounding leaves four Table 6 unemployment cells 0.07 to 0.08 points low (one of them the identical model run), and the largest gap anywhere is 0.11 (Table 6’s extreme \(\xi = 0\) wage). They come out low by 0.07 percentage points (all workers, substantial) and 0.08 (cognitive, no AI). Both trace to the same rounding: Table 1 gives the normal pool as \(\bar U = 0.038\), while the quit-rate derivation on p. 26 uses 3.84 percent and the published pool split of 1.76 and 2.08 requires 0.0384. At 0.0384 the all-workers row lands on the published 4.6 and nothing else changes materially (outside the two unemployment rows, no Table 3 cell moves by more than 0.01 points); the no-AI cognitive rate improves to 2.85, which still prints as 2.8 rather than the paper’s 2.9, so the pool accounts for the size of that gap without closing it. The default here stays at Table 1’s rounded value, in Tables 3, 5 and 6 alike; Fixed(U_bar=0.0384) switches to the other reading.

Printed precision is a stricter test than tolerance, and the two are worth keeping apart. Rounding half-up to the paper’s own number of decimals, 153 of the 169 cells in Tables 3, 5 and 6 land on the published digit and 16 do not: seven in Table 3, one in Table 5 and eight in Table 6, none off by more than 0.11 percentage points. Seven of the sixteen land at \(\bar U = 0.0384\): six unemployment cells the pool explains, plus one Table 5 GDP cell that crosses its rounding boundary by coincidence. tests/test_printed_precision.py holds the sixteen as an exact allowlist, and the repository README lists them cell by cell.

figure helper: one panel per variable, direct end-labels, no-AI baseline
def panel(ax, col, title, ylabel, baseline=None, fmt="{:.1f}", start=2025.0, ylim=None):
    """Draw one scenario-comparison panel onto an existing matplotlib axis.

    `ax` is the axis to draw on, `col` names the column of D[scenario] to plot.
    Passing the axis in (rather than creating it here) is what lets the callers
    below arrange one, two or three panels side by side.
    """
    for n in NAMES:
        # Boolean indexing: D[n].t >= start is a column of True/False, and putting it
        # inside D[n][...] keeps only the rows where it is True. This is pandas'
        # equivalent of a WHERE clause.
        df = D[n][D[n].t >= start]
        ax.plot(df.t, df[col], color=COL[n], label=n.capitalize())
        # .iloc[-1] takes the last row by position, so `y` is the 2030 value; the
        # annotation below prints it at the end of the line (a "direct label", which
        # is what keeps the chart readable without hunting through a legend).
        y = df[col].iloc[-1]
        ax.annotate(fmt.format(y), (df.t.iloc[-1], y), xytext=(4, 0),
                    textcoords="offset points", fontsize=7, color=INK2, va="center")
    if baseline is not None:
        ax.axhline(baseline, ls=(0, (4, 3)), lw=1.1, color=INK2, alpha=0.6, zorder=0)
    ax.set_title(title, loc="left")
    ax.set_ylabel(ylabel)
    ax.set_xlim(start, 2030.9)
    ax.set_xticks([2025, 2026, 2027, 2028, 2029, 2030])
    if ylim:
        ax.set_ylim(*ylim)
    return ax
Figure 2
fig, axes = plt.subplots(1, 2, figsize=(6.6, 2.4))
panel(axes[0], "gdp", "GDP", "percent above the no-AI path", ylim=(0, 34))
panel(axes[1], "gdp_growth", "GDP growth", "percent per year; 2 percent without AI",
      baseline=100 * (F.g + F.n), ylim=(1.5, 16.5))
axes[0].legend(loc="upper left", fontsize=7)
plt.show()
Figure 2: GDP and its growth rate in the three scenarios, 2025-2030, reproducing the paper’s Figure 2. Dashed line: the economy without AI; each line carries its January 2030 value.

4.1 The modest change scenario

In the modest scenario AI is a small technology. By 2030 just 4 percent of the economy’s tasks have been touched by AI (\(m_{2030} = 0.20\), \(d_{2030} = 0.20\)), productivity rises about 35 percent on those tasks (\(a_{2030} =\) 0.30), half of affected instances are automated and half augmented, and one new task arrives for every two automated.

GDP ends 1.6 percent above its no-AI path, an extra 10 months of ordinary growth accumulated since 2024, and the economy grows at 2.4 percent a year rather than 2. The average wage is 0.7 percent higher and the cognitive wage 0.4 percent. The net return to capital is 6.6 percent rather than 6.5, the capital stock is 2.3 percent larger, and the labor share falls from 60.0 to 59.4 percent of income. Cognitive employment is -0.5 percent below its mid-2026 level and the unemployment rate is 3.9 percent against a normal 3.8.

4.2 The substantial change scenario

Here 12 percent of the economy’s tasks are affected, roughly 19 percent of the tasks cognitive workers performed in 2025, AI raises productivity by about 57 percent on them, and three quarters of affected instances are automated.

GDP is 8.3 percent above the no-AI path and growth over the twelve months to 2030 is 5.4 percent a year, against a fastest dot-com-boom year of 4.7 percent in 1999. The average wage rises 2.1 percent, far less than GDP: the cognitive wage is -0.3 percent relative to no AI while wages in the rest of the economy are 5.9 percent higher. The labor share falls to 56.1 percent and the capital share rises to 43.9 percent, a shift of 3.9 points since 2024. Cognitive employment is -3.9 percent below its mid-2026 level while all-other employment is 4.6 percent above it, and because reallocation takes time the cognitive unemployment rate rises to 4.5 percent in 2030, from 2.8 percent in normal times, an increase of 60 percent. Section 4.2 puts the same rise at “more than a 50 percent increase”, from 2.9 percent; note that the base is the normal-times rate, since by mid-2026 this model is already at 3.0 percent.

Figure 3
fig, axes = plt.subplots(2, 2, figsize=(6.6, 4.2))
panel(axes[0][0], "wage", "Average wage", "percent above the no-AI path", ylim=(-0.5, 11))
panel(axes[0][1], "wC", "Cognitive wage", "percent above the no-AI path", baseline=0.0,
      ylim=(-13, 3.5))
panel(axes[1][0], "net_r", "Net return to capital",
      "percent per year; 6.5 without AI", baseline=100 * (F.r_bar - F.delta),
      ylim=(6.3, 8.6))
panel(axes[1][1], "labor_share", "Labor share", "percent of income; 60 without AI",
      baseline=100 * F.s_L0, ylim=(44, 61))
axes[0][0].legend(loc="upper left", fontsize=7)
plt.tight_layout()
plt.show()
Figure 3: Factor prices and the labor share in the three scenarios, 2025-2030, reproducing the paper’s Figure 3. Dashed lines: the economy without AI.

4.3 The extreme change scenario

By 2030 30 percent of the economy’s tasks are affected, roughly 48 percent of the tasks cognitive workers performed in 2025; AI more than doubles productivity on them (\(e^{a} =\) 2.2), 90 percent of affected instances are automated, and no new tasks are reinstated.

GDP is 32.4 percent above its no-AI path, growing at 15.5 percent a year. The labor share falls from 60.0 to 45.2 percent. Within labor the split is stark: the cognitive wage is -11.5 percent relative to no AI while the all-other wage is 33.6 percent above it. Cognitive employment is -21.4 percent below mid-2026, 17.9 percent of cognitive workers are unemployed, nearly one in five, and the unemployment rate for all workers is 11.9 percent. Capital is 56.3 percent above its no-AI path and measured TFP 13.4 percent.

Figure 4
fig, axes = plt.subplots(1, 3, figsize=(6.8, 2.3))
panel(axes[0], "cog_emp", "Cognitive employment", "percent change since mid-2026",
      baseline=0.0, ylim=(-23, 2))
panel(axes[1], "u_C", "Unemployment rate, cognitive",
      "percent of the group's labor force",
      baseline=100 * SS.U_C / (SS.U_C + F.l_C0), ylim=(0, 19))
panel(axes[2], "u_all", "Unemployment rate, all workers", "percent of the labor force",
      baseline=100 * F.U_bar, ylim=(0, 13))
axes[0].legend(loc="lower left", fontsize=7)
plt.tight_layout()
plt.show()
Figure 4: The labor market in the three scenarios, 2025-2030, reproducing the paper’s Figure 4. Dashed lines: the no-AI economy, in which the two rates stay at their normal levels.

4.4 The scenarios implied by survey responses

Table 2 reports the five parameters implied by the survey of 10,980 US adults. Those medians are transcribed here and run through the model, with everything the survey did not ask about held at the substantial scenario’s values. The comparison against Table 4 is indicative only: its columns are medians of outcomes across the 3,259 respondents who answered all five items, which is not the outcome at the median answers and cannot be recovered from published medians without the microdata.

Table 2 medians, and the model run at them against Table 4
# Transcribed from Table 2 of the paper. Building a DataFrame from a dict of lists
# requires every list to have the same length, one entry per row.
surv_par = pd.DataFrame({
    "object": ["m_2030", "d_2030", "psi", "a_2030", "mu"],
    "survey median (Table 2)": [0.44, 0.40, 0.47, 0.44, 0.064],
    "IQR (Table 2)": ["[0.20, 0.59]", "[0.22, 0.61]", "[0.25, 0.65]", "[0.09, 1.02]",
                      "[0.025, 0.111]"],
    "used here": [SURVEY_MEDIAN.m_2030, SURVEY_MEDIAN.d_2030, SURVEY_MEDIAN.psi,
                  SURVEY_MEDIAN.a_anchor + SURVEY_MEDIAN.g_a * 3.5, SURVEY_MEDIAN.mu],
})
surv_run = table3_column(simulate.run(F, SURVEY_MEDIAN))
table4_median = {
    "GDP, pct above no-AI": 8.6, "GDP growth, pct per year": 5.3,
    "Average wage, pct above no-AI": 2.6, "  cognitive occupations w_C": 0.6,
    "  all other occupations w_N": 6.4, "Net return r - delta, pct per year": 7.0,
    "Capital stock, pct above no-AI": 13.5, "Labor share, pct of income": 57.2,
    "Cognitive employment, pct since mid-2026": -4.2,
    "Unemployment rate, cognitive, pct": 4.6,
    "Unemployment rate, all workers, pct": 4.6,
}
surv_out = pd.DataFrame({
    # Iterating a dict gives its KEYS, so list(table4_median) is the row names and
    # .values() is needed to get the numbers. [T3[k][2] for k in ...] pulls index 2
    # of each 4-tuple, which is the substantial column.
    "row": list(table4_median),
    "model at the median answers": [surv_run[k] for k in table4_median],
    "Table 4 median of outcomes": list(table4_median.values()),
    "substantial scenario": [T3[k][2] for k in table4_median],
})
# A chunk shows only its LAST expression automatically, so an explicit display() is
# needed to emit the first of two tables.
display(show(surv_par, floatfmt=".3f"))
show(surv_out, floatfmt=".1f")
object survey median (Table 2) IQR (Table 2) used here
m_2030 0.440 [0.20, 0.59] 0.440
d_2030 0.400 [0.22, 0.61] 0.400
psi 0.470 [0.25, 0.65] 0.470
a_2030 0.440 [0.09, 1.02] 0.440
mu 0.064 [0.025, 0.111] 0.064
row model at the median answers Table 4 median of outcomes substantial scenario
GDP, pct above no-AI 9.9 8.6 8.3
GDP growth, pct per year 6.2 5.3 5.4
Average wage, pct above no-AI 4.3 2.6 2.1
cognitive occupations w_C 1.1 0.6 -0.3
all other occupations w_N 9.1 6.4 5.9
Net return r - delta, pct per year 7.0 7.0 7.0
Capital stock, pct above no-AI 14.7 13.5 13.8
Labor share, pct of income 56.3 57.2 56.1
Cognitive employment, pct since mid-2026 -4.5 -4.2 -3.9
Unemployment rate, cognitive, pct 5.1 4.6 4.5
Unemployment rate, all workers, pct 4.9 4.6 4.5

The two columns bracket the same conclusion the paper draws, that the public’s answers sit close to the substantial change scenario, but they are not the same statistic and the gap between them (GDP 9.9 against 8.6) is what the distinction costs.

4.5 Robustness: the supply of capital

Table 5 reruns the substantial and extreme scenarios at four elasticities of capital supply. At \(\varepsilon = 1\) the average wage changes sign in both scenarios: labor loses the tasks and works with capital that is scarce and expensive.

Table 5: four elasticities of capital supply
TABLE5_ROWS = ["GDP, pct above no-AI", "Average wage, pct above no-AI",
               "Net return r - delta, pct per year", "Capital stock, pct above no-AI",
               "Labor share, pct of income"]
TABLE5_PUB = {
    ("substantial", 1.0): (6.4, -1.6, 7.6, 9.3, 55.1),
    ("substantial", 3.0): (8.3, 2.1, 7.0, 13.8, 56.1),
    ("substantial", 6.0): (9.1, 3.7, 6.8, 15.7, 56.5),
    ("substantial", math.inf): (10.0, 5.6, 6.5, 18.2, 57.0),
    ("extreme", 1.0): (21.3, -9.2, 10.3, 33.5, 41.2),
    ("extreme", 3.0): (32.4, 9.7, 8.3, 56.3, 45.2),
    ("extreme", 6.0): (37.2, 18.3, 7.5, 67.1, 46.9),
    ("extreme", math.inf): (43.3, 30.1, 6.5, 82.2, 49.1),
}
t5 = {}
# .items() yields (key, value) pairs. The key here is itself a tuple, so
# `for (scen, eps), want in ...` unpacks the key into two names and the value into a
# third, all in one line.
for (scen, eps), want in TABLE5_PUB.items():
    col = table3_column(simulate.run(Fixed(eps=eps), SCENARIOS[scen]))
    label = f"{scen[:4]}, eps={'inf' if math.isinf(eps) else int(eps)}"
    t5[label] = [f"{col[r]:,.2f}  ({w:,.1f})" for r, w in zip(TABLE5_ROWS, want)]
show(pd.DataFrame({"row": TABLE5_ROWS, **t5}))
row subs, eps=1 subs, eps=3 subs, eps=6 subs, eps=inf extr, eps=1 extr, eps=3 extr, eps=6 extr, eps=inf
GDP, pct above no-AI 6.38 (6.4) 8.28 (8.3) 9.05 (9.1) 10.02 (10.0) 21.28 (21.3) 32.42 (32.4) 37.17 (37.2) 43.33 (43.3)
Average wage, pct above no-AI -1.56 (-1.6) 2.14 (2.1) 3.66 (3.7) 5.61 (5.6) -9.22 (-9.2) 9.65 (9.7) 18.32 (18.3) 30.11 (30.1)
Net return r - delta, pct per year 7.57 (7.6) 7.01 (7.0) 6.78 (6.8) 6.50 (6.5) 10.35 (10.3) 8.35 (8.3) 7.53 (7.5) 6.50 (6.5)
Capital stock, pct above no-AI 9.30 (9.3) 13.83 (13.8) 15.72 (15.7) 18.18 (18.2) 33.47 (33.5) 56.30 (56.3) 67.10 (67.1) 82.24 (82.2)
Labor share, pct of income 55.08 (55.1) 56.10 (56.1) 56.51 (56.5) 57.03 (57.0) 41.24 (41.2) 45.21 (45.2) 46.92 (46.9) 49.14 (49.1)

4.6 Robustness: wage rigidity

Table 6 reruns them at four rigidities of the cognitive wage. The parameter governs where the cost to cognitive workers shows up: at \(\xi = 0\) the wage takes it all and unemployment barely moves; at \(\xi = 0.9\) the cognitive wage ends above its no-AI path while cognitive unemployment reaches 24 percent.

Table 6: four rigidities of the cognitive wage
TABLE6_ROWS = ["GDP, pct above no-AI", "Average wage, pct above no-AI",
               "  cognitive occupations w_C", "  all other occupations w_N",
               "Cognitive employment, pct since mid-2026",
               "Unemployment rate, cognitive, pct", "Unemployment rate, all workers, pct"]
TABLE6_PUB = {
    ("substantial", 0.5): (8.3, 2.1, -0.3, 5.9, -3.9, 4.5, 4.6),
    ("substantial", 0.75): (7.9, 2.2, 0.7, 4.5, -4.6, 5.1, 4.9),
    ("substantial", 0.9): (7.7, 2.3, 1.4, 3.7, -5.0, 5.4, 5.2),
    ("extreme", 0.0): (36.6, 1.6, -42.2, 70.1, -1.3, 2.6, 3.1),
    ("extreme", 0.5): (32.4, 9.7, -11.5, 33.6, -21.5, 17.9, 11.9),
    ("extreme", 0.75): (30.5, 11.1, -2.9, 25.8, -25.9, 21.7, 13.9),
    ("extreme", 0.9): (29.2, 11.9, 2.8, 21.1, -28.5, 24.0, 15.2),
}
t6 = {}
for (scen, xi), want in TABLE6_PUB.items():
    # every row at the package default U_bar = 0.038, as in Table 3 above
    col = table3_column(simulate.run(Fixed(xi=xi), SCENARIOS[scen]))
    label = f"{scen[:4]}, xi={xi}"
    t6[label] = [f"{col[r]:,.2f}  ({w:,.1f})" for r, w in zip(TABLE6_ROWS, want)]
show(pd.DataFrame({"row": TABLE6_ROWS, **t6}))
row subs, xi=0.5 subs, xi=0.75 subs, xi=0.9 extr, xi=0.0 extr, xi=0.5 extr, xi=0.75 extr, xi=0.9
GDP, pct above no-AI 8.28 (8.3) 7.93 (7.9) 7.68 (7.7) 36.57 (36.6) 32.42 (32.4) 30.51 (30.5) 29.18 (29.2)
Average wage, pct above no-AI 2.14 (2.1) 2.23 (2.2) 2.30 (2.3) 1.60 (1.6) 9.65 (9.7) 11.12 (11.1) 11.94 (11.9)
cognitive occupations w_C -0.33 (-0.3) 0.70 (0.7) 1.37 (1.4) -42.09 (-42.2) -11.49 (-11.5) -2.86 (-2.9) 2.81 (2.8)
all other occupations w_N 5.86 (5.9) 4.52 (4.5) 3.67 (3.7) 70.14 (70.1) 33.61 (33.6) 25.82 (25.8) 21.10 (21.1)
Cognitive employment, pct since mid-2026 -3.87 (-3.9) -4.56 (-4.6) -5.02 (-5.0) -1.30 (-1.3) -21.44 (-21.5) -25.83 (-25.9) -28.47 (-28.5)
Unemployment rate, cognitive, pct 4.50 (4.5) 5.03 (5.1) 5.39 (5.4) 2.59 (2.6) 17.89 (17.9) 21.65 (21.7) 23.97 (24.0)
Unemployment rate, all workers, pct 4.53 (4.6) 4.88 (4.9) 5.12 (5.2) 3.08 (3.1) 11.86 (11.9) 13.89 (13.9) 15.12 (15.2)

4.7 An extension the paper does not run: AI slop

“Slop” means output that is plausible but low quality, so that using AI costs time in checking and rework. It is worth asking where that lands in this model, and the answer is unusually clean: it is not a missing mechanism, it is a low draw of \(a_t\).

The paper’s gain per instance is defined all-in, net of checking. Its own survey question asks how long a task takes with AI “counting the time spent checking and fixing the AI’s work”, and Table 1 sources the 0.30 to 0.45 range to field trials and frontier firms. A slop world is one where the realised all-in gain in the wild is far below what trials measured.

What makes that consequential is an asymmetry already visible in the labor-share line of Equation (11):

The labor-share line of Equation (11): what scales with the gain, and what does not
from aiscen import slop                     # the extension module, documented in aiscen/slop.py

a0 = slop.gain_2030(slop.BASE)              # the substantial scenario's 2030 gain
rows = [slop.decomposition(F, slop.scale_gain(slop.BASE, a))
        for a in (a0, a0 / 2, 0.09)]        # 0.09 is the survey's own lower quartile
show(pd.DataFrame(rows)[["a_2030", "displacement", "weak-link cushion", "ratio"]],
     floatfmt=(".3f", ".3f", ".4f", ".4f", ".1f"))
a_2030 displacement weak-link cushion ratio
0.448 0.068 0.0202 3.3482
0.224 0.068 0.0101 6.6964
0.090 0.068 0.0041 16.6667

Displacement, \((1-\rho)\psi_t m_t d_t\), contains no \(a_t\): an automated instance takes its whole wage bill to capital whether the machine saved an hour or a minute. The weak-link cushion that partly protects the labor share, \((1-\sigma)\psi_t m_t d_t a_t\), is proportional to \(a_t\). So slop subtracts the offset and leaves the harm intact.

the substantial scenario re-run under five slop variants
slop_tbl = pd.DataFrame(slop.cases(F))
disp = slop_tbl[["case", "a_2030", "eps_star"] + slop.REPORT_ROWS].copy()
# eps* diverges in the zero-gain row, so show the symbol rather than a huge number
disp["eps_star"] = disp["eps_star"].map(lambda x: "inf" if not np.isfinite(x) else f"{x:.2f}")
show(disp, floatfmt=".2f")
case a_2030 eps_star GDP, pct above no-AI Measured TFP, pct above no-AI Average wage, pct above no-AI cognitive occupations w_C Labor share, pct of income Cognitive employment, pct since mid-2026 Unemployment rate, cognitive, pct
substantial (baseline) 0.45 1.30 8.28 3.07 2.14 -0.33 56.10 -3.87 4.50
all-in gain halved 0.22 3.39 6.68 1.64 -0.34 -2.64 55.59 -3.68 4.40
gain halved, checking becomes new human work 0.22 2.00 5.04 1.62 0.72 -0.78 57.26 -2.38 3.79
gain halved, adoption stalls 0.22 3.39 4.29 1.04 -0.16 -1.48 57.25 -1.96 3.54
all-in gain quartered 0.11 7.58 5.81 0.88 -1.66 -3.87 55.32 -3.59 4.35
pure slop: no time saved at all 0.00 inf 4.90 0.07 -3.04 -5.17 55.03 -3.48 4.30

Three things to read off that table.

The wage sign flips before the gain is even halved. Equation (12)’s threshold contains \((1-\rho)/a_t\), so a lower gain pushes \(\varepsilon^*\) up, and slop pushes it straight through the calibrated \(\varepsilon = 3\):

the all-in gain at which the average wage stops rising
a_crit = slop.critical_gain(F)
print(f"average wage turns negative below a_2030 = {a_crit:.3f}"
      f"   (baseline {a0:.2f}; survey lower quartile 0.09)")
average wage turns negative below a_2030 = 0.253   (baseline 0.45; survey lower quartile 0.09)

That threshold is not exotic relative to what people believe. Table 2’s interquartile range for \(a_{2030}\) runs from 0.09 to 1.02, and Section 3.5 reports that about 30 percent of respondents expect AI to save no time at all on a task it is suited to. The lower half of the survey’s own distribution is a slop world.

Slop does not show up as a GDP disappointment. In the limiting case where AI saves nothing at all, GDP is still 4.9 percent above the no-AI path while measured TFP is 0.1 and the average wage is -3.0. Automation converts work from a fixed factor into an accumulable one: the rental rate rises, capital deepens, output rises, and labor pays for it. The signature of slop in this model is therefore “output fine, wages falling, labor share sliding”, not “the boom failed to arrive”. That is Acemoglu and Restrepo’s so-so automation in its purest form.

One version of slop helps workers. If checking and fixing is genuinely new human work rather than pure waste, that is a rise in the reinstatement ratio \(\rho\), and it is the only case here in which the average wage still rises (+0.7). It lifts the labor share to 57.3, as stalled adoption also does, at the cost of lower output. “Slop that makes work” and “slop that just does not deliver” are different worlds, and the model separates them.

NoteWhat the model cannot say about slop

Three limits, in increasing order of seriousness.

  1. Quality. Instances are homogeneous and prices are quality-adjusted by assumption, so the model can express slop only as a smaller cost saving, never as the same cost for a worse product. If sloppy output is counted at full price but is worth less, measured TFP overstates true gains and nothing here records the gap.
  2. Heterogeneity. The paper’s equations carry \(\sum_i m_i d_i a_i\), so “automated instances are sloppy, augmented ones are not” is representable in principle. The calibration sets a uniform \(a_t\), and this reimplementation follows it, so the variants above move the gain on both kinds of instance together.
  3. Externalities. Equation (9) is Hulten’s theorem, which holds in an efficient economy. If errors propagate downstream, force rework in other tasks, or erode trust in the output, the Domar-weighted sum is no longer the right aggregation and the TFP expression is an overstatement rather than an approximation. This is a structural limit of the framework, not a calibration quibble, and it is the version of the slop worry the model genuinely cannot host.

5 Verification summary

deviations against every published table, and the internal identities
def dev(sim, pub):
    """Largest absolute gap between a row of simulated and published values."""
    return max(abs(a - b) for a, b in zip(sim, pub))

t3_dev = [(row, dev(T3[row], PUBLISHED[row])) for row in ROW_ORDER]
t5_dev, t6_dev = [], []
for (scen, eps), want in TABLE5_PUB.items():
    col = table3_column(simulate.run(Fixed(eps=eps), SCENARIOS[scen]))
    t5_dev.append((f"{scen} eps={eps}", dev([col[r] for r in TABLE5_ROWS], want)))
for (scen, xi), want in TABLE6_PUB.items():
    # same convention as the Table 6 body above: default U_bar = 0.038 everywhere
    col = table3_column(simulate.run(Fixed(xi=xi), SCENARIOS[scen]))
    vals = [col[r] for r in TABLE6_ROWS]
    t6_dev.append((f"{scen} xi={xi}", dev(vals, want)))

# max(..., key=lambda x: x[1]) finds the element whose SECOND item is largest, then
# [0] takes that element's first item: the name of the worst-deviating row. The
# underscore in `for _, d in ...` is a conventional name for "a value I must unpack
# but do not use".
summary = pd.DataFrame([
    ("Table 3, the three scenarios in 2030", 20 * 4,
     max(d for _, d in t3_dev), max(t3_dev, key=lambda x: x[1])[0]),
    ("Table 5, elasticities of capital supply", 5 * 8,
     max(d for _, d in t5_dev), max(t5_dev, key=lambda x: x[1])[0]),
    ("Table 6, rigidities of the cognitive wage", 7 * 7,
     max(d for _, d in t6_dev), max(t6_dev, key=lambda x: x[1])[0]),
], columns=["published table", "cells compared", "largest absolute deviation",
            "where"])
show(summary, floatfmt=".3f")
published table cells compared largest absolute deviation where
Table 3, the three scenarios in 2030 80 0.082 Unemployment rate, cognitive, pct
Table 5, elasticities of capital supply 40 0.051 substantial eps=6.0
Table 6, rigidities of the cognitive wage 49 0.106 extreme xi=0.0

The largest deviation anywhere is 0.11, on figures the paper prints to one decimal.

Four internal identities, none of which involve the published numbers:

internal consistency checks
checks = []

# 1. the labor force adds up every month, in every scenario.
# Two `for` clauses in one comprehension is a nested loop, read left to right: for
# each scenario, for each month of that scenario. It yields one number per month
# across all three runs, and max() keeps the largest. A result near 1e-16 means the
# identity holds to the limit of floating-point precision.
worst = max(abs(m.l_C + m.l_N + m.U_C + m.U_N - F.L)
            for n in NAMES for m in RUNS[n].months)
checks.append(("l_C + l_N + U_C + U_N = L, every month (p. 20)", f"{worst:.2e}", "0"))

# 2. system (39) at the targets reproduces Proposition 1 (Table A.1, panel D)
worst = 0.0
for n in NAMES:
    x = P[n].at(2030.0)
    for dlnA in (0.0, 0.006):
        fr = statics.frictionless(F, x["md"], x["a"], x["psi"], x["rho"], dlnA)
        ac = statics.actual_at_employment(F, x["md"], x["a"], x["psi"], x["rho"],
                                          fr.l_C_star, fr.l_N_star, dlnA)
        worst = max(worst, abs(ac.dlnY - fr.dlnYL), abs(ac.s_L - fr.s_L),
                    abs(ac.dlnw_C_tilde - fr.dlnw_tilde))
checks.append(("(39) at the targets equals Proposition 1", f"{worst:.2e}", "0"))

# 3. the monthly ideas step against the closed form (43)
worst = max(abs(RUNS[n].at(2030.0).dlnA - simulate.ideas_closed_form(RUNS[n], 2030.0))
            for n in NAMES)
checks.append(("monthly ideas step vs closed form (43)", f"{100 * worst:.4f} pp",
               "< 0.02 pp (p. 41)"))

# 4. every hire leaves the pool
worst = max(abs(m.f_C * m.U_C + m.f_N * m.U_N - m.H_C - m.H_N)
            for n in NAMES for m in RUNS[n].months)
checks.append(("f_C U_C + f_N U_N = H_C + H_N (p. 20)", f"{worst:.2e}", "0"))

show(pd.DataFrame(checks, columns=["identity", "largest violation", "expected"]))
identity largest violation expected
l_C + l_N + U_C + U_N = L, every month (p. 20) 7.77e-16 0
(39) at the targets equals Proposition 1 3.35e-14 0
monthly ideas step vs closed form (43) 0.0196 pp < 0.02 pp (p. 41)
f_C U_C + f_N U_N = H_C + H_N (p. 20) 2.60e-18 0

One further cross-check of the model against itself: the measured TFP index (45) is a base-weighted index, its dual (25) a chained one, and Appendix C.4 says the two agree only to first order. They do, and the wedge grows with the size of the shock, which is how those two index concepts should differ.

Equation (45) against its dual (25)
rows = []
for n in NAMES:
    res = RUNS[n]
    # Start the chain at the no-AI baseline (zero gaps, base labor share), then one
    # point per month. `+` concatenates two lists.
    pts = [(F.s_L0, 0.0, 0.0)] + [(m.s_L_star, m.dlnw_common, m.dlnr_star)
                                  for m in res.months]

    # zip(pts, pts[1:]) pairs each point with the NEXT one: pts[1:] is the list from
    # index 1 onward, so the pairs are (point 0, point 1), (point 1, point 2), and so
    # on. That is the standard way to walk consecutive differences. Each pair is
    # unpacked into previous (sp, wp, rp) and current (sc, wc, rc) values, and the
    # sum accumulates the share-weighted change in the two factor prices.
    dual = sum(0.5 * (sp + sc) * (wc - wp) + (1 - 0.5 * (sp + sc)) * (rc - rp)
               for (sp, wp, rp), (sc, wc, rc) in zip(pts, pts[1:]))
    tfp = res.at(2030.0).dln_tfp
    rows.append({"scenario": n, "TFP gap, index (45)": 100 * tfp,
                 "TFP gap, chained dual (25)": 100 * dual,
                 "wedge, pct of the gap": 100 * abs(tfp - dual) / tfp})
show(pd.DataFrame(rows), floatfmt=".3f")
scenario TFP gap, index (45) TFP gap, chained dual (25) wedge, pct of the gap
modest 0.709 0.708 0.091
substantial 3.028 2.970 1.916
extreme 12.583 11.692 7.079

Reproducing this document

QUARTO_PYTHON=/opt/anaconda3/bin/python3 quarto render repro.qmd
python3 -m pytest -q          # the same checks as assertions, plus the ones not shown here

The package requires only the standard library; this document additionally uses pandas, matplotlib and tabulate for its tables and figures. README.md records the places where the paper leaves a modelling choice implicit and how each was resolved.