nodal-pricing-lmp
Locational marginal pricing (LMP) as a dual-space reasoning task — the price-formation rung of an energy-systems vertical built with the same validation discipline as its siblings (economic-dispatch, dcopf-grid-verifiers, multiperiod-dispatch, n1-contingency-dispatch), and the parallel robotics vertical built the same way (robot-cycle-time, bimanual-chore-scheduling). The vertical's primal environments ask what should run; this one asks what is it worth, and where. Given a congested lossless DC network (buses, directed lines with reactances and MW flow limits, generators with offer prices and capacity, nodal loads), the model must produce the economic readout of the optimal dispatch: nodal LMPs, the energy/congestion decomposition against a pinned reference bus, the binding-line set, and signed line shadow prices. The model is given instance data only — never the solved dispatch, which would leak the binding set.
The naive answer this environment defeats is the two-bus intuition that a node's price is bounded by the offer band. In a meshed network that is simply false: on a measured 44% of instances some bus is priced outside the offer band — below the cheapest offer or above the priciest — because counterflow on a congested line pays a bus to withdraw. A bus priced below the cheapest generator's offer is not a bug; it is what real traders get paid to understand, and it is unfakeable by band-clipping heuristics.
Task
Given the network and offers, output one flat JSON object:
{"lmp_usd_per_mwh": [...], "congestion_usd_per_mwh": [...],
"binding_lines": [...], "mu_usd_per_mwh": [...]}
all prices in $/MWh to 2 decimals. Rewards: format (0.10), consistency
(0.15 — the four KKT identities), binding_set (0.15 — Jaccard, false-positive
penalized), lmp_accuracy (0.45 — exp(−k·mean|ΔLMP|/scale)), and mu_accuracy
(0.15). The three graded components are hard-gated on the consistency gate:
an answer whose internal KKT identities do not hold scores 0 on all price
credit no matter how close its numbers look.
The three-legged ground truth
Prices are validated three ways, and the independence claim is stated exactly and no more:
- Primary (B-θ angle space): a time-invariant DC-OPF LP in bus angles; LMPs read off the nodal-balance equality duals, signed line shadow prices off the interleaved flow-limit inequality duals (scipy/HiGHS, sign mapping empirically pinned on the hand-computed 3-bus example).
- Cross-check (PTDF injection space): an independently formulated LP in
the shift-factor (injection) space, built from the spec text alone by an
implementer who never read the primary. It reconstructs
LMP_i = λ − Σ_k μ_k·PTDF_kiand must agree per element. - Certificate (convention-free): a finite-difference value-function probe. It perturbs each bus load by ±0.5 MW and checks the symmetric difference quotient of the primal objective against the claimed LMP — validating every price against the definition of marginal cost using primal objective values only, with no dual convention to inherit a sign bug from.
Honest independence framing. The two LP legs share the same scipy/HiGHS solver core (exactly as the dcopf-grid-verifiers sibling shares its solver across its two modeling paths), so a dual-extraction quirk could in principle hit both identically — the finite-difference certificate is the leg that cannot, because it never touches a dual. This environment therefore claims two independently formulated LPs solved by the same engine, cross-checked by a solver-agnostic finite-difference certificate — never "three independent implementations." (The pandapower cross-check the dcopf sibling uses is dropped here — pandapower does not expose nodal duals cleanly — which is what makes the dependency story scipy-only; see CALIBRATION.md.)
Ground-truth metrics (N = 200, seeds 0–199)
| Metric | Result |
|---|---|
| B-θ vs PTDF leg LMP disagreements | 0 / 200 |
| B-θ vs PTDF leg μ disagreements | 0 / 200 |
| Worst leg deltas | |ΔLMP| 1.3e-12, |Δμ| 5.9e-12 (tolerance 1e-2) |
| Finite-difference certificate failures | 0 / 200 |
| Congestion-rent three-way identity failures | 0 / 200 |
| Merchandising-surplus / cap violations (|LMP| > $2,000) | 0 / 200 |
| Congested instances | 152 / 200 (76%) |
| Two-plus binding lines | 55 / 200 (28%) |
| Out-of-band (some LMP outside the offer band) | 89 / 200 (44%) |
| Raw-draw accept rate (survivor-bias indicator) | 52% |
The congestion-rent identity is checked three ways on every instance —
Σ_k μ_k·flow_k from each leg's duals, and the merchandising surplus
Σ_i LMP_i·d_i − Σ_g LMP_bus(g)·p_g from the primary — all within
0.01·total_load $/h. Leg deltas ride HiGHS vertex duals that are exact to
~1e-12 on integer-quantized data, so the 1e-2 agreement tolerances carry ~10
orders of headroom.
Signed shadow prices and the energy/congestion decomposition
mu_usd_per_mwh[k] is line k's shadow price, signed by the line's stated
from→to orientation: mu[k] = +14.0 means the limit binds in the from→to
direction and relaxing it by 1 MW would cut total system cost by $14/h;
congestion in the to→from direction makes mu[k] negative; every non-binding
line reports exactly 0.
Prices decompose against reference bus 0 and nothing else: energy := lmp[0]
and congestion[i] := lmp[i] − lmp[0], so congestion[0] is 0.0 by
definition — a decomposition computed against any other reference bus is a
wrong answer here even though the LMPs themselves are reference-independent.
The network is lossless DC, so the marginal-loss component that real ISO
three-part LMPs carry is identically zero and omitted.
Worked example. On the pinned 3-bus fixture (equal-reactance triangle,
$20 gen at bus 0, $50 gen at bus 2, loads [0, 30, 90]) line 2 (0→2, limit
50 MW) carries exactly +50 and binds. The duals are LMP = [20, 35, 50],
mu = [0, 0, +45]: energy is $20 everywhere, bus 2's $30 congestion premium
is the +45 shadow price shifted through the PTDF, and bus 1 sits at $35 —
halfway, because the triangle splits its withdrawal. Every price is inside the
[20, 50] offer band on this instance — the point of the environment is that
in a mesh they generically are not.
The counterflow / out-of-band phenomenon
The hard, unfakeable case is a bus whose LMP lands outside the offer band.
When a line is congested, injecting at a bus that relieves that line (a
negative-PTDF, counterflow bus) is worth more than any generator's energy, so
its LMP is pushed below the cheapest offer — sometimes negative. The
two-bus practitioner intuition (a real, documented misconception) says a
node's price is clipped to [min offer, max offer]; a heuristic that clips to
the band scores near-exactly on in-band instances and collapses on
out-of-band ones, because the clipped price vector no longer lies in the span
of the binding lines' PTDF rows and the reconstruction identity (F4) closes
the consistency gate. Out-of-band lands 44% of instances, overwhelmingly
on the above-priciest side (~92%) with a low-side / negative-LMP tail
(~8–12%) — the asymmetry follows the tiered offer stack and is published in
the sweep table, not hidden.
Reward ladder
Median / mean total reward per baseline, graded through the real reward stack on the shipped config (N = 60 calibration sweep; full table in CALIBRATION.md):
| Baseline | Median | Mean | What it is |
|---|---|---|---|
| format-only | 0.10 | — | parseable, no valid identities |
| self-consistent spam | 0.25 | — | format + consistency floor, zero instance knowledge |
| all-zeros constant | 0.25 (cong) / 0.55 (uncong) | 0.32 | the zero-knowledge answer |
| uniform-λ (Tier 1) | 0.250 | 0.41 | network-blind merit-order price |
| two-bus, binding-set given (Tier 2) | 1.00 | 0.66 | band-clip applied by someone who does the rest of the math right |
| optimal | 1.00 | 1.00 | the true duals |
| zonal-split (descriptive, ungated) | — | — | merit + congestion redispatch, priced per local unit — what an engineer does for one binding line; fails on meshed/multi-marginal |
The two-bus baseline is deliberately bimodal — ~1.0 on in-band instances, ~0.21 on out-of-band ones (the designed kill) — so its median sits at the in-band mode (1.0) and hides the separation, while its mean (~0.66) shows the real ~0.34 gap to optimal. The ship gate is measured on the mean, the correct instrument for a bimodal baseline (see CALIBRATION.md, DR-C): mean sep(uniform-λ → two-bus) = 0.25 and mean sep(two-bus → optimal) = 0.34, both comfortably above the 0.08 floor. The uniform-λ Tier-1 median lands exactly on the 0.25 self-consistent-spam floor: it is fully KKT-consistent (it cannot be gate-killed) but carries no price level on the congested 76%, so it is difficulty-killed, not gate-killed.
Per-class breakdown (shipped config): the separation lives on the out-of-band and two-plus slices — there the two-bus baseline reads 0.21 (gate closed) against optimal's 1.0 — while on the uncongested class Tier-1 is simply correct and scores 1.0. This is where the mean's separation comes from and why the per-class table is published alongside it.
Anti-reward-hacking (24 regression-tested attack gates)
Single shared parse gate: format-invalid answers cascade to zero on all five rewards (invariant asserted across the whole attack corpus). The redundant fields are deliberate: their internal KKT consistency is the cheap-to-verify surface — an answer that launders wrong prices through a self-consistent story must reproduce the exact span structure of the true binding constraints, which is as hard as being right. Gates cover: the uniform-λ and two-bus baselines (honest partial credit, strictly separated); NaN/Infinity literals,
4300-digit integers, bracket bombs (all score 0 without crashing); arity attacks killed by fixed lengths; all-lines-binding farming (double wall on two payloads); slack-line μ (material → reconstruction wall, sub-tolerance → Jaccard wall); three sign-flip probes attributed to the identity that kills each; reference-bus reframing caught by the
congestion[0]tripwire; epsilon-μ hedging; a series/parallel-line misattribution channel (truth prices, wrong line blamed — priced to ~0.25 by the binding-set gate, and kept out of shipped instances by the generation-side alternate-support reject); and the 3-bus hand example pinned on both legs to catch any unit slip or sign regression across scipy/HiGHS versions.
Usage
pip install -e . # scipy-only: no [validate] extra
N_INSTANCES=200 python tests/test_validation.py # two-leg cross-check + FD cert + 24 gates
python calibration/measure.py --n 200 # difficulty sweep + ship gate
# give frontier models room to reason; -r 1 matches published baselines
vf-eval nodal-pricing-lmp -p anthropic -m <model> -n 50 -r 1 --max-tokens 16000
Lower token budgets truncate reasoning mid-derivation and score as format failures — the dual-space arithmetic is long. The cross-check needs only scipy — there is no validation-only extra to install, which is the cheapest dependency story in the portfolio.
import verifiers as vf
env = vf.load_environment("nodal-pricing-lmp", num_examples=300)
# disjoint train/eval: load_environment(1000) + load_environment(200, seed_offset=1000)
Instance generation
Random connected meshed networks (spanning tree + chords, 4–10 buses), 3–5 generators drawn from a tiered merit stack with ≥$1.00/MWh pairwise offer separation (kills cost-tie degeneracy), loads and limits integer MW. Every draw is solved at generation and put through the full degeneracy pipeline — finite-difference kink detection, weak-μ and μ-rank certificates, and an alternate-support span reject — before acceptance; the raw-draw accept rate (~52%) is dominated by plain primal-infeasible draws, not the degeneracy gates. Class mix on the shipped config: 76% congested, 28% two-plus binding lines, 44% out-of-band. Fully deterministic per seed. The congestion rate is deliberately oversampled relative to a real N-1-planned grid — a realistic sample would be mostly trivial uncongested instances and would not exercise the skill.
Baseline
Opus 4.8, 30 examples, --max-tokens 16000 (reproduce with the vf-eval
line above):
| Metric | Value |
|---|---|
| Mean reward (all 30) | 0.267 |
| Truncated at 16k tokens | 15 / 30 (mean output 10.1k tokens) |
| Mean reward on the 15 completed rollouts | 0.535 |
| — exact (1.0) | 6 / 15 |
| — right structure, wrong binding set (0.21) | 6 / 15 |
| — consistent but wrong (0.25) | 3 / 15 |
Read honestly: this environment is hard, and it discriminates. On the rollouts Opus finishes, it lands across a real 0.21→1.0 spread — it parses and stays KKT-consistent (format + consistency credit) but on 40% of completed instances misidentifies the binding constraint set, so the binding-gated price credit correctly stays shut. That is the measured skill: identifying which lines bind is the hard part, and even a frontier model gets it wrong often. The dominant failure, though, is truncation — half the rollouts exhaust 16k tokens because Opus solves the DC power flow by hand (iterating bus angles) instead of reasoning about the dual directly; a token-efficient LMP-theory approach fits comfortably. Higher token budgets would lift the completion rate; the number above is at the published 16k. Opus does not clear 0.9 on the multi-binding subset, so the environment is not frontier-saturated.
Calibration status. A frontier probe (Opus 4.8, above) was run. It shows
the binding-set-correctness gate is the dominant discriminator, not the
price-decay curve: when the binding set is right the prices are essentially
right too (credit ~1.0), and when it is wrong the price gate is shut — so the
reward decay constants K_LMP (= ln 2) and K_MU (= 5.0) are barely
exercised and stay at their analytically-pinned values (right-structure / ±$5
answers land ~0.5; the all-zeros and epsilon-μ hedge rows). A denser sweep of
K would only matter for the correct-binding-imperfect-price regime, which
the frontier rarely reaches; documented here rather than over-tuned.
Roadmap
LMP is the price-formation layer future storage and trading environments consume: a battery arbitrage or a virtual-bid environment needs a verified nodal price series to grade against, and this is that series. Next rung: unit-commitment prices (non-convex pricing / uplift), tracked at n1-contingency-dispatch's successor — the binding-set identification here is the prerequisite skill.