Skip to content

formats: carry the HVDC and setpoint fields the readers already read - #322

Draft
samtalki wants to merge 3 commits into
v09/abi-v5from
claude/pr-conversion-matrix-improve-crn6i0
Draft

formats: carry the HVDC and setpoint fields the readers already read#322
samtalki wants to merge 3 commits into
v09/abi-v5from
claude/pr-conversion-matrix-improve-crn6i0

Conversation

@samtalki

@samtalki samtalki commented Aug 15, 2026

Copy link
Copy Markdown
Member

Top of the v0.9.0 stack, on top of #323.

Four writers dropped, or warned about, data the matching reader was already prepared to take back. Each gap showed up in the conversion matrix as a warning naming a loss the format did not have to take. Green cells go from 21 to 27; total warnings from 727 to 689.

  • PSLF. A generator voltage setpoint is EPC's reg_kv, in kV, so a bus stating no nominal kV lost it. The bus vsched column is the scheduled magnitude in per unit and needs no base, so the setpoint rides there for those buses. Where the bus does state a base kV, reg_kv still carries it and vsched stays the bus voltage: routing a setpoint through vg * base_kv and back is not exact in binary, and a re-serialize would differ in the last digit.
  • egret. The reader read dc_branch and the writer emitted none, so every conversion into egret dropped its dclines. The writer now states the same power, voltage, and loss fields the reader takes; only a dcline cost curve, which egret has no slot for, is warned.
  • Surge. The reader reads a converter terminal's AC voltage setpoint, reactive limits, and loss model, and the writer stated none of them — the sending end's terminal voltage came back as 1.0 and the loss model as zero, silently. pt is now derived from the setpoint and the loss model rather than negated, which is the sign every other reader here stores. The reader's blanket "converter detail mapped best effort" warning becomes a count of the links that state detail differing from the neutral converter.
  • PowerModels. The dcline warning named no loss: every Hvdc field has a slot, including the aggregate bounds on mp_pmin/mp_pmax and the cost curve, and reads back exactly. Dropped.

The egret JSON and Surge JSON source rows pay part of it back — dclines that reach the next hop now report what each target does with them, where before those rows had none to carry.

Then a review pass over the stack, and the fixes it produced

  • synthesize_rate_a read only the two voltage ceilings, but the phasor difference is convex in the terminal voltages, so its largest value over the band box is at a corner — below roughly 10° the mixed one. On r=0, x=0.01, a ±2° window and a 0.9/1.1 band it returned 4.2 pu against 22.3 pu attainable, and synthesize_unrated_limits handed that to the OPF as a limit. It now takes both bands and maxes over the four corners.
  • The series admittance formed r² + x² directly, which overflows past an impedance magnitude of about 1e154 whose admittance is representable. The quotient then read as an exact zero and the branch left the DC network silently, while build_ybus errored on the same one. MIN_DIVISIBLE_MAGNITUDE documents that the bound is on the magnitude and never on the square; the computation now holds to that, in one place both the DC conventions and the guarded entry point use. Bit-identical below the overflow, so no case moves.
  • A .dss Clear reset the parse record along with the circuit, so a case that spent the include budget, or refused an include for escaping the case directory, and then cleared returned a truncated network with an empty warnings, nothing for powerio package to lift, and exit 0. Clear resets the circuit only.
  • The published package schema did not require powerio_version: schemars reads the serde default as optional and schemars(required) does not override it. A producer that validated against the $id and omitted it passed there and was refused by from_json.
  • Error::Multiconductor flattened to Parse, so a missing .dss reached through a package raised a parse error where the direct path raised FileNotFoundError with filename set. Both paths now agree.
  • set_operating_points_json reported the caller's JSON through the blanket From, which names every failure a serialization step — the same defect v0.9.0: the fixes the review of the stack below produced #316 fixed at the two powerio-pkg sites, missed at the pyo3 boundary. parse_convention also lost "pure", the third 0.8 spelling.
  • LODF, iterative path: a bridge redistributes nothing, so its column is the diagonal alone and neither the conjugate-gradient solve nor the scan behind it runs. On a radial feeder that was every solve.
  • Docs: p_shift is not zero under the new default; the bundle manifest schema_version this stack deleted; the geo sidecar's retired version key. A point sidecar states no polylines, so powerio geo reported every branch as unplaced on a run that placed everything it could.

Verified

cargo fmt --all --check, cargo clippy --workspace --all-targets at zero warnings, cargo test --workspace at 52 binaries with zero failures, and ruff check over the Python layer. The conversion matrix baseline in powerio-cli/tests/conversion_matrix_report.rs is updated in the same commit as the fidelity changes, with the reasoning for each moved cell.

No API, ABI, or schema version change, so PowerIO.jl needs nothing.

@github-actions

github-actions Bot commented Aug 15, 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.

  • 🟢 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

7 cases.

Source ↓ / target → MATPOWER .m PowerModels JSON PSS/E .raw PowerWorld .aux egret JSON pandapower JSON Surge JSON PSLF .epc
MATPOWER .m 🟢 0/0 🟢 0/0 🟡 8/8 🟡 8/8 🟢 0/0 🟡 5/5 🟢 0/0 🟡 6/6
PowerModels JSON 🟡 6/6 🟢 0/0 🟡 14/14 🟡 14/14 🟢 0/0 🟡 11/11 🟡 21/21 🟡 11/11
PSS/E .raw 🟡 13/13 🟢 0/0 🟢 0/0 🟡 1/1 🟢 0/0 🟡 3/3 🟢 0/0 🟡 5/5
PowerWorld .aux 🟡 12/12 🟢 0/0 🟢 0/0 🟢 0/0 🟢 0/0 🟡 2/2 🟢 0/0 🟢 0/0
egret JSON 🟡 1/1 🟢 0/0 🟡 9/9 🟡 9/9 🟢 0/0 🟡 6/6 🟡 1/1 🟡 11/11
pandapower JSON 🟡 6/6 🟢 0/0 🟡 11/11 🟡 11/11 🟡 5/5 🟢 0/0 🟢 0/0 🟡 11/11
Surge JSON 🟡 1/1 🟢 0/0 🟡 9/9 🟡 9/9 🟢 0/0 🟡 6/6 🟢 0/0 🟡 11/11
PSLF .epc 🟡 17/17 🟡 4/4 🟡 5/5 🟡 5/5 🟡 4/4 🟡 7/7 🟡 7/7 🟡 8/8
DeepMind OPFData JSON 🟡 3/3 🟡 2/2 🟡 5/5 🟡 5/5 🟡 3/3 🟡 4/4 🟡 2/2 🟡 4/4

Distribution

7 cases.

Source ↓ / target → OpenDSS .dss BMOPF JSON PMD JSON
OpenDSS .dss 🟢 0/0 🟡 140/140 🟡 88/88
BMOPF JSON 🟡 20/20 🟢 0/0 🟡 27/27
PMD JSON 🟡 15/15 🟡 57/57 🟢 0/0

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

@samtalki
samtalki marked this pull request as draft August 15, 2026 15:37
@samtalki
samtalki force-pushed the claude/pr-conversion-matrix-improve-crn6i0 branch 2 times, most recently from b3eeafb to 18564a8 Compare August 15, 2026 15:55
@samtalki samtalki changed the title matrix: guard the tap and impedance divisions on magnitude formats: carry the HVDC and setpoint fields the readers already read Aug 15, 2026
@samtalki
samtalki changed the base branch from main to v09/abi-v5 August 15, 2026 15:55
claude added 3 commits August 15, 2026 15:56
Four writers dropped, or warned about, data the matching reader was
already prepared to take back. Each gap showed up in the conversion
matrix as a warning that named a loss the format did not actually have
to take.

- PSLF. A generator voltage setpoint is EPC's `reg_kv`, in kV, so a bus
  stating no nominal kV lost it. The bus `vsched` column is the
  scheduled magnitude in per unit and needs no base, so the setpoint
  rides there for those buses and the reader prefers it over the solved
  voltage. Where the bus does state a base kV, `reg_kv` still carries
  it per generator and `vsched` stays the bus voltage: routing a
  setpoint through `vg * base_kv` and back is not exact in binary, and
  a re-serialize would differ in the last digit.

- egret. The reader read `dc_branch` and the writer emitted none, so
  every conversion into egret dropped its dclines. The writer now
  states the same power, voltage, and loss fields the reader takes;
  only a dcline cost curve, which egret has no slot for, is warned.

- Surge. The reader reads a converter terminal's AC voltage setpoint,
  reactive limits, and loss model, and the writer stated none of them:
  the sending end's terminal voltage came back as 1.0 and the loss
  model as zero, silently. The writer now states all three. `pt` is
  derived from the setpoint and the loss model rather than negated,
  which is the sign every other reader here stores, and the writer
  reports a line whose received power the loss model cannot reproduce.
  The reader's blanket "converter detail mapped best effort" warning
  becomes a count of the links that state detail differing from the
  neutral converter, the shape the load/branch/generator warnings
  beside it already had.

- PowerModels. The dcline warning named no loss: every `Hvdc` field has
  a slot, including the aggregate bounds on `mp_pmin`/`mp_pmax` and the
  cost curve, and reads back exactly. Dropped.

Together: 21 green conversion matrix cells to 27, and 727 warnings to
689. The `egret JSON` and `Surge JSON` source rows pay part of it back
— dclines that reach the next hop now report what each target does with
them, where before those rows had none to carry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NoWFTq3ofuu1SZ7aWUiVGf
…ment

Follow-ups from a review of the stack.

- `synthesize_rate_a` read only the two voltage ceilings, but the phasor
  difference is convex in the terminal voltages, so its largest value
  over the band box is at a corner — and below roughly 10° that corner
  is one terminal high and the other low. On r=0, x=0.01, a ±2° window
  and a 0.9/1.1 band it returned 4.2 pu against 22.3 pu attainable, and
  `synthesize_unrated_limits` handed that to the OPF as a limit. It now
  takes both bands and maxes over the four corners.

- The series admittance formed `r² + x²` directly, which overflows past
  an impedance magnitude of about 1e154 whose admittance is perfectly
  representable. The quotient then read as an exact zero and the branch
  left the DC network silently, while `build_ybus` errored on the same
  one. `MIN_DIVISIBLE_MAGNITUDE` documents that the bound is on the
  magnitude and never on the square; the computation now holds to that,
  in one place both the DC conventions and the guarded entry point use.
  Bit-identical below the overflow, so no case moves.

- A `.dss` `Clear` resets the findings along with the objects, so a case
  that spent the include budget and then cleared lost the refusal. The
  counters live on the executor and kept refusing, so the reader
  returned a truncated network with an empty `warnings`, nothing for
  `powerio package` to lift, and exit 0. The refusal is restated.

- The published package schema did not require `powerio_version`:
  schemars reads the `serde` default as optional and `schemars(required)`
  does not override it. A producer that validated against the `$id` and
  omitted it passed there and was refused by `from_json`.

- `Error::Multiconductor` flattened to `Parse`, so a missing `.dss`
  reached through a package raised a parse error where the direct path
  raised `FileNotFoundError` with `filename` set. Both paths now agree.

- `set_operating_points_json` reported the caller's JSON through the
  blanket `From`, which names every failure a serialization step: the
  message blamed our own writer for the caller's document. Same fix the
  stack already made at the two powerio-pkg sites.

- `parse_convention` lost `"pure"`, the third 0.8 spelling, so that
  caller got the generic unknown-value error instead of the migration
  message warning that `series` is a different formula.

- LODF, iterative path: a bridge redistributes nothing, so its column is
  zero and the conjugate-gradient solve behind it went unused. On a
  radial feeder that was every solve.

- The bundle doc still promised `p_shift` is zero without `Matpower`,
  named a manifest `schema_version` this stack deleted, and called
  `ReactanceOnly` deprecated where the code and `matrices.md` say it
  stays; the geo doc still showed the sidecar's retired `version` key.
  A point sidecar states no polylines, so `powerio geo` reported every
  branch as unplaced on a run that placed everything it could.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NoWFTq3ofuu1SZ7aWUiVGf
Cleanup pass over the two commits below.

The `.dss` include-budget fix was patched at the wrong depth. `Clear`
wiped `warnings` and `diagnostics` along with the circuit, and the
answer was to have the budget re-check whether its own finding survived
and restate it. That covered one of the two `Error` codes: an include
refused for escaping the case directory has no surviving counter to
restate it from, so it was lost outright. `Clear` resets the circuit,
not the record of how the script was read — the record now rides
through it, which covers both codes and drops the re-check. The
re-check also scanned `diagnostics` on every refused include, so a case
that emitted many findings before spending the budget turned the
refusal path quadratic, in the one place built to bound hostile input.

The MATPOWER dcline loss model was written out three times: as an
expression in the pandapower reader (with the hub's formula in a
comment beside it), as `pf * efficiency` in the PyPSA reader, and as a
new helper in the Surge one. `Pf`, `Pt`, `loss0` and `loss1` are one
relation on a hub type, so it lives on `Hvdc` now, with the "does this
line satisfy its own loss model" test a writer needs beside it.

Also: the LODF bridge skip reshaped a loop that has a dense twin, so it
no longer read like it — restored, and it now skips the m-entry scan as
well as the solve; the Surge converter-detail warning hand-rolled the
count-by-predicate helper the three warnings above it use, and its
field test re-implemented `num_not_default` in a form that missed
string-encoded numbers; the PSLF writer kept a second set to answer a
question its map already knew, and only reported a dropped setpoint
where one was really dropped; `read_generator` collapsed a nested chain
and two warnings differing by three words into one; the schema
generator cloned every property name to satisfy the borrow checker; the
package field doc, which schemars copies into the published schema,
sent an external validator author to a path in this repo; `dc_branch`
was the one element map egret inserted conditionally; and the law of
cosines was transcribed twice in tests beside the implementation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NoWFTq3ofuu1SZ7aWUiVGf
@samtalki
samtalki force-pushed the claude/pr-conversion-matrix-improve-crn6i0 branch from 18564a8 to 479354a Compare August 15, 2026 15:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants