Julia bindings for PowerIO, which reads
power system case files into a typed Network, writes them back, and converts
between formats. The Rust core does the parsing and the byte-exact write, so a
case reads identically in Julia, Python, C/C++, and Rust.
Supported formats (each reads and writes, so any pair converts):
- MATPOWER
.m - PSS/E
.rawrevision 33 - PowerWorld
.aux - PowerModels.jl network data JSON
- egret
ModelDataJSON
A same-format round trip is byte exact; cross-format conversion reports fields the target cannot represent as warnings.
pkg> add PowerIOWorking on the binding itself needs a local C ABI build; see Develop below.
using PowerIO
net = parse_file("case14.m")
PowerIO.n_buses(net), PowerIO.n_gens(net), PowerIO.base_mva(net)
PowerIO.source_format(net) # "Matpower"
PowerIO.reference_bus_id(net) # the slack bus id (or nothing)
text, warnings = convert_file("case14.m", "psse")
# egret and PowerModels both use .json; pass `from` to disambiguate:
egret = parse_file("grid.json"; from="egret")parse_file also reads from an IO; a String argument is always a path, so
in-memory text goes through an IO or parse_str with an explicit format:
net = parse_file(IOBuffer(read("case14.m", String)), "matpower")
net = parse_str(read("case14.m", String), "matpower")Serialization and the structured transport:
to_matpower(net) # ::String, byte exact when the input was MATPOWER
to_json(net) # the JSON transport
to_format(net, "powermodels-json") # (text, warnings)
from_json(to_json(net)) # Network with a live handleto_normalized derives a computation-ready copy: powers per unit, angles in
radians, tap 0 → 1, out-of-service and isolated elements dropped, source bus
ids preserved, bus types inferred:
norm = to_normalized(net)
PowerIO.source_format(norm) # "Normalized"to_dense returns the numeric tables as dense typed arrays straight from the
C ABI extractors (no JSON parse) for matrix assembly:
d = to_dense(net) # or to_dense("case14.m") for parse + extract
d.n, d.m, d.ng # bus / branch / generator counts
d.bus_ids # 1-based ids; row k of every per-bus table is bus_ids[k]
d.branch.from, d.branch.x # branch endpoints and reactances
d.reference_bus, d.n_components, d.is_radialto_arrow brings one table across the Arrow C Data Interface (needs the library
built with --features arrow; arrow_available() reports it). By default it
returns a NamedTuple of owned Julia Vectors (Tables.jl-shaped, flows into
Arrow.write, DataFrame, etc.), so there is no lifetime caveat. copy=false
returns a zero-copy ArrowTable whose columns view the producer's memory; keep it
alive while reading them. For the numeric tables alone, to_dense is a copy-free,
unsafe_wrap-free fast path (the C ABI fills Julia-owned buffers).
t = to_arrow(net, :branch) # :bus, :branch, :gen, :load, :shunt; owned columns
t.from, t.x, t.tap
z = to_arrow(net, :branch; copy=false) # zero-copy views; keep `z` alive while readingread_gridfm reads a gridfm-datakit Parquet dataset back into a Network — the
inverse of the gridfm writer, the ML→classical return leg (needs the library built
with --features gridfm; gridfm_available() reports it). The read is lossy but
power-flow-complete; what the schema can't round-trip comes back in warnings.
r = read_gridfm("out/case14/raw") # (; network, scenario, warnings)
to_matpower(r.network) # gridfm → any classical format
reads = read_gridfm_scenarios("out/case14/raw") # one result per scenario idAt first use the binding checks pio_abi_version against the version it targets
and refuses a stale or mismatched library with an error stating both versions.
| Target | Direction | Mechanism |
|---|---|---|
| PowerModels.jl | both | to_powermodels / from_powermodels |
| ExaPowerIO.jl / ExaModelsPower.jl | out | to_powerdata / parse_ac_power_data feeding build_polar_opf / build_rect_opf / build_dcopf |
| PowerDiff.jl | out | PowerDiff depends on PowerIO as its parser and data layer |
| MATPOWER / PSS/E / PowerWorld / PowerModels JSON / egret | file | parse_file / convert_file |
| GridFM (gridfm-datakit Parquet) | in | read_gridfm / read_gridfm_scenarios |
The parse_file / to_* naming is shared across Rust, Python, Julia, and the
C ABI; the cross language table is in docs/languages.md.
With a sibling powerio checkout, build the C ABI and using PowerIO finds it:
# in the sibling powerio checkout (arrow enables to_arrow, gridfm enables read_gridfm):
cargo build -p powerio-capi --release --features arrow,gridfm
For a non-sibling layout, point Julia at the library explicitly:
PowerIO.set_library!("/path/to/libpowerio_capi.dylib")
# or: ENV["POWERIO_CAPI"] = "...path..." before `using PowerIO`Users never build Rust: released versions fetch the per-platform binary as a lazy artifact. The pipeline is described in docs/binary.md.
0.1.x tracks powerio v0.2.2 (C ABI 3): 0.1.0 added the gridfm reader
(read_gridfm), 0.1.1 the PyPSA CSV writer (write_pypsa_csv_folder) and
reference_bus_indices, 0.1.2 the n_components / is_radial accessors. Next: a
fully typed immutable Network mirroring the Rust model (today's view is
JSON-backed), a Documenter site, package extensions for the PowerModels and
ExaPowerIO bridges, and distribution through a registered PowerIO_jll.
MIT.
