Skip to content

Use PowerIO 0.9 for case and GOC3 parsing - #60

Draft
samtalki wants to merge 8 commits into
madsuite-org:mainfrom
samtalki:v09/powerio-0.9-followups
Draft

Use PowerIO 0.9 for case and GOC3 parsing#60
samtalki wants to merge 8 commits into
madsuite-org:mainfrom
samtalki:v09/powerio-0.9-followups

Conversation

@samtalki

Copy link
Copy Markdown

Use PowerIO 0.9 for balanced case parsing and typed GOC3 SCOPF rows. Remove the raw GOC3 dictionary path, preserve fixed shunts when applying multiperiod loads, and treat zero branch ratings as unbounded.

Forward numeric types positionally through parse_ac_power_data and every LoadSeries path so static and multiperiod return types remain concrete. Fix explicit case path handling and add the ahead of time inference check to CI.

Validation:

  • GOC3: 3,521 passed.
  • Ahead of time compilation: 7 passed.
  • Focused parser and inference checks: 10 passed.
  • Clean package server install of PowerIO 0.9.0: typed AC and multiperiod smoke passed.

Closes #52.

samtalki and others added 8 commits August 18, 2026 04:47
…onary fallback

The parser backend moves from ExaPowerIO's MATPOWER reader to PowerIO, which
reads MATPOWER, PSS/E, PowerWorld and PowerModels JSON through one entry point
and returns the same NamedTuple layout the model builders already consume. A
`from` keyword reaches every model constructor for the cases where the format
cannot be told from the extension. ExaPowerIO stays for `get_path(:pglib)`, the
bundled case directory, and as the reference the parser tests compare against.

Multi-period loads now come from a `PowerIO.LoadSeries` built off the same
parsed network as the power data, so the series rows and `data.bus` align by
construction rather than by both parsing the file again. The per-period loads
are merged onto the bus rows as `busarray` is built, replacing the in-place
`update_load_data` mutation. Two behaviour changes fall out: a load curve no
longer scales fixed bus shunts, which it did through `gs`/`bs` before, and
supplying only one of `pd`/`qd` raises rather than silently leaving the other
half at its base-case value.

`sc_parser.jl` stops reaching into the raw `sdd_lookup` and `sdd_ts_lookup`
dictionaries. Every field it read has a typed home on PowerIO's device rows:
`initial_status.p` is `p_0`, `p_shutdown_ramp_ub` is `p_rd_sd`,
`p_startup_ramp_ub` is `p_ru_su`, and the `p_lb` series is `p_min`. The four
`(device, t, t_prime)` row arrays behind the startup and shutdown power
capability sums never left the file, so the sums are accumulated directly; the
commitment table is joined by a uid dictionary rather than a scan per row, and
the interval end times are computed once instead of re-summing `dt[1:t]` inside
the inner loop. Checked against the pre-deletion code on
C3E4N00073D1_scenario_303: p_sdpc and all eight sums are bit identical.

The parser tests cover the four formats end to end, the isbits row types the GPU
backends require, the horizon and load-argument errors, and the shunt behaviour
above. The type stability assertion is marked broken: PowerIO 0.9.0's table
accessors return the JSON3 value union and its `T` is a keyword-typed `Type`, so
the parse cannot be inferred concretely even though every row is concrete at run
time.
The DCOPF loop in runtests only evaluated callbacks, so nothing anywhere checked that dcopf_model reaches the right objective, and test_dcopf_case had no call sites at all. Static AC already carves out one solve for the smallest case and leaves every other case on callbacks; DCOPF now gets the same treatment, calling test_dcopf_case on case3 against solve_opf with DCPPowerModel. The reference is cached per case in dcopf_ref_cache, mirroring static_ref_cache, so the two solving backends do not recompute it.

The two model builds move out of the callbacks testset so the Float64 variables stay in scope for the solve, which is how the static AC block is already arranged.

Adds 8 assertions on the slices that solve: status, objective, and case3's three generator and three branch flows. EMP_TEST_SELECTION=nothing goes 2583 to 2591, EMP_TEST_SELECTION=cpu stays at 450 because CPU() does not solve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ccepted

MATPOWER, PSS/E and PowerWorld all read `rate_a = 0` as an unrated branch, and
real cases lean on it: every arc of stock case14, case57, case118 and
case13659pegase, 4924 of 25412 in case_ACTIVSg10k, 44104 of 208242 in
case_SyntheticUSA. Nine of the twenty four MATPOWER cases in the powerio test
corpus carry at least one. Read literally, the rating became a bound of exactly
zero on the flow and a thermal limit of `p^2 + q^2 <= 0`, so stock case14 came
back INFEASIBLE_PROBLEM_DETECTED instead of solved.

An unrated branch now gets an infinite flow bound and an infinite upper bound on
its thermal limit rows. The rating stays inside the thermal expression rather
than being replaced by infinity there, so the residual and its derivatives stay
finite. case14, case57 and case118 now solve and agree with PowerModels/Ipopt to
4.2e-11, 1.4e-12 and 1.5e-13 relative. Rated cases are untouched: their bounds
are the rating and their thermal upper bound is zero, exactly as before.

The README said nothing about which case files the package reads, and imported
GOC3Benchmark, which has not been a dependency since the GOC3 parser moved to
PowerIO. It now names the formats, the `from` keyword for the ones an extension
cannot identify, the PGLib-OPF name lookup, and the MW convention on the .Pd and
.Qd files. `from` is documented on opf_args, opf_model and mpopf_model.
The wrapper forwards T as a keyword, so its inference stays unresolved even
once PowerIO pins its return type; only a positional `::Type{T}` method
resolves it. Asserting both separates the two halves: the PowerIO assertion
flips as soon as the library is fixed, the wrapper assertion flips when the
call site here follows.
PowerIO 0.9 carries document order ordinals on the SCOPF instance rows, `j_dev` within the device class and `j_sdd` in the canonical producers then consumers stacking, and the initial on status on the device, ac line and transformer rows. Every index this package derived from a uid string is now a field read: `_uidnum`, `get_j_pr`, `get_j_cs`, `get_j_prcs`, `is_pr` and the `producers_first` threading are deleted, the stacked variable numbering is arithmetic on the ordinals, and the rows that reference a device from another table (energy windows, price blocks) map through per class dictionaries built from the instance rows.

The suffix rule read the digits in a uid as the device's position in its class, so any file that does not follow the competition uid convention was misindexed, including GOC's own 14 bus validation case; PowerIO warned and continued. On C3E4N00073D1_scenario_303 the two agree everywhere: no row differs across 154 producers, 51 consumers, the active reserve set rows, 105 ac lines, 15 transformers, 1 dc line and 73 buses.

`parse_sc_data` takes the GOC3 document text and returns the data and the lengths. `goc3_scopf_data` types PowerIO's parse of it, so the periods and the interval durations come off the instance instead of a second raw parse, and the three `goc3_add_status_flags!` calls take typed rows in place of the raw lookup dictionaries.

`save_go3_solution` takes the case file, rebuilds the uid to index tables from the instance and maps every solution row through them. Its old signature could not be called: it asked for `producers_first`, which `goc3_model` never returned, and it had no call sites.

Renaming every uid in the case and the unit commitment document, so the digits the retired rule would have read run in reverse of document order, must move neither the model nor the written solution. The regression asserts both: same variable count, same objective and constraints at x0, and every written field equal to the rename of the original, uid for uid.

Verified against PowerIO on the companion `v09/scopf-instance-consumers` branch: the goc3 slice and the full suite pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PowerIO's `T` keyword is a value of an abstract `Type{<:Real}`, so forwarding the wrapper's static parameter through it dropped the specialization: the result inferred as an unparameterized NamedTuple and every field read off it widened to `AbstractArray{BusRow{T}} where T`. Forwarding positionally keeps the static parameter, which is what the wrapper's own comment says its `::Type{T}` signature exists for.

Both inference assertions now hold and stop being broken. PowerIO 0.9 pins its own return type, which flips the first, and this call site follows it, which is what the second was waiting on. A single abstract field in the parse leaves `ExaModel(CORE, data)` unresolved under `--trim=safe`, so the assertions are the guard against that regressing.

EMP_TEST_SELECTION=all: 6713 passed, 0 failed, 0 broken, 13m39s.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pass the element type positionally through every PowerIO parse and load series call so the multiperiod argument function has one exact inferred return type. Restrict PGLib fallback resolution to bare case names, and run the compiled library check as a self-contained CI slice with a private output directory.
`parse_file(BalancedNetwork, ...)` returns the concrete network type rather
than one inferred from the call, and `Val(:live)` selects the method that
keeps the handle out of the AOT call graph. Every parse and LoadSeries route
takes both, so the row element types stay isbits and `ExaModel(CORE, data)`
resolves under `--trim=safe`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

Inconsistent shunt admittance scaling in multiperiod OPF methods

1 participant