Skip to content

feat: name the DC axes, expose operating point networks, and close the geometry regression (0.11.1) - #495

Open
samtalki wants to merge 8 commits into
mainfrom
claude/powermcp-powerio-tellegen-4orrbc
Open

feat: name the DC axes, expose operating point networks, and close the geometry regression (0.11.1)#495
samtalki wants to merge 8 commits into
mainfrom
claude/powermcp-powerio-tellegen-4orrbc

Conversation

@samtalki

@samtalki samtalki commented Sep 6, 2026

Copy link
Copy Markdown
Member

Summary

Additive 0.11.1 work, all under the 0.11.x compatibility rule: no public Rust or C signature changes, ABI 7 unchanged, new symbols only.

Named DC axes (PowerIO.jl#139, #140)

  • DcOperatorOptions and DcOperators::build_with; branch_rows, branch_identities, and skipped_branch_rows state the branch axis beside bus_ids. With skip_zero_impedance a zero impedance branch leaves the axis and is listed instead of failing the build with BUILD.OPERATOR.ZERO_IMPEDANCE, the same choice the admittance builders offer.
  • C ABI 7 gains the PioDcOperators handle: pio_calc_dc_operators, the axis accessors, and the eight calculations over one build. The existing pio_calc_* entry points are unchanged; the header is regenerated with cbindgen and check-capi-v7.sh passes against the PowerIO.jl branch of the same name (fix: correct to_powerdata, unify zero impedance handling, name the DC axes (0.11.1) PowerIO.jl#144).
  • Python: BalancedNetwork.calc_dc_index_map and skip_zero_impedance on every DC calc_* method.
  • MCP calc_matrix: row_ids and col_ids on every result, the eight DC calculations by name (vectors as {"format": "vector"}), and skip_zero_impedance.

Operating points: powerio::network_with_operating_point and Python OperatingPoint.network return the balanced network an entry states with the point's values applied, so a consumer selects a collection entry without emitting and reparsing it. summarize and calc_matrix accept an operating point entry through it.

Diagnostics: MCP records carry suggested_action, related, and details.

#479: Burhan Abdullah's regression cases from draft #480 run as ordinary tests in powerio-dist/tests/electrical_readiness.rs: a geometry defined line is reported by READ.DSS.GEOMETRY_UNRESOLVED when read and never receives the OpenDSS factory impedance or a fabricated conductor count.

Version 0.11.1, the ## 0.11.1 changelog section, and docs/release-notes/0.11.1-draft.md; the golden Arrow payloads are renumbered. Tagging stays with the maintainer.

Closes #479

Validation

cargo test --workspace --exclude powerio-py, cargo test -p powerio-capi (31 passed), cargo fmt --all --check, bash scripts/check-capi-v7.sh (with the PowerIO.jl checkout), bash scripts/capi-header-regen.sh, bash scripts/check-release-versions.sh, bash scripts/terminology-gate.sh, bash scripts/check-value-types.sh, python3 scripts/check-doc-symbols.py, and maturin build --release plus pytest python/tests in a 3.11 venv (222 passed, 1 skipped). bash scripts/ci-clippy.sh was rerunning at the time of opening; any finding lands as a follow-up commit on this branch.

Companions: eigenergy/PowerIO.jl#144 binds the new handle; samtalki/PowerMCP#2 (the follow-up to Power-Agent/PowerMCP#64) consumes calc_matrix axes and OperatingPoint.network.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NoJFzdnTLwtFZLPhUeyCvR


Generated by Claude Code

…e geometry regression (0.11.1)

Additive 0.11.1 work under the 0.11.x compatibility rule: no public Rust or C
signature changes, ABI 7 unchanged, new symbols only.

- DcOperatorOptions and DcOperators::build_with; branch_rows, branch_identities
  and skipped_branch_rows state the branch axis beside bus_ids. With
  skip_zero_impedance a zero impedance branch leaves the axis and is listed
  instead of failing the build with BUILD.OPERATOR.ZERO_IMPEDANCE, the same
  choice the admittance builders offer.
- C ABI 7 gains the PioDcOperators handle: pio_calc_dc_operators, the axis
  accessors, and the eight calculations over one build. The existing pio_calc_*
  entry points are unchanged; the header is regenerated with cbindgen.
- Python: BalancedNetwork.calc_dc_index_map and skip_zero_impedance on every
  DC calc_* method. OperatingPoint.network and
  powerio::network_with_operating_point return the balanced network an
  operating point states with the point's values applied.
- MCP calc_matrix reports row_ids and col_ids for every result, serves the
  eight DC calculations by name (vectors as {"format": "vector"}), takes
  skip_zero_impedance, and accepts an operating point entry; summarize
  describes an operating point through its network; diagnostic records carry
  suggested_action, related and details.
- The OpenDSS geometry regression cases from Burhan Abdullah's draft (#480)
  run as ordinary tests in powerio-dist/tests/electrical_readiness.rs: a
  geometry defined line is reported by READ.DSS.GEOMETRY_UNRESOLVED when it is
  read and never receives the OpenDSS factory impedance or a fabricated
  conductor count.

Reported from the PowerIO.jl port of PowerDiff.jl (eigenergy/PowerIO.jl#139,
#140). Version 0.11.1 with the changelog section, the release notes draft,
and the golden Arrow payloads renumbered.

Closes #479

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NoJFzdnTLwtFZLPhUeyCvR
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Conversion Matrix

Legend

Cells show X/Y: observed warnings / expected warnings. Counts include source parse, target write, and target readback.

A nonzero cell means the target format cannot carry what the source stated, and every warning names the field or record it cannot carry. A write or readback warning names a field of the target format; a source parse warning, which every cell of that row shares, names what the source document itself leaves unstated. A warning that named a writer PowerIO could extend instead of a limit of the format would be a defect in this table.

  • 🟢 0/0: no warnings and checked invariants held.
  • 🟡 X=Y: observed warnings match the reviewed expected count, and that count is nonzero.
  • 🔴 X!=Y or invariant failure: behavior changed. If warnings decreased because fidelity improved, update the expected counts in the same PR.
  • Expected counts are the *_WARNING_BASELINE arrays in powerio-cli/tests/conversion_matrix_report.rs; accept an intentional change by editing the matching source/target entry in the same PR.

Transmission

Six generated cases per writable source row; one vendored case per read-only source row.

Into a case format

Source ↓ / target → MATPOWER .m PowerModels JSON PSS/E .raw 33 PSS/E RAWX 35 PowerWorld .aux egret JSON pandapower JSON Surge JSON PSLF .epc
MATPOWER .m 🟢 0/0 🟡 1/1 🟡 15/15 🟡 15/15 🟡 18/18 🟡 7/7 🟡 14/14 🟡 23/23 🟡 14/14
PowerModels JSON 🟢 0/0 🟢 0/0 🟡 15/15 🟡 15/15 🟡 17/17 🟡 6/6 🟡 13/13 🟡 22/22 🟡 13/13
PSS/E .raw 33 🟡 1/1 🟡 1/1 🟢 0/0 🟢 0/0 🟡 2/2 🟡 1/1 🟡 3/3 🟡 1/1 🟡 2/2
PSS/E RAWX 35 🟡 1/1 🟡 1/1 🟢 0/0 🟢 0/0 🟡 2/2 🟡 1/1 🟡 3/3 🟡 1/1 🟡 2/2
PowerWorld .aux 🟡 1/1 🟢 0/0 🟡 6/6 🟡 6/6 🟢 0/0 🟢 0/0 🟡 8/8 🟢 0/0 🟡 6/6
egret JSON 🟢 0/0 🟢 0/0 🟡 9/9 🟡 9/9 🟡 12/12 🟢 0/0 🟡 7/7 🟡 1/1 🟡 7/7
pandapower JSON 🟢 0/0 🟢 0/0 🟡 7/7 🟡 7/7 🟡 8/8 🟢 0/0 🟢 0/0 🟢 0/0 🟡 7/7
Surge JSON 🟢 0/0 🟢 0/0 🟡 9/9 🟡 9/9 🟡 12/12 🟢 0/0 🟡 6/6 🟢 0/0 🟡 7/7
PSLF .epc 🟢 0/0 🟢 0/0 🟡 1/1 🟡 1/1 🟡 1/1 🟢 0/0 🟡 4/4 🟡 3/3 🟢 0/0
XIIDM 1.17 🟡 9/9 🟡 7/7 🟡 33/33 🟡 33/33 🟡 8/8 🟡 7/7 🟡 9/9 🟡 7/7 🟡 8/8
JIIDM 1.17 🟡 9/9 🟡 7/7 🟡 33/33 🟡 33/33 🟡 8/8 🟡 7/7 🟡 9/9 🟡 7/7 🟡 8/8
CGMES 3.0 🟡 8/8 🟡 6/6 🟡 67/67 🟡 42/42 🟡 6/6 🟡 6/6 🟡 7/7 🟡 6/6 🟡 6/6
UCTE-DEF .uct 🟡 18/18 🟡 12/12 🟡 6/6 🟡 6/6 🟡 18/18 🟡 12/12 🟡 6/6 🟡 12/12 🟡 18/18
PyPSA CSV 🟢 0/0 🟡 6/6 🟡 14/14 🟡 14/14 🟡 14/14 🟡 6/6 🟡 9/9 🟡 6/6 🟡 12/12
GridFM Parquet 🟡 6/6 🟢 0/0 🟡 14/14 🟡 14/14 🟡 14/14 🟡 6/6 🟡 9/9 🟢 0/0 🟡 12/12
PSS/E .raw 32 🟡 5/5 🟡 4/4 🟡 2/2 🟡 2/2 🟡 4/4 🟡 4/4 🟡 5/5 🟡 4/4 🟡 6/6
IEEE CDF 🟡 8/8 🟡 8/8 🟡 6/6 🟡 6/6 🟡 8/8 🟡 8/8 🟡 9/9 🟡 8/8 🟡 8/8
GO Challenge 3 JSON 🟡 6/6 🟡 5/5 🟡 7/7 🟡 7/7 🟡 9/9 🟡 5/5 🟡 7/7 🟡 5/5 🟡 7/7
DeepMind OPFData JSON 🟡 3/3 🟡 2/2 🟡 5/5 🟡 5/5 🟡 5/5 🟡 3/3 🟡 4/4 🟡 2/2 🟡 4/4

Into an exchange format

Source ↓ / target → XIIDM 1.17 JIIDM 1.17 CGMES 3.0 UCTE-DEF .uct
MATPOWER .m 🟡 70/70 🟡 70/70 🟡 57/57 🟡 42/42
PowerModels JSON 🟡 69/69 🟡 69/69 🟡 56/56 🟡 41/41
PSS/E .raw 33 🟡 32/32 🟡 32/32 🟡 12/12 🟡 29/29
PSS/E RAWX 35 🟡 32/32 🟡 32/32 🟡 12/12 🟡 29/29
PowerWorld .aux 🟡 54/54 🟡 54/54 🟡 30/30 🟡 33/33
egret JSON 🟡 68/68 🟡 68/68 🟡 36/36 🟡 36/36
pandapower JSON 🟡 55/55 🟡 55/55 🟡 35/35 🟡 52/52
Surge JSON 🟡 67/67 🟡 67/67 🟡 36/36 🟡 36/36
PSLF .epc 🟡 38/38 🟡 38/38 🟡 11/11 🟡 28/28
XIIDM 1.17 🟢 0/0 🟢 0/0 🟡 179/179 🟡 60/60
JIIDM 1.17 🟢 0/0 🟢 0/0 🟡 179/179 🟡 60/60
CGMES 3.0 🟡 44/44 🟡 44/44 🟢 0/0 🟡 51/51
UCTE-DEF .uct 🟡 36/36 🟡 36/36 🟡 12/12 🟢 0/0
PyPSA CSV 🟡 70/70 🟡 70/70 🟡 38/38 🟡 38/38
GridFM Parquet 🟡 64/64 🟡 64/64 🟡 32/32 🟡 40/40
PSS/E .raw 32 🟡 15/15 🟡 15/15 🟡 9/9 🟡 10/10
IEEE CDF 🟡 13/13 🟡 13/13 🟡 8/8 🟡 14/14
GO Challenge 3 JSON 🟡 8/8 🟡 8/8 🟡 7/7 🟡 14/14
DeepMind OPFData JSON 🟡 32/32 🟡 32/32 🟡 8/8 🟡 32/32

Into a dataset directory

Source ↓ / target → PyPSA CSV GridFM Parquet
MATPOWER .m 🟡 15/15 🟡 31/31
PowerModels JSON 🟡 14/14 🟡 30/30
PSS/E .raw 33 🟡 9/9 🟡 28/28
PSS/E RAWX 35 🟡 9/9 🟡 28/28
PowerWorld .aux 🟡 13/13 🟡 27/27
egret JSON 🟡 9/9 🟡 25/25
pandapower JSON 🟡 8/8 🟡 18/18
Surge JSON 🟡 9/9 🟡 25/25
PSLF .epc 🟡 8/8 🟡 27/27
XIIDM 1.17 🟡 15/15 🟡 53/53
JIIDM 1.17 🟡 15/15 🟡 53/53
CGMES 3.0 🟡 13/13 🟡 51/51
UCTE-DEF .uct 🟡 12/12 🟡 36/36
PyPSA CSV 🟢 0/0 🟡 15/15
GridFM Parquet 🟡 12/12 🟢 0/0
PSS/E .raw 32 🟡 6/6 🟡 12/12
IEEE CDF 🟡 9/9 🟡 11/11
GO Challenge 3 JSON 🟡 9/9 🟡 11/11
DeepMind OPFData JSON 🟡 5/5 🟡 4/4

Distribution

Seven cases per source row.

Into a distribution format

Source ↓ / target → OpenDSS .dss BMOPF JSON PMD JSON
OpenDSS .dss 🟢 0/0 🟡 66/66 🟡 88/88
BMOPF JSON 🟡 21/21 🟢 0/0 🟡 16/16
PMD JSON 🟡 27/27 🟡 44/44 🟢 0/0

Geographic Layer

One case per source row.

Into the geographic layer document

Source ↓ / target → geo-json
PowerWorld .aux substation coordinates 🟢 0/0

A geographic layer is a powerio.GeoLayer, not a network: no grid exchange format states a standalone layer, so the layer has its own table rather than a row of the network matrix. Its source is the substation coordinates of a vendored PowerWorld auxiliary file.

Full warning details: download conversion-matrix-warning-details for this workflow run.

`calc_dc_index_map` returns the `DcIndexMap` TypedDict, which mypy does not
accept where a `Dict[str, Any]` is declared, so the type gate failed on all
three `_axis_ids` call sites. `Mapping` states what the helper actually needs:
read-only keyed access. The same gate also flagged `DcIndexMap` itself as a
stub-only name with no runtime object, the deviation the allowlist exists to
record for every other TypedDict in the stubs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NoJFzdnTLwtFZLPhUeyCvR
`ptdf` and `lodf` reach `calc_ptdf` and `calc_lodf`, which factor the
reference grounded DC matrix and take no `skip_zero_impedance` option. The
tool accepted the flag anyway, dropped it, and echoed `"skip_zero_impedance":
true` back in the payload, so a caller reading the response had no way to see
that the zero impedance branches were still in the axis. Reject the pair with
a message that names the six DC calculations that do honor the flag, and say
the same in the tool's parameter description and the guide.

The guide also called those six "the eight DC calculations". The other two,
`branch_flow_dc` and `bus_injection_dc`, need voltage angles and so are not
matrix names at all.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NoJFzdnTLwtFZLPhUeyCvR
case9 has nine buses and nine branches, so the incidence assertion
`shape == [9, 9]` held whether the result was branches by buses or its
transpose. case14 has 14 buses and 20 branches; the shape now fails on a
swap, and the axis ids are compared against the branch identities and bus
ids `calc_dc_index_map` reports for the same case rather than only against
each other.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NoJFzdnTLwtFZLPhUeyCvR
The handle shipped with no C ABI test of its own. Build a three bus, two
branch case whose first branch states r = x = 0 and check the whole surface:
the refused build reports BUILD.OPERATOR.ZERO_IMPEDANCE, the skipping build
leaves branch row 0 out of the axis and lists it in
`skipped_branch_rows`, the bus and branch axes and one branch identity read
back, all eight calculations return a handle of the right shape, retain
outlives its release, a NULL handle reads as an empty axis instead of
faulting, and an out of range identity index returns an empty view.

The C probe gains the happy path over case9, where the axes match the bus
and branch counts and nothing is skipped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NoJFzdnTLwtFZLPhUeyCvR
`network_with_balanced_operating_point` returns the network beside the
diagnostics it raised, but `OperatingPoint.network` and
`powerio::network_with_operating_point` return the network alone. Net bus
injection quantities have no balanced network field, so they are dropped
there and nothing says so; only `emit` reports the omission, as
EMIT.OPERATING_POINT.DATA_OMITTED. Say this in the guide and the property's
docstring so a caller knows to emit the collection when the omission
matters. The facade exposes no public function that hands back both, and the
0.11.x line takes no new public signatures, so this pass documents the
behavior rather than changing it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NoJFzdnTLwtFZLPhUeyCvR
The `Generated schemas match` job runs the generator with `--check` and the
committed files still carried the 0.11.0 producer version. Regenerated with
`cargo run -p powerio-dist --example regen_bmopf_examples`.

ieee34.json and ieee123.json change only the version strings. 4bus_dy.json
also gains a `meta.provenance.powerio_bmopf_4` record: `proposal_provenance`
appends a new numbered record whenever the producer version differs from
every record already present, and that file has no vendored DSS source to
reset them, so it already carried `_1` through `_3`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NoJFzdnTLwtFZLPhUeyCvR
calc_adjacency_matrix and calc_weighted_laplacian take no such option, so
calc_matrix echoed the flag in the payload while the result ignored it,
the same defect the ptdf and lodf guard closed. The refused names now live
in one tuple the guard, the field text, and the docs share.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NoJFzdnTLwtFZLPhUeyCvR
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants