Skip to content

Exact bid-based SCED

Public API for the exact primal-dual interior-point security-constrained economic dispatch (SCED) solver. For physics, see Physics → Markets.

Overview

offer_sced solves the offer-based SCED LP exactly via a primal-dual interior-point method (PD-IPM). It is JIT-compatible and vmap-able. LMPs are recovered from IPM duals and match HiGHS LP marginals to within < 1e-4 $/MWh on tested cases.

The setup-time entry point is prepare_offer_sced(case, n_segments=...) which builds the segment widths, base prices, line capacity, and PTDF-based mapping matrices. The runtime entry point is offer_sced(setup, load_mw, offer_prices, p_min_rt=None, p_max_rt=None).

API

from powerzoojax.envs import (
    OfferSCEDSetup,
    OfferSCEDResult,
    prepare_offer_sced,
    offer_sced,
)

powerzoojax.envs.market.offer_sced.prepare_offer_sced(case, n_segments=3)

Prepare OfferSCEDSetup from CaseData. Called once at setup time.

Uses pure float32 arithmetic in the PD-IPM solver (no x64 required). Numerical stability is achieved via adaptive regularization + diagonal equilibration in the KKT solve (see offer_sced for details).

Parameters:

Name Type Description Default
case CaseData

CaseData instance.

required
n_segments int

Number of offer-curve segments per generator.

3

Returns:

Type Description
OfferSCEDSetup

OfferSCEDSetup ready for offer_sced().

powerzoojax.envs.market.offer_sced.offer_sced(setup, node_load_mw, offer_prices=None, p_min_rt=None, p_max_rt=None, max_iter=20, tol=1e-05)

Solve bid-based SCED via primal-dual interior point method (float32).

Uses adaptive regularization + diagonal equilibration to stabilize the (n_dim+1)×(n_dim+1) KKT solve in float32 arithmetic. The regularization reg = max(1e-8, 1e-4 * mean(D_L)) is proportional to the mean line dual, which stays small relative to H diagonal (reg/H_ii ~ 1e-4) so the LP optimum is not significantly perturbed. Equilibration further reduces the effective condition number by row/column scaling.

Parameters:

Name Type Description Default
setup OfferSCEDSetup

OfferSCEDSetup (from prepare_offer_sced).

required
node_load_mw Array

(n_nodes,) nodal load [MW].

required
offer_prices Array

(n_units, n_segments) offer prices [$/MWh]. If None, uses setup.base_seg_prices (truthful bids).

None
p_min_rt Array

(n_units,) runtime lower bounds [MW] for ramp coupling. If None, uses setup.p_min (static case, backward compatible).

None
p_max_rt Array

(n_units,) runtime upper bounds [MW] for ramp coupling. If None, uses setup.p_max (static case, backward compatible). When provided together with p_min_rt, segment widths are scaled proportionally to the available headroom (p_max_rt - p_min_rt), enforcing ramp constraints in the LP without changing problem shape.

None
max_iter int

PD-IPM iterations (uses lax.fori_loop with early stopping). Default 20: case5 converges in 10-12 iterations for typical GB demand; remaining iterations are frozen no-ops. Early stopping also prevents float32 post-convergence drift (mu rebounds after ~iter 12 for congested cases without it).

20
tol float

Convergence tolerance on duality gap (μ = sᵀλ / m). Default 1e-5 (rather than 1e-6) because the KKT Schur complement becomes ill-conditioned before the gap closes below ~1.5e-6 for congested LPs; the frozen last-valid iterate typically has μ~1e-6, well within physical accuracy requirements.

1e-05

Returns:

Type Description
OfferSCEDResult

OfferSCEDResult with nodal LMP computed from IPM duals.

powerzoojax.envs.market.offer_sced.OfferSCEDSetup

Precomputed data for bid-based single-period SCED.

Setup-time fields (frozen, passed to JIT-compiled solver). All line limits have 0 replaced by ±1e5 (unconstrained).

powerzoojax.envs.market.offer_sced.OfferSCEDResult

Output of exact bid-based SCED solver.

All numerical assertions require converged == True as precondition.

Usage example

import jax
import jax.numpy as jnp

from powerzoojax.case import load_case
from powerzoojax.envs import prepare_offer_sced, offer_sced

case = load_case("5")
setup = prepare_offer_sced(case, n_segments=3)

load_mw = jnp.full((case.n_loads,), 100.0)
nodal_load = case.nodes_loads_map @ load_mw
offer_prices = setup.base_seg_prices  # truthful

result = offer_sced(setup, nodal_load, offer_prices)
print("dispatch:", result.unit_power)
print("lmp:", result.lmp)
print("converged:", result.converged)

To enforce per-step ramp bounds at the LP level (used by MarketMARLEnv), pass p_min_rt and p_max_rt overrides:

prev = previous_dispatch_mw
p_min_rt = jnp.maximum(setup.p_min, prev - ramp_down_mw)
p_max_rt = jnp.minimum(setup.p_max, prev + ramp_up_mw)
result = offer_sced(setup, nodal_load, offer_prices, p_min_rt=p_min_rt, p_max_rt=p_max_rt)