0

Unit Commitment Lite

Fresh

Day-ahead binary unit-commitment RL environment: choose the T-by-G ON/OFF schedule that serves an hourly load at minimum total cost (energy + start...

Type
RL Env
Publisher
Jwilksbooth
Runtime
single-turn
License
mit
Size
v0.1.0
Published
Jul 2026

Cite

Notes

Only stored in your browser.

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, and mip_rel_gap = 0 pinned. 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 anchor0 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 metricValue
Discretionary 2-best gap (C_runnerup − C*)/C_discmedian 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):

MetricHaiku 4.5
Mean reward0.253
Format1.000
Feasibility0.320
Optimality0.051
Truncated0 / 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.toml pins 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.collect cannot 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 = 0 is 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_K is 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_t each 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.