Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ JuMP = "^1.28"
LinearAlgebra = "1"
Logging = "1"
MathOptInterface = "1"
PowerNetworkMatrices = "^0.19"
PowerNetworkMatrices = "^0.20"
PrettyTables = "3.1"
Random = "^1.10"
Serialization = "1"
Expand Down
7 changes: 5 additions & 2 deletions src/InfrastructureOptimizationModels.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import LinearAlgebra
import JSON3
import InfrastructureSystems
import PowerNetworkMatrices
import PowerNetworkMatrices: PTDF, VirtualPTDF, LODF, VirtualLODF
import PowerNetworkMatrices: PTDF, VirtualPTDF, LODF, VirtualLODF, VirtualMODF
import InfrastructureSystems: @assert_op, TableFormat, list_recorder_events, get_name
import InfrastructureSystems:
get_value_curve, get_power_units, get_function_data, get_proportional_term,
Expand Down Expand Up @@ -166,11 +166,13 @@ export InitialCondition

# Network Relevant Exports
export NetworkModel
export get_PTDF_matrix, get_LODF_matrix, get_reduce_radial_branches
export get_PTDF_matrix, get_MODF_matrix, get_reduce_radial_branches
export get_outages
export get_duals, get_reference_buses, get_subnetworks, get_bus_area_map
export get_evaluations, has_subnetworks, get_subsystem
export set_subsystem!, add_dual!
export requires_all_branch_models, supports_branch_filtering, ignores_branch_filtering
export supports_outages
export validate_network_model
export BranchReductionOptimizationTracker
export get_variable_dict, get_constraint_dict, get_constraint_map_by_type
Expand Down Expand Up @@ -530,6 +532,7 @@ export PTDF
export VirtualPTDF
export LODF
export VirtualLODF
export VirtualMODF
export get_name
export get_model_base_power
export get_optimizer_stats
Expand Down
66 changes: 46 additions & 20 deletions src/common_models/add_constraint_dual.jl
Original file line number Diff line number Diff line change
Expand Up @@ -79,30 +79,56 @@ function assign_dual_variable!(
if isempty(metas)
device_names = IS.get_name.(devices)
add_dual_container!(container, constraint_type, D, device_names, time_steps)
else
# Reuse the existing constraint container's row axis so the dual axis
# matches the constraint exactly. Network reductions (radial /
# degree-two) drop branches that pass the device-model filter, so the
# constraint axis is a strict subset of IS.get_name.(devices). Sizing
# the dual from the device list would leave the dual broadcast in
# process_duals incompatible with the constraint matrix.
for meta in metas
existing =
get_constraint(container, ConstraintKey(constraint_type, D, meta))
row_axis = axes(existing)[1]
add_dual_container!(
container,
constraint_type,
D,
row_axis,
time_steps;
meta = meta,
)
end
return
end
for meta in metas
key = ConstraintKey(constraint_type, D, meta)
existing = get_constraint(container, key)
_assign_dual_from_existing!(container, key, existing, D, time_steps)
end
return
end

# Sparse constraints (e.g. post-contingency flow-rate constraints keyed by
# (outage_id, name, t)) have no `axes`. Mirror the constraint's exact sparse keys
# into a Float64 dual container so the dual matches the constraint storage one-to-one.
function _assign_dual_from_existing!(
container::OptimizationContainer,
key::ConstraintKey,
existing::SparseAxisArray,
::Type{D},
time_steps,
) where {D}
dual_container =
SparseAxisArray(Dict(k => zero(Float64) for k in keys(existing.data)))
_assign_container!(container.duals, key, dual_container)
return
end
Comment on lines +95 to +106
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 don't mind this personally.


# Reuse the existing constraint container's row axis so the dual axis matches the
# constraint exactly. Network reductions (radial / degree-two) drop branches that
# pass the device-model filter, so the constraint axis is a strict subset of
# IS.get_name.(devices). Sizing the dual from the device list would leave the dual
# broadcast in process_duals incompatible with the constraint matrix.
function _assign_dual_from_existing!(
container::OptimizationContainer,
key::ConstraintKey,
existing::DenseAxisArray,
::Type{D},
time_steps,
) where {D}
row_axis = axes(existing)[1]
add_dual_container!(
container,
get_entry_type(key),
D,
row_axis,
time_steps;
meta = key.meta,
)
return
end

function _existing_constraint_metas(
container::OptimizationContainer,
::Type{T},
Expand Down
47 changes: 47 additions & 0 deletions src/core/device_model.jl
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ end
duals::Vector{DataType},
services::Vector{ServiceModel}
attributes::Dict{String, Any}
outages::AbstractVector{<:IS.InfrastructureSystemsComponent}
)

Establishes the model for a particular device specified by type. Uses the keyword argument
Expand All @@ -38,6 +39,15 @@ feedforward to enable passing values between operation model at simulation time
- `duals::Vector{DataType} = Vector{DataType}()`: use to pass constraint type to calculate the duals. The DataType needs to be a valid ConstraintType
- `time_series_names::Dict{Type{<:TimeSeriesParameter}, String} = get_default_time_series_names(D, B)` : use to specify time series names associated to the device`
- `attributes::Dict{String, Any} = get_default_attributes(D, B)` : use to specify attributes to the device
- `outages::AbstractVector{<:IS.InfrastructureSystemsComponent} = IS.InfrastructureSystemsComponent[]` :
N-1 contingencies to model when the formulation is security-constrained. The
constructor stores the `IS.get_uuid(outage)` of each entry as a key in the model's
`outages::Dict{UUID, Dict{DataType, Set{String}}}` field with empty inner maps;
template validation in downstream packages fills the inner maps with the per-type
set of monitored component names that each outage carries. Power-specific
validation (e.g. checking that entries are `PSY.Outage` subtypes) lives in
`PowerOperationsModels`. If `B` is not security-constrained, a non-empty value is
dropped with a warning.

# Example
```julia
Expand All @@ -55,6 +65,11 @@ mutable struct DeviceModel{
time_series_names::Dict{Type{<:ParameterType}, String}
attributes::Dict{String, Any}
subsystem::Union{Nothing, String}
# Keyed by UUID to match PNM's `get_registered_contingencies(::VirtualMODF) ::
# Dict{UUID, ContingencySpec}` so the consolidation step in network_model.jl can
# set-diff directly. UUIDs are also stable across (de)serialization in a way that
# live component references aren't.
outages::Dict{Base.UUID, Dict{DataType, Set{String}}}
Comment thread
acostarelli marked this conversation as resolved.
device_cache::Vector{D}
function DeviceModel(
::Type{D},
Expand All @@ -64,6 +79,8 @@ mutable struct DeviceModel{
duals = Vector{DataType}(),
time_series_names = get_default_time_series_names(D, B),
attributes = Dict{String, Any}(),
outages::AbstractVector{<:IS.InfrastructureSystemsComponent} =
IS.InfrastructureSystemsComponent[],
) where {D <: IS.InfrastructureSystemsComponent, B <: AbstractDeviceFormulation}
attributes_ = get_default_attributes(D, B)
for (k, v) in attributes
Expand All @@ -72,6 +89,7 @@ mutable struct DeviceModel{

_check_device_formulation(D)
_check_device_formulation(B)
outages_field = _add_device_model_outages(D, B, outages)
new{D, B}(
feedforwards,
use_slacks,
Expand All @@ -80,11 +98,39 @@ mutable struct DeviceModel{
time_series_names,
attributes_,
nothing,
outages_field,
Vector{D}(),
)
end
end

function _add_device_model_outages(
::Type{D},
::Type{B},
outages::AbstractVector{<:IS.InfrastructureSystemsComponent},
) where {D <: IS.InfrastructureSystemsComponent, B <: AbstractDeviceFormulation}
field = Dict{Base.UUID, Dict{DataType, Set{String}}}()
isempty(outages) && return field
if !supports_outages(B)
@warn "DeviceModel{$D, $B}: 'outages' kwarg ignored — formulation does \
not support N-1 contingencies."
return field
end
for outage in outages
field[IS.get_uuid(outage)] = Dict{DataType, Set{String}}()
end
return field
end

"""
supports_outages(::Type{<:AbstractDeviceFormulation}) -> Bool

Trait declaring whether a device formulation consumes `DeviceModel.outages`.
Defaults to `false`. POM specializes this to `true` for security-constrained branch
formulations (`AbstractSecurityConstrainedStaticBranch`).
"""
supports_outages(::Type{<:AbstractDeviceFormulation}) = false

get_component_type(
::DeviceModel{D, B},
) where {D <: IS.InfrastructureSystemsComponent, B <: AbstractDeviceFormulation} = D
Expand All @@ -101,6 +147,7 @@ get_attributes(m::DeviceModel) = m.attributes
get_attribute(::Nothing, ::String) = nothing
get_attribute(m::DeviceModel, key::String) = get(m.attributes, key, nothing)
get_subsystem(m::DeviceModel) = m.subsystem
get_outages(m::DeviceModel) = m.outages
get_device_cache(m::DeviceModel) = m.device_cache

set_subsystem!(m::DeviceModel, id::String) = m.subsystem = id
Expand Down
17 changes: 16 additions & 1 deletion src/core/dual_processing.jl
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
# DenseAxisArray duals broadcast over the backing array. Post-contingency
# duals are SparseAxisArray (Dict-backed), where `.data .= …` is undefined, so
# copy per key instead.
function _copy_dual_values!(dual::DenseAxisArray, constraint::DenseAxisArray)
dual.data .= jump_value.(constraint).data
return
end

function _copy_dual_values!(dual::SparseAxisArray, constraint::SparseAxisArray)
for (k, cref) in constraint.data
dual.data[k] = jump_value(cref)
end
return
end

function process_duals(container::OptimizationContainer, lp_optimizer)
var_container = get_variables(container)
for (k, v) in var_container
Expand Down Expand Up @@ -68,7 +83,7 @@ function process_duals(container::OptimizationContainer, lp_optimizer)
if JuMP.has_duals(jump_model)
for (key, dual) in get_duals(container)
constraint = get_constraint(container, key)
dual.data .= jump_value.(constraint).data
_copy_dual_values!(dual, constraint)
end
end

Expand Down
99 changes: 92 additions & 7 deletions src/core/network_model.jl
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@ Establishes the NetworkModel for a given AC network formulation type.
Adds slack buses to the network modeling.
- `PTDF_matrix::Union{PNM.PowerNetworkMatrix, Nothing}` = nothing
PTDF/VirtualPTDF matrix produced by PowerNetworkMatrices (optional).
- `LODF_matrix::Union{PNM.PowerNetworkMatrix, Nothing}` = nothing
LODF/VirtualLODF matrix produced by PowerNetworkMatrices (optional).
- `MODF_matrix::Union{PNM.VirtualMODF, Nothing}` = nothing
VirtualMODF matrix for security-constrained models (N-k contingencies).
If `nothing` and the template includes a security-constrained branch
formulation, the matrix is constructed from the system during
`instantiate_network_model!` (same pattern as PTDF).
- `reduce_radial_branches::Bool` = false
Enable radial branch reduction when building network matrices.
- `reduce_degree_two_branches::Bool` = false
Expand All @@ -41,7 +44,7 @@ Establishes the NetworkModel for a given AC network formulation type.
# Notes
- `modeled_branch_types` and `reduced_branch_tracker` are internal fields managed by the model.
- `subsystem` can be set after construction via `set_subsystem!(model, id)`.
- PTDF/LODF inputs are validated against the requested reduction flags and may raise
- PTDF inputs are validated against the requested reduction flags and may raise
a ConflictingInputsError if they are inconsistent with `reduce_radial_branches`
or `reduce_degree_two_branches`.
Comment thread
acostarelli marked this conversation as resolved.
Outdated

Expand All @@ -57,7 +60,7 @@ Establishes the NetworkModel for a given AC network formulation type.
mutable struct NetworkModel{T <: AbstractPowerModel}
use_slacks::Bool
PTDF_matrix::Union{Nothing, PNM.PowerNetworkMatrix}
LODF_matrix::Union{Nothing, PNM.PowerNetworkMatrix}
MODF_matrix::Union{Nothing, PNM.VirtualMODF}
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.

Ignoring for now.

subnetworks::Dict{Int, Set{Int}}
bus_area_map::Dict{IS.InfrastructureSystemsComponent, Int}
duals::Vector{DataType}
Expand All @@ -74,7 +77,7 @@ mutable struct NetworkModel{T <: AbstractPowerModel}
::Type{T};
use_slacks = false,
PTDF_matrix = nothing,
LODF_matrix = nothing,
MODF_matrix = nothing,
reduce_radial_branches = false,
reduce_degree_two_branches = false,
subnetworks = Dict{Int, Set{Int}}(),
Expand All @@ -86,7 +89,7 @@ mutable struct NetworkModel{T <: AbstractPowerModel}
new{T}(
use_slacks,
PTDF_matrix,
LODF_matrix,
MODF_matrix,
subnetworks,
Dict{IS.InfrastructureSystemsComponent, Int}(),
duals,
Expand All @@ -104,7 +107,7 @@ end

get_use_slacks(m::NetworkModel) = m.use_slacks
get_PTDF_matrix(m::NetworkModel) = m.PTDF_matrix
get_LODF_matrix(m::NetworkModel) = m.LODF_matrix
get_MODF_matrix(m::NetworkModel) = m.MODF_matrix
Comment on lines 63 to +110
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.

Not important

get_reduce_radial_branches(m::NetworkModel) = m.reduce_radial_branches
get_network_reduction(m::NetworkModel) = m.network_reduction
get_duals(m::NetworkModel) = m.duals
Expand All @@ -130,6 +133,88 @@ function add_dual!(model::NetworkModel, dual)
return
end

function _build_network_reductions(
model::NetworkModel,
irreducible_buses::Vector{Int64},
)
reductions = PNM.NetworkReduction[]
if model.reduce_radial_branches
push!(reductions, PNM.RadialReduction(; irreducible_buses = irreducible_buses))
end
if model.reduce_degree_two_branches
push!(
reductions,
PNM.DegreeTwoReduction(; irreducible_buses = irreducible_buses),
)
end
return reductions
end

# Verify a user-provided MODF Matrix was built with the same network reduction
# as the active reduction (derived from the PTDF Matrix). Equality of the bus
# reduction map is the decisive check: it fixes the reduced bus/arc numbering
# the post-contingency builder uses to index `modf_matrix[arc, outage_spec]`.
function _validate_provided_modf_reduction!(
modf::PNM.VirtualMODF,
network_reduction::PNM.NetworkReductionData,
)
if PNM.get_bus_reduction_map(modf.network_reduction_data) !=
PNM.get_bus_reduction_map(network_reduction)
throw(
IS.ConflictingInputsError(
"The provided MODF Matrix was built with a different network \
reduction than the active reduction derived from the PTDF \
Matrix. Rebuild the MODF with a consistent network reduction, \
or omit it so it is recalculated automatically.",
),
)
end
return
end

"""
True if any branch DeviceModel in `branch_models` uses a formulation that
consumes `DeviceModel.outages` (per `supports_outages`). POM's
`AbstractSecurityConstrainedStaticBranch` specialization makes that trait
return `true`; non-SC formulations default to `false`.

`BranchModelContainer` (`Dict{Symbol, DeviceModelForBranches}`) is defined at
the top of this file and exported from IOM.
"""
function _template_has_outage_aware_branch(branch_models::BranchModelContainer)
Comment thread
acostarelli marked this conversation as resolved.
for v in values(branch_models)
if supports_outages(get_formulation(v))
return true
end
end
return false
end

"""
Drop outages from each outage-aware-branch `DeviceModel` whose UUID isn't
registered on `modf_matrix`; without this they'd `KeyError` downstream in
post-contingency expression construction. PNM's `_register_outages!` silently
skips outages it can't convert to a `NetworkModification`, so the IOM-side
view of `m.outages` can be a strict superset of what's actually usable.
"""
function _consolidate_device_model_outages_with_modf!(
branch_models::BranchModelContainer,
modf_matrix::PNM.VirtualMODF,
)
registered = PNM.get_registered_contingencies(modf_matrix)
for m in values(branch_models)
supports_outages(get_formulation(m)) || continue
for uuid in setdiff(keys(m.outages), keys(registered))
@warn "Outage $(uuid) (DeviceModel{$(get_component_type(m)), \
$(get_formulation(m))}) is not registered on the MODF \
matrix and will not contribute any post-contingency \
constraints." _group = LOG_GROUP_MODELS_VALIDATION
delete!(m.outages, uuid)
end
end
return
end

# Default implementations for network model compatibility checks
# These can be extended in PowerOperationsModels for specific network formulations
requires_all_branch_models(::Type{<:AbstractPowerModel}) = true
Expand Down
Loading
Loading