Skip to content

Unify the trait-vs-list pattern in power flow aux var provisioning - #252

Draft
luke-kiernan wants to merge 2 commits into
mainfrom
lk/unify-pf-aux-var-trait
Draft

Unify the trait-vs-list pattern in power flow aux var provisioning#252
luke-kiernan wants to merge 2 commits into
mainfrom
lk/unify-pf-aux-var-trait

Conversation

@luke-kiernan

@luke-kiernan luke-kiernan commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Closes #193.

Where the issue's symbols live

The issue names _pf_provides_aux_var, _provides_control_aux_vars, and src/network_models/power_flow_evaluation.jl. None of those are in POM — they're in PowerSimulations.jl, which POM's ext/PowerFlowsExt/ is a port of (fork baseline ≈ PSI #1503). PSI main has exactly the two shapes the issue describes:

_pf_provides_aux_var(::Type{T}, pf_data) where {T <: PowerFlowAuxVariableType} =
    T in branch_aux_vars(pf_data) || T in bus_aux_vars(pf_data)          # list half
_pf_provides_aux_var(::Type{T}, pf_data) where {T <: PowerFlowHVDCAuxVariableType} =
    _provides_control_aux_vars(pf_data)                                  # trait half
_pf_provides_aux_var(::Type{PowerFlowTapRatio}, pf_data) = _provides_control_aux_vars(pf_data)
...
_provides_control_aux_vars(::PFS.PowerFlowContainer) = false
_provides_control_aux_vars(::PFS.ACPowerFlowData) = true

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 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. Both the registration path and the read-back guard go through it. No isa, no Union enumerations.

Two things fall out:

  • The runtime-flag cases (loss / voltage-stability factors, gated on get_calculate_*) now read as the same trait — only the right-hand side is state rather than a literal.
  • 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, 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 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_vars indirection disappears rather than being ported — each type gets a direct method on the one surface:

_pf_provides_aux_var(::Type{POM.PowerFlowTapRatio}, ::PFS.ACPowerFlowData) = true

with the false fallback 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_types entries 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 on C, but add_power_flow_data! only drives it for ACBranch and ACBus today and will need matching component-tuple getters. The new exhaustiveness test (below) fails loudly the moment such a type is defined without a pf_aux_var_types entry, 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, map over 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 PowerFlowAuxVariableType left 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 against IS.get_all_concrete_subtypes, disjoint, and still tuples.

Testing

  • Verified list-for-list equivalence with the previous hardcoded lists for all five container types plus the calculate_loss_factors path.
  • test_power_flow_in_the_loop.jl: 17/17 testsets, 870 passing, 0 failures. (AC Power Flow in the loop for PhaseShiftingTransformer runs 0 tests — pre-existing, empty on main too.)
  • Aqua 4/4; Test.detect_ambiguities clean; formatter run.
  • Targeted runs only, per the known lingering failures elsewhere in the suite — those are untouched and unassessed.

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

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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 all PowerFlowAuxVariableTypes 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_var specializations per power-flow container type (including runtime-flag gating for loss/stability factors).
  • Added a CI-guarding test asserting pf_aux_var_types exhaustively enumerates all concrete PowerFlowAuxVariableTypes 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.

@luke-kiernan
luke-kiernan marked this pull request as draft August 24, 2026 19:11
@luke-kiernan

Copy link
Copy Markdown
Collaborator Author

Reverting to draft because this feels premature, given that

_pf_provides_aux_var and _provides_control_aux_vars, the symbols the issue names, don't exist in the repo or its history

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.

@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown

Performance Results

Version Precompile Time
Main 6.110762615
This Branch 5.979312488
Version Build Time
Main-Build Time Precompile 106.621629672
Main-Build Time Postcompile 5.167665474
This Branch-Build Time Precompile 99.551073136
This Branch-Build Time Postcompile 5.198527496
Version Solve Time
Main-Solve Time Precompile 252.32902101
Main-Solve Time Postcompile 220.135390236
This Branch-Solve Time Precompile 140.917382468
This Branch-Solve Time Postcompile 90.242150268

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.

Unify the trait-vs-list pattern in _pf_provides_aux_var

4 participants