{"data":{"kind":"file","path":"README.md","version_id":"kv8meg5dy763m435qjzx30rn","entry":{"name":"README.md","path":"README.md","is_directory":false,"size":14735,"modified_at":"2026-07-07T18:32:26.293000","content_hash":"cdbf730cd253a2512c27595fb6344f42005c8dfd8a4f7eee3cb2bb829264f0ab"},"entries":[],"content":"# unit-commitment-lite\n\nDay-ahead binary unit commitment on a single bus — the DISCRETE on/off layer of\na power-systems vertical built with the same validation discipline as its\nsiblings ([economic-dispatch](https://github.com/JWilksBooth/economic-dispatch),\n[dcopf-grid-verifiers](https://github.com/JWilksBooth/dcopf-grid-verifiers),\n[multiperiod-dispatch](https://github.com/JWilksBooth/multiperiod-dispatch),\n[n1-contingency-dispatch](https://github.com/JWilksBooth/n1-contingency-dispatch),\n[nodal-pricing-lmp](https://github.com/JWilksBooth/nodal-pricing-lmp)). The\nvertical decomposes power-systems optimization along one axis per environment —\n**merit → network → time → security → COMMITMENT** — and this is the\ncurriculum's discrete-decision capstone.\n\n**The one-sentence axis split from the time sibling:** multiperiod-dispatch is\n*continuous* dispatch under ramp limits with the units already committed (the\ncoupling is a rate limit between consecutive periods); **this env is the\n*discrete* commit/decommit decision** — no ramps, dispatch is closed-form merit\nfill, and the intertemporal coupling is the min-up / min-down automaton plus the\nstart-vs-carry breakeven. Continuous time-coupling there, discrete time-coupling\nhere.\n\nFront-loaded practitioner caveat: real unit commitment is non-separable from\ndispatch precisely because of the couplings this env *cuts* — ramps (the time\nsibling's axis), spinning reserve (v0.2, below), and the network (the earlier\nsiblings). This is a heavily stylized UC that isolates the binary\ncommit/decommit + feasibility-automaton layer on purpose; the honest scope is\nstated so a power-systems reviewer reads it as a deliberate cut, not naivety.\n\n## Task\n\nGiven a small fleet (G = 5 units, each with integer marginal cost, Pmin, Pmax,\nmin-up, min-down, start cost, no-load cost, and an initial status + hours already\nheld in it) and an hourly integer-MW load profile over T in {24, 36}, output the\nfull T-by-G commitment matrix as JSON:\n`{\"commitment\": [[0,1,1,0,1], ...]}` — one inner list per hour, G bare 0/1 ints\nper list in unit order, the smallest answer in the portfolio (~120 tokens).\n\nThe model outputs **only** the commitment matrix. The grader completes dispatch\nby the portfolio's closed-form merit fill with Pmin floors, so total cost\n`C(U) = energy + start_cost per turn-on + no_load per committed unit-hour` is an\n**exact integer** for any serviceable commitment — no LP in the grading path,\nzero tolerance surface anywhere. Committing only the on/off pattern and letting\nmerit fill complete the dispatch is the design choice that keeps the answer tiny\nand the grading tolerance-free. **`C(U)` is `None` (never a partial sum) whenever\nany hour is unserviceable** — a committed set whose Pmin sum exceeds the load (the\nminimum-generation trap) or whose Pmax sum is below it is infeasible under the\nhard balance equality.\n\nRewards: format (0.10 — parse + T-by-G shape + strict 0/1), feasibility (0.40 —\nmin-up/min-down automaton legality including initial-condition residues, plus\nevery hour serviceable), and optimality (0.50 — `exp(-k·(C(U)−C*)/C_disc)`,\nhard-gated on feasibility, fail-closed below C* and on any `None` hour).\nOptimality is normalized by **discretionary** cost `C_disc = C* − C_floor`, not\ntotal C* — see the naive-heuristics section for why.\n\n## Ground truth validation\n\nTwo independently implemented solvers in **different paradigms** must agree on\nthe minimum total cost C* for every dataset instance:\n\n- **Primary:** a scipy/HiGHS MILP over binary `u[g,t]` + startup/shutdown + linear\n  dispatch, with Rajan-Takriti min-up/min-down facets, initial-condition residues\n  as hard u-fixings, a hard balance equality, and `mip_rel_gap = 0` pinned. The\n  decoded optimum is re-graded through the reward function's own merit-fill cost.\n- **Cross-check:** a hand-rolled **pure-numpy exhaustive forward DP** over the\n  joint unit-status automaton, written from the spec text only by an implementer\n  who never read the primary. Its Bellman minimum equals C* *by construction* —\n  an exhaustive minimum over the entire finite feasible set, tolerance-free, with\n  no solve tail. It imports **numpy + stdlib only — no solver, no scipy.**\n\n| Metric (200 instances, seeds 0–199) | Result |\n|---|---|\n| MILP-vs-DP C* mismatches (exact integer equality) | **0 / 200** |\n| C*−1 refutation certificates (HiGHS status 2 INFEASIBLE, exact) | 0 failures / 200 |\n| Anti-vacuity controls (objective ≤ C* attained at exactly C*) | 0 failures / 200 |\n| Cross-check certificates (exhaustive-DP min == MILP C*) | 0 failures / 200 |\n| `C_floor ≤ C*` arithmetic anchor | 0 failures / 200 |\n| Solver-status aborts (status ∈ {1,3,4}, no limits) | **0** |\n\n**Dual-paradigm independence, scoped precisely.** The cross-check pairs Bellman\nenumeration vs LP branch-and-bound, exact-int vs float, and structurally\ndifferent min-up/down encodings (algebraic Rajan-Takriti windows vs an automaton\nlegality tensor). That certifies the min-up/min-down + commitment *combinatorial*\nlayer and catches encoding-mechanics and end-of-horizon bugs (they surface as a\nmismatch abort). It does *not* certify the merit-fill dispatch-cost layer — that\nis common-mode via the one shared `merit_fill`, so it is established separately by\nthe serviceability/boundary unit gates and the independent `C_floor ≤ C*`\narithmetic anchor. The cross-check was additionally cross-validated against 2049\nbrute-force schedule enumerations (0 mismatches, including the runner-up and the\nplausible-count) plus a 46-seed brute anchor kept in the harness.\n\n**Exact-certificate property:** every cost is an integer sum, so there is NO\ntolerance anywhere — the grader, both solvers, and the certificates share exact\ninteger semantics, and C*−1 is *proven infeasible under the grader's own rules*\nwith no numerical slack to buy a cheaper certified schedule.\n\n**Zero extra dependencies — the vertical's first solver-free cross-check.** A\nstatic dependency-audit gate confirms `xcheck.py` imports only stdlib + numpy (no\nsolver extra — unlike bimanual's ortools `[validate]` extra or dcopf's\npandapower). Reproducing the full ground-truth validation needs the base deps and\nnothing more. This is direct evidence for the pending numpy+scipy bounty.\n\nGround-truth structure, measured (N=200; the free go/no-go metrics computed from\nthe DP for $0):\n\n| Ground-truth metric | Value |\n|---|---|\n| Discretionary 2-best gap `(C_runnerup − C*)/C_disc` | median 1.97% / mean 3.02% / max 8.31% |\n| Plausible-schedule count (distinct commitments within 5% of C_disc, top-K DP) | median 6 / max 47 |\n| MILP primary solve / refutation / DP | ~100 ms / ~80 ms / ~40 ms |\n\n## Anti-reward-hacking (16 regression-tested attack gates)\n\nSingle shared parse gate: format-invalid answers cascade to zero on all three\nrewards (the `format => feasibility => optimality` zero-cascade invariant is\nasserted across the whole attack corpus). The gates cover: the per-period-merit\nshortcut and blanket always-on (both fail on the Pmin trap or a min-down cycle;\nthe honest-partial rung is built with all init residues expired so it is not\nseed-flaky); `NaN`/`Infinity` literals, >4300-digit integers, bracket bombs, null\nbytes, `0.5`/`0.9999`/`2`/`\"1\"`/`true` smuggles (all score 0 without crashing,\nrejected by TYPE with a bool-before-int ladder); two-sided one-hour min-up and\nmin-down boundary probes (switch inside the forced init-residue window is\ninfeasible, at the first legal hour feasible); the end-of-horizon rule (a unit\nturned ON at T−1 carries no obligation past the horizon); transposed and\nduplicated/omitted matrices (format 0 via the T ≠ G guarantee); sub-C*\nfabrication (fail-closed); **the unserviceable-in-one-hour schedule constructed\ncheap on its good hours, which must score optimality EXACTLY 0.0 — `C(U)` is\n`None`, never a partial sum over the serviceable hours** (the central hacking\ngate); Arrow round-trip identity on the real dataset rows (init_on/init_hours as\nplain `int`, never `bool`); determinism; and a **static dependency-audit gate**\nasserting the shipped module and cross-check import no forbidden solver.\n\n## The naive heuristics and the honest difficulty verdict\n\n**Naive \"per-period merit commit\"** (commit the cheapest sufficient set each hour\nindependently, ignoring min-up/down, starts, no-load, and the Pmin sum) fails\nthree ways: infeasible by a min-down cycle or a live init residue; infeasible by\nthe Pmin-valley trap; or feasible-but-suboptimal by over-starting a peaker where\ncarrying at no-load through a valley is cheaper. **Always-on** (blanket commit\nevery unit every hour) is killed by both the Pmin trap and init-min-down\nresidues.\n\n**Why the reward normalizes by discretionary cost.** A large unavoidable baseload\nfloor dilutes the total-cost 2-best gap to ~0.03% — the optimum and the\nsecond-best are not separated at all (pure saturation). The *same* discrete\ndecisions normalized by discretionary cost `C_disc = C* − C_floor`, where\n`C_floor` is the pure economic-dispatch energy floor no commitment choice can\nundercut, give a real, gradable **median 2-best gap of 1.97%**. That\nnormalization is what rescues the reward; it is documented as a decision record\nin [CALIBRATION.md](CALIBRATION.md).\n\n**Read this honestly — it ships as a tier discriminator, not a graded frontier\nbenchmark.** The pre-committed null hypothesis (the design red-team's gate L3) was\nthat at G ≤ 5 the plausible-commitment space collapses onto a frontier model's\nenumeration ceiling. The free pre-baseline gate measured it: the median count of\ndistinct plausible schedules within 5% of C_disc is **6** (max 47), far under the\n~100 enumerability threshold. The env is **enumerable**, so it will **saturate at\nthe frontier** exactly as predicted. The discretionary normalization *succeeded*\nat its job — 1.97% is real separation vs the ~0.03% dilution that would have\nflatlined the reward — but normalization cannot shrink the combinatorial space.\nSo v0.1.0 ships honestly as a **Haiku-vs-Opus tier discriminator** whose distinct\nrole vs the vertical's already-saturated control (economic-dispatch) is\n**feasibility reasoning**: the min-up/min-down automaton and the Pmin trap are a\nfeasibility trap economic-dispatch has no analog for. Real headroom moves to\nv0.2. This verdict was written from the free DP-computed gates *before* any billed\nbaseline — no frontier dollars were chased on knob-fiddling.\n\n## Baseline\n\nHaiku 4.5, 50 examples, 1 rollout each, `--max-tokens 16000` (reproduce with\nthe `vf-eval` line below):\n\n| Metric | Haiku 4.5 |\n|---|---|\n| Mean reward | **0.253** |\n| Format | 1.000 |\n| Feasibility | 0.320 |\n| Optimality | 0.051 |\n| Truncated | 0 / 50 (mean output 1.7k tokens) |\n\nThe failure shape is exactly the designed one: the ~120-token binary matrix\nparses perfectly every time (format 1.0 — the smallest answer surface in the\nportfolio), and the model fails on **commitment logic** — 68% of its schedules\nviolate a min-up/min-down residue or commit an unserviceable set. This extends\nthe vertical's weak-model ladder (economic-dispatch 0.861 → dcopf 0.520 →\nmultiperiod 0.242 → this env 0.253) with the failure always on\nphysics/feasibility, never parsing. No frontier (Opus-class) baseline is\npublished: the free enumerability gate above predicts saturation, and paying to\nconfirm a measured verdict is not this portfolio's style — if one is run later,\nexpect near-saturation on completion. `OPTIMALITY_K` (the `exp(-k)` decay)\nremains analytically pinned; the Haiku run barely exercises it (feasibility\nfailures dominate), so a graded-band calibration would require a mid-tier model\npopulation this env, by measurement, does not discriminate.\n\n## Usage\n\n```bash\npip install -e .\n\n# dual-solver validation (MILP-vs-DP exact-int + certificates + 16 attack gates)\n# — reproduces with NO extra install: the cross-check is pure numpy\nN_INSTANCES=200 python tests/test_validation.py\n\n# difficulty sweep + the two free pre-baseline go/no-go gates\npython calibration/measure.py --n 200\n\n# give frontier models room to reason; -r 1 matches published baselines\nvf-eval unit-commitment-lite -p anthropic -m <model> -n 50 -r 1 --max-tokens 16000\n```\n\nLower token budgets truncate reasoning mid-derivation and score as format\nfailures — use exactly the line above.\n\n```python\nimport verifiers as vf\nenv = vf.load_environment(\"unit-commitment-lite\", num_examples=300)\n# disjoint train/eval: load_environment(1000) + load_environment(200, seed_offset=1000)\n```\n\n## Environment notes\n\n- **scipy >= 1.11 is a hard floor.** scipy 1.10's bundled HiGHS crashes natively\n  (0xC0000409) on some of this env's dense UC MILPs — a captured reproducer\n  exists. scipy 1.15.3 validated clean on 200/200. `pyproject.toml` pins the\n  floor; the decision record is in [CALIBRATION.md](CALIBRATION.md).\n- **N=200 validation runs in fresh-process subprocess chunks.** scipy/HiGHS leaks\n  memory across repeated dense solves (a C-level leak `gc.collect` cannot reach —\n  it OOM-kills a single process near seed ~160 at ~5 solves/seed), so the harness\n  chunks the sweep across fresh workers whose memory is reclaimed on exit. The\n  expensive top-K DP gate metrics (2–25 s/seed) run on a 20-seed subsample — they\n  are a distribution (median), not a per-instance validation.\n- **`mip_rel_gap = 0` is pinned, not precautionary.** HiGHS's 1e-4 default returns\n  a within-tolerance suboptimal incumbent (a captured seed is off by 1800 on a\n  ~28.6M objective, inside the default tolerance) — a FALSE certificate if trusted.\n- **`OPTIMALITY_K` is analytically pinned** (see Baseline — the measured Haiku\n  run barely exercises the graded band, and the enumerability gate predicts\n  frontier saturation, so there is no model population to sweep it against).\n\n## Roadmap\n\n- **v0.2 (frontier headroom): a spinning-reserve margin**\n  (`sum(pmax over committed) ≥ load[t] + R_t` each hour). This breaks per-valley\n  separability — you may be forced to carry reserve you would otherwise shed, so\n  \"decommit in the valley\" becomes wrong via a per-hour *global* constraint that\n  greedy cannot satisfy locally — while keeping dispatch closed-form (reserve is\n  one extra per-state feasibility check) so the exact-integer DP cross-check and\n  the dual-certificate spine survive intact. Ramps are explicitly **deferred to\n  v0.3+**: they re-collide with multiperiod-dispatch's own axis and re-import the\n  LP-relaxation certificate tail this env deliberately cut. Curriculum line:\n  merit → network → ramps → commitment → commitment + reserves.\n- v0.3+: reserves + the earlier siblings' couplings composed.\n","encoding":"utf-8","truncated":false,"total_bytes":14735},"status":null}