Skip to content
5 changes: 3 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ PowerFlows = "94fada2c-fd9a-4e89-8d82-81405f5cb4f6"
[sources]
InfrastructureSystems = {rev = "IS4", url = "https://github.com/Sienna-Platform/InfrastructureSystems.jl"}
PowerSystems = {rev = "psy6", url = "https://github.com/Sienna-Platform/PowerSystems.jl"}
InfrastructureOptimizationModels = {rev = "main", url = "https://github.com/Sienna-Platform/InfrastructureOptimizationModels.jl"}
PowerNetworkMatrices = {rev = "psy6", url = "https://github.com/Sienna-Platform/PowerNetworkMatrices.jl"}
# todo: repin once iom merged
InfrastructureOptimizationModels = {rev = "ac/ordc", url = "https://github.com/Sienna-Platform/InfrastructureOptimizationModels.jl"}
PowerNetworkMatrices = {rev = "ac/port-branch-admittance", url = "https://github.com/Sienna-Platform/PowerNetworkMatrices.jl"}

[extensions]
PowerFlowsExt = "PowerFlows"
Expand Down
124 changes: 39 additions & 85 deletions src/ac_transmission_models/AC_branches.jl
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,37 @@ function get_default_attributes(
)
end

"""
`MonitoredLine` DeviceModel attribute. When `true`, both endpoint buses of every
monitored line are pinned irreducible so zero-impedance lines survive the network
reduction. Defaults to `false` (such lines are reduced away and not modeled). For
the "base case flowgate" use case.
"""
const MODEL_ALL_BRANCHES_KEY = "model_all_branches"

# Specialize the generic `ACTransmission` defaults for `MonitoredLine` to add
# `MODEL_ALL_BRANCHES_KEY` (default `false`) alongside the inherited keys.
function get_default_attributes(
::Type{PSY.MonitoredLine},
::Type{V},
) where {V <: AbstractBranchFormulation}
return Dict{String, Any}(
PARALLEL_BRANCH_MAX_RATING_KEY => "single_element_contingency",
MODEL_ALL_BRANCHES_KEY => false,
)
end

function get_default_attributes(
::Type{PSY.MonitoredLine},
::Type{V},
) where {V <: AbstractSecurityConstrainedStaticBranch}
return Dict{String, Any}(
PARALLEL_BRANCH_MAX_RATING_KEY => "single_element_contingency",
"include_planned_outages" => false,
MODEL_ALL_BRANCHES_KEY => false,
)
end

# Resolve the per-DeviceModel attribute to one of the explicit PNM rating functions.
# `MixedBranchesParallel` ignores the attribute and always uses the plain sum, since
# the constituent branches may carry different DeviceModel preferences and there is
Expand Down Expand Up @@ -1393,59 +1424,22 @@ function add_to_objective_function!(
return
end

# Flip a π-admittance tuple to the opposite orientation (from<->to). Reduced equivalents
# may be keyed by an arc whose orientation is reversed vs the surviving branch's retained
# from->to; reorient so coefficients match (from_bus, to_bus). g/b are symmetric; from/to
# shunts swap; phase shift negates. Reduced line equivalents have tap == 1.
function _reverse_admittance(adm)
@assert adm.tap == 1.0 "Cannot reorient a reduced arc with a non-unit tap ($(adm.tap))."
return (
g = adm.g,
b = adm.b,
g_fr = adm.g_to,
b_fr = adm.b_to,
g_to = adm.g_fr,
b_to = adm.b_fr,
tap = adm.tap,
shift = -adm.shift,
)
end

# Reduction-aware admittance for the retained arc (from_no -> to_no). Returns the PNM
# series/parallel equivalent π-tuple oriented from->to when the arc was aggregated by
# reduction, or `nothing` when the arc is direct (caller falls back to the branch's own).
function _reduced_arc_admittance(nr::PNM.NetworkReductionData, from_no::Int, to_no::Int)
series_map = PNM.get_series_branch_map(nr)
parallel_map = PNM.get_parallel_branch_map(nr)
arc = (from_no, to_no)
rev = (to_no, from_no)
if haskey(series_map, arc)
return PNM.branch_admittance(series_map[arc], nr)
elseif haskey(series_map, rev)
return _reverse_admittance(PNM.branch_admittance(series_map[rev], nr))
elseif haskey(parallel_map, arc)
return PNM.branch_admittance(parallel_map[arc], nr)
elseif haskey(parallel_map, rev)
return _reverse_admittance(PNM.branch_admittance(parallel_map[rev], nr))
end
return nothing
end

# Admittance for `branch`'s ohm's law given its retained endpoints: the branch's own
# π-parameters for a direct/un-reduced arc, or PNM's reduction-aware equivalent for a
# series/parallel-aggregated arc.
# π-parameters (`PNM.branch_admittance`) for a direct/un-reduced arc, or PNM's
# reduction-aware equivalent (`PNM.reduced_arc_admittance`) for a series/parallel-aggregated
# arc.
function _resolve_branch_admittance(network_model, branch, from_no::Int, to_no::Int)
nr = get_network_reduction(network_model)
isempty(nr) && return PNM.branch_admittance(branch)
eq = _reduced_arc_admittance(nr, from_no, to_no)
eq = PNM.reduced_arc_admittance(nr, from_no, to_no)
return eq === nothing ? PNM.branch_admittance(branch) : eq
end

# One-pass per-branch network geometry for the native DCP/ACP builders. Computes each
# branch's retained endpoints and reduction-aware admittance ONCE so the ohm's-law and
# angle-limit builders don't each rebuild `number_to_name` and re-map endpoints.
# Each entry is a NamedTuple value-bag (same idiom as `_retained_bus` / `branch_admittance`
# in this file); `collapsed` marks branches whose endpoints fold into one retained bus.
# Each entry is a NamedTuple value-bag (same idiom as `_retained_bus`); `collapsed` marks
# branches whose endpoints fold into one retained bus.
function _branch_geometries(sys::PSY.System, network_model, devices)
number_to_name = _retained_number_to_name(sys, network_model)
geoms = NamedTuple[]
Expand Down Expand Up @@ -1475,46 +1469,6 @@ function _branch_geometries(sys::PSY.System, network_model, devices)
return geoms
end

"""
branch_flow_limits(branch) -> NamedTuple

Returns directional flow limits in per-unit MVA: `(from_to::Float64, to_from::Float64)`.
For symmetric branches both fields equal `PSY.get_rating(branch)`.
"""
function branch_flow_limits end

function branch_flow_limits(b::PSY.MonitoredLine)
fl = PSY.get_flow_limits(b, PSY.SU)
return (from_to = fl.from_to, to_from = fl.to_from)
end

function branch_flow_limits(
b::Union{
PSY.Line,
PSY.Transformer2W,
PSY.TapTransformer,
PSY.PhaseShiftingTransformer,
},
)
r = PSY.get_rating(b, PSY.SU)
return (from_to = r, to_from = r)
end

function branch_flow_limits(b::PNM.BranchesParallel)
r = PNM.get_equivalent_rating(b)
return (from_to = r, to_from = r)
end

function branch_flow_limits(b::PNM.BranchesSeries)
r = PNM.get_equivalent_rating(b)
return (from_to = r, to_from = r)
end

function branch_flow_limits(w::PNM.ThreeWindingTransformerWinding)
r = PNM.get_equivalent_rating(w)
return (from_to = r, to_from = r)
end

################################## Native ACP apparent-power rate constraints ###############

"""
Expand Down Expand Up @@ -2118,7 +2072,7 @@ function add_constraints!(
dname = PSY.get_name(d)
for w in PNM.three_winding_arcs(d)
wname = dname * "_" * w.suffix
adm = PNM.winding_admittance(w)
adm = PNM.winding_admittance(w.winding)
fr = _retained_bus(number_to_name, network_model, PSY.get_from(w.arc))
to = _retained_bus(number_to_name, network_model, PSY.get_to(w.arc))
fr.number == to.number && continue
Expand Down Expand Up @@ -2175,7 +2129,7 @@ function add_constraints!(
dname = PSY.get_name(d)
for w in PNM.three_winding_arcs(d)
wname = dname * "_" * w.suffix
adm = PNM.winding_admittance(w)
adm = PNM.winding_admittance(w.winding)
g, b, g_fr, b_fr, g_to, b_to, tm =
adm.g, adm.b, adm.g_fr, adm.b_fr, adm.g_to, adm.b_to, adm.tap
fr = _retained_bus(number_to_name, network_model, PSY.get_from(w.arc))
Expand Down
64 changes: 61 additions & 3 deletions src/ac_transmission_models/security_constrained_branch.jl
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,40 @@ function _find_shared_post_contingency_constraint_sources(
return src_lb, src_ub
end

"""
Locate the post-contingency slack variable containers that a shared constraint for
`(outage_id, name, t)` already references, registered by the source DeviceModel under
a component type other than `V`. Returns `(ub_source, lb_source)`; either slot is
`nothing` when the shared constraint was built without that slack (the source model
had `use_slacks = false`). Lets a reusing model alias those refs into its own slack
container so `get_variable`/`has_container_key` stay consistent with the constraints
it reuses, regardless of branch-model build order.
"""
function _find_shared_post_contingency_slack_sources(
container::OptimizationContainer,
::Type{V},
outage_id::String,
name::String,
t::Int,
) where {V <: PSY.ACTransmission}
target = (outage_id, name, t)
src_ub = nothing
src_lb = nothing
for (key, vc) in IOM.get_variables(container)
get_component_type(key) === V && continue
entry = get_entry_type(key)
if entry === PostContingencyFlowActivePowerSlackUpperBound &&
haskey(vc.data, target)
src_ub = vc
elseif entry === PostContingencyFlowActivePowerSlackLowerBound &&
haskey(vc.data, target)
src_lb = vc
end
!isnothing(src_ub) && !isnothing(src_lb) && break
end
return src_ub, src_lb
end

"""
Fast-path precheck: returns `true` iff any container of entry type `T` exists
under a component type other than `V`.
Expand Down Expand Up @@ -404,14 +438,38 @@ function add_constraints!(
container, T, V, outage_id, name, first(time_steps),
)
if !isnothing(src_lb) && !isnothing(src_ub)
# Reuse the first claimer's constraint refs verbatim; its
# slacks were already created and penalized, so do NOT add
# new ones here.
src_slack_ub, src_slack_lb =
_find_shared_post_contingency_slack_sources(
container, V, outage_id, name, first(time_steps),
)
source_has_slacks =
!isnothing(src_slack_ub) || !isnothing(src_slack_lb)
if source_has_slacks != use_slacks
error(
"Inconsistent `use_slacks` among security-constrained " *
"branch models that share post-contingency constraints " *
"for outage $outage_id, monitored component \"$name\": " *
"the shared constraints were built " *
(source_has_slacks ? "with" : "without") *
" slacks, but the `$V` branch model sets " *
"`use_slacks = $use_slacks`. Configure `use_slacks` " *
"identically across all security-constrained branch " *
"`DeviceModel`s whose outage/monitored sets overlap.",
)
end
for t in time_steps
con_ub[outage_id, name, t] =
src_ub.data[(outage_id, name, t)]
con_lb[outage_id, name, t] =
src_lb.data[(outage_id, name, t)]
isnothing(src_slack_ub) || (
slack_ub[outage_id, name, t] =
src_slack_ub.data[(outage_id, name, t)]
)
isnothing(src_slack_lb) || (
slack_lb[outage_id, name, t] =
src_slack_lb.data[(outage_id, name, t)]
)
end
continue
end
Expand Down
Loading
Loading