1"""Task-based production with compute (Model A, §1.3 of docs/model_specification.md).23Y = e^z K^alpha T^(1-alpha), ln T = ∫ ln y(x) dx (Cobb–Douglas over tasks x in [0,1]).4Tasks x <= iota are produced by compute with productivity psi(x) = psi0 * exp(-psi1 x); the rest by labour.5With unit elasticity across tasks the bundle is67 T = Psi(iota) * (Q_P/iota)^iota * (L/(1-iota))^(1-iota), Psi(iota) = psi0^iota * exp(-psi1 iota^2 / 2).89The automated set is [0, iota] with iota = min(iota_bar, iota*) and psi(iota*) = p_Q / w (threshold condition),10which combined with the factor-price conditions gives psi(iota) = iota/(1-iota) * L/Q_P (A.3).1112All functions are pure numpy; no state.13"""14from __future__ import annotations1516from dataclasses import dataclass1718import numpy as np19from scipy.optimize import brentq202122@dataclass(frozen=True)23class TaskTech:24 alpha: float = 0.3325 iota_bar: float = 0.526 psi0: float = 1.027 psi1: float = 3.02829 def psi(self, x):30 return self.psi0 * np.exp(-self.psi1 * np.asarray(x, dtype=float))3132 def Psi(self, iota):33 """exp(∫_0^iota ln psi(x) dx) = psi0^iota * exp(-psi1 iota^2/2)."""34 iota = np.asarray(iota, dtype=float)35 return self.psi0 ** iota * np.exp(-self.psi1 * iota ** 2 / 2.0)3637 # ------------------------------------------------------------------ automation threshold38 def iota_of(self, QP: float, L: float) -> float:39 """Automated task share from (A.3): psi(iota) = iota/(1-iota) * L/QP, capped at iota_bar.4041 LHS is decreasing in iota, RHS increasing from 0 to +inf, hence a unique interior root for QP > 0.42 """43 if QP <= 1e-9 * L:44 return 0.045 f = lambda i: np.log(self.psi(i)) - np.log(i / (1.0 - i)) - np.log(L / QP)46 # f(0+) = +inf, f(1-) = -inf -> unique root47 lo, hi = 1e-9, 1.0 - 1e-948 if f(lo) <= 0:49 return 0.050 root = brentq(f, lo, hi, xtol=1e-14, rtol=1e-12, maxiter=200)51 return float(min(root, self.iota_bar))5253 # ------------------------------------------------------------------ bundle and output54 def bundle(self, QP: float, L: float, iota: float | None = None) -> float:55 if iota is None:56 iota = self.iota_of(QP, L)57 if iota <= 0.0:58 return float(L)59 return float(self.Psi(iota) * (QP / iota) ** iota * (L / (1.0 - iota)) ** (1.0 - iota))6061 def output(self, z: float, K: float, QP: float, L: float, iota: float | None = None) -> float:62 T = self.bundle(QP, L, iota)63 return float(np.exp(z) * K ** self.alpha * T ** (1.0 - self.alpha))6465 # ------------------------------------------------------------------ prices (competitive, CRS)66 def prices(self, z: float, K: float, QP: float, L: float) -> dict:67 """Rental rate of capital, wage, compute rental, output, automated share and factor shares.6869 When iota is at the frontier iota_bar (corner), the compute price is still the marginal product of compute70 in the automated tasks, (1-alpha) iota Y / QP, and the threshold condition holds with inequality.71 """72 iota = self.iota_of(QP, L)73 Y = self.output(z, K, QP, L, iota)74 RK = self.alpha * Y / K75 w = (1.0 - self.alpha) * (1.0 - iota) * Y / L76 pQ = (1.0 - self.alpha) * iota * Y / QP if QP > 0 else 0.077 return dict(Y=Y, iota=iota, RK=RK, w=w, pQ=pQ,78 share_K=self.alpha, share_Q=(1.0 - self.alpha) * iota, share_L=(1.0 - self.alpha) * (1.0 - iota))7980 def production_compute_demand(self, pQ: float, w: float, L: float, z: float, K: float) -> float:81 """Compute demanded by the firm at rental pQ given the wage (used for compute-market clearing).8283 From the FOCs: pQ = (1-alpha) iota Y/QP and w = (1-alpha)(1-iota) Y/L -> QP = iota/(1-iota) * L * w/pQ,84 with iota from the threshold psi(iota) = pQ/w (capped at iota_bar). If pQ/w > psi(0), no task is automated.85 """86 ratio = pQ / w87 if ratio >= self.psi0:88 return 0.089 iota = min(self.iota_bar, float(-np.log(ratio / self.psi0) / self.psi1))90 return float(iota / (1.0 - iota) * L / ratio)91