Consolidate post-contingency balance expressions via target-index dispatch - #167
Conversation
…atch The system-wide, PTDF-nodal, and AreaBalance-area post-contingency balance/deployment expressions differed only in which extra index component sits between outage_id and t; the surrounding accumulation logic was duplicated three times per (healthy-deployment, outaged-generator) pair. Mirrors the _system_expression_type/ _balance_expression_targets pattern already used in add_to_expression.jl: _post_contingency_target_axes/_post_contingency_target_index dispatch on the expression type T (not on network_model's runtime type, since PostContingencyActivePowerBalance is built unconditionally under every network model) to resolve the axis/index, and one generic method per pair replaces the six network-model-specific copies. Also threads attribute_device_map through to the AreaBalance pre-contingency call site instead of recomputing it via a second PSY.get_component_supplemental_attribute_pairs call, which the unified signature now requires consistently across all three network models. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR refactors the G-1 security-constrained reserve model’s post-contingency balance/deployment expression construction by consolidating three previously near-duplicate implementations (system-wide balance, PTDF nodal deployment, AreaBalance area deployment) into shared generic methods, with the remaining differences handled via type-based target-index dispatch.
Changes:
- Added
_post_contingency_target_axesand_post_contingency_target_indexhelpers to resolve the “extra axis” (none/bus/area) based on expression typeT. - Collapsed multiple
add_to_expression!and_add_pre_contingency_*_terms_over_time!specializations into generic implementations parameterized byT. - Threaded
attribute_device_mapthrough the AreaBalance pre-contingency path to avoid redundant recomputation.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
acostarelli
left a comment
There was a problem hiding this comment.
This uses the splat operator a few times, is this a performance concern?
Secondarily, I would really like to cut back on the level of AI comments in this repository. The comments here feel very verbose and seem to assume the reader has the context of what the code was before the change.
Side-thought, my gut feeling is this particular idea for a refactor feels like something that could quickly be over-applied across POM. I would prefer something that feels more "sturdy" -- I'm not sure what I mean by that, but I think it has to do with the splatting.
| expression = lazy_container_addition!(container, T, | ||
| R, | ||
| string.(IS.get_uuid.(associated_outages)), | ||
| _post_contingency_target_axes(T, network_model, sys)..., |
There was a problem hiding this comment.
I forget the performance implications here, is using the spread operator bad?
There was a problem hiding this comment.
The refactor here is just to reduce code duplication: the types should be constant-propagated such that _post_contingency_target_axes and the ... are both resolved at compile time. I have not verified that is the case though
|
Performance Results
|
Agreed: AI loves to add superfluous comments about the evolution of the code ["changed from x to y because..."], when mostly care about the behavior.
Hmm. In the case of copperplate type models, we want to call lazy_container_addition!(container, T, R,
string.(IS.get_uuid.(associated_outages)),
time_steps;
meta = service_name,
)whereas in the other cases, we want to add another axis between the outages and the time steps (areas for area models, buses for PTDF model). I don't see how to make that happen without the Well, we could make |
|
Yeah that's fair. I think something is off-putting to me about this, but I think it's probably the container constructor that's bothering me. I would probably prefer the axes to be specified in one argument, not in varargs. |
|
I've diagnosed the CI failure: it's independent of this PR. So I'll merge this. |
Summary
Stacked on #166 (which is itself stacked on #148 /
ac/g1-port). Reviewing the outage/security-constrained code alongside the existing_system_expression_type/_balance_expression_targetsframework inadd_to_expression.jlsurfaced a natural consolidation: the system-wide, PTDF-nodal, and AreaBalance-area post-contingency balance/deployment expressions differed only in which extra index component sits betweenoutage_idandt— the surrounding accumulation logic (healthy-device reserve deployment / outaged-generator pre-contingency output) was duplicated three times._post_contingency_target_axes/_post_contingency_target_index, mirroring the existing_balance_expression_targetspattern: plain multiple dispatch (no closures/higher-order functions), returning aTupleof 0 or 1 extra index components.T, not onnetwork_model's runtime type —PostContingencyActivePowerBalanceis built unconditionally under every network model (the outer system-wide check), so it must always be axis-less regardless of whatnetwork_modelhappens to be at that call site. OnlyPostContingencyNodalActivePowerDeployment/PostContingencyAreaActivePowerDeploymentare network-model-specific, and their callers already guarantee the matching network model before invoking them. (Caught this the hard way — an earlier version dispatched onnetwork_modeldirectly and broke the PTDF path; fixed and re-verified.)add_to_expression!methods collapse into two generic ones + the two small dispatch helpers; the three_add_pre_contingency_*_terms_over_time!copies collapse into one.attribute_device_mapthrough instead of recomputing it via a redundantPSY.get_component_supplemental_attribute_pairscall.Deliberately did not extend this to
PostContingencyGenerationBalanceConstraint/PostContingencyCopperPlateBalanceConstraint— on inspection those aren't just an index-shape difference (the AreaBalance one sums in an extra baselineActivePowerBalanceexpression the system-wide one doesn't need), so forcing them into the same abstraction would conflate two different concerns for two call sites.Test plan
julia --project=test test/runtests.jl test_static_injection_security_constrained_models— 1219/1219 passTest.detect_ambiguities(PowerOperationsModels)— 0 ambiguities🤖 Generated with Claude Code