Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
b394fe6
Add outages field to ServiceModel for G-1 security-constrained reserves
SebastianManriqueM May 25, 2026
40f818a
Add SecurityConstrainedContingencyReserve and SecurityConstrainedRamp…
SebastianManriqueM May 25, 2026
8c8edc0
Add _build_service_model_outages! validator for SC reserve service mo…
SebastianManriqueM May 25, 2026
9abd829
Port static_injection_security_constrained_models.jl with sparse moni…
SebastianManriqueM May 25, 2026
712939c
Add test_static_injection_security_constrained_models.jl
SebastianManriqueM May 25, 2026
1ecf21c
SC reserves: service-level outage threading, dual supplemental attach…
SebastianManriqueM May 25, 2026
b86ab36
Apply formatter to SC model src and test files
SebastianManriqueM May 25, 2026
d3ea127
formatter
SebastianManriqueM May 26, 2026
533e29a
Add PostContingencyAreaInterchangeFlow expression type
SebastianManriqueM May 26, 2026
c0b3221
Admit AreaInterchange as a monitored component type
SebastianManriqueM May 26, 2026
fdf94b9
Add PostContingencyFlowRateConstraint coverage for AreaInterchange un…
SebastianManriqueM May 26, 2026
2c85ba7
Cover AreaInterchange monitoring in AreaBalance security-constrained …
SebastianManriqueM May 26, 2026
1e3f588
Add PTDF/AreaPTDF G-n testsets with monitored line subset
SebastianManriqueM May 26, 2026
6a60562
Scope SC reserve outage auto-discovery per ServiceModel
SebastianManriqueM May 26, 2026
08a1459
Fix docstring
SebastianManriqueM May 27, 2026
9e63e0e
Widen _service_outages return eltype to PSY.Outage
SebastianManriqueM May 27, 2026
96e57d2
fix: skipping post contingency constraints when outaged gen is not in…
SebastianManriqueM Jun 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/PowerSimulations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export RangeReserve
export RampReserve
export StepwiseCostReserve
export NonSpinningReserve
export SecurityConstrainedContingencyReserve
export SecurityConstrainedRampReserve
export PIDSmoothACE
export GroupReserve
export ConstantMaxInterfaceFlow
Expand Down Expand Up @@ -261,6 +263,8 @@ export RateofChangeConstraintSlackUp
export RateofChangeConstraintSlackDown
export PostContingencyActivePowerChangeVariable
export PostContingencyActivePowerReserveDeploymentVariable
export PostContingencyFlowActivePowerSlackUpperBound
export PostContingencyFlowActivePowerSlackLowerBound
Comment on lines +266 to +267
export DCVoltage
export DCLineCurrent
export ConverterPowerDirection
Expand Down Expand Up @@ -348,6 +352,8 @@ export PostContingencyActivePowerVariableLimitsConstraint
export PostContingencyActivePowerReserveDeploymentVariableLimitsConstraint
export PostContingencyGenerationBalanceConstraint
export PostContingencyRampConstraint
export PostContingencyActivePowerGenerationLimitsConstraint
export PostContingencyCopperPlateBalanceConstraint
export ImportExportBudgetConstraint
export PiecewiseLinearBlockIncrementalOfferConstraint
export PiecewiseLinearBlockDecrementalOfferConstraint
Expand Down Expand Up @@ -416,8 +422,11 @@ export FuelConsumptionExpression
export ActivePowerRangeExpressionLB
export ActivePowerRangeExpressionUB
export PostContingencyBranchFlow
export PostContingencyAreaInterchangeFlow
export PostContingencyActivePowerGeneration
export PostContingencyActivePowerBalance
export PostContingencyNodalActivePowerDeployment
export PostContingencyAreaActivePowerDeployment
export NetActivePower
export DCCurrentBalance

Expand Down Expand Up @@ -692,6 +701,7 @@ include("devices_models/devices/reactivepower_device.jl")

# Services Models
include("services_models/reserves.jl")
include("services_models/static_injection_security_constrained_models.jl")
include("services_models/reserve_group.jl")
include("services_models/transmission_interface.jl")
include("services_models/service_slacks.jl")
Expand Down
28 changes: 28 additions & 0 deletions src/core/constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,34 @@ each monitored arc m, the post-contingency flow constraint is:
"""
struct PostContingencyFlowRateConstraint <: PostContingencyConstraintType end

"""
Constraint on the post-contingency active power generation expression
(`PostContingencyActivePowerGeneration`) of each contributing generator
under each outage. Outaged generators are pinned to zero; all other
generators are bounded by their `active_power_limits`.

```math
P^{\\text{min}}_g \\le p_{g,t} + \\Delta rsv_{c,g,t} \\le P^{\\text{max}}_g,
\\quad \\forall c \\in \\mathcal{C},\\ \\forall g \\notin \\mathcal{G}_c,\\ \\forall t
```
"""
struct PostContingencyActivePowerGenerationLimitsConstraint <:
PostContingencyConstraintType end

"""
Constraint that closes the per-area post-contingency power balance for the
`AreaBalancePowerModel` network representation, summing the
`PostContingencyAreaActivePowerDeployment` with the pre-contingency
`ActivePowerBalance` expression for each area.

```math
\\sum_{g \\in \\mathcal{A}}(\\Delta rsv_{c,g,t} + p_{g,t}\\mathbb{1}_{g \\in \\mathcal{G}_c}) +
\\text{Bal}^{\\text{pre}}_{a,t} = 0,\\quad \\forall a \\in \\mathcal{A},\\ \\forall c,\\ \\forall t
```
"""
struct PostContingencyCopperPlateBalanceConstraint <:
PostContingencyConstraintType end

"""
Struct to create the constraint for branch flow rate limits from the 'from' bus to the 'to' bus.
For more information check [Branch Formulations](@ref PowerSystems.Branch-Formulations).
Expand Down
1 change: 1 addition & 0 deletions src/core/definitions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const UNSET_INI_TIME = Dates.DateTime(0)
const ABSOLUTE_TOLERANCE = 1.0e-3
const BALANCE_SLACK_COST = 1e6
const CONSTRAINT_VIOLATION_SLACK_COST = 2e5
const POST_CONTINGENCY_CONSTRAINT_VIOLATION_SLACK_COST = 1e5
const SERVICES_SLACK_COST = 1e5
const COST_EPSILON = 1e-3
const PTDF_ZERO_TOL = 1e-9
Expand Down
4 changes: 4 additions & 0 deletions src/core/expressions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ struct ComponentReserveDownBalanceExpression <: ExpressionType end
struct InterfaceTotalFlow <: ExpressionType end
struct PTDFBranchFlow <: ExpressionType end
struct PostContingencyBranchFlow <: PostContingencyExpressions end
struct PostContingencyAreaInterchangeFlow <: PostContingencyExpressions end
struct PostContingencyActivePowerGeneration <: PostContingencyExpressions end
struct PostContingencyNodalActivePowerDeployment <: PostContingencyExpressions end
struct PostContingencyAreaActivePowerDeployment <: PostContingencyExpressions end
struct NetActivePower <: ExpressionType end
struct RealizedShiftedLoad <: ExpressionType end
"""
Expand All @@ -47,10 +49,12 @@ should_write_resulting_value(::Type{DCCurrentBalance}) = true
should_write_resulting_value(::Type{PTDFBranchFlow}) = true
should_write_resulting_value(::Type{RealizedShiftedLoad}) = true
should_write_resulting_value(::Type{PostContingencyBranchFlow}) = true
should_write_resulting_value(::Type{PostContingencyAreaInterchangeFlow}) = true
#should_write_resulting_value(::Type{PostContingencyActivePowerGeneration}) = true

convert_result_to_natural_units(::Type{InterfaceTotalFlow}) = true
convert_result_to_natural_units(::Type{PostContingencyBranchFlow}) = true
convert_result_to_natural_units(::Type{PostContingencyAreaInterchangeFlow}) = true
convert_result_to_natural_units(::Type{PostContingencyActivePowerGeneration}) = true
convert_result_to_natural_units(::Type{PTDFBranchFlow}) = true
convert_result_to_natural_units(::Type{RealizedShiftedLoad}) = true
23 changes: 23 additions & 0 deletions src/core/formulations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,29 @@ abstract type AbstractReservesFormulation <: AbstractServiceFormulation end

abstract type AbstractSecurityConstrainedReservesFormulation <: AbstractReservesFormulation end

"""
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 +291 to +295

See also `SecurityConstrainedRampReserve`.
"""
struct SecurityConstrainedContingencyReserve <:
AbstractSecurityConstrainedReservesFormulation end

"""
Security-constrained ramp reserve formulation: like `RampReserve` for the
pre-contingency requirement/ramp/participation constraints, plus the same
G-1 post-contingency deployment + monitored-branch flow constraints as
`SecurityConstrainedContingencyReserve`.

See also `SecurityConstrainedContingencyReserve`.
"""
struct SecurityConstrainedRampReserve <:
AbstractSecurityConstrainedReservesFormulation end

abstract type AbstractAGCFormulation <: AbstractServiceFormulation end

struct PIDSmoothACE <: AbstractAGCFormulation end
Expand Down
10 changes: 10 additions & 0 deletions src/core/service_model.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ model at simulation time
- `feedforward::Array{<:AbstractAffectFeedforward}` : use to pass parameters between models
- `use_service_name::Bool` : use the name as the name for the service

For security-constrained reserve formulations, the set of contingencies a
service responds to is determined entirely by attaching outage supplemental
attributes to the `PSY.Service` instance via
`add_supplemental_attribute!(sys, service, outage)`. Template validation
then mirrors those attachments into `service_model.outages`. There is no
constructor-level outage allow-list.
Comment on lines +30 to +34

# Example

reserves = ServiceModel(PSY.VariableReserve{PSY.ReserveUp}, RangeReserve)
Expand All @@ -39,6 +46,7 @@ mutable struct ServiceModel{D <: PSY.Service, B <: AbstractServiceFormulation}
attributes::Dict{String, Any}
contributing_devices_map::Dict{Type{<:PSY.Component}, Vector{<:PSY.Component}}
subsystem::Union{Nothing, String}
outages::Dict{Base.UUID, Dict{DataType, Set{String}}}
function ServiceModel(
::Type{D},
::Type{B},
Expand Down Expand Up @@ -66,6 +74,7 @@ mutable struct ServiceModel{D <: PSY.Service, B <: AbstractServiceFormulation}
attributes_for_model,
contributing_devices_map,
nothing,
Dict{Base.UUID, Dict{DataType, Set{String}}}(),
)
end
end
Expand All @@ -89,6 +98,7 @@ get_contributing_devices_map(m::ServiceModel, key) =
get_contributing_devices(m::ServiceModel) =
[z for x in values(m.contributing_devices_map) for z in x]
get_subsystem(m::ServiceModel) = m.subsystem
get_outages(m::ServiceModel) = m.outages

set_subsystem!(m::ServiceModel, id::String) = m.subsystem = id

Expand Down
43 changes: 25 additions & 18 deletions src/core/variables.jl
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,31 @@ Docs abbreviation: ``\\Delta rsv_{r,g,c}``
struct PostContingencyActivePowerReserveDeploymentVariable <:
AbstractContingencyVariableType end

"""
Abstract supertype for non-negative slack variables that absorb infeasibility
in post-contingency branch-flow inequalities of the security-constrained
reserve formulations.
"""
abstract type AbstractContingencySlackVariableType <: VariableType end

"""
Struct to dispatch the creation of the non-negative slack variable applied to
the post-contingency upper branch-flow inequality.

Docs abbreviation: ``s^{f,\\text{ub}}_{c,\\ell,t}``
"""
struct PostContingencyFlowActivePowerSlackUpperBound <:
AbstractContingencySlackVariableType end

"""
Struct to dispatch the creation of the non-negative slack variable applied to
the post-contingency lower branch-flow inequality.

Docs abbreviation: ``s^{f,\\text{lb}}_{c,\\ell,t}``
"""
struct PostContingencyFlowActivePowerSlackLowerBound <:
AbstractContingencySlackVariableType end

"""
Struct to dispatch the creation of Active Power Variables above minimum power for Thermal Compact formulations

Expand Down Expand Up @@ -371,24 +396,6 @@ Docs abbreviation: ``f^\\text{sl,lo}``
"""
struct FlowActivePowerSlackLowerBound <: AbstractACActivePowerFlow end

"""
Struct to dispatch the creation of post-contingency active power flow upper bound
slack variables. Relaxes the post-contingency emergency-rating upper limit of a
monitored branch under a specific outage. Indexed per `(outage_id, name, t)`.

Docs abbreviation: ``f^\\text{sl,up}_{c}``
"""
struct PostContingencyFlowActivePowerSlackUpperBound <: VariableType end

"""
Struct to dispatch the creation of post-contingency active power flow lower bound
slack variables. Relaxes the post-contingency emergency-rating lower limit of a
monitored branch under a specific outage. Indexed per `(outage_id, name, t)`.

Docs abbreviation: ``f^\\text{sl,lo}_{c}``
"""
struct PostContingencyFlowActivePowerSlackLowerBound <: VariableType end

"""
Struct to dispatch the creation of Phase Shifters Variables

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,11 @@ function _resolve_monitored_arcs(
for (uuid, per_type) in get_outages(device_model)
kept = Tuple{DataType, String, Tuple{Int, Int}, String}[]
for (T, names) in per_type
# Per-type entries may include component types absent from the
# network-reduction's branch-arc maps (e.g. `PSY.AreaInterchange`
# under AreaBalance service-side outages). Skip those here so
# device-side AC SC builders only see branch-arc types.
haskey(name_to_arc_maps, T) || continue
name_to_arc = name_to_arc_maps[T]
component_to_reduction =
get(component_to_reduction_maps, T, Dict{String, String}())
Expand Down
126 changes: 123 additions & 3 deletions src/operation/template_validation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ function validate_template_impl!(model::OperationModel)
_check_security_constrained_network!(model.template.branches, network_model)
validate_network_model(network_model, unmodeled_branch_types, model_has_branch_filters)
_build_device_model_outages!(template, system)
_build_service_model_outages!(template, system)
return
end

Expand Down Expand Up @@ -382,9 +383,12 @@ function _monitored_components_by_modeled_type(
# Post-contingency flow limits only make sense on branch arcs: the
# post-contingency builder resolves every `per_type` key through
# `name_to_arc_maps`, which is keyed by ACTransmission branch types
# only. A monitored component that is either not modeled or not a
# branch type would `KeyError` there, so route it to the skip path.
if comp_type <: PSY.ACTransmission && comp_type in modeled_types
# only. `PSY.AreaInterchange` is admitted separately so the
# AreaBalance service-side builder can pick it up. A monitored
# component that is neither would `KeyError` there, so route it to
# the skip path.
admissible = (comp_type <: PSY.ACTransmission || comp_type <: PSY.AreaInterchange)
if admissible && comp_type in modeled_types
push!(get!(per_type, comp_type, Set{String}()), PSY.get_name(component))
else
push!(uncovered, comp_type)
Expand Down Expand Up @@ -475,3 +479,119 @@ function _warn_unmatched_user_outages(
end
return
end

"""
Populate `service_model.outages` for every security-constrained (SC) reserve
`ServiceModel` in the template, in a single pass over each SC service model.

The user opts a service into responding to a given contingency by attaching
the outage supplemental attribute to the `PSY.Service` instance directly
(`add_supplemental_attribute!(sys, service, outage)`). That attachment is
the sole selection mechanism: a service claims exactly the outages attached
to it, regardless of whether the outaged component is among the service's
Comment on lines +487 to +491
contributing devices.

`PlannedOutage`s attached to the service are still gated by the
`"include_planned_outages"` attribute on the SC `ServiceModel` (default
`false`). `UnplannedOutage`s and other `Outage` subtypes are always claimed.

A warning is emitted when an outage is attached to a `PSY.Service` whose
`(component_type, name)` does not correspond to an SC `ServiceModel` in the
template — that outage will not produce any post-contingency reserve
constraints.
"""
function _build_service_model_outages!(
template::ProblemTemplate,
sys::PSY.System,
)
sc_service_models = _sc_reserve_service_models(template)
template_sc_service_keys = Set{Tuple{DataType, String}}(
(get_component_type(m), get_service_name(m)) for m in sc_service_models
)
# Run the orphan-attachment check unconditionally so users still get
# feedback when they attached outages to a service whose SC `ServiceModel`
# was never registered (e.g. they forgot the `set_service_model!` call, or
# registered the service with a non-SC formulation).
_warn_outages_attached_to_unmodeled_services(sys, template_sc_service_keys)

isempty(sc_service_models) && return

modeled_types = Set{DataType}(get_component_types(template))
uncovered_types = Dict{DataType, Set{Base.UUID}}()

for m in sc_service_models
empty!(m.outages)
D = get_component_type(m)
service_name = get_service_name(m)
service = PSY.get_component(D, sys, service_name)
if service === nothing
@warn "ServiceModel{$D, $(get_formulation(m))} (service_name=\
$(service_name)) is in the template but no matching \
service exists in the system; it will not contribute any \
post-contingency constraints." _group =
LOG_GROUP_MODELS_VALIDATION
continue
end

for outage in PSY.get_supplemental_attributes(PSY.Outage, service)
outage_uuid = IS.get_uuid(outage)
if isempty(PSY.get_monitored_components(outage))
Comment on lines +522 to +538
@warn "Outage $(outage_uuid) ($(typeof(outage))) attached to \
service $(service_name) has empty \
monitored_components; no post-contingency variables \
or constraints will be created for this outage." _group =
LOG_GROUP_MODELS_VALIDATION
continue
end
_service_skips_outage(outage, m) && continue

per_type, uncovered = _monitored_components_by_modeled_type(
outage, outage_uuid, sys, modeled_types,
)
for comp_type in uncovered
push!(get!(uncovered_types, comp_type, Set{Base.UUID}()), outage_uuid)
end
isempty(per_type) && continue
m.outages[outage_uuid] = per_type
end
end

_warn_uncovered_monitored_types(uncovered_types)
return
end

function _sc_reserve_service_models(template::ProblemTemplate)
return ServiceModel[
m for m in values(get_service_models(template)) if
get_formulation(m) <: AbstractSecurityConstrainedReservesFormulation
]
end

# Whether SC service model `m` should skip `outage`. Dispatched on the outage
# subtype to keep `PlannedOutage`/`UnplannedOutage` branching out of the hot
# path. Planned outages are skipped unless the SC service model opts in via
# the `"include_planned_outages"` attribute.
_service_skips_outage(::PSY.Outage, ::ServiceModel) = false
_service_skips_outage(::PSY.PlannedOutage, m::ServiceModel) =
get_attribute(m, "include_planned_outages") !== true

function _warn_outages_attached_to_unmodeled_services(
sys::PSY.System,
template_sc_service_keys::Set{Tuple{DataType, String}},
)
for service in PSY.get_components(PSY.Service, sys)
attached_outages = PSY.get_supplemental_attributes(PSY.Outage, service)
isempty(attached_outages) && continue
key = (typeof(service), PSY.get_name(service))
key in template_sc_service_keys && continue
for outage in attached_outages
@warn "Outage $(IS.get_uuid(outage)) is attached to service \
$(PSY.get_name(service)) ($(typeof(service))) but the \
template does not include a security-constrained \
ServiceModel for it; the outage will not contribute any \
post-contingency reserve constraints." _group =
LOG_GROUP_MODELS_VALIDATION
end
end
return
end
Loading
Loading