"""F5-dev-01: Cobb-Douglas consumer. Closed-form Marshallian demands + independent numerical validation. Reads only data/parameters.csv; writes out/results.json and out/exclusions.csv. Deterministic: no randomness is used (random_seed = null).""" import json, os, hashlib import numpy as np import pandas as pd from scipy.optimize import minimize_scalar, minimize ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) DATA = os.path.join(ROOT, "data", "parameters.csv") OUT = os.path.join(ROOT, "out") # ---------------- data audit ---------------- raw = pd.read_csv(DATA) n_raw = len(raw) issues = [] required = ["alpha", "p1", "p2", "m"] dups = raw["parameter"][raw["parameter"].duplicated()].tolist() if dups: issues.append({"issue": f"duplicate parameter rows: {dups}", "handling": "fatal; not resolved"}) missing = [k for k in required if k not in set(raw["parameter"])] if missing: issues.append({"issue": f"missing parameters: {missing}", "handling": "fatal"}) vals = pd.to_numeric(raw["value"], errors="coerce") if vals.isna().any(): issues.append({"issue": "non-numeric values", "handling": "fatal"}) extra = [k for k in raw["parameter"] if k not in required] if extra: issues.append({"issue": f"unexpected parameters {extra}", "handling": "ignored"}) used = raw[raw["parameter"].isin(required)] pd.DataFrame([{"step": 1, "rule": "keep rows whose parameter is in {alpha,p1,p2,m}", "n_before": n_raw, "n_after": len(used), "n_removed": n_raw - len(used), "rationale": "only these parameters enter the model"}]).to_csv( os.path.join(OUT, "exclusions.csv"), index=False) P = dict(zip(used["parameter"], pd.to_numeric(used["value"]).astype(float))) a, p1, p2, m = P["alpha"], P["p1"], P["p2"], P["m"] valid = (0 < a < 1) and p1 > 0 and p2 > 0 and m > 0 if not valid: issues.append({"issue": "parameters outside 00, m>0", "handling": "closed form not reported"}) def u(x1, x2): return a * np.log(x1) + (1 - a) * np.log(x2) # ---------------- independent numerical maximisation (no closed form) ---------------- # (i) 1-D: substitute the budget line x2 = (m - p1 x1)/p2, maximise over x1 in (0, m/p1) # with a bounded Brent search (strict monotonicity => budget binds; checked below in (iii)). # (ii) dense grid on the budget line (1,000,001 points) as a derivative-free check. # (iii) 2-D SLSQP with the inequality budget constraint (does not impose binding). def neg_u_line(x1): return -u(x1, (m - p1 * x1) / p2) eps = 1e-12 * m / p1 r1 = minimize_scalar(neg_u_line, bounds=(eps, m / p1 - eps), method="bounded", options={"xatol": 1e-12}) x1_num, v_num = float(r1.x), float(-r1.fun) x2_num = (m - p1 * x1_num) / p2 grid = np.linspace(0, m / p1, 1_000_001)[1:-1] vg = u(grid, (m - p1 * grid) / p2) ig = int(np.argmax(vg)); x1_grid, v_grid = float(grid[ig]), float(vg[ig]) x0 = np.array([0.5 * m / (2 * p1), 0.5 * m / (2 * p2)]) # arbitrary feasible start r3 = minimize(lambda x: -u(x[0], x[1]), x0, method="SLSQP", bounds=[(1e-9, None), (1e-9, None)], constraints=[{"type": "ineq", "fun": lambda x: m - p1 * x[0] - p2 * x[1]}], options={"ftol": 1e-14, "maxiter": 1000}) x_slsqp, v_slsqp = r3.x.astype(float), float(-r3.fun) # ---------------- closed form (derived from FOCs; see report) ---------------- 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) results = { "task_id": "F5-dev-01", "x1_demand": x1 if valid else None, "x2_demand": x2 if valid else None, "indirect_utility": v if valid else None, "budget_identity_residual": resid if valid else None, "mrs_at_optimum": mrs if valid else None, "price_ratio": p1 / p2, "expenditure_share_good1": p1 * x1 / m if valid else None, "indirect_utility_numerical": v_num, "feasibility": "complete" if valid and not missing and not dups else "infeasible", "feasibility_reason": None if valid else "parameters violate assumptions", "data_issues": issues, "n_input_rows": {"parameters.csv": int(n_raw)}, "n_analysed": int(len(used)), "random_seed": None, "validation_details": { "input_sha256": hashlib.sha256(open(DATA, "rb").read()).hexdigest(), "brent_x1": x1_num, "brent_x2": x2_num, "brent_converged": bool(r1.success), "grid_points": int(grid.size), "grid_x1": x1_grid, "grid_utility": v_grid, "grid_step_x1": float(grid[1] - grid[0]), "slsqp_x1": float(x_slsqp[0]), "slsqp_x2": float(x_slsqp[1]), "slsqp_utility": v_slsqp, "slsqp_success": bool(r3.success), "slsqp_budget_slack": float(m - p1 * x_slsqp[0] - p2 * x_slsqp[1]), "abs_diff_utility_closed_vs_brent": abs(v - v_num), "abs_diff_x1_closed_vs_brent": abs(x1 - x1_num), "abs_diff_utility_closed_vs_grid": abs(v - v_grid), "abs_diff_utility_closed_vs_slsqp": abs(v - v_slsqp), "mrs_minus_price_ratio": mrs - p1 / p2, }, } with open(os.path.join(OUT, "results.json"), "w") as f: json.dump(results, f, indent=2, sort_keys=False) print(json.dumps({k: results[k] for k in ["x1_demand", "x2_demand", "indirect_utility", "indirect_utility_numerical", "budget_identity_residual", "mrs_at_optimum", "price_ratio"]}, indent=1))