1# Author: Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca2"""Checks of the theory module against the closed forms stated in paper/sections/theory.tex and welfare.tex."""3from __future__ import annotations45import itertools67import numpy as np8import pytest910from agentic_valuation.theory import aggregation_sim as ag11from agentic_valuation.theory import welfare as wf1213SEED = 20261027141516# ----------------------------------------------------------------------------------------- Propositions 1–317@pytest.mark.parametrize("K,rho", [(1, 0.0), (3, 0.2), (12, 0.4), (12, 0.0), (48, 0.8), (6, -0.1)])18def test_closed_form_mse_matches_simulation(K, rho):19 rng = np.random.default_rng(SEED)20 closed = ag.mse_mean_closed_form(K, 1.0, rho)21 sim = ag.simulate_mean_mse(K, 1.0, rho, 200_000, rng)22 assert abs(sim - closed) < 0.02 * max(closed, 0.05) + 1e-3232425def test_exchangeable_equal_weights_are_optimal_and_floor():26 for K, rho in itertools.product([2, 5, 12], [0.0, 0.3, 0.7]):27 S = ag.exchangeable_cov(K, 1.0, rho)28 assert ag.mse_optimal(S) == pytest.approx(ag.mse_equal(S), rel=1e-10)29 assert ag.mse_equal(S) >= rho # floor rho * sigma^2 is never beaten by K30 assert ag.mse_mean_closed_form(10_000, 1.0, 0.5) == pytest.approx(0.5, abs=1e-3)313233def test_general_sigma_optimal_dominates_equal_weights():34 rng = np.random.default_rng(SEED)35 for _ in range(20):36 A = rng.standard_normal((8, 8))37 S = A @ A.T + 0.1 * np.eye(8)38 assert ag.mse_optimal(S) <= ag.mse_equal(S) + 1e-1239 S = ag.exchangeable_cov(5, 2.0, 0.3) # rows with equal sums: equality40 assert ag.mse_optimal(S) == pytest.approx(ag.mse_equal(S))414243def test_nested_common_model_closed_form():44 for n, M in [(12, 1), (6, 2), (4, 3), (1, 12)]:45 S = ag.nested_cov(n, M, 0.3, 0.4, 0.5)46 assert ag.mse_equal(S) == pytest.approx(ag.mse_nested_closed_form(n, M, 0.3, 0.4, 0.5))47 # more providers at fixed K lowers the pooled MSE; the common component is a floor48 vals = [ag.mse_nested_closed_form(12 // M, M, 0.3, 0.4, 0.5) for M in (1, 2, 3, 4, 6, 12)]49 assert all(a > b for a, b in zip(vals, vals[1:]))50 assert min(vals) >= 0.3**2515253# ----------------------------------------------------------------------------------------- Propositions 4, 754def test_opinion_only_deliberation_contracts_dispersion_and_keeps_mean():55 rng = np.random.default_rng(SEED)56 x0 = rng.standard_normal((50_000, 12)) * 0.257 lam = 0.458 for anchor in ("mean", "median"):59 traj = ag.deliberate_opinion_only(x0, 4, lam, anchor)60 d0 = traj[0].std(axis=1)61 for r in range(5):62 np.testing.assert_allclose(traj[r].std(axis=1), d0 * (1 - lam) ** r, rtol=1e-10)63 # the anchoring statistic is invariant across rounds64 stat = np.median if anchor == "median" else np.mean65 np.testing.assert_allclose(stat(traj[r], axis=1), stat(traj[0], axis=1), atol=1e-12)66 if anchor == "mean":67 np.testing.assert_allclose(traj[4].mean(axis=1), traj[0].mean(axis=1), atol=1e-12)68 else: # exact formula: MSE_r = MSE_0 + (1 - (1-lam)^r)^2 (MSE_med - MSE_mean)69 m0 = np.mean(traj[0].mean(axis=1) ** 2)70 md = np.mean(np.median(traj[0], axis=1) ** 2)71 for r in range(5):72 mr = np.mean(traj[r].mean(axis=1) ** 2)73 assert mr == pytest.approx(m0 + (1 - (1 - lam) ** r) ** 2 * (md - m0), rel=0.02)74 assert md >= m0 # the median is dominated by the mean under Gaussian errors757677def test_information_conservation_and_over_shrinkage():78 sig2 = np.linspace(0.05, 0.4, 12) ** 279 S0 = np.diag(sig2)80 best0 = ag.mse_optimal(S0)81 prev = best082 for r in range(1, 6):83 Sr = ag.deliberation_cov(S0, r, 0.5)84 assert ag.mse_optimal(Sr) >= best0 - 1e-12 # no pool of round r beats the best round-0 pool85 iv = ag.mse_pool(ag.inverse_variance_weights(Sr), Sr)86 assert iv > prev # strictly dominated and worsening (heterogeneous variances)87 prev = iv88 assert prev <= ag.mse_equal(S0) + 1e-12 and prev == pytest.approx(ag.mse_equal(S0), rel=0.05)899091def test_degroot_consensus_is_round0_mean_for_complete_topology():92 K, lam = 7, 0.393 W = (1 - lam) * np.eye(K) + lam * np.ones((K, K)) / K94 np.testing.assert_allclose(ag.degroot_consensus_weights(W), np.full(K, 1 / K), atol=1e-10)959697# ----------------------------------------------------------------------------------------- Proposition 698def test_evidence_exchange_closed_form_and_sign():99 rng = np.random.default_rng(SEED)100 K, J = 12, 28101 views = [np.array([0, 1, 2, 3, 4 + 2 * k, 5 + 2 * k]) for k in range(K)]102 comm = ag.EvidenceCommittee(J=J, views=views, providers=np.arange(K) % 3, tau2=np.full(K, 0.01),103 sigma_c=0.05, sigma_u=0.12, sigma_g=0.04)104 full = ag.exchange_views(views)105 before, after = comm.mse_equal_closed_form(), comm.mse_equal_closed_form(full)106 assert after < before107 assert after == pytest.approx(0.05**2 + 0.12**2 / J + 0.04**2 / 3 + 0.01 / K)108 res = ag.simulate_deliberation(comm, 1, 0.0, 60_000, rng, exchange_at=1)109 assert res["mse_mean"][0] == pytest.approx(before, rel=0.03)110 assert res["mse_mean"][1] == pytest.approx(after, rel=0.03)111 # exchange restricted to the common view is vacuous112 same = ag.exchange_views(views, cited=[np.array([0, 1, 2, 3])] * K)113 assert comm.mse_equal_closed_form(same) == pytest.approx(before)114 # persuasion noise (independent of the errors) raises the MSE115 res2 = ag.simulate_deliberation(comm, 2, 0.3, 60_000, rng, anchor="mean", persuasion_sd=0.05)116 assert res2["mse_mean"][2] > res2["mse_mean"][0]117118119def test_partial_exchange_can_raise_mse():120 ex = ag.EvidenceCommittee(J=2, views=[np.array([0]), np.array([1]), np.array([1])], providers=np.zeros(3, int),121 tau2=np.zeros(3), sigma_u=1.0)122 before = ex.mse_equal_closed_form()123 partial = ex.mse_equal_closed_form(views=[np.array([0, 1]), np.array([1]), np.array([1])])124 assert before == pytest.approx(5 / 9) and partial == pytest.approx(13 / 18) and partial > before125126127# ----------------------------------------------------------------------------------------- Proposition 8128def test_placebo_invariance_under_separability():129 rng = np.random.default_rng(SEED)130 K, J = 12, 28131 views = [np.array([0, 1, 2, 3, 4 + 2 * k, 5 + 2 * k]) for k in range(K)]132 tau2 = np.linspace(0.05, 0.3, K) ** 2133 comm = ag.EvidenceCommittee(J=J, views=views, providers=np.arange(K) % 3, tau2=tau2, sigma_c=0.05,134 sigma_u=0.12, sigma_g=0.04)135 base = comm.mse_equal_closed_form()136 shocks = comm.draw_shocks(50_000, rng)137 x_id = comm.draw(50_000, rng, shocks=shocks)138 for _ in range(10):139 perm = rng.permutation(K)140 assert ag.placebo_mse(comm, perm) == pytest.approx(base)141 x_p = comm.draw(50_000, rng, tau2=tau2[perm], shocks=shocks)142 assert np.mean(x_p.mean(axis=1) ** 2) == pytest.approx(np.mean(x_id.mean(axis=1) ** 2), rel=0.03)143 # a prompt x view complementarity (variance depending on the match) breaks the invariance144 tau2_match = tau2.copy()145 tau2_match[0] = 1.0 # slot 0 mismatched prompt146 assert comm.mse_equal_closed_form(tau2=tau2_match) > base147148149# ----------------------------------------------------------------------------------------- Welfare150def test_failure_probability_monotone_for_financeable_matches():151 p = wf.WelfareParams()152 u_s = np.array([-0.10, -0.05, 0.0]) # financeable at the error-free appraisal (a < 0)153 prev = wf.failure_prob_given_seller(u_s, 0.0, 0.01, p)154 for s in np.linspace(0.02, 0.3, 15):155 cur = wf.failure_prob_given_seller(u_s, 0.0, float(s), p)156 assert np.all(cur >= prev - 1e-12)157 prev = cur158 # undervaluation bias raises, overvaluation lowers, the failure probability159 f_neg = wf.failure_prob_given_seller(u_s, -0.03, 0.1, p)160 f_pos = wf.failure_prob_given_seller(u_s, 0.03, 0.1, p)161 assert np.all(f_neg > f_pos)162 # a larger haircut raises it163 assert np.all(wf.failure_prob_given_seller(u_s, 0.0, 0.1, wf.with_params(p, h1=0.5)) >164 wf.failure_prob_given_seller(u_s, 0.0, 0.1, p))165 # non-financeable matches (a > 0): noise lowers the failure probability166 u_hi = np.array([0.10])167 assert wf.failure_prob_given_seller(u_hi, 0.0, 0.2, p) < wf.failure_prob_given_seller(u_hi, 0.0, 0.05, p)168169170def test_market_simulation_failure_rate_increasing_in_s():171 rng = np.random.default_rng(SEED)172 p = wf.WelfareParams()173 shocks = wf.draw_market_shocks(100_000, rng)174 rates = [wf.simulate_market(0.0, s, p, 100_000, rng, shocks=shocks)["failure_rate"] for s in (0.02, 0.05, 0.1, 0.2)]175 assert all(a < b for a, b in zip(rates, rates[1:]))176 exact = wf.simulate_market(0.0, 0.1, p, 100_000, rng, shocks=shocks)177 assert exact["failure_rate"] == pytest.approx(exact["failure_rate_exact"], abs=0.01)178179180def test_expected_loss_increasing_in_bias_and_spread():181 p = wf.WelfareParams()182 els = [wf.expected_loss(b, 0.1, p) for b in (-0.05, 0.0, 0.05, 0.1)]183 assert all(a < b for a, b in zip(els, els[1:]))184 els = [wf.expected_loss(0.0, s, p) for s in (0.0, 0.05, 0.1, 0.2, 0.3)]185 assert all(a < b for a, b in zip(els, els[1:]))186187188def test_systemic_mean_flat_and_dispersion_increasing_in_rho():189 rng = np.random.default_rng(SEED)190 p = wf.WelfareParams()191 N, n = 30, 20_000192 shocks = {"C": rng.standard_normal(n), "E": rng.standard_normal((n, N))}193 res = [wf.systemic_loss(r, 0.0, 0.1, p, N, n, rng, shocks=shocks) for r in (0.0, 0.25, 0.5, 0.75, 1.0)]194 means = [x["mean"] for x in res]195 assert max(means) - min(means) < 0.03 * np.mean(means)196 sds = [x["sd"] for x in res]197 covs = [x["cov_pair"] for x in res]198 assert all(a < b for a, b in zip(sds, sds[1:]))199 assert all(a <= b + 1e-12 for a, b in zip(covs, covs[1:]))200