TeX 53.2%
Python 46.6%
1"""Simulation: budget identities state by state, debt law of motion, feasibility, transfers,2market clearing and residuals of the small general equilibrium, zero-sum revaluation."""3import numpy as np4import pytest56from src import distributions as D7from src import equilibrium as E8from src import model as M91011def _hv(P, h, p):12 return np.where(h >= 0, P.h_grid[np.maximum(h, 0)], 0.0) * p131415def test_budget_identity_every_branch(P_small, sim, prices):16 """Uses = sources for every household with an unclipped policy, by branch."""17 P = P_small18 rec, _ = sim19 p, rho = rec.p, rec.rho20 y = P.ybar[rec.age - 1] * P.z_grid[rec.z]21 reb = np.where(rec.age == 1, prices.transfer_rebate, 0.0)22 sources = y + rec.a0 * prices.R_a + reb23 ok = rec.flag == 024 uses = np.full(len(rec.age), np.nan)25 own0 = rec.h0 >= 026 # stay27 m = ok & (rec.choice == 0)28 uses[m] = rec.c[m] + rec.pay[m] + P.m * _hv(P, rec.h0[m], p) + rec.a1[m] + rec.gift[m]29 # refinance30 m = ok & (rec.choice == 1)31 uses[m] = rec.c[m] + rec.a1[m] + rec.gift[m] + rec.b0[m] + (P.kappa_f + P.m) * _hv(P, rec.h0[m], p) - rec.Bnew[m] + rec.pay[m]32 # buy (renter or mover): sources include the sale of the old house net of the balance33 m = ok & (rec.choice == 2)34 sale = np.where(own0, (1 - P.kappa_s) * _hv(P, rec.h0, p) - rec.b0, 0.0)35 uses[m] = rec.c[m] + (1 + P.kappa_b + P.m) * _hv(P, rec.h1[m], p) - rec.Bnew[m] + rec.pay[m] + rec.a1[m] + rec.gift[m] - sale[m]36 # rent (sitting tenant pays rho, seller pays rho as a new tenant: identical in the stationary state)37 m = ok & (rec.choice == 3)38 uses[m] = rec.c[m] + rho * rec.s[m] + rec.a1[m] + rec.gift[m] - sale[m]39 gap = uses - sources40 assert ok.mean() > 0.9941 assert np.nanmax(np.abs(gap[ok])) < 1e-842 assert (rec.flag == 1).sum() == 0, "forced sales in the stationary baseline"434445def test_budget_identity_with_transitory_income(P_small, pols, prices):46 """With the transitory income component on, uses = sources with the realised income."""47 P = M.build_params(M.load_config(overrides={**{k: v for k, v in P_small.cfg["simulation"].items() and {}},48 "simulation.n_dynasties": P_small.n_dyn, "simulation.burn_periods": 6,49 "grids.n_a": P_small.n_a, "grids.n_x": P_small.n_x, "income.sigma_transitory": 0.03}))50 recs, _ = D.simulate_stationary(P, pols, prices, n_record=1, burn=6)51 rec = recs[0]52 assert rec.y_shock.std() > 0.0153 y = P.ybar[rec.age - 1] * P.z_grid[rec.z] * rec.y_shock54 sources = y + rec.a0 * prices.R_a55 m = (rec.flag == 0) & (rec.choice == 3) & (rec.h0 < 0) # sitting renters56 uses = rec.c[m] + rec.rho * rec.s[m] + rec.a1[m] + rec.gift[m]57 assert np.max(np.abs(uses - sources[m])) < 1e-858 m = (rec.flag == 0) & (rec.choice == 0) # owners who stay59 uses = rec.c[m] + rec.pay[m] + P.m * _hv(P, rec.h0[m], rec.p) + rec.a1[m] + rec.gift[m]60 assert np.max(np.abs(uses - sources[m])) < 1e-8616263def test_debt_law_of_motion_in_simulation(P_small, sim, prices):64 P = P_small65 rec, _ = sim66 m = (rec.choice == 0) & (rec.b0 > 0)67 b_next = np.array([M.next_balance(b, n, prices.i_new, prices.pi_next) for b, n in zip(rec.b0[m], rec.n0[m])])68 assert np.allclose(rec.b1[m], b_next, atol=1e-10)69 # new loans: balance after the first payment, remaining N-1 payments70 m = rec.Bnew > 071 b_next = np.array([M.next_balance(B, P.N, prices.i_new, prices.pi_next) for B in rec.Bnew[m]])72 assert np.allclose(rec.b1[m], b_next, atol=1e-10)73 assert np.all(rec.n1[m] == P.N - 1)74 # loans are repaid after N payments: nobody with n = 1 carries a balance forward75 m = (rec.choice == 0) & (rec.n0 == 1)76 assert np.all(rec.b1[m] < 1e-9)777879def test_feasibility_and_constraints(P_small, sim, prices):80 P = P_small81 rec, _ = sim82 assert np.all(rec.c >= P.c_min - 1e-12)83 assert np.all(rec.a1 >= 0)84 assert np.all(rec.s[rec.h1 < 0] >= P.s_min - 1e-12)85 assert np.all(rec.b1 >= 0)86 # LTV at origination87 m = rec.Bnew > 088 assert np.all(rec.Bnew[m] <= (1 - P.theta) * _hv(P, rec.h1[m], rec.p) + 1e-9)89 # DSTI at origination at the qualifying rate90 y = P.ybar[rec.age - 1] * P.z_grid[rec.z]91 assert np.all([M.payment(B, P.N, prices.i_q) <= P.psi * yy + 1e-8 for B, yy in zip(rec.Bnew[m], y[m])])92 # gifts only at the gift age, estates only at J93 assert np.all(rec.gift[rec.age != P.j_gift] == 0)94 assert np.all(rec.estate[rec.age != P.J] == 0)959697def test_estate_terminal_identity(P_small, sim):98 P = P_small99 rec, _ = sim100 m = rec.age == P.J101 est = rec.a1[m] + (1 - P.kappa_s) * _hv(P, rec.h1[m], rec.p) - rec.b1[m]102 assert np.allclose(rec.estate[m], np.maximum(est, 0), atol=1e-10)103104105def test_dynasty_link_consistency(P_small, sim):106 """Gifts received by age-1 members equal (1 - tau) x the gifts given at age j_gift in the same107 slot and period; the age distribution is uniform."""108 P = P_small109 rec, _ = sim110 counts = np.bincount(rec.age, minlength=P.J + 1)[1:]111 assert np.all(counts == counts[0])112 # per period: young age 1 and old age 7 share a slot113 n_rec = len(rec.age) // (2 * P.n_dyn)114 for k in range(n_rec):115 sl = slice(k * 2 * P.n_dyn, (k + 1) * 2 * P.n_dyn)116 a, g, gr, slot, old = rec.age[sl], rec.gift[sl], rec.gift_received[sl], rec.slot[sl], rec.is_old[sl]117 giv = {s: v for s, v, aa, o in zip(slot, g, a, old) if o and aa == P.j_gift}118 got = {s: v for s, v, aa, o in zip(slot, gr, a, old) if (not o) and aa == 1}119 assert set(giv) == set(got)120 assert all(abs((1 - P.tau_b) * giv[s] - got[s]) < 1e-10 for s in giv)121122123def test_lottery_is_mean_preserving(P_small, sim):124 P = P_small125 rec, _ = sim126 pts, w = D.estate_lottery(P, rec)127 e = rec.estate_received[rec.is_old & (rec.age == P.j_estate)]128 assert abs(pts @ w - e.mean()) < 1e-6 and np.all(np.diff(pts) >= 0)129130131def test_common_random_numbers(P_small, pols, prices):132 r1, _ = D.simulate_stationary(P_small, pols, prices, n_record=1, burn=6)133 r2, _ = D.simulate_stationary(P_small, pols, prices, n_record=1, burn=6)134 assert np.array_equal(r1[0].c, r2[0].c)135136137@pytest.mark.slow138def test_general_equilibrium_clears(ge_small):139 P, res = ge_small140 assert res.equilibrium == "GE"141 assert res.converged, f"small GE did not converge: residuals {res.residuals}"142 tol = P.cfg["equilibrium"]["tol_excess"]143 assert abs(res.residuals[0]) < tol and abs(res.residuals[1]) < tol144 # calibration normalises the baseline at p = p_r = 1145 assert abs(res.p - 1.0) < 0.01 and abs(res.ptil_r - 1.0) < 0.01146 assert res.bellman["owner_mean"] < 5e-3 and res.bellman["renter_mean"] < 5e-3147 # the lottery fixed point is consistent with the simulated estates148 assert np.max(np.abs(res.ev.lottery_new[0] - res.lottery[0])) < 5e-3 * max(1.0, res.lottery[0].max())149150151@pytest.mark.slow152def test_surprise_revaluation_zero_sum_and_contract_ordering(ge_small):153 """Partial-equilibrium impact of a 10 % surprise: households' real gain on nominal154 positions = - lender revaluation (identity), FRM = TERM5 for a level surprise (balance155 erosion only), ARM erodes nothing."""156 P, res = ge_small157 core = E.ss_core(res)158 out = {}159 for contract in ("frm", "term5", "arm"):160 sh = E.Shock(kind="inflation", pi_u=0.10, persistence=1, contract=contract)161 paths = E.shock_paths(P, sh, 4)162 out[contract] = paths163 assert abs(out["frm"]["balance_factor"] - 1 / 1.1) < 1e-12164 assert abs(out["term5"]["balance_factor"] - 1 / 1.1) < 1e-12165 assert out["arm"]["balance_factor"] == 1.0166 assert abs(out["frm"]["R_a"][0] - P.R / 1.1) < 1e-12 # nominal savers lose167 # persistent surprise: FRM contract rate stays at i*, term5 reprices to the new nominal rate168 sh = E.Shock(kind="inflation", pi_u=0.10, persistence=2, contract="frm")169 paths = E.shock_paths(P, sh, 4)170 assert paths["i_new"][0] > P.i_nom + 1e-6171 rates0 = E.frm_rate_by_n(P, paths, 0)172 assert np.allclose(rates0[: P.N - 1], P.i_nom) # pre-shock originations keep i*173 assert abs(rates0[P.N - 1] - paths["i_new"][0]) < 1e-12 # a loan with N payments left at t=0 is new174 assert abs(E.frm_rate_by_n(P, paths, 1)[P.N - 1] - paths["i_new"][1]) < 1e-12 # new loan at t=1 carries i_1175 assert abs(E.frm_rate_by_n(P, paths, 1)[P.N - 2] - paths["i_new"][0]) < 1e-12 # originated at t=0176 # zero sum: households' aggregate nominal revaluation equals minus the lender's177 rec = res.ev.rec178 bf = out["frm"]["balance_factor"]179 debt_gain = (1 - bf) * rec.b0180 dep_loss = rec.a0 * (P.R - P.R / 1.1)181 households = (debt_gain - dep_loss).sum()182 lender = -(1 - bf) * rec.b0.sum() + (P.R - P.R / 1.1) * rec.a0.sum()183 assert abs(households + lender) < 1e-8 * max(1.0, abs(households))184