Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

PowerIO guide

PowerIO reads power system data into typed values, writes those values in the formats other tools read, and builds the sparse matrices and graph data that solvers consume. The Rust crates are the implementation, and the Python package, PowerIO.jl, and the C ABI expose the same operations under the same names.

use powerio::{PioValue, parse};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let module = parse("case9.m")?;         // PioModule<PioValue>
    match module.value() {
        PioValue::BalancedNetwork(network) => {
            println!("{} buses", network.buses().len());
        }
        other => println!("{}", other.type_name()),
    }
    Ok(())
}
import powerio
module = powerio.parse("case9.m")                 # PioModule[BalancedNetwork]
using PowerIO
module_ = parse("case9.m")                        # PioModule{BalancedNetwork}
feeder = parse("IEEE13Nodeckt.dss")               # PioModule{MulticonductorNetwork}

A parse returns a module, which is one typed value together with the data that explains it: the source descriptions, the source map, the reader’s diagnostics, the derivation history, and, while the process runs, the source bytes themselves. Which value you get depends on what the source declares:

SourceValue
MATPOWER, PSS/E, XIIDM, CGMES, UCTE-DEF, and the other balanced formatsBalancedNetwork
OpenDSS, PowerModelsDistribution JSON, BMOPF JSONMulticonductorNetwork
a PyPSA directory whose inputs vary by snapshotTimeSeries<BalancedNetwork>
a GridFM Parquet datasetScenarioSet<BalancedNetwork>
a DOE GO Challenge 3 problem fileAcScucInstance
that problem file beside its solution fileAcScucSolution
a DeepMind OPFData fileAcOpfSolution
a geographic layer document or a PowerWorld .pwd displayGeoLayer

parse reads a grid exchange format into a module, and emit writes a module out in one. The other two operations, serialize and deserialize, write and read PowerIO IR, the JSON document that stores a complete module for another PowerIO consumer to read back. PowerIO IR is not a grid exchange format, so parse does not accept it; deserialize does.

Whichever format you use, writing an unchanged module back to its own format reproduces the source bytes byte for byte, and writing to another format keeps what that format can represent and reports each loss as a diagnostic with a stable code. Converting from one kind of value to another is an explicit operation that adds an entry to the module history; nothing converts as a side effect.

To install PowerIO and run a first conversion, start with Getting started. Core concepts defines the module, the value types, diagnostics, and sources. Formats and fidelity says what each reader keeps and each writer reports, and Rust, Python, Julia, and C shows each operation in all four languages.

Getting started

Install

Rust:

cargo add powerio             # parsing, emission, PowerIO IR
cargo add powerio -F matrix   # and sparse matrices, sensitivities, graph data

Use Rust 1.88 or newer. Add the gridfm feature when your application reads or writes GridFM Parquet directories: cargo add powerio -F gridfm.

Python:

pip install powerio           # parsing, emission, PowerIO IR
pip install 'powerio[all]'    # and SciPy matrices, NetworkX graphs, Polars for GridFM

Julia:

using Pkg; Pkg.add("PowerIO")

The powerio command:

cargo install powerio-cli

For C and C++, build the shared library from a checkout and include the checked in header.

git clone https://github.com/eigenergy/powerio
cd powerio
cargo build -p powerio-capi --release --features arrow,matrix,gridfm,dist,prob
# target/release/libpowerio_capi.{so,dylib,dll}; header powerio-capi/include/powerio.h

Parse, inspect, emit

Download case9.m to your working directory for these examples. In a repository checkout, the same file is tests/data/case9.m; pass that path when running from the root.

Rust. The example is a complete program; it imports only parse, emit, and the PioValue enum, and ? hands any failure to main.

use powerio::{PioValue, emit, parse};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let module = parse("case9.m")?;
    let PioValue::BalancedNetwork(network) = module.value() else {
        panic!("expected a balanced network");
    };
    println!("{} buses", network.buses().len());
    for finding in module.diagnostics() {
        eprintln!("{}: {}", finding.code(), finding.message());
    }
    emit(&module, "matpower", "copy.m")?;      // the source bytes, unchanged
    let result = emit(&module, "psse", "case9.raw")?;
    for finding in result.diagnostics() {
        eprintln!("{}", finding.code());       // what PSS/E cannot carry
    }
    Ok(())
}

Python:

import powerio

module = powerio.parse("case9.m")
network = module.value                       # BalancedNetwork
for finding in module.diagnostics:
    print(finding.code, finding.message)
powerio.emit(module, "matpower", "copy.m")   # the source bytes, unchanged
result = powerio.emit(module, "psse")        # text in memory
result.text
result.diagnostics

Julia:

using PowerIO

module_ = parse("case9.m")
net = module_.value                          # BalancedNetwork
length(net.buses)                            # 9
module_.diagnostics
emit(module_, "matpower", "copy.m")          # the source bytes, unchanged
result = emit(module_, "psse")               # text in memory
result.diagnostics

Command line:

powerio convert case9.m --to psse -o case9.raw   # findings on stderr
powerio summary case9.m                          # counts, bases, and findings as JSON

Keep a module for later

Use serialize to save the value with its diagnostics and history, then deserialize to restore it in any PowerIO binding:

powerio::serialize(&module, "case9.pio.json")?;
let restored = powerio::deserialize("case9.pio.json")?;
powerio.serialize(module, "case9.pio.json")
restored = powerio.deserialize("case9.pio.json")
serialize(module_, "case9.pio.json")
restored = deserialize("case9.pio.json")

PowerIO IR stores the typed data and source metadata. Original input bytes stay in memory, so an unchanged parsed module can echo them, while a restored module produces fresh output. Inspect the returned emission diagnostics when exporting to another tool.

Where next

Core concepts

PowerIO borrows its structure from compiler infrastructure. Source text and the data a program computes with are different representations, and you move between them through a small set of explicit, checked operations. The electrical content is described in ordinary power system terms, and the same types and operations appear in Rust, C, Python, Julia, PowerIO IR, and the MCP server.

The module

PioModule<T> contains one typed value and the data that explains it:

PioModule<T>
├── value: T
├── diagnostics
├── producer
├── sources and source map
├── history
└── extensions

In Rust you reach the value through module.value() and the diagnostics through module.diagnostics(). Python and Julia expose both as properties, value and diagnostics, and C exposes borrowed accessors because its values are opaque. Diagnostics belong to the module rather than to the network or solution inside it.

While the process runs, a module can keep the bytes it was parsed from, which is how writing it back to its own format reproduces them byte for byte. Those bytes are not part of serialized PowerIO IR, and editing the value drops them, so the next same format write serializes the edited value instead of the old bytes.

Values

PioValue is the closed set of types a module can contain at the dynamic boundary, meaning automatic parsing, PowerIO IR, the C, Python, and Julia bindings, and the MCP server.

BalancedNetwork
MulticonductorNetwork
OperatingPoint<BalancedNetwork>
OperatingPoint<MulticonductorNetwork>
TimeSeries<T>
ScenarioSet<T>
DcPfInstance, AcPfInstance, DcOpfInstance, AcOpfInstance,
McAcPfInstance, McAcOpfInstance, AcScucInstance
DcPfSolution, AcPfSolution, DcOpfSolution, AcOpfSolution, SocwrOpfSolution,
McAcPfSolution, McAcOpfSolution, AcScucSolution
GeoLayer
TypeMeaning
BalancedNetworkA self contained balanced case: equipment identities, terminals, physical parameters, ratings, limits, costs, and the source’s operating assignment.
MulticonductorNetworkThe conductor resolved distribution model.
OperatingPoint<N>A possibly partial alternate electrical assignment over fixed equipment. It makes no claim of completeness or power flow feasibility.
TimeSeries<T>Values of one type ordered in time.
ScenarioSet<T>Named alternatives of one type, optionally with probabilities and with no time order.
*InstanceThe complete input of one named calculation: fixed inputs, unknowns, bounds, objectives, horizon, contingencies, and formulation choices.
*SolutionThe result of one calculation: computed quantities, termination, residuals, multipliers, and the objective or bound.
GeoLayerCoordinates and routes for the elements of a case, as a document of its own.

The two network types are peers, and neither is a subtype of the other. BalancedNetwork is where MATPOWER, PSS/E, XIIDM, CGMES, and the other balanced formats meet; MulticonductorNetwork is where OpenDSS, PowerModelsDistribution JSON, and BMOPF meet. To get a balanced positive sequence equivalent from a multiconductor network you call an explicit transformation, which reports each assumption it made and each thing it lost.

A BalancedNetwork parsed from PSS/E and one parsed from MATPOWER are the same type with the same meanings. Each format has a documented profile, and data outside that profile is reported and stays in the retained source; it is not folded into the model. There is no universal network format. The balanced model does not absorb multiconductor data, other energy carriers, or calculation data.

A Rust application can put its own type in a module, PioModule<MyType>, and get the same source, diagnostic, and history behavior. That type stays outside the dynamic boundary until PowerIO adds it to PioValue, the IR schema, and the bindings, so until then it cannot pass through PowerIO IR or reach the other languages.

Operating points

An operating point can override demand, setpoints, dispatch, voltages, injections, equipment service status, switch positions, transformer taps, phase shifts, and the corresponding multiconductor controls. Any quantity it leaves out resolves to the network’s own assignment, which is why an operating point can be partial.

It cannot change equipment identities or terminals, physical parameters, ratings, costs, commitment or reserve structure, the horizon, or the equipment set. If your scenario changes any of those, it contains a network or a calculation instance instead of an operating point.

Topology is calculated from the declared terminals, equipment service status, and switch positions, which together give the energized connectivity. Tap and phase shift changes affect the equations and leave the connectivity alone.

Collections

TimeSeries<T> and ScenarioSet<T> nest without flattened names, so TimeSeries<OperatingPoint<BalancedNetwork>>, ScenarioSet<TimeSeries<BalancedNetwork>>, and the other combinations keep their structural type. An operating point entry refers to the shared base network instead of copying its tables, so a series of operating points contains one network and one sparse set of overrides per point. A series of networks or instances contains complete values when their physical or calculation data differs.

Indexing a collection returns the contained value or a view rooted in the owning module; nothing reparses and no complete network is copied.

Calculation instances and solutions

Network data, calculation inputs, and results are separate types. A MATPOWER case parses to a BalancedNetwork, and you construct a DcPfInstance, AcPfInstance, DcOpfInstance, or AcOpfInstance from it. A solver takes the instance and returns the corresponding solution; PowerIO itself never solves.

SocwrOpfSolution is a PowerModels SOCWR relaxation together with its objective lower bound. It is not an AcOpfSolution unless voltage recovery and AC residual checks support that claim.

Diagnostics

Every operation reports diagnostics. Each Diagnostic has a stable dotted code, a severity (error, warning, remark, or note), a message, and, where available, a target, source byte spans, related diagnostics, and a suggested action. Branch on the code rather than on the rendered message; the code is the part that stays stable.

A successful operation keeps its diagnostics on the returned module or result. A failed operation returns or raises the language’s PowerIO error, which contains the same diagnostics.

Sources, formats, and destinations

A Source owns one or more named immutable byte buffers read from a file, a directory, or memory. In Rust and C you build it yourself, since those languages need an explicit owner for the bytes. Python and Julia take a path, an open file, or a bytes value directly, because the interpreter already owns them; Rust, Python, Julia, and C explains the split. parse detects the format from the source name and content unless you declare one.

emit writes one format and returns an EmitResult. The result contains one artifact per file produced, each with its name and either its bytes (for a memory destination) or its path after a write; the layout, which is one file or a directory; the fidelity, which is either an exact echo of retained source bytes or fresh canonical output; and the emission diagnostics, which report each loss. A single file format produces one artifact, and a directory format such as PyPSA CSV, GridFM, or CGMES produces one per file.

resolve_format maps an accepted spelling of a format to its canonical token and reports the conventional file suffix, the output layout, and whether a fresh writer exists. It describes formats only; values are named by PioValue and its counterparts in each language.

PowerIO IR

serialize writes a module as PowerIO IR and deserialize reads it back with its types, diagnostics, sources, history, and extensions intact. The document has an integer generation that changes only when the serialized representation changes. PowerIO IR is absent from grid exchange format discovery, so parse does not accept it. PowerIO IR defines the document and its generation rule.

Derived data

Sparse matrices, dense solver rows, factorizations, and caches are analysis data computed from a value. They keep element mappings back into that value, they are not stored in a module, and their representation can change without changing any public meaning.

Every transformation declares its input and output types and returns diagnostics. Multiconductor to balanced conversion moves to a less detailed representation under stated assumptions, while constructing an instance from a network moves to a more specific one. Format conversion is parse followed by emit at the same level. LLVM and MLIR lessons lists the decisions PowerIO shares with LLVM and MLIR and the ones it does not.

Transmission networks

A balanced network is the positive sequence transmission model. All of the balanced formats in the format table parse to it, so one reader and one writer per format cover all the conversions between them.

using PowerIO
module_ = parse("case118.m")       # PioModule{BalancedNetwork}
net = module_.value
length(net.buses)                  # 118
net.branches[1]                    # buses, branches, generators, loads, and the other tables

The network keeps what the source says: the element inventory, terminal connections, impedances, ratings, generator capability bounds and cost curves, and the source’s operating assignment. Ratings and costs stay on the network as reusable data, and which bounds a calculation enforces is decided when you construct an instance, so one parsed case serves power flow, DC OPF, and AC OPF without reparsing (see Calculation instances and solutions).

A few conventions hold across the accessors. Bus identifiers are the source’s own; dense zero based indices exist only in matrix results, which include the mapping. Powers are MW and MVAr as the source gives them, and angles are degrees; to_normalized derives a per unit, radian, in service only copy when a solver wants one. A branch tap of 0 means 1, and a rate_a of 0 means unrated.

Sources that describe more than the balanced calculation view, such as XIIDM, CGMES, and PSS/E RAW 35, also fill in the detailed connectivity: substations, voltage levels, connectivity nodes, terminals, switches, operational limit groups, and tap changer controls. A writer whose format can represent those writes them, and one whose format cannot reports what it left out.

emit(module_, "matpower", "copy.m")            # the source bytes, unchanged
result = emit(module_, "psse")                 # fresh PSS/E text
for finding in result.diagnostics
    println(finding.code, ": ", finding.message)
end

From a library you call parse, keep the module, and call emit; on the command line, powerio convert does both in one call.

Building matrices from a balanced network has its own chapter, Matrices and graphs.

Distribution networks

A multiconductor network is the conductor level distribution model, and OpenDSS, PowerModelsDistribution engineering JSON, and BMOPF JSON all parse to it. When you need a calculation, construct an McAcPfInstance or McAcOpfInstance from it explicitly (see Calculation instances and solutions).

using PowerIO
feeder = parse("IEEE13Nodeckt.dss")        # PioModule{MulticonductorNetwork}
net = feeder.value
net.lines[1]                               # terminal maps and the line code reference
net.linecodes[1]                           # per length impedance matrices, SI units
feeder.diagnostics                         # what the reader kept, assumed, or refused

The model identifies individual conductors throughout. Buses have ordered terminals and explicit grounding, lines and switches map conductors between terminal sets, transformers list their windings with connection kinds, and loads and generators attach per terminal. Impedance and shunt matrices are in SI units, per unit length, on line codes. An element the reader has no typed slot for stays verbatim in the untyped table and is reported.

The OpenDSS profile is the static circuit, meaning the element definitions and their electrical data. Load shapes, solve commands, monitors, and other calculation instructions are outside it; they stay in the retained source, are reported as uninterpreted, and survive a same format write byte for byte.

emit(feeder, "dss", "copy.dss")            # the source bytes, sidecars included
result = emit(feeder, "pmd")               # fresh PMD JSON
result.text
result.diagnostics

Nothing converts between the multiconductor and balanced models implicitly. The balanced positive sequence equivalent is an explicit transformation that reports its assumptions, such as voltage bases per zone, phase aggregation, and switch merging, and refuses what it cannot represent:

use powerio::transform::{to_balanced, to_balanced_report};

let report = to_balanced_report(&feeder)?;   // readiness, assumptions, losses, diagnostics
let balanced = to_balanced(&feeder)?;        // PioModule<BalancedNetwork>

Python exposes the same pair as module.to_balanced_report() and module.to_balanced(). The transformation has no C entry point in 0.11, so PowerIO.jl does not bind it.

Multiconductor admittance matrices build directly from the multiconductor network through powerio_matrix::calc_multiconductor_admittance_matrix, which is Rust only in 0.11.

BMOPF schema versions

PowerIO v0.11.0 supports draft BMOPF 0.2, subject to Task Force review. The schema version is separate from the PowerIO release and from PowerIO IR generation 2. Producer provenance records the proposal revision and schema digest. Previously emitted schema identifiers remain readable aliases.

An unqualified bmopf-json emission preserves an unchanged source byte for byte. An explicit schema version always re-encodes the selected version:

powerio convert feeder.json --to bmopf-json@0.1.0 -o legacy.json
powerio convert feeder.json --to bmopf-json@0.2.0 -o proposal.json

The same format strings work with Rust powerio::emit, Python powerio.emit, C pio_emit, Julia emit and the PowerIO MCP emission tool. Rust also exposes BmopfSchemaVersion and BmopfEmitOptions for the distribution writer.

Legacy output relocates proposed-only equipment and transformer fields into extras, with diagnostics. A consumer that ignores those extensions cannot be assumed to calculate the same network. Proposal output uses the declared tables and preserves winding ratings, taps, neutral impedances and current-limit data.

Unequal bus phase bounds remain individual values through parsing, generation-2 IR and explicit BMOPF output. Uniform values use v_min/v_max; unequal values use v_min_phase/v_max_phase in the typed Rust model. A present scalar takes precedence over the corresponding vector. PMD voltage arrays instead follow terminal order and use engineering voltage units.

The C and Julia bus views expose unequal limits as phase_to_ground_voltage_min_v and phase_to_ground_voltage_max_v, while voltage_min_v and voltage_max_v represent uniform bounds. C spans borrow from the network’s owner and remain valid for the lifetime of that handle. The unreleased ABI 7 layout and Julia definitions are updated together.

Parsing, structural schema validity, semantic consistency and computational support are separate checks. Contradictory versions, invalid dimensions, unknown electrical references and inconsistent bounds produce error diagnostics. The native multiconductor admittance builder supports ideal grounded-WYE transformer coupling and rejects leakage, other winding connections, floating neutrals, core shunts or tap decisions that need a different formulation.

The BMOPF proposal schema is pinned to fe8671a, with SHA-256 74d6c6de3637d52e42a26c4cb0584f51df70d69f360b236cf5e23afaf7669462. Fresh 0.2.0 output places the immutable retrieval URL in meta.$schema and records the canonical identity, proposal status, digest and revision under meta.provenance.powerio_bmopf. Existing provenance is preserved; a name collision uses a numbered producer entry. Reading the former canonical or version-directory identifiers remains supported without fetching remote data.

Energy prices

Draft BMOPF 0.2 uses energy_cost_rate in $/kWh. Generator entries follow phase order, as do voltage-source entries. Neutral terminals have no price entry. PowerIO retains source prices in VoltageSource.energy_cost_rate and in generation-2 IR. C and Julia expose energy_cost_rate_per_kwh; Python’s voltage-source records expose energy_cost_rate.

The reader also accepts the deprecated per-phase cost spelling. Explicit 0.1.0 output keeps generator cost and relocates source prices to extras.voltage_source, with a diagnostic. Only a consumer that reads that overlay can use the retained source prices. PowerIO stores the coefficients; the selected solver determines whether and how they enter its objective.

Time series and scenarios

Some sources declare more than one value, and PowerIO keeps the structure they declare. An ordered sequence of values is a time series, a set of named alternatives is a scenario set, and the element type says what varies from one entry to the next.

SourceParses to
PyPSA CSV directory, snapshot axis with varying inputsTimeSeries<BalancedNetwork>
PyPSA CSV directory, fixed network with electrical assignments per snapshotTimeSeries<OperatingPoint<BalancedNetwork>>
Egret JSON with system.time_keys in the scalar profileTimeSeries<BalancedNetwork>
GridFM Parquet dataset (the gridfm feature)ScenarioSet<BalancedNetwork>

A time point keeps the source’s exact label and, when the source gives one, an interval duration; PowerIO imposes no calendar meaning on labels. Scenario identifiers are the source’s own strings, and you look scenarios up by identifier rather than by position. An operating point entry refers to its shared base network instead of copying the network tables. Core concepts says what an operating point can and cannot change.

using PowerIO
dataset = parse("gridfm_case14/")          # PioModule{ScenarioSet{BalancedNetwork}}
scenarios = dataset.value
keys(scenarios)                            # the scenario identifiers
one = scenarios["7"]                       # BalancedNetwork

series_module = parse("pypsa_folder/")     # PioModule{TimeSeries{BalancedNetwork}}
series = series_module.value
length(series)                             # the number of declared time points
first_hour = series[1]                     # one based, like every Julia axis

Indexing returns the contained entry or a view rooted in the owning module; nothing reparses and no complete network is copied. If an entry needs diagnostics, history, emission, or serialization of its own, wrap it in a module.

Only the facade reads the Egret time series. powerio::parse returns the series, while the component crate’s powerio_tx::parse reads only the static profile and refuses a document with system.time_keys.

Data outside a source’s profile, such as PyPSA investment periods, stochastic scenarios, and unit commitment coupling, or Egret reserve and contingency data, stays in the retained source, is reported as uninterpreted, and survives a same format emission. Formats and fidelity says where each profile ends.

Calculation instances and solutions

An instance is the complete input for one named calculation, and a solution is the result of one; the solution shares the instance it solves. PowerIO has seven instance types, from DC power flow through multiconductor AC OPF to AC security constrained unit commitment, and eight solution types, because SocwrOpfSolution is an SOCWR relaxation of an AcOpfInstance and is not labeled an AC OPF solution.

DcPfInstance       DcPfSolution
AcPfInstance       AcPfSolution      SocwrOpfSolution
DcOpfInstance      DcOpfSolution
AcOpfInstance      AcOpfSolution
McAcPfInstance     McAcPfSolution
McAcOpfInstance    McAcOpfSolution
AcScucInstance     AcScucSolution

A source parses to an instance or a solution only when it declares that calculation:

SourceParses to
DOE GO Challenge 3 problem fileAcScucInstance
that problem file beside its solution fileAcScucSolution
DeepMind OPFData JSONAcOpfSolution
BMOPF JSONMulticonductorNetwork; construct the instance explicitly

A MATPOWER case or a PowerModels file stays a network, because it has ratings and costs that power flow, DC OPF, and AC OPF can all use and nothing in the file commits you to one calculation. The GO Challenge 3 data format also serves later challenges, but only its Challenge 3 problem file defines a calculation PowerIO has a type for; optional fields outside that formulation are retained in the source and reported.

GO Challenge 3 splits its problem and solution across two files. Put both in one directory and parse the directory:

solution = powerio.parse("scenario_002")

A directory with only the problem file returns an AcScucInstance. A solution file on its own is refused, because it contains neither the component definitions nor the time axis. The returned module retains both files and its diagnostics.

A DC instance says which branch susceptance formula it uses. In Rust you select it with with_branch_susceptance_formula(formula) and read it with branch_susceptance_formula(); the PowerIO IR document stores it in the approximation field as one of series_susceptance, tap_adjusted_reactance, or reactance_only. Matrices and graphs defines the three.

An instance contains or shares its network. emit writes that network in a grid exchange format and reports the calculation fields the format has no place for, while serialize preserves the complete instance:

using PowerIO
scuc = parse("scenario_002")             # PioModule{AcScucInstance}
emit(scuc, "matpower", "scenario_002.m") # diagnostic: scheduling data omitted
serialize(scuc, "scenario_002.pio.json")

Solvers consume instances; PowerIO never solves. The instance is the mathematical input only. The choice of equations, B-theta or PTDF for DC OPF and polar or SOC for AC OPF, belongs to the solver and does not create another instance type.

A solution lists its values by stable element identifier, along with the termination claim and residuals that PowerIO computes itself rather than taking on trust. An OPFData solution exposes the instance it solves, and residual checks run against that instance’s network. Emitting a solution writes the bus voltages, generator dispatch, and branch terminal flows the target format supports and reports the objectives, multipliers, termination data, and residuals it left out. An SOCWR result is never written as an AC power flow solution; emit writes only its instance network and reports that the W-space values and the objective lower bound were omitted.

Matrices and graphs

powerio-matrix calculates sparse matrices and graph data from parsed networks. Source bus identifiers are not dense indices, so each result comes with the element mapping you need to read its rows and columns, and the dense [0, n) index space exists only inside a result that says which bus each index is.

powerio-prob owns the calculation instances and builds no matrices itself; the matrix crate turns those instances into sparse operators. The files of the DC OPF bundle are defined in DC OPF bundle, and the Rust API is documented in the crate reference.

Capabilities

matrixshapecalculationnotes
MATPOWER Bp (FDPF)\(n \times n\)calc_bprime_matrix-Im(Y_bus) after the makeB Bp edits
MATPOWER Bpp (FDPF)\(n \times n\)calc_bdoubleprime_matrix-Im(Y_bus) after the makeB Bpp edits
\(\Re(Y_{\mathrm{bus}})\), \(-\Im(Y_{\mathrm{bus}})\)\(n \times n\)calc_admittance_matrixfull admittance, keeps taps and shifts
LACPF (linear AC power flow) block\(2n \times 2n\)calc_lacpf_matrix\(\begin{bmatrix}G & -B \\ -B & -G\end{bmatrix}\), flat start, indefinite
PowerModels DC incidence \(A\)\(m \times n\)DcOperators::calc_incidence_matrixrow \(e\) has \(+1\) at the from bus, \(-1\) at the to bus
DC branch susceptances \(b\)\(m\)DcOperators::calc_branch_susceptancesone signed susceptance per in service branch
DC branch flow matrix \(B_f\)\(m \times n\)DcOperators::calc_branch_flow_matrix\(B_f = \operatorname{diag}(b)A\)
DC bus susceptance \(B\)\(n \times n\)DcOperators::calc_bus_susceptance_matrix\(B = A^\mathsf{T}\operatorname{diag}(b)A\)
DC branch phase shift injection\(m\)DcOperators::calc_branch_phase_shift_injection\(b .* shift\)
DC bus phase shift injection \(p_{shift}\)\(n\)DcOperators::calc_bus_phase_shift_injection\(p_{shift} = A^\mathsf{T}(b .* shift)\)
DC bus injection \(p_{bus}\)\(n\)DcOperators::calc_bus_injection_dc\(p_{bus} = -Bv_a + p_{shift}\)
weighted bus factor \(L\)\(n \times n\)calc_weighted_laplacian\(L = C \operatorname{diag}(w) C^\mathsf{T}\); internal solver data
solver branch flow matrix\(m \times n\)calc_solver_branch_flow_matrixpositive solver susceptance magnitudes times \(C^\mathsf{T}\); internal solver data
PTDF\(m \times n\)calc_ptdfroutes through Auto solver selection; calc_ptdf_lodf_with_options exposes the choice
LODF\(m \times m\)calc_lodfroutes through Auto solver selection; option based builds can prune small output entries
AC power flow Jacobian\(2n \times 2n\)calc_power_flow_jacobianpolar or rectangular voltage coordinates
multiconductor admittanceconductor by conductorcalc_multiconductor_admittance_matrixfrom a MulticonductorNetwork; Rust only in 0.11
adjacency\(n \times n\)calc_adjacency_matrixsparse graph adjacency
petgraph graphn/aIndexedNetwork::to_petgraphUnGraph<usize, usize>

PTDF and LODF need a linear solve, and calc_ptdf, calc_lodf, calc_ptdf_lodf, and the option based calc_ptdf_lodf_with_options all go through the same solver selection. You pick the path with SensitivitySolver, the solver field on SensitivityOptions. Dense forces the dense grounded factorization. Sparse factors the grounded DC bus susceptance matrix once with a sparse Cholesky and reuses that factorization for every right hand side. Auto picks dense up to a reduced dimension of 512 (with a memory ceiling) and sparse above it. The sparse path avoids forming the \((n-r) \times (n-r)\) dense inverse, though the PTDF and LODF outputs themselves can still be large. It also needs positive finite internal factor weights w = -b, so the grounded matrix L = -B is positive definite once reference coverage has been checked; the dense path can handle nonsingular indefinite cases. Every connected component must contain at least one reference bus. The DC OPF bundle (\(A\), \(b\), \(L\), costs, bounds, thermal limits, \(C_g\)) is prepared from a DcOpfInstance and documented in DC OPF bundle.

Bp and Bpp are the fast decoupled power flow matrices from MATPOWER makeB. Solvers reduce Bp to PV+PQ buses for active power mismatch to voltage angle updates, and reduce Bpp to PQ buses for reactive power mismatch to voltage magnitude updates. PowerIO exports the full \(n \times n\) matrices so you can apply your own bus type reduction.

The public DC calculations live on DcOperators: calc_incidence_matrix, calc_branch_susceptances, calc_bus_susceptance_matrix, calc_branch_flow_matrix, calc_branch_phase_shift_injection, calc_bus_phase_shift_injection, calc_bus_injection_dc, and calc_branch_flow_dc. A calc_* name means the call computes a new result; a plain noun is a stored field or a borrowed accessor. The incidence matrix follows PowerModels, branches by buses, with \(+1\) at the from bus and \(-1\) at the to bus, and the phase shift injection is a separate result. The sensitivity and DC OPF builders use a transposed incidence factor internally, and that factor stays internal rather than becoming a second public incidence API.

GridFM datasets

Reading and writing GridFM needs the gridfm cargo feature; the CLI and the Python wheel are built with it. The export is a Parquet dataset under <case>/raw/ with bus_data, gen_data, branch_data, and y_bus_data. A single parsed case writes one scenario. A scenario batch stacks the rows of snapshots that share the same element set, with the scenario column as the key; to build one you need unique scenario ids, one system base, and bus, branch, and generator row IDs that match across the snapshots. Column names and units follow the GridFM data kit output schema.

Reading a GridFM dataset recovers everything its balanced tables hold: bus types, voltages and limits; nodal load and shunt totals; generator dispatch, bounds, and quadratic costs; branch parameters and terminal flows; and base_mva. Dense bus indices, nodal demand, and a fixed quadratic cost triple are how the GridFM tables are laid out, so reading them reports no conversion loss.

Writing a richer network into GridFM reports each projection the format forces: bus renumbering, several load or shunt records aggregated into one, equipment and metadata left out, costs that do not fit the fixed quadratic columns, and terminal flows evaluated from bus voltages when the source has no branch solution. A GridFM dataset read and written back without an edit in between produces none of these findings.

Conventions

  • Weighted bus solver factors. Stored nonzero off-diagonal entries are negative; diagonals are nonnegative and positive for buses incident to a positive weight branch. For \(L = C \operatorname{diag}(w) C^\mathsf{T}\) with nonnegative branch weights \(w\), \(L_{ii} = \sum_j \lvert L_{ij} \rvert\). This is the M-matrix form an SDDM (symmetric diagonally dominant M-matrix) or Cholesky solver expects once the grounded matrix is positive definite; a consumer can recover an edge weight as \(-L_{ij} > 0\).

  • Bus indexing. Source bus IDs are preserved on the model as a newtype and need not be contiguous. IndexedNetwork::bus_index(id) maps them into dense zero based indices in \([0,n)\), returning None for an unknown source ID. The matrix builders turn that into Error::UnknownBus at the point they need a bus that is not in the network.

  • Taps and shifts. \(\mathrm{tap} = 0\) means \(\mathrm{tap} = 1\) (Branch::calc_effective_tap). MATPOWER Bp clears bus shunts and line charging, sets tap magnitudes to one, and keeps phase shifts. MATPOWER Bpp keeps bus shunts, line charging, and tap magnitudes while clearing phase shifts. \(Y_{\mathrm{bus}}\) keeps both tap magnitudes and phase shifts.

  • Branch shunt admittance is stored per unit. Branch::charging is the stored per terminal admittance when present: g_fr, b_fr, g_to, and b_to are already per unit on the system base. Branch::b is the legacy MATPOWER BR_B total projection for formats that carry only one charging value. Matrix builders use Branch::calc_terminal_charging(), so terminal values feed \(Y_{\mathrm{bus}}\) even when the legacy total is zero or stale.

  • FDPF scheme. Scheme selects between the two MATPOWER fast decoupled forms. Xb clears resistance for Bp; Bx clears resistance for Bpp. The default is Bx.

  • Zero impedance branches. BuildOptions::skip_zero_impedance controls the builders whose branch denominator can be zero. The default false returns Error::ZeroImpedance; true skips the branch and records the skipped source branch rows in MatrixStats as skipped_zero_impedance and skipped_zero_impedance_branches. Full AC admittance builders use \(r^2 + x^2\); DC incidence and reactance only FDPF forms use \(x\). The gridfm export still zeros its admittance and flow columns for these rows and records dropped_zero_impedance in gridfm_meta.json.

  • Reference coverage. IndexedNetwork::check_reference_coverage verifies that every in service island has a reference bus.

  • Branch susceptance formulas. BranchSusceptanceFormula selects the branch susceptance vector \(b\) and, for formulas that include phase shifts, the phase shift injection. In the PowerModels form, DcOperators::calc_incidence_matrix returns the \(m \times n\) branch by bus matrix \(A_{pm}\). Each branch row has \(+1\) at the from bus and \(-1\) at the to bus. The signed matrix combines with \(b\) to form the direct PowerModels operators \(B = A_{pm}^\mathsf{T} \operatorname{diag}(b) A_{pm}\) and \(B_f = \operatorname{diag}(b) A_{pm}\). The phase shift injection is \(p_{shift} = A_{pm}^\mathsf{T}(b \circ shift)\), so \(p_{bus}=-B\theta+p_{shift}\). \(b\) is negative for an inductive branch, the PowerModels series susceptance sign.

    Solver preparation retains the \(n \times m\) bus by branch factor \(A_s=A_{pm}^\mathsf{T}\). It uses the positive factor weight \(w=-b\), so its sparse matrix is \(L=A_s \operatorname{diag}(w) A_s^\mathsf{T}=-B\). The two orientations have separate names so that a transposition cannot slip across a language boundary unnoticed.

    The default SeriesSusceptance uses \(b = -x/(r^2 + x^2)\), which takes the whole series impedance into account, plus the phase shift injection vector p_shift. A tap does not scale it. It reduces to \(b = -1/x\) when the branch has no resistance.

    TapAdjustedReactance reproduces MATPOWER’s makeBdc: \(b = -1/(x\tau)\) for a transformer with tap ratio \(\tau\), plus p_shift.

    ReactanceOnly is the textbook \(b = -1/x\) with resistance, taps, and shifts ignored. The resulting \(-B\) matches MATPOWER Bp under Scheme::Xb when phase shifts are zero.

Output

Matrices are written as Matrix Market files or kept in memory. A symmetric matrix is stored as its lower triangle with the symmetric header and 1-based indices (io::mtx::emit_mtx). The sensitivities command writes <case>_ptdf.mtx, <case>_lodf.mtx, and <case>_sensitivity_meta.json. Pick the branch susceptance formula with --formula series-susceptance|tap-adjusted-reactance|reactance-only, the PTDF/LODF solve path with --solver dense|sparse|auto, and drop entries whose absolute value is at or below a threshold with --drop-tolerance <value>. On the sparse path the CLI writes the retained Matrix Market coordinates through temp files rather than holding the full sparse output in memory; the Rust calc_ptdf_lodf_with_options API still returns CsMat values and is meant for outputs that fit in memory. The metadata file lists the requested solver, the solver path used, matrix dimensions, nonzero counts, the tolerance, and how many entries were dropped. The dcopf subcommand writes its matrix family together with a JSON manifest.

The standard case solver property fixture lives at powerio-matrix/tests/fixtures/solver_matrix_stats.json. It holds bprime, bdoubleprime, and ybus_imag stats for case9, case14, case30, case57, and case118: n, nnz, min diagonal, M-matrix sign pattern, diagonal dominance margin, zero impedance skips, row sum checks, SPD checks, and a condition estimate when the solver input is SPD.

IndexedNetwork::to_petgraph returns the network as an undirected petgraph graph, one node per bus and one edge per in service branch. The connectivity report and the radial check are built on it, and you can hand the returned graph straight to other petgraph algorithms.

Formats and fidelity

The format table in the repository README lists each format with its token and its read and write support. This page covers the numeric conventions, the independent checks each reader and writer passes, and, format by format, what a reader keeps and what a writer reports.

Conventions

PowerIO’s numeric conventions follow MATPOWER and PowerModels.jl. For each quantity the table gives the reference implementation and the matching PowerIO code:

QuantityConventionReferencepowerio
Bus type codes\(1 = \mathrm{PQ}\), \(2 = \mathrm{PV}\), \(3 = \mathrm{ref}\), \(4 = \mathrm{isolated}\)MATPOWER idx_busnetwork::BusType
Impedance, susceptanceper unit on baseMVA, never rescaledMATPOWER idx_brch (BR_B already per unit)matpower
Branch terminal admittanceMATPOWER BR_B splits half to each end; richer sources use canonical g_fr/b_fr/g_to/b_to; one-value targets receive the total susceptance projectionPowerModels matpower.jl; MATPOWER idx_brchnetwork::BranchCharging, Branch::calc_terminal_charging
Tap ratio0 means a line (treated as 1); nonzero is a transformerMATPOWER idx_brch TAPBranch::calc_effective_tap
Phase shift, angledegrees in the model; PowerModels JSON carries radiansPowerModels make_per_unit!powermodels-json
Angle limitsangmin/angmax default ±360 (unconstrained)MATPOWER idx_brch ANGMIN/ANGMAXBranch::has_angle_limits
pandapower/PyPSA impedanceline r/x are converted between per unit and ohms with \(Z_{\mathrm{base}} = V_{\mathrm{kV}}^2 / \mathrm{baseMVA}\); pandapower line charging is capacitance per km (c_nf_per_km, converted via \(2\pi f \ell Z_{\mathrm{base}}\)); PyPSA line b is siemenspandapower PPC conversion, PyPSA static componentspandapower-json, pypsa-csv
dcline Pt/Qf/Qtsign flips vs MATPOWERPowerModels matpower.jlpowermodels-json
Generator cost\(c_2 p^2 + c_1 p\) maps to \(q = 2c_2\), \(c = c_1\); coefficients high order firstMATPOWER idx_cost, egret matpower_parserGenCost::calc_quadratic
source_id["bus", id] for bus-tied elementsPowerModels matpower.jlpowermodels-json
PSLF shuntsEPC pu_mw/pu_mvar are per unit on sbase; Shunt stores MW/MVAr at \(V = 1\)paired EPC/RAW case checkspslf
DOE GO Challenge 3an input/problem data file parses to AcScucInstance; one Source containing that file and its matching output/solution data file parses to AcScucSolution; instance.network() returns the shared BalancedNetworkpinned GO-3 data model, C3DataUtilities, and GOC3Benchmark.jl D1/D2/D3 filespowerio::parse, powerio::emit
Surge anglesSurge JSON carries voltage angles, phase shifts, and angle limits in radians; BalancedNetwork stores degreesRust Surge round trip testssurge-json
DeepMind OPFData JSONDeepMind OPFData carries p.u. powers and radian angles; BalancedNetwork stores the solved snapshot in MW/MVAr and degrees, with zero based links mapped to one based bus IDsPaper Appendix A, the PyG loader, the smallest complete official fixture, and size independent FullTop and N-1 property testsopfdata-json
UCTE-DEF units and signsohm, microsiemens, kV, MW, MVAr, and ampere on the node voltage level; generation and its limits are negative for an injection and the reader negates them; a current limit becomes rate_a as \(\sqrt{3}, U I / 1000\) MVA; no system base, so the balanced view uses 100 MVA at 50 HzPowSybl Core UcteImporter and UcteNode.fixucte
IEEE CDF shunts and tapsbus G/B are per unit on the title card MVA base and Shunt stores MW/MVAr at \(V = 1\); the tap bus is the from bus and the final turns ratio is the MATPOWER TAP; the phase shifter angle keeps its signMATPOWER cdf2mpc, PowSybl IeeeCdfBusReader and IeeeCdfBranchReader, the vendored 14 and 30 bus cases against case14.m and case30.mieee-cdf

egret’s own MATPOWER parser uses the same reductions (bus type as matpower_bustype, polynomial coefficients reversed to a {degree: coefficient} map, piecewise to [[mw, cost], ...], impedances left per unit), which is why a MATPOWER case taken through powerio to egret JSON matches egret’s direct import.

Validation

The harness script evals/validation/run_validation.sh checks powerio against five independent tools, and each classic text reader and writer runs under an oracle. The conversion matrix covers MATPOWER, PSS/E, and egret sources against all five legacy text targets, each PowerWorld output is read back and bridged to PowerModels JSON, and the PMread leg covers the PowerModels JSON read side. pandapower JSON and PyPSA CSV folders have dedicated import validators, because pandapower has its own JSON schema and PyPSA is a directory format; both validate the write direction only, since the pandapower JSON and PyPSA readers have no external oracle. DOE GO Challenge 3 has a separate pinned reference job, described below. Surge JSON and the remaining source and target pairs (PowerModels JSON and PowerWorld sources into the targets other than PowerModels) rest on the Rust round trip suite.

  • PowerModels.jl (validate_powermodels.jl, validate_psse.jl, core_json.jl) reads MATPOWER, PowerModels JSON, and PSS/E. The MATPOWER to PowerModels JSON path is checked field by field after per unit normalization, and the others by element counts and demand, generation, and shunt totals.
  • egret (validate_egret.py) is the oracle for egret output, which PowerModels cannot read. It loads powerio’s egret JSON with egret.data.model_data.ModelData and compares counts, totals, and generator cost curves.
  • ExaPowerIO.jl (validate_exapowerio.jl) reads MATPOWER through powerio’s C ABI and compares value for value.
  • pandapower (validate_pandapower.py, validate_pandapower_converter.py) cross checks the MATPOWER parse and \(Y_{\mathrm{bus}}\), then imports powerio’s pandapower JSON output back into pandapower and compares counts and \(Y_{\mathrm{bus}}\).
  • PyPSA (validate_pypsa.py) imports powerio’s PyPSA CSV folder output and checks counts, totals, line r/x/b rebased from ohms on the bus0 voltage, and transformer r/x/tap_ratio/s_nom rebased from the transformer s_nom base. A mismatch in the line and transformer split fails the case.

The conversion matrix

evals/validation/validate_matrix.py converts each source to each legacy text target and checks the electrical core of the output (bus, branch, and generator counts and the per unit demand, generation, and shunt totals) against the source’s own core as an independent oracle reads it. The diagonal is checked byte exact, meaning that writing a case back to its own format reproduces the file. Sources are the real native files where they exist (the vendored PSS/E .raw and egret .json) and representative MATPOWER cases otherwise: basic (case9), shunts and transformers (case14, case30), size (case118, case2869pegase), HVDC with a mixed piecewise and polynomial gencost (t_case9_dcline), and a piecewise cost case (pglib_opf_case5_pjm).

All 65 legacy text cells pass (13 source cases × 5 targets). Each writer preserves the core regardless of fidelity tier, which is why the core is the invariant checked across the whole matrix; cost, HVDC, and angle limits are tier specific and are covered by the dedicated checks above and the Rust suite. The pandapower JSON and PyPSA CSV validators run alongside this matrix and are reported as separate legs.

Running it

cargo build --release -p powerio-capi
python3.11 -m venv .venv
.venv/bin/python -m pip install --upgrade pip maturin -r evals/validation/requirements.txt
env VIRTUAL_ENV=$PWD/.venv .venv/bin/maturin develop --release
julia --project=evals/validation -e 'using Pkg; Pkg.instantiate()'
bash evals/validation/run_validation.sh

The oracle tools (PowerModels.jl, egret, ExaPowerIO.jl, pandapower, PyPSA) are declared only in evals/validation/Project.toml and evals/validation/requirements.txt, and no PowerIO release artifact depends on them. evals/validation/run_validation.sh expects the Python oracles to import in the selected Python 3.11+ environment, so a missing PyPSA, pandapower, or egret import is a setup failure.

Format notes

Each loss produces a coded diagnostic. A reader itemizes what it keeps only in the retained source, with the table and the count of affected rows; a writer reports what the target cannot represent, and emit returns those findings with the result. A code spells out the format and the reason, as in READ.CGMES.RECORD_UNMAPPED, READ.CGMES.FIELD_UNMAPPED, READ.XIIDM.FIELD_UNMAPPED, or EMIT.PSSE.FIELD_DROPPED for RAW and RAWX, so there is no generic parse warning that hides the cause.

XIIDM and JIIDM

PowerIO reads PowSybl IIDM 1.0 through 1.17 in the XML (xiidm) and JSON (jiidm) encodings and writes 1.17 in either; the IIDM versions table lists what each version changed. One element mapping serves both encodings, so a JIIDM document reads to the same network as the XIIDM document of the same network, and fresh JIIDM follows PowSybl’s JSON layout: plural array fields for repeated elements, typed scalars, and each element’s attributes in the order PowSybl’s sequential JSON reader consumes them.

The mapping covers substations, voltage levels, bus breaker and node breaker topology, busbar sections, switches, lines, tie and boundary lines, loads, generators, batteries, shunts, static VAR compensators, two and three winding transformers, tap changers and controls, operational limits, reactive limits, HVDC converters, aliases, properties, and PowSybl active power control. Areas, one level of nested XIIDM networks, nonlinear shunt section models, physical DC equipment, and VSC/LCC converters also parse and survive fresh emission. An unknown extension subtree stays available for byte exact same format emission and produces a diagnostic, but it does not become an unnamed field on the network. Fresh emission preserves detailed connectivity when the source has it and allocates any missing local node numbers without changing stable PowerIO identities. A three winding transformer whose ratedU0 differs from ratedU1 keeps that leg impedance base for fresh emission.

XIIDM gives electrical quantities in physical units and has no system MVA base, so the balanced calculation view uses 100 MVA as its internal normalization and does not report a missing source value. Fresh XIIDM or JIIDM emission reports a network base other than 100 MVA, because the target has nowhere to put that normalization; impedance and admittance conversion still uses the network’s actual base, so the physical electrical values come out unchanged.

CGMES

PowerIO reads CGMES 2.4.15 on CIM16 and CGMES 3.0 on CIM100. CGMES 2.4.15 uses the namespace http://iec.ch/TC57/2013/CIM-schema-cim16# with the ENTSO-E extension namespace http://entsoe.eu/CIM/SchemaExtension/3/1# (IEC TS 61970-600-1/-2:2017); CGMES 3.0 uses http://iec.ch/TC57/CIM100# and http://iec.ch/TC57/CIM100-European# (IEC 61970-600-1/-2:2021). Both use the IEC 61970-552 CIMXML instance syntax, in which rdf:ID defines a record, rdf:about extends one, and md:FullModel heads each profile document. EQ is required, and SSH, SV, and boundary profile data are used when present. SSH assignments take precedence over SV observations, so an SV shunt section count that differs from the SSH assignment is reported and not kept. A set with TopologicalNode data reads one bus per TopologicalNode, and that data wins even for node breaker equipment, so source TopologicalNode identities survive.

A set without TopologicalNode data still reads when its declared profile URIs describe node breaker equipment, using the same test PowSybl makes in CgmesModelTripleStore.computeIsNodeBreaker: each CGMES 2.4.15 document that declares EquipmentCore also declares EquipmentOperation (and each EquipmentBoundary document declares EquipmentBoundaryOperation), or the EQ is CGMES 3.0 CoreEquipment with ConnectivityNode records. The buses are then the connected components of the ConnectivityNode graph joined by switches that are closed and in service, which is the graph PowSybl’s NodeMapping and SwitchConversion hand to IIDM.

A switch is open when SSH Switch.open says so, otherwise when EQ Switch.normalOpen says so, and otherwise closed. SV SvStatus.inService, or failing that SSH Equipment.inService, decides service status, which CGMES defines as availability for topology processing (PowSybl 7.3 reads only the switch position, and since the official sets contain no closed switch out of service, both rules agree there). A terminal is connected unless SSH ACDCTerminal.connected is false; a disconnected terminal leaves its equipment on the bus of its ConnectivityNode with the equipment out of service, which is where PowSybl inserts a fictitious open switch. Each bus takes the nominal voltage of its nodes’ VoltageLevel (a Bay resolves to its VoltageLevel), a node in a Line container takes the base voltage of attached conducting equipment or transformer ends, and a node no terminal references gets no bus. A bus is named after a BusbarSection on it, or else after its first ConnectivityNode. Its mRID is the UUIDv5, under PowerIO’s CGMES namespace, of the sorted ConnectivityNode mRIDs it joins, so the same nodes always yield the same mRID and no source TopologicalNode mRID is invented; the bus_breaker_buses table stays empty and calculated_buses lists the nodes of each bus. READ.CGMES.TOPOLOGY_CALCULATED, a remark, gives the bus, node, and switch counts and the identity rule; READ.CGMES.CONNECTIVITY_INSUFFICIENT, an error, says which data is missing when a set has neither TopologicalNode records nor calculable connectivity, such as a bus branch EQ without TP.

The calculated topology has limits. SvVoltage observations reference TopologicalNodes, so bus voltages keep their defaults and READ.CGMES.RECORD_UNMAPPED counts the observations. No TopologicalIsland supplies an angle reference, so the reference bus comes from referencePriority, an external injection, or the largest machine. A closed switch between two voltage levels joins its nodes into one bus placed in the first node’s level, as PowSybl merges those levels. A 2.4.15 junction terminal in EQ_BD has no ConnectivityNode and is read as disconnected when there is no TP_BD. Finally, PowSybl’s bus view omits a component with no busbar section and fewer than two feeders while PowerIO keeps each component as a bus, so bus counts differ by those components. Each document’s Model.modelingAuthoritySet is read for the boundary and state variable authority checks, reported, and not kept; fresh output writes PowerIO’s own modeling authority.

A source can be an XML profile directory, a directory of profile ZIP files, or one ZIP containing the profiles. The mapping covers hierarchy, AC and DC equipment, detailed connectivity, current, active power, and apparent power operating limits, tap changers and controls, reactive limits, and the operating and solution values in SSH and SV. Diagram, geography, dynamics, and unrecognized CIM classes are counted in diagnostics. A field on a recognized class that the mapping does not consume gets a grouped READ.CGMES.FIELD_UNMAPPED diagnostic with the class, field, count, and sample identities.

Fresh output is a deterministic CGMES 3.0 EQ, TP, SSH, and SV profile set. Imported UUID mRIDs survive for mapped equipment, terminals, tap changers, operational limit sets, hierarchy, and topology records, and a missing mRID becomes a UUIDv5 derived from the component type and stable identity. Tap changer tables, tap controls and table points, reactive curve points, and individual limit values keep their electrical values and relationships but get deterministic subordinate mRIDs on fresh emission. Operational limit helper objects that PowerIO generates do not become source metadata on readback. Limit type objects keep the PATL or TATL name common CGMES importers require, while generated limit set objects omit an unrepresented display name. A third party subordinate identity or field without a typed record still gets a diagnostic. The source neutral limit model keeps permanent and temporary limits, so fresh output uses PATL and TATL; parsing reports a PATLT or TCT substitution and any fractional duration rounded to whole seconds.

CGMES input keeps each source Substation, including distinct substations joined by a transformer. XIIDM and JIIDM emission joins output container groups only when the IIDM rule that a transformer belongs to one substation requires it, and it reports that hierarchy change; the transformer stays a transformer with the same electrical data. Distinct BaseVoltage identities at the same kV also get a precise collapse diagnostic before fresh output uses one record keyed by voltage. Busbar VoltageLimit records are combined with the enclosing voltage level into the most restrictive valid low and high voltage range; an inconsistent pair is diagnosed and ignored, and fresh emission writes the resulting VoltageLevel fields instead of recreating the individual VoltageLimit records. CGMES uses physical units and has no system MVA base, so the balanced calculation view uses 100 MVA as its internal normalization and does not report a missing source value.

PSS/E

PowerIO reads RAW revisions 32 through 35 and RAWX revision 35, and RAW and RAWX share one electrical mapping. RAW 32 records end before the bus voltage limits (NVHI, NVLO, EVHI, EVLO), the load INTRPT field, the transformer VECGRP field, and the winding CNXA field that revision 33 added, so the reader lays each record out by the header revision, defaults those fields, and reports a revision 32 record that ends before its last typed field as READ.PSSE.VALUE_DEFAULTED with the record’s byte range. RAW 34 maps its substation section. RAW 35 and RAWX 35 map and freshly emit substations, nodes, switches, busbar sections, and equipment terminal references. Fresh RAW 34/35 and RAWX output preserves AC line and transformer names, and RAWX terminal rows use the exact type, buses, and local identifier chosen for their electrical equipment row. When a source neutral connectivity node has no PSS/E number, fresh RAWX allocates a positive number within its substation before resolving exact regulation targets, and reports the default.

An explicit RAW revision outside 32 through 35, an invalid system base or frequency, or a nonfinite record value is rejected. Fresh output accepts only revisions 33 through 35 and returns an error when detailed connectivity cannot form valid RAWX tables. A RAW 32 module keeps its source text like any other revision, but no emission target names revision 32, so writing it back as PSS/E produces fresh revision 33 text and its unmodeled sections survive only in the retained source.

Generator IREG/NREG, switched shunt SWREG/NREG, and transformer CONT/NODE resolve to exact terminal references, including an explicit target on the same bus. Each winding of a three winding transformer keeps its control mode, regulated terminal, limits, tap position and range, and number of tap positions. A positive COD enables automatic adjustment, a negative COD keeps the same mode with automatic adjustment disabled, and zero is fixed; |COD| = 4 controls a DC line quantity on a two winding transformer, and |COD| = 5 controls asymmetric active power flow. Unsupported RAWX tables, including multiterminal DC, FACTS, GNE, induction machine, multisection line, zone, owner, and interarea transfer records, remain only in byte exact same format emission and produce counted diagnostics. Unknown RAWX tables and caseid fields get the same retained source diagnostic, and fresh output diagnoses detailed records and fields that RAWX cannot carry. Three winding transformers are kept as typed records, and the indexed view lowers each one as a star into \(Y_{\mathrm{bus}}\)/connectivity; two terminal DC lines map to the neutral HVDC model. A switched shunt keeps its steady state susceptance BINIT as the shunt b along with its mode, voltage band, regulated bus, and step blocks, and a two winding transformer’s magnetizing susceptance survives a round trip through MAG2. The reader converts CW 1/2/3, CZ 1/2/3, and CM 1/2 into the neutral tap ratio, system base impedance, and magnetizing admittance, and fresh output uses the electrically equivalent canonical CW = CZ = CM = 1 representation.

UCTE-DEF

PowerIO reads UCTE-DEF revisions 2003.09.01 and 2007.05.01 and writes 2007.05.01 under the token ucte (alias uct, extension .uct). The column layout is the one PowSybl Core’s UcteRecordParser reads for both revisions; a 2003 file leaves the element name columns blank. The reader maps the ##N nodes to buses named by their 8 character node code, with a bus id equal to the node’s position in the block and the base kV taken from the voltage level digit (750, 380, 220, 150, 120, 110, 70, 27, 330, or 500 kV). Each ##Z country is a ControlArea area named by its ISO code, and the cross border nodes (country letter X) form one CrossBorder area named XX, so a tie line keeps both ends and the cross border node’s own load and generation. Node types PQ, PU, and UT map to PQ, PV, and reference; type QT (Q and angle constant) reads as PQ with a warning. A node’s load and generation become one load and one generator, with the PowSybl consistency rules applied and reported: a missing set point reads as zero, a missing limit as 9999, inverted limits are swapped, a set point outside its limits moves the limit, and a voltage regulating node with no voltage reference reads as PQ.

##L lines are branches on the voltage level base with the total susceptance split evenly. Busbar couplers (status 2 and 7) are switches, an equivalent element (status 1 and 9) keeps that mark in its extras, and a reactance under 0.05 ohm reads as 0.05 ohm, as PowSybl reads it. A coupler whose two node codes are equal is ignored with a diagnostic, following PowSybl Core’s UcteImporter, and a line joining two voltage levels is refused, as PowSybl refuses it. ##T transformers are branches from the regulated winding (node 2) to the non regulated winding (node 1), whose voltage level carries the impedance; the rated voltages set the tap, and the magnetizing admittance sits on the regulated side. ##R phase regulation multiplies the tap by \(1 + n’ \delta u / 100\) and becomes a voltage control when it has a target, while angle regulation applies the PowSybl asymmetrical or symmetrical formulas to the tap and the phase shift and becomes a disabled active flow control. A record with both folds the phase regulation’s ratio into the angle formula, as PowSybl does, instead of multiplying the two results. Both regulations stay in the branch extras so that fresh UCTE output can write them back as they were read. ##TT special descriptions stay in the transformer extras and ##E exchange schedules stay in the retained source only; both are reported with READ.UCTE.RETAINED_SOURCE_ONLY. UCTE uses physical quantities and has no system MVA base, so the balanced calculation view uses 100 MVA as its internal normalization and does not report a missing source value. Each finding points at its record through a source span.

Fresh output writes nodes grouped by country in bus order. A bus keeps its name when it is a UCTE node code; otherwise it receives <country><spot><level><busbar>: the country letter of its area’s ISO name, else the area number’s entry in the UCTE country table in ISO order; the bus id in base 36 as the five character spot; the voltage level digit nearest its base kV (380 kV when the bus has none); and busbar 1, bumped on a collision. Each derived code is reported with EMIT.UCTE.VALUE_SUBSTITUTED. A base kV that is not a UCTE level is written under the nearest level with a warning; ohm, kV, MW, and ampere values stay physical, so reading the file back expresses them per unit on that level. A phase shift becomes a one step symmetrical angle regulation, its step solved against the phase regulation written beside it, and a voltage control with a tap range becomes a phase regulation; a line joining two voltage levels is written as a transformer. UCTE requires a node’s generation bounds to contain its dispatch, so the writer widens an inconsistent interval and reports the substituted bounds. An out of service generator contributes no dispatch but still supplies the node’s plant type letter, which keeps that source classification stable on readback. Shunts, HVDC, storage, static VAR compensators, three winding transformers, costs, capability columns, angle limits, voltage bands and angles, rate B and C, remote regulation, and a frequency other than 50 Hz are reported as dropped.

PowerWorld

.aux is read and written, .pwb binary cases are read only, and a .pwd display file parses to a GeoLayer (Geographic and display data). .aux has no system base, so the reader defaults to 100 MVA. No third party .aux reader exists, so the writer is validated by PowerIO’s own read back plus a PowerModels JSON bridge. The .pwb layouts are reverse engineered; the decode evidence and coverage matrix are maintainer notes at powerio-tx/src/format/powerworld/FORMAT.md.

PSLF

.epc is read and written. The reader maps the static power flow core: buses, lines, two and three winding transformers, generators, loads, fixed shunts, controlled shunts at their initial g/b, and limited two terminal DC records. Three winding transformers are kept as typed records, and the indexed view lowers each one as a star into \(Y_{\mathrm{bus}}\)/connectivity. Unsupported sections stay in the retained source text and produce diagnostics.

MATPOWER

Canonical MATPOWER output, for a case that did not start as MATPOWER, omits dcline; the byte exact echo path keeps it when the case was read from MATPOWER. Storage is written as an mpc.storage block.

egret

egret output writes HVDC as dc_branch, the element its reader already reads, so the power, voltage, and loss fields survive a round trip; a dcline cost curve and storage are the only things dropped. The reader takes the power flow ModelData subset (numeric bus ids, scalar values). A document whose system.time_keys vary the scalar profile parses to TimeSeries<BalancedNetwork> through powerio::parse, whereas the component crate’s powerio_tx::parse reads only the static profile and refuses it.

pandapower JSON

The pandapower JSON writer lays the power flow core out as split oriented pandapowerNet tables. Line ohms are referred to the from bus voltage, as pandapower’s build_branch reads them, and a bus with baseKV 0 writes vn_kv set to \(1\) (warned) so the per unit impedances survive. A branch with a tap, a shift, or terminals on two voltage levels becomes a trafo row with tap_changer_type = "Ratio"; because pandapower’s magnetizing model is inductive only, its MATPOWER charging b goes out as one bus shunt per terminal (warned, \(Y_{\mathrm{bus}}\) exact). The file is labeled with f_hz set to \(50\) and c_nf_per_km compensated, so a 60 Hz source keeps its exact \(Y_{\mathrm{bus}}\). A reference bus without a generator gets an ext_grid row, which reads back as a Ref generator. The writer also warns on dropped HVDC, storage, capability columns, angle limits, rate B/C, nonfinite values (written as JSON null), and costs poly_cost cannot carry. The reader models ratio, ideal, and pandapower 2.x tap changers, off nominal vn_hv_kv/vn_lv_kv, lv side taps, and shunt vn_kv scaling; ZIP load composition, line shunt conductance, magnetizing branches, tabular tap changers, reactive cost coefficients, and any other table with rows warn with row counts.

PyPSA CSV folders

PyPSA CSV folders are canonicalized directory outputs rather than byte exact text conversions. The mapping covers static buses, generators, loads, lines (ohms on the bus0 voltage, as PyPSA computes them), transformers (rebased between the system base and the transformer s_nom), shunts, storage units, and base MVA. The reader maps links to HVDC with a warning, requires v_nom and balanced CSV quoting, and warns on stores, nonzero g, and each CSV it does not read (time series, carriers). The writer keys tables by bus name, falling back to the numeric id when names collide (warned), and warns on dropped HVDC, q limits, mbase, transformer angle limits, rate B/C, isolated buses, nonfinite p limits, and slackless or normalized networks. Nonnumeric bus names read back as dense synthetic ids with the originals on Bus.name.

DOE GO Challenge 3 JSON

DOE GO Challenge 3 JSON is a grid exchange format “for Challenge 3 and beyond,” so the format name is broader than any one calculation type. PowerIO recognizes its Challenge 3 input/problem data file and returns AcScucInstance. The instance has the declared time points and durations, initial commitment and dispatch, time varying bounds, costs and reserves, energy windows, contingencies, and one shared BalancedNetwork, which instance.network() returns. One directory or memory Source containing both the input/problem data file and its matching output/solution data file returns AcScucSolution; an output/solution data file on its own is rejected, because it has neither the component definitions nor the time axis. Problem data is parse only. A complete AcScucSolution emits the official output/solution data file, including bus voltage, shunt step, device commitment, dispatch and reserves, AC line status, transformer tap, phase shift and status, and DC line terminal power fields. The pinned GO-3 model validates PowerIO’s small problem and output documents and all of the D1/D2/D3 input/problem data files, and C3DataUtilities reports no data, ignored, or solution errors for those documents. The older pinned GO-3 model and D1/D2/D3 files still have network.bus.con_loss_factor, a field that version 1.1.1 of the data format removed; PowerIO keeps the original source and reports one bounded diagnostic instead of treating it as an electrical network or AC SCUC field. Optional bus location labels and incomplete coordinate pairs stay in Bus.extras, and optional consumer descriptions, voltage setpoints, and nameplate capacities stay in Load.extras; each reports READ.GOC3.OPTIONAL_FIELD_UNTYPED. A producer description has no generator metadata field to go to and reports READ.GOC3.RETAINED_SOURCE_ONLY.

Surge JSON

PowerIO reads and writes the versioned surge-json network document. The reader maps buses, loads, fixed shunts, branches, generators, storage, and HVDC links into BalancedNetwork, keeps the original source for same format echo, and warns about source sections that stay only in the retained document. The writer emits a canonical Surge network body for the supported power flow core, and richer MATPOWER generator capability or ramp columns and unsupported cost shapes are reported in the emission diagnostics. An HVDC link has the terminal voltage setpoints, the reactive limits, and the loss model on its converter terminals; a Surge link has no terminal reactive flow, no cost curve, and no received power (the reader derives it from the setpoint and the loss model), so those are warned. A link with converter or control detail beyond the neutral converter this writer emits (firing angles, converter transformer taps, commutation impedance, a DC voltage schedule) is warned on the way in.

DeepMind OPFData JSON

The DeepMind OPFData reader takes one raw JSON document from a FullTop or N-1 release into the balanced transmission model. Topology, limits, loads, shunts, and quadratic costs come from grid; solved bus voltages, generator dispatch, and branch flows come from solution. Powers and ratings are converted from per unit, angles from radians, link indices from zero based to one based, and flow columns from [pt, qt, pf, qf] into the canonical terminal order. Original bus IDs and names, areas and zones, and frequency are absent, and the solver’s initial generator values differ from the solved snapshot, so a conversion to another format reports those facts. The adapter works from the feature widths and the row and link counts in each file rather than from a case name registry or expected element counts, so the same path covers all published grid families (14 through 13,659 buses) and both FullTop and N-1 examples; generator and branch outages appear as absent rows and links and are validated against that example’s solution topology. The published releases are derived from PGLib-OPF cases, but the reader does not use PGLib case names or a case registry, so a document from another source is accepted when it follows the same object layout, feature column order, units, and link rules. The paper’s Appendix A is the published format definition and the PyTorch Geometric loader is the executable reference. No separate JSON Schema or format version marker is published, so a document that departs from that layout is rejected by the reader’s shape and topology checks. An unrecognized object field stays in the retained source and produces a projection warning, so same format echo still works. The raw source echoes byte exactly; there is no canonical writer, .pt cache reader, archive reader, downloader, or batch directory API.

IEEE Common Data Format

IEEE Common Data Format (ieee-cdf, alias cdf) is read only. The reader takes the title card MVA base and date; the bus records (number, name, area, loss zone, type, solved voltage and angle, load, generation, base kV, desired voltage, MVAr or voltage limits, shunt G and B, remote controlled bus); the branch records (tap and Z bus, circuit, type, R, X, B, the three MVA ratings, control bus and side, turns ratio, phase shift angle, tap limits, step size, and the controlled quantity limits); and the interchange records (area number, slack bus, export, tolerance, code, and name). The tap bus is the from bus and a nonzero turns ratio marks a transformer. A blank branch type reads as a transmission line, as the PowSybl reader reads it; a type 1 through 4 branch without a ratio reads as unity; and types 2, 3, and 4 get a regulating transformer control block whose ntp derives from the step size. A type 1 bus reads as PQ with a fixed reactive generator and the limit columns as its voltage band, and every type 2 or 3 bus, along with any bus with nonzero generation, gets a generator. The format has no active power limits, no machine base, and no voltage limits, so pmin 0 MW, pmax 9999 MW, mbase equal to the system base, and vmax/vmin 1.1/0.9 p.u. are assumed and reported as READ.IEEE_CDF.VALUE_DEFAULTED. Loss zone names, tie lines, the branch area and loss zone columns, alternate swing bus names, and the title originator, year, and season survive in the retained source only (READ.IEEE_CDF.RETAINED_SOURCE_ONLY).

A record cut before a mandatory field reads that field as zero and reports READ.IEEE_CDF.RECORD_TRUNCATED with the record’s span. A header item count, terminator, misplaced record, zero impedance branch, or undeclared bus reference is READ.IEEE_CDF.SOURCE_MALFORMED, and a type or side code outside the documented set is READ.IEEE_CDF.VALUE_SUBSTITUTED. A title card without a positive MVA base, or a record whose bus numbers or numeric fields cannot be decoded, ends the read with a spanned PARSE.IEEE_CDF.MALFORMED. The column ranges follow PowSybl Core’s IeeeCdfBusReader and IeeeCdfBranchReader, which read the public archive files; those files place the last two branch limits one column to the left of the 1973 table, and the reader accepts both layouts. A .txt or .cdf file whose first card is a CDF title card is detected without a declared format. Fresh output of this format is not required, so there is no writer; emit to ieee-cdf is refused as read only, and a case converts to any writable format instead. The PowSybl gate reads every public IEEE case with PyPowSybl’s own CDF importer, compares its bus, branch, generator, load, and shunt counts and its load and generation totals with the PowerIO parse, then reloads fresh MATPOWER written from each case.

GridFM Parquet datasets

GridFM Parquet datasets (behind the gridfm feature, following the GridFM data kit output schema) parse to a scenario set of balanced networks over one shared element identity map. Each scenario recovers the complete native balanced table data: bus types, voltages, and limits; nodal load and shunt totals; generator dispatch, bounds, and the cp0/cp1/cp2 polynomial as given; branch r/x/b/tap/shift/rate_a/angle limits and pf/qf/pt/qt terminal flows; and baseMVA. Dense bus indices, nodal demand records, and line classification for a unit tap with zero shift are GridFM source facts and do not produce reader diagnostics.

Writing a richer network reports the projections into GridFM’s fixed tables: source bus renumbering, several loads or shunts combined at one bus, metadata and equipment with no column, and costs outside the fixed quadratic representation. If a branch has no solution in the source, the writer evaluates pf/qf/pt/qt from the stored bus voltages and reports that derived value. Generated component identities give PowerIO stable references and are not treated as source metadata. A native GridFM network writes without findings. Matrices and graphs describes the dataset the writer produces. Both directions need the gridfm cargo feature, which the CLI and the Python wheel include.

IIDM versions

PowerIO reads every IIDM serialization version PowSybl has published and writes 1.17. A document of an older version is read with that version’s rules and reported with READ.XIIDM.VERSION_COMPATIBILITY, and a namespace naming a version outside this table is refused with PARSE.XIIDM.VERSION_UNSUPPORTED. The table lists, per version, what the reader does differently from 1.17, and each row is checked against the PowSybl fixture of the same network in tests/data/xiidm/powsybl.

VersionReadWhat the version states differently
1.0XIIDMiTesla namespace. Busbar sections carry the calculated bus v and angle. Three winding transformers have no ratedU0 (leg 1 rated voltage is the impedance base), no leg 1 tap changer, and no phase tap changers. Tie lines state both half lines inline with _1/_2 suffixes and ucteXnodeCode. Shunts state bPerSection, maximumSectionCount, and currentSectionCount and never regulate. Static VAR compensators spell voltageSetPoint and reactivePowerSetPoint. Batteries spell p0 and q0. Ratio tap changers state targetV. Loading limits sit directly on the equipment as the selected DEFAULT group. A switch closing a bus or node onto itself is discarded, as PowSybl does. No minimumValidationLevel.
1.1XIIDMPowSybl namespace. Calculated buses under node breaker topology. Three winding transformer ratedU0, leg 1 tap changers, and phase tap changers.
1.2XIIDMfictitious on every identifiable, targetDeadband on tap changers, ratedS, and shunt voltage regulation.
1.3XIIDMShunt linear and nonlinear models with sectionCount, aliases, and boundary line generation.
1.4XIIDMAlias types.
1.5XIIDMActive and apparent power limits.
1.6XIIDMVoltage levels outside substations and VSC regulating terminals.
1.7XIIDMminimumValidationLevel and the equipment validation namespace.
1.8XIIDMBatteries spell targetP and targetQ. Fictitious bus injections (reported, not retained). Self connected switches are refused.
1.9XIIDMShunt p.
1.10XIIDMTie lines reference two dangling lines. Load models.
1.11XIIDM, JIIDMpairingKey replaces ucteXnodeCode. Subnetworks. Voltage angle limits (reported, not retained).
1.12XIIDM, JIIDMOperational limits groups and ratio tap changer regulationMode/regulationValue.
1.13XIIDM, JIIDMAreas, isCondenser, and active power control 1.2.
1.14XIIDM, JIIDMSolved tap positions and section counts, regulating on static VAR compensators and phase tap changers.
1.15XIIDM, JIIDMDC nodes, grounds, lines, switches, and AC/DC converters.
1.16XIIDM, JIIDMshuntCompensator and boundaryLine element names and multiple selected limit groups.
1.17XIIDM, JIIDMOptional zero conductance and susceptance, retained and lowTapPosition defaults, DC switch resistance. Writers use this version.

JIIDM has no namespace, so the document’s version field selects the same rules and minimumValidationLevel selects the validation level. PowSybl ships JIIDM fixtures from 1.11 on; an older version value reads with that version’s XML rules.

Missing generator costs

PSS/E .raw files have no generator cost curves. Converting a PSS/E case to MATPOWER writes mpc.gen and omits mpc.gencost with a warning, because powerio does not invent zero costs. If your workflow needs costs, pick a policy explicitly:

powerio convert case.raw --from psse --to matpower --missing-gen-cost zero -o case.m
powerio dcopf case.m -o out --missing-gen-cost quadratic --default-gen-cost 0.01,2.0,0.0
powerio gridfm case.raw --from psse -o out --missing-gen-cost zero
  • preserve: leave missing costs absent (default for conversion and GridFM export);
  • require: fail on an in service generator without cost (default for DC OPF export);
  • zero: fill missing rows with a MATPOWER polynomial cost [0, 0, 0];
  • quadratic: fill missing rows with --default-gen-cost C2,C1,C0.

--gen-cost-csv overrides costs by generator row before the missing cost policy runs. The header is gen_index,bus,c2,c1,c0,startup,shutdown: gen_index is zero based in the current generator table, bus must match that generator’s bus id (which catches a stale table after reordering), and startup/shutdown default to zero. GridFM stores cp0/cp1/cp2 columns; missing or unsupported costs still write zero columns, and the manifest separates missing_cost_gens, unsupported_cost_gens, zeroed_cost_gens, and synthesized_gen_costs.

Geographic and display data

PowerIO keeps coordinates when a supported source provides them. They are optional. No parser invents them, and when you emit a located case to a network format that has no place for coordinates, the writer reports the loss.

A standalone geographic document parses to powerio.GeoLayer, a value like any other case. The canonical .geo.json, plain GeoJSON, CSV or JSON records with aliased field names, headerless buscoords CSV, and a PowerWorld .pwd display all parse to it; a .pwd becomes a diagram space layer whose features target substations.

use powerio::{PioValue, emit, parse, serialize};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let module = parse("layer.geo.json")?;
    let PioValue::GeoLayer(layer) = module.value() else {
        panic!("a layer document parses to powerio.GeoLayer");
    };

    // A layer travels through PowerIO IR and out as the canonical document.
    serialize(&module, "layer.pio.json")?;
    emit(&module, "geo-json", "layer.geo.json")?;
    Ok(())
}

If you need the raw display record, PwdDisplay is still available; it has the canvas, the save stamp, and the symbol table in diagram coordinates.

Coordinate fields

Balanced and multiconductor networks use the same JSON shape for coordinates:

#![allow(unused)]
fn main() {
pub struct Location {
    /// Longitude for geographic coordinates.
    pub x: f64,
    /// Latitude for geographic coordinates.
    pub y: f64,
    /// Point origin when it differs from the network default.
    pub kind: Option<CoordsKind>,
}

pub enum CoordsKind { Source, Synthetic, Manual, Derived }

pub struct GeoMeta {
    pub space: CoordinateSpace,
    pub kind: Option<CoordsKind>,
}

pub struct Canvas {
    pub width: Option<f64>,
    pub height: Option<f64>,
    pub units: Option<String>,
}

pub enum CoordinateSpace {
    Geographic { crs: Option<String> },
    Projected { crs: Option<String> },
    Diagram { canvas: Option<Canvas> },
    Unknown,
}
}

Balanced networks reach these types as powerio::geo::{Location, CoordsKind, CoordinateSpace, GeoMeta, Canvas} through BalancedNetwork.geo and Bus.location; multiconductor networks use the matching powerio_dist::geo types through MulticonductorNetwork.geo and DistBus.location, and a package serialization test keeps the two JSON shapes identical. A branch can also have a polyline route (Branch.route, DistLine.route) when the source provides intermediate geometry. Without one, a renderer draws the branch endpoint to endpoint from the bus locations.

The coordinate space belongs to the network, and a point sets its own kind only when its origin differs from the network default. For geographic coordinates, x is longitude and y is latitude, which is GeoJSON axis order, and a missing CRS means EPSG:4326. kind says whether the coordinates came from the source, a generated layout, a manual edit, or a derived transform.

Harvest and emit

A parser promotes source coordinates into location, sets the space, and removes the raw keys from extras. Writers read location.

FormatFieldsSpace
PowerWorld auxLatitude:1/Longitude:1 bus columns, else the bare Latitude/Longitude pair (SubNum stays in extras: it is identity rather than geometry)geographic
pandapowerbus geo GeoJSON Point stringsgeographic
PyPSAbuses.csv x/ygeographic
DOE GO Challenge 3bus longitude/latitudegeographic
OpenDSSBuscoordsunknown; a diagnostic identifies values within longitude and latitude bounds
BMOPF JSONlongitude/latitude (the BMOPFTools sideload convention; emission is opt in via BmopfEmitOptions::sideload_coordinates)geographic

MATPOWER, PSS/E, PowerModels, egret, PSLF, and Surge have no place for geometry. Emitting a located case to one of them reports the dropped locations, in the same way it reports a dropped base_frequency. If you need the coordinates to survive, powerio geo extract writes them as a separate layer document.

The geographic document

Coordinates also arrive and leave as files of their own, such as a Buscoords CSV next to a DSS master or a GeoJSON export from a GIS tool, and a renderer can hand back the layout it computed the same way. The container for such a file is GeoLayer, which the Rust facade’s parse returns as PioValue::GeoLayer.

The canonical form is a GeoJSON FeatureCollection with one foreign member, powerio_geo, and the suggested extension is .geo.json:

{
  "type": "FeatureCollection",
  "powerio_geo": { "powerio_version": "0.11.0", "space": "geographic", "kind": "source" },
  "features": [
    { "type": "Feature",
      "geometry": { "type": "Point", "coordinates": [-80.05, 34.20] },
      "properties": { "target": "bus", "id": "312", "uid": "buses:11" } },
    { "type": "Feature",
      "geometry": { "type": "LineString", "coordinates": [[-80.05, 34.20], [-80.10, 34.30]] },
      "properties": { "target": "branch", "uid": "branches:4", "from": "312", "to": "410" } }
  ]
}

When the space is geographic this is valid RFC 7946 GeoJSON, so GIS tools open it directly.

Parsing is tolerant and emission is canonical. GeoLayer::parse takes UTF-8 text plus a file name hint and touches no filesystem. It accepts headerless buscoords CSV (bus, x, y), CSV and JSON records with aliased field names (bus_i/bus/id, lat/latitude/y, lon/lng/longitude/x, branch endpoint pairs), and GeoJSON Point and LineString features. A feature refers to its element by up to three key fields, tried in order: uid, then id, then case insensitive name. A branch route can also fall back to the unordered (from, to) bus pair. A bare integer branch id (branch, branchid, branchnumber, catsid) is accepted on the way in as a one based positional row alias and is not written back; the uid in the payload is the durable key. A branch key ignores a bare id property, because GIS exports and RFC 7946 tooling put a feature row counter there.

BalancedNetwork::to_geo_layer() turns a network’s coordinates into a layer, and BalancedNetwork::apply_geo_layer(&layer) applies one and returns a GeoApplyReport with the matched and unmatched feature counts plus unlocated_buses and unlocated_branches, the elements still without geometry when the pass ends. Together those counts let you tell a layer that matched nothing from a model that needed nothing, and report.require_located() is the one line check for a caller that wants everything placed. The multiconductor equivalents, to_dist_geo_layer and apply_dist_geo_layer, live in powerio::dist_geo. The CLI wraps the same functions:

$ powerio geo extract case.aux -o case.geo.json
$ powerio geo apply case.m layout.csv -o placed.m
$ powerio geo convert buscoords.csv -o case.geo.json

PowerWorld display files

The .pwd reader returns a diagram space GeoLayer whose features place the decoded substations. Python also keeps the raw display compatibility helper parse_display, which returns DisplayData(kind="powerworld", data=PwdDisplay(...)) with the canvas dimensions, a timestamp, and the substation symbols.

The facade helpers connect it to the geo model. to_geo_layer_from_pwd lifts the substation symbols into a diagram space GeoLayer, which is also what powerio geo extract case.pwd does. to_geo_layer_from_aux_text parses the Latitude and Longitude columns of an AUX Substation table straight into a geographic layer. apply_substation_points joins either layer onto buses through the SubNum extras key. to_lonlat_from_pwd_mercator is a documented, approximate inverse of the projection PowerWorld’s auto generated layouts use, for when you want to place a diagram on a map. The component crate keeps to_geo_layer_from_aux_substations(&AuxFile) for parser authors, since the facade does not expose its borrowed parser type.

A bus row in a complete case export has its own coordinates as well. The aux reader promotes the substation Latitude:1/Longitude:1 pair, or, when those are absent, the bus’s own bare Latitude/Longitude pair, into Bus.location, and a promoted pair leaves extras.

In Rust a display file parses to PioValue::GeoLayer. There is no path from a display file to a BalancedNetwork; it is always a layer, and both module emission and PowerIO IR handle it as one.

Distribution graph projection

MulticonductorNetwork::to_graph() returns a bus and terminal graph and does not need coordinates to do it; in Python that is dist_net.to_graph(). Graph topology and geographic placement stay separate data.

PowerIO stores and transports coordinates and does not compute them. Laying out a case that has none is renderer math and belongs in the consumer, which can store the result with kind = synthetic so the origin of the coordinates survives.

Python has parse_geo, and both network types have to_geo_layer() and apply_geo_layer().

Rust, Python, Julia, and C

Rust, Python, Julia, and C expose the same operations on the same power system types:

source -> parse -> PioModule<T> -> calculation or update -> emit
                         |
                         +-> serialize -> PowerIO IR

parse and emit handle grid exchange formats, while serialize and deserialize handle PowerIO IR. A calc_* function returns a derived matrix, vector, report, or count, and a to_* function transforms a value in memory into another semantic type. The nouns are stored values and fields: module, value, diagnostics, network, bus, branch, generator, load, operating point, time series, scenario set, instance, solution.

MeaningRustPythonJuliaC ABI 7
name a filepass the name to parsepass a path to parsepass a path string to parsepio_source_open
acquire memorySource::from_memory(name, bytes)pass a file or bytes-like objectpass IO or AbstractVector{UInt8}pio_source_from_memory
parseparse(input), parse_with_options(input, &options)parse(source, format=..., name=...)parse(source; format=..., name=...)pio_parse
module valuemodule.value()module.valuemodule.valuepio_module_value
module diagnosticsmodule.diagnostics()module.diagnosticsmodule.diagnosticspio_module_diagnostics
emit a formatemit(&module, format, destination)emit(module, format, destination=None)emit(module, format, destination=nothing)pio_emit
serialize IRserialize(&module, destination)serialize(module, destination=None)serialize(module, destination=nothing)pio_module_serialize
deserialize IRdeserialize(source)deserialize(source)deserialize(source)pio_module_deserialize
apply updatesapply_updatesapply_updatesapply_updates!pio_apply_updates

To find out what you parsed, Rust matches on the PioValue case that module.value() returns, Python uses isinstance, Julia dispatches on PioModule{T} and on the concrete value types, and C compares canonical structural type names with pio_value_is_type before calling the typed accessor for the type it handles.

Only Rust and C have a Source type, because both need an explicit owner for acquired bytes. A Python path, open file, or bytes-like object, or a Julia path, IO, or byte vector, already says where the bytes come from, and the interpreter owns them, so parse takes them directly and reads the source name from the path or from the name argument.

A format made of related files goes through the same parse. For GO Challenge 3, a directory holding the problem file returns AcScucInstance, and once the matching solution file sits beside it the same call returns AcScucSolution.

Member access

Member names match across the four languages; what differs is the syntax, which follows what each language’s users expect.

LanguageA memberA table’s length
Rustaccessor method: module.value(), module.diagnostics(), network.buses()network.buses().len()
Pythonread only property: module.value, module.diagnostics, network.buseslen(network.buses) or network.n_buses
Juliaproperty: module.value, module.diagnostics, net.buseslength(net.buses)
Cone function per member: pio_module_value, pio_module_diagnosticsa _count function

Rust containers (PioModule, BalancedNetwork, MulticonductorNetwork, instances, and solutions) keep their fields private because they maintain invariants; value_mut severs retained source bytes, for example, and add_diagnostic rejects a duplicate identifier. Rust element records (Bus, Branch, Generator, Load, and the rest) are plain public structs, because struct literals and pattern matching are how Rust users build and read them. Python keeps the n_* counts because a table property builds one dict per row. A Python element is a dict keyed by the Rust field names, while a Julia element is an immutable struct whose field names are the C ABI names, which spell out the quantity and unit (vm_pu, active_power_mw). Unifying those two sets of field names is listed under Known limits.

Collections

TimeSeries<T> and ScenarioSet<T> contain typed entries, and each language reaches them its own way.

LanguageOperations
Rustlen, iter, checked get
Pythonlen, iteration, series[index], scenarios[id]; TimeSeries is a Sequence and ScenarioSet a Mapping
Julialength, iteration, 1-based getindex
Czero-based length and entry access; scenario lookup by identifier

An entry is either the contained value or a view rooted in the owning module, so indexing does not serialize, expand, or copy a complete network.

Calculations

The DC matrix and vector functions have the same names in each language:

calc_incidence_matrix
calc_branch_susceptances
calc_bus_susceptance_matrix
calc_branch_flow_matrix
calc_branch_phase_shift_injection
calc_bus_phase_shift_injection
calc_branch_flow_dc
calc_bus_injection_dc

Rust and C use zero based sparse matrix positions, Python sparse matrices use SciPy’s zero based positions, and Julia presents one based indices. Stable component identities and source identifiers do not change at a language boundary. Matrices and graphs gives the signs and equations.

Errors and ownership

Rust returns Result, Python raises PowerIOError subclasses, Julia throws a PowerIOError with structured diagnostics, and C returns a documented failure value and writes one PioError * through its error output. Each failure has a stable diagnostic code.

Python and Julia keep the native owner alive behind the borrowed typed views they hand you, whereas C callers retain and release opaque handles themselves. Ownership differs; the data types and the operation names do not.

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")

if isinstance(module.value, powerio.BalancedNetwork):
    print(module.value.n_buses)

for diagnostic in module.diagnostics:
    print(diagnostic.code, diagnostic.severity, diagnostic.message)

Diagnostics live on the module rather than on the contained network or solution. To find out what you have, use Python’s normal type system; there is no .kind property, kind enum, or typed narrowing helper.

The value classes are BalancedNetwork, dist.MulticonductorNetwork, OperatingPoint, TimeSeries, ScenarioSet, GeoLayer, 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.

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)

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.

Other functions

FunctionResult
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
module.to_balanced_report(), module.to_balanced()the multiconductor to balanced transformation

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 access is off unless POWERIO_MCP_ALLOWED_ROOTS lists the directories the server may read, and remote URI schemes are rejected. Host approval, request identifiers, timeouts, and cancellation are MCP transport concerns and do not touch the PowerIO data.

C ABI

powerio-capi exports ABI 7 through powerio-capi/include/powerio.h. The header is generated from the Rust declarations and checked in. Regenerate it with:

cbindgen --config powerio-capi/cbindgen.toml --crate powerio-capi \
  --output powerio-capi/include/powerio.h

scripts/capi-header-parity.sh compares the exported symbols with the header in every CI feature job, and scripts/capi-header-regen.sh regenerates the header with cbindgen and diffs it once. Do not edit the header by hand.

ABI 7 is the only C API in PowerIO 0.11; symbols from ABI 4, 5, and 6 are not exported and have no aliases. Compare pio_abi_version() with PIO_ABI_VERSION before you use the library.

The exported symbol set is fixed. The gridfm cargo feature adds GridFM Parquet parsing and emission behind the same entry points, and the arrow, matrix, dist, and prob feature names are still accepted by the build but gate nothing. pio_schema_report returns a JSON document with the release (powerio_version), the ABI (abi), the PowerIO IR schema name and generation (powerio_ir with schema and version), the BMOPF schema version, the compiled features, and the diagnostic namespaces and error categories.

Parse and inspect a module

PioError *error = NULL;
PioSource *source = pio_source_open("case9.m", 7, &error);
PioModule *module = pio_parse(source, NULL, 0, &error);
PioValueHandle *value = pio_module_value(module);

if (!pio_value_is_type(value, "powerio.BalancedNetwork", 23)) {
    /* handle an unexpected PowerIO type */
}

PioBalancedNetwork *network = pio_value_balanced_network(value, &error);
size_t buses = pio_balanced_network_bus_count(network);

PioDiagnostics *diagnostics = pio_module_diagnostics(module);
for (size_t i = 0; i < pio_diagnostics_len(diagnostics); i++) {
    PioStringView code = pio_diagnostic_code(diagnostics, i);
    /* code.data has code.len bytes and is not NUL terminated */
}

pio_diagnostics_release(diagnostics);
pio_balanced_network_release(network);
pio_value_release(value);
pio_module_release(module);
pio_source_release(source);

Use pio_source_from_memory for text or binary bytes you already hold in memory; both source constructors feed the same pio_parse. pio_geo_layer_parse reads a geographic layer straight from text, with no source object, for callers that have the layer document in memory.

pio_module_value returns an owner rooted value handle, and the exact typed accessors return owner rooted views without serializing or copying the module value. Releasing a module does not invalidate a child you have retained. Every opaque handle has matching retain and release functions, and release(NULL) is a no-op.

Structural type names have replaced ordinal kind integers. Check the type with pio_value_type_name or pio_value_is_type, then call the exact typed accessor for the type you handle.

Diagnostics and errors

Fallible functions take one PioError ** output and signal failure with a null return or a documented failure value. Inspect pio_error_code, pio_error_message, and pio_error_diagnostics, and branch on the stable code rather than the message text. If you pass a null error output the error is discarded.

Every string and buffer comes with an explicit length. PioStringView, PioByteView, PioSizeView, and PioF64View borrow their data from an owning handle and need not end in NUL.

Emit and serialize

pio_emit writes a grid exchange format. With a memory destination the artifact bytes stay in the returned PioEmitResult; with a path destination the artifacts are written to disk and the result holds the same list of artifacts and the same diagnostics.

PioDestination *destination = pio_destination_memory("case", 4, &error);
PioEmitResult *result = pio_emit(module, "matpower", 8, destination, &error);

for (size_t i = 0; i < pio_emit_result_artifact_count(result); i++) {
    PioArtifact *artifact = pio_emit_result_artifact(result, i, &error);
    PioStringView name = pio_artifact_name(artifact);
    PioByteView bytes = pio_artifact_bytes(artifact);
    /* consume name and bytes before releasing artifact */
    pio_artifact_release(artifact);
}

pio_emit_result_release(result);
pio_destination_release(destination);

pio_module_serialize writes PowerIO IR and pio_module_deserialize reads it. The IR header is "schema": "pio-ir" with integer "version": 2, and pio_schema_report reports both; the producer record names the PowerIO release separately. pio_module_deserialize refuses an unsupported schema name or generation and tells you what it found. ABI 7 has no reader for earlier generations and no module JSON aliases.

Collections, updates, and calculations

Time series and scenario set handles give you the length and owner rooted access to each element. Positions are zero based in C, and scenario sets can also be looked up by scenario ID.

Typed update constructors produce PioOperatingPointUpdate, PioNetworkUpdate, and PioCalculationUpdate. pio_apply_updates validates the whole batch before it applies anything and returns a PioUpdateReport listing the exact component IDs and fields it changed, and whether energized connectivity changed.

Named matrix and vector functions expose the public DC calculations directly:

pio_calc_incidence_matrix
pio_calc_branch_susceptances
pio_calc_bus_susceptance_matrix
pio_calc_branch_flow_matrix
pio_calc_branch_phase_shift_injection
pio_calc_bus_phase_shift_injection
pio_calc_branch_flow_dc
pio_calc_bus_injection_dc

Sparse matrices come back as owned CSR arrays and vectors as owned double arrays. The C API has no public DC data bundle.

CLI and MCP

The powerio command

The powerio binary runs the same operations from a shell, and with no subcommand it opens the interactive TUI.

powerio convert case14.m --to psse -o case14.raw   # parse + emit, findings on stderr
powerio summary case14.m                           # the canonical network summary JSON
powerio serialize case14.m -o case14.pio.json      # PowerIO IR
powerio verify case30.m --kind bdoubleprime        # matrix stats and the SDDM check
powerio batch -i tests/data -o out --matrices bprime,bdoubleprime
powerio sensitivities case30.m -o out              # PTDF and LODF
powerio dcopf case30.m -o out                      # the static DC OPF bundle
powerio gridfm case14.m -o out                     # a GridFM Parquet dataset
powerio geo extract case.aux -o layer.geo.json     # standalone geographic layers

The other subcommands are batch, which writes matrix families for every case in a directory, gen, which writes synthetic cases, geo apply and geo convert, corpus, the private corpus harness described in Corpus harness, and tui. dcopf accepts the alias dc-opf. --from and --to take the format tokens and aliases of the format table; iidm and rawx are accepted as input spellings only.

Format names, structural value types, and diagnostic codes are the same strings the language APIs use. Diagnostics print one per line on stderr as CODE: message.

Standard input and output

convert, summary, serialize, verify, dcopf, and sensitivities read the case from standard input when the input is -. A stream has no file name to infer a format from, so --from is required, and a gridfm dataset is a directory, so it cannot arrive on a stream at all. convert and serialize write a single text result to standard output when -o is - or omitted; a directory format such as pypsa-csv or cgmes needs an output directory.

cat case9.m | powerio convert - --from matpower --to psse -o - > case9.raw
powerio serialize - --from psse < case9.raw

Exit status

StatusMeaning
0success
1a failure without a PowerIO error category
2request: the arguments name something the request cannot satisfy (a format the writer cannot produce, a missing --from for standard input); clap usage errors also exit 2
3io: a path could not be read or written
4parse: the input is malformed, or a refused include left the output incomplete
5data: valid input that the operation cannot satisfy
6output: the writer could not produce the requested format

The categories are the ErrorCategory values the Rust and Python APIs report on every failure, so a shell script and a Python caller branch on the same five names.

Every failure the binary reports is a PowerIO error with a registered diagnostic code, including the ones the command line raises itself (REQUEST.CLI.FORMAT_REQUIRED, REQUEST.CLI.TARGET_UNSUPPORTED, REQUEST.CLI.OUTPUT_REQUIRED, REQUEST.CLI.FAMILY_MISMATCH, REQUEST.CLI.OPTION_INVALID, REQUEST.CLI.NO_CASES, PARSE.CLI.ERRORS_REPORTED, VALIDATE.CLI.INPUT_LACKS_DATA, EMIT.CLI.SIDECAR_PATH, EMIT.CLI.ERRORS_REPORTED). A reader that reports errors ends the run with PARSE.CLI.ERRORS_REPORTED and status 4 after the output is written; a writer that reports errors ends it with EMIT.CLI.ERRORS_REPORTED and status 6. A failure that reaches the top of the program without a registered code is reported as BIND.CLI.UNCLASSIFIED and exits 1.

Diagnostics format

--diagnostics-format text (the default) prints one CODE: message line per diagnostic as the command runs, wrote <path> when a file lands, and a failure as Error: and Caused by: lines.

--diagnostics-format json (accepted before or after the subcommand) prints one JSON array on stderr when the command ends, and nothing else. The array holds every diagnostic the run produced, warnings and errors alike, as PowerIO IR diagnostic records: the same encoding a module’s diagnostics field carries in a .pio.json document, with the same fields (id, severity, code, message, target, spans, related, details, suggested_action). A failure adds its record, and each further reason in its cause chain becomes a note record whose related names the failure record:

[{"id": "failure", "severity": "error", "code": "READ.IO.OPEN",
  "message": "cannot open source `case9.m`"},
 {"id": "d0", "severity": "note", "code": "READ.IO.OPEN",
  "message": "No such file or directory (os error 2)", "related": ["failure"]}]

The exit status stays the process exit status and is not repeated inside the records.

The MCP server

powerio-mcp (from the Python package) calls the public Python API directly. Its tools are parse, emit, summarize, diagnostics, to_normalized, calc_matrix, to_balanced_report, to_balanced, display, and about. parse returns serialized PowerIO IR, and the tools that accept powerio_ir deserialize that same IR rather than defining a network, collection, calculation, update, or solution representation of their own.

You address a collection entry with a plain zero based time_index or a scenario_id; no tool exports or expands an entry into a static network. emit(format, destination) writes a file or directory, and if you omit destination it returns the artifacts in memory. Diagnostics stay structured records with code, severity, message, target, and source spans.

pip install 'powerio[mcp]'   # Python 3.10 or later
powerio-mcp                  # stdio transport

The server accepts exactly one input source: serialized PowerIO IR, a grid exchange path, or grid exchange content in memory. Filesystem access is off unless POWERIO_MCP_ALLOWED_ROOTS names the directories the server may read, and remote URI schemes are rejected.

Known limits and versions

Known limits

  • Format profiles are bounded. PyPSA support covers the CSV electrical profile; multi carrier components, investment periods, and stochastic data are retained and reported rather than typed, and PyPSA NetCDF does not parse. Egret support is the scalar network profile with time series, with unit commitment fields outside it. OpenDSS support is the static circuit; load shapes and solve instructions are retained and reported.
  • DOE GO Challenge 3 problem data, DeepMind OPFData, PowerWorld PWB, and the IEEE Common Data Format are parse only, though a complete AcScucSolution does emit the official GO Challenge 3 solution file.
  • PowerIO does not solve anything; instances feed external solvers.
  • Balanced to multiconductor construction, load linearized multiconductor admittance from an operating point, and a general multi period planning instance are waiting on a later release, and dynamic simulation data has no representation.
  • The multiconductor to balanced transformation is Rust and Python only. Multiconductor admittance assembly, powerio_matrix::calc_multiconductor_admittance_matrix, is Rust only.
  • Only the MATPOWER and PSS/E readers attach source spans, marking the record each finding is about. Findings from the other readers, from transformations, and from writers have no byte range, and the text rendering of a diagnostic is still CODE: message.
  • There is no one call convert in the library; you compose parse with emit. The command line does have powerio convert.
  • The MATPOWER, PSS/E, and PowerWorld AUX readers tokenize without owned strings. PyPSA CSV, PSLF, and OpenDSS tokenize through owned strings, and the Egret, GO Challenge 3, OPFData, and pandapower readers decode through a generic JSON value tree first.
  • Two sets of field names coexist. Rust records and Python dict rows use the MATPOWER derived names (vm, pg, rate_a, tap); C ABI 7 and Julia spell out the quantity and unit (vm_pu, active_power_mw, rate_a_mva, tap_ratio). Settling on one set of names for all four languages is a 1.0 decision, and it is separate from the PowerIO IR keys, which change only with a generation.
  • Python tables return dict rows; typed element views like Julia’s are not offered yet.
  • to_balanced and to_balanced_report live in powerio::transform rather than the facade root, and serialize_diagnostics returns a String where serialize returns an EmitResult.
  • SocwrOpfSolution calls its branch flows branch_from_active_power where the other solutions say branch_from_active_flow, and its bus_order and branch_order are iterators rather than vectors. PowerIO IR generation 2 has the same split in its keys.

Versions

One PowerIO release version covers the Rust crates, the Python distribution, and PowerIO.jl. The boundaries checked independently are:

BoundaryValue at 0.11.0Checked whereMoves when
PowerIO release0.11.0the manifests, powerio::VERSION, powerio.versions(), pio_versionevery release
C ABI7the pio_abi_version handshake at loadan existing C signature or documented behavior changes
PowerIO IR generation2, and the reader accepts 2the document header, powerio::IR_VERSION, powerio::IR_MIN_VERSIONthe serialized representation changes
Rust toolchain1.88rust-version in the workspace manifest, checked by CIa dependency in the locked graph requires a newer compiler
Python3.9 or later; the mcp extra needs 3.10, the bench extra 3.11pyproject.tomla dependency drops a version

The release version is what you install. ABI 7 is what a compiled consumer has to match. The IR generation identifies the serialized document; its rule is in PowerIO IR. The MCP server passes PowerIO IR documents through and defines no electrical data shape of its own.

The 0.11.x line is for compatible fixes, performance work, and additive change; a break in the public Rust API that cannot be avoided goes into 0.12. The Rust API stays pre-1.0 as long as public signatures expose types from the pre-1.0 sprs and petgraph crates.

Developer guides

These pages are for contributors and for anyone whose code depends on PowerIO internals. They cover the change from 0.10, the PowerIO IR document and its field reference, the BMOPF mapping, the crate graph, the design decisions borrowed from LLVM and MLIR, the DC OPF bundle, the private corpus harness, benchmarks, and the checks a release runs.

Everything here describes the current implementation; earlier releases are in the changelog. The dated design record behind the 0.11 API lives outside the guide at docs/design/ and is not API authority.

From 0.10 to 0.11

PowerIO 0.11 breaks the 0.10 source API and the C ABI once, to remove duplicate ways of doing the same thing and to expose the same concepts in Rust, Python, Julia, and C. After that the 0.11.x line stays compatible, and any further unavoidable public break waits for 0.12.

One parse, one emit

Every grid exchange format now goes through parse, which opens the input itself. You can hand it a file or directory name, content already in memory, or a Source of named buffers, and all of them reach the same call.

let module = powerio::parse("case9.m")?;
let module = powerio::parse(case_directory)?;
let module = powerio::parse(bytes)?;

// Content in memory carries the name `<memory>`, which identifies no format,
// so a format read from a file extension is declared or named.
let module = powerio::parse_with_options(
    bytes,
    &powerio::ParseOptions::default().format("matpower")?,
)?;
let module = powerio::parse(powerio::Source::from_memory("case9.m", bytes)?)?;
0.100.11
parse(Source::open(path)?, None)parse(path)
parse(source, Some(format))parse_with_options(source, &ParseOptions::default().format(format)?)
emit(&module, format, Destination::path(path))emit(&module, format, path)
serialize(&module, Destination::path(path))serialize(&module, path)
deserialize(Source::open(path)?)deserialize(path)
network.to_json()serialize(&PioModule::new(network), destination)
BalancedNetwork::from_json(text)deserialize(Source::from_memory("module.pio.json", text)?)?.into_value()
parse_display(source, from)parse(input), which returns PioValue::GeoLayer; PwdDisplay remains for the raw display record
DisplayData, DisplayFormatremoved from Rust; Python keeps parse_display and DisplayData for the raw PowerWorld display record
a layer written by handemit(&module, "geo-json", path)

Python accepts a path, a file object, or a bytes-like object. A str is taken as a path, so wrap text already in memory in io.StringIO.

module = powerio.parse("case9.m")
module = powerio.parse(io.StringIO(text), format="matpower", name="case9.m")
module = powerio.parse(binary_data, format="pwb", name="case.pwb")

Julia uses multiple dispatch on a path string, IO, or AbstractVector{UInt8}.

module_ = parse("case9.m")
module_ = parse(IOBuffer(text); format="matpower", name="case9.m")
module_ = parse(bytes; format="pwb", name="case.pwb")

In C you construct a PioSource with pio_source_open or pio_source_from_memory and then call pio_parse.

These 0.10 names are removed:

  • parse_file
  • parse_text
  • parse_str
  • parse_bytes

Read the module fields

PioModule<T> contains the typed value and its diagnostics, producer, sources, source mappings, history, and extensions. Rust, Python, and Julia expose these members using their usual language conventions: Rust has accessor methods such as value() and diagnostics(), and Python and Julia have properties such as .value and .diagnostics.

In Rust, dynamic parsing returns PioModule<PioValue>, so match on module.value() directly. In Python, use isinstance(module.value, BalancedNetwork); in Julia, dispatch on PioModule{BalancedNetwork} or another concrete parameter.

PioValueKind, .kind, try_into_typed, IntoTypedModule, and the binding wrappers that duplicated each language’s own type inspection are removed. The C ABI uses structural names such as powerio.BalancedNetwork and exact type predicates instead of an ordinal value kind enum.

Diagnostics belong to the module, so Python and Julia no longer have a callable diagnostics().

Emit formats; serialize PowerIO IR

emit is now the one way to produce a grid exchange format.

powerio::emit(&module, "matpower", "copy.m")?;
memory_result = powerio.emit(module, "matpower")
file_result = powerio.emit(module, "psse", "case.raw")
memory_result = emit(module_, "matpower")
file_result = emit(module_, "psse", "case.raw")

The result lists every artifact it wrote, the output layout, the fidelity, and the emission diagnostics, and the same call handles text, binary, single file, and directory formats.

PowerIO IR goes through serialize and deserialize instead. .pio.json is not a grid exchange format, so format discovery does not list it. Both calls use the PowerIO IR document shape, "schema": "pio-ir" with integer "version": 2, and the producer record names the PowerIO release separately. Documents from earlier generations have to be regenerated from their original power system data.

These 0.10 names are removed:

  • write_to
  • write_string
  • write_file
  • to_format
  • module JSON read and write names
  • BalancedNetwork::to_json, from_json, and to_json_with_diagnostics
  • the model-json JSON classification family
  • the pio-json format token

to_* is still there for a genuine semantic transformation in memory.

Use ordinary collection operations

TimeSeries<T> and ScenarioSet<T> hold real typed values, so you use them like any other collection. Rust has len, iter, and checked get; Python has iteration and indexing; Julia has length, iteration, and 1-based getindex; C has opaque length and element access.

Remove calls to StateInventory, StateSelector, SelectedState, list_states, select_state, and export_state and index the collection instead. What you get back is the contained typed value itself, or an owner rooted typed view of it; nothing is encoded and reparsed on the way out.

Apply typed updates

PowerIO 0.11 adds OperatingPointUpdate, NetworkUpdate, CalculationUpdate, apply_updates, and UpdateReport. An update identifies its component by stable ComponentId and gives an absolute value with an explicit unit; the whole batch is validated first and then applied atomically. In Julia the mutating functions end in !.

UpdateReport lists the component IDs and fields that changed and says whether energized connectivity changed. A bus demand update has to name a load or an explicit allocation rule, because PowerIO will not pick an arbitrary load to receive aggregate demand. LoadAllocation::ProportionalToCurrentActivePower keeps the current shares and refuses an all zero basis; LoadAllocation::Equal splits the replacement evenly, which is how you restore demand after setting every participating load to zero.

Use calculation names that state the result

The DC data types PioDcData, DcNetworkData, and dc_data, and the phrase “DC branch coefficients”, are gone, though the DC OPF bundle files written by powerio dcopf and emit_dcopf_bundle remain. Use the named calculations instead:

calc_incidence_matrix
calc_branch_susceptances
calc_bus_susceptance_matrix
calc_branch_flow_matrix
calc_branch_phase_shift_injection
calc_bus_phase_shift_injection
calc_branch_flow_dc
calc_bus_injection_dc

The public orientation and signs are:

A[e, from] = +1
A[e, to]   = -1

Bf = Diagonal(b) * A
B  = A' * Diagonal(b) * A

p_branch = -Bf * va + b .* shift
p_shift  = A' * (b .* shift)
p_bus    = -B * va + p_shift

BranchSusceptanceFormula picks between the documented series susceptance, tap adjusted reactance, and reactance only equations.

Calculation instances and solutions

PowerIO 0.11 registers these calculation pairs:

DcPfInstance       DcPfSolution
AcPfInstance       AcPfSolution
DcOpfInstance      DcOpfSolution
AcOpfInstance      AcOpfSolution
McAcPfInstance     McAcPfSolution
McAcOpfInstance    McAcOpfSolution
AcScucInstance     AcScucSolution

SocwrOpfSolution holds a PowerModels SOCWR relaxation, including its W-space values and objective lower bound. PowerIO does not call it an AcOpfSolution unless voltage recovery and AC residual checks support that claim.

The initial assignment of an instance is its initial_point field.

C ABI 7

PowerIO 0.11 replaces ABI 6 with ABI 7, which drops the ABI 4, 5, and 6 aliases and the Arrow export. Sources, destinations, modules, typed values, collections, diagnostics, artifacts, sparse matrices, and vectors are all opaque reference counted handles, every buffer comes with an explicit length, and a borrowed typed handle keeps its module owner alive.

PowerIO.jl moves to ABI 7 in the same release. Julia packages should depend on PowerIO.jl rather than call the C ABI directly.

Inputs accepted by 0.11

PowerIO still accepts documented aliases for names that external formats define, such as rawx for the canonical psse-rawx format token. An alias like that names the same third party format; it does not keep a prerelease PowerIO API or IR shape alive, and 0.11 has no beta source or document aliases at all.

PowerIO IR

A .pio.json file serializes one PioModule<PioValue>, which is one typed value together with its diagnostics, producer, sources, source mappings, history, and extensions. A current document begins like this:

{
  "schema": "pio-ir",
  "version": 2,
  "producer": { "name": "powerio", "version": "0.11.0" },
  "value": {
    "type": "powerio.BalancedNetwork",
    "data": {}
  }
}

Use serialize to produce it and deserialize to read it. PowerIO IR is not one of the grid exchange formats. MATPOWER, PSS/E, XIIDM, CGMES, OpenDSS, PMD JSON, BMOPF, and the other formats in the format registry exist to exchange power system data with other tools, and they enter through parse and leave through emit. PowerIO IR preserves PowerIO types and module records instead, and it is deliberately left out of grid exchange format discovery. Use it when both sides consume PowerIO values, including calculation instances, solutions, time series, and scenario sets.

The generated JSON Schema is checked in at docs/schema/pio-ir/2/schema.json and served from https://powerio.dev/schema/pio-ir/2/schema.json. That schema, the serializer, and the deserializer are all tested from the same Rust types. docs/schema/README.md lists the earlier pio-package and powerio.module documents as one history under pio-ir.

Module records

The document stores these common records when present:

fieldmeaning
producerthe software operation that created this module
sourcessource names, sizes, declared formats, and digests
source_mapJSON Pointer paths in value.data mapped to source byte ranges
diagnosticsstructured findings with stable codes and severities
historyordered derivations that produced the current value
extensionsnamespaced data outside the PowerIO core schema

Source bytes retained at runtime are not serialized. A source record names a source without exposing a local absolute path; a parser may keep the bytes in memory for byte exact same format emission while the process runs, but that buffer is separate from the stored module.

The deserializer checks the cross references before it returns a module: diagnostic source references and source mappings must name declared source IDs, byte ranges must fit the declared source length, and history references must name records in the same document. The MATPOWER and PSS/E readers attach the byte range of the record a finding is about to every diagnostic they raise at a known record, so a document serialized from one of their modules has those spans; the other readers attach none yet.

Typed values

value.type is the canonical structural type name used by Rust, C, Python, Julia, and the document schema. Examples include:

powerio.BalancedNetwork
powerio.MulticonductorNetwork
powerio.OperatingPoint<powerio.BalancedNetwork>
powerio.TimeSeries<powerio.MulticonductorNetwork>
powerio.ScenarioSet<powerio.TimeSeries<powerio.BalancedNetwork>>
powerio.DcOpfInstance
powerio.SocwrOpfSolution

value.data has the exact shape that type demands, and the deserializer rejects a document where the two disagree, as it rejects a wrong schema name or version, an unknown PowerIO type, duplicate IDs, invalid references, nonfinite values in untyped positions, or a collection whose entries disagree with its element type. PowerIO IR reference defines every structural type field by field: type, unit, sign convention, invariant, and the value a reader takes when a field is absent.

Typed floating point fields spell nonfinite values as "Infinity", "-Infinity", and "NaN". JSON null is not a floating point value.

Collections and operating points

TimeSeries<T> stores ordered time points and values of T, and ScenarioSet<T> stores named alternatives of T with optional probabilities and no implied time order. Nested collections keep their structural type; the document does not invent a flattened name for each composition.

An OperatingPoint<N> stores a shared base network and typed overrides keyed by stable component ID. The serializer preserves that relationship rather than expanding each entry into another complete static network.

Determinism

serialize is a function of the module alone. Serializing one module twice produces identical text, and serializing the module that text deserializes to produces the same text again. Members are written in a fixed order, record fields in declaration order and map keys (extras, quantities, extensions, details) sorted. Diagnostic IDs are minted d0, d1, … in record order for records that have none, so the minted IDs depend only on record order. Every float is written in the shortest decimal form that reads back to the same value, and nonfinite values use the three string spellings above. Equal modules therefore produce equal documents, which is what lets you use a document as a cache key, a golden file, or the input of a content digest.

Resource limits

Before it retains large input data, deserialization applies explicit limits on source count and byte lengths, diagnostics, source map and history records, extension data, collection lengths, ID lengths, and nested value depth. Hitting a limit produces a structured PowerIO diagnostic rather than an allocation failure or a truncated result.

Generations

The integer version is the generation of the serialized representation. It is a property of the document alone, separate from the Rust memory layout, the PowerIO release, any grid exchange format, and the C ABI, and it changes only when the representation changes. producer.version records the release that wrote the document; the reader reports it and ignores it when deciding compatibility.

When a generation bumps inside one minor release line, the release ships with a reader for the generation it replaces, so every 0.11.x release reads every generation any 0.11.x release wrote. powerio::IR_VERSION is the generation a build writes and powerio::IR_MIN_VERSION the oldest it reads; in 0.11 both are 2. A refused document is reported with the schema name, generation, and producer it claims and the remedy: a later generation needs a newer PowerIO, and an earlier schema name or generation has to be regenerated from its source data. docs/schema/README.md is the ledger of every generation and the archive of every published schema.

PowerIO IR reference

This page lists every structural value type in the PowerIO IR, field by field: each field’s type, unit, and sign convention, the invariant the deserializer or the constructors enforce, and what a reader uses when the field is absent. The generated schema at docs/schema/pio-ir/2/schema.json is the machine form of the same definitions. To keep the two from drifting apart, powerio/tests/ir_reference.rs reads this page and checks in both directions that each table lists the same fields the schema defines for its definition.

Reading the tables

Each field table sits under a line that begins with the words “Schema definition” and gives, in backticks, the $defs entries of the schema the table documents; that line is how the test pairs a table with its definitions. The columns are:

  • field: the JSON member name in value.data (or in the nested record).
  • type: float is a JSON number or one of the strings "Infinity", "-Infinity", and "NaN"; null is refused at a float position. float or null is a float the source may leave unstated. id is a bus identifier: a nonnegative integer, the source’s own bus number. matrix is an array of equal length float arrays. token is one of the listed strings.
  • unit: the physical unit. p.u. is per unit on the network’s base_mva and the bus base_kv unless the row says otherwise.
  • sign: the direction a positive value means; blank when the quantity is unsigned or the sign carries no convention.
  • invariant: what the deserializer refuses or the constructors hold.
  • if absent: required when the serializer always writes the field and the deserializer refuses a document without it; otherwise the value a reader takes when the member is missing.

uid on an element row is the stable component identity that operating points, solutions, and updates refer to. When the source gives none, parse and serialize assign {table}:{row}, so a document PowerIO writes has one wherever a table below says “assigned at serialization”. extras on an element is the map of source fields the typed model has no slot for, keyed by the source’s own field names; the member is required in the document but may be empty.

Structural type names

value.type is one of these names, and value.data has the shape of the schema definition beside it.

structural typeschema definition
powerio.BalancedNetworkBalancedNetwork
powerio.MulticonductorNetworkMulticonductorNetwork
powerio.GeoLayerGeoLayer
powerio.OperatingPoint<powerio.BalancedNetwork>StoredOperatingPoint
powerio.OperatingPoint<powerio.MulticonductorNetwork>StoredOperatingPoint2
powerio.TimeSeries<powerio.BalancedNetwork>StoredTimeSeries
powerio.TimeSeries<powerio.MulticonductorNetwork>StoredTimeSeries2
powerio.TimeSeries<powerio.OperatingPoint<powerio.BalancedNetwork>>StoredOperatingPointTimeSeries
powerio.TimeSeries<powerio.OperatingPoint<powerio.MulticonductorNetwork>>StoredOperatingPointTimeSeries2
powerio.ScenarioSet<powerio.BalancedNetwork>StoredScenarioSet
powerio.ScenarioSet<powerio.MulticonductorNetwork>StoredScenarioSet2
powerio.ScenarioSet<powerio.OperatingPoint<powerio.BalancedNetwork>>StoredOperatingPointScenarioSet
powerio.ScenarioSet<powerio.OperatingPoint<powerio.MulticonductorNetwork>>StoredOperatingPointScenarioSet2
powerio.ScenarioSet<powerio.TimeSeries<powerio.BalancedNetwork>>StoredScenarioSet3
powerio.ScenarioSet<powerio.TimeSeries<powerio.MulticonductorNetwork>>StoredScenarioSet4
powerio.ScenarioSet<powerio.TimeSeries<powerio.OperatingPoint<powerio.BalancedNetwork>>>StoredScenarioSet5
powerio.ScenarioSet<powerio.TimeSeries<powerio.OperatingPoint<powerio.MulticonductorNetwork>>>StoredScenarioSet6
powerio.DcPfInstanceDcPfInstance
powerio.AcPfInstanceAcPfInstance
powerio.DcOpfInstanceDcOpfInstance
powerio.AcOpfInstanceAcOpfInstance
powerio.McAcPfInstanceMcAcPfInstance
powerio.McAcOpfInstanceMcAcOpfInstance
powerio.AcScucInstanceAcScucInstance
powerio.DcPfSolutionDcPfSolution
powerio.AcPfSolutionAcPfSolution
powerio.DcOpfSolutionDcOpfSolution
powerio.AcOpfSolutionAcOpfSolution
powerio.SocwrOpfSolutionSocwrOpfSolution
powerio.McAcPfSolutionMcAcPfSolution
powerio.McAcOpfSolutionMcAcOpfSolution
powerio.AcScucSolutionAcScucSolution

powerio.BalancedNetwork

The positive sequence transmission model, in the units MATPOWER uses: powers in MW and MVAr, voltage magnitudes in per unit, angles in degrees, and impedances in per unit on base_mva. Bus identifiers are the source’s own.

Schema definition: BalancedNetwork.

fieldtypeunitsigninvariantif absent
namestringrequired
base_mvafloatMVAfinite and positiverequired
base_frequencyfloatHzpositive60
source_formattokenone of the format tokens (matpower, psse, psse-rawx, powermodels-json, egret-json, powerworld, powerworld-pwb, pslf, pandapower-json, pypsa-csv, gridfm, goc3-json, surge-json, opfdata-json, xiidm, cgmes, in-memory, normalized)required
busesarray of Busid uniquerequired
loadsarray of Loadbus names a busrequired
shuntsarray of Shuntbus names a busrequired
branchesarray of Branchfrom and to name busesrequired
generatorsarray of Generatorbus names a busrequired
storagearray of Storagebus names a busrequired
hvdcarray of Hvdcfrom and to name busesrequired
switchesarray of Switchfrom and to name buses[]
transformers_3warray of Transformer3Wevery winding bus names a bus[]
static_var_compensatorsarray of StaticVarCompensatorbus names a bus[]
areasarray of Areanumber unique[]
solverSolverParams or nullnull
case_metadataCaseMetadataevery member null
geoGeoMeta or nullnull
detailed_connectivityDetailedConnectivity or nullnull
generated_uidsarray of stringsubset of component uid values; identifies identities PowerIO assigned because the source stated none[]

Bus

Schema definition: Bus.

fieldtypeunitsigninvariantif absent
ididunique within busesrequired
kindtoken PQ, PV, REF, ISOLATEDMATPOWER type codes 1, 2, 3, 4required
vmfloatp.u.required
vafloatdegreesrequired
base_kvfloatkVnonnegative; 0 when the source states nonerequired
vmaxfloatp.u.vmin <= vmax; Infinity for no boundrequired
vminfloatp.u.required
evhifloat or nullp.u.emergency band, stated only when it differs from vmaxnull (equals vmax)
evlofloat or nullp.u.stated only when it differs from vminnull (equals vmin)
areaintegerrequired
zoneintegerrequired
namestring or nullnull
uidstring or nullunique within busesassigned at serialization
locationLocation or nullnetwork coordinate spacenull
extrasobjectrequired

Load

Schema definition: Load.

fieldtypeunitsigninvariantif absent
busidnames a busrequired
pfloatMWpositive is consumptionrequired
qfloatMVArpositive is inductive consumptionrequired
in_servicebooleanrequired
voltage_modelLoadVoltageModel or nullnull (constant power)
uidstring or nullunique within loadsassigned at serialization
extrasobjectrequired

LoadVoltageModel is tagged by kind. constant_power has no further member. zip has p_constant_power, p_constant_current, p_constant_impedance (MW, summing to p) and q_constant_power, q_constant_current, q_constant_impedance (MVAr, summing to q), plus an optional v_nom (kV), an optional source load_type code, and an optional scaling factor. exponential has gamma_p, gamma_q, and v_nom, with P = p (V / v_nom)^gamma_p and Q = q (V / v_nom)^gamma_q.

Shunt

Schema definition: Shunt.

fieldtypeunitsigninvariantif absent
busidnames a busrequired
gfloatMW at V = 1 p.u.positive is consumptionrequired
bfloatMVAr at V = 1 p.u.positive is capacitive injectionthe initial value of a switched shuntrequired
in_servicebooleanrequired
section_countinteger or nullnull (unset)
controlSwitchedShuntControl or nullnull (fixed shunt)
uidstring or nullunique within shuntsassigned at serialization
extrasobjectrequired

Schema definition: SwitchedShuntControl.

fieldtypeunitsigninvariantif absent
modetoken locked, continuous, discretePSS/E MODSW 0, 1, 2 and uprequired
vhighfloatp.u.vlow <= vhighrequired
vlowfloatp.u.required
control_busid or nullnames a busnull (the shunt’s own bus)
regulating_terminalTerminalReference or nullnull
rmpctfloatpercentrequired
blocksarray of ShuntBlockrequired

Schema definition: ShuntBlock.

fieldtypeunitsigninvariantif absent
stepsintegerrequired
gfloatMW at V = 1 p.u. per steppositive is consumptionrequired
bfloatMVAr at V = 1 p.u. per steppositive is capacitive injectionrequired

Branch

Schema definition: Branch.

fieldtypeunitsigninvariantif absent
fromidnames a busrequired
toidnames a busrequired
rfloatp.u.required
xfloatp.u.r and x not both zero when a matrix is builtrequired
bfloatp.u.positive is capacitivetotal line charging; half at each end unless charging is presentrequired
chargingBranchCharging or nullcanonical per terminal admittance when presentnull (derive from b)
rate_afloatMVA0 is unratedrequired
rate_bfloatMVA0 is unratedrequired
rate_cfloatMVA0 is unratedrequired
rating_setsarray of BranchRatingSet[]
current_ratingsBranchCurrentRatings or nullnull
tapfloatratio at the from side0 means 1 (a line); otherwise positiverequired
shiftfloatdegreespositive means the from side voltage leads the to siderequired
in_servicebooleanrequired
angminfloatdegreesangmin <= angmax; -360 and 360 mean unconstrainedrequired
angmaxfloatdegreesrequired
controlTransformerControl or nullnull (a line or a fixed ratio transformer)
solutionBranchSolution or nullnull
namestring or nullnull
uidstring or nullunique within branchesassigned at serialization
routearray of Location or nullnetwork coordinate spacenull
extrasobjectrequired

Schema definition: BranchCharging.

fieldtypeunitsigninvariantif absent
g_frfloatp.u.required
b_frfloatp.u.positive is capacitiverequired
g_tofloatp.u.required
b_tofloatp.u.positive is capacitiverequired

Schema definition: BranchRatingSet.

fieldtypeunitsigninvariantif absent
namestringrequired
rate_mvafloatMVArequired

Schema definition: BranchCurrentRatings.

fieldtypeunitsigninvariantif absent
c_rating_afloatsource units (amperes for PSS/E)required
c_rating_bfloatsource unitsrequired
c_rating_cfloatsource unitsrequired

Schema definition: BranchSolution.

fieldtypeunitsigninvariantif absent
pffloatMWpositive flows into the branch at the from terminalrequired
qffloatMVArpositive flows into the branch at the from terminalrequired
ptfloatMWpositive flows into the branch at the to terminalrequired
qtfloatMVArpositive flows into the branch at the to terminalrequired

Schema definition: TransformerControl.

fieldtypeunitsigninvariantif absent
modetoken fixed, voltage, reactive_flow, active_flow, dc_line_quantity, asymmetric_active_flowPSS/E COD magnitude 0 through 5required
enabledbooleanautomatic adjustment on; the sign of PSS/E CODrequired
controlled_busid or nullnames a busnull
controlled_bus_on_winding_sidebooleanfalse
regulating_terminalTerminalReference or nullnull
band_minfloatp.u. voltage, MVAr, or MW as mode selectsband_min <= band_maxrequired
band_maxfloatas band_minrequired
tap_minfloatratio, or degrees for phase controltap_min <= tap_maxrequired
tap_maxfloatas tap_minrequired
ntpintegernumber of tap positionsrequired
mva_basefloatMVArequired
winding_connection_anglefloat or nulldegreesasymmetric active power flow control onlynull

Generator

Schema definition: Generator.

fieldtypeunitsigninvariantif absent
busidnames a busrequired
pgfloatMWpositive is generationrequired
qgfloatMVArpositive is generationrequired
qmaxfloatMVArqmin <= qmax; Infinity and -Infinity mean unboundedrequired
qminfloatMVArrequired
vgfloatp.u.required
mbasefloatMVApositive when statedrequired
pmaxfloatMWpmin <= pmaxrequired
pminfloatMWrequired
in_servicebooleanrequired
costGenCost or nullnull (no cost curve)
capsmap of string to floatMW, MVAr, MW per minute, or a fraction per keykeys among pc1, pc2, qc1min, qc1max, qc2min, qc2max, ramp_agc, ramp_10, ramp_30, ramp_q, apf, the MATPOWER columns past PMIN{}
energy_sourcetoken hydro, nuclear, wind, thermal, solar, otherother
voltage_regulation_onbooleantrue
regulated_busid or nullnames a busnull (the generator’s own bus)
regulating_terminalTerminalReference or nullnull
active_power_controlActivePowerControl or nullnull
uidstring or nullunique within generatorsassigned at serialization

Schema definition: GenCost.

fieldtypeunitsigninvariantif absent
modelinteger1 is piecewise linear, 2 is polynomialrequired
startupfloatcurrencyrequired
shutdownfloatcurrencyrequired
ncostintegerpolynomial: coeffs.len() == ncost; piecewise: coeffs.len() == 2 * ncostrequired
coeffsarray of floatpolynomial: currency per MW^k per hour, highest order first; piecewise: alternating MW and currency per hour breakpointsrequired

Schema definition: ActivePowerControl.

fieldtypeunitsigninvariantif absent
participatebooleanrequired
droop_percentfloat or nullpercentnull
participation_factorfloat or nullnull
minimum_target_active_power_mwfloat or nullMWnull
maximum_target_active_power_mwfloat or nullMWnull

Storage

The PowerModels storage model.

Schema definition: Storage.

fieldtypeunitsigninvariantif absent
busidnames a busrequired
psfloatMWpositive is withdrawn from the network (charging)required
qsfloatMVArpositive is withdrawn from the networkrequired
energyfloatMWh0 <= energy <= energy_ratingrequired
energy_ratingfloatMWhrequired
charge_ratingfloatMWrequired
discharge_ratingfloatMWrequired
charge_efficiencyfloatfractionin [0, 1]required
discharge_efficiencyfloatfractionin [0, 1]required
thermal_ratingfloatMVArequired
current_ratingfloat or nullamperesnull
qminfloatMVArqmin <= qmaxrequired
qmaxfloatMVArrequired
rfloatp.u.required
xfloatp.u.required
p_lossfloatMWstandby lossrequired
q_lossfloatMVArstandby lossrequired
in_servicebooleanrequired
active_power_controlActivePowerControl or nullnull
uidstring or nullunique within storageassigned at serialization
extrasobjectrequired

Hvdc

A two terminal HVDC line in the MATPOWER dcline convention, whatever the source format.

Schema definition: Hvdc.

fieldtypeunitsigninvariantif absent
fromidnames a busrequired
toidnames a busrequired
in_servicebooleanrequired
pffloatMWpositive flows from the from bus to the to bus, measured at the from endrequired
ptfloatMWpositive flows from the from bus to the to bus, measured at the to endrequired
qffloatMVArpositive is injected into the from busrequired
qtfloatMVArpositive is injected into the to busrequired
vffloatp.u.voltage setpoint at the from busrequired
vtfloatp.u.voltage setpoint at the to busrequired
pminfloatMWbounds on pf; pmin <= pmaxrequired
pmaxfloatMWrequired
qminffloatMVArbounds on qfrequired
qmaxffloatMVArrequired
qmintfloatMVArbounds on qtrequired
qmaxtfloatMVArrequired
loss0floatMWconstant loss termrequired
loss1floatMW per MWlinear loss term in pfrequired
resistance_ohmfloat or nullohmnull
nominal_voltage_kvfloat or nullkVnull
converters_modetoken side1_rectifier_side2_inverter, side1_inverter_side2_rectifier, or nullnull
converter1HvdcConverter or nullnull
converter2HvdcConverter or nullnull
costGenCost or nullusage cost in pfnull
uidstring or nullunique within hvdcassigned at serialization
extrasobjectrequired

Schema definition: HvdcConverter.

fieldtypeunitsigninvariantif absent
componentComponentIdrequired
kindtoken vsc, lccrequired
loss_factor_percentfloatpercent of active powerrequired
power_factorfloat or nullnull
reactive_power_setpoint_mvarfloat or nullMVArnull
regulating_terminalTerminalReference or nullnull
voltage_regulator_onboolean or nullnull
voltage_setpoint_kvfloat or nullkVnull

Switch

A transmission switch. A closed switch stays a switch in the data; the matrix calculations do not lower it to a zero impedance branch.

Schema definition: Switch.

fieldtypeunitsigninvariantif absent
fromidnames a busrequired
toidnames a busrequired
closedbooleanrequired
thermal_ratingfloat or nullMVAnull
current_ratingfloat or nullamperesnull
pffloat or nullMWpositive flows into the switch at the from terminalnull
qffloat or nullMVArpositive flows into the switch at the from terminalnull
ptfloat or nullMWpositive flows into the switch at the to terminalnull
qtfloat or nullMVArpositive flows into the switch at the to terminalnull
uidstring or nullunique within switchesassigned at serialization
extrasobjectrequired

Transformer3W

Three windings joined at a star point; the indexed view star-lowers the record for matrix calculations.

Schema definition: Transformer3W.

fieldtypeunitsigninvariantif absent
namestring or nullnull
windingsarray of Windingexactly three, in the order primary, secondary, tertiaryrequired
zarray of Impedanceexactly three: z12, z23, z31required
mag_gfloatp.u. on the system base, at the star pointrequired
mag_bfloatp.u. on the system base, at the star pointpositive is capacitiverequired
star_vmfloatp.u.solved star point voltagerequired
star_vafloatdegreesrequired
in_servicebooleanrequired
uidstring or nullunique within transformers_3wassigned at serialization
extrasobjectrequired

Schema definition: Winding.

fieldtypeunitsigninvariantif absent
busidnames a busrequired
nominal_kvfloatkV0 defers to the terminal bus base_kvrequired
tapfloatratio1 is nominal (PSS/E WINDV with CW = 1)required
shiftfloatdegreesas Branch.shiftrequired
rate_afloatMVA0 is unratedrequired
rate_bfloatMVArequired
rate_cfloatMVArequired
controlTransformerControl or nullnull

Schema definition: Impedance.

fieldtypeunitsigninvariantif absent
rfloatp.u. on the system baserequired
xfloatp.u. on the system baserequired
base_mvafloatMVAthe source’s declared base for the pair; positiverequired

Area

Schema definition: Area.

fieldtypeunitsigninvariantif absent
numberintegerunique within areas; matches Bus.arearequired
namestring or nullnull
net_interchangefloatMWpositive is export out of the arearequired
tolerancefloatMWrequired
slack_busid or nullnames a busnull
area_typestring or nullnull
uidstring or nullnull

StaticVarCompensator

Schema definition: StaticVarCompensator.

fieldtypeunitsigninvariantif absent
busidnames a busrequired
b_min_siemensfloatsiemensb_min_siemens <= b_max_siemensrequired
b_max_siemensfloatsiemensrequired
voltage_setpoint_kvfloatkVrequired
reactive_power_setpoint_mvarfloatMVArpositive is injectionrequired
regulation_modetoken voltage, reactive_powerrequired
regulatingbooleanrequired
regulating_terminalTerminalReference or nullnull
pfloatMWpositive is consumptionrequired
qfloatMVArpositive is consumptionrequired
in_servicebooleanrequired
uidstring or nullnull
extrasobjectrequired

SolverParams

Each member is set only when the source has it.

Schema definition: SolverParams.

fieldtypeunitsigninvariantif absent
newton_tolerancefloat or nullMW and MVAr mismatchnull
max_iterationsinteger or nullnull
zero_impedance_thresholdfloat or nullp.u. reactancenull
adjust_tapsboolean or nullnull
adjust_phase_shiftboolean or nullnull
adjust_dc_tapsboolean or nullnull
adjust_switched_shuntboolean or nullnull
adjust_area_interchangeboolean or nullnull

CaseMetadata

Schema definition: CaseMetadata.

fieldtypeunitsigninvariantif absent
case_datestring or nullnull
forecast_distanceinteger or nullminutes, as the source statesnull
source_model_formatstring or nullnull
minimum_validation_levelstring or nullnull

Coordinates

Schema definition: GeoMeta.

fieldtypeunitsigninvariantif absent
kindtoken source, synthetic, manual, derived, or nulldefault origin of points without their own kindnull

Schema definition: Location.

fieldtypeunitsigninvariantif absent
xfloatnetwork coordinate space; longitude in geographic spacerequired
yfloatnetwork coordinate space; latitude in geographic spacerequired
kindtoken as GeoMeta.kind, or nullnull (the network default)

Component references

Schema definition: ComponentId.

fieldtypeunitsigninvariantif absent
component_typestringthe element table or record kindrequired
local_idstringthe source supplied or assigned identityrequired

Schema definition: TerminalReference.

fieldtypeunitsigninvariantif absent
equipmentComponentIdrequired
terminalinteger1 for single terminal equipment, else the side numberrequired

DetailedConnectivity

The hierarchy and bus breaker or node breaker connectivity a source gives beyond the balanced calculation view (XIIDM, CGMES, PSS/E RAW 35 and RAWX). Every collection is empty when absent. The schema defines the record types under the names below. Their field names end in their units (_kv, _mw, _mvar, _a, _ohm, _h, _f, _km, _degrees, _percent, _seconds), and terminal powers use the load sign convention (positive is consumption) where the record says so.

Schema definition: DetailedConnectivity.

fieldtypeunitsigninvariantif absent
subnetworksarray of Subnetwork[]
substationsarray of Substation[]
voltage_levelsarray of VoltageLevelkVnominal_kv positive[]
bus_breaker_busesarray of BusBreakerBus[]
calculated_busesarray of CalculatedBus[]
connectivity_nodesarray of ConnectivityNode[]
busbar_sectionsarray of BusbarSection[]
junctionsarray of Junction[]
terminalsarray of Terminal[]
switchesarray of TopologySwitch[]
internal_connectionsarray of InternalConnection[]
operational_limit_groupsarray of OperationalLimitGroupamperes, MW, or MVA per limit kind[]
tap_changersarray of TapChangerlow_tap_position <= tap_position when stated[]
equipment_reactive_limitsarray of EquipmentReactiveLimitsMVAr[]
boundary_linesarray of BoundaryLine[]
tie_linesarray of TieLine[]
component_metadataarray of ComponentMetadata[]
omitted_fieldsarray of OmittedFielda field absent from the source, distinct from a stated zero[]
dc_converter_unitsarray of DcConverterUnit[]
dc_topological_nodesarray of DcTopologicalNode[]
dc_nodesarray of DcNode[]
dc_groundsarray of DcGround[]
dc_busbarsarray of DcBusbar[]
dc_linesarray of DcLine[]
dc_series_devicesarray of DcSeriesDevice[]
dc_switchesarray of DcSwitch[]
voltage_source_convertersarray of VoltageSourceConverter[]
line_commutated_convertersarray of LineCommutatedConverter[]

powerio.MulticonductorNetwork

The conductor level distribution model, in SI units (watts, vars, volts, amperes, ohms, siemens, meters) with angles in radians. Bus identifiers and terminal names are the source’s own strings; for OpenDSS the terminal names are its node numbers. An element’s terminal_map lists, in conductor order, the terminals of the named bus it connects to.

Schema definition: MulticonductorNetwork.

fieldtypeunitsigninvariantif absent
namestring or nullnull
base_frequencyfloatHzpositiverequired
source_formattoken dss, bmopf-json, pmd-json, or nullnull
busesarray of DistBusid uniquerequired
linecodesarray of DistLineCodename uniquerequired
linesarray of DistLinebus_from, bus_to name buses; linecode names a line coderequired
switchesarray of DistSwitchbus_from, bus_to name busesrequired
transformersarray of DistTransformerevery winding bus names a busrequired
loadsarray of DistLoadbus names a busrequired
shuntsarray of DistShuntbus names a busrequired
capacitorsarray of DistCapacitorbus names a bus[]
generatorsarray of DistGeneratorbus names a busrequired
ibrsarray of DistIbrbus names a bus; control_profile names a profile[]
control_profilesarray of DistControlProfilename unique[]
sourcesarray of VoltageSourceBMOPF carries exactly onerequired
untypedarray of UntypedObjectrequired
commandsarray of [verb, args] string pairssource orderrequired
optionsarray of [name, value] string pairssource orderrequired
extrasobjectrequired
geoDistGeoMeta or nullnull

DistBus

Schema definition: DistBus.

fieldtypeunitsigninvariantif absent
idstringunique within busesrequired
terminalsarray of stringordered, uniquerequired
groundedarray of stringeach names a terminal of this bus; zero impedance to groundrequired
v_minfloat or nullvoltsv_min <= v_maxnull (unbounded)
v_maxfloat or nullvoltsnull (unbounded)
v_min_phasearray of float or nullvoltsphase order excludes neutral and earth; scalar v_min takes precedencenull
v_max_phasearray of float or nullvoltsphase order excludes neutral and earth; scalar v_max takes precedencenull
vpn_minarray of float or nullvoltsper phase to neutral boundnull
vpn_maxarray of float or nullvoltsnull
vpp_minarray of float or nullvoltsper phase to phase boundnull
vpp_maxarray of float or nullvoltsnull
vpos_minfloat or nullvoltspositive sequence boundnull
vpos_maxfloat or nullvoltsnull
vneg_maxfloat or nullvoltsnegative sequence magnitude capnull
vzero_maxfloat or nullvoltszero sequence magnitude capnull
vn_maxfloat or nullvoltsneutral to ground magnitude capnull
locationDistLocation or nullnetwork coordinate spacenull
extrasobjectrequired

DistLineCode

Schema definition: DistLineCode.

fieldtypeunitsigninvariantif absent
namestringunique within linecodesrequired
n_conductorsintegerpositive; the order of every matrixrequired
r_seriesmatrixohm per metern_conductors square, symmetricrequired
x_seriesmatrixohm per metern_conductors square, symmetricrequired
g_frommatrixsiemens per meterhalf of the shunt admittance, at the from endrequired
b_frommatrixsiemens per meterpositive is capacitiverequired
g_tomatrixsiemens per meterhalf of the shunt admittance, at the to endrequired
b_tomatrixsiemens per meterpositive is capacitiverequired
i_maxarray of float or nullamperes per conductornull
s_maxarray of float or nullVA per conductornull
sourcestring or nullorigin of the matrices (BMOPF source)null
extrasobjectrequired

DistLine

Schema definition: DistLine.

fieldtypeunitsigninvariantif absent
namestringunique within linesrequired
bus_fromstringnames a busrequired
bus_tostringnames a busrequired
terminal_map_fromarray of stringn_conductors terminals of bus_fromrequired
terminal_map_toarray of stringn_conductors terminals of bus_torequired
linecodestringnames a line coderequired
lengthfloatmetersnonnegativerequired
i_maxarray of float or nullamperes per conductornull (the line code’s)
s_maxarray of float or nullVA per conductornull (the line code’s)
routearray of DistLocation or nullnetwork coordinate spacenull
extrasobjectrequired

DistSwitch

Schema definition: DistSwitch.

fieldtypeunitsigninvariantif absent
namestringunique within switchesrequired
bus_fromstringnames a busrequired
bus_tostringnames a busrequired
terminal_map_fromarray of stringterminals of bus_from, same length as terminal_map_torequired
terminal_map_toarray of stringterminals of bus_torequired
openbooleanrequired
i_maxarray of float or nullamperes per conductornull
extrasobjectrequired

DistTransformer

Schema definition: DistTransformer.

fieldtypeunitsigninvariantif absent
namestringunique within transformersrequired
phasesinteger1 through 3required
windingsarray of DistWindingtwo or threerequired
xsc_pctarray of floatpercent[xhl] for two windings, [xhl, xht, xlt] for threerequired
extrasobjectrequired

Schema definition: DistWinding.

fieldtypeunitsigninvariantif absent
busstringnames a busrequired
terminal_maparray of stringterminals of busrequired
conntoken wye, deltarequired
v_reffloatvolts, line to line for two and three phasespositiverequired
s_ratingfloatVApositiverequired
r_pctfloatpercent of the winding baserequired
tapfloatratio1 is nominalrequired
r_neutralfloat or nullohmnull
x_neutralfloat or nullohmnull

DistLoad

Schema definition: DistLoad.

fieldtypeunitsigninvariantif absent
namestringunique within loadsrequired
busstringnames a busrequired
terminal_maparray of stringterminals of busrequired
configurationtoken wye, delta, single_phaserequired
p_nomarray of floatwatts per phasepositive is consumptionone entry per active phaserequired
q_nomarray of floatvars per phasepositive is inductive consumptionsame length as p_nomrequired
voltage_modelDistLoadVoltageModelrequired
extrasobjectrequired

DistLoadVoltageModel is tagged by model. constant_power, constant_current, and constant_impedance each have v_nom (volts per active phase). zip has v_nom and the per phase coefficient arrays alpha_z, alpha_i, alpha_p (active power) and beta_z, beta_i, beta_p (reactive power), each triple summing to one. exponential has v_nom, gamma_p, and gamma_q.

DistCapacitor

Schema definition: DistCapacitor.

fieldtypeunitsigninvariantif absent
namestringunique within capacitorsrequired
busstringnames a busrequired
terminal_maparray of stringterminals of busrequired
configurationtoken wye, delta, single_phaserequired
q_ratedfloatvars, whole bank at v_nompositive is capacitive injectionrequired
v_nomfloatvolts, line to line for the three phase configurationspositiverequired
extrasobjectrequired

DistShunt

Schema definition: DistShunt.

fieldtypeunitsigninvariantif absent
namestringunique within shuntsrequired
busstringnames a busrequired
terminal_maparray of stringterminals of bus, the order of the matricesrequired
gmatrixsiemenssquare in terminal_maprequired
bmatrixsiemenspositive is capacitivesquare in terminal_maprequired
extrasobjectrequired

DistGenerator

Schema definition: DistGenerator.

fieldtypeunitsigninvariantif absent
namestringunique within generatorsrequired
busstringnames a busrequired
terminal_maparray of stringterminals of busrequired
configurationtoken wye, delta, single_phaserequired
p_nomarray of floatwatts per phasepositive is generationrequired
q_nomarray of floatvars per phasepositive is generationsame length as p_nomrequired
p_minarray of float or nullwatts per phasep_min <= p_maxnull
p_maxarray of float or nullwatts per phasenull
q_minarray of float or nullvars per phaseq_min <= q_maxnull
q_maxarray of float or nullvars per phasenull
s_maxarray of float or nullVA per conductornull
i_maxarray of float or nullamperes per conductornull
costarray of float or nullcurrency per kWh per phase, or one scalarnull
extrasobjectrequired

DistIbr

Schema definition: DistIbr.

fieldtypeunitsigninvariantif absent
namestringunique within ibrsrequired
busstringnames a busrequired
terminal_maparray of stringterminals of busrequired
prime_movertoken PV, BATTERY, GENERIC, STATCOM, DSTATCOMrequired
topologytoken SINGLE_PHASE, THREE_LEG, FOUR_LEGrequired
s_maxarray of floatVA per phasenameplaterequired
p_availfloat or nullwattsnull
p_minarray of float or nullwatts per phasenull
p_maxarray of float or nullwatts per phasenull
q_minarray of float or nullvars per phasenull
q_maxarray of float or nullvars per phasenull
i_maxarray of float or nullamperes per conductornull
voltage_aggregationtoken PER_PHASE, AVERAGE, or nullnull
control_profilestring or nullnames a control profilenull
extrasobjectrequired

DistControlProfile

Schema definition: DistControlProfile.

fieldtypeunitsigninvariantif absent
namestringunique within control_profilesrequired
volt_varVoltVarControl or nullnull
volt_wattVoltWattControl or nullnull
power_factorPowerFactorControl or nullnull
extrasobjectrequired

Schema definition: VoltVarControl.

fieldtypeunitsigninvariantif absent
breakpointsarray of floatp.u. voltageascendingrequired
q_limitsarray of floatq_unitpositive is injectionsame length as breakpointsrequired
voltage_referencetoken PN_PER_PHASE, PP_PER_PHASE, PP_AVERAGED, PG_AVERAGED, PN_AVERAGED, PG_PER_PHASE, or nullnull
q_reftoken VAR_MAX, VAR_AVAILABLE, or nullnull
q_unittoken VA_FRACTION, VAR, or nullnull
p_min_for_qfloat or nullwattsnull
p_min_for_q_maxfloat or nullwattsnull

Schema definition: VoltWattControl.

fieldtypeunitsigninvariantif absent
breakpointsarray of floatp.u. voltageascendingrequired
p_limitsarray of floatp_unitsame length as breakpointsrequired
voltage_referencetoken as VoltVarControl.voltage_reference, or nullnull
p_reftoken P_AVAILABLE, P_MAX, S_MAX, or nullnull
p_unittoken VA_FRACTION, W, or nullnull

Schema definition: PowerFactorControl.

fieldtypeunitsigninvariantif absent
pffloatin [-1, 1]required

VoltageSource

Schema definition: VoltageSource.

fieldtypeunitsigninvariantif absent
namestringunique within sourcesrequired
busstringnames a busrequired
terminal_maparray of stringterminals of busrequired
v_magnitudearray of floatvolts per terminal0 on a grounded terminal; same length as terminal_maprequired
v_anglearray of floatradians per terminalsame length as terminal_maprequired
energy_cost_rateoptional array of float$/kWhpositive injection supplies the networkone entry per phase in terminal-map order; excludes neutral terminalsno stated source-price term
extrasobjectrequired

UntypedObject

An object the reader recognized but does not type, kept so a conversion can report it precisely.

Schema definition: UntypedObject.

fieldtypeunitsigninvariantif absent
classstringrequired
namestringrequired
propsarray of [key, value] string pairssource orderrequired

Distribution coordinates

Schema definition: DistGeoMeta.

fieldtypeunitsigninvariantif absent
kindtoken source, synthetic, manual, derived, or nullnull

Schema definition: DistLocation.

fieldtypeunitsigninvariantif absent
xfloatnetwork coordinate space; longitude in geographic spacerequired
yfloatnetwork coordinate space; latitude in geographic spacerequired
kindtoken as DistGeoMeta.kind, or nullnull (the network default)

powerio.GeoLayer

A standalone geographic document: element points and routes in one coordinate space, keyed by element identity rather than embedded in a case. The canonical .geo.json, GeoJSON, aliased CSV or JSON records, headerless buscoords CSV, and a PowerWorld .pwd display all parse to one, and apply_geo_layer places it onto a network.

Schema definition: GeoLayer.

fieldtypeunitsigninvariantif absent
spaceCoordinateSpacethe space every feature’s coordinates are inrequired
kindtoken source, synthetic, manual, derived, or nulldefault origin of features without their own kindnull
featuresarray of GeoFeatureempty

CoordinateSpace is tagged by space. geographic has an optional crs; x is longitude and y latitude in decimal degrees, and a null crs means EPSG:4326. projected has an optional crs for planar coordinates. diagram has an optional canvas for drawing coordinates with no earth referent. unknown has no further member and means the source declared no space.

Schema definition: Canvas.

fieldtypeunitsigninvariantif absent
widthfloat or nullcanvas unitspositivethe drawing width the source statesnull
heightfloat or nullcanvas unitspositivethe drawing height the source statesnull
unitsstring or nullthe source’s own name for its canvas unitsnull

GeoFeature

Schema definition: GeoFeature.

fieldtypeunitsigninvariantif absent
targettoken bus, branch, or substationthe element family the feature placesrequired
keyElementKeyat least one member names an element, unless a branch states both endpointsrequired
geometryGeoGeometryevery coordinate is finiterequired
fromstring or nulla branch’s endpoint bus, the unordered fallback identitynull
tostring or nullthe other endpoint busnull
kindtoken as GeoLayer.kind, or nullnull (the layer default)

GeoGeometry is one tagged object: point is a single [x, y] position for a placed element, and line_string is an array of [x, y] positions for a route. Positions are in the layer’s coordinate space, a route has at least one, and every coordinate is finite.

ElementKey

Matching tries uid, then id, then case insensitive name; a branch additionally falls back to the unordered (from, to) bus pair.

Schema definition: ElementKey.

fieldtypeunitsigninvariantif absent
uidstring or nullthe durable identity (buses:3, branches:7)null
idstring or nullthe source’s own element identifiernull
namestring or nullmatched case insensitivelynull
indexinteger or nullpositive1-based row alias, accepted on read and never writtennull

powerio.OperatingPoint<N>

An alternate electrical assignment, possibly partial, over the fixed component identities of one base network N: demand, setpoints, dispatch, voltages, injections, service status, switch positions, transformer taps, and phase shifts. Any quantity the point leaves out resolves to the network’s own value.

Schema definitions: StoredOperatingPoint, StoredOperatingPoint2.

fieldtypeunitsigninvariantif absent
networkBalancedNetwork or MulticonductorNetwork, as the type name saysthe complete base networkrequired
quantitiesmap of quantity name to StoredQuantityper quantity, belowkeys among the quantity names of the network familyrequired

Schema definition: StoredQuantity.

fieldtypeunitsigninvariantif absent
identitiesarray of stringeach names a component of the network; unique; same length as valuesrequired
valuesarray of floatthe quantity’s unit; a flag is 0 or 1required

An operating point stored inside a collection or an instance omits the network, because the enclosing record gives it once.

Schema definition: StoredOperatingPointAssignment.

fieldtypeunitsigninvariantif absent
quantitiesmap of quantity name to StoredQuantityas StoredOperatingPoint.quantitiesrequired

Balanced quantities. A bus quantity is keyed by the bus id’s decimal spelling; an element quantity by the element’s uid.

quantitykeyed byunitsign
bus_voltage_magnitudebus idp.u.
bus_voltage_anglebus iddegrees
bus_active_injectionbus idMWpositive into the network
bus_reactive_injectionbus idMVArpositive into the network
generator_active_powergenerator uidMWpositive is generation
generator_reactive_powergenerator uidMVArpositive is generation
generator_voltage_setpointgenerator uidp.u.
generator_in_servicegenerator uidflag
load_active_powerload uidMWpositive is consumption
load_reactive_powerload uidMVArpositive is consumption
branch_in_servicebranch uidflag
branch_tap_ratiobranch uidratio
branch_phase_shiftbranch uiddegreesas Branch.shift
switch_closedswitch uidflag

Multiconductor quantities. A terminal quantity is keyed bus_id/terminal, a per phase element quantity element_name/terminal, and a whole element quantity by the element name.

quantitykeyed byunitsign
terminal_voltage_magnitudebus/terminalvolts
terminal_voltage_anglebus/terminalradians
load_active_powerload/terminalwattspositive is consumption
load_reactive_powerload/terminalvarspositive is consumption
generator_active_powergenerator/terminalwattspositive is generation
generator_reactive_powergenerator/terminalvarspositive is generation
transformer_taptransformer nameratio
capacitor_stepscapacitor namecount
switch_closedswitch nameflag

powerio.TimeSeries<T>

Ordered time points and one value of T per point. Labels are the source’s own; PowerIO imposes no calendar meaning.

Schema definitions: StoredTimeSeries, StoredTimeSeries2.

fieldtypeunitsigninvariantif absent
time_pointsarray of TimePointrequired
valuesarray of the element type, each a complete networksame length as time_pointsrequired

A series of operating points gives the shared base network once.

Schema definitions: StoredOperatingPointTimeSeries, StoredOperatingPointTimeSeries2.

fieldtypeunitsigninvariantif absent
networkthe base network, or nullnull only for an empty seriesrequired when values is nonempty
time_pointsarray of TimePointrequired
valuesarray of StoredOperatingPointAssignmentsame length as time_points; identities name components of networkrequired

Schema definition: TimePoint.

fieldtypeunitsigninvariantif absent
labelstringnonempty, boundedrequired
durationDuration or nullthe interval the point coversnull

Schema definition: Duration.

fieldtypeunitsigninvariantif absent
secsintegersecondsrequired
nanosintegernanosecondsbelow one billionrequired

powerio.ScenarioSet<T>

Named alternatives of T with no implied order. Either every scenario has a probability or none does; when present, the probabilities are nonnegative and sum to one within SCENARIO_PROBABILITY_TOLERANCE.

Schema definitions: StoredScenarioSet, StoredScenarioSet2, StoredScenarioSet3, StoredScenarioSet4, StoredScenarioSet5, StoredScenarioSet6.

fieldtypeunitsigninvariantif absent
scenariosarray of the scenario recordid uniquerequired

Schema definitions: StoredScenario, StoredScenario2, StoredScenario3, StoredScenario4, StoredScenario5, StoredScenario6.

fieldtypeunitsigninvariantif absent
idstringnonempty, bounded, unique in the setrequired
probabilityfloat or nullin [0, 1]null
valuethe element type: a network, a network time series, or an operating point time seriesrequired

A scenario set of operating points gives the shared base network once.

Schema definitions: StoredOperatingPointScenarioSet, StoredOperatingPointScenarioSet2.

fieldtypeunitsigninvariantif absent
networkthe base network, or nullnull only for an empty setrequired when scenarios is nonempty
scenariosarray of StoredOperatingPointScenarioid uniquerequired

Schema definition: StoredOperatingPointScenario.

fieldtypeunitsigninvariantif absent
idstringnonempty, bounded, unique in the setrequired
probabilityfloat or nullin [0, 1]null
quantitiesmap of quantity name to StoredQuantityidentities name components of networkrequired

Instances

An instance is the complete input to one named calculation, including the network it runs over. An instance may also include an initial_point, a starting operating assignment a solver may use, whose identities refer to components of the instance’s network.

powerio.DcPfInstance

Schema definition: DcPfInstance.

fieldtypeunitsigninvariantif absent
networkBalancedNetworkat least one REF busrequired
approximationtoken series_susceptance, tap_adjusted_reactance, reactance_onlythe DC branch susceptance formularequired
initial_pointStoredOperatingPointAssignment or nullnull

The bus specifications are derived from the network: a REF bus contributes its stated angle, an ISOLATED bus no equation, and every other bus its net active injection over in service generators and loads.

powerio.AcPfInstance

Schema definition: AcPfInstance.

fieldtypeunitsigninvariantif absent
networkBalancedNetworkat least one REF busrequired
specificationsarray of AcBusSpecificationone per bus, in bus table orderrequired
initial_pointStoredOperatingPointAssignment or nullnull

AcBusSpecification is tagged by kind. pq gives p (MW) and q (MVAr), the prescribed net injection; pv gives p and vm (p.u.); reference gives vm and va (degrees); isolated gives nothing. Powers are net injections into the network, positive for generation.

powerio.DcOpfInstance

Schema definition: DcOpfInstance.

fieldtypeunitsigninvariantif absent
networkBalancedNetworkat least one REF bus and one in service generator on a non isolated busrequired
objectiveObjectiverequired
constraintsActiveConstraintsrequired
approximationtoken as DcPfInstance.approximationrequired
initial_pointStoredOperatingPointAssignment or nullnull

powerio.AcOpfInstance

Schema definition: AcOpfInstance.

fieldtypeunitsigninvariantif absent
networkBalancedNetworkat least one REF bus and one in service generator on a non isolated busrequired
objectiveObjectiverequired
constraintsActiveConstraintsrequired
initial_pointStoredOperatingPointAssignment or nullnull

Schema definition: Objective.

fieldtypeunitsigninvariantif absent
termsarray of ObjectiveTermempty is a feasibility problem[]

ObjectiveTerm is tagged by term: network_generator_cost sums the network’s generator cost curves; active_power_dispatch_cost prices active dispatch.

Schema definition: ActiveConstraints.

fieldtypeunitsigninvariantif absent
generator_capabilityConstraintSelectionactive and reactive generator limitsrequired
voltage_boundsConstraintSelectionbus voltage magnitude boundsrequired
thermal_limitsConstraintSelectionbranch thermal limitsrequired
angle_boundsConstraintSelectionbranch angle difference boundsrequired

ConstraintSelection is tagged by select: all (every element with a stated limit), none (the family is relaxed), or only with identities, the list of element identities (uid values, or bus ids) the family applies to.

powerio.McAcPfInstance

Schema definition: McAcPfInstance.

fieldtypeunitsigninvariantif absent
networkMulticonductorNetworkat least one voltage sourcerequired
initial_pointStoredOperatingPointAssignment or nullnull

The prescribed terminal powers, source voltages, and active regulator and capacitor controls are derived from the network.

powerio.McAcOpfInstance

Schema definition: McAcOpfInstance.

fieldtypeunitsigninvariantif absent
networkMulticonductorNetworkat least one voltage sourcerequired
objectiveObjectiverequired
constraintsMulticonductorActiveConstraintsrequired
initial_pointStoredOperatingPointAssignment or nullnull

Schema definition: MulticonductorActiveConstraints.

fieldtypeunitsigninvariantif absent
terminal_voltage_boundsConstraintSelectionrequired
conductor_limitsConstraintSelectioncurrent or apparent power limitsrequired
generator_capabilityConstraintSelectionper phase boundsrequired

powerio.AcScucInstance

The DOE GO Challenge 3 formulation: one balanced network plus scheduling, reserve, and contingency inputs. Powers in the inputs are per unit on the network’s base_mva, times are hours from the start of the horizon, and costs are dollars or dollars per per unit hour, as the data format gives them.

Schema definition: AcScucInstance.

fieldtypeunitsigninvariantif absent
networkBalancedNetworkrequired
inputsScucInputsidentities name components of network; time varying records match the horizonrequired

Schema definition: ScucInputs.

fieldtypeunitsigninvariantif absent
interval_durationsarray of floathourspositive; chronologicalrequired
devicesarray of ScucDeviceid uniquerequired
active_reserve_zonesarray of ScucActiveReserveZonerequired
reactive_reserve_zonesarray of ScucReactiveReserveZonerequired
contingenciesarray of ScucContingencyrequired
shuntsarray of ScucShuntrequired
transformer_controlsarray of ScucTransformerControlrequired
branch_switching_costsarray of ScucBranchSwitchingCostrequired
violation_costsScucViolationCostsrequired

Schema definition: ScucDevice.

fieldtypeunitsigninvariantif absent
idComponentIdthe generator or load in the networkrequired
kindtoken producer, consumerrequired
on_costfloatdollarsrequired
startup_costfloatdollarsrequired
shutdown_costfloatdollarsrequired
initial_on_statusbooleanrequired
initial_commitmentScucInitialCommitmentrequired
minimum_up_timefloathoursrequired
minimum_down_timefloathoursrequired
ramp_limitsScucRampLimitsrequired
reserve_limitsScucReserveLimitsrequired
reactive_capabilityScucReactiveCapabilityrequired
energy_lower_boundsarray of ScucEnergyRequirementrequired
energy_upper_boundsarray of ScucEnergyRequirementrequired
startup_cost_adjustmentsarray of ScucStartupCostAdjustmentrequired
startup_limitsarray of ScucStartupLimitrequired
periodsarray of ScucDevicePeriodone per interval, chronologicalrequired

ScucReactiveCapability is tagged by kind: none; linear with reactive_power_at_zero_active_power and a slope; bounded with the _max and _min forms of both.

Schema definition: ScucDevicePeriod.

fieldtypeunitsigninvariantif absent
on_status_minbooleanrequired
on_status_maxbooleanrequired
active_power_minfloatp.u.active_power_min <= active_power_maxrequired
active_power_maxfloatp.u.required
reactive_power_minfloatp.u.reactive_power_min <= reactive_power_maxrequired
reactive_power_maxfloatp.u.required
energy_cost_blocksarray of ScucEnergyCostBlockrequired
reserve_costsScucReserveCostsrequired

Schema definition: ScucEnergyCostBlock.

fieldtypeunitsigninvariantif absent
block_sizefloatp.u.nonnegativerequired
marginal_costfloatdollars per p.u. hourrequired

Schema definition: ScucReserveCosts.

fieldtypeunitsigninvariantif absent
regulation_upfloatdollars per p.u. hourrequired
regulation_downfloatdollars per p.u. hourrequired
synchronizedfloatdollars per p.u. hourrequired
nonsynchronizedfloatdollars per p.u. hourrequired
ramping_up_onlinefloatdollars per p.u. hourrequired
ramping_up_offlinefloatdollars per p.u. hourrequired
ramping_down_onlinefloatdollars per p.u. hourrequired
ramping_down_offlinefloatdollars per p.u. hourrequired
reactive_upfloatdollars per p.u. hourrequired
reactive_downfloatdollars per p.u. hourrequired

Schema definition: ScucReserveLimits.

fieldtypeunitsigninvariantif absent
regulation_upfloatp.u.nonnegativerequired
regulation_downfloatp.u.nonnegativerequired
synchronizedfloatp.u.nonnegativerequired
nonsynchronizedfloatp.u.nonnegativerequired
ramping_up_onlinefloatp.u.nonnegativerequired
ramping_up_offlinefloatp.u.nonnegativerequired
ramping_down_onlinefloatp.u.nonnegativerequired
ramping_down_offlinefloatp.u.nonnegativerequired

Schema definition: ScucRampLimits.

fieldtypeunitsigninvariantif absent
upfloatp.u. per hournonnegativerequired
downfloatp.u. per hournonnegativerequired
startupfloatp.u. per hournonnegativerequired
shutdownfloatp.u. per hournonnegativerequired

Schema definition: ScucInitialCommitment.

fieldtypeunitsigninvariantif absent
accumulated_up_timefloathoursnonnegativerequired
accumulated_down_timefloathoursnonnegativerequired

Schema definition: ScucEnergyRequirement.

fieldtypeunitsigninvariantif absent
start_timefloathoursstart_time <= end_timerequired
end_timefloathoursrequired
energyfloatp.u. hourrequired

Schema definition: ScucStartupCostAdjustment.

fieldtypeunitsigninvariantif absent
maximum_down_timefloathoursrequired
costfloatdollarsrequired

Schema definition: ScucStartupLimit.

fieldtypeunitsigninvariantif absent
start_timefloathoursstart_time <= end_timerequired
end_timefloathoursrequired
maximum_startupsintegerrequired

Schema definition: ScucActiveReserveZone.

fieldtypeunitsigninvariantif absent
idComponentIduniquerequired
busesarray of ComponentIdeach names a bus of the networkrequired
regulation_up_requirement_fractionfloatfraction of zone loadrequired
regulation_down_requirement_fractionfloatfraction of zone loadrequired
synchronized_requirement_fractionfloatfraction of zone loadrequired
nonsynchronized_requirement_fractionfloatfraction of zone loadrequired
ramping_up_requirementarray of floatp.u.one per intervalrequired
ramping_down_requirementarray of floatp.u.one per intervalrequired
regulation_up_violation_costfloatdollars per p.u. hourrequired
regulation_down_violation_costfloatdollars per p.u. hourrequired
synchronized_violation_costfloatdollars per p.u. hourrequired
nonsynchronized_violation_costfloatdollars per p.u. hourrequired
ramping_up_violation_costfloatdollars per p.u. hourrequired
ramping_down_violation_costfloatdollars per p.u. hourrequired

Schema definition: ScucReactiveReserveZone.

fieldtypeunitsigninvariantif absent
idComponentIduniquerequired
busesarray of ComponentIdeach names a bus of the networkrequired
reactive_up_requirementarray of floatp.u.one per intervalrequired
reactive_down_requirementarray of floatp.u.one per intervalrequired
reactive_up_violation_costfloatdollars per p.u. hourrequired
reactive_down_violation_costfloatdollars per p.u. hourrequired

Schema definition: ScucContingency.

fieldtypeunitsigninvariantif absent
idComponentIduniquerequired
componentsarray of ComponentIdChallenge 3 requires exactly one AC line, transformer, or DC linerequired

Schema definition: ScucShunt.

fieldtypeunitsigninvariantif absent
idComponentIdthe shunt in the networkrequired
initial_stepintegerstep_min <= initial_step <= step_maxrequired
step_minintegerrequired
step_maxintegerrequired
conductance_per_stepfloatp.u.required
susceptance_per_stepfloatp.u.positive is capacitiverequired

Schema definition: ScucTransformerControl.

fieldtypeunitsigninvariantif absent
idComponentIdthe two winding transformer in the networkrequired
tap_ratio_minfloatp.u.tap_ratio_min <= tap_ratio_maxrequired
tap_ratio_maxfloatp.u.required
phase_shift_minfloatradiansphase_shift_min <= phase_shift_maxrequired
phase_shift_maxfloatradiansrequired

Schema definition: ScucBranchSwitchingCost.

fieldtypeunitsigninvariantif absent
idComponentIdthe branch or transformer in the networkrequired
connection_costfloatdollarsrequired
disconnection_costfloatdollarsrequired

Schema definition: ScucViolationCosts.

fieldtypeunitsigninvariantif absent
active_power_balancefloatdollars per p.u. hourrequired
reactive_power_balancefloatdollars per p.u. hourrequired
branch_thermal_limitfloatdollars per p.u. hourrequired
energy_requirementfloatdollars per p.u. hourrequired

Solutions

A solution contains the instance it solves, and its arrays follow the instance network’s table order: one entry per bus, per branch, or per generator. Injections are net injections into the network at a bus, positive for generation; branch flows are measured into the branch at the named terminal. Each solution says how the calculation ended and what residuals the producer reported. producer is the producer’s free text solver identity, or null.

Termination is tagged by kind: converged, iteration_limit, infeasible, unbounded, failed, or not_reported, for a source that stores a solved calculation without termination information, as DeepMind OPFData does.

Schema definition: Residuals.

fieldtypeunitsigninvariantif absent
max_active_power_mismatchfloat or nullMWlargest absolute active balance mismatchnull (not reported)
max_reactive_power_mismatchfloat or nullMVArlargest absolute reactive balance mismatchnull (not reported)

Schema definition: GeneratorDispatch.

fieldtypeunitsigninvariantif absent
p_mwarray of floatMWpositive is generationone per generatorrequired
q_mvararray of floatMVArpositive is generationone per generator, or emptyrequired

Schema definition: ThreeWindingTransformerTerminalActivePower.

fieldtypeunitsigninvariantif absent
p_mwarray of floatMWpositive flows into the transformer at the windingthree entries, winding orderrequired

Schema definition: ThreeWindingTransformerTerminalPower.

fieldtypeunitsigninvariantif absent
p_mwarray of floatMWpositive flows into the transformer at the windingthree entries, winding orderrequired
q_mvararray of floatMVArpositive flows into the transformer at the windingthree entries, winding orderrequired

powerio.DcPfSolution

Schema definition: DcPfSolution.

fieldtypeunitsigninvariantif absent
instanceDcPfInstancerequired
terminationTerminationrequired
residualsResidualsrequired
producerstring or nullnull
bus_voltage_anglearray of floatdegreesone per busrequired
bus_active_injectionarray of floatMWpositive into the networkone per busrequired
branch_from_active_flowarray of floatMWpositive into the branch at the from terminalone per branchrequired
branch_to_active_flowarray of floatMWpositive into the branch at the to terminalone per branchrequired
three_winding_transformer_terminal_active_powersarray of ThreeWindingTransformerTerminalActivePowerone per three winding transformerrequired
generator_dispatchGeneratorDispatch or nullnull

powerio.AcPfSolution

Schema definition: AcPfSolution.

fieldtypeunitsigninvariantif absent
instanceAcPfInstancerequired
terminationTerminationrequired
residualsResidualsrequired
producerstring or nullnull
bus_voltage_magnitudearray of floatp.u.one per busrequired
bus_voltage_anglearray of floatdegreesone per busrequired
bus_active_injectionarray of floatMWpositive into the networkone per busrequired
bus_reactive_injectionarray of floatMVArpositive into the networkone per busrequired
branch_from_active_flowarray of floatMWpositive into the branch at the from terminalone per branchrequired
branch_from_reactive_flowarray of floatMVArpositive into the branch at the from terminalone per branchrequired
branch_to_active_flowarray of floatMWpositive into the branch at the to terminalone per branchrequired
branch_to_reactive_flowarray of floatMVArpositive into the branch at the to terminalone per branchrequired
three_winding_transformer_terminal_powersarray of ThreeWindingTransformerTerminalPowerone per three winding transformerrequired
generator_dispatchGeneratorDispatch or nullnull

powerio.DcOpfSolution

Multipliers and marginals are the optimizing producer’s optional economic outputs, in the objective’s units per MW; no currency is assumed.

Schema definition: DcOpfSolution.

fieldtypeunitsigninvariantif absent
instanceDcOpfInstancerequired
terminationTerminationrequired
residualsResidualsrequired
producerstring or nullnull
bus_voltage_anglearray of floatdegreesone per busrequired
bus_active_injectionarray of floatMWpositive into the networkone per busrequired
branch_from_active_flowarray of floatMWpositive into the branch at the from terminalone per branchrequired
branch_to_active_flowarray of floatMWpositive into the branch at the to terminalone per branchrequired
generator_active_powerarray of floatMWpositive is generationone per generatorrequired
three_winding_transformer_terminal_active_powersarray of ThreeWindingTransformerTerminalActivePowerone per three winding transformerrequired
objectivefloatobjective unitsrequired
bus_active_power_marginalarray of float or nullobjective units per MWone per busnull
branch_from_limit_multiplierarray of float or nullobjective units per MWone per branch; finite and nonnegativenull
branch_to_limit_multiplierarray of float or nullobjective units per MWone per branch; finite and nonnegativenull

powerio.AcOpfSolution

Schema definition: AcOpfSolution.

fieldtypeunitsigninvariantif absent
instanceAcOpfInstancerequired
terminationTerminationrequired
residualsResidualsrequired
producerstring or nullnull
bus_voltage_magnitudearray of floatp.u.one per busrequired
bus_voltage_anglearray of floatdegreesone per busrequired
bus_active_injectionarray of floatMWpositive into the networkone per busrequired
bus_reactive_injectionarray of floatMVArpositive into the networkone per busrequired
branch_from_active_flowarray of floatMWpositive into the branch at the from terminalone per branchrequired
branch_from_reactive_flowarray of floatMVArpositive into the branch at the from terminalone per branchrequired
branch_to_active_flowarray of floatMWpositive into the branch at the to terminalone per branchrequired
branch_to_reactive_flowarray of floatMVArpositive into the branch at the to terminalone per branchrequired
generator_active_powerarray of floatMWpositive is generationone per generatorrequired
generator_reactive_powerarray of floatMVArpositive is generationone per generatorrequired
three_winding_transformer_terminal_powersarray of ThreeWindingTransformerTerminalPowerone per three winding transformerrequired
objectivefloatobjective unitsrequired
bus_active_power_marginalarray of float or nullobjective units per MWone per busnull
bus_reactive_power_marginalarray of float or nullobjective units per MVArone per busnull
branch_from_limit_multiplierarray of float or nullobjective units per MVAone per branch; finite and nonnegativenull
branch_to_limit_multiplierarray of float or nullobjective units per MVAone per branch; finite and nonnegativenull

powerio.SocwrOpfSolution

The PowerModels SOCWR relaxation of an AcOpfInstance. Its objective is a lower bound, and its voltage products make no claim of an AC feasible phasor.

Schema definition: SocwrOpfSolution.

fieldtypeunitsigninvariantif absent
instanceAcOpfInstancerequired
terminationTerminationrequired
residualsResidualsrequired
producerstring or nullnull
valuesSocwrOpfValuesrequired
dualsSocwrOpfDualsevery member null
objective_lower_boundfloatobjective unitsrequired

Schema definition: SocwrOpfValues.

fieldtypeunitsigninvariantif absent
bus_voltage_magnitude_squaredarray of floatp.u. squaredw[i] = |V_i|^2; one per busrequired
branch_voltage_product_realarray of floatp.u. squaredRe(V_from conj(V_to)); one per branchrequired
branch_voltage_product_imaginaryarray of floatp.u. squaredIm(V_from conj(V_to)); one per branchrequired
generator_active_powerarray of floatMWpositive is generationone per generatorrequired
generator_reactive_powerarray of floatMVArpositive is generationone per generatorrequired
branch_from_active_powerarray of floatMWpositive into the branch at the from terminalone per branchrequired
branch_from_reactive_powerarray of floatMVArpositive into the branch at the from terminalone per branchrequired
branch_to_active_powerarray of floatMWpositive into the branch at the to terminalone per branchrequired
branch_to_reactive_powerarray of floatMVArpositive into the branch at the to terminalone per branchrequired
three_winding_transformer_terminal_powersarray of ThreeWindingTransformerTerminalPowerone per three winding transformerrequired

Schema definition: SocwrOpfDuals.

fieldtypeunitsigninvariantif absent
bus_active_power_marginalarray of float or nullobjective units per MWone per busnull
bus_reactive_power_marginalarray of float or nullobjective units per MVArone per busnull
branch_from_thermal_limit_multiplierarray of float or nullobjective units per MVAone per branch; finite and nonnegativenull
branch_to_thermal_limit_multiplierarray of float or nullobjective units per MVAone per branch; finite and nonnegativenull

powerio.McAcPfSolution

Terminal columns run in bus table order and, within a bus, in the bus’s stated terminal order. Source columns run in source table order and, within a source, in its terminal_map order.

Schema definition: McAcPfSolution.

fieldtypeunitsigninvariantif absent
instanceMcAcPfInstancerequired
terminationTerminationrequired
residualsResidualsrequired
producerstring or nullnull
terminal_voltage_magnitudearray of floatvoltsone per terminalrequired
terminal_voltage_anglearray of floatradiansone per terminalrequired
terminal_current_magnitudearray of float or nullamperesone per terminalnull
terminal_active_powerarray of float or nullwattspositive into the networkone per terminalnull
source_active_injectionarray of floatwattspositive into the networkone per source terminalrequired

powerio.McAcOpfSolution

Schema definition: McAcOpfSolution.

fieldtypeunitsigninvariantif absent
instanceMcAcOpfInstancerequired
terminationTerminationrequired
residualsResidualsrequired
producerstring or nullnull
terminal_voltage_magnitudearray of floatvoltsone per terminalrequired
terminal_voltage_anglearray of floatradiansone per terminalrequired
terminal_current_magnitudearray of float or nullamperesone per terminalnull
terminal_active_powerarray of float or nullwattspositive into the networkone per terminalnull
source_active_injectionarray of floatwattspositive into the networkone per source terminalrequired
generator_active_powerarray of floatwattspositive is generationgenerator table order, each generator’s terminal_map orderrequired
objectivefloatobjective unitsrequired

powerio.AcScucSolution

The GO Challenge 3 output fields. Every series is values[t][row] over the instance’s time points and the corresponding table order; a series a producer did not supply is empty.

Schema definition: AcScucSolution.

fieldtypeunitsigninvariantif absent
instanceAcScucInstancerequired
terminationTerminationrequired
residualsResidualsrequired
producerstring or nullnull
network_outputsScucNetworkOutputsrequired
device_outputsScucDeviceOutputsrequired
objectivefloat or nulldollarsnull

Schema definition: ScucNetworkOutputs.

fieldtypeunitsigninvariantif absent
bus_vmarray of array of floatp.u.one row per time point, one per busrequired
bus_vaarray of array of floatradiansone per busrequired
shunt_steparray of array of integerone per shuntrequired
ac_line_on_statusarray of array of booleanone per AC linerequired
transformer_tmarray of array of floatratioone per two winding transformerrequired
transformer_taarray of array of floatradiansone per two winding transformerrequired
transformer_on_statusarray of array of booleanone per two winding transformerrequired
dc_line_pdc_frarray of array of floatp.u.positive from the from bus to the to busone per DC line[]
dc_line_qdc_frarray of array of floatp.u.positive is injected into the from busone per DC line[]
dc_line_qdc_toarray of array of floatp.u.positive is injected into the to busone per DC line[]

Schema definition: ScucDeviceOutputs.

fieldtypeunitsigninvariantif absent
on_statusarray of array of booleanone row per time point, one per devicerequired
startup_statusarray of array of booleanrequired
shutdown_statusarray of array of booleanrequired
p_onarray of array of floatp.u.positive is production for a producer, consumption for a consumerrequired
qarray of array of floatp.u.as p_onrequired
p_reg_res_uparray of array of floatp.u.nonnegativerequired
p_reg_res_downarray of array of floatp.u.nonnegativerequired
p_syn_resarray of array of floatp.u.nonnegativerequired
p_nsyn_resarray of array of floatp.u.nonnegativerequired
p_ramp_res_up_onlinearray of array of floatp.u.nonnegativerequired
p_ramp_res_up_offlinearray of array of floatp.u.nonnegativerequired
p_ramp_res_down_onlinearray of array of floatp.u.nonnegativerequired
p_ramp_res_down_offlinearray of array of floatp.u.nonnegativerequired
q_res_uparray of array of floatp.u.nonnegativerequired
q_res_downarray of array of floatp.u.nonnegativerequired

Module records

The records beside value in a .pio.json document. .pio.json schema explains what each one is for; the tables here list their fields.

Schema definition: Producer.

fieldtypeunitsigninvariantif absent
namestringnonempty, boundedrequired
versionstringnonempty, boundedrequired

Schema definition: SourceDescriptor.

fieldtypeunitsigninvariantif absent
idstringunique among sources; spans and source map entries name itrequired
namestringa file name, never a local file pathrequired
byte_lengthintegerbytesevery span into this source ends at or before itrequired
formatstring or nulla format tokennull
digestDigest or nullnull

Schema definition: Digest.

fieldtypeunitsigninvariantif absent
algorithmtoken sha256required
valuestring64 lowercase hexadecimal charactersrequired

Schema definition: SourceSpan.

fieldtypeunitsigninvariantif absent
sourcestringnames a source idrequired
byte_startintegerbytes into the retained sourcebyte_start <= byte_end <= byte_lengthrequired
byte_endintegerbytes into the retained sourcehalf openrequired

Schema definition: SourceMapEntry.

fieldtypeunitsigninvariantif absent
targetstringan RFC 6901 pointer into value.datarequired
relationtoken exact, defaulted, inferred, converted_units, aggregated, split, synthetic, transformed, retained_extrarequired
spansarray of SourceSpanempty only for defaulted, synthetic, and transformed[]

Schema definition: Diagnostic.

fieldtypeunitsigninvariantif absent
idstringunique among diagnostics; assigned d0, d1, … at serialization when a record has nonerequired
codestringNAMESPACE.SCOPE.SPECIFICrequired
severitytoken error, warning, remark, noterequired
messagestringone linerequired
targetstring or nullan RFC 6901 pointer into value.datanull
spansarray of SourceSpanthe records the finding is about[]
relatedarray of stringeach names a diagnostic id in the document[]
detailsobject{}
suggested_actionstring or nullnull

Schema definition: HistoryEntry.

fieldtypeunitsigninvariantif absent
idstringunique among historyrequired
kindtoken parse, transform, edit, repair, solverequired
namestringthe operationrequired
input_typestring or nulla structural type namenull
output_typestring or nulla structural type namenull
parametersobject{}
assumptionsarray of string[]
lossesarray of string[]

BMOPF field mapping

This page says what each BMOPF field becomes in PowerIO, in which unit, and which constraint or objective term it enters. It is the authority for the BMOPF converter, so if you find a field that reads or writes differently from what is written here, one of the two has a defect.

Schema versions come from the dsopt-schema repository. 0.1.0 is the version the IEEE PES Task Force on Benchmarking Multiconductor OPF accepts, and 0.2.0 is the proposal that adds the element classes 0.1.0 has no table for. PowerIO reads both and writes 0.2.0 by default; to write 0.1.0, pass BmopfEmitOptions::with_schema_version(BmopfSchemaVersion::Bmopf010).

Conventions

Every BMOPF quantity is SI and absolute: volts, amperes, watts, vars, volt-amperes, ohms, siemens, metres, radians, hertz, and a cost rate in currency per kilowatt-hour. PowerIO stores the same units with no scaling, so a mapping below gives a unit only where the two names differ.

A matrix element A_k_j is row k, column j, counting from one, and lands in row k - 1, column j - 1 of the corresponding ConductorMatrix. These matrices are symmetric, so the reader fills an unstated transpose cell from its mirror; a stated cell wins over its mirror. A per terminal array has one entry per name in the element’s own terminal map, in that order, and PowerIO keeps that order.

An absent constraint field means no constraint and reads as None; an absent parameter field is zero. A field with no typed slot lands in the element’s extras, and the reader reports it under READ.BMOPF.RETAINED_SOURCE_ONLY. Fields with no typed slot lists every one.

Document level

BMOPFPowerIONote
namePioModule value name
meta.$schemaresolved by BmopfSchemaVersion::from_schema_idAbsent raises READ.BMOPF.SCHEMA_ABSENT; a value naming no version raises READ.BMOPF.SCHEMA_UNKNOWN. Both parse, and both versions are accepted.
meta.schema_versionexplicit schema versionThe reader checks agreement with meta.$schema. Fresh proposal output pins a retrieval URL and records proposal status and schema digest in provenance.
meta.frequencyMulticonductorNetwork::base_frequency, HzAbsent defaults to 60 with READ.BMOPF.VALUE_DEFAULTED.
meta.* (the rest)MulticonductorNetwork::extras["bmopf_meta"]Re-emitted, except the three the writer owns.
terminal_conventionsMulticonductorNetwork::extras["bmopf_terminal_conventions"]Re-emitted verbatim; authored from the terminal names when the source states none.
extrasMulticonductorNetwork::extras["bmopf_extras"]Re-emitted verbatim, minus the tables the reader types out of it.

bus

DistBus, in MulticonductorNetwork::buses().

BMOPFPowerIONote
terminal_namesterminalsOrdered; fixes every per-terminal order on this bus.
perfectly_grounded_terminalsgrounded
v_min, v_maxv_min_phase, v_max_phase and scalar v_min, v_maxUnequal bounds remain ordered phase vectors through IR and bindings. A scalar edit explicitly overrides the vector; balanced lowering rejects unequal phase bounds.
vpn_min, vpn_maxvpn_min, vpn_maxPer phase terminal, kept as arrays.
vpp_min, vpp_maxvpp_min, vpp_maxPer ordered phase pair.
vpos_min, vpos_maxvpos_min, vpos_maxScalars.
vneg_max, vzero_maxvneg_max, vzero_maxMagnitude caps; the lower bound is always zero.
vn_maxvn_maxNeutral to ground cap.
longitude, latitudelocation and MulticonductorNetwork::geoThe BMOPFTools coordinate fields, outside both schema versions. Read into the coordinate space; written back only with BmopfEmitOptions::sideload_coordinates.

line and linecode

DistLine and DistLineCode.

BMOPFPowerIONote
line.bus_from, line.bus_tobus_from, bus_to
line.terminal_map_from, line.terminal_map_toterminal_map_from, terminal_map_toPosition i of the from map fixes matrix index i.
line.linecodelinecode
line.lengthlength, m
line.R_series_i_j, line.X_series_i_ja synthesized DistLineCode named after the line, ohm per metreThe inline branch states absolute ohms, so the reader divides by length to store per metre and keeps the line’s own length. READ.BMOPF.VALUE_INFERRED names the synthesis.
line.G_from_i_j, line.B_from_i_j, line.G_to_i_j, line.B_to_i_jthe same synthesized line code’s g_from, b_from, g_to, b_to
line.i_max, line.s_maxi_max, s_maxPer conductor; override the line code’s.
linecode.R_series_i_j, linecode.X_series_i_jr_series, x_series, ohm per metre
linecode.G_from_i_j, linecode.B_from_i_j, linecode.G_to_i_j, linecode.B_to_i_jg_from, b_from, g_to, b_to, siemens per metreHalf the total shunt at each end.
linecode.i_max, linecode.s_maxi_max, s_maxPer conductor, applied at both ends.
linecode.sourcesource
linecode.line_geometry, linecode.derivationextras0.2.0 fields; retained, not typed.

A line has exactly one impedance source, and the oneOf in both schema versions enforces it: either a linecode with a length, or inline R_series_1_1 and X_series_1_1 with no linecode.

switch

DistSwitch.

BMOPFPowerIONote
bus_from, bus_to, terminal_map_from, terminal_map_tothe same names
open_switchopen
i_maxi_maxPer conductor. A switch has no shunt, so both ends carry the same magnitude and one array bounds both.

load

DistLoad. Each array has one entry per branch of the load: a phase to neutral branch for WYE, the terminal pair for SINGLE_PHASE, or a line to line branch for DELTA.

BMOPFPowerIONote
bus, terminal_map, configurationthe same namesconfiguration reads case-insensitively; an unrecognized value reads as WYE with READ.BMOPF.VALUE_UNSUPPORTED.
p_nom, q_nomp_nom, q_nom, W and var
modelthe DistLoadVoltageModel variantCONSTANT_POWER, CONSTANT_CURRENT, CONSTANT_IMPEDANCE, ZIP, EXPONENTIAL.
v_nomthe variant’s v_nom
alpha_z, alpha_i, alpha_p, beta_z, beta_i, beta_pthe Zip variant’s fieldsThe three active fractions sum to one, and so do the three reactive.
gamma_p, gamma_qthe Exponential variant’s fields

generator

DistGenerator. Arrays are per phase conductor; WYE is the only configuration the specification supports.

BMOPFPowerIONote
bus, terminal_map, configurationthe same names
p_min, p_max, q_min, q_maxthe same names, W and varWhen a bound pair is equal the dispatch is pinned, and the reader states the same values as p_nom and q_nom so a power flow target has a setpoint.
s_maxs_max, VABounds the sum of squares of that phase’s active and reactive power.
i_maxi_max, APer phase, with an optional trailing entry bounding the neutral return current.
costcost, currency per kWhKept exactly as stated: one entry per phase. A bare scalar reads as a one-entry statement.

voltage_source

VoltageSource. Both versions permit exactly one.

BMOPFPowerIONote
bus, terminal_mapthe same names
v_magnitudev_magnitude, VPer terminal, phase to ground; a grounded terminal states zero.
v_anglev_angle, radPer terminal.
costextras["cost"]A 0.2.0 field; retained and re-emitted, not typed.
p_min, p_max, q_min, q_maxextrasThe same standing as cost.

shunt and capacitor

DistShunt is raw admittance, grounding impedance included; DistCapacitor is a bank with a nameplate rating.

BMOPFPowerIONote
shunt.bus, shunt.terminal_mapthe same names
shunt.G_i_j, shunt.B_i_jg, b, total siemens in conductor order
capacitor.bus, capacitor.terminal_map, capacitor.configurationthe same names
capacitor.q_ratedq_rated, varThe whole bank, not one element.
capacitor.v_nomv_nom, VLine to line for the three-phase configurations; across the element terminals for SINGLE_PHASE.

transformer

DistTransformer with windings: Vec<DistWinding>. The BMOPF subtype is kept in DistTransformer::extras["bmopf_subtype"], because the winding list alone does not pin down every subtype; a centre tap unit, for example, reads as two secondary windings.

BMOPFPowerIONote
bus_from, bus_towindings[0].bus, windings[1].bus
terminal_map_from, terminal_map_tothe corresponding terminal_mapcenter_tap expands the three to-side terminals into two windings.
v_nom_from, v_nom_towindings[k].v_ref, VFor center_tap, v_nom_to is the per leg voltage.
s_ratingwindings[k].s_rating, VA
r_series_from, r_series_towindings[k].r_pct, percent of that winding’s own baseThe base is n_phases * v_ref^2 / s_rating.
x_series_from, x_series_toxsc_pct, percentThe short-circuit test measures the series sum referred to one side, which is what this field is.
r_series, x_series (three-phase legacy)windings[0].r_pct and xsc_pctThe lumped wye-side spelling; the delta winding is lossless in this model.
tap_ratio (0.2.0), tap (retained under 0.1.0)windings[0].tap / windings[1].tapThe multiplier on the nameplate turns ratio.
tap_ratio_min, tap_ratio_maxextrasBounds have no typed winding slot.
r_neutral_from, x_neutral_fromwindings[0].r_neutral, x_neutral, ohmThe winding’s own neutral to earth branch.
r_neutral_to, x_neutral_towindings[1].r_neutral, x_neutral
g_no_load, b_no_loadextrasThe magnetising branch has no typed slot.
i_max_from, i_max_toextrasPer winding conductor of that side, in that side’s own amperes.
n_winding.windings[]one DistWinding eachbus, terminal_map, v_nom, configuration, r_winding, delta_roll, i_max.
n_winding.x_scxsc_pct, ordered 12, 13, ..., 1n, 23, ..., (n-1)nKeyed i_j with i < j in BMOPF, all referred to winding 1.
single_phase_autotransformer, open_delta_regulatorwindings plus extras["bmopf_subtype"]The regulator ratio, its bounds, the ANSI type and the open delta connection ride in extras.

Under schema 0.1.0, the nine fields that have no subtype slot (tap, tap_min, tap_max, the four winding neutral fields, and the two no-load fields) are written to extras.transformer.<subtype>.<name> and folded back on read, with each move reported under EMIT.BMOPF.RETAINED_SOURCE_ONLY. Under 0.2.0 the subtypes declare all nine, so nothing moves; only the three tap names change, to tap_ratio, tap_ratio_min, and tap_ratio_max.

OpenDSS Xscarray populates every winding-pair reactance in this order. The reader applies scalar XHL/XHT/XLT updates at edit boundaries; regenerated four-or-more-winding records use the complete Xscarray.

ibr and control_profile

DistIbr and DistControlProfile. Both are typed under either version. Because 0.1.0 has no top-level table for them, that version writes them under extras; on read they come from either place, and the top-level copy wins.

BMOPFPowerIONote
bus, terminal_mapthe same names
topologytopology: SINGLE_PHASE, THREE_LEG, FOUR_LEG
prime_moverprime_mover: PV, BATTERY, GENERIC, STATCOM, DSTATCOM
s_maxs_max, VA per phase
i_maxi_max, A per conductor
p_availp_avail, W
p_min, p_max, q_min, q_maxthe same names, per phase
control_profilecontrol_profile, an id
voltage_aggregationvoltage_aggregation: PER_PHASE, AVERAGE
costextras["cost"]Retained and re-emitted, not typed.
dc_bus, dc_terminal_map, dc_control, dc_v_set, dc_p_ref, dc_droop, dc_deadband, dc_link_coupled, p_dc_min, p_dc_maxextrasThe DC coupling fields; retained, not typed.
r_filter, x_filter, b_filter_shunt, grid_forming, v_ref_internalextrasRetained, not typed.
control_profile.power_factor.pfPowerFactorControl
control_profile.volt_var.*VoltVarControl: voltage_reference, breakpoints, q_limits, q_unit, q_ref, and the two active-power thresholds
control_profile.volt_watt.*VoltWattControl: voltage_reference, breakpoints, p_limits, p_unit, p_ref

Fields with no typed slot

The classes below read into MulticonductorNetwork::untyped() by class and name, with their properties kept as text, and are written back to the table the target version declares: the top level under 0.2.0, extras under 0.1.0. Reading each one reports READ.BMOPF.RETAINED_SOURCE_ONLY.

dc_bus, dc_branch, dc_grounding, dc_load, dc_source, time_series, wire_data, line_geometry, and a per-element time_series reference map.

They pass through and re-emit unchanged. Nothing reads their values into a calculation, so if a case’s behaviour depends on them, the instance PowerIO builds from it does not represent that case. The reader says so per class rather than leaving you to discover it.

The calculation

powerio::to_mc_ac_opf_instance builds a McAcOpfInstance through McAcOpfInstance::from_network. The instance shares the network rather than copying it: it selects which of the network’s stated limits are active constraints and which objective terms are summed, and reads the numbers from the network.

Objective. There is one term, ObjectiveTerm::ActivePowerDispatchCost, which is the specification’s default objective: the sum over every dispatchable element and every phase of that phase’s cost rate against the active power the element injects into the network. A positive cost minimises the element’s injection and a negative cost maximises it, and that holds the same way for a generator, the voltage source, and an IBR. The rate comes from the element’s own per phase cost array: a generator reads it from DistGenerator::cost, and a voltage source or IBR from its extras["cost"]. ObjectiveTerm::NetworkGeneratorCost is the term for a balanced network and is not part of a BMOPF instance.

Constraints. MulticonductorActiveConstraints selects three families, each ConstraintSelection::All by default, which matches the specification’s rule that constraints are active for the elements present.

FamilyThe BMOPF fields it activates
terminal_voltage_boundsbus.v_min, v_max, vpn_min, vpn_max, vpp_min, vpp_max, vpos_min, vpos_max, vneg_max, vzero_max, vn_max
conductor_limitsline.i_max, line.s_max, linecode.i_max, linecode.s_max, switch.i_max, transformer.i_max_from, transformer.i_max_to
generator_capabilitygenerator.p_min, p_max, q_min, q_max, s_max, i_max, and the same bounds on an IBR

A family selects by stable element identity, so a study that relaxes one line’s thermal limit refers to that line instead of restating the bound. ConstraintSelection::None relaxes a whole family, and Only lists the elements whose limits stay active.

Equipment behaviour has no selection family, because there is no limit to relax: a closed switch equates its two ends conductor by conductor, an open switch carries no current, the ideal winding pair relates its two coil voltages by the turns ratio and balances ampere-turns, and the voltage source fixes its terminal voltage with a free current. Those hold in every instance built from a network that includes them.

The solution

Solved values stay in McAcOpfSolution and are not written back into the network, so the network still describes the case and the solution describes one result of it.

McAcOpfSolutionUnit and order
terminal_voltage_magnitudeV, resolved terminal order
terminal_voltage_anglerad, resolved terminal order
terminal_current_magnitudeA, resolved terminal order, when the solver reports it
terminal_active_powerW, resolved terminal order, when the solver reports it
source_active_injectionW, per source terminal
generator_active_powerW, generator table order with each generator’s terminal map order
objectivethe optimised objective value
termination, residuals, producerhow the solve ended, its residuals, and what produced it

Checked against what

The reference data is the two published example networks, vendored at tests/data/dist/bmopf/. powerio-dist/tests/bmopf.rs checks that each one parses, that writing the result validates against the schema of the version written, that a second write is identical to the first, and that parsing the written document reproduces the model. The equations above were checked against the specification pages of math-and-data-model-specifications rather than inferred from the data.

BMOPFTools.jl is publicly available and is identified as the generator of example_ieee13.json. The PowerIO 0.11 compatibility change exercises the typed module API, explicit legacy schema version, retained diagnostics, transformer core-shunt locations and nominal n-winding imports. Its numerical suite compares power-flow voltages and transformer admittance with OpenDSS, including independently prepared BMOPF cases and native OpenDSS conversions.

PowerIO’s structural tests additionally check triangular matrix completion, conductor order, regulator fields, proposal provenance and rejected malformed records. These checks distinguish data preservation from the equations a particular solver supports; retaining a regulator or n-winding record does not imply that PowerIO’s own matrix compiler implements it.

Explicit terminal-coil no-load admittance

transformer.<subtype>.<id>.no_load_shunt holds {winding, g, b} in transformer extras and generation-2 IR. The one-based winding index fixes its physical location, and g + j b is siemens per coil at the terminal voltage. It cannot coexist with the existing from-side g_no_load and b_no_load fields.

OpenDSS and PMD exciting-branch percentages map to winding 2 with a negative magnetizing susceptance. Conversion uses the actual tapped WYE phase-to-neutral or DELTA phase-to-phase coil voltage and divides total transformer VA by the phase count. A winding-2 shunt converts back to those percentages; other locations report that this target parameterization cannot represent them. Legacy BMOPF output preserves the object under extras.transformer and reports the relocation. Nonzero core shunts reject the limited PowerIO passive transformer matrix profile before execution.

The independent check evals/validation/validate_bmopf_core_shunts.py compares six transformer topologies with OpenDSS Yprim through an intermediate PowerIO IR document. BMOPFTools’ 0.11 adapter uses an equivalent bus-shunt matrix and retains the original coil object in provenance. Successful parsing alone does not establish support for a transformer calculation.

Crate graph and architecture map

The workspace is layered. Shared infrastructure sits below every parser and a thin facade sits on top; no lower crate depends on the facade, and the two network crates do not depend on each other.

powerio-core          Source, FormatId, Diagnostic, Error, PioModule<T>,
                      TimePoint, TimeSeries<T>, ScenarioSet<T>, Destination
├── powerio-tx        BalancedNetwork and the balanced readers and writers
└── powerio-dist      MulticonductorNetwork and the OpenDSS, PMD, and BMOPF converters

powerio-prob          operating points, updates, the seven instances, the eight
                      solutions, and GO Challenge 3, OPFData, and BMOPF assembly;
                      depends on core, tx, and dist; matrix free
powerio-matrix        sparse matrices and graph data for both network families;
                      depends on core, tx, dist, and prob
powerio               PioValue, parse, emit, serialize, deserialize, and the
                      re-exports; depends on core, tx, dist, and prob, and on
                      matrix behind the `matrix` feature
powerio-cli, powerio-capi, powerio-py    over the facade

A change has to keep that shape. powerio-core owns no electrical type, parser, matrix, instance, or IR record layout. powerio-tx and powerio-dist stay usable on their own, so a distribution consumer pulls in neither the balanced model nor the matrix stack. powerio-prob does not depend on powerio-matrix. The facade re-exports the component crates, which is why cargo add powerio gets you everything, and its matrix feature adds the matrix crate without creating a cycle. CI checks these edges against cargo metadata, so the map below and the manifests cannot drift apart.

Components

The PowerIO component map: powerio-core at the bottom; powerio-tx and powerio-dist as independent siblings above it; powerio-prob over both networks; powerio-matrix over the networks and prob; the powerio facade over the component crates with an optional matrix feature edge; powerio-cli, powerio-capi, and powerio-py over the facade; solvers consume instances from powerio-prob and the GridFM pipeline reads Parquet datasets from powerio-matrix.

The facade does not force a consumer to depend on every component crate, and the two network crates are siblings with no edge between them. Solvers consume instances but sit outside PowerIO, and their workspaces and numerical caches stay outside with them.

Data flow

The PowerIO data flow: a Source of named immutable bytes enters parse, which produces a PioModule holding the typed value, retained source, diagnostics, and history; time series and scenario sets expose owner rooted typed entries through indexing and iteration; to_balanced derives a balanced module from a multiconductor module with reported assumptions and losses; calc operations return matrices and vectors with element mappings; emit produces grid exchange formats; serialize and deserialize connect the module to PowerIO IR generation 2.

Every arrow out of the module is an explicit call that returns diagnostics; nothing is transformed as a side effect of something else.

The diagram sources are in docs/diagrams/*.dot. scripts/check-architecture-map.py checks the drawn crate edges against cargo metadata and, with --render, regenerates the images.

LLVM and MLIR lessons

PowerIO’s design borrows from LLVM and MLIR where their problems overlap with reading, transforming, and writing power system data. This page goes through each lesson taken and where the shipped design applies it, then the mechanisms left out on purpose. Primary references: the MLIR language reference, diagnostics, interfaces, pass management, and dialect definition documents.

Adopted

A small shared foundation under acyclic higher layers. LLVM’s library layering puts Support under IR under the producers. PowerIO’s powerio-core owns sources, diagnostics, errors, the module, and the generic containers; the network crates, the calculation crate, and the matrix crate stack over it in one direction, and CI asserts the edges from cargo metadata (Crate graph).

Source ownership that survives parsing. MLIR’s source manager keeps buffers alive so locations mean something after parsing. A PioModule retains its source, and same format emission returns those bytes unchanged. A diagnostic has a source identifier plus a byte range into those exact bytes, end to end, and the MATPOWER and PSS/E readers attach the range of the record a finding is about, for the failure that ends a read and for warnings alike; the other readers attach no span yet.

Deterministic serialization. serialize is a function of the module alone, so one module serializes to identical text every time, and serializing the module that text deserializes to reproduces the text. Members are written in a fixed order (record fields in declaration order, map keys sorted), diagnostic IDs are minted d0, d1, … in record order for records that have none, and every float is written in the shortest decimal form that reads back to the same value. Two equal modules therefore compare equal as documents, which is what a cache key, a golden file, or a content digest needs.

Entry points that acquire their own input, with configuration as a value. MLIR reads a module through one operation with three input kinds: parseSourceFile(filename, block, config) opens the named file itself, parseSourceFile(sourceMgr, ...) takes a source manager the caller built, and parseSourceString(text, block, config, sourceName) takes content whose name supplies diagnostic locations. LLVM’s object::createBinary has the same pair of a path and a buffer and “autodetect[s] the file type” rather than taking a format argument. PowerIO’s parse, emit, serialize, and deserialize take their input or output through IntoSource and IntoDestination, which accept a file or directory name, content already in memory, and a built Source or Destination, so the ordinary read is parse("case.raw") with one call and one failure point, and detection is the default. Optional configuration is a documented value, as in writeBytecodeToFile(op, os, config = {}) and spirv::serialize(module, binary, options = {}); Rust has no default arguments, so each operation is a pair, parse with parse_with_options.

Its own representation read separately from foreign formats. MLIR reads its own text and bytecode through parseSourceFile, which detects the encoding, and reaches foreign formats through the mlir-translate registry of named translations. PowerIO does the same. serialize and deserialize handle PowerIO IR, parse and emit translate grid exchange formats, and parse refuses a .pio.json by naming deserialize. A future binary IR encoding therefore goes behind deserialize, detected from its own leading bytes, rather than behind a new operation. The naming lines up as well. MLIR spells its own binary encoding spirv::serialize and spirv::deserialize, and LLVM reserves emit for producing a target file from IR (TargetMachine::addPassesToEmitFile).

One value set, so which value a format produces is data. MLIR has one IR and reads foreign formats through registered translations whose result is that IR, rather than a reading operation per category of input. PowerIO’s PioValue is that one set, so a geographic layer is a value in it, powerio.GeoLayer, and the canonical .geo.json, GeoJSON, aliased CSV or JSON records, headerless buscoords CSV, and a PowerWorld .pwd display all reach it through parse. There is no second reading path beside parse, so a document that is not a grid case still parses, serializes, and emits like every other value.

Structured diagnostics with stable severities and attached context. The four severities (error, warning, remark, note) are MLIR’s, with the same meanings: a remark reports on success, a note attaches context to another finding. PowerIO adds a stable dotted code, which is what you branch on, plus targets, related records, and suggested actions.

Typed representations at more than one abstraction level. The value families span reusable networks, calculation instances, and solutions the way a compiler holds IR at several levels; nothing forces the richer levels through the poorer ones.

Explicit transformations with testable boundaries. Each transformation names its input and output types, returns diagnostics, and refuses what it cannot represent; balanced lowering exposes powerio::transform::to_balanced_report so you can inspect its assumptions and refusals before you transform a value. Nothing rewrites as a side effect, and format loss diagnostics belong to the EmitResult returned by emit.

Verification at representation boundaries. Parsers and transformations verify what they produce and report findings rather than repairing silently; repairs are explicit operations that leave history records.

Shared operations instead of per format switches. The parse and emission dispatchers route once, at the facade; matrix calculations, serializers, and inspectors consume the concrete typed values, so a new format adds one parser and one serializer rather than a case in every consumer.

Analysis caches. Factorizations and prepared solver arrays are derived data behind the public results, invalidated when their inputs change, the way pass manager analyses are; IndexedNetwork, the derived index view, stays public in 0.11 because downstream consumers build matrices through it directly.

Registries checked mechanically where tables drift. Structural type names, format tokens, diagnostic codes, and drawn architecture edges are each held to one source by a CI gate, which is the maintainable slice of MLIR’s declarative dialect definitions.

Serialization specified apart from memory. Each PowerIO IR generation has an explicit schema and validation rules, and the Rust structs do not define the public document layout. As in MLIR bytecode and LLVM bitcode, the document has an integer generation and a separate producer string, and the reader decides compatibility from the generation alone. The generation window follows LLVM’s bitcode epoch rule: within one minor release line every release reads what the line wrote, and the floor moves only at a line boundary. PowerIO IR reference defines every structural type field by field, the way the MLIR language reference defines its types, and a test holds the page to the generated schema. PowerIO makes no LLVM or MLIR compatibility promise by analogy.

Scrutiny proportional to permanence. A new core concept (a value family or common module record) needs a registered structural type name, an exact IR representation, and binding coverage. A new format adapter needs none of that.

Not adopted

PowerIO 0.11 has no SSA values, no generic operation tree, no region nesting, no global context, no open runtime dialect registry, no generic pass manager, and no bytecode. The existing Rust types describe power system data more directly than an operation tree would, and none of those mechanisms has a PowerIO use with measured benefit. Public names stay power system names. A bus is a bus, a lowering is named for its concrete result, and no Rust struct is renamed to an operation so it resembles MLIR. A PioContext would be justified only by measured interning or shared allocation needs, and none has appeared.

Two MLIR bytecode mechanisms are also left out while one generation covers the whole document. Per dialect versions would let one value family evolve on its own schedule; PowerIO adopts them only when a family needs a representation change the others do not. Writer back deployment (setDesiredBytecodeVersion) would let a newer build write an older generation for a consumer pinned to it; PowerIO adopts it only when such a consumer appears.

DC OPF bundle

powerio dcopf <case>.m -o <out> assembles a DcOpfInstance and writes <out>/<case>_dcopf/. Rust callers pass an assembled instance to powerio_matrix::emit_dcopf_bundle. The directory contains Matrix Market files and dcopf_meta.json.

The bundle writes the DC problem in the solver’s positive form, where b.mtx holds the positive susceptance magnitude of each branch and A.mtx is bus by branch. The public calculations in Matrices and graphs use the PowerModels form instead, where calc_branch_susceptances is negative for an inductive branch and calc_incidence_matrix is branch by bus. So the bundle’s b is the negated public b, and A.mtx is the transposed public A.

Definitions

  • Format. Matrix Market. Matrices are coordinate real; square symmetric ones (L, L_grounded) use the symmetric header and store the lower triangle only. Vectors are array real general, one value per line.
  • Index base. .mtx row and column indices are 1-based, as Matrix Market requires. reference_buses in the manifest are 0-based dense bus indices.
  • Sign convention. The DC bus susceptance matrix \(L\) uses the positive M-matrix form: stored nonzero off-diagonal entries are negative, diagonals are nonnegative, and \(L_{ii} = \sum_j \lvert L_{ij} \rvert\). An off-diagonal entry is \(L_{ij} = -b_e\) for the branch between \(i\) and \(j\), so a consumer recovers the branch susceptance as \(-L_{ij} > 0\).
  • Units. The default PerUnit divides power by base_mva and rescales cost so it is a function of per unit power, \(q \leftarrow 2c_2 \cdot \mathrm{base}^2\) and \(c \leftarrow c_1 \cdot \mathrm{base}\). Native keeps MW and native cost. The manifest records which one you chose.
  • Generator costs. The default export policy for costs is require, so an in service generator without cost data is an error. Pass --missing-gen-cost to fill the missing rows for a feasibility test.
  • Reference buses. reference_buses in the manifest lists every grounded bus as a 0-based dense index. Each in service island needs at least one reference. If several references lie in one island, the bundle fixes all of those voltage angles to zero; it is not a participation factor slack model.
  • Branch susceptance formula. b.mtx holds \(b_e\), positive for an inductive branch, the coefficient on \(\theta_f - \theta_t\). The complete flow is \(f_e = b_e(\theta_f - \theta_t) - b_e\delta_e\), where \(\delta_e\) is the phase shift in shift.mtx. The default, SeriesSusceptance, uses \(b_e = x/(r^2 + x^2)\) plus the phase shift terms, with no tap scaling. TapAdjustedReactance uses \(b_e = 1/(x \tau)\) plus p_shift. ReactanceOnly (\(b_e = 1/x\), taps and shifts ignored) is kept because it is the textbook DC linearization, and reproducing a published result needs it exactly as written. The manifest records the formula.

Matrices

fileshapewhat
A.mtx\(n \times m\)signed incidence matrix; column \(e\) has \(+1\) at from-bus, \(-1\) at to-bus
L.mtx\(n \times n\)DC bus susceptance matrix \(L = A \operatorname{diag}(b) A^\mathsf{T}\); with positive branch weights, its rank is \(n-c\) for \(c\) connected components
L_grounded.mtx\((n-k) \times (n-k)\)\(L\) with \(k\) reference rows and columns removed; SPD when every island is grounded
BAt.mtx\(m \times n\)branch flow matrix \(B A^\mathsf{T}\) over the bundle’s positive susceptance magnitudes; complete flow adds flow_offset. In PowerModels signs the same flow is \(p_\text{branch} = -B_f,v_a + b \odot \text{shift}\) with negated susceptances
Cg.mtx\(n \times n_{\mathrm{gen}}\)generator-to-bus incidence, one \(1\) per column

Vectors

The bus indexed vectors, of length \(n\), are pd (load), gs (shunt conductance, the constant real power a shunt draws at one per unit voltage; a nodal balance subtracts it beside pd), q, c, and c0 (the diagonal, linear, and constant cost terms), pmax and pmin (generation bounds), e_r (reference indicator: \(1\) at every reference bus, else \(0\)), p_shift (phase shift injection; zero only under ReactanceOnly, which ignores shifts, or when the case has no phase shifter), and fixed_withdrawal, equal to pd + gs + p_shift.

The branch indexed vectors, of length \(m\), are b (susceptances), shift (radians), flow_offset (equal to -b * shift elementwise), fmax (thermal limits; \(0\) means unlimited per MATPOWER), and the radian limits angle_min and angle_max.

The generator space vectors, of length \(n_{\mathrm{gen}}\), are q_gen, c_gen, c0_gen, pmax_gen, and pmin_gen.

The bundle schema only represents polynomial generator costs. If a generator has a piecewise linear cost, preparation returns a typed error instead of writing zero polynomial coefficients; the in memory generator space preparation keeps those breakpoints exactly.

The constant cost terms c0 and c0_gen do not move the argmin. They are there so that a consumer reporting objective values can reconstruct the full cost.

Generator space is canonical. The nodal q, c, c0, pmax, and pmin files aggregate the generators at each bus: the bounds are the sums of the generator bounds, and the cost curves combine by the parallel rule \(q = 1 / \sum_i 1/q_i\), which is the curve of the split that costs least. That aggregate agrees with generator space only while the cheapest split stays inside the bound of each generator. A bus with one generator keeps that generator’s curve.

Manifest (dcopf_meta.json)

The manifest has schema powerio.dcopf and is stamped with the writing release in powerio_version. It describes the Matrix Market files with structured metadata:

  • dimensions: n_buses, n_source_branches, n_branch_columns, n_generators, n_reference_buses, and n_grounded_buses.
  • index_base: dense = 0 for manifest bus, branch, generator, and reference indices; matrix_market = 1 for .mtx coordinates.
  • branch_susceptance_formula, units, build_options, and zero_impedance. build_options records both skip_zero_impedance and synthesize_unrated_limits. The zero impedance block records the skip flag, denominator rule, skipped count, and skipped source branch rows.
  • grounding: reference buses, removed rows and columns, the grounded operator (L_grounded), and the reference selector (e_r).
  • operators[]: one entry per emitted operator with name, file, kind, rows, cols, index_space, and units.

cost_policy, synthesized_gen_costs, patched_gen_costs, files[], and powerio_version are top level fields.

Solving with it

The complete affine equations are \[ f = B A^\mathsf{T}\theta + \texttt{flow_offset} \] and \[ L\theta = C_g p_g - \texttt{fixed_withdrawal}. \] Factor the grounded system, since L_grounded is SPD when every island has a reference. Drop all reference_buses entries from the right hand side, solve the reduced system, and set each reference angle to \(0\). e_r identifies the grounded buses without parsing the manifest. You can use the full singular \(L\) instead when the right hand side sums to zero within each connected component.

An interior point DC OPF solver builds reweighted bus Laplacians at each Newton step from the same A and b (only the edge weights change), so A is the durable operator to hand over.

Corpus harness

powerio corpus runs the conversion matrix’s properties against a private case corpus without letting a byte of it into the repository. The vendored fixtures cover the readers and writers on a handful of public cases, and real corpora hold the tail: dialect quirks, fields no fixture uses, and records at scales no fixture reaches. Much of that data is confidential, so it cannot become a fixture, appear in a warning quoted in a commit, or fingerprint itself through a numeric constant in a test.

powerio corpus ingest <corpus-dir> --work <scratch-dir> [--max-bytes N]
powerio corpus compare --work <scratch-dir>
powerio corpus walk --work <scratch-dir> [--walks N] [--hops N] [--seed N] [--settle N]
powerio corpus report --work <scratch-dir> -o findings.jsonl --summary summary.md

The corpus directory is only ever read. The work directory holds raw values and is disposable, so keep it on the machine that owns the corpus. findings.jsonl is the boundary: before report writes a byte, it audits its own output against every string the corpus taught it. The tool and the conversion matrix test in powerio-cli/tests/conversion_matrix_report.rs run the same code, the invariants module of powerio-cli, so the CI gate and the harness cannot drift apart.

Ingest and bucketing

Every readable file is parsed into the typed model and given an electrical fingerprint: element counts, base MVA, the sorted degree sequence of the bus graph, and quantized multisets of impedances and injections. Siblings of one case in different formats share a fingerprint even when all their names differ, so they land in one bucket, and every other file gets a bucket of its own. Bucket identifiers are ordinals assigned in fingerprint order, so nothing about a bucket identifier, path, or report row comes from a source file name. An unparseable file is a finding rather than an error, and a parser panic is reported by code location plus a tool minimized token level mutation distance, not by file content.

Compare

compare runs every leg from the pristine case: the four matrix properties in both directions for every sibling pair, and as a PowerIO round trip for every sibling on its own. The properties are warning accounting on read and write, core survival, Y_bus entry for entry and per bus injections, and the canonicalized typed model diff. Each typed model diff path is paired against the warnings that leg emitted, and a diff with no covering warning becomes an undeclared loss finding.

Walk

walk converts each bucket’s case through random cycles of formats and grades the chain properties: the route must not change the destination (converting A to B to C must land where A to C does), conversion must settle (a second pass through a format changes nothing), and an emptied table must stay empty (rows reappearing means a writer made up data no reader gave it). compare already grades each hop’s own loss, so walk findings carry only the chain properties, each with the format path and the seed that replays it.

The run learns as it goes. A ledger in the work directory keeps, per directed format pair, every distinct signature that edge has produced, and the next path is drawn toward the pairs that have taught the least. The run ends when --settle consecutive walks teach the ledger nothing. The ledger persists across runs; delete it to start over.

The anonymization boundary

The tool enforces the boundary itself rather than relying on the care of whoever reads the report:

  • Identifiers: every name in a case is replaced with its element class ordinal (bus#12, gen#3) in every emitted string, warning texts included.
  • Values: findings give relative deltas, ratios, and quantized magnitudes. A field’s exact value appears only when it is a format constant such as a mode flag or a column count, and not when it is grid data.
  • Text: no line of a source file is echoed, and comments in decks are not echoed either.
  • Findings are property shaped: format, record type, field path, expected behavior, observed behavior, and structural preconditions. That is enough to reproduce the problem without any of the case.

From a finding to a fix

  1. Triage by severity: crash, silent value change, silent drop, undeclared loss, miscounted warning, declared loss confirmed.
  2. Restate the finding as a falsifiable sentence about the format, using the findings file alone.
  3. Build a minimal synthetic reproducer from an existing vendored fixture, from powerio gen, or by hand as a case of two to four buses with canonical values. The test must fail before the fix and pass after. A finding you cannot reproduce synthetically is not actionable; it goes into the report as open and does not go into a commit.
  4. Fix under the existing decision order: carry the data, stop retaining restatements, warn only on losses. Then run the full gate, including the conversion matrix with rederived baselines.
  5. Commit one property per commit, and describe the property rather than the run that found it. Rerun the tool to confirm the finding class closed and no other bucket regressed.

A fixture added this way comes with its generation script or handwritten provenance, recorded in the fixture README. Corpus paths do not belong in a diff.

Performance

PowerIO has five benchmark tiers, listed below. They answer different questions, so keep their numbers separate when you publish them.

tiercommandwhat it answers
Rust microbenchmarkscargo bench -p powerio-tx --bench parseparser, writer, and PowerWorld reader timing inside one process
Matrix microbenchmarkscargo bench -p powerio-matrix --bench matrixsparse matrix, DC OPF component, and dense sensitivity builder timing after parse/indexing
Cross tool parser and matrix comparisonjulia --project=evals/validation evals/performance/bench_julia.jl --jsonpowerio through the C ABI against ExaPowerIO.jl and PowerModels.jl, including parse plus Y bus construction
Python parser comparison.venv/bin/python evals/performance/bench_parse.py --json <cases>Python package parse and matrix path against pandapower reader paths
C ABI release sizethree cargo build -p powerio-capi --release feature sets plus statbinary size for core, arrow,matrix, and all release features

The published tables come from evals/performance/render_tables.py, which renders the JSON the harnesses write, and this page is the reference for how those numbers are made. Each refresh also records the snapshot environment: machine model, chip, core count, memory, OS, Rust, C compiler, Julia, Python, and the package versions of the comparison harnesses. Regenerate the JSON inputs first, then render:

bash evals/validation/fetch_cases.sh
cargo build --release -p powerio-capi --features arrow,matrix
python3.11 -m venv .venv
.venv/bin/python -m pip install --upgrade pip maturin -r evals/validation/requirements.txt
env VIRTUAL_ENV=$PWD/.venv .venv/bin/maturin develop --release
julia --project=evals/validation evals/performance/bench_julia.jl --json
.venv/bin/python evals/performance/bench_parse.py --json \
  tests/data/case2869pegase.m \
  tests/data/large/case9241pegase.m \
  tests/data/large/case13659pegase.m \
  tests/data/large/case193k.m
# tests/data/large/ is not in the repository; evals/validation/fetch_cases.sh fills it.
python3 evals/performance/render_tables.py
python3 evals/performance/render_tables.py --check

The Julia benchmark writes rows for parse only and matrix_rows for parse plus Y bus construction. Each tool is timed on its own path: PowerIO on ABI 7 parsing and typed network access, PowerModels on parse_file, make_per_unit!, and calc_admittance_matrix, and ExaPowerIO on parse_matpower plus a sparse Y bus assembled from its parsed branch admittance rows.

The Rust Criterion benchmarks measure PowerWorld .pwb and .aux parse timings. Fetch the public fixtures, run cargo bench -p powerio-tx --bench parse -- "parse_aux_|parse_pwb_", then run python3 evals/performance/extract_powerworld_bench.py before you render the tables. If you are publishing the Texas7k local row, pass its aux and pwb paths through POWERIO_BENCH_AUX and POWERIO_BENCH_PWB during the Criterion run.

Matrix builder timings do not include parsing. The matrix benchmark parses each fixture once, builds IndexedNetwork once, and times only the derived matrix construction. Its pipeline row measures Pipeline::run for the paired \(Y_{\mathrm{bus}}\) export, including MTX, shunt, and metadata writes:

cargo bench -p powerio-matrix --bench matrix
python3 evals/performance/extract_matrix_bench.py
python3 evals/performance/render_tables.py

While you work on one builder, filter the run down to the benchmarks you care about, for example:

cargo bench -p powerio-matrix --bench matrix -- 'matrix_bprime|matrix_ybus|dcopf_'

Criterion compares each run against whatever baseline is in your local target/criterion, so treat a Performance has regressed line as a reason to investigate rather than as a publishable claim by itself. A number that goes into a release note or benchmark page needs the commit, tree cleanliness, machine, toolchain, command, fixtures, and whether the optional large cases were present.

Before you publish a C ABI change, measure the release binary size. ABI 7 exports one symbol set and only the gridfm feature changes the binary, so two builds cover the range (the library suffix is .so on Linux, .dylib on macOS, and .dll on Windows):

cargo build -p powerio-capi --release --no-default-features
cp target/release/libpowerio_capi.so /tmp/libpowerio_capi-core.so
cargo build -p powerio-capi --release --no-default-features --features gridfm
cp target/release/libpowerio_capi.so /tmp/libpowerio_capi-gridfm.so
stat -c '%s %n' /tmp/libpowerio_capi-core.so /tmp/libpowerio_capi-gridfm.so

Testing and release checks

If your change alters numerical semantics, it needs tests and a short note in code or docs saying why. If it is a performance change, it needs before and after measurements.

Baseline checks

scripts/ci-mirror.sh runs everything rust.yml runs, in the same feature combinations: format, clippy, the terminology and symbol gates, header parity, every crate’s tests in each gated feature set, the C smoke and header programs, schema generation, packaging checks, and the docs build. Run it before pushing rather than assembling your own subset, because a hand assembled subset misses the feature gated suites. cargo test --workspace, for example, builds powerio-capi with default features only and skips every test behind arrow, gridfm, matrix, or prob.

Point POWERIO_JL at a PowerIO.jl checkout to include the Julia binding suite against the freshly built library, or set POWERIO_JL_OPTIONAL=1 to run without one.

The Python binding tests need a wheel built into a virtual environment. Build it from the repository root, where pyproject.toml lives:

python3 -m venv .venv && source .venv/bin/activate
pip install maturin pytest
maturin build --release -o dist
pip install dist/*.whl
python -m pytest python/tests

Install the built wheel rather than an editable one. When pytest runs from the repository root, the powerio/ crate directory there shadows a maturin develop install.

Route changes

Pick the smallest set of gates that covers what you changed, then run the release gates before you claim a release.

changed surfaceextra gates
parser or writer semanticsbash evals/validation/run_validation.sh; format round trip tests; affected cargo +nightly fuzz run <target> -- -runs=1 harnesses
rich model fieldsbash evals/validation/run_rich_validation.sh
matrix calculationscargo test -p powerio-matrix; cargo bench -p powerio-matrix --bench matrix
problem instances or DC OPF bundlescargo test -p powerio-prob --no-default-features; cargo test -p powerio --features matrix
PowerWorld binary readerPowerWorld parser tests plus `cargo bench -p powerio-tx –bench parse – “parse_aux_
C ABIscripts/capi-header-parity.sh; scripts/capi-smoke.sh; cargo test -p powerio-capi --no-default-features; cargo test -p powerio-capi --features arrow,matrix,gridfm,dist,prob; bash scripts/ci-clippy.sh capi-no-default; bash scripts/ci-clippy.sh capi-release
Python package metadata or extrasmaturin build --release --out /tmp/powerio-wheel-check; inspect wheel METADATA
Julia binding compatibilitybuild powerio-capi --features arrow,matrix,gridfm,dist,prob, then run PowerIO.jl tests with POWERIO_CAPI
shared surface with PowerIO.jlpush a same-named PowerIO.jl companion branch; the tandem CI job tests against it
CLI behaviorcargo test -p powerio-cli --test cli
documentation or websitemdbook build docs; mdbook test docs; RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps; regenerate schemas and the C header when their source rustdoc changes; run scripts/capi-header-parity.sh; check links to retired guide outputs

evals/validation/run_validation.sh needs the Python oracle stack in the same Python 3.11+ venv as the local wheel, and treats a missing PyPSA, pandapower, or egret as a setup failure. evals/validation/run_rich_validation.sh treats the committed PowerModels rich oracle as strict, so missing Julia is a setup failure there too.

Release gates

Before you publish a release claim, run the full set below on top of the baseline checks:

cargo test -p powerio-capi --no-default-features
cargo test -p powerio-capi --features arrow,matrix,gridfm,dist,prob
bash scripts/ci-clippy.sh capi-no-default
bash scripts/ci-clippy.sh capi-release
cargo build -p powerio-capi --release --features arrow,matrix,gridfm,dist,prob
scripts/capi-header-parity.sh
scripts/capi-smoke.sh
POWERIO_CAPI=$PWD/target/release/libpowerio_capi.so \
  julia --project=../PowerIO.jl -e 'using Pkg; Pkg.test()'   # .dylib on macOS
cargo bench -p powerio-matrix --bench matrix -- 'matrix_bprime|matrix_ybus|dcopf_'
(cd evals/performance/asv && ../../../.venv/bin/asv check -E existing:../../../.venv/bin/python)
(cd evals/performance/asv && ../../../.venv/bin/asv run --quick --show-stderr -E existing:../../../.venv/bin/python --dry-run)
for target in $(cd fuzz && ls fuzz_targets | sed 's/\.rs$//'); do
  cargo +nightly fuzz run "$target" -- -runs=1
done
bash evals/validation/run_validation.sh
bash evals/validation/run_rich_validation.sh

run_validation.sh checks the classic transmission paths against PowerModels.jl, ExaPowerIO.jl, egret, and pandapower to writer matrix; run_rich_validation.sh covers fields outside the MATPOWER row shape (branch terminal admittance, switches, current ratings, solution values, HVDC costs, load voltage models). GOC3 has its own goc3-reference CI job, which pins the GO-3 data model, C3DataUtilities, and the D1, D2, and D3 files from GOC3Benchmark.jl. That job validates PowerIO’s problem and solution documents with the GO-3 data model, parses all three benchmark problems as AcScucInstance, runs the Challenge 3 data checks on them, and runs the same checks on a PowerIO AcScucSolution output document. Surge has no external validator in this harness, so its current evidence is its Rust parser, writer, routing, stored module, and round trip tests. The format chapter says what the independent checks prove for each format.

The gates do not prove that every field of every source format survives. Known losses are part of the public behavior and show up as warnings.

Benchmark updates

Regenerate the benchmark JSON before you change a published table:

julia --project=evals/validation evals/performance/bench_julia.jl --json
.venv/bin/python evals/performance/bench_parse.py --json <cases>
cargo bench -p powerio-tx --bench parse -- "parse_aux_|parse_pwb_"
python3 evals/performance/extract_powerworld_bench.py
cargo bench -p powerio-matrix --bench matrix
python3 evals/performance/extract_matrix_bench.py
python3 evals/performance/render_tables.py
python3 evals/performance/render_tables.py --check

The ASV suite tracks Python wheel parse and matrix timing across git history. To smoke test it on an uncommitted worktree, point it at the local venv:

cd evals/performance/asv
../../../.venv/bin/asv check -E existing:../../../.venv/bin/python
../../../.venv/bin/asv run --quick --show-stderr -E existing:../../../.venv/bin/python --dry-run

Do not edit generated benchmark tables by hand. When you publish new numbers, update the snapshot environment described on the performance page as well: commit, tree cleanliness, machine, OS, toolchain, Python stack, Julia stack, commands, fixtures, and optional local data.

Broad local corpora stay local. Pass them in through the documented environment variables or --root flags, review the reports the run writes under evals/validation/results/, and do not commit corpus paths or generated outputs.