Skip to content

[WIP] Add event models - #163

Draft
acostarelli wants to merge 1 commit into
ac/sienna1-portfrom
ac/event-model
Draft

[WIP] Add event models#163
acostarelli wants to merge 1 commit into
ac/sienna1-portfrom
ac/event-model

Conversation

@acostarelli

Copy link
Copy Markdown
Member

No description provided.

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>

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 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.

Comment thread src/core/event_model.jl
Comment on lines +41 to +46
struct StateVariableValueCondition <: AbstractEventCondition
variable_type::VariableType
device_type::Type{<:PSY.Device}
device_name::String
value::Float64
end
Comment thread src/core/event_model.jl
Comment on lines +56 to +58
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.

Comment thread src/core/event_model.jl
Comment on lines +90 to +105
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
Comment thread test/Project.toml

[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"}
Comment thread src/core/event_model.jl
get_device_type(c::StateVariableValueCondition) = c.device_type
get_device_name(c::StateVariableValueCondition) = c.device_name
get_value(c::StateVariableValueCondition) = c.value

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.

So far reading, I feel like this could be in IOM.

Comment thread src/core/event_model.jl
- `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.

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.

Which p.u., DU or SU?

Comment thread src/core/event_model.jl
- `value::Float64`: value to compare to in p.u.
"""
struct StateVariableValueCondition <: AbstractEventCondition
variable_type::VariableType

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.

Pass by type

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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
end

Wait, that looks awfully similar to existing code in IOM:

struct VariableKey{T <: VariableType, U <: InfrastructureSystemsType} <:
       OptimizationContainerKey{T, U}
    meta::String
end

which leads me to question if this is necessary. Can we just use the existing container structure instead?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Oh. Wait, this must be unfinished: nothing creates a StateVariableValueCondition.

Comment thread src/core/event_model.jl
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.

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 takes the simulation state? Then why is this in POM and not PSI?

Comment thread src/core/event_model.jl

get_condition_function(c::DiscreteEventCondition) = c.condition_function

mutable struct EventModel{D <: PSY.Contingency, B <: AbstractEventCondition} <:

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.

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)

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.

Why do we have a helper if this is the call site?

@@ -0,0 +1,326 @@
# Event (contingency) arguments: add event parameters during ArgumentConstructStage.

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

@@ -0,0 +1,326 @@
# Event (contingency) arguments: add event parameters during ArgumentConstructStage.
# Ported from PowerSimulations.jl `src/contingency_model/contingency_arguments.jl`,

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.

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],

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 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)

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.

Strange to me that this works across multiple event formulations

@acostarelli

acostarelli commented Jul 1, 2026

Copy link
Copy Markdown
Member Author

@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 luke-kiernan left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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!(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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. Controls lhs_type for the active-power upper-bound constraint:

  • U <: PSY.ThermalGenlhs_type = ActivePowerRangeExpressionUB (always, unconditional)
  • U <: PSY.RenewableGenlhs_type is ActivePowerRangeExpressionUB if has_service_model(device_model), else ActivePowerVariable
  • U <: PSY.ElectricLoadlhs_type = ActivePowerVariable (always, unconditional)

Axis 2 — W <: PM.AbstractActivePowerModel vs W <: PM.AbstractPowerModel: identical active-power block in both; the AbstractPowerModel variant additionally calls add_reactive_power_contingency_constraint to add ReactivePowerOutageConstraint.

Comment thread src/core/event_model.jl
- `value::Float64`: value to compare to in p.u.
"""
struct StateVariableValueCondition <: AbstractEventCondition
variable_type::VariableType

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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
end

Wait, that looks awfully similar to existing code in IOM:

struct VariableKey{T <: VariableType, U <: InfrastructureSystemsType} <:
       OptimizationContainerKey{T, U}
    meta::String
end

which leads me to question if this is necessary. Can we just use the existing container structure instead?

Comment thread src/core/event_model.jl
- `value::Float64`: value to compare to in p.u.
"""
struct StateVariableValueCondition <: AbstractEventCondition
variable_type::VariableType

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Oh. Wait, this must be unfinished: nothing creates a StateVariableValueCondition.

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.

4 participants