"""Mapping the task-based stochastic growth economy to the LQ delegation game (§1.9 of the specification). Steps ----- 1. Deterministic steady state for given compute stock K_Q and delegation share D. 2. Linearised household problem solved in sequence space (horizon T): consumption and wealth paths for given price paths; with beta*R = 1 wealth has a unit root, so the terminal condition is a_{T+1} = a_T (new steady wealth level). 3. kappa0 = response of log savings a_1 to a known news shock theta = eps_1 with the aggregate capital path fixed; kappa_full = symmetric rational-expectations response (aggregate = individual); alpha = 1 - kappa0/kappa_full. 4. |U_xx| in log units = abar^2 * [ -u''(c) - beta u''(c) R MPC ] with the MPC from the same linear solve. 5. Delegation share from the threshold rule (A.10) and compute-market clearing (A.4) in the short run. """ from __future__ import annotations from dataclasses import dataclass, field import numpy as np from scipy.optimize import brentq from scipy.stats import lognorm from age.economy.tasks import TaskTech from age.equilibrium.lq_game import LQParams, private_value @dataclass(frozen=True) class EconParams: beta: float = 0.96 gamma: float = 2.0 alpha: float = 0.33 delta: float = 0.06 rho_z: float = 0.90 sigma_eps: float = 0.02 tasks: TaskTech = field(default_factory=lambda: TaskTech(alpha=0.33, iota_bar=0.5, psi0=1.0, psi1=3.0)) # compute / delegation technology delta_Q: float = 0.20 pK_Q: float = 1.0 qbar: float = 0.002 ebar: float = 0.001 pE: float = 1.0 tau_s: float = 0.01 # information about the TFP innovation (savings mapping; precisions in units of log TFP) tau_H: float = 500.0 tau_A: float = 5000.0 M: int = 1 # information about the predictable return component (portfolio mapping; s.d. in return units, ILLUSTRATIVE) sigma_R: float = 0.16 # total risky-return s.d. (calibrated: Shiller 1984-2025 real equity s.d. 0.157) sig_theta_R: float = 0.06 # s.d. of the predictable return component theta sig_H_R: float = 0.10 # human forecast noise s.d. sig_A_R: float = 0.04 # foundation-model forecast noise s.d. # delegation disutility, lognormal in goods units (fraction of steady-state consumption per year) kappa_mu: float = -6.0 kappa_sigma: float = 1.0 @property def tau_theta(self): return 1.0 / self.sigma_eps ** 2 def u2(self, c): # u''(c) return -self.gamma * c ** (-self.gamma - 1.0) def u1(self, c): # u'(c) return c ** (-self.gamma) # --------------------------------------------------------------------------- 1. steady state def steady_state(p: EconParams, KQ: float = 0.0, D: float = 0.0, L_endowment: float = 1.0) -> dict: """Deterministic steady state (z = 0) for given compute stock and delegation share. Labour is inelastic; supervision time tau_s*D is subtracted from hours. Capital from R = 1/beta. The compute price in the steady state is the production compute rental; the compute-capital price is held at pK_Q. """ L = L_endowment - p.tau_s * D QP = max(KQ - p.qbar * D, 0.0) RK = 1.0 / p.beta - 1.0 + p.delta T = p.tasks.bundle(QP, L) K = T * (p.alpha / RK) ** (1.0 / (1.0 - p.alpha)) pr = p.tasks.prices(0.0, K, QP, L) Y = pr['Y'] IQ = p.delta_Q * KQ * p.pK_Q fee = pr['pQ'] * p.qbar + p.pE * p.ebar C = Y - p.delta * K - IQ - p.pE * p.ebar * D assert C > 0, "non-positive steady-state consumption" return dict(K=K, L=L, QP=QP, KQ=KQ, D=D, Y=Y, C=C, RK=RK, R=1.0 - p.delta + RK, w=pr['w'], pQ=pr['pQ'], iota=pr['iota'], share_L=pr['share_L'], share_Q=pr['share_Q'], share_K=pr['share_K'], fee=fee, IQ=IQ, I=p.delta * K, user_cost_Q=(1.0 / p.beta - 1.0 + p.delta_Q) * p.pK_Q) # --------------------------------------------------------------------------- 2. linearised household (sequence space) def _solve_household(p: EconParams, ss: dict, what: np.ndarray, Rhat: np.ndarray, a0: float = 0.0, windfall: float = 0.0, ge: bool = False, z: np.ndarray | None = None): """Solve the linearised household problem for given log-deviation price paths (length T+1). Unknowns: c_hat[0..T], a_hat[1..T+1]. Equations: budget (T+1), Euler (T), terminal a_{T+1} = a_T (1). `windfall` adds a lump-sum (in units of abar) to beginning-of-period wealth at t=0 (for the MPC). With ge=True the aggregate capital path equals the household's own path (symmetric equilibrium) and prices respond: Y_hat = z + alpha a_hat, w_hat = Y_hat, R_hat = (RK/R)(z - (1-alpha) a_hat); `what`/`Rhat` are then ignored. Returns (c_hat, a_hat) with a_hat indexed 0..T+1. """ T = len(what) - 1 cbar, abar, wbar, Rbar, L = ss['C'], ss['K'], ss['w'], ss['R'], ss['L'] rr = ss['RK'] / ss['R'] n = 2 * T + 2 A = np.zeros((n, n)) b = np.zeros(n) ic = lambda t: t # c_hat[t], t = 0..T ia = lambda t: T + 1 + (t - 1) # a_hat[t], t = 1..T+1 if ge: what = z.copy() Rhat = rr * z row = 0 for t in range(T + 1): # budget: cbar c_t + abar a_{t+1} = wbar L w_t + Rbar abar (R_t + a_t) A[row, ic(t)] = cbar A[row, ia(t + 1)] = abar rhs = wbar * L * what[t] + Rbar * abar * Rhat[t] if t == 0: rhs += Rbar * abar * a0 + abar * windfall else: A[row, ia(t)] -= Rbar * abar if ge: # price responses to own (= aggregate) capital A[row, ia(t)] -= wbar * L * p.alpha A[row, ia(t)] -= Rbar * abar * rr * (-(1.0 - p.alpha)) b[row] = rhs row += 1 for t in range(T): # Euler: c_t = c_{t+1} - (1/gamma) R_{t+1} A[row, ic(t)] = 1.0 A[row, ic(t + 1)] = -1.0 b[row] = -(1.0 / p.gamma) * Rhat[t + 1] if ge: A[row, ia(t + 1)] -= (1.0 / p.gamma) * rr * (-(1.0 - p.alpha)) row += 1 A[row, ia(T + 1)] = 1.0 # terminal: a_{T+1} = a_T A[row, ia(T)] = -1.0 x = np.linalg.solve(A, b) c_hat = x[: T + 1] a_hat = np.concatenate([[a0], x[T + 1:]]) return c_hat, a_hat def _price_paths_pe(p: EconParams, ss: dict, theta: float, T: int): """Price paths when the aggregate capital path is fixed and z_s = rho^(s-1) theta for s>=1 (news at t=0).""" z = np.zeros(T + 1) for s in range(1, T + 1): z[s] = p.rho_z ** (s - 1) * theta what = z.copy() # w = (1-a)(1-iota) Y/L, Y ∝ e^z with K, QP, L fixed Rhat = (ss['RK'] / ss['R']) * z # R = 1 - delta + RK, RK ∝ e^z return what, Rhat, z def _solve_ge(p: EconParams, ss: dict, theta: float, T: int): """Symmetric rational-expectations response (aggregate k_hat = a_hat), one linear solve.""" what, Rhat, z = _price_paths_pe(p, ss, theta, T) return _solve_household(p, ss, what, Rhat, ge=True, z=z) def linearized_household(p: EconParams, ss: dict, T: int = 400) -> dict: theta = 1.0 what, Rhat, _ = _price_paths_pe(p, ss, theta, T) c_pe, a_pe = _solve_household(p, ss, what, Rhat) kappa0 = a_pe[1] / theta c_ge, a_ge = _solve_ge(p, ss, theta, T) kappa_full = a_ge[1] / theta alpha = 1.0 - kappa0 / kappa_full # MPC out of a beginning-of-period windfall (units: consumption per unit of wealth) c_w, a_w = _solve_household(p, ss, np.zeros(T + 1), np.zeros(T + 1), windfall=1.0) MPC = c_w[0] * ss['C'] / ss['K'] # dc/da in levels cbar = ss['C'] Uxx_level = p.u2(cbar) + p.beta * p.u2(cbar) * ss['R'] * MPC Uxx_log = ss['K'] ** 2 * Uxx_level return dict(kappa0=kappa0, kappa_full=kappa_full, alpha=alpha, MPC=MPC, Uxx_level=Uxx_level, Uxx_log=Uxx_log, a_pe=a_pe, a_ge=a_ge, c_pe=c_pe, c_ge=c_ge) def lq_params_from_economy(p: EconParams, ss: dict, lin: dict | None = None) -> tuple[LQParams, dict]: lin = lin or linearized_household(p, ss) lqp = LQParams(kappa0=lin['kappa0'], alpha=lin['alpha'], tau_theta=p.tau_theta, tau_H=p.tau_H, tau_A=p.tau_A, M=p.M) return lqp, lin # --------------------------------------------------------------------------- 5. delegation share and compute market def private_value_goods(p: EconParams, ss: dict, lqp: LQParams, lin: dict, D: float) -> float: """Private value of delegation in goods per year. Savings mapping: PV (units |U_xx|) * |U_xx|_log / u'(c). Portfolio mapping: PV * |U_xx|_goods (already in goods). """ if 'Uxx_goods' in lin: return private_value(lqp, D) * lin['Uxx_goods'] return private_value(lqp, D) * abs(lin['Uxx_log']) / p.u1(ss['C']) def delegation_share(p: EconParams, ss: dict, lqp: LQParams, lin: dict, pQ: float, D_guess: float = 0.0) -> float: """D = F_kappa( PV_goods(D) - fee(pQ) - tau_s w ) solved as a fixed point (bisection on D).""" fee = pQ * p.qbar + p.pE * p.ebar kap = lognorm(s=p.kappa_sigma, scale=np.exp(p.kappa_mu) * ss['C']) def f(D): net = private_value_goods(p, ss, lqp, lin, D) - fee - p.tau_s * ss['w'] return D - (kap.cdf(net) if net > 0 else 0.0) if f(0.0) >= 0: return 0.0 if f(1.0) <= 0: return 1.0 return float(brentq(f, 0.0, 1.0, xtol=1e-10)) def compute_market_short_run(p: EconParams, ss: dict, lqp: LQParams, lin: dict, KQ: float, z: float = 0.0) -> dict: """Short-run equilibrium with fixed K, L, K_Q: find Q_P such that Q_P + qbar D(pQ(Q_P)) = K_Q (A.4). pQ(Q_P) is the production compute rental when the firm uses Q_P (decreasing in Q_P); D(pQ) is the delegation share at that rental (increasing in Q_P). Excess demand is increasing in Q_P -> unique root. """ K, L = ss['K'], ss['L'] if KQ <= 0: return dict(QP=0.0, D=0.0, pQ=0.0, **p.tasks.prices(z, K, 0.0, L)) def excess(QP): pr = p.tasks.prices(z, K, QP, L) D = delegation_share(p, ss, lqp, lin, pr['pQ']) return QP + p.qbar * D - KQ if excess(KQ) <= 0: # all compute to production, delegation demand nil QP = KQ else: QP = brentq(excess, 1e-10, KQ, xtol=1e-12) pr = p.tasks.prices(z, K, QP, L) D = delegation_share(p, ss, lqp, lin, pr['pQ']) fee = pr['pQ'] * p.qbar + p.pE * p.ebar return dict(QP=QP, D=D, fee=fee, KQ=KQ, **pr) # --------------------------------------------------------------------------- portfolio (allocation) mapping def portfolio_lq_params(p: EconParams, ss: dict, investors_earn_wages: bool = True, M: int | None = None) -> dict: """Mean-variance allocation of wealth between risky capital claims and a safe store of value at fixed R_f. k_i = (E_i[R^K'] - R_f)/(gamma_a sigma_R^2), gamma_a = gamma / W (W = steady-state wealth = K). Next-period expected return: E[R^K'] = Rbar^K [1 - (1-alpha) X] + theta, theta ~ N(0, sig_theta_R^2) the predictable return component (news), X the aggregate log capital deviation. Hence, in log holdings, x_i = kappa0 E_i[theta] + alpha_s E_i[X], kappa0 = 1/(gamma sigma_R^2), alpha_s = -(1-alpha) Rbar^K kappa0. Certainty-equivalent loss of a holding error x (log units) is (1/2) gamma sigma_R^2 K x^2 -> |U_xx| in goods. CRS restriction (Lemma 2) holds iff investors also receive wage income (U_XX = -U_xX); with separate investors and workers U_XX = 0 (upsilon = 0) and Proposition 4(a) applies. Signals in return units: tau = 1/sigma^2. """ kappa0 = 1.0 / (p.gamma * p.sigma_R ** 2) alpha_s = -(1.0 - p.alpha) * ss['RK'] * kappa0 Uxx_goods = p.gamma * p.sigma_R ** 2 * ss['K'] ups = 1.0 if investors_earn_wages else 0.0 lqp = LQParams(kappa0=kappa0, alpha=alpha_s, tau_theta=p.sig_theta_R ** -2, tau_H=p.sig_H_R ** -2, tau_A=p.sig_A_R ** -2, M=M or p.M, upsilon=ups) return dict(kappa0=kappa0, alpha=alpha_s, Uxx_goods=Uxx_goods, upsilon=ups, lqp=lqp)