"""F5-conf-04: VFI vs closed form for Brock-Mirman growth with log utility. Run from workspace root: ~/wp29/venv/bin/python out/analysis.py""" import json, os import numpy as np, pandas as pd df = pd.read_csv("data/model.csv") p = dict(zip(df["parameter"], df["value"])) alpha = float(p["alpha"]); beta = float(p["beta"]) y_min = float(p["y_min"]); y_max = float(p["y_max"]); N = int(float(p["grid_points"])) tol = float(p["vfi_tolerance"]); spacing = str(p["grid_spacing"]).strip() ev = [float(p["eval_y_1"]), float(p["eval_y_2"]), float(p["eval_y_3"])] assert spacing == "log" # ---- closed form (guess V = A + B ln y, verify) ---- ab = alpha * beta B = 1.0 / (1.0 - ab) A = (np.log(1 - ab) + (ab / (1 - ab)) * np.log(ab)) / (1 - beta) c_closed = lambda y: (1 - ab) * np.asarray(y) V_closed = lambda y: A + B * np.log(y) # ---- VFI: discrete choice of next-period output y' on the grid ---- grid = np.exp(np.linspace(np.log(y_min), np.log(y_max), N)) k_needed = grid ** (1.0 / alpha) # savings needed to reach y' C = grid[:, None] - k_needed[None, :] # consumption for (y, y') U = np.where(C > 0, np.log(np.where(C > 0, C, 1.0)), -np.inf) V = np.zeros(N) it = 0; sup = np.inf while sup >= tol: RHS = U + beta * V[None, :] Vn = RHS.max(axis=1) sup = float(np.max(np.abs(Vn - V))) V = Vn; it += 1 if it > 100000: raise RuntimeError("VFI did not converge") pol_idx = (U + beta * V[None, :]).argmax(axis=1) c_grid = grid - k_needed[pol_idx] c_num = np.interp(ev, grid, c_grid) # linear interpolation in y v_num_1 = float(np.interp(1.0, grid, V)) c_cl = c_closed(ev) err = np.abs(c_num - c_cl) # diagnostics (not required keys) sup_V_err = float(np.max(np.abs(V - V_closed(grid)))) sup_c_err_grid = float(np.max(np.abs(c_grid - c_closed(grid)))) res = { "task_id": "F5-conf-04", "c_closed_y05": float(c_cl[0]), "c_closed_y1": float(c_cl[1]), "c_closed_y2": float(c_cl[2]), "c_num_y05": float(c_num[0]), "c_num_y1": float(c_num[1]), "c_num_y2": float(c_num[2]), "max_abs_error_policy": float(err.max()), "value_closed_y1": float(V_closed(1.0)), "value_num_y1": v_num_1, "savings_rate_closed_form": float(ab), "contraction_modulus": beta, "grid_points_used": N, "n_iterations": it, "final_sup_change": sup, "feasibility": "complete", "feasibility_reason": None, "data_issues": [ {"issue": "Evaluation points 0.5, 1.0, 2.0 are not nodes of the 400-point log grid.", "handling": "VFI policy c(y) and value V(y) linearly interpolated in y between adjacent grid nodes, as allowed by the task."}, {"issue": "Choice variable discretised: next-period output y' restricted to the same grid (discrete-choice VFI); log utility unbounded so -inf assigned to infeasible c<=0.", "handling": "Standard grid-search VFI; every grid y has feasible choices (y'=y_min always feasible), so no node is dropped. Discretisation error is reported."}, ], "n_input_rows": {"model.csv": int(len(df))}, "n_analysed": N, "random_seed": None, "diagnostics": { "sup_abs_error_value_on_grid": sup_V_err, "sup_abs_error_policy_on_grid": sup_c_err_grid, "abs_error_policy_each_eval": [float(x) for x in err], "iteration_error_bound_beta_tol_over_1mbeta": beta * tol / (1 - beta), "grid_log_step": float(np.log(grid[1] / grid[0])), "steady_state_y": float(ab ** (alpha / (1 - alpha))), "A": float(A), "B": float(B), }, } os.makedirs("out", exist_ok=True) with open("out/results.json", "w") as f: json.dump(res, f, indent=2) print(json.dumps(res, indent=2))