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