dist: JSON reader validation hardening - #267
Conversation
Track bmopf-report PR #16 (schema 0.1.0): meta case_study_generator + frequency, uppercase load model enum, per-sequence bus voltage bounds replacing the vsym arrays (multiconductor payload bumps to /2, 2.0.0), lumped three phase transformer series impedance referred per winding rating, transformer taps/neutral/no-load and the IBR, control profile, DC, and time series tables relocated under the extras escape hatch, a typed capacitor element, inline line impedance via a synthesized linecode, line i_max/s_max, and one-triangle symmetric matrix mirroring on read. The reader keeps accepting every pre-0.1.0 spelling. The vendored schema and example networks track bmopf-report@f2e3684; a regen_bmopf_examples example rebuilds the checked-in outputs. Grounded terminals now count as referenced in the unused-terminal prune, so standalone grounding survives a write. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review follow-up on the BMOPF 0.1.0 alignment: the new DistLine i_max/s_max fields wrote BMOPF but vanished silently through the dss and PMD writers, against the fidelity policy. - The PMD ENGINEERING line has its own cm_ub/sm_ub slots, so the line ratings now map both directions: the reader takes line-level cm_ub/sm_ub into the typed fields (an inline-impedance line's ratings stay on the synthesized linecode, whose sm_ub now re-inlines alongside cm_ub), and the writer emits them back. - The dss writer warns when it drops line i_max/s_max; the normamps/emergamps mapping decision stays with #266. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YPB7LVTeMcdBYnzA9QdpPe
A PMD source spells an unbounded phase as JSON null, which restores as Inf in the model; the writer's num zero fallback then turned "no limit" into a zero limit. Line and switch i_max/s_max now go through a bounds guard that drops the field with a warning when any entry is nonfinite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YPB7LVTeMcdBYnzA9QdpPe
- PMD bound arrays keep finite entries beside a null-derived infinity: generator pg/qg bounds and linecode cm_ub/sm_ub no longer drop whole. The PMD writer respells nonfinite entries as null; the BMOPF writer drops such fields with a warning instead of zeroing; the dss writer skips emergamps with a warning on a nonfinite first entry. - A PMD matrix column that fails as_array warns and stays zero instead of silently dropping the whole matrix. - New warn_unresolved_references pass on both JSON readers: dangling bus and linecode references warn naming the reference, instead of parsing into a phantom bus downstream. - The .json funnel rejects documents with neither the PMD data_model marker nor the schema-required BMOPF bus + voltage_source tables; an explicit format still overrides. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YPB7LVTeMcdBYnzA9QdpPe
Conversion MatrixLegendCells show
Transmission7 cases.
Distribution7 cases.
Full warning details: download |
There was a problem hiding this comment.
Pull request overview
Hardens powerio-dist’s JSON ingestion and cross-format conversion behavior by preserving mixed finite/unbounded bound arrays, emitting explicit warnings for partial parse failures, and tightening .json format classification to avoid silently misreading unrelated JSON documents. This fits the distribution subsystem’s goal of loss-aware conversions with explicit diagnostics rather than silent coercions.
Changes:
- Preserve mixed finite + null-derived-infinity bound arrays in PMD reads/writes, and make BMOPF/DSS handling of nonfinite bounds loud (drop/skip with warnings).
- Improve robustness of matrix parsing (warn per malformed column and keep remaining data) and add end-of-parse validation warnings for dangling bus/linecode references.
- Align BMOPF schema handling (0.1.0) including extras relocation, new typed elements/fields, and updated fixtures + baseline expectations.
Reviewed changes
Copilot reviewed 26 out of 29 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/data/dist/bmopf/example_ieee13.json | Updates vendored BMOPF IEEE13 example fixture to the newer schema/vintage layout. |
| powerio-pkg/src/package.rs | Bumps multiconductor payload schema URL/version to /2 / 2.0.0. |
| powerio-pkg/src/lowering.rs | Updates dropped-field detection for renamed per-sequence voltage bound fields. |
| powerio-pkg/examples/generate_schemas.rs | Generates multiconductor schema under pio-payload-multiconductor/2. |
| powerio-dist/tests/rich.rs | Updates expected BMOPF load voltage model strings to uppercase schema spellings. |
| powerio-dist/tests/pmd.rs | Adds/updates tests for mixed bounds, malformed matrix columns, dangling refs, and rating propagation. |
| powerio-dist/tests/matrix.rs | Adjusts conversion-matrix harness comparisons for DSS/PMD terminal renumbering and grounding/terminal pruning semantics. |
| powerio-dist/tests/bmopf.rs | Expands BMOPF tests for schema validity, one-triangle matrices, dangling refs, and nonfinite-bound dropping behavior. |
| powerio-dist/src/pmd/write.rs | Writes line-level ratings (i_max/s_max) via cm_ub/sm_ub and warns on dropped typed capacitors. |
| powerio-dist/src/pmd/read.rs | Keeps mixed finite/nonfinite bound arrays, warns on malformed matrix columns, and runs unresolved-ref warnings post-parse. |
| powerio-dist/src/model.rs | Adds per-sequence bus voltage bound scalars, line-level ratings, typed capacitors, and unresolved-reference warning pass. |
| powerio-dist/src/lib.rs | Re-exports new DistCapacitor type. |
| powerio-dist/src/dss/write.rs | Avoids emitting emergamps=inf, warns when skipping, and warns/drops unsupported line-level ratings + capacitors. |
| powerio-dist/src/dss/read.rs | Initializes new line rating fields on DSS read (currently None). |
| powerio-dist/src/convert.rs | Tightens .json format inference to avoid BMOPF “accept anything” fallback and adds tests for unclassifiable JSON. |
| powerio-dist/src/bmopf/write.rs | Writes BMOPF schema 0.1.0 $id, relocates tables into extras, adds bounds dropping for nonfinite arrays, adds capacitor emission. |
| powerio-dist/src/bmopf/read.rs | Reads schema 0.1.0 shapes (meta frequency, extras tables, transformer overflow merge), mirrors one-triangle symmetric matrices, warns on dangling refs. |
| powerio-dist/examples/regen_bmopf_examples.rs | Adds example regeneration helper for checked-in BMOPF outputs. |
| powerio-dist/examples/bmopf/ieee34.json | Regenerates BMOPF ieee34 example output consistent with schema 0.1.0 and new canonicalization. |
| powerio-dist/examples/bmopf/4bus_dy.json | Updates 4bus_dy example output to schema 0.1.0 meta + field names. |
| powerio-dist/docs/conversion-matrix.md | Updates warning-count baselines in conversion matrix documentation. |
| powerio-cli/tests/conversion_matrix_report.rs | Updates distribution warning baselines to match new schema/fixtures and warning behavior. |
| docs/src/pio-json-schema.md | Updates multiconductor payload docs to /2 and notes /1 remains served for legacy. |
| docs/schema/pio-package/0.1/schema.json | Regenerates package schema reflecting new fields/types (per-sequence bounds, line ratings, capacitors). |
| CHANGELOG.md | Adds unreleased notes covering validation hardening and BMOPF schema 0.1.0 alignment. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
A parsed [] / null / string got routed to the BMOPF reader for its 'top level is not an object' error; the classifier's own message naming the PMD and BMOPF identification rules is the clearer failure. The BMOPF fallback now covers only malformed JSON (the parse error case). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YPB7LVTeMcdBYnzA9QdpPe
|
The "PowerIO.jl against this C ABI" failure is the companion-branch mechanism, not this PR's code: the failing "BMOPF v0.6.2 fidelity gates" assertions pin the pre-0.1.0 writer shape ( Resolution is sequencing: once #264 and its PowerIO.jl companion merge, a rebase of this branch onto main goes green. Alternatively, pushing a PowerIO.jl branch named Generated by Claude Code |
The dangling-reference fixture used v_mag where the BMOPF reader (and writer) spell v_magnitude, so the source parsed with an empty magnitude array and an extras warning the test never meant to create. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YPB7LVTeMcdBYnzA9QdpPe
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 29 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
powerio-dist/src/model.rs:942
warn_unresolved_referencescurrently skips empty bus IDs (""), but the graph projection will still synthesize a phantom bus for an empty string. This means a missingbusfield (which several readers parse as an empty string) can still slip through with no warning. Consider warning on empty bus references explicitly instead of skipping them.
let mut bus = |what: &str, id: &str| {
if !id.is_empty() && !buses.contains(&id.to_ascii_lowercase()) {
warnings.push(format!("{what}: references undefined bus `{id}`"));
}
};
A reader leaves an empty string where the field is missing, and the graph projection synthesizes a phantom bus for it the same as for a typo; the pass now names the empty field instead of skipping it (the doc comment claimed the parse site warns, which it does not). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YPB7LVTeMcdBYnzA9QdpPe
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 29 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
powerio-dist/src/dss/write.rs:946
- This warning is emitted while writing a DSS switch, but the message prefixes it with
line. Consider changing it toswitchfor clarity and consistency.
Some([]) => self.warn(format!(
"line {}: i_max is empty; emergamps not emitted",
sw.name
)),
The renumbering-leg arity fallback exists for named terminals that dss positions and PMD integer connections cannot carry; numeric maps survive those legs verbatim, so letting them fall back too would hide a silent permutation. The dedicated renumbering-scheme pin stays with #266. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YPB7LVTeMcdBYnzA9QdpPe
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 29 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (2)
powerio-dist/src/pmd/write.rs:295
line_ratingswrites line-leveli_max/s_maxwithjson!(Vec<f64>), which will panic if any entry is non-finite (PMD null restores to ±Inf). These arrays should be emitted with per-entry finite→number / non-finite→null conversion so mixed finite/null bounds round-trip safely.
fn line_ratings(o: &mut Map<String, Value>, l: &DistLine) {
if let Some(i_max) = &l.i_max {
o.insert("cm_ub".into(), json!(i_max));
}
if let Some(s_max) = &l.s_max {
o.insert("sm_ub".into(), json!(s_max));
}
}
powerio-dist/src/pmd/write.rs:326
- When re-inlining impedance for
pmd_inlinelines, the writer emits synthesized linecode ratings viajson!(Vec<f64>). With the new behavior of preserving non-finite entries ini_max/s_max, this can panic during serialization; emit non-finite entries as JSON null instead.
Some(c) if inline => {
insert_impedance_matrices(&mut o, c, net.base_frequency);
if let Some(i_max) = &c.i_max {
o.insert("cm_ub".into(), json!(i_max));
}
if let Some(s_max) = &c.s_max {
o.insert("sm_ub".into(), json!(s_max));
}
Closes #262. Stacked on #264 (
worktree-bmopf-schema-010) — the first three commits here are that PR; review the last commit only. Merging #264 first collapses this diff to the validation work."pg_ub": [500.0, 500.0, null]keeps its finite phase bounds: the null-derived infinity stays in the array instead of dragging the whole field toNone. The PMD writer respells nonfinite entries as null (PMD's own unbounded spelling), the BMOPF writer drops such a field with a warning instead of coercing to a zero limit, and the dss writer skipsemergampswith a warning when the firsti_maxentry is nonfinite (it previously printedemergamps=inf).as_arraywarns (naming the element, key, and column) and stays zero; the parseable columns survive instead of the whole matrix dropping toNonesilently.warn_unresolved_referencespass runs at the end of both JSON readers: any element referencing an undefined bus, and any line referencing an undefined linecode, produces a warning naming the reference. Previouslygraph.rs::bus_indexsynthesized a phantom bus, so a typo parsed cleanly into a topologically wrong network. Comparison is ASCII case-insensitive, matchingDistNetwork::bus/linecode.parse_file/parse_strroute a.jsonto PMD on thedata_modelmarker and to BMOPF on the schema-requiredbus+voltage_sourcetables; anything else errors with a message naming both rules (a PowerModels document used to fall through and parse into a bogus near-emptyDistNetwork). Malformed JSON still routes to the BMOPF reader for its parse error, and an explicit--from/format override still forces a reader.Not addressed here, filed separately: a multiconductor
.pio.jsonpayload serializes nonfinite floats (a PMD-sourced Inf bound, a BMOPF missinglengthNaN) as JSON null, which the payload reader then rejects — that predates this change and needs its own treatment.All four #262 acceptance criteria are covered by new tests (
mixed_finite_and_null_generator_bounds_survive,malformed_matrix_column_warns_and_keeps_the_rest,dangling_references_warn,bmopf_dangling_bus_reference_warns,parse_file_rejects_unclassifiable_json), and the conversion-matrix baselines are unchanged — no fixture leg gained or lost a warning.🤖 Generated with Claude Code
https://claude.ai/code/session_01YPB7LVTeMcdBYnzA9QdpPe
Generated by Claude Code