[WIP] Add event models - #163
Conversation
Bring the build-time event (contingency-outage) framework over from
PowerSimulations.jl. EventModel{<:PSY.Contingency}/EventKey and the four
AbstractEventCondition types are PowerSystems-specific, so they live here in
POM (reversing the earlier descope-to-IOM note).
- core/event_model.jl, core/event_keys.jl: EventModel/EventKey (<: the IOM
AbstractEventModel/AbstractEventKey abstracts), conditions, and a
set_event_model!(device_model, event_model) convenience that derives the key.
- common_models/contingency{,_arguments,_constraints}.jl: real
add_event_arguments!/add_event_constraints!, the EventParameter parameter
path, offset->balance add_to_expression!, the reactive outage constraint, and
get_max_active_power (an IOM extension stub).
- feedforward_interface.jl event stubs are now documented fallbacks.
- test/test_events.jl: build/solve-level coverage (thermal/renewable/load x
CopperPlate/DCP/ACP) plus a behavioral AvailableStatusParameter->0 forces
dispatch to zero test; mock_construct_device!(...; add_event_model=true) wired.
Depends on the DeviceModel.events field added in IOM ac/sienna1-port; the
[sources] IOM pin is pointed at that branch until it merges to main.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds a build-time contingency/event modeling framework to PowerOperationsModels (POM), wiring EventModel/EventKey storage into DeviceModel usage and implementing event parameter/constraint builders so single-stage DecisionModels can include outage-style events.
Changes:
- Adds core event types (
EventModel,EventKey, event conditions) and exports/import bridges needed for event model attachment and lookup. - Implements contingency argument/constraint builders (parameters + outage constraints + balance offsets) overriding the prior no-op event hooks.
- Adds build/solve-level tests exercising event build + constraint presence across CopperPlate/DCP/ACP and multiple device types.
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| test/test_utils/mock_operation_models.jl | Enables mock device construction to attach an EventModel instead of erroring. |
| test/test_events.jl | New test coverage for building/solving models with event parameters/constraints. |
| test/Project.toml | Pins InfrastructureOptimizationModels source rev for test environment. |
| src/PowerOperationsModels.jl | Wires in new includes/imports/exports for event framework + contingency builders. |
| src/core/feedforward_interface.jl | Updates event hook comments; keeps generic no-op fallbacks. |
| src/core/event_model.jl | Introduces event condition types and EventModel plus convenience set_event_model!. |
| src/core/event_keys.jl | Adds EventKey implementation and accessor methods. |
| src/common_models/contingency.jl | Adds event-parameter defaults and get_max_active_power bridge for outage RHS. |
| src/common_models/contingency_constraints.jl | Adds outage constraint builders for thermal/renewable/load across network models. |
| src/common_models/contingency_arguments.jl | Adds event-parameter builders and offset→balance add_to_expression! methods. |
| Project.toml | Pins InfrastructureOptimizationModels source rev for the package environment. |
| .claude/pom_port_plan.md | Updates port plan documentation to reflect the event framework being implemented in POM. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| struct StateVariableValueCondition <: AbstractEventCondition | ||
| variable_type::VariableType | ||
| device_type::Type{<:PSY.Device} | ||
| device_name::String | ||
| value::Float64 | ||
| end |
| Establishes an event condition that is triggered when a user defined function evaluates to true. | ||
| The function should take the simulation state as its only argument and return true when the event should be triggered and false otherwise. | ||
|
|
| function get_empty_timeseries_mapping( | ||
| ::Type{PSY.FixedForcedOutage}, | ||
| ) | ||
| return Dict{Symbol, Union{String, Nothing}}( | ||
| :outage_status => nothing, | ||
| ) | ||
| end | ||
|
|
||
| function get_empty_timeseries_mapping( | ||
| ::Type{PSY.GeometricDistributionForcedOutage}, | ||
| ) | ||
| return Dict{Symbol, Union{String, Nothing}}( | ||
| :mean_time_to_recovery => nothing, | ||
| :outage_transition_probability => nothing, | ||
| ) | ||
| end |
|
|
||
| [sources] | ||
| InfrastructureOptimizationModels = {rev = "main", url = "https://github.com/Sienna-Platform/InfrastructureOptimizationModels.jl"} | ||
| InfrastructureOptimizationModels = {rev = "ac/sienna1-port", url = "https://github.com/Sienna-Platform/InfrastructureOptimizationModels.jl"} |
| get_device_type(c::StateVariableValueCondition) = c.device_type | ||
| get_device_name(c::StateVariableValueCondition) = c.device_name | ||
| get_value(c::StateVariableValueCondition) = c.value | ||
|
|
There was a problem hiding this comment.
So far reading, I feel like this could be in IOM.
| - `variable_type::Type{<:VariableType}`: variable to be monitored | ||
| - `device_type::Type{<:PSY.Device}`: device type to be monitored | ||
| - `device_name::String`: name of monitored device | ||
| - `value::Float64`: value to compare to in p.u. |
There was a problem hiding this comment.
Which p.u., DU or SU?
| - `value::Float64`: value to compare to in p.u. | ||
| """ | ||
| struct StateVariableValueCondition <: AbstractEventCondition | ||
| variable_type::VariableType |
There was a problem hiding this comment.
I'd wonder about taking things one step further and adding type parameters:
struct StateVariableValueCondition{T, D} where {T <: VariableType, D <: PSY.Device}
variable_type::T
device_type::D
device_name::String
value::Float64
endWait, that looks awfully similar to existing code in IOM:
struct VariableKey{T <: VariableType, U <: InfrastructureSystemsType} <:
OptimizationContainerKey{T, U}
meta::String
endwhich leads me to question if this is necessary. Can we just use the existing container structure instead?
There was a problem hiding this comment.
Oh. Wait, this must be unfinished: nothing creates a StateVariableValueCondition.
| DiscreteEventCondition(condition_function::Function) | ||
|
|
||
| Establishes an event condition that is triggered when a user defined function evaluates to true. | ||
| The function should take the simulation state as its only argument and return true when the event should be triggered and false otherwise. |
There was a problem hiding this comment.
It takes the simulation state? Then why is this in POM and not PSI?
|
|
||
| get_condition_function(c::DiscreteEventCondition) = c.condition_function | ||
|
|
||
| mutable struct EventModel{D <: PSY.Contingency, B <: AbstractEventCondition} <: |
There was a problem hiding this comment.
Strange to me to have DeviceModel and ServiceModel be IOM types but not this.
| device_model::DeviceModel{U, W}, | ||
| event_model::EventModel, | ||
| ) where {T <: EventParameter, U <: PSY.Component, W <: AbstractDeviceFormulation} | ||
| _add_parameters!(container, T, devices, device_model, event_model) |
There was a problem hiding this comment.
Why do we have a helper if this is the call site?
| @@ -0,0 +1,326 @@ | |||
| # Event (contingency) arguments: add event parameters during ArgumentConstructStage. | |||
| @@ -0,0 +1,326 @@ | |||
| # Event (contingency) arguments: add event parameters during ArgumentConstructStage. | |||
| # Ported from PowerSimulations.jl `src/contingency_model/contingency_arguments.jl`, | |||
There was a problem hiding this comment.
POM is usually setup that we have both the argument and model construct stages in one file suffixed _constructor.jl and then everything else is a separate file.
| area_name = PSY.get_name(PSY.get_area(PSY.get_bus(d))) | ||
| name = PSY.get_name(d) | ||
| add_proportional_to_jump_expression!( | ||
| get_expression(container, T, PSY.Area)[area_name, t], |
There was a problem hiding this comment.
This shouldn't get called repeatedly.
| ) where { | ||
| T <: Union{Vector{U}, IS.FlattenIteratorWrapper{U}}, | ||
| } where {U <: PSY.StaticInjection} | ||
| for (key, event_model) in get_events(device_model) |
There was a problem hiding this comment.
Strange to me that this works across multiple event formulations
|
@luke-kiernan @jd-lara Thoughts on keeping this in PSI? From my understanding, other model constructions parts don't rely on the presence/absence of event variables or constraints, so they could be added when the simulation is built. I'm just thinking it's not a power device formulation so it doesn't really belong in POM. |
luke-kiernan
left a comment
There was a problem hiding this comment.
So far reading, I feel like this could be in IOM.
It takes the simulation state? Then why is this in POM and not PSI?
Seconded: this seems like it's organizationally in the wrong spot.
The EventKey definition should go back in IOM: it's structurally identical to the other key families. Maybe a few other types/helpers end up back there too, like AbstractEventCondition. EventModel and other feeds forwards/multi-timestep stuff should go in PSI.
| [d for d in devices if PSY.has_supplemental_attributes(d, event_type)] | ||
| isempty(devices_with_attributes) && | ||
| error("no devices found with a supplemental attribute for event $event_type") | ||
| lhs_type = |
There was a problem hiding this comment.
Perf note: type unstable. Doesn't look like there's much you can do, though
| # outage bound reuses IOM's `EventParameter` `add_parameterized_upper_bound_range_constraints` | ||
| # path (RHS = `get_max_active_power(device) * AvailableStatusParameter`). | ||
|
|
||
| function add_event_constraints!( |
There was a problem hiding this comment.
These add_event_constraints! functions are a prime candidate for consolidation via multiple dispatch. I asked AI to analyze them:
There are 6
add_event_constraints!methods, forming a 3×2 grid:Axis 1 — device type
U. Controlslhs_typefor the active-power upper-bound constraint:
U <: PSY.ThermalGen→lhs_type = ActivePowerRangeExpressionUB(always, unconditional)U <: PSY.RenewableGen→lhs_typeisActivePowerRangeExpressionUBifhas_service_model(device_model), elseActivePowerVariableU <: PSY.ElectricLoad→lhs_type = ActivePowerVariable(always, unconditional)Axis 2 —
W <: PM.AbstractActivePowerModelvsW <: PM.AbstractPowerModel: identical active-power block in both; theAbstractPowerModelvariant additionally callsadd_reactive_power_contingency_constraintto addReactivePowerOutageConstraint.
| - `value::Float64`: value to compare to in p.u. | ||
| """ | ||
| struct StateVariableValueCondition <: AbstractEventCondition | ||
| variable_type::VariableType |
There was a problem hiding this comment.
I'd wonder about taking things one step further and adding type parameters:
struct StateVariableValueCondition{T, D} where {T <: VariableType, D <: PSY.Device}
variable_type::T
device_type::D
device_name::String
value::Float64
endWait, that looks awfully similar to existing code in IOM:
struct VariableKey{T <: VariableType, U <: InfrastructureSystemsType} <:
OptimizationContainerKey{T, U}
meta::String
endwhich leads me to question if this is necessary. Can we just use the existing container structure instead?
| - `value::Float64`: value to compare to in p.u. | ||
| """ | ||
| struct StateVariableValueCondition <: AbstractEventCondition | ||
| variable_type::VariableType |
There was a problem hiding this comment.
Oh. Wait, this must be unfinished: nothing creates a StateVariableValueCondition.
No description provided.