Multi-period economic dispatch RL environment with ramp limits. Third environment in a power-systems vertical (economic-dispatch → dcopf-grid-verifiers → this). The physics axis here is time: unit output changes between consecutive periods are limited by ramp rates, and the fleet is drawn with real merit-stack texture — cheap units are slow, fast units are expensive. Per-period merit-order reasoning is therefore provably infeasible on a measured 74% of instances: the cheap baseload cannot climb out of the valley into the peak fast enough, so cost-optimal scheduling must anticipate the climb periods ahead of time or pay expensive fast units to carry the peak.
Given 5–8 consecutive periods, a duck-curve-shaped load profile, and 3–5 committed units (linear cost, [Pmin, Pmax], per-period ramp limit), output the full schedule as JSON: {"dispatch_mw": [[unit MWs period 1], [period 2], ...]} such that every period balances, every unit respects its range, and no unit's output changes faster than its ramp limit.
Two independently assembled LP formulations must agree (unit-major assembly solved with HiGHS dual simplex vs a separately written time-major assembly solved with HiGHS interior point):
| Metric (200 instances, seeds 0–199) | Result |
|---|---|
| Objective mismatches (>0.1% rel and >$1 abs) | 0 / 200 |
| Worst relative objective gap | 8.8e-16 |
| Ramp-blind merit-order dispatch infeasible | 149 / 200 (74%) |
| Median cost-of-ramps premium (constrained subset) | 5.4% |
Both formulations share scipy/HiGHS, so this is cross-implementation validation of the constraint assembly (the dominant error source), not independent-solver validation — stated plainly. See CALIBRATION.md for how the load-profile design was iterated against measured difficulty (a naive gradual climb produced ramp infeasibility without economic cost — the duck-curve construction fixes exactly that).
50 instances (seeds 0–49), 1 rollout each, July 2026:
| Model | total | format | feasibility | optimality |
|---|---|---|---|---|
| claude-haiku-4-5 (8k tokens) | 0.242 | 0.980 | 0.200 | 0.135 |
| claude-opus-4-8 (16k tokens) | 0.890 | 0.920 | 0.900 | 0.879 |
The weak model writes near-perfectly parseable schedules and only 1 in 5 survives the ramp physics. The frontier model reaches 90% feasibility but its optimality-when-feasible is 0.977 (vs 0.998 on the congestion sibling) — the economics of pre-positioning slow units is harder than it looks even when the constraints are satisfied. Across the vertical the weak-model ladder is 0.861 → 0.520 → 0.242 (economic-dispatch → dcopf-grid-verifiers → this).
vf-eval multiperiod-dispatch -p anthropic -m claude-haiku-4-5-20251001 -n 50 -r 1 --max-tokens 8000 --save-results
vf-eval multiperiod-dispatch -p anthropic -m claude-opus-4-8 -n 50 -r 1 --max-tokens 16000 --save-results"...G2 ramp: 72.5 - 6.5 = 66 > 64.3 ... Can't meet ramp constraint. After multiple iterations, optimal solution: [emits schedule]"
The model correctly computes that a ramp limit is violated — then submits a schedule anyway, one that also puts G3 at 6.8 MW, below its minimum. Feasibility reward: 0. The ramp coupling across periods is precisely the reasoning per-period merit order cannot shortcut.
| Reward | Weight | Description |
|---|---|---|
reward_format |
0.10 | Parseable T×G JSON matrix, finite values only |
reward_feasibility |
0.35 | Positive-proof check: per-period balance, unit bounds, all ramp transitions |
reward_optimality |
0.55 | Gated on feasibility; `exp(-5 × |
Anti-reward-hacking carried over from the sibling environments and regression-tested by 7 attack gates in tests/test_validation.py: the ramp-blind cheap schedule scores 0 (feasibility gate); NaN/Infinity literals and oversized integers (including CPython's >4300-digit limit and deeply-nested-bracket payloads) are rejected at parse; and tolerance abuse (riding the ±0.5 MW grading tolerance to beat the LP optimum) is settled at a penalty price of 2×·T·(max unit cost) — scaled by the horizon T because a single within-tolerance violation can buy an advantageous position that pays off across every remaining period — with imbalance priced symmetrically so over-generation is as costly as under-service.
pip install -e .
# validation harness (dual-formulation cross-check + attack gates)
N_INSTANCES=200 python tests/test_validation.py
# difficulty distribution / calibration sweep
python calibration/measure.py --n 300
# give frontier models room to reason — a bare command defaults to 4096 tokens
# and frontier rollouts truncate, collapsing the score to ~0.5
vf-eval multiperiod-dispatch -p anthropic -m <model> -n 50 -r 1 --max-tokens 16000import verifiers as vf
env = vf.load_environment("multiperiod-dispatch", num_examples=300)
# disjoint train/eval: load_environment(1000) + load_environment(200, seed_offset=1000)Tiered fleet (40% baseload $15–30/MWh with 5–15%/period ramps, 40% mid-merit $35–60 at 15–35%, 20% peakers $70–130 at 50–100%; at least one fast unit guaranteed). Load profiles are duck-curve shaped in explicit phases — valley plateau, steep 1–2 period climb, peak plateau, shoulder decline — with the climb rate constructed at 55–95% of the drawn fleet's aggregate ramp capability, so instances are physically followable by construction (draw rejection ~5%, no survivor bias) while merit-blind allocation frequently is not. 25% of instances are near-flat "mild days" (the easy tier). Fully deterministic per seed; ~10² instances/sec generation.
- v0.2: startup costs + on/off commitment decisions (discrete on/off is delivered as the sibling env unit-commitment-lite (local); this repo stays the continuous ramp-coupling rung)
- Shipped siblings: n1-contingency-dispatch (N-1 security, 68% insecure), nodal-pricing-lmp (LMP pricing, 44% out-of-band), unit-commitment-lite (local) (discrete unit commitment, the axis-sibling of this env)