Unify the trait-vs-list pattern in power flow aux var provisioning - #252
Unify the trait-vs-list pattern in power flow aux var provisioning#252luke-kiernan wants to merge 2 commits into
Conversation
Closes #193. `branch_aux_vars`/`bus_aux_vars` enumerated, per PowerFlowContainer type, which auxiliary variables that container provides, and the read-back guard in `calculate_aux_variable_value!` asked the question a second way via `in` against both lists. Replace both with a single dispatch surface: _pf_provides_aux_var(::Type{T}, pf_data)::Bool with a `false` fallback, so containers that provide nothing (PSSEExporter, and network models running no evaluator) keep their no-op behavior by inheriting it rather than by returning an empty list. The runtime-flag cases (loss and voltage-stability factors, gated on `get_calculate_*`) now take the same shape as the rest; only their right-hand side differs. The three DC containers collapse into one `PowerFlowData{<:AbstractDCPowerFlow}` method instead of three identical ones. `branch_aux_vars`/`bus_aux_vars` survive as thin derivations of the trait, since registration genuinely needs a list of which aux vars to create. They filter `POM.pf_aux_var_types(C)`, a new tuple-valued trait in POM giving the universe of power flow aux vars indexed by branch and by bus. The derivation is written so all of the type reasoning folds at compile time: the universe is a tuple literal, and `map` visits each element with its concrete `Type{T}` known, so every trait call resolves statically and the constant methods fold away. `provides` must be a named function with a `where {T}` parameter rather than the equivalent lambda -- Julia declines to specialize on an argument slot holding a `Type` unless a type parameter binds it, and with a lambda every DC container and the PSSEExporter left unfolded `::Bool` calls. Verified with `code_typed`: all six container types over both component types leave zero residual predicate calls and zero dynamic dispatch, against a fully dynamic `Filter`/`Generator`/`collect` chain before. Since the runtime path no longer reflects over the type tree, a new `PowerFlowAuxVariableType` missing from the tuples would silently never be registered. A new test closes that gap by asserting exhaustiveness via `IS.get_all_concrete_subtypes`. Verified list-for-list equivalence with the previous hardcoded lists for all five container types plus the `calculate_loss_factors` path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR refactors how power-flow auxiliary variables are declared and gated, replacing hand-maintained per-container lists and list-membership guards with a single dispatch-based trait (_pf_provides_aux_var(::Type{T}, pf_data)::Bool). It introduces a tuple-based “universe” of power-flow aux var types (pf_aux_var_types) so registration can still derive concrete lists while keeping predicate checks consistent and enabling compile-time folding.
Changes:
- Added
POM.pf_aux_var_types(::Type{ACBranch|ACBus})as tuple-valued traits enumerating allPowerFlowAuxVariableTypes by indexing component. - Replaced list-membership read-back guarding with
_pf_provides_aux_var(get_entry_type(key), pf_data)and implemented_pf_provides_aux_varspecializations per power-flow container type (including runtime-flag gating for loss/stability factors). - Added a CI-guarding test asserting
pf_aux_var_typesexhaustively enumerates all concretePowerFlowAuxVariableTypes and that the branch/bus tuples are disjoint and remain tuples.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
test/test_power_flow_in_the_loop.jl |
Adds a reflection-based test to ensure pf_aux_var_types remains exhaustive/disjoint/tuple-valued. |
src/core/auxiliary_variables.jl |
Introduces pf_aux_var_types tuple traits defining the complete branch/bus-indexed PF aux-var universe. |
ext/PowerFlowsExt/pf_solve_and_aux.jl |
Switches the aux-var read-back guard from list-membership to the _pf_provides_aux_var trait. |
ext/PowerFlowsExt/pf_input_mapping.jl |
Defines _pf_provides_aux_var specializations and derives branch_aux_vars/bus_aux_vars from pf_aux_var_types via _provided_aux_vars. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Reverting to draft because this feels premature, given that
The issue started off over in PSI then got moved here to POM. Thus the weird situation with referring to functions that doesn't exist yet. Need to move over the rest of the power flow in the loop machinery, then this will become relevant. |
|
Performance Results
|
Closes #193.
Where the issue's symbols live
The issue names
_pf_provides_aux_var,_provides_control_aux_vars, andsrc/network_models/power_flow_evaluation.jl. None of those are in POM — they're in PowerSimulations.jl, which POM'sext/PowerFlowsExt/is a port of (fork baseline ≈ PSI #1503). PSImainhas exactly the two shapes the issue describes:POM has ported the list half only, and without PSI's named wrapper — it's inlined as
key_type in branch_aux_vars(pf_data) || key_type in bus_aux_vars(pf_data)at the call site. The control half is absent because none of its aux var types (PowerFlowTapRatio,PowerFlowSwitchedShuntSusceptance,PowerFlowFACTSReactivePower,PowerFlowHVDCAuxVariableType) exist in POM yet. That's the transformer/control work @jd-lara's comment on the issue refers to.So this PR fixes the half POM has, in the shape the issue asks for, and makes the other half a drop-in when it lands.
The change
One dispatch surface,
_pf_provides_aux_var(::Type{T}, pf_data)::Bool, with afalsefallback — so containers that provide nothing (PSSEExporter, and network models running no evaluator) keep their no-op behavior by inheriting it rather than by returning an empty list. Both the registration path and the read-back guard go through it. Noisa, noUnionenumerations.Two things fall out:
get_calculate_*) now read as the same trait — only the right-hand side is state rather than a literal.PowerFlowData{<:AbstractDCPowerFlow}method instead of three identical ones.branch_aux_vars/bus_aux_varssurvive as thin derivations, since registration genuinely needs a list of which aux vars to create. They filterPOM.pf_aux_var_types(C), a new tuple-valued trait in POM giving the universe of PF aux vars indexed by branch and by bus.How the control half drops in
When the control aux var types land, PSI's
_provides_control_aux_varsindirection disappears rather than being ported — each type gets a direct method on the one surface:with the
falsefallback covering every other container. That is "express both through one mechanism," and it's why this is the target shape for that port rather than a divergence from it.One concrete thing that port will need beyond the trait methods:
pf_aux_var_typesentries for the component types those aux vars are indexed by (switched shunts, FACTS devices, HVDC lines)._provided_aux_vars(pf_data, ::Type{C})is already parameterized onC, butadd_power_flow_data!only drives it forACBranchandACBustoday and will need matching component-tuple getters. The new exhaustiveness test (below) fails loudly the moment such a type is defined without apf_aux_var_typesentry, so it doubles as the checklist for that port.The code was written with compilation and type-stability in mind: e.g. tuples over lists,
mapover generators, named functions with type parameters over lambdas, etc. Claude verified that these things have the desired effects: calls resolve statically, types are concrete, etc.The tradeoff, and its guard
Dropping runtime reflection means a new
PowerFlowAuxVariableTypeleft out of the tuples would silently never register — the silent-skip pattern this repo dislikes. The reflection moved into CI instead: a new testset asserts the tuples are exactly exhaustive againstIS.get_all_concrete_subtypes, disjoint, and still tuples.Testing
calculate_loss_factorspath.test_power_flow_in_the_loop.jl: 17/17 testsets, 870 passing, 0 failures. (AC Power Flow in the loop for PhaseShiftingTransformerruns 0 tests — pre-existing, empty onmaintoo.)Test.detect_ambiguitiesclean; formatter run.Note on sequencing
@jd-lara's comment on the issue says to fix this once PF-in-the-loop is updated with @m-bossart's new transformer architecture. Given the above, that comment reads as being about the control half — which this PR deliberately doesn't port, and instead prepares a slot for. Happy to hold it for that work if you'd rather land them together.
🤖 Generated with Claude Code