#!/usr/bin/env python3 """ Standalone feasibility checker for the grid-unit-commitment mission. Zero dependencies (stdlib only). Mirrors the verifier's 5 checks in exact order: 1. shape 2. on/off consistency (bounds) 3. demand balance 4. spinning reserve 5. min-up/min-down (with init state) Prints the first violation found (verification stops there, like the real verifier) or PASS plus the full cost breakdown (fuel/no-load/startup/total, co2, min reserve margin). Usage: python3 check_grid_uc.py must be {"on": [[0/1 x24] x60], "p": [[MW x24] x60]}. """ import json import sys EPS = 1e-6 def check(instance_path, solution_path): inst = json.load(open(instance_path)) sol = json.load(open(solution_path)) H = inst["hours"] gens = inst["generators"] G = len(gens) reserve_pct = inst["reserve_pct"] demand = inst["demand_mw"] wind = inst["wind_mw"] solar = inst["solar_mw"] on = sol.get("on") p = sol.get("p") # 1. shape if not isinstance(on, list) or not isinstance(p, list) or len(on) != G or len(p) != G: return {"feasible": False, "first_violation": f"shape: expected {G} rows, got on={len(on) if isinstance(on, list) else 'n/a'} p={len(p) if isinstance(p, list) else 'n/a'}"} for g in range(G): if len(on[g]) != H or len(p[g]) != H: return {"feasible": False, "first_violation": f"shape: gen {g} row length mismatch (expected {H})"} for h in range(H): v = p[g][h] if not isinstance(v, (int, float)) or v != v or v in (float("inf"), float("-inf")): return {"feasible": False, "first_violation": f"shape: p[{g}][{h}]={v} not finite"} # 2. on/off consistency for g in range(G): gen = gens[g] for h in range(H): ov = on[g][h] if ov not in (0, 1): return {"feasible": False, "first_violation": f"on[{g}][{h}]={ov} not 0/1"} if ov == 0: if abs(p[g][h]) > EPS: return {"feasible": False, "first_violation": f"gen {g} h{h} is off but p={p[g][h]} (expected 0)"} else: if p[g][h] < gen["pmin"] - EPS or p[g][h] > gen["pmax"] + EPS: return {"feasible": False, "first_violation": f"gen {g} h{h} p={p[g][h]} outside [{gen['pmin']},{gen['pmax']}]"} # 3. demand net_demand = [demand[h] - wind[h] - solar[h] for h in range(H)] for h in range(H): tot = sum(p[g][h] for g in range(G)) if abs(tot - net_demand[h]) > EPS: return {"feasible": False, "first_violation": f"h{h} demand mismatch: sum_p={tot} != net_demand={net_demand[h]}"} # 4. reserve reserve_req = [reserve_pct * demand[h] for h in range(H)] min_margin = float("inf") for h in range(H): margin = sum((gens[g]["pmax"] - p[g][h]) for g in range(G) if on[g][h] == 1) min_margin = min(min_margin, margin) if margin < reserve_req[h] - EPS: return {"feasible": False, "first_violation": f"h{h} reserve violation: margin={margin} < req={reserve_req[h]}"} # 5. min-up / min-down (init_hours counts as already-elapsed time in init_on state) for g in range(G): gen = gens[g] min_up, min_down = gen["min_up"], gen["min_down"] prev_state = gen["init_on"] run_len = gen["init_hours"] for h in range(H): cur = on[g][h] if cur != prev_state: if prev_state == 1 and run_len < min_up: return {"feasible": False, "first_violation": f"gen {g} switched off at h{h} after {run_len}h on (min_up={min_up})"} if prev_state == 0 and run_len < min_down: return {"feasible": False, "first_violation": f"gen {g} switched on at h{h} after {run_len}h off (min_down={min_down})"} run_len = 1 prev_state = cur else: run_len += 1 # cost breakdown fuel_cost = sum(p[g][h] * gens[g]["fuel_cost"] for g in range(G) for h in range(H) if on[g][h] == 1) noload_cost = sum(gens[g]["no_load_cost"] for g in range(G) for h in range(H) if on[g][h] == 1) startup_cost = 0.0 for g in range(G): gen = gens[g] prev = gen["init_on"] for h in range(H): cur = on[g][h] if prev == 0 and cur == 1: startup_cost += gen["startup_cost"] prev = cur total_cost = fuel_cost + noload_cost + startup_cost co2 = sum(p[g][h] * gens[g]["co2_rate"] for g in range(G) for h in range(H) if on[g][h] == 1) return { "feasible": True, "first_violation": None, "hours": H, "generators": G, "fuel_cost": fuel_cost, "noload_cost": noload_cost, "startup_cost": startup_cost, "total_cost": total_cost, "co2_tonnes": co2, "min_reserve_margin_mw": min_margin, } if __name__ == "__main__": if len(sys.argv) != 3: print(__doc__) sys.exit(2) result = check(sys.argv[1], sys.argv[2]) if result["feasible"]: print("PASS") for k, v in result.items(): if k not in ("feasible", "first_violation"): print(f" {k}: {v}") else: print("FAIL:", result["first_violation"]) sys.exit(0 if result["feasible"] else 1)