Python API
Install the base package for parsing, emission, PowerIO IR, and typed values:
pip install powerio
The matrix and graph helpers need optional packages, so install the extra you want:
pip install 'powerio[matrix]' # NumPy and SciPy
pip install 'powerio[graph]' # NetworkX
pip install 'powerio[gridfm]' # Polars
pip install 'powerio[all]' # the three above
pip install 'powerio[pandas]' # pandas and PyArrow tables, Python 3.10 or later
pip install 'powerio[mcp]' # the MCP server, Python 3.10 or later
Importing powerio and calling parse, emit, serialize, or deserialize
does not import any of those optional packages.
Parse one source
powerio.parse accepts a path, a file object, or a bytes-like object. A str
is always a path, so wrap raw text in io.StringIO. There is no Python
Source class, because a path, file object, or bytes-like value already says
where the bytes come from and the interpreter owns them; parse takes it
directly. Rust and C build a Source because they need that ownership made
explicit, as Rust, Python, Julia, and C explains.
from io import StringIO
from pathlib import Path
import powerio
case = powerio.parse(Path("case9.m"))
case_from_text = powerio.parse(
StringIO(matpower_text), format="matpower", name="case9.m"
)
case_from_binary = powerio.parse(
pwb_bytes, format="pwb", name="case.pwb"
)
format is optional when the source name and content identify the format.
name applies only to memory and file object sources, where it supplies a
source name for diagnostics and format detection. There is no separate
parse_file, parse_text, or parse_bytes API.
Parse a GO Challenge 3 solution
Put the GO Challenge 3 problem file and its matching solution file in one
directory, and the ordinary parse call reads both:
solution = powerio.parse("scenario_002")
assert isinstance(solution.value, powerio.AcScucSolution)
With only the problem file, the same call returns AcScucInstance. A solution
file on its own fails, because it has neither the component definitions nor the
time axis. The solution module keeps both files and its diagnostics.
Module values and diagnostics
Parsing returns a PioModule[T], where module.value is the concrete Python
value and module.diagnostics is the list of diagnostics stored on that
module.
module = powerio.parse("case9.m")
print(module.type_name) # powerio.BalancedNetwork
if isinstance(module.value, powerio.BalancedNetwork):
print(module.value.n_buses)
for diagnostic in module.diagnostics:
print(diagnostic.code, diagnostic.severity, diagnostic.message)
records = powerio.diagnostic_records(module.diagnostics)
Diagnostics live on the module rather than on the contained network or
solution. Branch on module.value with isinstance; there is no .kind
property, kind enum, or typed narrowing helper. module.type_name is the
canonical structural name of the value, the same string the C ABI and PowerIO
IR use, for messages and machine-readable results.
powerio.diagnostic_records turns those diagnostics into JSON-ready
dictionaries, and powerio.diagnostic_record turns one. Each record keeps
code, severity, message, and target, and adds id,
suggested_action, related, details, and spans when the diagnostic
carries them.
The value classes are BalancedNetwork, dist.MulticonductorNetwork,
OperatingPoint, TimeSeries, ScenarioSet, GeoLayer, ContingencySet,
SubsystemSet, MonitoredSet, the PF, OPF, and SCUC instances and solutions,
and SocwrOpfSolution.
Emit grid exchange formats
powerio.emit is the only function that writes a grid exchange format:
result = powerio.emit(module, "matpower")
text = result.text
result = powerio.emit(module, "psse", "case.raw")
result = powerio.emit(module, "pypsa", "case-directory")
With no destination the artifacts stay in memory; a path destination writes one
file or a directory, and a writable file object accepts a single file artifact.
An EmitResult has the artifacts (one Artifact per produced file, with its
name and either data for a memory result or path after a filesystem
commit), the layout, the fidelity, and the emission diagnostics. result.text
is the UTF-8 memory artifact when there is a single one, and None otherwise.
PowerIO IR has its own pair of functions:
ir = powerio.serialize(module)
powerio.serialize(module, "case.pio.json")
same_module = powerio.deserialize(ir.artifacts[0].data)
The IR header is "schema": "pio-ir" with the integer "version": 2, and
powerio.versions()["powerio_ir"] reports both. The producer record gives
powerio.__version__ separately. deserialize refuses a document whose schema
or version it does not support and reports what it found. PowerIO IR is not a
grid exchange format, so it does not appear in format discovery.
PSS/E contingency analysis files
A contingency set is the content of a PSS/E .con file: named cases, each a
list of outage actions. A subsystem is PSS/E’s name for a bus selection stated
in a .sub file through bus, area, zone, owner and kV range selectors; a
.con automatic specification and a .mon statement refer to a subsystem by
name, and selecting a subsystem’s buses evaluates its selectors against a
network. A monitored set is the content of a .mon file: the elements whose
loading and voltage a study reports. A geo layer is a coordinate document kept
beside a case: points for buses and routes for branches in one coordinate
space, keyed by element identity, read from five text forms and written as a
GeoJSON FeatureCollection carrying a powerio_geo member.
A .con, .sub, or .mon file parses to ContingencySet, SubsystemSet,
or MonitoredSet. Each has a text property holding the file. A contingency
set and a subsystem set also reach a network through a BalancedNetwork
method.
case = powerio.parse("case.raw").value
cases = powerio.parse("cases.con").value
print(cases.text.splitlines()[0])
resolution = case.resolve_contingencies(cases.text)
print(resolution["resolved"], "of", resolution["cases"], "cases bound")
for result in resolution["case_results"]:
if not result["resolved"]:
print(result["name"], result["unresolved"][0]["reason"])
groups = powerio.parse("groups.sub").value
expanded, notes = case.expand_contingencies(cases.text, groups.text)
print(expanded)
print(case.select_subsystem_buses(groups.text, "A1"))
resolve_contingencies reports rather than refuses: a case naming an element
the network does not hold is counted unresolved and listed in case_results
with the reason each action did not bind. Each reason is a fixed name such as
no_such_branch; C reports the same names.
Each element a case bound to states its type, which names the table, the
row it occupies there, the element’s own in_service flag, and its id.
id is None when the network states no identity for that row, and type
and row name the element either way.
expand_contingencies turns an automatic specification such as SINGLE BRANCH IN SUBSYSTEM 'A1' into one explicit case per element, and returns the
expanded .con text with the readers’ and the expansion’s notes.
MonitoredSet is text only in Python. It carries the file as its text
property and has no method that binds it to a network, because binding a
monitored element file to a network and a subsystem set is a Rust operation.
C reads a monitored set’s statement count with
pio_monitored_set_statement_count and its text with
pio_monitored_set_to_mon. C selects a subsystem’s buses with
pio_balanced_network_select_subsystem_buses, which names the same buses as
select_subsystem_buses.
Collections
TimeSeries behaves like a Python sequence and ScenarioSet like a mapping:
series = module.value
first = series[0]
for value in series:
use(value)
scenarios = scenario_module.value
base = scenarios["base"]
for scenario_id in scenarios:
use(scenario_id, scenarios[scenario_id])
Entries are owner rooted typed values, so indexing does not serialize or copy a complete network.
Typed updates
PowerIO supplies OperatingPointUpdate, NetworkUpdate, and
CalculationUpdate. Each update targets a stable ComponentId, and power
values use ActivePower, ReactivePower, or ApparentPower so the unit is
explicit.
report = powerio.apply_updates(
module,
[
powerio.OperatingPointUpdate.set_load_active_power(
load_id, powerio.ActivePower.megawatts(42.0)
)
],
)
for change in report.changes:
print(change.component_id, change.field)
print(report.connectivity_changed)
The whole batch is validated before anything is mutated, so a failed batch
leaves the module unchanged. The UpdateReport lists each change and says
whether energized connectivity changed.
Matrices and vectors
The derived calculations are calc_* methods on BalancedNetwork:
A = network.calc_incidence_matrix()
b = network.calc_branch_susceptances()
B = network.calc_bus_susceptance_matrix()
Bf = network.calc_branch_flow_matrix()
p_branch = network.calc_branch_flow_dc(voltage_angles)
p_bus = network.calc_bus_injection_dc(voltage_angles)
Every DC calculation shares two axes. calc_dc_index_map names them:
bus_ids maps a bus row to the source bus id (every bus in table order),
branch_rows maps a branch row to its position in the branch table (three
winding transformer windings follow the branches), and branch_ids gives the
stable identity of that row, the branch uid when the source states one and
branches:<row> otherwise. Out of service branches and self loops have no
row. A zero impedance branch fails a DC calculation with
BUILD.OPERATOR.ZERO_IMPEDANCE; skip_zero_impedance=True drops it and
calc_dc_index_map lists it under skipped_branch_rows.
axes = network.calc_dc_index_map(skip_zero_impedance=True)
A = network.calc_incidence_matrix(skip_zero_impedance=True)
assert A.shape == (len(axes["branch_ids"]), len(axes["bus_ids"]))
calc_admittance_matrix, calc_bprime_matrix, calc_ptdf, calc_lodf,
to_normalized, and to_networkx are methods of the same class. SciPy is
imported only when you ask for a sparse matrix, NumPy only for the array based
helpers, and NetworkX only inside to_networkx.
An OperatingPoint entry of a TimeSeries or ScenarioSet exposes
.network, the balanced network with that point’s values applied, so a solver
receives the entry without emitting and reparsing it. The property returns the
network alone: net bus injection quantities have no balanced network field, so
they are dropped and the property reports nothing. emit states that same
omission as EMIT.OPERATING_POINT.DATA_OMITTED, so emit the collection when
you need the diagnostic.
Other functions
| Function | Result |
|---|---|
resolve_format(name) | the canonical FormatInfo for a token or alias, or None |
features() | which build features the installed extension carries |
versions() | the release, the PowerIO IR identity, and the BMOPF schema version |
parse_geo(text, name_hint=None) | a geographic layer in canonical form with its diagnostics |
parse_display(path, format=None) | the raw PowerWorld .pwd display record as DisplayData |
from_ppc(ppc) | a BalancedNetwork from a pandapower or PYPOWER case dictionary |
PioModule.from_value(value) | a module around a value built in Python |
diagnostic_record(diagnostic) | one diagnostic as a JSON-ready dictionary |
diagnostic_records(diagnostics) | every diagnostic as a JSON-ready dictionary, in order |
module.to_balanced_report(), module.to_balanced() | the multiconductor to balanced transformation |
GeoLayer.geojson returns the canonical .geo.json document for a layer, the
GeoJSON FeatureCollection with the powerio_geo member that parse_geo reads.
C returns the same text from pio_geo_layer_to_geojson.
Errors
A parse failure raises PowerIOParseError, and valid data that cannot satisfy
an operation raises PowerIODataError. Both derive from PowerIOError and
have a stable diagnostic code, so branch on .code rather than on the rendered
message. A Rust panic inside the extension raises PowerIOError with code
BIND.PY.PANIC instead of pyo3_runtime.PanicException, and the module is
left unchanged, because each mutation is built in full before it is installed.
MCP server
The optional MCP server accepts paths, grid exchange content held in memory,
and serialized PowerIO modules through the powerio_ir field. Electrical
inputs and outputs stay PowerIO types and PowerIO IR; the server does not
define another network, calculation, update, or solution schema.
Filesystem reads and writes default to the directory captured at server startup.
POWERIO_MCP_ALLOWED_ROOTS selects explicit directories instead; the compatibility
settings POWERIO_MCP_ROOT and POWERIO_MCP_ALLOWED_ROOT follow it in precedence.
Remote URI schemes are rejected. Host
approval, request identifiers, timeouts, and cancellation are MCP transport
concerns and do not touch the PowerIO data.