The .pio.json schema
.pio.json is the serialized form of powerio_pkg::NetworkPackage: a versioned
envelope around one PowerIO IR payload. The envelope shape and stability policy
are below. The crate is the implementation; compiler-ir.md is the architecture
note.
Two stability tiers
A .pio.json file has two parts with different stability promises.
-
The envelope — every field except
model. This is the versioned, documented surface:schema,schema_version,producer,model_kind,origin,sources,source_maps,diagnostics,validation,summary,lowering_history,operating_points,derived. Its shape changes only under the versioning policy below. -
The payload — the
modelfield’sbalanced_network/multiconductor_networkobject: the serde snapshot of the PowerIO Rust IR (powerio::Network/powerio_dist::DistNetwork). The payload is a declared contract of its own, named by the top-levelpayload_schemaURL and versioned bypayload_schema_version. A consumer that computes on payload fields pins the payload version; a tool that routes or audits packages pins the envelope version and can keep treating the payload as opaque.
The two versions are independent because they change at different rates and
break different consumers: the payload grows whenever the IR grows (a minor
payload_schema_version bump), while the envelope bookkeeping barely moves.
The payload is PowerIO’s own IR schema for both model kinds. Interchange
formats stay at the converter boundary: for distribution models the
multiconductor payload is the same model the BMOPF reader and writer translate
to and from, and powerio convert --to bmopf-json emits a standalone BMOPF
exchange file when one is needed. The BMOPF schema itself (bmopf-report) never
defines the payload.
Versioning policy (envelope)
schema_versionis semver. The current value is0.1.1; theschemaURL ishttps://powerio.dev/schema/pio-package/0.1.- Optional additive envelope fields (a reader that ignores them loses nothing
it relied on before) land without a version change;
operating_pointslanded this way. The minor version bumps when a reader needs to depend on a field being present. - Envelope field moves or removals bump the major version, or ship a migration.
- A reader tolerates unknown later top-level fields (they are ignored, not an error), so a package from a newer producer still loads. A later version can preserve them in an extras map instead of dropping them.
- A reader accepts same major
schema_versionvalues and rejects a different major version before using the payload. - Every package states
producer.versionandschema_version.
Versioning policy (payload)
payload_schemanames the payload contract per model kind:https://powerio.dev/schema/pio-payload-balanced/1andhttps://powerio.dev/schema/pio-payload-multiconductor/1. The URLs are identifiers, not fetch locations (the JSON Schema$idconvention).payload_schema_versionis semver, currently1.0.0for both kinds. Additive optional fields bump the minor; field moves or removals bump the major.- A reader rejects a
payload_schema_versionwith a different major (or one that does not parse as semver) before computing on payload fields. Absent fields — every package written before 0.1.1 — are accepted; such payloads predate the declared contract. - The payload field tables are the rustdoc of
powerio::Networkandpowerio_dist::DistNetwork; the serde snapshot of those structs is the normative shape.
Row identity
Every row of the balanced payload tables (buses, loads, shunts,
branches, switches, generators, storage, hvdc, transformers_3w)
carries a uid string: the source record uid where the format defines one
(GOC3), and a {table}:{row} value synthesized at package build otherwise. A
synthesized uid records the row the element had when the package was built and
sticks to the element from then on. Uids are unique per table; a duplicate is a
validation error. Operating point updates resolve against these identities
(below). Rows in packages written before 0.1.1 carry no uid, which is what
keeps their row-addressed operating points valid.
Explicit model kind
model_kind is a standalone top-level field and is authoritative. A reader
must branch on it and must not infer the payload kind from which field is
present. The payload is additionally self-describing: model is tagged by
kind, so model.kind and model_kind carry the same value.
NetworkPackage::kind_is_consistent asserts the two agree; a reader should
reject a package where they disagree.
"model_kind": "balanced",
"model": { "kind": "balanced", "balanced_network": { "...": "..." } }
model_kind values: balanced, multiconductor (the enum is non-exhaustive;
later families can be added).
Envelope reference
| field | type | required | notes |
|---|---|---|---|
schema | string (URL) | yes | identifies the package format; defaults to the current URL on read |
schema_version | string (semver) | yes | envelope version; defaults to current on read |
producer | object | yes | {tool, version, git_commit?, features[]} |
package_id | string | no | stable content id, e.g. "sha256:..."; unset by the scaffold |
created_at | string (RFC 3339) | no | unset by default for deterministic output |
model_kind | enum | yes | balanced | multiconductor; authoritative |
payload_schema | string (URL) | no | declared payload contract for model_kind; absent on pre-0.1.1 packages |
payload_schema_version | string (semver) | no | payload version; a different major is rejected on read |
model | object | yes | {kind, <kind>_network}; follows the Rust model payload |
origin | object | yes | tagged by kind: in_memory | file | folder | binary_file | derived | composite |
sources | array | no | declared source artifacts: {id, kind, path?, format?, hash?} |
source_maps | array | no | {element_path, source_ref, mapping_kind, confidence} |
diagnostics | array | no | structured findings (see below) |
validation | object | yes | {status, counts, passes[]} |
summary | object | yes | {elements{}, topology?, units?} |
lowering_history | array | no | LoweringRecord per pass |
operating_points | object | no | replayable updates over the one static payload |
derived | object | no | optional matrix stats, normalized solver table metadata, and cache keys |
Operating points
operating_points records a time axis and an ordered list of payload field
updates. A point names a table, a row identity and/or a zero based row, and the
fields to overwrite. Materializing a point clones the static payload, applies
those field updates, and clears operating_points in the returned package.
Updates resolve by identity first. When the referenced table carries uid
values, element.source_uid is authoritative: it selects the row, a present
element.row must agree with the resolved row, and an unknown or duplicated
uid is an error (reported by validation and fatal to materialization). A
producer that knows the identity can omit row entirely. When the table
carries no uids (packages written before 0.1.1), source_uid is advisory and
row addresses the update alone. An update may not overwrite uid itself, and
an element ref with neither row nor source_uid does not parse.
The block shape is:
| field | type | notes |
|---|---|---|
time_axis.periods | integer | number of available operating points |
time_axis.duration_hours | array of numbers | optional per period duration |
time_axis.labels | array of strings | optional labels, such as "1", "2", … |
points[] | array | one replayable state |
points[].index | integer | zero based period index; addresses time_axis.duration_hours and time_axis.labels |
points[].updates[] | array | row field updates to apply for this point |
updates[].element.table | string | payload table name, such as generators, loads, branches, or hvdc |
updates[].element.row | integer | zero based row; optional when source_uid is present, then a consistency check |
updates[].element.source_uid | string | the target row’s payload identity (uid); authoritative when the table carries uids |
updates[].fields | object | field names and JSON values to overwrite |
metadata | object | optional series or point metadata |
GO Challenge 3 packages use this block for the scheduling time series. The
static model reflects the first interval that can be represented by
Network; operating_points carries replayable updates for every interval.
NetworkPackage::materialize_operating_point(index) returns a new static
package with origin.kind = "derived" and
origin.pass = "materialize-operating-point".
"operating_points": {
"time_axis": { "periods": 2, "duration_hours": [1.0, 1.0], "labels": ["1", "2"] },
"points": [
{ "index": 0, "updates": [] },
{ "index": 1,
"updates": [
{ "element": { "table": "loads", "row": 0, "source_uid": "device_1" },
"fields": { "p": 12.5, "q": 3.2 } }
] }
],
"metadata": { "source_format": "goc3-json" }
}
Derived metadata
derived.normalized_solver_tables records the compact identity metadata for
powerio::Network::to_normalized_solver_tables() without embedding every table
row in the package. The full tables are a derived artifact; this metadata lets a
compiler cache prove it was built from the same lowering pass and row order.
The block carries:
pass:"balanced-to-normalized-solver-tables";units: per unit power, per unit voltage, radian angles, per unit impedance and admittance, zero based dense indices;row_counts: counts for buses, loads, shunts, branches, switches, arcs, generators, storage, and HVDC rows;bus_ids,reference_bus_indices, andcomponent_labels;branch_from_arc_indicesandbranch_to_arc_indices;source_rows: source row indices for rows that survived normalization, withnullfor synthetic rows such as 3-winding star buses and branches.
Diagnostics
Each diagnostic carries a stable dotted code, a severity (debug, info,
warning, error, fatal; ordered worst-last), the stage it came from
(parse, read, canonicalize, validate, lower, emit, bind,
partner), a human message, and where known an element_path, a source_ref,
a details object, a suggested_action, and a safe_to_ignore list. Code
namespaces by leading segment: PARSE, READ, IR, VALIDATE, FIDELITY,
LOWER, EMIT, BINDING, PARTNER, PERF.
Source maps
A source_map entry records where a canonical field came from: an element_path
(a JSON pointer, or a best-effort locator in v0.1), a source_ref into a declared
source, a mapping_kind (exact, defaulted, inferred, converted_units,
lowered, aggregated, split, synthetic, retained_extra), and a
confidence (exact, high, medium, low). Balanced packages emit source
maps for stable bus, load, shunt, branch, and generator fields. Balanced
source_ref.field values use the same canonical field names as the payload, so
they can be compared directly with element_path. When a source format folds
several canonical elements into one source row, the source map records that
relation with another mapping kind; MATPOWER load and shunt fields use
mapping_kind = split and point to the bus record while keeping fields such as
p, q, g, and b. Values that the source format does not carry are not
mapped as exact; MATPOWER base_frequency has no source map. When a
multiconductor network is packaged, its defaulted fields lift into source maps
with mapping_kind = defaulted, and its retained source becomes
origin.retained_source. Validation diagnostics attach the matching source_ref
when the package has a source map for the reported field.
NetworkPackage::lower_multiconductor_to_balanced(options) returns a new
balanced package with origin.kind = derived and
origin.pass = "multiconductor-to-balanced". It preserves the parent
lowering_history and appends a LoweringRecord whose options, assumptions,
approximations, dropped fields, diagnostics, and validation status describe the
pass. Lowered balanced source maps use lowered, aggregated,
converted_units, synthetic, and defaulted mapping kinds. The pass is never
implicit during package readback, format conversion, matrix construction,
bindings, or MCP operations.
Example
{
"schema": "https://powerio.dev/schema/pio-package/0.1",
"schema_version": "0.1.1",
"producer": { "tool": "powerio", "version": "0.5.1" },
"model_kind": "multiconductor",
"payload_schema": "https://powerio.dev/schema/pio-payload-multiconductor/1",
"payload_schema_version": "1.0.0",
"model": {
"kind": "multiconductor",
"multiconductor_network": {
"base_frequency": 60.0,
"loads": [
{ "name": "l1", "bus": "b1", "configuration": "wye",
"voltage_model": { "model": "zip", "v_nom": [230.0], "alpha_z": [0.5], "...": "..." } }
]
}
},
"origin": { "kind": "file", "format": "dss", "retained_source": true },
"sources": [ { "id": "src0", "kind": "file", "format": "dss" } ],
"source_maps": [
{ "element_path": "/model/multiconductor_network/vsource.source#basekv",
"source_ref": { "source_id": "src0", "field": "basekv" },
"mapping_kind": "defaulted", "confidence": "high" }
],
"validation": { "status": "ok", "counts": { "fatal": 0, "error": 0, "warning": 0, "info": 0, "debug": 0 } },
"summary": { "elements": { "buses": 1, "loads": 1 }, "units": { "power": "W/var", "angle": "radians" } }
}