0

Nodal Pricing Lmp

Fresh

Locational marginal pricing (LMP) dual-space RL environment: given a congested lossless DC network, the model must produce the economic readout of ...

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

Cite

Notes

Only stored in your browser.

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_ki and 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 certificatenever "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)

MetricResult
B-θ vs PTDF leg LMP disagreements0 / 200
B-θ vs PTDF leg μ disagreements0 / 200
Worst leg deltas|ΔLMP| 1.3e-12, |Δμ| 5.9e-12 (tolerance 1e-2)
Finite-difference certificate failures0 / 200
Congestion-rent three-way identity failures0 / 200
Merchandising-surplus / cap violations (|LMP| > $2,000)0 / 200
Congested instances152 / 200 (76%)
Two-plus binding lines55 / 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):

BaselineMedianMeanWhat it is
format-only0.10parseable, no valid identities
self-consistent spam0.25format + consistency floor, zero instance knowledge
all-zeros constant0.25 (cong) / 0.55 (uncong)0.32the zero-knowledge answer
uniform-λ (Tier 1)0.2500.41network-blind merit-order price
two-bus, binding-set given (Tier 2)1.000.66band-clip applied by someone who does the rest of the math right
optimal1.001.00the 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):

MetricValue
Mean reward (all 30)0.267
Truncated at 16k tokens15 / 30 (mean output 10.1k tokens)
Mean reward on the 15 completed rollouts0.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.