Consolidate add_to_expression! device-injection methods; fix AreaBalance bugs#135
Consolidate add_to_expression! device-injection methods; fix AreaBalance bugs#135luke-kiernan wants to merge 2 commits into
Conversation
…nce bugs Refactors the device-injection family of add_to_expression! methods to share a network-model-aware target resolver (_balance_expression_targets) plus IOM's add_device_terms_to_expression! driver, replacing ~17 hand-rolled device/time loops with thin delegations. Covers the variable->balance, parameter->balance (nodal/area/system), and compact-UC OnVariable families. Also ports PSI fix 22d2059c2 and related corrections POM missed by branching before it (see #134): - must-run compact-UC OnVariable called a non-existent 2-arg add_proportional_to_jump_expression! (MethodError); now add_constant_... . - OnVariable/ThermalGen + AreaBalance indexed an ACBus container AreaBalance never creates (KeyError); now PSY.Area. - FlowActivePowerToFromVariable/TwoTerminalHVDC + AreaBalance used an undefined network_reduction and unbound W (UndefVarError); now Area-indexed with HVDCTwoTerminalDispatch. - compact-UC OnVariable under AreaBalance/AreaPTDF now routes through the resolver, fixing a KeyError (AreaBalance) and a silent missing-area contribution (AreaPTDF). IOM is pinned to the lk/add-device-terms-driver branch until its PR merges into ac/canonical-key-component-type. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4a42a0d to
3af53a6
Compare
There was a problem hiding this comment.
Pull request overview
This PR refactors device-injection add_to_expression! methods by centralizing network-model-specific balance target resolution into _balance_expression_targets and delegating device×time looping to IOM’s add_device_terms_to_expression!, while also porting several AreaBalance/compact-UC bug fixes that previously caused runtime errors or mis-indexing.
Changes:
- Added
_balance_expression_targetsplus shared helper drivers (_add_variable_to_balance!,_add_ts_parameter_to_balance!,_add_compact_on_to_balance!, etc.) to reduce duplicated injection loops across network models. - Fixed multiple AreaBalance bugs (incorrect expression container indexing, undefined variables/types) and corrected compact-UC must-run handling.
- Temporarily repinned
InfrastructureOptimizationModelsto a driver branch to pick up the new IOM driver dependency.
Reviewed changes
Copilot reviewed 2 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/common_models/add_to_expression.jl |
Introduces shared balance-target resolver + helper adders; migrates many injection methods to the IOM driver; fixes AreaBalance/compact-UC/HVDC injection bugs. |
Project.toml |
Repins IOM source to lk/add-device-terms-driver and updates pinning notes (one TODO comment remains inconsistent). |
test/Project.toml |
Same IOM repin for the test environment, with corresponding pinning comments (one TODO comment remains inconsistent). |
Comments suppressed due to low confidence (1)
src/common_models/add_to_expression.jl:1102
- This
FlowActivePowerToFromVariable/TwoTerminalHVDC+AreaBalancePowerModelpath was previously failing (undefined vars / wrong indexing) and appears to be a bug-fix. It would be good to add a regression test that builds anAreaBalancePowerModelwith aTwoTerminalHVDCline and verifies the area balance receives the flow term, so this doesn’t silently regress again.
function add_to_expression!(
container::OptimizationContainer,
::Type{T},
::Type{U},
devices::IS.FlattenIteratorWrapper{V},
::DeviceModel{V, HVDCTwoTerminalDispatch},
network_model::NetworkModel{AreaBalancePowerModel},
) where {
T <: ActivePowerBalance,
U <: FlowActivePowerToFromVariable,
V <: PSY.TwoTerminalHVDC,
}
variable = get_variable(container, U, V)
expression = get_expression(container, T, PSY.Area)
time_steps = get_time_steps(container)
for d in devices
name = PSY.get_name(d)
area_name = PSY.get_name(PSY.get_area(PSY.get_arc(d).to))
for t in time_steps
add_proportional_to_jump_expression!(
expression[area_name, t],
variable[name, t],
get_variable_multiplier(U, V, HVDCTwoTerminalDispatch),
)
end
end
return
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ::DeviceModel{V, W}; | ||
| scale::Union{Nothing, Function} = nothing, | ||
| ) where {T <: ExpressionType, U <: VariableType, V <: PSY.StaticInjection, W} |
|
Performance Results
|
| devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}}, | ||
| network_model::NetworkModel, | ||
| ::DeviceModel{V, W}; | ||
| scale::Union{Nothing, Function} = nothing, |
There was a problem hiding this comment.
iirc positional args are better than keyword args for compilation purposes
| # - AreaBalancePowerModel -> area only | ||
| # - PTDFPowerModel/AreaPTDF... -> system/area + nodal bus | ||
| ################################################################################# | ||
|
|
There was a problem hiding this comment.
Hmm. Dramatically better, but still some redundacy here.
- First thing in return value in
_balance_expression_targetsis alwaysget_expression(container, T, [something]) - Final version of
_balance_expression_targetsis basically combination of nodal + area. Also it's the only spot where_system_expression_typeis called anywhere in this file. - All the
_add_foo_to_balance!functions only differ in what they pass as the 3rd argument:
add_device_terms_to_expression!(container,
d -> _balance_expression_targets(container, T, network_model, d),
function (d)
... # this differs
end,
devices,
)That being said, I'm not sure how feasible it is to captialize on this: we do want the get_{variable/multiplier/parameter} calls to happen once, not per-device.
- Make `scale` a positional arg bound to type parameter `S` instead of a
`Union{Nothing, Function}` keyword, so the concrete closure type is known
at compile time (no dynamic dispatch on `scale(d)`).
- Extract shared `add_device_terms_to_expression!` boilerplate into
`_add_balance_terms!`, leaving each `_add_*_to_balance!` to supply only its
per-device term closure (getter lookups still hoisted, once per call).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
In the initial PSI into IOM+POM split, the 90+ variations on
add_to_expression!was one thing where I told myself "I'll fix this properly later." This PR is to get the ball rolling on that fix: refactor so as to minimize code spaghetti and ensure good test coverage.Consolidates the device-injection family of
add_to_expression!methods onto a shared, network-model-aware target resolver (_balance_expression_targets) plus IOM's newadd_device_terms_to_expression!driver. ~17 hand-rolleddevice × timeloops collapse to thin delegations. Net −81 lines inadd_to_expression.jl.Depends on IOM PR Sienna-Platform/InfrastructureOptimizationModels.jl#112 (the driver), which targets IOM
main.What's consolidated
OnVariable: a single resolver-based_add_compact_on_to_balance!(must-run aware) replaces the nodal-only_add_to_expression!and both AreaBalance variants; CopperPlate/PTDF use a newscalekwarg on_add_variable_to_balance!for the per-devicep_min.PTDF parameter methods are intentionally left out (their
AbstractPTDFModel/security-constrained semantics differ from the variable family and need a separate decision).Bug fixes (port of PSI
22d2059c2, which POM missed by branching before it — see #134)OnVariablecalled a non-existent 2-argadd_proportional_to_jump_expression!→MethodError. Nowadd_constant_to_jump_expression!(On≡1 → P_min is a constant).OnVariable/ThermalGen+ AreaBalance indexed anACBuscontainer AreaBalance never creates →KeyError. NowPSY.Area.FlowActivePowerToFromVariable/TwoTerminalHVDC+ AreaBalance used an undefinednetwork_reductionand unboundW→UndefVarError. Now Area-indexed,HVDCTwoTerminalDispatch.OnVariableunder AreaBalance/AreaPTDF: routing through the resolver fixes aKeyError(AreaBalance) and a silent missing-area-contribution (AreaPTDF, where the generic method previously added to nodal only).Type stability / perf
The driver's inner loop is allocation-free even on heterogeneous target tuples (AreaPTDF mixes a
String-keyed area expression and anInt-keyed nodal expression) viaBase.tailrecursion. Cold build time unchanged.Tests
Constructor suites pass against IOM
main+ the driver branch (thermal/load/renewable/network/branch/HVDC; 5244/5244 on the thermal+network subset). The fixed paths above are currently unexercised by any test system — tracked in #134 for follow-up coverage.Follow-ups
lk/add-device-terms-driver(= IOMmain+ the driver) so it builds now. After IOM Cost expression refactor, parameter broadcasting, and PF headroom slack porting #112 merges intomain, flip the two[sources]pins here back torev = "main".add_to_expression!paths #134.🤖 Generated with Claude Code