Skip to content

[WIP] Port G-1 security-constrained reserves (PSI #1617)#148

Draft
acostarelli wants to merge 5 commits into
mainfrom
ac/g1-port
Draft

[WIP] Port G-1 security-constrained reserves (PSI #1617)#148
acostarelli wants to merge 5 commits into
mainfrom
ac/g1-port

Conversation

@acostarelli

Copy link
Copy Markdown
Member

Add the reserve/service-side security-constrained contingency layer:
post-contingency reserve deployment under generator (G-1) outages with
monitored-branch post-contingency flow constraints, across the CopperPlate,
AreaBalance, PTDF and AreaPTDF network models.

- New SecurityConstrained{Contingency,Ramp}Reserve formulations; contingency
  variable/expression/constraint types; post-contingency slack-cost constant;
  natural-unit conversions and exports.
- Service-side outage population in template validation
  (_build_service_model_outages! and helpers); admit PSY.AreaInterchange as a
  monitored component type.
- New services_models/static_injection_security_constrained_models.jl
  (sparse, monitored post-contingency containers) and its test file.

Two AreaBalance objective expected-values were re-recorded for psy6
(two_area_pjm_DA system-data drift; model structure matches PSI exactly per
moi_tests/constraint-key checks).

[sources] pins InfrastructureOptimizationModels to its matching ac/g1-port
branch (ServiceModel.outages + 1-D time-only store output methods).

Co-Authored-By: Claude Opus 4.8 (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 ports G-1 (security-constrained) reserve formulations into PowerOperationsModels, adding service-side post-contingency reserve-deployment modeling and an extensive regression test suite to match the behavior being ported from PowerSimulations (PSI #1617).

Changes:

  • Add security-constrained reserve formulations (contingency + ramp) with post-contingency deployment variables and sparse monitored-component flow constraints.
  • Add template validation to build per-service outage scoping (service_model.outages) based on outages attached to services.
  • Add comprehensive tests for multiple network models/formulations and adjust test dependencies to track required upstream IOM changes.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
test/test_static_injection_security_constrained_models.jl New regression suite covering SC reserve build/solve behavior and outage scoping.
test/Project.toml Pins test dependency InfrastructureOptimizationModels to ac/g1-port.
src/services_models/static_injection_security_constrained_models.jl New service-side SC reserve model implementation (deployment vars, post-contingency expressions/constraints).
src/services_models/reserves.jl Adds default requirement time-series mapping for SC reserve formulations.
src/PowerOperationsModels.jl Includes new service-model file and exports SC types/keys.
src/operation/template_validation.jl Adds _build_service_model_outages! and broadens monitored-type admission (incl. AreaInterchange).
src/core/variables.jl Adds contingency variable supertypes + SC reserve deployment variable types and unit-conversion flags.
src/core/formulations.jl Introduces SC reserve formulation types.
src/core/expressions.jl Adds post-contingency expression types and output/unit-conversion hooks.
src/core/definitions.jl Adds post-contingency slack penalty constant.
src/core/constraints.jl Adds post-contingency balance/limit constraint types used by SC reserves.
Project.toml Pins InfrastructureOptimizationModels to ac/g1-port.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +50 to +51
current_v =
filter(x -> x[col_name] == device_name, required_variables)[!, "value"]
Comment on lines +106 to +110
test_reserves_deployment(
outage_dict[outage_name][i],
reserve_dict[outage_name][i],
)
end
Comment on lines +80 to +85
# Fall back: in the new G-1 pattern, outages are attached to the outaged
# generator, not the reserve service. Resolve from system if empty.
if isempty(associated_outages)
all_outages = collect(PSY.get_supplemental_attributes(PSY.UnplannedOutage, sys))
associated_outages = all_outages
end
Comment thread src/core/formulations.jl Outdated
Comment on lines +339 to +343
Security-constrained contingency reserve formulation: requires a
`RequirementTimeSeriesParameter` and deploys reserves under each G-1 outage
attached to a contributing generator. Post-contingency branch-flow constraints
are added only for the monitored components listed on each outage's
`monitored_components`.
Comment on lines +1378 to +1380
has_requirement_ts =
haskey(get_time_series_names(model), RequirementTimeSeriesParameter) &&
length(PSY.get_time_series_keys(service)) > 0
Comment on lines +1531 to +1535
has_requirement_ts =
has_requirement_ts_default || (
haskey(get_time_series_names(model), RequirementTimeSeriesParameter) &&
length(PSY.get_time_series_keys(service)) > 0
)
Comment on lines +1629 to +1633
has_requirement_ts =
has_requirement_ts_default || (
haskey(get_time_series_names(model), RequirementTimeSeriesParameter) &&
length(PSY.get_time_series_keys(service)) > 0
)
Comment on lines +1715 to +1719
has_requirement_ts =
has_requirement_ts_default || (
haskey(get_time_series_names(model), RequirementTimeSeriesParameter) &&
length(PSY.get_time_series_keys(service)) > 0
)
@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown

Performance Results

Version Precompile Time
Main 2.970724753
This Branch 2.893881756
Version Build Time
Main-Build Time Precompile 86.422143273
Main-Build Time Postcompile 1.284536202
This Branch-Build Time Precompile 83.892469971
This Branch-Build Time Postcompile 1.248909553
Version Solve Time
Main-Solve Time Precompile 3255.017961501
Main-Solve Time Postcompile 3218.755276428
This Branch-Solve Time Precompile 2047.640857522
This Branch-Solve Time Postcompile 2009.279259954

@acostarelli acostarelli left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Review 1

Comment thread src/core/expressions.jl
struct PTDFBranchFlow <: ExpressionType end
abstract type PostContingencySystemBalanceExpressions <: SystemBalanceExpressions end
struct PostContingencyActivePowerBalance <: PostContingencySystemBalanceExpressions end
struct PostContingencyAreaInterchangeFlow <: PostContingencyExpressions end

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think PostContingencyExpressions is an IOM type. Maybe it should move, along with the other contingency types it has.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Leave this for later.

@@ -0,0 +1,2592 @@
function get_outage_total_power_by_step_dict(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Double-check that all test cases were ported as 1:1 as possible from the original.

::Set{<:DataType},
network_model::NetworkModel{<:AreaBalancePowerModel},
) where {SR <: PSY.AbstractReserve}
_construct_service_model_areabalance!(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

For these construct_service! methods, we have these _construct_service_model_* helpers when we could instead dispatch on the network model.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It also seems like those helpers have a lot of similarities.

service,
service_name,
slack_resolved,
F(),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We should be passing by type not instance.

contents[(outage_id, name, t)] = zero(JuMP.AffExpr)
end
end
expression_container = SparseAxisArray(contents)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'm seeing this a lot in this PR, creating a SparseAxisArray directly and then the keys manually. Check the IOM API, we might be able to do this cleaner than we were doing in PSI. If not, we should probably update the IOM API.

jump_model = get_jump_model(container)

use_slacks = get_use_slacks(service_model)
slack_ub = if use_slacks

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I've seen this use_slacks routine at least in one other place.

#! format: on

# Add `multiplier * var` to a JuMP scalar in place (post-contingency builders).
function _add_to_jump_expression!(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This already exists in jump_utils.jl

device_name = PSY.get_name(device)
current_v =
filter(x -> x[col_name] == device_name, required_variables)[!, "value"]
if i == 1

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Surely there's a more Julian way to do this sum.

Bugs:
- has_requirement_ts now checks for the specific requirement time series via
  PSY.has_time_series(service, ts_type, "requirement") instead of testing only
  that some time series exists on the service.
- Test helper get_reserve_total_power_by_step_dict now scopes the deployment
  filter by outage id, so a device responding to multiple outages no longer
  mixes deployments across them.
- compare_outage_power_and_deployed_reserves now forwards its tolerance kwarg
  to test_reserves_deployment.

Docs/comments:
- Reworded the SecurityConstrainedContingencyReserve docstring (outages scoped
  to the service with system fallback; requirement time series optional) and
  the misleading outage-attachment comment in the test.

Cleanups:
- Removed the redundant _add_to_jump_expression! wrapper (inline
  JuMP.add_to_expression!), pass the formulation by type to the slack builder,
  build the irregular sparse containers via the IOM sparse_container_spec
  explicit-keys overload, and use mapreduce for the test accumulators.

Refactors:
- Collapsed the 6 construct_service! methods + 3 near-duplicate helpers into one
  generic construct_service! dispatching the network model, a shared
  _construct_service_model_sc! core, and three thin _add_post_contingency_network_terms!
  methods; the boolean flags became a requires_requirement_ts trait.
- Extracted the duplicated use_slacks logic into _make_post_contingency_slacks
  and _add_post_contingency_flow_rate_constraint!.

Depends on the matching IOM sparse_container_spec overload (ac/g1-port).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
::Dict{Symbol, DeviceModel},
::Set{<:DataType},
network_model::NetworkModel{
<:Union{PM.AbstractDCPModel, CopperPlatePowerModel, AreaBalancePowerModel},

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Do we have to rely on PowerModels here? We are trying to phase out this dependency, but maybe we still need it right now.

PostContingencyAreaActivePowerDeployment, ActivePowerBalance,
service, model, network_model,
)
add_post_contingency_flow_expressions!(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Reminder for myself: why do we have multiple dispatches, one that takes sys and one that doesn't? If we passed sys anyway could we combine with the PTDF one?

# Shared ModelConstructStage skeleton for every supported network model. The
# network-specific post-contingency deployment/flow terms are dispatched on the
# network model through `_add_post_contingency_network_terms!`.
function _construct_service_model_sc!(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This does not need to be a separate function...

contributing_devices, service, model, network_model,
)
attribute_device_map = PSY.get_component_supplemental_attribute_pairs(
PSY.Generator, PSY.UnplannedOutage, sys,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

What is the difference between UnplannedOutages and regular Outages? Should this dispatch on an abstract type?


# Shared ArgumentConstructStage helper used by both formulations: builds
# pre-contingency reserve variable + post-contingency deployment variable.
function _construct_service_arguments_sc!(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This also doesn't need to be a separate function.

name = PSY.get_name(device)
area_name = PSY.get_name(PSY.get_area(PSY.get_bus(device)))
for t in time_steps
JuMP.add_to_expression!(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We should use the jump_utils.jl where possible.

PSY.get_associated_components(sys, outage; component_type = PSY.Generator)
outage_id = string(IS.get_uuid(outage))
for device in contributing_devices
mult = device in associated_devices ? 0.0 : mult_default

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think if we're going to be multiplying be 0, we shouldn't add to the expression at all.

Comment thread src/services_models/static_injection_security_constrained_models.jl
Comment thread src/operation/template_validation.jl Outdated
]
end

# Whether SC service model `m` should skip `outage`. Dispatched on the outage

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Unnecessary comment

Comment thread src/services_models/reserves.jl

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

Copilot reviewed 11 out of 12 changed files in this pull request and generated 2 comments.

Comment on lines +800 to +815
# Cache PTDF columns per (monitored_type, arc) — multiple outages may
# monitor the same arc.
pre_flow_cache = Dict{DataType, Any}()
for (uuid, entries) in resolved
outage_id = string(uuid)
# Positional slice over the bus/time axes; matches `ptdf_col`'s
# positional indexing and avoids keyed lookup mismatches when the
# nodal expression's bus axis is a subset of the PTDF column space
# (e.g. AreaPTDFPowerModel).
post_cont_expr = nodal_deployment[outage_id, :, :].data
for (entry_type, name, arc, _) in entries
pre_flow = get!(pre_flow_cache, entry_type) do
get_expression(container, PTDFBranchFlow, entry_type)
end
ptdf_col = ptdf[arc, :]
for t in time_steps
Comment thread src/core/formulations.jl Outdated
Comment on lines +339 to +342
Security-constrained contingency reserve formulation: deploys reserves under
each G-1 outage scoped to the reserve service (the outages attached to the
service, falling back to the system's outages when none are attached). A
`RequirementTimeSeriesParameter` is optional — when present the requirement /
Anthony Costarelli and others added 3 commits June 28, 2026 15:35
Port unported upstream PSI (sm/g-1_monitored_c) changes:
- Broaden UnplannedOutage -> abstract PSY.Outage at the post-contingency
  balance/deployment dispatch and supplemental-attribute lookups so planned
  outages flow through when opted in (PSI b74bd31/9b819fe/2577880/5d5b3c6).
- Correct SecurityConstrainedContingencyReserve docstring: outages scoped to
  the PSY.Service, requirement time series optional (PSI ced97500).

Review fixes:
- Skip zero-coefficient terms for outaged generators instead of adding them.
- Cache PTDF columns by arc to avoid repeated KLU solves per outage/time.
- add_variables! dispatches on ::Type{F} instead of a formulation instance.
- Inline the two single-call construct_service! helpers.
- Narrow the SC DCP path to AbstractPTDFModel, dropping PowerModels coupling.
- Trim/remove unnecessary comments; document the requirement time-series name.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the hand-built SparseAxisArray + manual ExpressionKey/ConstraintKey/
VariableKey + _assign_container! boilerplate in both security-constrained
models with add_expression_container! (seed-by-keys) and the new
add_variable_container!/add_constraints_container! pre-built overloads, per
IOM's "never create container keys directly" rule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Post-contingency expression, constraint, and slack containers are now
created and prefilled via IOM's sparse_keys kwarg instead of building a
SparseAxisArray in POM and handing it across. The container's keys are the
resolved (outage, name, t) tuples — sparse storage, no cartesian holes.

Branch slack containers are created only when use_slacks (dropping the
conditional post-loop registration). Removed the stale docstring claiming
the expression pre-fill is needed for the parallel PTDF build: those tasks
return results and the writes into the container happen serially on the
main thread.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment on lines +431 to +433
container, PostContingencyFlowActivePowerSlackUpperBound, V;
sparse_keys = index_keys,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
container, PostContingencyFlowActivePowerSlackUpperBound, V;
sparse_keys = index_keys,
)
container, PostContingencyFlowActivePowerSlackUpperBound, V;
sparse_keys = index_keys,
)

Comment on lines +440 to +442
container, PostContingencyFlowActivePowerSlackLowerBound, V;
sparse_keys = index_keys,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[JuliaFormatter] reported by reviewdog 🐶

Suggested change
container, PostContingencyFlowActivePowerSlackLowerBound, V;
sparse_keys = index_keys,
)
container, PostContingencyFlowActivePowerSlackLowerBound, V;
sparse_keys = index_keys,
)

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

Copilot reviewed 12 out of 13 changed files in this pull request and generated 2 comments.

Comment thread src/core/expressions.jl
Comment on lines +11 to +13
struct PostContingencyAreaInterchangeFlow <: PostContingencyExpressions end
struct PostContingencyNodalActivePowerDeployment <: PostContingencyExpressions end
struct PostContingencyAreaActivePowerDeployment <: PostContingencyExpressions end
Comment thread test/Project.toml

[sources]
InfrastructureOptimizationModels = {rev = "main", url = "https://github.com/Sienna-Platform/InfrastructureOptimizationModels.jl"}
InfrastructureOptimizationModels = {rev = "ac/g1-port", url = "https://github.com/Sienna-Platform/InfrastructureOptimizationModels.jl"}
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.

3 participants