import os
import pytest
import gridoxide
from _pglib import fixtures
pytestmark = pytest.mark.skipif(
not hasattr(gridoxide, "dc_opf"),
reason="built without the opf-highs feature, so there is no solver",
)
PUBLISHED = {
"pglib_opf_case3_lmbd": 5695.9,
"pglib_opf_case5_pjm": 17480.0,
"pglib_opf_case14_ieee": 2051.5,
"pglib_opf_case30_ieee": 7472.8,
"pglib_opf_case118_ieee": 93101.0,
}
def case(name):
return os.path.join(fixtures(), f"{name}.json")
@pytest.fixture(scope="module")
def congested():
return gridoxide.dc_opf(case("pglib_opf_case5_pjm"))
def test_shapes_are_consistent(congested):
r = congested
assert len(r.dispatch) == len(r.generator_index) == 5
assert len(r.lmp) == len(r.angles) == 5
assert len(r.flows) == 6
assert len(r.shed) >= 0
def test_generation_meets_demand_exactly(congested):
assert sum(congested.dispatch) + sum(congested.shed) == pytest.approx(1000.0, abs=1e-6)
assert sum(congested.shed) == pytest.approx(0.0, abs=1e-9)
@pytest.mark.parametrize("name,published", sorted(PUBLISHED.items()))
def test_objectives_track_the_published_dc_baseline(name, published):
result = gridoxide.dc_opf(case(name))
assert result.objective == pytest.approx(published, rel=5e-4)
def test_the_susceptance_choice_is_exposed_and_changes_the_answer():
name = "pglib_opf_case30_ieee"
published = PUBLISHED[name]
default = gridoxide.dc_opf(case(name)).objective
textbook = gridoxide.dc_opf(case(name), dc_approximation="ignore_r").objective
assert abs(default - published) < abs(textbook - published)
assert textbook == pytest.approx(published * 1.00423, rel=5e-4)
with pytest.raises(ValueError, match="dc_approximation"):
gridoxide.dc_opf(case(name), dc_approximation="nonsense")
def test_prices_spread_only_where_something_binds(congested):
spread = max(congested.lmp) - min(congested.lmp)
assert congested.binding, "case5_pjm should be congested"
assert spread > 1.0, f"a congested case should show a price spread, got {spread}"
uncongested = gridoxide.dc_opf(case("pglib_opf_case14_ieee"))
assert not uncongested.binding
assert max(uncongested.lmp) - min(uncongested.lmp) == pytest.approx(0.0, abs=1e-6)
def test_binding_limits_report_flow_rate_and_price(congested):
for b in congested.binding:
assert 0 <= b.branch < len(congested.flows)
assert b.rate > 0
assert abs(abs(b.flow) - b.rate) == pytest.approx(0.0, abs=1e-6)
assert congested.flows[b.branch] == pytest.approx(b.flow, abs=1e-6)
assert b.price != 0.0
def test_dispatch_respects_the_generator_limits(congested):
import json
with open(os.path.join(fixtures(), "pglib_opf_case5_pjm.opf.json")) as f:
data = json.load(f)
limits = {g["index"]: (g["p_min"], g["p_max"]) for g in data["generator"]}
for index, p in zip(congested.generator_index, congested.dispatch):
low, high = limits[index]
assert low - 1e-6 <= p <= high + 1e-6, f"generator {index}: {p} outside [{low}, {high}]"
def test_shedding_can_be_disabled():
with_shed = gridoxide.dc_opf(case("pglib_opf_case5_pjm"))
without = gridoxide.dc_opf(case("pglib_opf_case5_pjm"), allow_shedding=False)
assert without.objective == pytest.approx(with_shed.objective, rel=1e-9)
def test_the_companion_document_can_be_given_explicitly():
explicit = gridoxide.dc_opf(
case("pglib_opf_case5_pjm"),
data_path=os.path.join(fixtures(), "pglib_opf_case5_pjm.opf.json"),
)
implicit = gridoxide.dc_opf(case("pglib_opf_case5_pjm"))
assert explicit.objective == pytest.approx(implicit.objective, rel=1e-12)
def test_the_solver_backend_is_selectable():
default = gridoxide.dc_opf(case("pglib_opf_case5_pjm"))
ipm = gridoxide.dc_opf(case("pglib_opf_case5_pjm"), solver="ipm")
assert default.objective == pytest.approx(ipm.objective, rel=1e-12)
try:
highs = gridoxide.dc_opf(case("pglib_opf_case5_pjm"), solver="highs")
except ValueError as e:
assert "opf-highs" in str(e)
else:
assert highs.objective == pytest.approx(ipm.objective, rel=1e-9)
with pytest.raises(ValueError, match="unknown solver"):
gridoxide.dc_opf(case("pglib_opf_case5_pjm"), solver="nonsense")
def test_missing_files_are_reported_clearly():
with pytest.raises(RuntimeError, match="reading"):
gridoxide.dc_opf(case("no_such_case"))
with pytest.raises(RuntimeError, match="data_path"):
gridoxide.dc_opf(case("pglib_opf_case5_pjm"), data_path="/nonexistent.json")