feat: powerio-dist, a multiconductor distribution crate with dss/PMD/BMOPF converters#82
feat: powerio-dist, a multiconductor distribution crate with dss/PMD/BMOPF converters#82samtalki wants to merge 20 commits into
Conversation
New workspace member for the multiconductor distribution domain: typed model in wire coordinates with lossless converters between OpenDSS .dss, PowerModelsDistribution ENGINEERING JSON, and the draft BMOPF schema (frederikgeth/bmopf-report). This commit adds the crate skeleton, the workspace wiring (members, default-members, dep pin), and the CI clippy and test coverage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BMOPF draft schema + both example networks (frederikgeth/bmopf-report f93bca6), IEEE 13/34/123 feeders from the official OpenDSS test case tree (dss-extensions/electricdss-tst 3b20839), and eight original micro cases isolating the four transformer subtypes, switch state, a four wire linecode, constructor defaults, and a ten conductor linecode. All .dss cases solve in OpenDSS. tools/solve_dss.py dumps reference node voltages via opendssdirect; tools/pmd/ is a scratch Julia project that generates and checks ENGINEERING JSON with PowerModelsDistribution. Anchor the Python dist/ and build/ gitignore patterns to the repo root so the tests/data/dist fixture tree is trackable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tokenizer mirrors OpenDSS's TParser: comma and equals delimiters, the five quote pairs, ! and // comments, @var substitution with node suffix retention, and quoted tokens evaluating as RPN (ten register stack, degree trig, exact reference shift semantics). The script layer splits command lines with line level block comments, resolves verbs and property names with the reference's exact-then-first-prefix rule over the definition order tables extracted from epri-dev/OpenDSS-C, follows Redirect/Compile relative to the including file, and accumulates New/Edit/~/like assignments into raw objects with values kept as untyped tokens for the readers to interpret. Property tables cover line, linecode, load, transformer, vsource, capacitor, generator, swtcontrol, and regcontrol; other classes parse untyped. Parser vars live on the accumulator so definitions cross redirect boundaries. Integration tests pin object counts on the vendored IEEE 13/34/123 feeders (nested redirect resolution included) and the micro cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DistNetwork is the multiconductor hub in wire coordinates: string bus ids, ordered terminal names (OpenDSS node numbers as strings, ground as terminal "0" in the bus's grounded set), terminal maps on every element, SI units and radians. The dss reader lowers the raw layer into it, materializing every constructor default explicitly and recording each materialization per element in DistNetwork::defaulted; specified properties outside the typed fields ride in extras so writers can reproduce them. Semantics verified against the engine and the reference source: the ProcessBusDefs node fill rule, To_Meters factors with ConvertLineUnits' none handling (SI conversion of both sides reproduces the engine's length multiplier in every case), sequence to phase matrices, switch lines becoming ideal switches with SwtControl state resolved in source order, wdg context and array forms on transformers (numeric arrays through the RPN capable vector parser), capacitors as phase only shunt admittances, and the constructor defaults table dumped empirically by tools/verify_defaults.py (generator kW=1000/PF=0.88 per the constructor, not the property display strings). Bus and node sets match dss.Circuit.AllBusNames()/dss.Bus.Nodes() on IEEE 13, 34, and 123. Vendored fixture JSON is linguist-vendored so language stats stay Rust. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
write_bmopf_json emits the strict document: schema valid wherever the draft schema permits the data, with every dropped field named in the conversion warnings. Generators map a fixed injection to pinned p_min=p_max bounds (BMOPF carries bounds and cost, no dispatch setpoint); a missing cost emits 0 with a warning. Three phase wye-wye units decompose into one single_phase entry per phase, the convention the public example networks use; center tap secondaries collapse into the shared two winding shape with the xht/xlt split reported as dropped. The ten conductor linecode emits double digit matrix keys the draft schema's single digit patterns reject; a test pins the rejection as the concrete artifact behind the schema feedback. parse_bmopf_str is liberal where the writer is strict: out of schema fields land in extras with warnings, transformer subtypes ride in extras so writing back reproduces the grouping, and DistBus gained the four voltage bound families so the ENWL example round trips. Both public examples parse, re-emit schema valid, and round trip to model equality; negative tests cover missing required fields, unknown fields, enum case, wrong types, and negative linecode ampacity (the draft constrains linecode i_max items but not switch i_max, an asymmetry the tests note for feedback). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
write_pmd_json reproduces the conventions PMD's own dss2eng emits: integer terminals with the grounded neutral as terminal 4 and zero rg/xg, ENABLED status and source_id strings, linecode cm_ub from the emergency rating, b_fr/b_to as cmatrix halves in nanofarads per meter (the numeric convention the ENGINEERING JSON actually carries), the delta-wye lag connection's barrel roll with polarity -1, per winding tap arrays with the engine's 0.9/1.1/(1/32) defaults, the switch as a 1e-7 ohm series element, and voltage source Thevenin matrices computed with the engine's short circuit formulas. parse_pmd_str inverts all of it, restoring null to Inf or NaN by field suffix and rebuilding matrices with inner arrays as columns; raw b_fr/b_to and rs/xs arrays ride in extras so ENGINEERING round trips stay bit exact across the basis changes. The model convention moved with it: implicit dss ground connections now materialize as a perfectly grounded neutral terminal named max(4, highest node + 1), matching PMD and the public BMOPF examples, and i_max carries the emergency rating everywhere. Voltage source magnitudes and angles use the engine's general phase formulas. Reference fixtures under tests/data/dist/pmd were generated by PMD itself (provenance and regeneration command in the fixture README); the agreement tests pin bus maps, grounding, powers, ratings, impedances (to PMD's rounded 1609.3 m mile), and the Thevenin matrices against them. PMD parse_file accepts our emitted JSON for transformer free cases; documents with windings of unequal connection counts hit a PMD JSON reader limitation that rejects PMD's own print_file output the same way, pinned in tests and noted for upstream feedback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ness write_dss regenerates a solvable case from the model: linecodes in meters with cmatrix recovered from the susceptance halves, elements with explicit bus dots (perfectly grounded terminals emit as node 0, the reader's exact inverse), materialized defaults emitted explicitly, extras whose keys are class properties reproduced verbatim, and a VoltageBases/Calcvoltagebases/Solve tail from a per bus voltage estimate propagated from the sources through lines and transformer ratios. DistNetwork::to_format dispatches all three writers behind the echo tier: writing back to the source format returns the retained source bytes. The matrix harness pins the full 3x3: diagonal byte identity on all 15 fixtures, canonical writer idempotence for every (fixture, target) pair, and off diagonal round trips compared on the common projection with the lossy transforms named per cell (BMOPF transformer restatements; grounded terminal identity for the one public example that grounds phase terminals). docs/conversion-matrix.md is generated by an ignored test; every cell passes. serde_json's float_roundtrip feature is on: the default parser can sit one ULP off its own serializer, which the round trip contracts cannot absorb. tools/physics_check.py re-solves every emitted dss case against its original under opendssdirect with control actions frozen on both sides (the converter drops RegControl to fixed taps by documented policy). Canonical regeneration currently meets the 1e-8 bound on IEEE 13 and all micro cases (the degenerate defaults case surfaced a real bug, now fixed: a defaulted load kv has to materialize into the model like every other default); the remaining JSON path deviations are the documented BMOPF model losses plus small residues under diagnosis. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three conversion fidelity fixes driven by the physics oracle: - The dss reader now applies the engine's %loadloss coupling (setting it rewrites %R to %loadloss/2 on the first two windings), so regulator transformers keep their real resistance through the JSON formats instead of the 0.2 percent default. - The dss writer reconstructs Z1/Z0 sequence impedances from ENGINEERING rs/xs Thevenin matrices (z1 = self - mutual, z0 = self + 2 mutual), so a source that lost its MVAsc form through PMD keeps its stiffness. - tools/physics_check.py freezes control actions before the in-file Solve on both sides, tightens the solver tolerance to 1e-10 (the default 1e-4 swamps the bound), and normalizes deviations per bus. The deliberately degenerate defaults fixture surfaced an OpenDSS behavioral asymmetry, reproduced in isolation: an untouched load seeds VBase 7200 V while writing kv=12.47 (the same default) computes 12470/sqrt(3), a 1.2e-4 relative difference in the load's linearized admittance. Materialized defaults are self consistent; the residual voltage deviation on that fixture is engine state, not data, and the element admittances agree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The z0/z1 emission uses the lowercase keys and sorted order a reparse reproduces from extras, so the canonical text reaches its fixed point on the first write. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…cs gate A switch that came through the ENGINEERING model carries PMD's series matrices; the dss writer now overrides the engine's switch dummy with sequence values derived over the forced length, taking IEEE 13 through PMD and back to 1.8e-9 pu agreement. The PMD writer reports every dropped extra per element (vminpu and friends fell silently before). tools/physics_check.py now classifies all 33 cells: the 1e-8 bound holds wherever conversion fidelity is the only variable; cells with a documented cause (BMOPF constant power loads, the center tap collapse, no vminpu field in ENGINEERING, PMD's 1e-7 ohm switch convention against the engine's 1e-3 ohm dummy, regulator bank restatement, and the engine's written versus defaulted property seeding) carry their reason and a bound, and the script exits nonzero only on unexplained deviations. Current run: every cell passes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The matrix harness gives each dss reparse a unique temp path (the test binary runs threads in parallel and raced on a shared file), bus_ref reports non numeric terminal names it cannot turn into dss nodes, and the physics script cleans its staged copy up on the exception path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nd Python wheel powerio-dist grows the dispatch layer the bindings share: parse_str / parse_file / convert_str / convert_file with format inference (.dss is OpenDSS; .json is sniffed for the top level PMD ENGINEERING data_model key against the BMOPF layout, via serde IgnoredAny so a nested or quoted occurrence is not the marker). Format names are validated before any file read or parse, and the one-shot converters merge the reader's parse warnings into the returned Conversion: with no handle to query, that is the only place the loud half of the contract can surface. powerio-capi gains a `dist` cargo feature (off by default, like `arrow`) with pio_dist_parse_file / pio_dist_parse_str / pio_dist_warnings / pio_dist_to_format / pio_dist_convert_file / pio_dist_convert_str / pio_dist_network_free behind an opaque PioDistNetwork handle, following the existing errbuf/warnbuf conventions. Additive only; PIO_ABI_VERSION stays 3. The header is regenerated (PIO_DIST guard via cbindgen [defines]), smoke.c exercises the surface under -DPIO_DIST, and a c-abi-dist CI job mirrors the arrow one. Shared tails were deduplicated along the way: finish_handle now underlies both handle constructors (IndexCore::build moves under the panic guard), finish_conversion underlies all four converters, and required_cstr replaces eleven inline NULL/UTF-8 checks. The CLI convert subcommand accepts dss / pmd-json / bmopf-json targets and routes by family with FormatArg::transmission()/distribution(); a cross family request errors with the family message even when --from is omitted (unambiguous extensions decide), and read_network rejects distribution --from values for the transmission-only subcommands instead of letting the hub report a confusing unknown format. The Python wheel ships powerio.dist unconditionally (DistCase + parse/convert functions returning the shared Conversion namedtuple), with type stubs and a test suite; io errors map to the precise OSError subclass to match the transmission surface. Part of #2 / #53. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…anguage map updates The powerio-dist crate doc now leads with the three formats, states the fidelity contract including defaults materialization, and documents the float formatting policy: shortest round trip representation everywhere, float_roundtrip on the parse side so canonical writes are idempotent, null for nonfinite in PMD (suffix restoration), 0 plus a warning in BMOPF. The workspace README lists the crate and the distribution formats, the crate README points at the generated conversion matrix and the oracle harnesses, and docs/languages.md gains the dist naming map across Rust, Python, and the C ABI. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…onvergence review A review pass over the whole branch with the OpenDSS source as ground truth surfaced defects in every module; all are fixed with focused tests. dss engine semantics, verified against epri-dev/OpenDSS-C: the Load property table gains the missing vlowpu slot (its absence shifted every later positional index); var values store brace wrapped so RPN substitution evaluates; Compile keeps the redirected directory while Redirect restores it; Class.Name.Prop= references canonicalize abbreviations, and the prop= / objname.prop= forms edit the active object; an unknown named property resets the positional pointer; vsource magnitude uses the engine's polygon chord formula (basekv*1e3*pu/(2 sin(pi/n))); units=none on either side of a line/linecode pair takes the other side's units for the length product; 2 phase wye capacitors divide kv by sqrt(3) and conn=ll means delta for capacitors and generators; the load/generator kw/kvar/pf spec follows the last written property, matching RecalcElementData; malformed matrices warn and keep their text instead of silently substituting defaults recorded as "defaulted". dss writer: Set VoltageBases derives from the stashed basekv token so the sqrt(3) round trip is a fixed point; written phases/conn/kv tokens stash in extras and the writer prefers them (a 2 phase delta load no longer reconstructs as 3 phase, a 1 phase delta no longer re-emits as wye); unrepresentable names warn; Set options re-emit verbatim and dropped commands warn; non numeric terminal names positionalize to their bus index; half present impedance pairs warn instead of evaporating; degenerate shapes warn instead of panicking. bmopf: shunt G/B size mismatches pad instead of zeroing the smaller matrix; the center tap collapse converts resistance through ohms (it was 4x off on the 240 V base); unknown configurations, subtypes, and malformed matrix keys warn into extras; empty matrices emit schema valid zero matrices loudly; a missing voltage source warns; 3 wire wye-wye maps route to Unsupported instead of decomposing wrongly. pmd: status, polarity (euro lead transformers no longer force convert to the ANSI lag connection), inline line impedances, per phase taps with their bounds, settings/files, bus rg/xg, switch impedances, and linecode sm_ub all round trip through extras stashes instead of silently rewriting to defaults. API, pre release: convert_file/convert_str take a typed DistTargetFormat and the argument order is input, target, source across Rust, C, and Python; DistTargetFormat gains FromStr and name(); Conversion is non_exhaustive; Mat is re-exported; DistNetwork's manual Default seeds 60 Hz; to_format documents the parse warning split and the mutate-then-echo caveat; pio_dist_warnings returns an owned string (warning text is unbounded); Python io errors raise the precise OSError subclass with the path attached. Gates: the physics harness now stages Controlmode/tolerance for the object=circuit spelling too, which tightened the IEEE 34/123 baselines from 1e-4 convergence noise to 1e-11 and let three documented carve-outs drop to the plain 1e-8 bound; an empty emission glob fails instead of passing vacuously; the matrix harness compares x_series, sizes, endpoints, and both terminal maps; the unused IEEE 34 Run wrapper is dropped from the fixtures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…er token safety
Compile pins the working directory to the compiled file's own directory at
exit (the engine never reassigns CurrDir), so a Compile inside a Compile no
longer leaks the inner script's directory into the caller's relative paths.
The load kw/kvar/pf specification now follows the engine across edit
boundaries: the raw layer records a checkpoint at the end of every object
command line (like= splices carry the source's checkpoints), and the reader
replays a per edit state machine with RecalcElementData semantics at each
boundary. A load specified by kvar whose kw arrives in a later edit (~, Edit,
like=) derives q from the rederived power factor, matching opendssdirect;
the single line case keeps the once-only recalc. Generators stay on the
verified eager write-time fold.
The dss writer routes every option and extras value through one tokenizer
aware emitter: values pick the first quote pair whose closer they do not
contain (the lexer protects comment characters inside quotes), and values no
wrapper can carry emit as written with a warning instead of silently
splitting onto the next positional property on reparse. The derived option
skip and the reader's base frequency pickup accept engine style
abbreviations (Set volt=, Set defaultb=), non numeric source extras warn
before falling back, source phases= derives from the terminal map instead of
re-energizing de-energized phases, and maps shorter than the dss conductor
fill warn that a reparse materializes the neutral.
The PMD reader processes document sections in a fixed order (linecode before
line), so an inline impedance line whose synthesized {name}_z code collides
with a real linecode takes the collision suffix instead of silently sharing
the name; linecodes size from the widest of the six matrices, so an xs-only
ten conductor code reaches the BMOPF double digit key warning. The BMOPF
center tap collapse converts each half winding through its own s_rating
(unequal half ratings were 20 percent off) and warns when the discarded half
ratings differ from the primary's.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t; wrap empty values The engine binds Set option abbreviations to the FIRST option in table order, so prefixes of defaultbasefrequency shorter than `defaultb` mean DefaultDaily, and `ca` means CapkVAR. The reader's frequency pickup and the writer's derived key skip now stop at the unique resolution point instead of claiming every prefix, and calcvoltagebases leaves the option skip entirely (it is a command, never a Set option). An empty extras value emits as `()` instead of `key=`, which made the lexer eat the next token as the value. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
summoning @deakinmt @amritanshup7 @frederikgeth @MartaVanin for their wisdom |
Updated and pushed to the experimental branch here
Updated and pushed that that same branch
Also updated
Also updated (although as it is draft so not taking too seriously so I am not for example point to the branch)
I assume it is referring to the models in the (private) OpendssToPmd repo, yes this is correct that it would be good to get some counterexamples which function as counterexamples (todo)
To discuss (even the Optional parameters at present need confirming)
Updated
Transformer impedances are on the todo list / are derived from a (non-standard) ieee 13 bus pmd case file
Transformer impedances are on the todo list / are derived from a (non-standard) ieee 13 bus pmd case file
The section of the spec where this is covered has been clarified to make the point that generators with fixed P, Q make sense to be captured by a load element with negative value for the powers.
We won't have IEEE 13 node as an example in the long-run due to the issues around licensing, and probably we will support wye-wye in the next spec version, so this can be fixed but not super high priority.
There is are no '0' nodes in the ieee case example shared. I have updated this so that only nodes '4' are perfectly grounded (in the PMD file I am taking these from, it appears ambiguous which nodes are perfectly grounded). NB: I am wondering if we explicitly park the ieee 13 bus case as one of these examples as long-term we are not going to be able to support it anyway due to the licence issue.
Happy either way here - not sure there are that many use-cases exist for different from / to values of these parameters, so perhaps we combine. NB we spell out *_from in bmopf rather than *_fr (change from pmd). |
|
Naming note for when this revives, following the taxonomy now codified in docs/languages.md (#93): BMOPF and PMD engineering data are single-document JSON formats, so they belong to the 🤖 Generated with Claude Code |
…tion data The opendss tree retains the upstream BSD 3 clause notice verbatim (its redistribution condition), the authored micro cases release under CC BY 4.0, the PMD exports carry their sources' licenses, and the bmopf directory documents the vendoring basis and the underlying data lineage (the ENWL example derives from the CSIRO four wire dataset, CC BY 4.0) while tracking whatever license the task force publishes for the schema and examples. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… surface Union merge with the 0.1.0 release line. The capi gains both optional features side by side (dist and gridfm; pio_read_gridfm now routes through the shared finish_network tail, which also moves its IndexCore build under the panic guard); the CLI FormatArg carries the gridfm read-only variant next to the distribution targets with the family routing intact; the README moves the distribution formats from "under development" to supported and points at the conversion matrix and the fixture licenses. Taxonomy conformance with the hub's new convert_str(text, to, format): the dist converter's source parameter is now `format` in Rust and Python, matching the transmission sibling exactly (required rather than defaulted, since in-memory text has no extension to infer from). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Thanks for the quick turnaround on all of this @deakinmt .
On licensing:
|
|
Awesome, thanks. This is very exciting progress! |
|
Here's some notes on transformer models and load models I generated a while back, to serve as extensions of the spec. Haven't been critically reviewed yet, so feel free to ask Fable to do that first. update: new load model note |
|
@samtalki Do you have any BMOPF JSONs for the IEEE test cases? Then I can use those to test my Julia parser and data (quality) profiler? |
|
Confirming that based on preliminary testing in frederikgeth/BMOPFTools.jl#11, there are no significant mismatches between parsing through PMD and through Powerio. (And the shunt issue should be resolved when merging the changes on main back into this branch). For now I recommend to stick with a raw conversion from OpenDSS into the BMOPF JSON. We need to do inference on the OpenDSS data to tag the neutral wires/terminals and tag things like voltage regulators. Let me first work through that in my package. We can port / move that functionality down the line from BMFOPFTools to PowerIO. |
|
by the way, in case you are not aware, there's more test cases hiding here: https://github.com/tshort/OpenDSS/tree/master/Distrib/IEEETestCases |
Scope
New workspace crate
powerio-dist: a multiconductor distribution network model in wire coordinates with lossless conversion between OpenDSS.dss, PowerModelsDistribution ENGINEERING JSON ("PMD JSON"), and the draft JSON schema of the IEEE PES Task Force on Benchmarking Multiconductor OPF ("BMOPF JSON", frederikgeth/bmopf-report). The surface ships in all bindings: Rust (powerio_dist::{parse_file, parse_str, convert_file, convert_str}), the CLI (powerio convert feeder.dss --to pmd-json), Python (powerio.dist), and the C ABI (pio_dist_*behind adistcargo feature of powerio-capi, additive, no ABI bump). PowerIO.jl is untouched; a follow-up issue tracks its dist surface.Closes #2 (reframed per the discussion there). Starts #53.
Design
The canonical model (
DistNetwork) is a network in wire coordinates: string bus ids, ordered string terminal names per bus, explicit grounding, terminal maps on every element, SI units and radians internally (BMOPF semantics, the most explicit of the three formats). The transmissionNetworkis positive sequence and stays separate; the two hubs share conventions, not types. dss node 0 connections materialize as a perfectly grounded terminal, matching both PMD and the public BMOPF examples.Fidelity contract. Same as the transmission crates, two tiers. Writing back to the source format echoes the retained source text byte for byte. Every cross format write regenerates from the typed model and reports each field the target cannot represent in
Conversion::warnings; nothing drops silently. The one-shot converters (convert_file/convert_str, and their C and Python forms) merge the reader's parse warnings into the result, since there is no handle to query them from.Explicit defaults with provenance. The dss reader materializes every OpenDSS class default into an explicit model value and records which fields were defaulted (
DistNetwork::defaulted). The default table was read out of the OpenDSS source (epri-dev/OpenDSS-C class constructors) and then verified empirically againstopendssdirect; two display strings in the source disagree with the constructor values (generator kW and PF), and the constructor wins. Consequence: BMOPF output is always fully explicit, and a degenerate circuit converts with a warning naming each materialized default.Format conventions reproduced exactly. dss: TParser semantics (delimiters, quote pairs,
~/More continuations, Redirect/Compile,@varinterpolation, RPN expressions, property name prefixes,|matrix rows, bus dotting), per the OpenDSS source. PMD JSON: column major matrices,nullfor nonfinite with field suffix restoration, uppercase enums, kV/kW scales, degrees, the ENGINEERINGdata_modelmarker; PMDparse_fileaccepts the emitted JSON unchanged (one documented exception below). BMOPF JSON: schema valid output, flat matrix keys, fully explicit values; every emitted document validates against the vendored schema in tests.Conversion matrix
Generated into
powerio-dist/docs/conversion-matrix.mdby an ignored test.echois the byte exact diagonal;okis a canonical write that reparses to the common projection of the model; warning counts are fidelity losses the conversion reports individually.Diagonal byte identity and canonical idempotence (
write(parse(write(parse(x)))) == write(parse(x))) hold on every fixture. Off diagonal round trips reproduce the typed model exactly on the fields both formats carry.Oracle gates
tools/physics_check.py): every emitted.dssre-solves inopendssdirect(controls off, tolerance 1e-10) and is compared per node against the original. Where conversion fidelity is the only variable the worst deviation meets the 1e-8 pu bound (IEEE 13 canonical 2.2e-9, IEEE 13 through PMD 1.8e-9, IEEE 34 and 123 canonical at 1e-11, micro cases at machine epsilon). Cells above the bound carry a documented cause that the conversion also reports as warnings: BMOPF holds constant power loads only, the center tap collapse to two windings, novminpuin the ENGINEERING model, PMD's 1e-7 ohm switch convention, regulator banking, and an engine asymmetry where OpenDSS seeds an untouched load's voltage base differently from a writtenkv=(isolated in a four line reproduction; affects any pipeline that materializes defaults).tools/pmd/pmdtool.jl): PMDparse_fileaccepts the emitted PMD JSON and agrees with PMD's own parse of the source.dss(with the documented stripped field list). Exception: PMD v0.16parse_jsoncannot read ragged transformerconnections(its_fix_arrays!hcats lists of lists), which affects PMD's ownprint_fileoutput for the same cases identically; verified, and carved out of the gate with a comment.Feedback on the draft BMOPF schema
Observations from building a strict reader and writer against the public frederikgeth/bmopf-report repo, offered as input to the task force. The ten conductor fixture in this PR is the concrete artifact for the first item.
^R_series_\d_\dmatches single digits only, so a 10x10 linecode emitsR_series_10_10keys the schema rejects; the pattern is also unanchored at the tail, soR_series_1_1junkvalidates.^R_series_\d+_\d+$fixes both (same forX_series,G_from/G_to,B_from/B_to, and the shuntG_i_j/B_i_jkeys).s_ratingunits. Documented as kVA, used as VA (their issue #1). The example values only make sense as VA.$id. The schema's$idpoints at an earlier repository URL.additionalProperties: falsevs extensibility. Schema valid output cannot carry any sidecar data, so every conversion into BMOPF is lossy by construction (this PR keeps dropped fields in the in-memory model so round trips through BMOPF stay lossless, and reports each drop). Whether the schema wants an explicit extension mechanism is an open question worth settling before tooling proliferates.switch.i_maxitems are unconstrained whilelinecode.i_maxitems must be nonnegative; a negative switch ampacity validates.example_ieee13.json. The substation transformer's series impedance does not pass dimensional analysis against the prose "ohms on the wye side": ohms on the 4.16 kV wye side gives 2.77e-4 for the entered 0.008 percent XHL, while the example holds 2.116e-4, which works out to the delta side base divided by 1000. The single phase regulators show a similar factor. Possibly a kV/V or kVA/VA slip in the example generation; the OpenDSS re-solve gate in this PR is consistent with the prose interpretation.p_min = p_max. A normative statement on whether pinned bounds are the intended encoding would help.single_phaseunits; this convention is currently inferable only from the example and deserves a sentence in the spec.example_ieee13.json. The example marks the highest numbered terminal of every bus perfectly grounded, including three wire buses where that terminal is a phase (bus 632, terminals["1","2","3"], grounded["3"]). This PR grounds only where OpenDSS grounds (node 0 connections).b_fr/g_frsplit vs a singleb/gon lines and shunts remains an open modeling question; either answer is implementable, but the choice affects every converter.Deferred, with follow-up issues
Release notes for the maintainer
powerio-capi --features arrowfor the five platform tarballs; shipping the dist surface to Julia and other C consumers means addingdistto that feature list at the next release. Decide alongside whether powerio-dist joins the crates.io publish list.🤖 Generated with Claude Code