TeX 53.2%
Python 46.6%
1"""Household solver: feasibility, monotonicity, terminal condition, Bellman residuals, thresholds."""2import numpy as np34from src import household_solver as HS5from src import model as M6from src.household_solver import NEG789def test_values_finite_and_monotone_in_assets(P_small, pols):10 for j in range(1, P_small.J + 1):11 V = pols[j - 1].V_rent12 feas = V > NEG / 213 assert feas[:, 5:].all()14 for iz in range(P_small.n_z):15 v = V[iz][feas[iz]]16 assert np.all(np.diff(v) >= -1e-9), f"V_rent not increasing in a at age {j}, z {iz}"171819def test_owner_value_decreasing_in_debt(P_small, pols):20 for j in (2, 5, 8):21 V = pols[j - 1].V_own22 for iz in range(P_small.n_z):23 for ih in range(P_small.n_h):24 v = V[iz, ih, :, P_small.N - 2, -1] # across balances at the top asset25 v = v[v > NEG / 2]26 assert np.all(np.diff(v) <= 1e-9)272829def test_policies_feasible(P_small, pols, prices):30 P = P_small31 for j in range(1, P.J + 1):32 pol = pols[j - 1]33 assert np.all(pol.aR >= 0) and np.all(pol.aB >= 0) and np.all(pol.aS >= 0) and np.all(pol.aF >= 0)34 feasB = pol.W_B > NEG / 235 assert np.all(pol.hB[feasB] >= 0)36 # loan never above the LTV / DSTI cap37 cap = pol.maxloan[np.arange(P.n_z)[:, None], np.maximum(pol.hB, 0)]38 assert np.all(pol.BB[feasB] <= cap[feasB] + 1e-9)39 assert np.all(pol.BB[feasB] <= (1 - P.theta) * prices.p * P.h_grid[np.maximum(pol.hB, 0)][feasB] + 1e-9)40 # gifts only at the gift age41 if j != P.j_gift:42 assert np.all(pol.gB == 0) and np.all(pol.gR == 0) and np.all(pol.gS == 0)434445def test_terminal_condition_is_warm_glow(P_small, prices):46 P = P_small47 EV_own, EV_rent = HS.terminal_continuation(P, prices)48 a = P.a_grid49 assert np.allclose(EV_rent[0], M.warm_glow(P, (1 - P.tau_b) * a))50 # an owner with no debt: estate = a + (1 - kappa_s) p h51 est = a + (1 - P.kappa_s) * prices.p * P.h_grid[1]52 assert np.allclose(EV_own[0, 1, 0, 0, :], M.warm_glow(P, (1 - P.tau_b) * est))53 # negative estate (underwater at death) is infeasible54 big_b = P.b_grid[-1]55 if big_b > (1 - P.kappa_s) * prices.p * P.h_grid[0]:56 assert EV_own[0, 0, -1, 0, 0] <= NEG / 2575859def test_bellman_residuals_small(P_small, pols, prices, lottery):60 res = HS.bellman_residuals(P_small, pols, prices, lottery, n_points=200, n_fine=300)61 assert res["owner_mean"] < 5e-3 and res["renter_mean"] < 5e-362 assert res["owner_max"] < 5e-2 and res["renter_max"] < 2e-2636465def test_ownership_threshold_single_crossing(P_small, pols, prices):66 P = P_small67 th = HS.ownership_threshold(P, pols[0], prices, 1)68 for iz, v in th.items():69 assert np.isfinite(v["x_hat"])70 assert v["sign_changes"] == 1, f"tenure choice at age 1 not single-crossing for z index {iz}"71 # thresholds (in cash) are above the pure feasibility cash need72 assert all(v["x_hat"] > v["feasibility_cash"] for v in th.values())737475def test_threshold_rises_with_price_and_down_payment(P_small, lottery):76 P = P_small77 base = HS.ownership_threshold(P, HS.solve_lifecycle(P, M.stationary_prices(P, 1.0, 1.0), lottery)[0], M.stationary_prices(P, 1.0, 1.0), 1)78 hi_p = HS.ownership_threshold(P, HS.solve_lifecycle(P, M.stationary_prices(P, 1.08, 1.0), lottery)[0], M.stationary_prices(P, 1.08, 1.0), 1)79 iz = P.n_z // 280 assert hi_p[iz]["x_hat"] > base[iz]["x_hat"]81 cfg = M.load_config(overrides={"mortgage.theta": 0.35, "simulation.n_dynasties": P.n_dyn,82 "grids.n_a": P.n_a, "grids.n_x": P.n_x})83 P2 = M.build_params(cfg)84 pr2 = M.stationary_prices(P2, 1.0, 1.0)85 hi_theta = HS.ownership_threshold(P2, HS.solve_lifecycle(P2, pr2, lottery)[0], pr2, 1)86 assert hi_theta[iz]["x_hat"] > base[iz]["x_hat"]87