"""F5-dev-01: Cobb-Douglas Marshallian demands, closed form + independent numerical check. Run from workspace root: ~/wp29/venv/bin/python out/analysis.py """ import json, os import numpy as np import pandas as pd from scipy.optimize import minimize_scalar, minimize df = pd.read_csv("data/parameters.csv") n_rows = int(len(df)) issues = [] P = {} for _, r in df.iterrows(): name = str(r["parameter"]).strip() if name in P: issues.append({"issue": f"duplicate parameter {name}", "handling": "first value kept"}) continue P[name] = float(r["value"]) for k in ["alpha", "p1", "p2", "m"]: if k not in P or not np.isfinite(P[k]): raise SystemExit(f"missing/invalid parameter {k}") a, p1, p2, m = P["alpha"], P["p1"], P["p2"], P["m"] # Assumptions for interior unique optimum assert 0 < a < 1, "alpha must be in (0,1)" assert p1 > 0 and p2 > 0 and m > 0, "prices and income must be positive" def u(x1, x2): return a * np.log(x1) + (1 - a) * np.log(x2) # ---- closed form (Lagrangian FOCs: a/x1 = lam p1, (1-a)/x2 = lam p2, budget binds) ---- x1 = a * m / p1 x2 = (1 - a) * m / p2 v = float(u(x1, x2)) resid = p1 * x1 + p2 * x2 - m mrs = (a / x1) / ((1 - a) / x2) price_ratio = p1 / p2 share1 = p1 * x1 / m # ---- numerical validation (does NOT use closed form) ---- # Monotone utility => budget binds; parametrise by x1 on (0, m/p1), x2 = (m - p1 x1)/p2. def neg_u_line(t): return -u(t, (m - p1 * t) / p2) eps = 1e-12 * m / p1 res1 = minimize_scalar(neg_u_line, bounds=(eps, m / p1 - eps), method="bounded", options={"xatol": 1e-12}) x1_num, v_num_line = float(res1.x), float(-res1.fun) x2_num = (m - p1 * x1_num) / p2 # Fine grid search on budget line as secondary check grid = np.linspace(m / p1 * 1e-6, m / p1 * (1 - 1e-6), 2_000_001) gv = u(grid, (m - p1 * grid) / p2) i = int(np.argmax(gv)) x1_grid, v_grid = float(grid[i]), float(gv[i]) # Inequality-constrained 2-D optimiser (budget as <=, not imposed as equality), starting off-optimum cons = [{"type": "ineq", "fun": lambda z: m - p1 * z[0] - p2 * z[1]}] res2 = minimize(lambda z: -u(z[0], z[1]), x0=[1.0, 1.0], method="SLSQP", bounds=[(1e-9, None), (1e-9, None)], constraints=cons, options={"ftol": 1e-14, "maxiter": 500}) x_slsqp, v_slsqp = res2.x.tolist(), float(-res2.fun) v_num = max(v_num_line, v_grid) # baseline numerical maximum on budget line diag = { "closed_form": {"x1": x1, "x2": x2, "v": v}, "bounded_scalar": {"x1": x1_num, "x2": x2_num, "v": v_num_line, "success": bool(res1.success)}, "grid": {"x1": x1_grid, "v": v_grid, "n_points": int(grid.size), "spacing": float(grid[1] - grid[0])}, "slsqp_inequality": {"x": x_slsqp, "v": v_slsqp, "success": bool(res2.success), "budget_slack": float(m - p1 * x_slsqp[0] - p2 * x_slsqp[1])}, "abs_diff_v_closed_minus_numerical": float(v - v_num), "abs_diff_x1_closed_minus_scalar": float(x1 - x1_num), "abs_diff_x1_closed_minus_grid": float(x1 - x1_grid), } results = { "task_id": "F5-dev-01", "x1_demand": x1, "x2_demand": x2, "indirect_utility": v, "budget_identity_residual": float(resid), "mrs_at_optimum": float(mrs), "price_ratio": float(price_ratio), "expenditure_share_good1": float(share1), "indirect_utility_numerical": v_num, "feasibility": "complete", "feasibility_reason": None, "data_issues": issues, "n_input_rows": {"parameters.csv": n_rows}, "n_analysed": n_rows, "random_seed": None, } os.makedirs("out", exist_ok=True) with open("out/results.json", "w") as f: json.dump(results, f, indent=2) with open("out/validation_details.json", "w") as f: json.dump(diag, f, indent=2) print(json.dumps(results, indent=2)) print(json.dumps(diag, indent=2))