Case data#

pwrs represents a network as a MatpowerCase. Its standard fields follow the MATPOWER case struct, while numeric matrices are NumPy arrays or, for the generalized matrices A, N, and H, SciPy sparse matrices.

Built-in cases#

The package exposes built-in MATPOWER and PGLib-OPF cases through pwrs and the data subpackages:

import pwrs as mp
from pwrs import case9

case_a = mp.case9()
case_b = case9()

# Data sets remain grouped under pwrs.data.
case_c = mp.data.matpower.case9()
pglib_case = mp.data.pglibopf.pglib_opf_case14_ieee()

The first three forms load the same case. MATPOWER case functions remain available from the root package for compatibility, while pwrs.data groups the matpower and pglibopf collections explicitly. Packaged cases use the canonical JSON representation described below.

Unified load and save API#

Use pwrs.load() and pwrs.save() for case files. The format is normally inferred from the path suffix:

from pathlib import Path

import pwrs as mp

source = mp.case30()
path = mp.save(source, "case30.json")
loaded = mp.load(path)

assert isinstance(path, Path)
assert loaded.bus.shape == source.bus.shape

Both functions are also available from pwrs.corex.io. read_case() and write_case() are aliases; format-specific functions such as read_json() and write_npz() are available when an application intentionally requires a particular representation.

The supported formats are:

Format

Suffix

Intended use

JSON

.json

Canonical, human-readable, reviewable case data

MAT-file

.mat

Exchange with MATLAB and MATPOWER

NumPy archive

.npz

Fast, pickle-free local loading

Excel workbook

.xlsx

Sheet-oriented inspection and editing

PYPOWER mapping

none

In-memory interoperability with dictionary-based code

To use a path without a suffix, specify format; the appropriate suffix is then appended:

path = mp.save(mp.case9(), "case9-copy", format="npz")
case = mp.load("case9-copy", format="npz")

assert path.name == "case9-copy.npz"

Accepted file format names are "json", "mat", "npz", "excel", and "xlsx". An explicit format that conflicts with an existing suffix is rejected instead of silently writing a differently encoded file. Legacy .xls workbooks and MATLAB .m case functions are not part of the unified file API.

JSON#

JSON is the canonical repository format. It is stable and readable, places each dense matrix row on one line, and records array dtypes so that a round-trip does not convert integer arrays to floating point. For example:

mp.write_json(mp.case9(), "case9.json")
case = mp.read_json("case9.json")

Canonical files contain schema metadata and a __dtypes__ table. Sparse A, N, and H matrices use an explicit COO representation. Non-finite numeric values are encoded as "Infinity", "-Infinity", and "NaN", keeping the file valid standards-compliant JSON while restoring the original numeric values on load.

The reader also accepts older packaged JSON cases without schema or dtype metadata. New files written by pwrs always use the canonical schema.

MAT-files#

MAT-files contain a MATLAB struct. The default variable name is mpc, matching MATPOWER conventions:

mp.save(mp.case30(), "case30.mat")
case = mp.load("case30.mat")

Use variable_name when the struct has another name:

mp.save(mp.case30(), "network.mat", variable_name="network")
case = mp.load("network.mat", variable_name="network")

The name must be a valid MATLAB variable name. Loading fails clearly when the requested variable is absent or is not a struct.

NPZ#

NPZ is intended for repeated local loads where text review is not required:

mp.save(mp.case1354pegase(), "case1354pegase.npz")
case = mp.load("case1354pegase.npz")

The archive is uncompressed to minimize decoding overhead. It preserves array dtypes and stores sparse matrices as validated CSC components. Loading always uses allow_pickle=False; object arrays and archives with missing or unknown schema metadata are rejected. This makes NPZ suitable for development caches, but JSON should remain the checked-in source representation when human diff and review matter.

Excel#

Excel represents each case field on a separate sheet. Standard MATPOWER matrices have named columns, vector and string fields have a value column, and sparse matrices use row, column, and value columns. A required _meta sheet records the schema, dtype, shape, and scalar fields:

mp.save(mp.case30(), "case30.xlsx")
case = mp.load("case30.xlsx")

Excel support requires openpyxl, included in the all dependency group:

uv sync --group all

The reader validates headers, declared shapes, numeric dtypes, sparse coordinates, and sheet registration. Extra sheets are rejected because they would otherwise look like case data but be silently ignored. Use the comments case field rather than an unregistered notes sheet.

Legacy .xls files are not supported; save them as .xlsx first.

PYPOWER dictionaries#

load() accepts a PYPOWER-style mapping directly and returns an independent MatpowerCase:

ppc = {
    "version": "2",
    "baseMVA": 100.0,
    "bus": [[1, 3, 0, 0, 0, 0, 1, 1, 0, 230, 1, 1.1, 0.9]],
    "gen": [[1, 0, 0, 10, -10, 1, 100, 1, 10, 0]],
    "branch": [],
}

case = mp.load(ppc)
ppc_copy = mp.to_pypower(case)

from_pypower() is the explicit conversion equivalent to load(ppc), and to_pypower() returns a deep, independent dictionary. Mutating either the input mapping or converted output therefore does not mutate the case.

The format argument for a mapping may be omitted or set to "pypower" or "dict". save() always writes a file, so use to_pypower() when the desired output is another in-memory mapping.

Validation and preserved fields#

All formats pass through one canonical schema. A case must contain baseMVA, bus, gen, and branch. The supported optional fields are:

  • dense matrices: gencost, dcline, dclinecost, areas, and fparm;

  • dense or sparse matrices: A, N, and H;

  • vectors: l, u, Cw, z0, zl, and zu;

  • string lists: gentype, genfuel, bus_name, branch_name, and comments;

  • scalars: version, success, iterations, et, and f.

Dense matrices are normalized to two dimensions, vectors to one dimension, and inputs are copied. Deprecated busname and branchname fields are normalized to bus_name and branch_name. Unknown fields, conflicting aliases, invalid shapes, and invalid types are rejected rather than silently dropped.

Saving solved cases#

Structured results from runpf(), runopf(), and related functions can be saved directly as reusable cases:

options = mp.mpoption("out.all", 0, "verbose", 0)
result = mp.runpf(mp.case30(), options)

mp.save(result, "case30-solved.npz")
solved_case = mp.load("case30-solved.npz")

The solved bus, generator, and branch matrices and standard result scalars such as success, iterations, et, and f are preserved when present. Runtime solver state such as internal ordering, model objects, multipliers, and raw solver output is deliberately omitted because it is not reusable case data and may contain arbitrary Python objects.

Deprecated compatibility functions#

loadcase() and savecase() remain available for MATPOWER compatibility but emit DeprecationWarning for data IO. New code should use pwrs.load() and pwrs.save().

The legacy savecase() path is still required when generating a MATLAB .m case function. The unified API intentionally handles data formats only and does not execute or generate Python or MATLAB source files.