"""F5-conf-04: VFI vs closed form, optimal growth with log utility. Run from workspace root: ~/wp29/venv/bin/python out/analysis.py Reads only data/model.csv; deterministic (no randomness).""" import json, os, csv import numpy as np ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) DATA = os.path.join(ROOT, "data", "model.csv") OUT = os.path.join(ROOT, "out") with open(DATA, newline="") as f: rows = list(csv.DictReader(f)) p = {r["parameter"]: r["value"] for r in rows} 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"])) assert p["grid_spacing"].strip() == "log" tol = float(p["vfi_tolerance"]) eval_y = [float(p["eval_y_1"]), float(p["eval_y_2"]), float(p["eval_y_3"])] # ---------------- closed form (guess-and-verify V = A + B ln y) ---------------- ab = alpha * beta B = 1.0 / (1.0 - ab) A = (np.log(1.0 - ab) + ab / (1.0 - ab) * np.log(ab)) / (1.0 - beta) c_closed = lambda y: (1.0 - ab) * np.asarray(y) V_closed = lambda y: A + B * np.log(y) savings_rate = ab # ---------------- VFI on prescribed grid (discrete choice of y' on grid) -------- grid = np.exp(np.linspace(np.log(y_min), np.log(y_max), N)) k_needed = grid ** (1.0 / alpha) # capital needed to reach y' = grid[j] C = grid[:, None] - k_needed[None, :] # c(i, j) = y_i - y'_j^(1/alpha) feasible = C > 0 U = np.full_like(C, -np.inf) U[feasible] = np.log(C[feasible]) assert feasible.any(axis=1).all(), "some grid point has no feasible choice" V = np.zeros(N) n_iter = 0 sup_change = np.inf max_iter = 100000 while sup_change >= tol and n_iter < max_iter: W = U + beta * V[None, :] V_new = W.max(axis=1) sup_change = float(np.max(np.abs(V_new - V))) V = V_new n_iter += 1 converged = sup_change < tol pol_j = (U + beta * V[None, :]).argmax(axis=1) c_pol = grid - k_needed[pol_j] # boundary diagnostics: does the optimal choice ever hit grid edges? hits_lower = int(np.sum(pol_j == 0)); hits_upper = int(np.sum(pol_j == N - 1)) # interpolation at evaluation points: linear in ln y (grid is uniform in ln y) lg = np.log(grid) interp = lambda f, y: float(np.interp(np.log(y), lg, f)) c_num = [interp(c_pol, y) for y in eval_y] c_cl = [float(c_closed(y)) for y in eval_y] on_grid = [bool(np.any(np.isclose(grid, y, rtol=1e-12, atol=0))) for y in eval_y] max_err = float(max(abs(a - b) for a, b in zip(c_num, c_cl))) # whole-grid diagnostics (for report only) grid_policy_err = float(np.max(np.abs(c_pol - c_closed(grid)))) grid_value_err = float(np.max(np.abs(V - V_closed(grid)))) data_issues = [] if not all(on_grid): data_issues.append({"issue": "Evaluation points %s are not grid nodes of the log-spaced grid" % [y for y, o in zip(eval_y, on_grid) if not o], "handling": "VFI policy and value interpolated linearly in ln(y) between adjacent grid nodes"}) data_issues.append({"issue": "Design choice: the choice variable is next-period output y' restricted to the same grid (discrete-choice VFI); consumption c = y - y'^(1/alpha)", "handling": "No deviation from prescribed grid/parameters; discretisation error of order of the grid spacing is reported, not corrected"}) feas = "complete" if converged else "partial" res = { "task_id": "F5-conf-04", "c_closed_y05": c_cl[0], "c_closed_y1": c_cl[1], "c_closed_y2": c_cl[2], "c_num_y05": c_num[0], "c_num_y1": c_num[1], "c_num_y2": c_num[2], "max_abs_error_policy": max_err, "value_closed_y1": float(V_closed(1.0)), "value_num_y1": interp(V, 1.0), "savings_rate_closed_form": float(savings_rate), "contraction_modulus": float(beta), "grid_points_used": int(N), "n_iterations": int(n_iter), "final_sup_change": float(sup_change), "feasibility": feas, "feasibility_reason": None if converged else "VFI did not reach tolerance within max_iter", "data_issues": data_issues, "n_input_rows": {"model.csv": len(rows)}, "n_analysed": int(N), "random_seed": None, # supplementary diagnostics "diag_grid_log_step": float(lg[1] - lg[0]), "diag_max_abs_policy_error_on_grid": grid_policy_err, "diag_max_abs_value_error_on_grid": grid_value_err, "diag_policy_hits_lower_bound": hits_lower, "diag_policy_hits_upper_bound": hits_upper, "diag_value_error_y1": interp(V, 1.0) - float(V_closed(1.0)), "diag_closed_form_A": float(A), "diag_closed_form_B": float(B), "diag_vfi_error_bound_beta_over_1mbeta_times_final_change": float(beta / (1 - beta) * sup_change), } os.makedirs(OUT, exist_ok=True) with open(os.path.join(OUT, "results.json"), "w") as f: json.dump(res, f, indent=2) with open(os.path.join(OUT, "exclusions.csv"), "w") as f: f.write("step,rule,n_before,n_after,n_removed,rationale\n") f.write("1,none (all parameter rows used),%d,%d,0,model.csv contains only parameters; nothing excluded\n" % (len(rows), len(rows))) print(json.dumps({k: v for k, v in res.items() if k != "data_issues"}, indent=1))