1"""Closed forms of the LQ delegation game vs Monte Carlo, and Proposition 3 (zero wedge)."""2import numpy as np3import pytest4from hypothesis import given, settings, strategies as st56from age.equilibrium import lq_game as lq78P = lq.LQParams(kappa0=1.0, alpha=-0.3, tau_theta=1.0, tau_H=1.0, tau_A=4.0, M=2)91011def test_best_response_consistency():12 """Strategies satisfy x_i = E_i[kappa0 theta + alpha X] for both agent types."""13 D = 0.414 s = lq.strategies(P, D)15 bH, bA = P.beta_H, P.beta_A16 # non-delegator: a_H = (kappa0 + alpha Abar) bH17 assert np.isclose(s['aH'], (P.kappa0 + P.alpha * s['Abar']) * bH)18 # delegator: a_A = bA (kappa0 + alpha Abar) + alpha (D/M)(1-bA) a_A19 assert np.isclose(s['aA'], bA * (P.kappa0 + P.alpha * s['Abar']) + P.alpha * (D / P.M) * (1 - bA) * s['aA'])202122def test_full_info_limits():23 """tau_A -> inf with D=1 reproduces the full-information response kappa0/(1-alpha)."""24 p = lq.LQParams(kappa0=1.0, alpha=-0.3, tau_theta=1.0, tau_H=1.0, tau_A=1e9, M=1)25 s = lq.strategies(p, 1.0)26 assert np.isclose(s['Abar'], p.kappa0 / (1 - p.alpha), rtol=1e-6)27 assert lq.moments(p, 1.0)['V_noise'] < 1e-8282930def test_moments_vs_monte_carlo():31 D = 0.532 m = lq.moments(P, D)33 sim = lq.simulate(P, D, n_agents=4000, n_draws=3000, seed=1)34 assert np.isclose(sim['EX2'], m['EX2'], rtol=0.08)35 assert np.isclose(sim['Ex2'], m['Ex2'], rtol=0.05)36 assert np.isclose(sim['V_disp'], m['V_disp'], rtol=0.05)37 assert np.isclose(sim['Abar_hat'], m['Abar'], rtol=0.05)383940def test_lln_breaks_only_for_delegators():41 """Aggregate noise is zero at D=0 and grows with D^2/M."""42 assert lq.moments(P, 0.0)['V_noise'] == 0.043 v1 = lq.moments(lq.LQParams(**{**P.__dict__, 'M': 1}), 0.6)['V_noise']44 v4 = lq.moments(lq.LQParams(**{**P.__dict__, 'M': 4}), 0.6)['V_noise']45 assert v4 < v1464748def test_welfare_decomposition_matches_quadratic_form():49 """Under CRS (upsilon=1, U_Xtheta=0) the AP decomposition equals the exact quadratic welfare up to a constant."""50 Ds = np.linspace(0, 1, 11)51 exact = np.array([lq.welfare_gross(P, D) for D in Ds])52 dec = np.array([lq.welfare_decomposed(P, D)['W_rel'] for D in Ds])53 diff = exact - dec54 assert np.allclose(diff, diff[0], atol=1e-10)555657@settings(max_examples=60, deadline=None)58@given(alpha=st.floats(-0.95, 0.95), D=st.floats(0.0, 1.0), M=st.integers(1, 6),59 tau_H=st.floats(0.1, 10), tau_A=st.floats(0.1, 20), tau_t=st.floats(0.1, 10))60def test_proposition3_zero_wedge(alpha, D, M, tau_H, tau_A, tau_t):61 p = lq.LQParams(kappa0=1.0, alpha=alpha, tau_theta=tau_t, tau_H=tau_H, tau_A=tau_A, M=M)62 assert abs(lq.wedge(p, D)) < 1e-5636465def test_proposition4a_nonzero_wedge_without_crs():66 p = lq.LQParams(kappa0=1.0, alpha=-1 / 3, tau_theta=1, tau_H=1, tau_A=4, M=1, upsilon=0.0)67 assert lq.wedge(p, 0.5) > 1e-3 # over-delegation when the aggregate externality is weaker than pecuniary68 p2 = lq.LQParams(**{**p.__dict__, 'upsilon': 2.0})69 assert lq.wedge(p2, 0.5) < -1e-3707172def test_private_value_alpha0_closed_form():73 p = lq.LQParams(kappa0=1.3, alpha=0.0, tau_theta=2.0, tau_H=0.5, tau_A=6.0, M=3)74 pv = lq.private_value(p, 0.3)75 closed = p.kappa0 ** 2 / 2 * (p.tau_A - p.tau_H) / ((p.tau_A + p.tau_theta) * (p.tau_H + p.tau_theta))76 assert np.isclose(pv, closed)777879def test_delegation_equilibrium_threshold_and_comparative_statics():80 from scipy.stats import lognorm81 cdf = lambda c: lognorm(s=1.0, scale=np.exp(-2.5)).cdf(np.maximum(c, 0))82 D1 = lq.delegation_equilibrium(P, cdf, scale=1.0)83 D2 = lq.delegation_equilibrium(lq.LQParams(**{**P.__dict__, 'tau_A': 8.0}), cdf, scale=1.0)84 assert 0 < D1 < 1 and D2 > D1 # better model -> more delegation85 # planner agrees with the market under CRS (Proposition 3)86 q = lambda u: lognorm(s=1.0, scale=np.exp(-2.5)).ppf(np.clip(u, 1e-9, 1 - 1e-9))87 Dp = lq.planner_delegation(P, cdf, q, scale=1.0)88 assert abs(Dp - D1) < 0.0189