import json
import tempfile
from pathlib import Path
import pytest
np = pytest.importorskip("numpy")
from gridoxide.matpower import ( COST_MODEL_PIECEWISE,
COST_MODEL_POLYNOMIAL,
convert,
load_mpc,
)
from _pglib import CASES as PGLIB_CASES, PIECEWISE, fixtures
CASES = sorted(PGLIB_CASES + [PIECEWISE])
@pytest.mark.parametrize("case", CASES)
def test_cost_curves_are_transcribed_faithfully(case):
mpc = load_mpc(fixtures() / f"{case}.m")
gencost = np.atleast_2d(mpc["gencost"])
gen = np.atleast_2d(mpc["gen"])
opf = json.loads((fixtures() / f"{case}.opf.json").read_text())
by_index = {g["index"]: g for g in opf["generator"]}
checked = 0
for row in range(len(gen)):
entry = by_index.get(row)
if entry is None or "cost" not in entry or row >= len(gencost):
continue
model = int(gencost[row, 0])
n_cost = int(gencost[row, 3])
raw = [float(v) for v in gencost[row, 4 : 4 + n_cost * (1 if model == COST_MODEL_POLYNOMIAL else 2)]]
if model == COST_MODEL_POLYNOMIAL:
assert entry["cost"]["model"] == "polynomial"
assert entry["cost"]["coefficients"] == list(reversed(raw)), (
f"{case} generator {row}: raw {raw} should reverse to "
f"{entry['cost']['coefficients']}"
)
else:
assert entry["cost"]["model"] == "piecewise_linear"
pairs = [[raw[i], raw[i + 1]] for i in range(0, len(raw), 2)]
assert entry["cost"]["points"] == pairs, (
f"{case} generator {row}: raw {raw} should pair up to "
f"{entry['cost']['points']}"
)
xs = [x for x, _ in pairs]
assert xs == sorted(xs), f"{case} generator {row}: breakpoints not ascending"
checked += 1
assert checked > 0, f"{case}: no cost rows were actually compared"
@pytest.mark.parametrize("case", CASES)
def test_generator_limits_come_from_the_right_columns(case):
mpc = load_mpc(fixtures() / f"{case}.m")
gen = np.atleast_2d(mpc["gen"])
opf = json.loads((fixtures() / f"{case}.opf.json").read_text())
by_index = {g["index"]: g for g in opf["generator"]}
for row in range(len(gen)):
entry = by_index.get(row)
if entry is None:
continue
assert entry["p_max"] == pytest.approx(float(gen[row, 8]))
assert entry["p_min"] == pytest.approx(float(gen[row, 9]))
assert entry["node"] == int(gen[row, 0])
@pytest.mark.parametrize("case", CASES)
def test_every_pglib_branch_carries_a_rating(case):
opf = json.loads((fixtures() / f"{case}.opf.json").read_text())
unlimited = [b for b in opf["branch_limit"] if b["unlimited"]]
assert not unlimited, f"{case}: {len(unlimited)} unrated branches"
assert all(b["rate_a"] > 0.0 for b in opf["branch_limit"])
def test_a_case_without_ratings_is_marked_unlimited_not_dropped():
source = (
Path(__file__).resolve().parents[2]
/ "tests" / "data" / "benchmark-grids" / "matpower" / "case14.m"
)
if not source.exists():
pytest.skip("benchmark-grids submodule not initialized")
with tempfile.TemporaryDirectory() as tmp:
tmp = Path(tmp)
convert(source, tmp / "case14.json")
opf = json.loads((tmp / "case14.opf.json").read_text())
assert len(opf["branch_limit"]) == 20
assert all(b["unlimited"] for b in opf["branch_limit"])
assert all(b["rate_a"] == 0.0 for b in opf["branch_limit"])
def test_piecewise_costs_are_read_as_point_pairs():
case = """
function mpc = tiny
mpc.version = '2';
mpc.baseMVA = 100.0;
mpc.bus = [
\t1\t3\t0\t0\t0\t0\t1\t1.0\t0\t345\t1\t1.1\t0.9;
\t2\t1\t50\t10\t0\t0\t1\t1.0\t0\t345\t1\t1.1\t0.9;
];
mpc.gen = [
\t1\t50\t0\t100\t-100\t1.0\t100\t1\t200\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0\t0;
];
mpc.branch = [
\t1\t2\t0.01\t0.1\t0\t150\t150\t150\t0\t0\t1\t-360\t360;
];
mpc.gencost = [
\t1\t0\t0\t3\t0\t0\t100\t1500\t200\t4000;
];
"""
with tempfile.TemporaryDirectory() as tmp:
tmp = Path(tmp)
(tmp / "tiny.m").write_text(case)
convert(tmp / "tiny.m", tmp / "tiny.json")
opf = json.loads((tmp / "tiny.opf.json").read_text())
cost = opf["generator"][0]["cost"]
assert cost["model"] == "piecewise_linear"
assert cost["points"] == [[0.0, 0.0], [100.0, 1500.0], [200.0, 4000.0]]
assert opf["branch_limit"][0]["rate_a"] == 150.0
assert opf["branch_limit"][0]["unlimited"] is False
assert COST_MODEL_PIECEWISE == 1 and COST_MODEL_POLYNOMIAL == 2