diff --git a/Project.toml b/Project.toml index efa0e9dc..c73a6482 100644 --- a/Project.toml +++ b/Project.toml @@ -27,7 +27,7 @@ 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"} +InfrastructureOptimizationModels = {rev = "jd/network-sources", url = "https://github.com/Sienna-Platform/InfrastructureOptimizationModels.jl"} PowerNetworkMatrices = {rev = "psy6", url = "https://github.com/Sienna-Platform/PowerNetworkMatrices.jl"} [extensions] diff --git a/src/PowerOperationsModels.jl b/src/PowerOperationsModels.jl index e29654a9..bd295b63 100644 --- a/src/PowerOperationsModels.jl +++ b/src/PowerOperationsModels.jl @@ -12,6 +12,10 @@ import Logging import PowerNetworkMatrices # Brought into namespace so the `export PTDF` / `export VirtualPTDF` below resolve (re-export to POM users) import PowerNetworkMatrices: PTDF, VirtualPTDF +# Brought into namespace so `NetworkReductionSpec(RadialReduction())` resolves unqualified +# after `using PowerOperationsModels` (re-export to POM users) +import PowerNetworkMatrices: + RadialReduction, DegreeTwoReduction, WardReduction, ZeroImpedanceBranchReduction import PowerSystems import PowerSystems: get_component import PrettyTables @@ -307,6 +311,9 @@ include("ac_transmission_models/branch_constructor.jl") # Network Models include("network_models/network_reductions.jl") +include("network_models/reduction_exceptions.jl") +include("network_models/network_sources.jl") +include("network_models/network_data.jl") include("network_models/instantiate_network_model.jl") include("network_models/network_slack_variables.jl") include("network_models/copperplate_model.jl") @@ -960,9 +967,19 @@ export AbstractReactivePowerNetworkModel export NFANetworkModel export DCPLLNetworkModel +# Network source declarations (network_models/network_sources.jl); the derived +# network-data containers stay unexported — they are build outputs, not user inputs. +export NetworkReductionSpec +export PrebuiltMatrixSource +export PrebuiltCoreSource + # PowerNetworkMatrices export PTDF export VirtualPTDF +export RadialReduction +export DegreeTwoReduction +export WardReduction +export ZeroImpedanceBranchReduction # Other utilities export get_name diff --git a/src/ac_transmission_models/AC_branches.jl b/src/ac_transmission_models/AC_branches.jl index 76a7a8d8..23cdf3f9 100644 --- a/src/ac_transmission_models/AC_branches.jl +++ b/src/ac_transmission_models/AC_branches.jl @@ -165,7 +165,7 @@ function add_variables!( U <: PSY.ACTransmission, F <: AbstractBranchFormulation} time_steps = get_time_steps(container) - net_reduction_data = network_model.network_reduction + net_reduction_data = get_network_reduction(network_model) branch_names = get_branch_argument_variable_axis(net_reduction_data, devices) reduced_branch_tracker = get_reduced_branch_tracker(network_model) all_branch_maps_by_type = PNM.get_all_branch_maps_by_type(net_reduction_data) @@ -513,7 +513,7 @@ function add_constraints!( V <: AbstractActivePowerModel, } time_steps = get_time_steps(container) - net_reduction_data = network_model.network_reduction + net_reduction_data = get_network_reduction(network_model) reduced_branch_tracker = get_reduced_branch_tracker(network_model) branch_names = get_branch_argument_constraint_axis( net_reduction_data, @@ -574,7 +574,7 @@ function add_constraints!( V <: AbstractPTDFNetworkModel, } time_steps = get_time_steps(container) - net_reduction_data = network_model.network_reduction + net_reduction_data = get_network_reduction(network_model) reduced_branch_tracker = get_reduced_branch_tracker(network_model) branch_names = get_branch_argument_constraint_axis( net_reduction_data, @@ -665,7 +665,7 @@ function add_flow_rate_constraint_with_parameters!( V <: AbstractPTDFNetworkModel, } time_steps = get_time_steps(container) - net_reduction_data = network_model.network_reduction + net_reduction_data = get_network_reduction(network_model) reduced_branch_tracker = get_reduced_branch_tracker(network_model) # POM's `get_branch_argument_constraint_axis` already performs per-arc claim @@ -821,7 +821,7 @@ function add_expressions!( ) where {B <: PSY.ACTransmission} time_steps = get_time_steps(container) ptdf = get_network_matrix(network_model) - net_reduction_data = network_model.network_reduction + net_reduction_data = get_network_reduction(network_model) branch_names = get_branch_argument_variable_axis(net_reduction_data, devices) # `collect` to a Vector so the spawn loop below can index it for multi-threading. name_to_arc_map = collect(PNM.get_name_to_arc_map(net_reduction_data, B)) @@ -877,7 +877,7 @@ function add_constraints!( time_steps = get_time_steps(container) branch_flow_expr = get_expression(container, PTDFBranchFlow, T) flow_variables = get_variable(container, FlowActivePowerVariable, T) - net_reduction_data = network_model.network_reduction + net_reduction_data = get_network_reduction(network_model) reduced_branch_tracker = get_reduced_branch_tracker(network_model) branches = get_branch_argument_constraint_axis( net_reduction_data, diff --git a/src/ac_transmission_models/security_constrained_branch.jl b/src/ac_transmission_models/security_constrained_branch.jl index dcc71b41..a973abdc 100644 --- a/src/ac_transmission_models/security_constrained_branch.jl +++ b/src/ac_transmission_models/security_constrained_branch.jl @@ -408,7 +408,7 @@ function add_constraints!( } time_steps = get_time_steps(container) - net_reduction_data = network_model.network_reduction + net_reduction_data = get_network_reduction(network_model) all_branch_maps_by_type = PNM.get_all_branch_maps_by_type(net_reduction_data) resolved = _resolve_monitored_arcs(device_model, net_reduction_data) @@ -611,7 +611,7 @@ function _add_modf_post_contingency_flow_expressions!( modf_matrix = get_contingency_matrix(network_model) registered_contingencies = PNM.get_registered_contingencies(modf_matrix) - net_reduction_data = network_model.network_reduction + net_reduction_data = get_network_reduction(network_model) resolved = _resolve_monitored_arcs(model, net_reduction_data) expression_container = _add_post_contingency_sparse_expression!( diff --git a/src/area_interchange.jl b/src/area_interchange.jl index 5496ddb5..4b8a29bb 100644 --- a/src/area_interchange.jl +++ b/src/area_interchange.jl @@ -180,7 +180,7 @@ function add_constraints!( ) area_ex_var = get_variable(container, FlowActivePowerVariable, PSY.AreaInterchange) - net_reduction_data = network_model.network_reduction + net_reduction_data = get_network_reduction(network_model) # Memoize the PTDF orientation sign per (type, name): a degree-two series # member whose native from→to is `:ToFrom` relative to the merged arc # contributes with a flipped sign to the area sum. diff --git a/src/common_models/add_parameters.jl b/src/common_models/add_parameters.jl index fd499112..7e6e7aaf 100644 --- a/src/common_models/add_parameters.jl +++ b/src/common_models/add_parameters.jl @@ -275,7 +275,7 @@ function _add_time_series_parameters!( end time_steps = get_time_steps(container) - net_reduction_data = network_model.network_reduction + net_reduction_data = get_network_reduction(network_model) reduced_branch_tracker = get_reduced_branch_tracker(network_model) all_branch_maps_by_type = PNM.get_all_branch_maps_by_type(net_reduction_data) diff --git a/src/common_models/make_system_expressions.jl b/src/common_models/make_system_expressions.jl index 49c6f76f..7424f91e 100644 --- a/src/common_models/make_system_expressions.jl +++ b/src/common_models/make_system_expressions.jl @@ -148,7 +148,7 @@ end ################################################################################# function _verify_area_subnetwork_topology(sys::PSY.System, subnetworks::Dict{Int, Set{Int}}) - if length(subnetworks) < 1 + if length(subnetworks) <= 1 @debug "Only one subnetwork detected in the system. Area - Subnetwork topology check is valid." return end @@ -156,7 +156,8 @@ function _verify_area_subnetwork_topology(sys::PSY.System, subnetworks::Dict{Int @warn "More than one subnetwork detected in AreaBalanceNetworkModel. Topology consistency checks must be conducted." area_map = PSY.get_aggregation_topology_mapping(PSY.Area, sys) - for (area, buses) in area_map + # Keyed by area name, not by the Area component. + for (area_name, buses) in area_map bus_numbers = [ PSY.get_number(b) for @@ -169,7 +170,7 @@ function _verify_area_subnetwork_topology(sys::PSY.System, subnetworks::Dict{Int end end if length(subnets) > 1 - @error "Area $(PSY.get_name(area)) is connected to multiple subnetworks $(subnets)." + @error "Area $(area_name) is connected to multiple subnetworks $(subnets)." throw( IS.ConflictingInputsError( "AreaBalanceNetworkModel doesn't support systems with Areas distributed across multiple asynchronous areas", diff --git a/src/core/network_formulations.jl b/src/core/network_formulations.jl index 18542b79..b8363f48 100644 --- a/src/core/network_formulations.jl +++ b/src/core/network_formulations.jl @@ -42,6 +42,13 @@ requires_all_branch_models(::Type{AreaBalanceNetworkModel}) = false branches_modeled(::Type{CopperPlateNetworkModel}) = false branches_modeled(::Type{AreaBalanceNetworkModel}) = false +# Aggregated formulations resolve injections by area name or subnetwork reference bus, never +# through the reduction's bus map, so a reduction handed to one is computed and then ignored. +# `_validate_network_source` rejects a non-default source rather than discarding it silently. +honors_network_reduction(::Type{<:AbstractNetworkModel}) = true +honors_network_reduction(::Type{CopperPlateNetworkModel}) = false +honors_network_reduction(::Type{AreaBalanceNetworkModel}) = false + # AC network models allocate a ReactivePowerBalance expression; active-power-only models do not # (see common_models/make_system_expressions.jl). Used to drop reactive-only device models. network_has_reactive_power(::Type{<:AbstractNetworkModel}) = true diff --git a/src/initial_conditions/initialization.jl b/src/initial_conditions/initialization.jl index 3b717740..941c836d 100644 --- a/src/initial_conditions/initialization.jl +++ b/src/initial_conditions/initialization.jl @@ -5,26 +5,25 @@ function get_initial_conditions_template( # This is done to avoid passing the duals but also not re-allocating the PTDF when it # exists + main_network_model = get_network_model(model.template) network_model = NetworkModel( get_network_formulation(model.template); - use_slacks = get_use_slacks(get_network_model(model.template)), - network_matrix = get_network_matrix(get_network_model(model.template)), - # Carry the contingency matrix forward so security-constrained branch models - # (which read the registered contingencies off the network model's contingency matrix) - # can build inside the initial-conditions sub-model. Without this the IC - # build hits `get_registered_contingencies(nothing)`. - contingency_matrix = get_contingency_matrix(get_network_model(model.template)), - reduce_radial_branches = get_reduce_radial_branches( - get_network_model(model.template), - ), + use_slacks = get_use_slacks(main_network_model), + network_source = get_network_source(main_network_model), + reduction_exceptions = get_reduction_exceptions(main_network_model), ) set_hvdc_network_model!( network_model, deepcopy(get_hvdc_network_model(model.template)), ) - network_model.network_reduction = - deepcopy(get_network_reduction(get_network_model(model.template))) - network_model.subnetworks = get_subnetworks(get_network_model(model.template)) + # The IC sub-model never runs `instantiate_network_model!`: it reads the main + # model's derived matrices and reduction rather than deriving its own, so the + # contingency matrix's registered contingencies are available to the + # security-constrained branch models. Sharing the source and the derived data by + # reference is what makes an IC/main network divergence structurally impossible — + # the IC build reads the reduction, it does not extend it. + IOM.set_network_data!(network_model, get_network_data(main_network_model)) + network_model.subnetworks = get_subnetworks(main_network_model) # Initialization builds a fresh, empty EvaluationContainer: no power-flow (or other) # evaluations are run during initialization. network_model.evaluations = IOM.EvaluationContainer() diff --git a/src/network_models/instantiate_network_model.jl b/src/network_models/instantiate_network_model.jl index cec315a4..99d32bfc 100644 --- a/src/network_models/instantiate_network_model.jl +++ b/src/network_models/instantiate_network_model.jl @@ -11,14 +11,6 @@ discovering subnetworks, applying network reductions, etc. # Helper functions (moved from IOM) ################################################################################# -function _make_subnetworks_from_subnetwork_axes(ptdf::PNM.PTDF) - subnetworks = Dict{Int, Set{Int}}() - for (ref_bus, ptdf_axes) in ptdf.subnetwork_axes - subnetworks[ref_bus] = Set(ptdf_axes[1]) - end - return subnetworks -end - function _make_subnetworks_from_subnetwork_axes(ptdf::PNM.VirtualPTDF) subnetworks = Dict{Int, Set{Int}}() for (ref_bus, ptdf_axes) in ptdf.subnetwork_axes @@ -74,168 +66,6 @@ _assign_subnetworks_to_buses( ::PSY.System, ) where {T <: AbstractNetworkModel} = nothing -function _push_component_buses!(buses::Set{Int64}, branch::PSY.Branch) - arc = PSY.get_arc(branch) - push!(buses, PSY.get_number(PSY.get_from(arc))) - push!(buses, PSY.get_number(PSY.get_to(arc))) - return -end - -function _push_component_buses!(buses::Set{Int64}, branch::PSY.ThreeWindingTransformer) - for arc in ( - PSY.get_primary_star_arc(branch), - PSY.get_secondary_star_arc(branch), - PSY.get_tertiary_star_arc(branch), - ) - push!(buses, PSY.get_number(PSY.get_from(arc))) - push!(buses, PSY.get_number(PSY.get_to(arc))) - end - return -end - -function _push_component_buses!(buses::Set{Int64}, device::PSY.StaticInjection) - push!(buses, PSY.get_number(PSY.get_bus(device))) - return -end - -function _push_component_buses!(buses::Set{Int64}, bus::PSY.ACBus) - PSY.get_available(bus) && push!(buses, PSY.get_number(bus)) - return -end - -# AreaInterchange <: PSY.Branch but connects two Areas, not two buses; it has no -# arc, so the PSY.Branch method above would error on PSY.get_arc. Not reachable -# today (modeled_branch_types entries are filtered to <: PSY.ACTransmission -# before reaching _push_component_buses!, and AreaInterchange isn't one), but -# guard it explicitly so a future caller can't hit that MethodError. -function _push_component_buses!(::Set{Int64}, ::PSY.AreaInterchange) - return -end - -# Fallback for monitored/outaged component types with no bus-pinning rule. Reached -# from `_add_outage_monitored_irreducible_buses!`, which iterates the raw -# `PSY.get_monitored_components(outage)` UUIDs (unfiltered — unlike the -# template-validation path), so any non-{Branch, ThreeWindingTransformer, -# StaticInjection, ACBus} monitored type lands here. Warn and skip rather than -# MethodError so this PTDF-side protected set stays reconcilable with PNM's -# `VirtualMODF` collector (`_accumulate_protected_buses!(::PSY.Component)`, which also -# warn-skips). The consequence is real, hence the explicit message. -function _push_component_buses!(::Set{Int64}, c::PSY.Component) - @warn "Outage-monitored component $(typeof(c)) ($(PSY.get_name(c))) has no \ - reduction-protection rule; its bus is not pinned and may be reduced away, \ - so the contingency it participates in will not be enforced. This mirrors \ - PNM's VirtualMODF _accumulate_protected_buses! warn-skip; if this type \ - should be protected, add a _push_component_buses! method for it." maxlog = 5 - return -end - -# Outages registered on an outage-aware branch DeviceModel pin both their -# monitored and their outaged (associated) component buses so the network -# reduction can't collapse them: the MODF column for a contingency is keyed by -# the outaged arc's endpoints, and post-contingency flow constraints reference -# the monitored components' real bus numbers. -function _add_outage_monitored_irreducible_buses!( - irreducible_buses::Set{Int64}, - sys::PSY.System, - branch_models::BranchModelContainer, -) - outage_uuids = Set{Base.UUID}() - for m in values(branch_models) - IOM.supports_outages(get_formulation(m)) || continue - union!(outage_uuids, keys(get_outages(m))) - end - - for outage_uuid in outage_uuids - outage = PSY.get_supplemental_attribute(sys, outage_uuid) - for uuid in PSY.get_monitored_components(outage) - component = IS.get_component(sys, uuid) - if isnothing(component) - throw( - IS.ConflictingInputsError( - "Monitored component with UUID $(uuid) on outage $(IS.get_uuid(outage)) not found in system. Data requires correction", - ), - ) - end - _push_component_buses!(irreducible_buses, component) - end - for component in PSY.get_associated_components(sys, outage) - _push_component_buses!(irreducible_buses, component) - end - end - return -end - -# Buses that must survive PNM network reductions because something monitored is -# pinned to them: branch endpoints carrying a `BranchRatingTimeSeriesParameter` -# (dynamic line ratings), and the monitored/outaged endpoints of outages -# registered on outage-aware (security-constrained) branch DeviceModels. -function _get_irreducible_buses_due_to_monitored_components( - sys::PSY.System, - network_model::NetworkModel, - branch_models::BranchModelContainer, -) - @debug "Identifying buses that are irreducible due to monitored components" - irreducible_buses = Set{Int64}() - for branch_type in network_model.modeled_branch_types - branch_type <: PSY.ACTransmission || continue - device_model = branch_models[nameof(branch_type)] - if !haskey( - get_time_series_names(device_model), - BranchRatingTimeSeriesParameter, - ) - continue - end - - if branch_type == PSY.ThreeWindingTransformer - @warn "Dynamic branch ratings for ThreeWindingTransformers are not implemented yet. Skipping it." - continue - end - - ts_name = - get_time_series_names(device_model)[BranchRatingTimeSeriesParameter] - ts_type = PSY.Deterministic #TODO workaround since we dont have the container - - branches = PSY.get_available_components(branch_type, sys) - for branch in branches - if !PSY.has_time_series(branch, ts_type, ts_name) - continue - end - _push_component_buses!(irreducible_buses, branch) - end - end - _add_outage_monitored_irreducible_buses!(irreducible_buses, sys, branch_models) - # `model_all_branches` MonitoredLine models pin their lines so zero-impedance - # ones survive the reduction instead of being merged away. - _add_model_all_branches_irreducible_buses!(irreducible_buses, branch_models) - return collect(irreducible_buses) -end - -# Pin both endpoint buses of every branch a `model_all_branches` MonitoredLine model -# covers. Dispatch on the model type so it is a no-op for other branch types. -function _add_model_all_branches_irreducible_buses!( - irreducible_buses::Set{Int64}, - branch_models::BranchModelContainer, -) - for m in values(branch_models) - _pin_model_all_branches!(irreducible_buses, m) - end - return -end - -_pin_model_all_branches!(::Set{Int64}, ::DeviceModel) = nothing - -function _pin_model_all_branches!( - irreducible_buses::Set{Int64}, - m::DeviceModel{PSY.MonitoredLine}, -) - get_attribute(m, MODEL_ALL_BRANCHES_KEY) === true || return - # The device cache is the modeled set (available + filter_function). - for branch in get_device_cache(m) - _push_component_buses!(irreducible_buses, branch) - end - return -end - # Drop (and warn about) any branch type whose components were all merged away by the # reduction — e.g. a lone zero-impedance monitored line. Such a type has no surviving # arc in `name_to_arc_maps`, so building its flow vars/constraints would fail. Absence @@ -247,11 +77,13 @@ function _prune_fully_reduced_branch_models!( branch_models::BranchModelContainer, ) merged_buses = Set{Int64}() - for removed in values(PNM.get_bus_reduction_map(network_model.network_reduction)) + for removed in values( + PNM.get_bus_reduction_map(get_network_reduction(network_model)), + ) union!(merged_buses, removed) end isempty(merged_buses) && return - name_to_arc_maps = PNM.get_name_to_arc_maps(network_model.network_reduction) + name_to_arc_maps = PNM.get_name_to_arc_maps(get_network_reduction(network_model)) pruned = DataType[] for branch_type in network_model.modeled_branch_types branch_type <: PSY.ACTransmission || continue @@ -289,7 +121,7 @@ function _warn_partially_reduced_monitored_lines!( network_model::NetworkModel, branch_models::BranchModelContainer, ) - removed_arcs = PNM.get_removed_arcs(network_model.network_reduction) + removed_arcs = PNM.get_removed_arcs(get_network_reduction(network_model)) isempty(removed_arcs) && return for m in values(branch_models) _warn_reduced_monitored_lines!(removed_arcs, m) @@ -336,110 +168,183 @@ function _get_unmodeled_branch_types( return unmodeled end +_is_default_source(::IOM.DefaultNetworkSource) = true +_is_default_source(::IOM.AbstractNetworkSource) = false + +# A formulation that never consults the reduction would compute the requested one and then +# ignore it, so accepting a source silently discards the caller's input. Erroring restores +# the guarantee the dedicated CopperPlate/AreaBalance methods used to give by construction. +function _validate_network_source( + ::Type{T}, + source::IOM.AbstractNetworkSource, +) where {T <: AbstractNetworkModel} + honors_network_reduction(T) && return + _is_default_source(source) && return + throw( + IS.ConflictingInputsError( + "$(T) aggregates the power balance and resolves injections by area or reference \ + bus, so it never consults a network reduction. The supplied \ + $(nameof(typeof(source))) would be computed and then ignored. Drop \ + `network_source` from the NetworkModel, or pick a formulation that models \ + individual buses.", + ), + ) +end + function _validate_network_and_branches( - model::NetworkModel, + model::NetworkModel{T}, branch_models::BranchModelContainer, sys::PSY.System, -) +) where {T <: AbstractNetworkModel} unmodeled = _get_unmodeled_branch_types(branch_models, sys) IOM._check_branch_network_compatibility(model, unmodeled) + _validate_network_source(T, get_network_source(model)) return end ################################################################################# -# Generic fallback for AbstractNetworkModel (Ybus-based models: ACP, ACR, etc.) +# Shared derivation steps ################################################################################# -function IOM.instantiate_network_model!( - model::NetworkModel{T}, - branch_models::BranchModelContainer, - number_of_steps::Int, +# The single reduction decision for the whole build. Subnetworks always fall out of +# the Ybus, so they are assigned unconditionally — nothing can have set them. +function _reduced_ybus!( + model::NetworkModel, sys::PSY.System, -) where {T <: AbstractNetworkModel} - _validate_network_and_branches(model, branch_models, sys) - irreducible_buses = _get_irreducible_buses_due_to_monitored_components( - sys, - model, - branch_models, - ) - _instantiate_ybus_network_reduction!(model, sys, irreducible_buses) - _finalize_network_reduction!(model, branch_models, number_of_steps) + exceptions::Vector{Int}, +) + ybus = _source_ybus(get_network_source(model), sys, exceptions) + model.subnetworks = _make_subnetworks_from_subnetwork_axes(ybus) + return ybus +end + +#= +The one source-aware Ybus resolution, dispatched on the source so every formulation +family gets it. A source that derives the network from the system applies the build's +reduction exceptions; a prebuilt source already fixed its reduction before the template +was known, so its Ybus is reproduced from that reduction and the exceptions are validated +against it instead. Families that cannot consume a prebuilt sensitivity matrix still +honour the reduction it carries. +=# +function _source_ybus( + source::IOM.AbstractNetworkSource, + sys::PSY.System, + exceptions::Vector{Int}, +) + _warn_ignored_radial_exceptions(source, exceptions) + return _build_ybus(source, sys, exceptions) +end + +function _source_ybus( + source::PrebuiltMatrixSource, + sys::PSY.System, + exceptions::Vector{Int}, +) + reduction = PNM.get_network_reduction_data(PNM.get_core(get_matrix(source))) + _validate_prebuilt_exceptions(reduction, exceptions) + return _prebuilt_ybus(source, reduction, sys) +end + +function _source_ybus( + source::PrebuiltCoreSource, + sys::PSY.System, + exceptions::Vector{Int}, +) + reduction = PNM.get_network_reduction_data(get_core(source)) + _validate_prebuilt_exceptions(reduction, exceptions) + return _prebuilt_ybus(source, reduction, sys) +end + +function _warn_ignored_radial_exceptions( + source::IOM.AbstractNetworkSource, + exceptions::Vector{Int}, +) + isempty(exceptions) && return + any(_is_radial_reduction, _source_reductions(source)) || return + @warn "Irreducible buses identified. The reduction of any radial branch between 2 irreducible buses will be ignored" return end -# DCP additionally supports MODF security-constrained branches: build/reconcile -# the contingency matrix between the Ybus reduction and the finalize step, since -# reconciliation can replace `network_reduction` before the branch maps are -# populated. -function IOM.instantiate_network_model!( - model::NetworkModel{DCPNetworkModel}, - branch_models::BranchModelContainer, - number_of_steps::Int, +# A prebuilt source fixes the reduction before the template is known, so the buses the +# template pins cannot be honored. Erroring here rather than warning: a pinned bus that +# was already eliminated makes every contingency and time-varying rating on it silently +# unenforceable. +function _validate_prebuilt_exceptions( + reduction::PNM.NetworkReductionData, + exceptions::Vector{Int}, +) + isempty(exceptions) && return + dropped = setdiff(exceptions, keys(PNM.get_bus_reduction_map(reduction))) + isempty(dropped) && return + throw( + IS.ConflictingInputsError( + "The prebuilt network source eliminated buses $(sort!(collect(dropped))), which \ + the template pins as reduction exceptions (outage-monitored components or \ + time-varying branch ratings). Rebuild the matrix with these buses passed as \ + `irreducible_buses`, or pass a `NetworkReductionSpec` so the build derives the \ + reduction itself.", + ), + ) +end + +# A prebuilt source fixed the reduction before the build, so its Ybus is reproduced from +# the source's own reduction rather than from the model. The result is checked against +# that reduction: anything the spec cannot reproduce must fail loudly rather than hand +# downstream code a Ybus describing a different network than the matrices do. +function _prebuilt_ybus( + source::IOM.AbstractNetworkSource, + reduction::PNM.NetworkReductionData, sys::PSY.System, ) - _validate_network_and_branches(model, branch_models, sys) - irreducible_buses = _get_irreducible_buses_due_to_monitored_components( + ybus = _build_ybus( + NetworkReductionSpec(_source_reductions(source)), sys, - model, - branch_models, + _source_irreducible_buses(reduction), ) - _instantiate_ybus_network_reduction!(model, sys, irreducible_buses) - _maybe_build_dcp_modf_matrix!(model, branch_models, sys, irreducible_buses) - _finalize_network_reduction!(model, branch_models, number_of_steps) - return + reproduced = PNM.get_network_reduction_data(ybus) + if PNM.get_bus_reduction_map(reproduced) != PNM.get_bus_reduction_map(reduction) + throw( + IS.ConflictingInputsError( + "The Ybus rebuilt from the prebuilt network source's reduction retained a \ + different bus set than the source itself ($(length(PNM.get_bus_reduction_map(reproduced))) \ + vs $(length(PNM.get_bus_reduction_map(reduction))) buses). The source's \ + reduction is not reproducible from its recorded reduction spec; build the \ + matrices from a `NetworkReductionSpec` instead.", + ), + ) + end + return ybus end -# Ybus-derived reduction shared by every Ybus-based network model (ACP, ACR, -# DCP, NFA, ...): build the (possibly reduced) Ybus, derive the subnetworks, and -# install the reduction data on the model. -function _instantiate_ybus_network_reduction!( - model::NetworkModel, +# Derive the MODF only when the template uses an outage-aware branch formulation. +# Registration is template-scoped to match the reduction exceptions: a contingency +# whose buses were not pinned must not be registered, or it would resolve to no arc +# modifications and silently return the unmodified base row. +function _derive_contingency_matrix( + core::PNM.VirtualFactorCore, sys::PSY.System, - irreducible_buses::Vector{Int64}, + branch_models::BranchModelContainer, ) - if model.reduce_radial_branches && model.reduce_degree_two_branches - @info "Applying both radial and degree two reductions" - ybus = PNM.Ybus( - sys; - network_reductions = PNM.NetworkReduction[ - PNM.RadialReduction(), - PNM.DegreeTwoReduction(), - ], - irreducible_buses = irreducible_buses, - ) - elseif model.reduce_radial_branches - @info "Applying radial reduction" - if !isempty(irreducible_buses) - @warn "Irreducible buses identified due to DLRs. The reduction of any radial branch between 2 irreducible buses wil be ignored" - end - ybus = - PNM.Ybus( - sys; - network_reductions = PNM.NetworkReduction[PNM.RadialReduction()], - irreducible_buses = irreducible_buses, + modf = PNM.VirtualMODF( + core, + sys; + automatically_register_outages = false, + ) + registered = PNM.get_registered_contingencies(modf) + for m in values(branch_models) + IOM.supports_outages(get_formulation(m)) || continue + for outage_uuid in keys(get_outages(m)) + # The same outage can be attached to several branch DeviceModels. + haskey(registered, outage_uuid) && continue + PNM._register_outage!( + modf, + sys, + PSY.get_supplemental_attribute(sys, outage_uuid), ) - elseif model.reduce_degree_two_branches - @info "Applying degree two reduction" - ybus = PNM.Ybus( - sys; - network_reductions = PNM.NetworkReduction[PNM.DegreeTwoReduction()], - irreducible_buses = irreducible_buses, - ) - else - ybus = PNM.Ybus(sys) - end - # Reuse the Ybus built above (it carries the reduction-aware subnetwork - # grouping in `subnetwork_axes`) instead of a throwaway PNM.find_subnetworks. - if isempty(model.subnetworks) - model.subnetworks = _make_subnetworks_from_subnetwork_axes(ybus) + end end - model.network_reduction = deepcopy(PNM.get_network_reduction_data(ybus)) - #if !isempty(model.network_reductionget_net_reduction_data) - # TODO: Network reimplement this when it becomes necessary. We don't have any - # reductions that are incompatible right now. - # check_network_reduction_compatibility(T) - #end - return + _consolidate_device_model_outages_with_modf!(branch_models, modf) + return modf end function _finalize_network_reduction!( @@ -448,7 +353,7 @@ function _finalize_network_reduction!( number_of_steps::Int, ) PNM.populate_branch_maps_by_type!( - model.network_reduction, + get_network_reduction(model), IOM._get_filters(branch_models), ) # After the reduction is known and the branch maps populated, before the @@ -461,50 +366,58 @@ function _finalize_network_reduction!( end ################################################################################# -# AreaBalanceNetworkModel +# Ybus-only families (ACP, ACR, IVR, LPACC, NFA, CopperPlate, AreaBalance, ...) ################################################################################# function IOM.instantiate_network_model!( - model::NetworkModel{AreaBalanceNetworkModel}, + model::NetworkModel{T}, branch_models::BranchModelContainer, number_of_steps::Int, sys::PSY.System, -) +) where {T <: AbstractNetworkModel} _validate_network_and_branches(model, branch_models, sys) - # `network_reduction` must be populated before `populate_branch_maps_by_type!` and - # `build_problem!` consume it (AreaBalance applies no bus reduction -> identity map). - if model.network_reduction === nothing - model.network_reduction = deepcopy(PNM.get_network_reduction_data(PNM.Ybus(sys))) - end - PNM.populate_branch_maps_by_type!(model.network_reduction) - _reset_reduced_branch_tracker!(model, number_of_steps) + exceptions = _collect_reduction_exceptions(sys, model, branch_models) + ybus = _reduced_ybus!(model, sys, exceptions) + IOM.set_network_data!( + model, + YbusNetworkData(ybus, deepcopy(PNM.get_network_reduction_data(ybus))), + ) + _finalize_network_reduction!(model, branch_models, number_of_steps) return end ################################################################################# -# CopperPlateNetworkModel +# DCPNetworkModel — Ybus plus an optional MODF over the Ybus's own factorization ################################################################################# function IOM.instantiate_network_model!( - model::NetworkModel{CopperPlateNetworkModel}, + model::NetworkModel{DCPNetworkModel}, branch_models::BranchModelContainer, number_of_steps::Int, sys::PSY.System, ) _validate_network_and_branches(model, branch_models, sys) - if isempty(model.subnetworks) - model.subnetworks = PNM.find_subnetworks(sys) - end - # `network_reduction` must always be populated: `build_problem!` reads - # `network_reduction.bus_reduction_map` and device `add_to_expression!` maps every - # bus through `get_mapped_bus_number(network_reduction, ...)`. CopperPlate applies no - # reduction, so this is the identity map (each retained bus -> itself). - model.network_reduction = deepcopy(PNM.get_network_reduction_data(PNM.Ybus(sys))) - if length(model.subnetworks) > 1 - @debug "System Contains Multiple Subnetworks. Assigning buses to subnetworks." - _assign_subnetworks_to_buses(model, sys) + exceptions = _collect_reduction_exceptions(sys, model, branch_models) + ybus = _reduced_ybus!(model, sys, exceptions) + reduction = deepcopy(PNM.get_network_reduction_data(ybus)) + if IOM._template_has_outage_aware_branch(branch_models) + core = PNM.VirtualFactorCore( + ybus; + tol = PTDF_ZERO_TOL, + system_uuid = IS.get_uuid(sys), + ) + IOM.set_network_data!( + model, + DCPNetworkData( + ybus, + _derive_contingency_matrix(core, sys, branch_models), + reduction, + ), + ) + else + IOM.set_network_data!(model, DCPNetworkData(ybus, reduction)) end - _reset_reduced_branch_tracker!(model, number_of_steps) + _finalize_network_reduction!(model, branch_models, number_of_steps) return end @@ -513,321 +426,94 @@ end ################################################################################# function IOM.instantiate_network_model!( - model::NetworkModel{<:AbstractPTDFNetworkModel}, + model::NetworkModel{T}, branch_models::BranchModelContainer, number_of_steps::Int, sys::PSY.System, -) - irreducible_buses = _get_irreducible_buses_due_to_monitored_components( - sys, - model, - branch_models, - ) +) where {T <: AbstractPTDFNetworkModel} _validate_network_and_branches(model, branch_models, sys) - if IOM.get_network_matrix(model) === nothing || !isempty(irreducible_buses) - if IOM.get_network_matrix(model) !== nothing - @warn "Provided PTDF Matrix is being ignored since irreducible buses were identified because of DLRs. Recalculating PTDF Matrix with PowerNetworkMatrices.PTDF and the identified irreducible buses." - else - @info "No PTDF Matrix provided. Calculating using PowerNetworkMatrices.PTDF" - end - - if model.reduce_radial_branches && model.reduce_degree_two_branches - @info "Applying both radial and degree two reductions" - ptdf = PNM.VirtualPTDF( - sys; - tol = PTDF_ZERO_TOL, - network_reductions = PNM.NetworkReduction[ - PNM.RadialReduction(), - PNM.DegreeTwoReduction(), - ], - irreducible_buses = irreducible_buses, - ) - elseif model.reduce_radial_branches - @info "Applying radial reduction" - if !isempty(irreducible_buses) - @warn "Irreducible buses identified due to DLRs. The reduction of any radial branch between 2 irreducible buses wil be ignored" - end - ptdf = PNM.VirtualPTDF( - sys; - tol = PTDF_ZERO_TOL, - network_reductions = PNM.NetworkReduction[PNM.RadialReduction()], - irreducible_buses = irreducible_buses, - ) - elseif model.reduce_degree_two_branches - @info "Applying degree two reduction" - ptdf = PNM.VirtualPTDF( - sys; - tol = PTDF_ZERO_TOL, - network_reductions = PNM.NetworkReduction[PNM.DegreeTwoReduction()], - irreducible_buses = irreducible_buses, - ) - else - # No radial/degree-two reduction requested, but irreducible buses may - # still be pinned (e.g. `model_all_branches` MonitoredLines, outages, - # DLRs). Forward them so the base zero-impedance branch coalescing - # cannot merge a pinned bus away. - ptdf = PNM.VirtualPTDF( - sys; - tol = PTDF_ZERO_TOL, - irreducible_buses = irreducible_buses, - ) - end - model.network_matrix = ptdf - model.network_reduction = deepcopy(ptdf.network_reduction_data) - else - model.network_reduction = deepcopy(model.network_matrix.network_reduction_data) - end - - if !model.reduce_radial_branches && PNM.has_radial_reduction( - PNM.get_reductions(model.network_matrix.network_reduction_data), - ) - throw( - IS.ConflictingInputsError( - "The provided network matrix has reduced radial branches and mismatches the network \ - model specification reduce_radial_branches = false. Set the keyword argument \ - reduce_radial_branches = true in your network model"), - ) - end - if !model.reduce_degree_two_branches && PNM.has_degree_two_reduction( - PNM.get_reductions(model.network_matrix.network_reduction_data), - ) - throw( - IS.ConflictingInputsError( - "The provided network matrix has reduced degree two branches and mismatches the network \ - model specification reduce_degree_two_branches = false. Set the keyword argument \ - reduce_degree_two_branches = true in your network model"), - ) - end - if model.reduce_radial_branches && - PNM.has_ward_reduction( - PNM.get_reductions(model.network_matrix.network_reduction_data), + exceptions = _collect_reduction_exceptions(sys, model, branch_models) + IOM.set_network_data!( + model, + _ptdf_network_data( + get_network_source(model), + model, + sys, + branch_models, + exceptions, + ), ) - throw( - IS.ConflictingInputsError( - "The provided network matrix has a ward reduction specified and the keyword argument \ - reduce_radial_branches = true. Set the keyword argument reduce_radial_branches = false \ - or provide a modified network matrix without the Ward reduction."), - ) - end - - if model.reduce_radial_branches - @assert !isempty(model.network_matrix.network_reduction_data) - end - model.subnetworks = _make_subnetworks_from_subnetwork_axes(model.network_matrix) if length(model.subnetworks) > 1 @debug "System Contains Multiple Subnetworks. Assigning buses to subnetworks." _assign_subnetworks_to_buses(model, sys) end - _maybe_build_modf_matrix!(model, branch_models, sys, irreducible_buses) - PNM.populate_branch_maps_by_type!( - model.network_reduction, - IOM._get_filters(branch_models), - ) - # After the reduction is known and the branch maps populated, before the - # device constructors run: drop branch types fully merged away (else their - # flow vars/constraints would fail to build) and warn about partial drops. - _prune_fully_reduced_branch_models!(model, branch_models) - _warn_partially_reduced_monitored_lines!(model, branch_models) - _reset_reduced_branch_tracker!(model, number_of_steps) + _finalize_network_reduction!(model, branch_models, number_of_steps) return end -""" -Buses retained by `nrd` (reduction representatives — i.e. keys of the bus -reduction map). This is the matrix's bus dimension. PNM's invariant is that -irreducible buses are never eliminated, so they remain keys of the bus -reduction map; the `@assert` here makes that invariant load-bearing instead of -silently papered over with a `union`. -""" -function _retained_buses(nrd::PNM.NetworkReductionData) - retained = Set(keys(PNM.get_bus_reduction_map(nrd))) - @assert issubset(PNM.get_irreducible_buses(nrd), retained) "irreducible buses are not a subset of bus_reduction_map keys; PNM reduction invariant violated" - return retained -end - -# Network reductions requested by the model flags. The cohesive bus set is passed -# separately via the `irreducible_buses` kwarg of `VirtualPTDF`/`VirtualMODF` -# (PS6 PNM convention), so the reduction constructors take no arguments here. -function _model_network_reductions(model::NetworkModel) - reductions = PNM.NetworkReduction[] - if model.reduce_radial_branches - push!(reductions, PNM.RadialReduction()) - end - if model.reduce_degree_two_branches - push!(reductions, PNM.DegreeTwoReduction()) - end - return reductions -end - -""" -Rebuild PTDF and MODF onto the union of their retained buses when they diverge. -Returns `true` if a rebuild happened; throws if one pass fails to converge them, -since mismatched reductions break the nodal-balance vs. MODF-column dimensions. -""" -function _reconcile_ptdf_modf_reduction!( - model::NetworkModel{<:AbstractPTDFNetworkModel}, +# Sources that derive the network from the system: PTDF and MODF wrap the same core, so +# their reductions cannot diverge. +function _ptdf_network_data( + ::IOM.AbstractNetworkSource, + model::NetworkModel, sys::PSY.System, + branch_models::BranchModelContainer, + exceptions::Vector{Int}, ) - ptdf_nrd = PNM.get_network_reduction_data(model.network_matrix) - modf_nrd = PNM.get_network_reduction_data(IOM.get_contingency_matrix(model)) - retained_ptdf = _retained_buses(ptdf_nrd) - retained_modf = _retained_buses(modf_nrd) - retained_ptdf == retained_modf && return false - - @warn "PTDF and MODF reduced to different bus sets \ - (|PTDF retained|=$(length(retained_ptdf)), \ - |MODF retained|=$(length(retained_modf))). Reconciling both onto \ - the cohesive union of retained buses so the nodal-balance and \ - post-contingency dimensions agree." - cohesive = collect(union(retained_ptdf, retained_modf)) - reductions = _model_network_reductions(model) - model.network_matrix = PNM.VirtualPTDF( - sys; - tol = PTDF_ZERO_TOL, - network_reductions = reductions, - irreducible_buses = cohesive, - ) - model.contingency_matrix = PNM.VirtualMODF( - sys; + ybus = _reduced_ybus!(model, sys, exceptions) + core = PNM.VirtualFactorCore( + ybus; tol = PTDF_ZERO_TOL, - network_reductions = reductions, - irreducible_buses = cohesive, + system_uuid = IS.get_uuid(sys), ) - - if _retained_buses(PNM.get_network_reduction_data(model.network_matrix)) != - _retained_buses(PNM.get_network_reduction_data(IOM.get_contingency_matrix(model))) - throw( - IS.ConflictingInputsError( - "PTDF and MODF reductions remain dimensionally inconsistent \ - after one reconciliation pass; aborting build.", - ), - ) - end - return true + return _assemble_ptdf_data(core, PNM.VirtualPTDF(core), sys, branch_models) end -# Populate the contingency matrix when the template uses an outage-aware -# (security-constrained) branch formulation and one was not provided explicitly. -# The MODF reproduces the PTDF's network reduction (same reductions + irreducible -# buses) so the nodal-balance rows and the post-contingency MODF columns align. -# Then drop outages on SC DeviceModels that PNM couldn't register on the MODF so -# the post-contingency builder doesn't KeyError on them. -function _maybe_build_modf_matrix!( - model::NetworkModel{<:AbstractPTDFNetworkModel}, - branch_models::BranchModelContainer, +function _ptdf_network_data( + source::PrebuiltCoreSource, + model::NetworkModel, sys::PSY.System, - irreducible_buses::Vector{Int64}, + branch_models::BranchModelContainer, + exceptions::Vector{Int}, ) - IOM._template_has_outage_aware_branch(branch_models) || return - if IOM.get_contingency_matrix(model) === nothing - @info "MODF Matrix not provided. Calculating using PowerNetworkMatrices.VirtualMODF" - reductions = PNM.NetworkReduction[] - if model.reduce_radial_branches - push!(reductions, PNM.RadialReduction()) - end - if model.reduce_degree_two_branches - push!(reductions, PNM.DegreeTwoReduction()) - end - model.contingency_matrix = PNM.VirtualMODF( - sys; - tol = PTDF_ZERO_TOL, - network_reductions = reductions, - irreducible_buses = irreducible_buses, - ) - end - # Reconcile PTDF/MODF reductions before outage consolidation populates the - # branch maps. If a rebuild happened, re-derive the model's network reduction - # and subnetworks from the rebuilt PTDF so all downstream axes agree. - if _reconcile_ptdf_modf_reduction!(model, sys) - model.network_reduction = - deepcopy(PNM.get_network_reduction_data(model.network_matrix)) - model.subnetworks = _make_subnetworks_from_subnetwork_axes(model.network_matrix) - if length(model.subnetworks) > 1 - _assign_subnetworks_to_buses(model, sys) - end - end - _consolidate_device_model_outages_with_modf!( - branch_models, - IOM.get_contingency_matrix(model), - ) - return + core = get_core(source) + _reduced_ybus!(model, sys, exceptions) + return _assemble_ptdf_data(core, PNM.VirtualPTDF(core), sys, branch_models) end -# DCP counterpart of `_maybe_build_modf_matrix!`: populate the contingency -# matrix when the template uses an outage-aware (security-constrained) branch -# formulation, reconcile its reduction against the Ybus-derived one, and drop -# outages PNM could not register. Must run before -# `_finalize_network_reduction!` because reconciliation can replace -# `model.network_reduction`. -function _maybe_build_dcp_modf_matrix!( - model::NetworkModel{DCPNetworkModel}, - branch_models::BranchModelContainer, +# A prebuilt VirtualPTDF supplies both the core and its populated row cache, so it is +# reused as the PTDF wrapper rather than a fresh VirtualPTDF(core). +function _ptdf_network_data( + source::PrebuiltMatrixSource, + model::NetworkModel, sys::PSY.System, - irreducible_buses::Vector{Int64}, + branch_models::BranchModelContainer, + exceptions::Vector{Int}, ) - IOM._template_has_outage_aware_branch(branch_models) || return - if IOM.get_contingency_matrix(model) === nothing - @info "MODF Matrix not provided. Calculating using PowerNetworkMatrices.VirtualMODF" - model.contingency_matrix = PNM.VirtualMODF( - sys; - tol = PTDF_ZERO_TOL, - network_reductions = _model_network_reductions(model), - irreducible_buses = irreducible_buses, - ) - end - _reconcile_ybus_modf_reduction!(model, sys) - _consolidate_device_model_outages_with_modf!( - branch_models, - IOM.get_contingency_matrix(model), - ) - return -end - -""" -Rebuild the Ybus reduction and the MODF onto the union of their retained buses -when they diverge (the DCP analogue of `_reconcile_ptdf_modf_reduction!`). -Returns `true` if a rebuild happened; throws if one pass fails to converge them, -since mismatched reductions break the nodal-balance vs. MODF-column dimensions. -""" -function _reconcile_ybus_modf_reduction!( - model::NetworkModel{DCPNetworkModel}, + matrix = get_matrix(source) + core = PNM.get_core(matrix) + # Called for its reproducibility validation against the source's reduction; the + # Ybus itself is not stored (nothing downstream reads it for this family). + _source_ybus(source, sys, exceptions) + # Subnetworks come from the matrix, not the Ybus, because the matrix's own axes are + # what the PTDF rows are indexed on. + model.subnetworks = _make_subnetworks_from_subnetwork_axes(matrix) + return _assemble_ptdf_data(core, matrix, sys, branch_models) +end + +function _assemble_ptdf_data( + core::PNM.VirtualFactorCore, + ptdf, sys::PSY.System, + branch_models::BranchModelContainer, ) - modf_nrd = PNM.get_network_reduction_data(IOM.get_contingency_matrix(model)) - retained_ybus = _retained_buses(model.network_reduction) - retained_modf = _retained_buses(modf_nrd) - retained_ybus == retained_modf && return false - - @warn "Ybus and MODF reduced to different bus sets \ - (|Ybus retained|=$(length(retained_ybus)), \ - |MODF retained|=$(length(retained_modf))). Reconciling both onto \ - the cohesive union of retained buses so the nodal-balance and \ - post-contingency dimensions agree." - cohesive = collect(union(retained_ybus, retained_modf)) - reductions = _model_network_reductions(model) - ybus = PNM.Ybus( - sys; - network_reductions = reductions, - irreducible_buses = cohesive, - ) - model.contingency_matrix = PNM.VirtualMODF( - sys; - tol = PTDF_ZERO_TOL, - network_reductions = reductions, - irreducible_buses = cohesive, - ) - model.network_reduction = deepcopy(PNM.get_network_reduction_data(ybus)) - model.subnetworks = _make_subnetworks_from_subnetwork_axes(ybus) - - if _retained_buses(model.network_reduction) != _retained_buses( - PNM.get_network_reduction_data(IOM.get_contingency_matrix(model)), - ) - throw( - IS.ConflictingInputsError( - "Ybus and MODF reductions remain dimensionally inconsistent \ - after one reconciliation pass; aborting build.", - ), + reduction = deepcopy(PNM.get_network_reduction_data(core)) + if IOM._template_has_outage_aware_branch(branch_models) + return PTDFNetworkData( + ptdf, + _derive_contingency_matrix(core, sys, branch_models), + reduction, ) end - return true + return PTDFNetworkData(ptdf, reduction) end diff --git a/src/network_models/network_data.jl b/src/network_models/network_data.jl new file mode 100644 index 00000000..37b5fcdf --- /dev/null +++ b/src/network_models/network_data.jl @@ -0,0 +1,91 @@ +#= +The network artifacts one build derives from its source. One container type per +formulation family, so "does this build have a contingency matrix" is answered by +dispatch instead of an isnothing check, and a family cannot accidentally read a +matrix it never derived. + +The reduction data is a deepcopy: POM extends it with `populate_branch_maps_by_type!` +and must never write into a core that the caller may have supplied or shared. That +function only ever accumulates — it never clears — so sharing it would leak one +template's branch maps into the next build off the same core. The copy may only ever be +EXTENDED: its bus and arc mapping must stay identical to the core's, because the MODF is +indexed with arcs resolved through this copy. + +Consequence: `PNM.get_network_reduction_data(matrix)` and `get_reduction(network_data)` +are the same reduction but not the same object, and only the latter carries the branch +maps. Read the reduction off the container, never off the matrix. +=# + +"""Ybus-only families: no factorization, no sensitivity matrices.""" +struct YbusNetworkData <: IOM.AbstractNetworkData + ybus::PNM.Ybus + reduction::PNM.NetworkReductionData +end + +"""DCP: a Ybus, plus a MODF derived from that same Ybus when the template is security constrained.""" +struct DCPNetworkData{M} <: IOM.AbstractNetworkData + ybus::PNM.Ybus + contingency_matrix::M + reduction::PNM.NetworkReductionData +end + +"""PTDF families: a PTDF, plus an optional MODF, both wrapping one factorization core.""" +struct PTDFNetworkData{P, M} <: IOM.AbstractNetworkData + matrix::P + contingency_matrix::M + reduction::PNM.NetworkReductionData +end + +# The no-contingency constructors omit the argument, storing this singleton in +# `contingency_matrix`. It is the type parameter, not the field's presence, that +# `has_contingency_matrix` dispatches on. A named sentinel rather than `nothing` because +# `nothing` already means "not instantiated yet" on IOM's `network_data` field. +struct NoContingencyMatrix end + +DCPNetworkData(ybus::PNM.Ybus, reduction::PNM.NetworkReductionData) = + DCPNetworkData(ybus, NoContingencyMatrix(), reduction) + +PTDFNetworkData(matrix, reduction::PNM.NetworkReductionData) = + PTDFNetworkData(matrix, NoContingencyMatrix(), reduction) + +get_reduction(nd::YbusNetworkData) = nd.reduction +get_reduction(nd::DCPNetworkData) = nd.reduction +get_reduction(nd::PTDFNetworkData) = nd.reduction + +# Ybus-only and DCP families have no sensitivity matrix, so the Ybus is the meaningful +# answer for get_network_matrix. Kept so the public IOM getter doesn't throw for these +# families; no caller reaches it today — the only get_network_matrix read sites +# (AC_branches.jl, transformer_models.jl) are NetworkModel{<:AbstractPTDFNetworkModel}. +get_matrix(nd::YbusNetworkData) = nd.ybus +get_matrix(nd::DCPNetworkData) = nd.ybus +get_matrix(nd::PTDFNetworkData) = nd.matrix + +has_contingency_matrix(::IOM.AbstractNetworkData) = false +has_contingency_matrix(::DCPNetworkData{NoContingencyMatrix}) = false +has_contingency_matrix(::DCPNetworkData) = true +has_contingency_matrix(::PTDFNetworkData{P, NoContingencyMatrix}) where {P} = false +has_contingency_matrix(::PTDFNetworkData) = true + +# Named distinctly from IOM's `get_contingency_matrix(m::NetworkModel)` (exported and +# brought into this module's scope via `using InfrastructureOptimizationModels`): +# reusing that name here would silently define a POM-local method table that shadows +# the NetworkModel method for every unqualified call in this package. +get_network_data_contingency_matrix(::DCPNetworkData{NoContingencyMatrix}) = error( + "DCPNetworkData has no contingency matrix. Check `has_contingency_matrix` before \ + calling this getter.", +) +get_network_data_contingency_matrix(nd::DCPNetworkData) = nd.contingency_matrix +get_network_data_contingency_matrix( + ::PTDFNetworkData{P, NoContingencyMatrix}, +) where {P} = error( + "PTDFNetworkData has no contingency matrix. Check `has_contingency_matrix` before \ + calling this getter.", +) +get_network_data_contingency_matrix(nd::PTDFNetworkData) = nd.contingency_matrix + +# The consumer-facing getters keep their IOM names and forward into the derived +# container, so the PTDF and MODF read sites in the device models are unchanged. +IOM.get_network_matrix(m::NetworkModel) = get_matrix(get_network_data(m)) +IOM.get_contingency_matrix(m::NetworkModel) = + get_network_data_contingency_matrix(get_network_data(m)) +IOM.get_network_reduction(m::NetworkModel) = get_reduction(get_network_data(m)) diff --git a/src/network_models/network_reductions.jl b/src/network_models/network_reductions.jl index 4f023672..d8274c64 100644 --- a/src/network_models/network_reductions.jl +++ b/src/network_models/network_reductions.jl @@ -337,28 +337,6 @@ function get_branch_argument_constraint_axis( return collect(keys(constraint_submap)) end -# Verify a user-provided contingency matrix was built with the same network reduction -# as the active reduction (derived from the network 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 contingency matrix was built with a different network \ - reduction than the active reduction derived from the network \ - matrix. Rebuild the MODF with a consistent network reduction, \ - or omit it so it is recalculated automatically.", - ), - ) - end - return -end - """ Drop outages from each outage-aware-branch `DeviceModel` whose UUID isn't registered on `modf_matrix`; without this they'd `KeyError` downstream in diff --git a/src/network_models/network_sources.jl b/src/network_models/network_sources.jl new file mode 100644 index 00000000..b4d31b2c --- /dev/null +++ b/src/network_models/network_sources.jl @@ -0,0 +1,81 @@ +#= +The declaration of which network a NetworkModel is built on. `_build_ybus` is the +single place a Ybus is constructed during network model instantiation: one Ybus per +build means one reduction, so every matrix derived from it agrees by construction. +=# + +""" +Build the network from the system, applying `reductions` and honoring the build's +reduction exceptions. + +A zero-impedance branch reduction is always applied first. Include a +`ZeroImpedanceBranchReduction` to override its parameters; it replaces that step rather than +adding one, so its position in the vector is irrelevant. More than one errors. +""" +struct NetworkReductionSpec <: IOM.AbstractNetworkSource + reductions::Vector{PNM.NetworkReduction} +end + +NetworkReductionSpec(reductions::PNM.NetworkReduction...) = + NetworkReductionSpec(collect(PNM.NetworkReduction, reductions)) + +""" +Reuse a `VirtualPTDF` the caller already built, including its populated row cache. +Its own reduction becomes the build's reduction; the reduction exceptions derived +from the template are not applied, because the matrix already exists. + +Only a `VirtualPTDF` is accepted: it carries the factorization core, so a MODF +sharing its reduction can be derived from it. A dense `PNM.PTDF` carries no core and +is deliberately not a valid source. +""" +struct PrebuiltMatrixSource{M <: PNM.VirtualPTDF} <: IOM.AbstractNetworkSource + matrix::M +end + +""" +Reuse a factorization core the caller already built — the cheapest reuse, since the +PTDF and MODF wrappers are derived from it without re-factorizing the ABA matrix. +""" +struct PrebuiltCoreSource{C <: PNM.VirtualFactorCore} <: IOM.AbstractNetworkSource + core::C +end + +get_matrix(source::PrebuiltMatrixSource) = source.matrix +get_core(source::PrebuiltCoreSource) = source.core + +_source_reductions(source::NetworkReductionSpec) = source.reductions +_source_reductions(::IOM.DefaultNetworkSource) = PNM.NetworkReduction[] +_source_reductions(source::PrebuiltMatrixSource) = + PNM.get_applied_reductions(PNM.get_network_reduction_data(get_matrix(source))) +_source_reductions(source::PrebuiltCoreSource) = + PNM.get_applied_reductions(PNM.get_network_reduction_data(get_core(source))) + +"The user-supplied irreducible bus set an already-applied reduction was built with." +_source_irreducible_buses(reduction::PNM.NetworkReductionData) = + collect(Int, PNM.get_user_irreducible_buses(PNM.get_reductions(reduction))) + +_is_radial_reduction(::PNM.NetworkReduction) = false +_is_radial_reduction(::PNM.RadialReduction) = true + +function _build_ybus( + source::NetworkReductionSpec, + sys::PSY.System, + exceptions::Vector{Int}, +) + reductions, zero_impedance_reduction = + PNM.split_zero_impedance_reduction(source.reductions) + return PNM.Ybus( + sys; + network_reductions = reductions, + irreducible_buses = exceptions, + zero_impedance_reduction = zero_impedance_reduction, + ) +end + +function _build_ybus( + ::IOM.DefaultNetworkSource, + sys::PSY.System, + exceptions::Vector{Int}, +) + return PNM.Ybus(sys; irreducible_buses = exceptions) +end diff --git a/src/network_models/reduction_exceptions.jl b/src/network_models/reduction_exceptions.jl new file mode 100644 index 00000000..5674cae3 --- /dev/null +++ b/src/network_models/reduction_exceptions.jl @@ -0,0 +1,168 @@ +#= +Buses that must survive PNM network reductions because something the template models +is pinned to them. One rule per method, dispatched on the DeviceModel, so a new rule +is a new method rather than another branch in a growing loop. + +This set is the sole authority on reduction exceptions: the buses the caller pinned on the +`NetworkModel` plus the buses these rules derive. PNM's own `_collect_protected_buses` +protects every system Outage; this protects only what the template actually models, so a +contingency the model never enforces cannot block a reduction. +=# + +function _push_component_buses!(buses::Set{Int}, branch::PSY.Branch) + arc = PSY.get_arc(branch) + push!(buses, PSY.get_number(PSY.get_from(arc))) + push!(buses, PSY.get_number(PSY.get_to(arc))) + return +end + +function _push_component_buses!(buses::Set{Int}, branch::PSY.ThreeWindingTransformer) + for arc in ( + PSY.get_primary_star_arc(branch), + PSY.get_secondary_star_arc(branch), + PSY.get_tertiary_star_arc(branch), + ) + push!(buses, PSY.get_number(PSY.get_from(arc))) + push!(buses, PSY.get_number(PSY.get_to(arc))) + end + return +end + +function _push_component_buses!(buses::Set{Int}, device::PSY.StaticInjection) + push!(buses, PSY.get_number(PSY.get_bus(device))) + return +end + +function _push_component_buses!(buses::Set{Int}, bus::PSY.ACBus) + PSY.get_available(bus) && push!(buses, PSY.get_number(bus)) + return +end + +# AreaInterchange <: PSY.Branch but connects Areas, so it has no arc and the PSY.Branch +# method above would error on PSY.get_arc. Reachable: `_pin_outage_buses!` iterates +# `get_associated_components` unfiltered. +function _push_component_buses!(::Set{Int}, ::PSY.AreaInterchange) + return +end + +# Warn-skip instead of MethodError so this set stays reconcilable with PNM's +# `_accumulate_protected_buses!(::PSY.Component)`, which also warn-skips. +function _push_component_buses!(::Set{Int}, c::PSY.Component) + @warn "Outage-monitored component $(typeof(c)) ($(PSY.get_name(c))) has no \ + reduction-protection rule; its bus is not pinned and may be reduced away, so \ + its contingency will not be enforced. Add a _push_component_buses! method \ + for this type if it should be protected." maxlog = 5 + return +end + +function _collect_reduction_exceptions( + sys::PSY.System, + model::NetworkModel, + branch_models::BranchModelContainer, +) + @debug "Collecting reduction exceptions" _group = + IOM.LOG_GROUP_NETWORK_CONSTRUCTION + # Seeded with the caller's own exceptions; the template's rules add to them. + buses = Set{Int}(get_reduction_exceptions(model)) + for m in values(branch_models) + _pin_irreducible_buses!(buses, m, sys) + end + return collect(buses) +end + +_pin_irreducible_buses!(::Set{Int}, ::DeviceModel, ::PSY.System) = nothing + +function _pin_irreducible_buses!( + buses::Set{Int}, + m::DeviceModel{T}, + sys::PSY.System, +) where {T <: PSY.ACTransmission} + _pin_time_series_branch_buses!(buses, m, sys) + _pin_outage_buses!(buses, m, sys) + return +end + +function _pin_irreducible_buses!( + buses::Set{Int}, + m::DeviceModel{PSY.MonitoredLine}, + sys::PSY.System, +) + _pin_time_series_branch_buses!(buses, m, sys) + _pin_outage_buses!(buses, m, sys) + _pin_model_all_branches!(buses, m) + return +end + +# Rule 1: a branch carrying a rating time series pins both its endpoints, so the +# reduction cannot merge away the bus a time-varying limit is applied at. +function _pin_time_series_branch_buses!( + ::Set{Int}, + m::DeviceModel{PSY.ThreeWindingTransformer}, + ::PSY.System, +) + haskey(get_time_series_names(m), BranchRatingTimeSeriesParameter) || + return + _warn_three_winding_rating_unsupported() + return +end + +function _pin_time_series_branch_buses!( + buses::Set{Int}, + m::DeviceModel{T}, + sys::PSY.System, +) where {T <: PSY.ACTransmission} + ts_names = get_time_series_names(m) + haskey(ts_names, BranchRatingTimeSeriesParameter) || return + ts_name = ts_names[BranchRatingTimeSeriesParameter] + # TODO workaround since we dont have the container + ts_type = PSY.Deterministic + for branch in PSY.get_available_components(T, sys) + PSY.has_time_series(branch, ts_type, ts_name) || continue + _push_component_buses!(buses, branch) + end + return +end + +function _warn_three_winding_rating_unsupported() + @warn "Dynamic branch ratings for ThreeWindingTransformers are not implemented yet. Skipping it." + return +end + +# Rule 2: an outage registered on an outage-aware branch model pins both its +# monitored and its outaged endpoints. The MODF column for a contingency is keyed by +# the outaged arc's endpoints, and post-contingency flow constraints reference the +# monitored components' real bus numbers. +function _pin_outage_buses!(buses::Set{Int}, m::DeviceModel, sys::PSY.System) + IOM.supports_outages(get_formulation(m)) || return + for outage_uuid in keys(get_outages(m)) + outage = PSY.get_supplemental_attribute(sys, outage_uuid) + for uuid in PSY.get_monitored_components(outage) + component = IS.get_component(sys, uuid) + if isnothing(component) + throw( + IS.ConflictingInputsError( + "Monitored component with UUID $(uuid) on outage $(IS.get_uuid(outage)) not found in system. Data requires correction", + ), + ) + end + _push_component_buses!(buses, component) + end + for component in PSY.get_associated_components(sys, outage) + _push_component_buses!(buses, component) + end + end + return +end + +# Rule 3: a `model_all_branches` MonitoredLine model pins its lines so zero-impedance +# ones survive the reduction instead of being merged away. +function _pin_model_all_branches!( + buses::Set{Int}, + m::DeviceModel{PSY.MonitoredLine}, +) + get_attribute(m, MODEL_ALL_BRANCHES_KEY) === true || return + for branch in get_device_cache(m) + _push_component_buses!(buses, branch) + end + return +end diff --git a/src/operation/build_problem.jl b/src/operation/build_problem.jl index e5186e59..fde42594 100644 --- a/src/operation/build_problem.jl +++ b/src/operation/build_problem.jl @@ -87,7 +87,7 @@ function build_problem!( get_network_model(template), transmission_model.subnetworks, sys, - transmission_model.network_reduction.bus_reduction_map) + PNM.get_bus_reduction_map(get_network_reduction(transmission_model))) initialize_hvdc_system!( container, diff --git a/src/utils/print.jl b/src/utils/print.jl index 63e53e18..d399f85c 100644 --- a/src/utils/print.jl +++ b/src/utils/print.jl @@ -16,7 +16,7 @@ function _show_method( table = [ "Network Model" string(get_network_formulation(network_model)) "Slacks" get_use_slacks(network_model) - "PTDF" !isnothing(get_network_matrix(network_model)) + "Network Data" !isnothing(get_network_data(network_model)) "Duals" isempty(get_duals(network_model)) ? "None" : string.(get_duals(network_model)) "HVDC Network Model" isnothing(get_hvdc_network_model(network_model)) ? "None" : replace(string(get_hvdc_network_model(network_model)), r"[()]" => "") ] diff --git a/test/Project.toml b/test/Project.toml index 89209fde..ad4e731e 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -33,7 +33,7 @@ TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" [sources] -InfrastructureOptimizationModels = {rev = "main", url = "https://github.com/Sienna-Platform/InfrastructureOptimizationModels.jl"} +InfrastructureOptimizationModels = {rev = "jd/network-sources", url = "https://github.com/Sienna-Platform/InfrastructureOptimizationModels.jl"} InfrastructureSystems = {rev = "IS4", url = "https://github.com/Sienna-Platform/InfrastructureSystems.jl"} PowerNetworkMatrices = {rev = "psy6", url = "https://github.com/Sienna-Platform/PowerNetworkMatrices.jl"} PowerSystemCaseBuilder = {rev = "psy6", url = "https://github.com/Sienna-Platform/PowerSystemCaseBuilder.jl"} diff --git a/test/test_ac_transmission_security_constrained_models.jl b/test/test_ac_transmission_security_constrained_models.jl index ce9bbcf3..97f5d58b 100644 --- a/test/test_ac_transmission_security_constrained_models.jl +++ b/test/test_ac_transmission_security_constrained_models.jl @@ -43,7 +43,7 @@ end template = get_thermal_dispatch_template_network( NetworkModel( PTDFNetworkModel; - network_matrix = PNM.VirtualPTDF(c_sys5), + network_source = PrebuiltMatrixSource(PNM.VirtualPTDF(c_sys5)), ), ) set_device_model!(template, PSY.Line, POM.SecurityConstrainedStaticBranch) @@ -78,7 +78,7 @@ end IOM.get_expression(container, POM.ActivePowerBalance, PSY.ACBus).data time_steps = IOM.get_time_steps(container) - net_reduction_data = network_model.network_reduction + net_reduction_data = IOM.get_network_reduction(network_model) name_to_arc_maps = PNM.get_name_to_arc_maps(net_reduction_data) all_branch_maps_by_type = PNM.get_all_branch_maps_by_type(net_reduction_data) @@ -158,8 +158,8 @@ end # MOI-count / objective-value magic numbers, which differ under POM/PS6. @testset "Security Constrained branch formulation Network DC-PF with VirtualPTDF + auto-MODF" begin - # Exercises the VirtualPTDF + auto-constructed MODF code path: contingency_matrix - # is intentionally omitted so it must be auto-built during instantiate. + # Exercises the prebuilt-VirtualPTDF code path with a derived MODF: the contingency + # matrix is always derived from the source's factorization core during instantiate. c_sys5 = PSB.build_system(PSITestSystems, "c_sys5") all_branches = collect(get_components(PSY.ACTransmission, c_sys5)) for line_name in ["1", "2", "3"] @@ -174,8 +174,7 @@ end template = get_thermal_dispatch_template_network( NetworkModel( PTDFNetworkModel; - network_matrix = PNM.VirtualPTDF(c_sys5), - # contingency_matrix intentionally omitted — exercises auto-construction + network_source = PrebuiltMatrixSource(PNM.VirtualPTDF(c_sys5)), ), ) set_device_model!(template, PSY.Line, POM.SecurityConstrainedStaticBranch) @@ -194,7 +193,7 @@ end @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == IOM.ModelBuildStatus.BUILT - # MODF should have been auto-populated during build + # MODF should have been derived during build nm = IOM.get_network_model(IOM.get_template(ps_model)) @test !isnothing(IOM.get_contingency_matrix(nm)) @@ -211,7 +210,10 @@ end c_sys14 = PSB.build_system(PSB.PSITestSystems, "c_sys14") template = get_thermal_dispatch_template_network( - NetworkModel(PTDFNetworkModel; network_matrix = PNM.VirtualPTDF(c_sys14)), + NetworkModel( + PTDFNetworkModel; + network_source = PrebuiltMatrixSource(PNM.VirtualPTDF(c_sys14)), + ), ) set_device_model!(template, PSY.Line, POM.StaticBranch) set_device_model!(template, PSY.TwoWindingTransformer, POM.StaticBranch) @@ -231,7 +233,7 @@ end IOM.get_expression(container, POM.ActivePowerBalance, PSY.ACBus).data time_steps = IOM.get_time_steps(container) - net_reduction_data = network_model.network_reduction + net_reduction_data = IOM.get_network_reduction(network_model) modeled_branch_types = network_model.modeled_branch_types n_checked = 0 @@ -300,8 +302,7 @@ end template = get_thermal_dispatch_template_network( NetworkModel( PTDFNetworkModel; - network_matrix = PNM.VirtualPTDF(c_sys14), - contingency_matrix = PNM.VirtualMODF(c_sys14), + network_source = PrebuiltMatrixSource(PNM.VirtualPTDF(c_sys14)), ), ) set_device_model!(template, PSY.Line, POM.SecurityConstrainedStaticBranch) @@ -331,7 +332,7 @@ end nodal_balance = IOM.get_expression(container, POM.ActivePowerBalance, PSY.ACBus).data - net_reduction_data = network_model.network_reduction + net_reduction_data = IOM.get_network_reduction(network_model) modeled_branch_types = network_model.modeled_branch_types name_to_arc_maps = PNM.get_name_to_arc_maps(net_reduction_data) n_checked = 0 @@ -393,11 +394,7 @@ end end auto_template = get_thermal_dispatch_template_network( - NetworkModel( - PTDFNetworkModel; - network_matrix = PNM.PTDF(c_sys5), - contingency_matrix = PNM.VirtualMODF(c_sys5), - ), + NetworkModel(PTDFNetworkModel), ) set_device_model!(auto_template, PSY.Line, POM.SecurityConstrainedStaticBranch) set_device_model!( @@ -465,7 +462,7 @@ end outage_uuid_str = string(outage_uuid) template = get_thermal_dispatch_template_network( - NetworkModel(PTDFNetworkModel; contingency_matrix = PNM.VirtualMODF(sys)), + NetworkModel(PTDFNetworkModel), ) set_device_model!(template, PSY.Line, POM.SecurityConstrainedStaticBranch) set_device_model!( @@ -553,7 +550,7 @@ end outage_uuid = string(IS.get_uuid(outage)) template = get_thermal_dispatch_template_network( - NetworkModel(PTDFNetworkModel; contingency_matrix = PNM.VirtualMODF(sys)), + NetworkModel(PTDFNetworkModel), ) set_device_model!(template, PSY.Line, POM.SecurityConstrainedStaticBranch) ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @@ -627,7 +624,7 @@ end ] @testset "$label" begin template = get_thermal_dispatch_template_network( - NetworkModel(NetFormulation; contingency_matrix = PNM.VirtualMODF(sys)), + NetworkModel(NetFormulation), ) set_device_model!(template, PSY.Line, POM.SecurityConstrainedStaticBranch) set_device_model!( @@ -775,7 +772,7 @@ end @test !isempty(con_ub.data) @test !isempty(con_lb.data) - net_reduction_data = network_model.network_reduction + net_reduction_data = IOM.get_network_reduction(network_model) name_to_arc_map = PNM.get_name_to_arc_maps(net_reduction_data)[PSY.Line] all_branch_maps_by_type = PNM.get_all_branch_maps_by_type(net_reduction_data) @@ -957,11 +954,7 @@ end PSY.add_supplemental_attribute!(sys, component, transition_data) end template = get_thermal_dispatch_template_network( - NetworkModel( - PTDFNetworkModel; - network_matrix = PNM.PTDF(sys), - contingency_matrix = PNM.VirtualMODF(sys), - ), + NetworkModel(PTDFNetworkModel), ) set_device_model!(template, PSY.Line, POM.SecurityConstrainedStaticBranch) set_device_model!( @@ -1036,11 +1029,7 @@ end PSY.add_supplemental_attribute!(sys, component_parallel, transition_data) end template = get_thermal_dispatch_template_network( - NetworkModel( - PTDFNetworkModel; - network_matrix = PNM.PTDF(sys), - contingency_matrix = PNM.VirtualMODF(sys), - ), + NetworkModel(PTDFNetworkModel), ) set_device_model!(template, PSY.Line, POM.SecurityConstrainedStaticBranch) set_device_model!( @@ -1113,16 +1102,10 @@ end PSY.add_supplemental_attribute!(sys, component, transition_data) end nr = NetworkReduction[DegreeTwoReduction()] - ptdf = PNM.PTDF(sys; network_reductions = nr) - modf = PNM.VirtualMODF(sys; network_reductions = nr) template = get_thermal_dispatch_template_network( NetworkModel( PTDFNetworkModel; - network_matrix = ptdf, - contingency_matrix = modf, - reduce_degree_two_branches = PNM.has_degree_two_reduction( - ptdf.network_reduction_data, - ), + network_source = NetworkReductionSpec(nr), ), ) set_device_model!(template, PSY.Line, POM.SecurityConstrainedStaticBranch) @@ -1234,16 +1217,10 @@ end PSY.add_supplemental_attribute!(sys, outaged_component, transition_data) end nr = NetworkReduction[DegreeTwoReduction()] - ptdf = PNM.PTDF(sys; network_reductions = nr) - modf = PNM.VirtualMODF(sys; network_reductions = nr) template = get_thermal_dispatch_template_network( NetworkModel( PTDFNetworkModel; - network_matrix = ptdf, - contingency_matrix = modf, - reduce_degree_two_branches = PNM.has_degree_two_reduction( - ptdf.network_reduction_data, - ), + network_source = NetworkReductionSpec(nr), ), ) set_device_model!(template, PSY.Line, POM.SecurityConstrainedStaticBranch) @@ -1365,7 +1342,7 @@ end # HiGHS returns the dual values directly. c_sys5 = _attach_all_branch_outages!(PSB.build_system(PSITestSystems, "c_sys5")) template = get_thermal_dispatch_template_network( - NetworkModel(PTDFNetworkModel; network_matrix = PNM.PTDF(c_sys5)), + NetworkModel(PTDFNetworkModel), ) set_device_model!( template, @@ -1401,7 +1378,7 @@ end c_sys5 = _attach_all_branch_outages!(PSB.build_system(PSITestSystems, "c_sys5")) template = PowerOperationsProblemTemplate( - NetworkModel(PTDFNetworkModel; network_matrix = PNM.PTDF(c_sys5)), + NetworkModel(PTDFNetworkModel), ) set_device_model!(template, PSY.PowerLoad, StaticPowerLoad) set_device_model!(template, PSY.ThermalStandard, ThermalStandardUnitCommitment) @@ -1433,16 +1410,15 @@ end _test_post_contingency_line_duals(IOM.get_optimization_container(ps_model)) end -# Local re-implementation of PSI's internal `_retained_buses(nrd)` helper, which -# POM does not export. Definition matches PSI exactly: -# _retained_buses(nrd) = Set(keys(PNM.get_bus_reduction_map(nrd))) +# The buses a reduction retained: the representatives, i.e. the keys of its bus +# reduction map. _sc_retained_buses(nrd) = Set(keys(PNM.get_bus_reduction_map(nrd))) -@testset "SC PTDF/MODF reductions are reconciled to a cohesive bus set" begin - # Regression: PTDF/MODF supplied with only [Radial, DegreeTwo] (no pre-baked - # irreducible buses) plus many monitored components can reduce to different - # bus sets. POM must reconcile them onto one cohesive reduction so `build!` - # succeeds without the caller replicating the irreducible-bus computation. +@testset "SC PTDF and MODF reduce to one bus set" begin + # Regression: a [Radial, DegreeTwo] reduction plus many monitored components used + # to be able to reduce the PTDF and the MODF onto different bus sets. Both now wrap + # one factorization core built from one Ybus, so they cannot diverge and the caller + # never replicates the irreducible-bus computation. sys = PSB.build_system(PSB.PSITestSystems, "test_RTS_GMLC_sys") all_lines = collect(get_components(PSY.Line, sys)) @test length(all_lines) > 1 @@ -1460,16 +1436,10 @@ _sc_retained_buses(nrd) = Set(keys(PNM.get_bus_reduction_map(nrd))) end nr = NetworkReduction[RadialReduction(), DegreeTwoReduction()] - # Caller provides matrices WITHOUT pre-baking irreducible buses. - ptdf = PNM.PTDF(sys; network_reductions = nr) - modf = PNM.VirtualMODF(sys; network_reductions = nr) template = get_thermal_dispatch_template_network( NetworkModel( PTDFNetworkModel; - network_matrix = ptdf, - contingency_matrix = modf, - reduce_radial_branches = true, - reduce_degree_two_branches = true, + network_source = NetworkReductionSpec(nr), ), ) set_device_model!(template, PSY.Line, POM.SecurityConstrainedStaticBranch) @@ -1505,7 +1475,7 @@ end function _build_sc_slack_model(use_slacks) template = get_thermal_dispatch_template_network( - NetworkModel(PTDFNetworkModel; network_matrix = PNM.PTDF(c_sys5)), + NetworkModel(PTDFNetworkModel), ) set_device_model!( template, @@ -1645,7 +1615,7 @@ end function _build_sc(use_slacks) template = get_thermal_dispatch_template_network( - NetworkModel(PTDFNetworkModel; network_matrix = PNM.PTDF(c_sys5)), + NetworkModel(PTDFNetworkModel), ) set_device_model!( template, @@ -1750,8 +1720,7 @@ end template = get_thermal_dispatch_template_network( NetworkModel( PTDFNetworkModel; - network_matrix = PNM.VirtualPTDF(c_sys14), - contingency_matrix = PNM.VirtualMODF(c_sys14), + network_source = PrebuiltMatrixSource(PNM.VirtualPTDF(c_sys14)), ), ) set_device_model!( diff --git a/test/test_arc_admittance_equivalence.jl b/test/test_arc_admittance_equivalence.jl new file mode 100644 index 00000000..aa953218 --- /dev/null +++ b/test/test_arc_admittance_equivalence.jl @@ -0,0 +1,73 @@ +import PowerNetworkMatrices as PNM + +# Phase 2 (not started here) would replace POM's per-branch admittance derivation +# (`PNM.branch_admittance` / `PNM.get_series_susceptance`, in `AC_branches.jl`) with +# per-arc reads off the `Ybus` and its `VirtualFactorCore`. Both paths derive from the +# same device data and the same reduction, so this test is the gate: it checks whether +# they actually agree before any builder is allowed to switch over. +# +# `PNM.get_name_to_arc_maps`/`get_all_branch_maps_by_type` are empty until +# `PNM.populate_branch_maps_by_type!(nrd)` is called (POM's own +# `_finalize_network_reduction!` does this during real builds); without it this test's +# outer loop iterates zero times and passes vacuously, so the call is required here. +# `PNM.YBUS_ELTYPE = ComplexF32` (definitions.jl:1): `Ybus.data`, and therefore +# `ArcAdmittanceMatrix`/`BA_Matrix`/`VirtualFactorCore.arc_susceptances` derived from it, carry +# only single-precision values even though their containers are declared `Float64`. The +# per-branch route (`get_series_susceptance` on the device's own `Float64` r/x) has no such +# limit. `rtol = 1e-6` is not arbitrary slack: it is ~10x the largest relative error measured +# (9.2e-8, consistent with Float32's ~1.2e-7 unit roundoff) across every case/reduction below, +# so it still fails loudly on a genuine formula divergence while tolerating the known storage +# truncation. +# +# NOT exercised here: a parallel branch group with r != 0 on any member. Both fixtures' +# parallel arcs are loss-free, so `get_series_susceptance(::AbstractBranchesParallel)`'s +# `sum(1/x_i)` (correct only when r_i == 0) is never checked against the impedance-correct +# combination BA_Matrix computes from the summed complex Ybus entry; a real disagreement there +# could be hiding under the Float32 noise floor this test already tolerates. +@testset "Per-arc DC susceptance matches the per-branch derivation" begin + for case in ("case11_network_reductions", "c_sys14") + sys = PSB.build_system(PSITestSystems, case) + for reductions in ( + PNM.NetworkReduction[], + PNM.NetworkReduction[PNM.RadialReduction()], + PNM.NetworkReduction[PNM.RadialReduction(), PNM.DegreeTwoReduction()], + ) + ybus = PNM.Ybus( + sys; + network_reductions = reductions, + make_arc_admittance_matrices = true, + ) + core = PNM.VirtualFactorCore(ybus) + nrd = PNM.get_network_reduction_data(ybus) + PNM.populate_branch_maps_by_type!(nrd) + arc_lookup = PNM.get_arc_lookup(core) + per_arc = PNM._get_arc_susceptances(core) + maps = PNM.get_all_branch_maps_by_type(nrd) + + n_checked = 0 + for (branch_type, name_map) in PNM.get_name_to_arc_maps(nrd) + for (name, (arc, map_name)) in name_map + # Arcs absorbed by the reduction (radial/degree-two) never make it + # into the retained arc_lookup; nothing to compare for them. + haskey(arc_lookup, arc) || continue + entry = maps[map_name][branch_type][arc] + from_branch = PNM.get_series_susceptance(entry, PSY.SU) + from_arc = per_arc[arc_lookup[arc]] + n_checked += 1 + # Sign convention differs between the branch-side getter and the + # BA-extracted arc value (BA takes abs of the first nonzero per arc + # column); compare magnitudes only. + @test isapprox(abs(from_branch), abs(from_arc); rtol = 1e-6) + end + end + @test n_checked > 0 + end + end +end + +# Series phase shift has no per-arc accessor: it is recoverable in principle from +# `angle(arc_admittance_from_to[arc, to_bus] / arc_admittance_to_from[arc, from_bus]) / 2` +# (the two ArcAdmittanceMatrix coupling terms, per `_pi_to_ybus`'s `Y12 = -Y_l*e^{jα}/tap`, +# `Y21 = -Y_l*e^{-jα}/tap`), but PNM exposes no function that does this — a caller would have +# to index `ArcAdmittanceMatrix.data` directly via `get_arc_lookup`/`get_bus_lookup`. Phase 2 +# must keep `PNM.get_series_phase_shift` as a per-branch call. diff --git a/test/test_device_branch_constructors.jl b/test/test_device_branch_constructors.jl index 565f1491..029f4d91 100644 --- a/test/test_device_branch_constructors.jl +++ b/test/test_device_branch_constructors.jl @@ -5,7 +5,7 @@ const DC_NETWORK_MODELS_FOR_TESTING = [PTDFNetworkModel, DCPNetworkModel] limits = PSY.get_flow_limits(PSY.get_component(MonitoredLine, system, "1"), PSY.SU) for model in DC_NETWORK_MODELS_FOR_TESTING template = get_thermal_dispatch_template_network( - NetworkModel(model; network_matrix = PTDF(system)), + NetworkModel(model), ) model_m = DecisionModel(template, system; optimizer = HiGHS_optimizer) @test build!(model_m; output_dir = mktempdir(; cleanup = true)) == @@ -51,7 +51,7 @@ end set_rating!(PSY.get_component(Line, system, "2"), 1.5 * PSY.SU) for model in DC_NETWORK_MODELS_FOR_TESTING template = get_thermal_dispatch_template_network( - NetworkModel(model; network_matrix = PTDF(system)), + NetworkModel(model), ) set_device_model!(template, DeviceModel(Line, StaticBranch)) set_device_model!(template, DeviceModel(MonitoredLine, StaticBranchUnbounded)) @@ -178,7 +178,7 @@ end for model in DC_NETWORK_MODELS_FOR_TESTING template = get_template_dispatch_with_network( - NetworkModel(model; network_matrix = PTDF(system)), + NetworkModel(model), ) set_device_model!( template, @@ -550,7 +550,7 @@ end # remove_component!(system, line) # # template = get_template_dispatch_with_network( - # NetworkModel(PTDFNetworkModel; network_matrix = PTDF(system)), + # NetworkModel(PTDFNetworkModel), # ) # set_device_model!(template, DeviceModel(TwoWindingTransformer, PhaseAngleControl)) # model_m = DecisionModel(template, system; optimizer = HiGHS_optimizer) @@ -862,7 +862,7 @@ end # Test with DC Power Flow Model for net_model in DC_NETWORK_MODELS_FOR_TESTING template = get_template_dispatch_with_network( - NetworkModel(net_model; network_matrix = PTDF(system)), + NetworkModel(net_model), ) # Set device model for ThreeWindingTransformer set_device_model!(template, DeviceModel(ThreeWindingTransformer, StaticBranch)) @@ -915,7 +915,7 @@ _bus_merged_away(nrd, b) = any(b in s for s in values(PNM.get_bus_reduction_map( ml = PSY.get_component(MonitoredLine, sys, "1") PSY.set_r!(ml, 0.0 * PSY.SU) PSY.set_x!(ml, 1e-5 * PSY.SU) - # No `network_matrix` provided, so a VirtualPTDF is built and the reduction runs. + # The default network source builds a VirtualPTDF, so the reduction runs. template = get_thermal_dispatch_template_network(NetworkModel(PTDFNetworkModel)) set_device_model!( template, diff --git a/test/test_device_hvdc.jl b/test/test_device_hvdc.jl index cc89e0f6..e4fd3a0c 100644 --- a/test/test_device_hvdc.jl +++ b/test/test_device_hvdc.jl @@ -3,7 +3,7 @@ template_uc = PowerOperationsProblemTemplate(NetworkModel( DCPNetworkModel, #use_slacks=true, - #network_matrix =PTDF(sys_5), + #network_source = PrebuiltMatrixSource(VirtualPTDF(sys_5)), #duals=[CopperPlateBalanceConstraint], )) @@ -28,7 +28,6 @@ NetworkModel( PTDFNetworkModel; #use_slacks=true, - network_matrix = PTDF(sys_5), #duals=[CopperPlateBalanceConstraint], ), ) @@ -124,7 +123,7 @@ end ) end template = PowerOperationsProblemTemplate( - NetworkModel(PTDFNetworkModel; network_matrix = PTDF(sys)), + NetworkModel(PTDFNetworkModel), ) set_device_model!(template, ThermalStandard, ThermalDispatchNoMin) set_device_model!(template, PowerLoad, StaticPowerLoad) @@ -172,7 +171,7 @@ end sys = _generate_test_hvdc_sys() for scheme in ("bin2", "nmdt") template = PowerOperationsProblemTemplate( - NetworkModel(PTDFNetworkModel; network_matrix = PTDF(sys)), + NetworkModel(PTDFNetworkModel), ) set_device_model!(template, ThermalStandard, ThermalDispatchNoMin) set_device_model!(template, PowerLoad, StaticPowerLoad) diff --git a/test/test_device_load_constructors.jl b/test/test_device_load_constructors.jl index dda27b3f..8ab715c5 100644 --- a/test/test_device_load_constructors.jl +++ b/test/test_device_load_constructors.jl @@ -13,6 +13,36 @@ test_path = mktempdir() @test solve!(ps_model) == IOM.RunStatus.SUCCESSFULLY_FINALIZED end +@testset "AreaBalance subnetwork topology check fires only above one subnetwork" begin + c_sys = PSB.build_system(PSISystems, "two_area_pjm_DA") + area_map = PSY.get_aggregation_topology_mapping(PSY.Area, c_sys) + buses = [ + PSY.get_number(b) for b in first(values(area_map)) if + PSY.get_bustype(b) != PSY.ACBusTypes.ISOLATED + ] + @test length(buses) > 1 + + # A single subnetwork is the normal case for AreaBalance and must not warn: the + # network model now always populates at least one, where it used to be empty. + single = Dict(first(buses) => Set(buses)) + @test_logs min_level = Logging.Warn POM._verify_area_subnetwork_topology( + c_sys, + single, + ) + + # Two subnetworks with one Area straddling both is the condition the check exists + # to reject, and it still throws. + split_at = length(buses) ÷ 2 + straddling = Dict( + buses[1] => Set(buses[1:split_at]), + buses[split_at + 1] => Set(buses[(split_at + 1):end]), + ) + @test_throws IS.ConflictingInputsError POM._verify_area_subnetwork_topology( + c_sys, + straddling, + ) +end + @testset "AreaInterchange with a network model that reduces branches" begin # AreaInterchange <: PSY.Branch but connects Areas, not buses; it has no # arc. Building this with a network model that actually performs radial and @@ -22,8 +52,10 @@ end transform_single_time_series!(c_sys, Hour(24), Hour(1)) network = NetworkModel( DCPNetworkModel; - reduce_radial_branches = true, - reduce_degree_two_branches = true, + network_source = NetworkReductionSpec( + PNM.RadialReduction(), + PNM.DegreeTwoReduction(), + ), ) template = get_thermal_dispatch_template_network(network) set_device_model!(template, AreaInterchange, StaticBranch) diff --git a/test/test_initial_conditions.jl b/test/test_initial_conditions.jl new file mode 100644 index 00000000..1bb229b5 --- /dev/null +++ b/test/test_initial_conditions.jl @@ -0,0 +1,22 @@ +@testset "IC network model shares the main model's derived network" begin + sys = PSB.build_system(PSITestSystems, "c_sys5") + template = get_thermal_dispatch_template_network( + NetworkModel( + POM.DCPNetworkModel; + network_source = NetworkReductionSpec(PNM.RadialReduction()), + ), + ) + model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + + main = get_network_model(get_template(model)) + ic = get_network_model(POM.get_initial_conditions_template(model, 2)) + # Shared by reference, not rebuilt: this is what makes an IC/main network + # divergence structurally impossible rather than something to validate. + @test get_network_source(ic) === get_network_source(main) + @test get_network_data(ic) === get_network_data(main) + @test get_network_reduction(ic) === get_network_reduction(main) + @test get_subnetworks(ic) == get_subnetworks(main) + @test get_reduction_exceptions(ic) == get_reduction_exceptions(main) +end diff --git a/test/test_model_decision.jl b/test/test_model_decision.jl index b69742ed..27163703 100644 --- a/test/test_model_decision.jl +++ b/test/test_model_decision.jl @@ -205,7 +205,7 @@ end networks = [PTDFNetworkModel, DCPNetworkModel, ACPNetworkModel] for network in networks template = get_thermal_dispatch_template_network( - NetworkModel(network; use_slacks = true, network_matrix = PTDF(c_sys5_re)), + NetworkModel(network; use_slacks = true), ) model = DecisionModel(template, c_sys5_re; optimizer = ipopt_optimizer) @test build!(model; output_dir = mktempdir(; cleanup = true)) == @@ -223,7 +223,11 @@ end LMPs = [] for (ix, network) in enumerate(networks) template = get_template_dispatch_with_network( - NetworkModel(network; network_matrix = ptdf, duals = dual_constraint[ix]), + NetworkModel( + network; + network_source = PrebuiltMatrixSource(ptdf), + duals = dual_constraint[ix], + ), ) if network == PTDFNetworkModel set_device_model!( diff --git a/test/test_native_dcp_acp_models.jl b/test/test_native_dcp_acp_models.jl index 4335e150..6c18806b 100644 --- a/test/test_native_dcp_acp_models.jl +++ b/test/test_native_dcp_acp_models.jl @@ -67,8 +67,10 @@ end sys = PSB.build_system(PSITestSystems, "c_sys14") net = NetworkModel( DCPNetworkModel; - reduce_radial_branches = true, - reduce_degree_two_branches = true, + network_source = NetworkReductionSpec( + PNM.RadialReduction(), + PNM.DegreeTwoReduction(), + ), ) template = get_thermal_dispatch_template_network(net) # TODO: fails with HiGHS QP solver, why? @@ -82,8 +84,10 @@ end sys = PSB.build_system(PSITestSystems, "c_sys14") net = NetworkModel( ACPNetworkModel; - reduce_radial_branches = true, - reduce_degree_two_branches = true, + network_source = NetworkReductionSpec( + PNM.RadialReduction(), + PNM.DegreeTwoReduction(), + ), ) template = get_thermal_dispatch_template_network(net) model = DecisionModel(template, sys; optimizer = ipopt_optimizer) @@ -1051,7 +1055,7 @@ end @test min(rate, limits.from_to, limits.to_from) != rate template = get_thermal_dispatch_template_network( - NetworkModel(PTDFNetworkModel; network_matrix = PTDF(sys)), + NetworkModel(PTDFNetworkModel), ) set_device_model!(template, DeviceModel(PSY.MonitoredLine, StaticBranchBounds)) model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) diff --git a/test/test_native_network_reductions.jl b/test/test_native_network_reductions.jl index c186dfc7..28cae942 100644 --- a/test/test_native_network_reductions.jl +++ b/test/test_native_network_reductions.jl @@ -24,6 +24,17 @@ function _case11_with_forecast() return sys end +function _network_source_from_flags(radial::Bool, degree_two::Bool) + reductions = PNM.NetworkReduction[] + if radial + push!(reductions, PNM.RadialReduction()) + end + if degree_two + push!(reductions, PNM.DegreeTwoReduction()) + end + return NetworkReductionSpec(reductions) +end + function _solve_case11_native( network_formulation, optimizer; @@ -34,8 +45,10 @@ function _solve_case11_native( sys = _case11_with_forecast() net = NetworkModel( network_formulation; - reduce_radial_branches = reduce_radial_branches, - reduce_degree_two_branches = reduce_degree_two_branches, + network_source = _network_source_from_flags( + reduce_radial_branches, + reduce_degree_two_branches, + ), ) template = get_thermal_dispatch_template_network(net) model = DecisionModel(template, sys; optimizer = optimizer) @@ -332,8 +345,10 @@ end PSY.add_component!(sys, shunt) net = NetworkModel( ACPNetworkModel; - reduce_radial_branches = true, - reduce_degree_two_branches = true, + network_source = NetworkReductionSpec( + PNM.RadialReduction(), + PNM.DegreeTwoReduction(), + ), ) template = get_thermal_dispatch_template_network(net) set_device_model!( @@ -379,8 +394,10 @@ end # # net = NetworkModel( # DCPNetworkModel; - # reduce_radial_branches = true, - # reduce_degree_two_branches = true, + # network_source = NetworkReductionSpec( + # PNM.RadialReduction(), + # PNM.DegreeTwoReduction(), + # ), # ) # template = get_thermal_dispatch_template_network(net) # set_device_model!( @@ -424,8 +441,10 @@ end net = NetworkModel( ACPNetworkModel; - reduce_radial_branches = true, - reduce_degree_two_branches = true, + network_source = NetworkReductionSpec( + PNM.RadialReduction(), + PNM.DegreeTwoReduction(), + ), ) template = get_thermal_dispatch_template_network(net) set_device_model!( @@ -587,3 +606,218 @@ end @test tightened_rating < representative_raw @test JuMP.upper_bound(pft["1-6-i_1", first(time_steps)]) == tightened_rating end + +@testset "Reduction flags map to the same PNM reductions everywhere" begin + for (radial, degree_two, expected) in ( + (false, false, 0), + (true, false, 1), + (false, true, 1), + (true, true, 2), + ) + net = NetworkModel( + POM.DCPNetworkModel; + network_source = _network_source_from_flags(radial, degree_two), + ) + reductions = POM._source_reductions(get_network_source(net)) + @test length(reductions) == expected + if radial + @test POM.PNM.RadialReduction() in reductions + end + end + + # Unit coverage above pins the source -> reductions mapping itself, but not the + # build sites that consume it. Drive each one through the real build pipeline and + # read the reduction back off the built network model/matrices, so a site wired to + # the wrong source would fail here. + for (radial, degree_two) in ((false, false), (true, false), (false, true), (true, true)) + model_dcp, status_dcp = _solve_case11_native( + DCPNetworkModel, HiGHS_optimizer; + reduce_radial_branches = radial, reduce_degree_two_branches = degree_two, + ) + @test status_dcp == IOM.ModelBuildStatus.BUILT + nr_dcp = get_network_reduction(get_network_model(get_template(model_dcp))) + @test PNM.has_radial_reduction(nr_dcp) == radial + @test PNM.has_degree_two_reduction(nr_dcp) == degree_two + + # Plain PTDFNetworkModel, no outage-aware branch: no contingency matrix is + # ever built, so this isolates the PTDF site. + model_ptdf, status_ptdf = _solve_case11_native( + PTDFNetworkModel, HiGHS_optimizer; + reduce_radial_branches = radial, reduce_degree_two_branches = degree_two, + ) + @test status_ptdf == IOM.ModelBuildStatus.BUILT + nr_ptdf = get_network_reduction(get_network_model(get_template(model_ptdf))) + @test PNM.has_radial_reduction(nr_ptdf) == radial + @test PNM.has_degree_two_reduction(nr_ptdf) == degree_two + + # PTDFNetworkModel + an outage-aware branch derives a MODF alongside the PTDF. + # No reconciliation pass exists any more to repair a divergence after the fact, + # so the MODF's own reduction is asserted directly below: this isolates the MODF + # site the way the two checks above isolate theirs, and pins the property the + # shared factorization core is supposed to guarantee. + sys_modf = _case11_with_forecast() + outaged_line = PSY.get_component(PSY.Line, sys_modf, "4-5-i_1") + transition = PSY.GeometricDistributionForcedOutage(; + mean_time_to_recovery = 10, + outage_transition_probability = 0.9999, + monitored_components = [outaged_line], + ) + PSY.add_supplemental_attribute!(sys_modf, outaged_line, transition) + net_modf = NetworkModel( + PTDFNetworkModel; + network_source = _network_source_from_flags(radial, degree_two), + ) + template_modf = get_thermal_dispatch_template_network(net_modf) + set_device_model!(template_modf, PSY.Line, SecurityConstrainedStaticBranch) + model_modf = DecisionModel(template_modf, sys_modf; optimizer = HiGHS_optimizer) + @test build!( + model_modf; + output_dir = mktempdir(; cleanup = true), + console_level = Logging.Error, + ) == IOM.ModelBuildStatus.BUILT + + network_model_modf = get_network_model(get_template(model_modf)) + nr_ptdf_modf = get_network_reduction(network_model_modf) + @test PNM.has_radial_reduction(nr_ptdf_modf) == radial + @test PNM.has_degree_two_reduction(nr_ptdf_modf) == degree_two + + modf_matrix = IOM.get_contingency_matrix(network_model_modf) + nr_modf = PNM.get_network_reduction_data(modf_matrix) + @test PNM.has_radial_reduction(nr_modf) == radial + @test PNM.has_degree_two_reduction(nr_modf) == degree_two + + # The MODF must not merely carry the same reduction flags as the PTDF; it must + # be reduced onto the very same bus set, which is what makes the nodal-balance + # rows and the post-contingency MODF columns dimensionally compatible. + nr_ptdf_matrix = + PNM.get_network_reduction_data(IOM.get_network_matrix(network_model_modf)) + @test PNM.get_bus_reduction_map(nr_modf) == + PNM.get_bus_reduction_map(nr_ptdf_matrix) + @test PNM.get_bus_reduction_map(nr_modf) == + PNM.get_bus_reduction_map(nr_ptdf_modf) + end +end + +@testset "Reduction exceptions are collected per device model" begin + sys = _case11_with_forecast() + # Rating time series must share the load forecast's resolution/initial times — + # DecisionModel rejects a system with mixed time series resolutions. + rating_line = get_component(PSY.Line, sys, "4-5-i_1") + rating_data = Dict( + DateTime("2020-01-01T08:00:00") => fill(0.9, 5), + DateTime("2020-01-01T08:30:00") => fill(0.9, 5), + DateTime("2020-01-01T09:00:00") => fill(0.9, 5), + ) + add_time_series!( + sys, + rating_line, + Deterministic("branch_rating", rating_data, Dates.Minute(5)), + ) + net = NetworkModel( + POM.DCPNetworkModel; + network_source = NetworkReductionSpec(PNM.RadialReduction()), + ) + template = get_thermal_dispatch_template_network(net) + set_device_model!( + template, + DeviceModel( + PSY.Line, + StaticBranch; + time_series_names = Dict(BranchRatingTimeSeriesParameter => "branch_rating"), + ), + ) + model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + + branch_models = get_branch_models(get_template(model)) + exceptions = POM._collect_reduction_exceptions( + sys, + get_network_model(get_template(model)), + branch_models, + ) + # "4-5-i_1" carries the rating time series; its endpoint buses (4, 5) must be + # pinned so the reduction can't merge them away. + @test 4 in exceptions + @test 5 in exceptions + @test length(exceptions) == 2 +end + +@testset "Caller-supplied reduction_exceptions survive the build's reduction" begin + # Bus 8 hangs off a radial branch, so RadialReduction absorbs it into bus 1 unless + # something pins it. Asserted through a real build so the wiring from the + # NetworkModel keyword to the Ybus's irreducible set is what is under test. + function _retained_after_build(exceptions) + net = NetworkModel( + POM.DCPNetworkModel; + network_source = NetworkReductionSpec(PNM.RadialReduction()), + reduction_exceptions = exceptions, + ) + template = get_thermal_dispatch_template_network(net) + model = DecisionModel( + template, + _case11_with_forecast(); + optimizer = HiGHS_optimizer, + ) + @test build!( + model; + output_dir = mktempdir(; cleanup = true), + console_level = Logging.Error, + ) == IOM.ModelBuildStatus.BUILT + network_model = get_network_model(get_template(model)) + @test get_reduction_exceptions(network_model) == exceptions + return Set( + keys(PNM.get_bus_reduction_map(get_network_reduction(network_model))), + ) + end + + @test !(8 in _retained_after_build(Int[])) + @test 8 in _retained_after_build([8]) +end + +@testset "Non-PTDF families honour a prebuilt source's reduction" begin + # DCP consumes no sensitivity matrix, so a prebuilt PTDF or core is unused as a + # matrix — but it still declares the reduced network the build must run on. + reductions = PNM.NetworkReduction[PNM.RadialReduction()] + + function _dcp_reduction_from(build_source) + sys = _case11_with_forecast() + net = NetworkModel(POM.DCPNetworkModel; network_source = build_source(sys)) + template = get_thermal_dispatch_template_network(net) + model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) + @test build!( + model; + output_dir = mktempdir(; cleanup = true), + console_level = Logging.Error, + ) == IOM.ModelBuildStatus.BUILT + return get_network_reduction(get_network_model(get_template(model))) + end + + from_matrix = _dcp_reduction_from( + sys -> PrebuiltMatrixSource( + PNM.VirtualPTDF( + sys; + tol = POM.PTDF_ZERO_TOL, + network_reductions = reductions, + ), + ), + ) + @test PNM.has_radial_reduction(from_matrix) + + from_core = _dcp_reduction_from( + sys -> PrebuiltCoreSource( + PNM.VirtualFactorCore( + PNM.Ybus(sys; network_reductions = reductions); + tol = POM.PTDF_ZERO_TOL, + system_uuid = IS.get_uuid(sys), + ), + ), + ) + @test PNM.has_radial_reduction(from_core) + + # Control: the same formulation and template with a source that reduces nothing. + # Without this the two assertions above could pass on an unreduced network. + @test !PNM.has_radial_reduction( + _dcp_reduction_from(sys -> IOM.DefaultNetworkSource()), + ) +end diff --git a/test/test_network_constructors.jl b/test/test_network_constructors.jl index ac49548f..7815470d 100644 --- a/test/test_network_constructors.jl +++ b/test/test_network_constructors.jl @@ -16,16 +16,11 @@ function _reduced_ptdf_duals_template() sys = build_system(PSITestSystems, "case11_network_reductions") add_load_time_series_data!(sys) nr = NetworkReduction[RadialReduction(), DegreeTwoReduction()] - ptdf = PTDF(sys; network_reductions = nr) template = PowerOperationsProblemTemplate( NetworkModel(PTDFNetworkModel; - network_matrix = ptdf, + network_source = NetworkReductionSpec(nr), duals = [CopperPlateBalanceConstraint], - reduce_radial_branches = PNM.has_radial_reduction(ptdf.network_reduction_data), - reduce_degree_two_branches = PNM.has_degree_two_reduction( - ptdf.network_reduction_data, - ), use_slacks = false), ) # Mirror the filter shape from issue #1594: a voltage threshold that selects diff --git a/test/test_network_constructors_with_branch_rating_time_series.jl b/test/test_network_constructors_with_branch_rating_time_series.jl index 80a9bcd4..5f06f32a 100644 --- a/test/test_network_constructors_with_branch_rating_time_series.jl +++ b/test/test_network_constructors_with_branch_rating_time_series.jl @@ -100,10 +100,7 @@ end initial_date = "2024-01-01", ) template = get_thermal_dispatch_template_network( - NetworkModel( - PTDFNetworkModel; - network_matrix = PTDF_ref[sys], - ), + NetworkModel(PTDFNetworkModel), ) set_device_model!(template, line_device_model) @@ -190,10 +187,7 @@ end ) template = get_thermal_dispatch_template_network( - NetworkModel( - PTDFNetworkModel; - network_matrix = PTDF(sys), - ), + NetworkModel(PTDFNetworkModel), ) set_device_model!(template, line_device_model) set_device_model!(template, PSY.MonitoredLine, StaticBranch) @@ -277,10 +271,7 @@ end ) template = get_thermal_dispatch_template_network( - NetworkModel( - PTDFNetworkModel; - network_matrix = PTDF(sys), - ), + NetworkModel(PTDFNetworkModel), ) set_device_model!(template, line_device_model) set_device_model!(template, PSY.MonitoredLine, StaticBranch) @@ -363,10 +354,7 @@ end ) template = get_thermal_dispatch_template_network( - NetworkModel( - PTDFNetworkModel; - network_matrix = PTDF(sys), - ), + NetworkModel(PTDFNetworkModel), ) set_device_model!(template, line_device_model) ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @@ -454,14 +442,10 @@ end initial_date = "2024-01-01", ) nr = NetworkReduction[DegreeTwoReduction()] - ptdf = PTDF(sys; network_reductions = nr) template = get_thermal_dispatch_template_network( NetworkModel( PTDFNetworkModel; - #network_matrix = ptdf, - reduce_degree_two_branches = PNM.has_degree_two_reduction( - ptdf.network_reduction_data, - ), + network_source = NetworkReductionSpec(nr), ), ) set_device_model!(template, line_device_model) @@ -788,7 +772,7 @@ end IS.get_time_series_uuid(PSY.Deterministic, line_b, "branch_rating") template = get_thermal_dispatch_template_network( - NetworkModel(PTDFNetworkModel; network_matrix = PTDF(sys)), + NetworkModel(PTDFNetworkModel), ) set_device_model!( template, diff --git a/test/test_network_sources.jl b/test/test_network_sources.jl new file mode 100644 index 00000000..c904238a --- /dev/null +++ b/test/test_network_sources.jl @@ -0,0 +1,441 @@ +@testset "NetworkReductionSpec builds a Ybus with the requested reductions" begin + sys = PSB.build_system(PSITestSystems, "case11_network_reductions") + + plain = POM._build_ybus(POM.NetworkReductionSpec(), sys, Int[]) + @test isempty(POM.PNM.get_reductions(POM.PNM.get_network_reduction_data(plain))) + + radial = POM._build_ybus( + POM.NetworkReductionSpec(POM.PNM.RadialReduction()), + sys, + Int[], + ) + @test POM.PNM.has_radial_reduction( + POM.PNM.get_reductions(POM.PNM.get_network_reduction_data(radial)), + ) +end + +@testset "DefaultNetworkSource applies no reductions" begin + sys = PSB.build_system(PSITestSystems, "case11_network_reductions") + ybus = POM._build_ybus(IOM.DefaultNetworkSource(), sys, Int[]) + @test isempty(POM.PNM.get_reductions(POM.PNM.get_network_reduction_data(ybus))) +end + +@testset "Reduction exceptions reach the Ybus" begin + sys = PSB.build_system(PSITestSystems, "case11_network_reductions") + # Bus 8 is absorbed by the radial reduction when nothing pins it. + unpinned = POM._build_ybus( + POM.NetworkReductionSpec(POM.PNM.RadialReduction()), + sys, + Int[], + ) + pinned = POM._build_ybus( + POM.NetworkReductionSpec(POM.PNM.RadialReduction()), + sys, + [8], + ) + retained(y) = Set( + keys(POM.PNM.get_bus_reduction_map(POM.PNM.get_network_reduction_data(y))), + ) + @test 8 in retained(pinned) + @test !(8 in retained(unpinned)) +end + +@testset "PTDFNetworkData shares one reduction between PTDF and MODF" begin + sys = PSB.build_system(PSITestSystems, "c_sys5") + ybus = POM._build_ybus(POM.NetworkReductionSpec(), sys, Int[]) + core = POM.PNM.VirtualFactorCore( + ybus; + tol = POM.PTDF_ZERO_TOL, + system_uuid = IS.get_uuid(sys), + ) + ptdf = POM.PNM.VirtualPTDF(core) + modf = POM.PNM.VirtualMODF(core, sys; automatically_register_outages = false) + + nd = POM.PTDFNetworkData( + ptdf, + modf, + deepcopy(POM.PNM.get_network_reduction_data(core)), + ) + @test POM.has_contingency_matrix(nd) + @test POM.PNM.get_bus_reduction_map( + POM.PNM.get_network_reduction_data(POM.get_matrix(nd)), + ) == POM.PNM.get_bus_reduction_map( + POM.PNM.get_network_reduction_data(POM.get_network_data_contingency_matrix(nd)), + ) + + without = POM.PTDFNetworkData( + ptdf, + deepcopy(POM.PNM.get_network_reduction_data(core)), + ) + @test !POM.has_contingency_matrix(without) + @test_throws ErrorException POM.get_network_data_contingency_matrix(without) +end + +@testset "get_network_data_contingency_matrix errors without a contingency matrix" begin + sys = PSB.build_system(PSITestSystems, "c_sys5") + ybus = POM._build_ybus(POM.NetworkReductionSpec(), sys, Int[]) + + dcp_without = POM.DCPNetworkData( + ybus, + deepcopy(POM.PNM.get_network_reduction_data(ybus)), + ) + @test !POM.has_contingency_matrix(dcp_without) + @test_throws ErrorException POM.get_network_data_contingency_matrix(dcp_without) + + core = POM.PNM.VirtualFactorCore( + ybus; + tol = POM.PTDF_ZERO_TOL, + system_uuid = IS.get_uuid(sys), + ) + ptdf_without = POM.PTDFNetworkData( + POM.PNM.VirtualPTDF(core), + deepcopy(POM.PNM.get_network_reduction_data(core)), + ) + @test !POM.has_contingency_matrix(ptdf_without) + @test_throws ErrorException POM.get_network_data_contingency_matrix(ptdf_without) +end + +@testset "PrebuiltMatrixSource forwards its VirtualPTDF's own reduction" begin + sys = PSB.build_system(PSITestSystems, "case11_network_reductions") + ybus = POM._build_ybus( + POM.NetworkReductionSpec(POM.PNM.RadialReduction()), + sys, + Int[], + ) + core = POM.PNM.VirtualFactorCore( + ybus; + tol = POM.PTDF_ZERO_TOL, + system_uuid = IS.get_uuid(sys), + ) + ptdf = POM.PNM.VirtualPTDF(core) + source = POM.PrebuiltMatrixSource(ptdf) + + @test POM.get_matrix(source) === ptdf + @test POM.PNM.RadialReduction() in POM._source_reductions(source) + @test !hasmethod(POM._build_ybus, Tuple{typeof(source), PSY.System, Vector{Int}}) + + dense = POM.PNM.PTDF(ybus) + @test_throws MethodError POM.PrebuiltMatrixSource(dense) +end + +@testset "PrebuiltCoreSource forwards the wrapped core's own reduction" begin + sys = PSB.build_system(PSITestSystems, "case11_network_reductions") + ybus = POM._build_ybus( + POM.NetworkReductionSpec(POM.PNM.RadialReduction()), + sys, + Int[], + ) + core = POM.PNM.VirtualFactorCore( + ybus; + tol = POM.PTDF_ZERO_TOL, + system_uuid = IS.get_uuid(sys), + ) + source = POM.PrebuiltCoreSource(core) + + @test POM.get_core(source) === core + @test POM._source_reductions(source) == + POM.PNM.get_applied_reductions(POM.PNM.get_network_reduction_data(core)) + @test POM.PNM.RadialReduction() in POM._source_reductions(source) + @test !hasmethod(POM._build_ybus, Tuple{typeof(source), PSY.System, Vector{Int}}) +end + +@testset "PTDF and MODF share one reduction after a real build" begin + sys = PSB.build_system(PSITestSystems, "c_sys5") + net = NetworkModel( + POM.PTDFNetworkModel; + network_source = POM.NetworkReductionSpec(POM.PNM.RadialReduction()), + ) + template = get_thermal_dispatch_template_network(net) + model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + + nm = get_network_model(get_template(model)) + ptdf_nrd = POM.PNM.get_network_reduction_data(IOM.get_network_matrix(nm)) + @test POM.PNM.get_bus_reduction_map(ptdf_nrd) == + POM.PNM.get_bus_reduction_map(IOM.get_network_reduction(nm)) +end + +@testset "A prebuilt VirtualPTDF reuses its own core" begin + sys = PSB.build_system(PSITestSystems, "c_sys5") + vptdf = POM.PNM.VirtualPTDF(sys; tol = POM.PTDF_ZERO_TOL) + net = NetworkModel( + POM.PTDFNetworkModel; + network_source = POM.PrebuiltMatrixSource(vptdf), + ) + template = get_thermal_dispatch_template_network(net) + model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + @test IOM.get_network_matrix(get_network_model(get_template(model))) === vptdf +end + +@testset "subnetworks is not a constructor keyword" begin + @test_throws MethodError NetworkModel( + POM.CopperPlateNetworkModel; + subnetworks = Dict(1 => Set([1, 2])), + ) +end + +# Both testsets below build the same SC configuration used throughout +# test_ac_transmission_security_constrained_models.jl (there is no single +# canonical "SC template" helper in the test suite — every testset there +# builds `get_thermal_dispatch_template_network(net)` and then overrides +# Line/TwoWindingTransformer with SecurityConstrainedStaticBranch inline). +function _sc_template_with_outages(sys, net) + for line_name in ["1", "2", "3"] + line = get_component(PSY.ACTransmission, sys, line_name) + PSY.add_supplemental_attribute!( + sys, + line, + PSY.GeometricDistributionForcedOutage(; + mean_time_to_recovery = 10, + outage_transition_probability = 0.9999, + monitored_components = collect(get_components(PSY.ACTransmission, sys)), + ), + ) + end + template = get_thermal_dispatch_template_network(net) + set_device_model!(template, PSY.Line, POM.SecurityConstrainedStaticBranch) + set_device_model!( + template, + PSY.TwoWindingTransformer, + POM.SecurityConstrainedStaticBranch, + ) + return template +end + +@testset "PTDF and MODF share one factorization core" begin + sys = PSB.build_system(PSITestSystems, "c_sys5") + net = NetworkModel(POM.PTDFNetworkModel) + template = _sc_template_with_outages(sys, net) + model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + + nm = get_network_model(get_template(model)) + ptdf = IOM.get_network_matrix(nm) + modf = IOM.get_contingency_matrix(nm) + # Object identity, not equality: PTDF and MODF must wrap the exact same + # VirtualFactorCore. Two independently-built cores would be `==`-comparable + # in their reduction data but `!==`, which is exactly the regression this + # pins: it fails only if `_assemble_ptdf_data` stops deriving the MODF from + # the core it handed the PTDF. + @test PNM.get_core(ptdf) === PNM.get_core(modf) +end + +@testset "Template outages are registered on the derived MODF" begin + # c_sys14 (not c_sys5): needs a branch type the template's SC models do + # NOT cover, to build a discriminating negative case. TwoWindingTransformer + # stays on the template's default StaticBranch (non-SC) below, while Line + # is elevated to SC — c_sys5 has no TwoWindingTransformer instances at all. + sys = PSB.build_system(PSITestSystems, "c_sys14") + all_lines = collect(get_components(PSY.Line, sys)) + for line_name in ["Line1", "Line2", "Line3"] + line = get_component(PSY.Line, sys, line_name) + PSY.add_supplemental_attribute!( + sys, + line, + PSY.GeometricDistributionForcedOutage(; + mean_time_to_recovery = 10, + outage_transition_probability = 0.9999, + monitored_components = all_lines, + ), + ) + end + # Attached to a TwoWindingTransformer, whose DeviceModel is plain + # StaticBranch (not SC) below — no SC model can claim this outage, so + # template-scoped registration must exclude it while unconditional + # (`automatically_register_outages = true`) would register it regardless + # of DeviceModel coverage. This is the discriminating negative case. + uncovered_transformer = get_component(PSY.TwoWindingTransformer, sys, "Trans1") + uncovered_outage = PSY.GeometricDistributionForcedOutage(; + mean_time_to_recovery = 10, + outage_transition_probability = 0.9999, + monitored_components = all_lines, + ) + PSY.add_supplemental_attribute!(sys, uncovered_transformer, uncovered_outage) + + template = get_thermal_dispatch_template_network(NetworkModel(POM.PTDFNetworkModel)) + set_device_model!(template, PSY.Line, POM.SecurityConstrainedStaticBranch) + + model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + + nm = get_network_model(get_template(model)) + registered = PNM.get_registered_contingencies(IOM.get_contingency_matrix(nm)) + @test !isempty(registered) + + branch_models = IOM.get_branch_models(get_template(model)) + n_checked = 0 + for m in values(branch_models) + IOM.supports_outages(IOM.get_formulation(m)) || continue + # Falsifiable: if template-scoped registration were dropped (e.g. back to + # unconditional `automatically_register_outages = true`, or to no + # registration at all), this would only hold by accident on a system where + # every outage happens to already be registered — here it is a genuine + # subset check against the template's own outage keys. + @test issubset(keys(IOM.get_outages(m)), keys(registered)) + n_checked += 1 + end + # At least one outage-aware branch model must actually exist, or the loop + # above would vacuously pass with zero iterations. + @test n_checked > 0 + + # The discriminating assertion: an outage attached to a component no SC + # model covers must never reach the registry. + @test !haskey(registered, IS.get_uuid(uncovered_outage)) +end + +_zibr(rt) = POM.PNM.ZeroImpedanceBranchReduction(; + resistance_tolerance = rt, + susceptance_threshold = 0.0, +) +_reductions(y) = POM.PNM.get_reductions(POM.PNM.get_network_reduction_data(y)) +_bus_map(y) = POM.PNM.get_bus_reduction_map(POM.PNM.get_network_reduction_data(y)) + +@testset "ZeroImpedanceBranchReduction is accepted in the spec and reaches the Ybus" begin + sys = PSB.build_system(PSITestSystems, "case11_network_reductions") + # PNM rejects a ZeroImpedanceBranchReduction inside `network_reductions`. + zibr = _zibr(0.002) + ybus = POM._build_ybus(POM.NetworkReductionSpec(zibr), sys, Int[]) + @test POM.PNM.get_zero_impedance_reduction(_reductions(ybus)) == zibr +end + +@testset "The zero-impedance setting selects, rather than merely toggling" begin + sys = PSB.build_system(PSITestSystems, "case11_network_reductions") + n(rt) = + length(_bus_map(POM._build_ybus(POM.NetworkReductionSpec(_zibr(rt)), sys, Int[]))) + + # The smallest branch resistance here is 0.00064, and the default tolerance is 0.0, so + # the default and 0.0005 must both merge nothing while 0.002 merges only some. + @test length(_bus_map(POM._build_ybus(POM.NetworkReductionSpec(), sys, Int[]))) == 11 + @test n(0.0005) == 11 + @test n(0.002) == 7 +end + +@testset "More than one ZeroImpedanceBranchReduction is rejected" begin + sys = PSB.build_system(PSITestSystems, "case11_network_reductions") + spec = POM.NetworkReductionSpec(_zibr(0.001), _zibr(0.002)) + @test_throws IS.ConflictingInputsError POM._build_ybus(spec, sys, Int[]) +end + +@testset "The zero-impedance entry composes with other reductions, in either order" begin + sys = PSB.build_system(PSITestSystems, "case11_network_reductions") + zibr = _zibr(0.001) + radial = POM.PNM.RadialReduction() + first_ybus = POM._build_ybus(POM.NetworkReductionSpec(zibr, radial), sys, Int[]) + last_ybus = POM._build_ybus(POM.NetworkReductionSpec(radial, zibr), sys, Int[]) + + for y in (first_ybus, last_ybus) + @test POM.PNM.has_radial_reduction(_reductions(y)) + @test POM.PNM.get_zero_impedance_reduction(_reductions(y)) == zibr + end + @test _bus_map(first_ybus) == _bus_map(last_ybus) +end + +@testset "A non-default zero-impedance setting round-trips through a prebuilt source" begin + sys = PSB.build_system(PSITestSystems, "case11_network_reductions") + zibr = _zibr(0.002) + ybus = POM._build_ybus(POM.NetworkReductionSpec(zibr), sys, Int[]) + core = POM.PNM.VirtualFactorCore( + ybus; + tol = POM.PTDF_ZERO_TOL, + system_uuid = IS.get_uuid(sys), + ) + source = POM.PrebuiltCoreSource(core) + + @test zibr in POM._source_reductions(source) + + rebuilt = POM._source_ybus(source, sys, Int[]) + @test _bus_map(rebuilt) == _bus_map(core) +end + +# AreaBalanceNetworkModel requires a system with Areas; CopperPlate does not. +_aggregated_case(::Type{POM.CopperPlateNetworkModel}) = + (PSB.build_system(PSITestSystems, "c_sys5"), false) +function _aggregated_case(::Type{POM.AreaBalanceNetworkModel}) + sys = PSB.build_system(PSISystems, "two_area_pjm_DA") + transform_single_time_series!(sys, Hour(24), Hour(1)) + return (sys, true) +end + +function _aggregated_template(formulation, needs_interchange) + template = get_thermal_dispatch_template_network(NetworkModel(formulation)) + if needs_interchange + set_device_model!(template, PSY.AreaInterchange, StaticBranch) + end + return template +end + +function _aggregated_template(formulation, needs_interchange, source) + template = get_thermal_dispatch_template_network( + NetworkModel(formulation; network_source = source), + ) + if needs_interchange + set_device_model!(template, PSY.AreaInterchange, StaticBranch) + end + return template +end + +@testset "Aggregated formulations reject a network source they cannot honor" begin + source = POM.NetworkReductionSpec(POM.PNM.RadialReduction()) + for formulation in (POM.CopperPlateNetworkModel, POM.AreaBalanceNetworkModel) + @test !POM.honors_network_reduction(formulation) + @test_throws IS.ConflictingInputsError POM._validate_network_source( + formulation, + source, + ) + # Assert the message, not just the type: AreaBalance also throws + # ConflictingInputsError on a system with no Areas, so a type-only check could + # pass for the wrong reason. + @test_throws "computed and then ignored" POM._validate_network_source( + formulation, + source, + ) + # The default source is what these formulations always used. + @test isnothing( + POM._validate_network_source(formulation, IOM.DefaultNetworkSource()), + ) + end +end + +@testset "The aggregated guard blocks a build, and the default source still builds" begin + source = POM.NetworkReductionSpec(POM.PNM.RadialReduction()) + for formulation in (POM.CopperPlateNetworkModel, POM.AreaBalanceNetworkModel) + sys, needs_interchange = _aggregated_case(formulation) + + # `build!` traps the error and reports FAILED rather than propagating it. + rejected = DecisionModel( + _aggregated_template(formulation, needs_interchange, source), + sys; + optimizer = HiGHS_optimizer, + ) + @test build!(rejected; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.FAILED + + accepted = DecisionModel( + _aggregated_template(formulation, needs_interchange), + sys; + optimizer = HiGHS_optimizer, + ) + @test build!(accepted; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT + end +end + +@testset "Bus-level formulations still accept a network source" begin + sys = PSB.build_system(PSITestSystems, "c_sys5") + # Falsifiable: if the guard keyed on something broader than the trait, this would throw. + @test POM.honors_network_reduction(POM.PTDFNetworkModel) + @test POM.honors_network_reduction(POM.DCPNetworkModel) + net = NetworkModel( + POM.PTDFNetworkModel; + network_source = POM.NetworkReductionSpec(POM.PNM.RadialReduction()), + ) + template = get_thermal_dispatch_template_network(net) + model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) + @test build!(model; output_dir = mktempdir(; cleanup = true)) == + IOM.ModelBuildStatus.BUILT +end diff --git a/test/test_parallel_branch_parameter_multipliers.jl b/test/test_parallel_branch_parameter_multipliers.jl index cc9f72f7..3f265bf0 100644 --- a/test/test_parallel_branch_parameter_multipliers.jl +++ b/test/test_parallel_branch_parameter_multipliers.jl @@ -31,7 +31,7 @@ ) template = get_thermal_dispatch_template_network( - NetworkModel(PTDFNetworkModel; network_matrix = PTDF(sys)), + NetworkModel(PTDFNetworkModel), ) set_device_model!(template, line_device_model) ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @@ -87,7 +87,7 @@ end ) template = get_thermal_dispatch_template_network( - NetworkModel(PTDFNetworkModel; network_matrix = PTDF(sys)), + NetworkModel(PTDFNetworkModel), ) set_device_model!(template, line_device_model) ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) @@ -129,7 +129,7 @@ end Dict{String, Any}(POM.PARALLEL_BRANCH_MAX_RATING_KEY => method) end template = get_thermal_dispatch_template_network( - NetworkModel(PTDFNetworkModel; network_matrix = PTDF(sys)), + NetworkModel(PTDFNetworkModel), ) set_device_model!(template, DeviceModel(Line, StaticBranch; attributes = attrs)) ps_model = DecisionModel(template, sys; optimizer = HiGHS_optimizer) diff --git a/test/test_postcontingency_mixed_outage_axes.jl b/test/test_postcontingency_mixed_outage_axes.jl index f2647d66..0fc9a1c0 100644 --- a/test/test_postcontingency_mixed_outage_axes.jl +++ b/test/test_postcontingency_mixed_outage_axes.jl @@ -1,7 +1,7 @@ # Ported from PowerSimulations.jl PR #1579 (MODF SCUC migration), adapted to # POM / PS6 APIs. Exercises the Phase-4 `_build_device_model_outages!` planned # vs. unplanned outage-axis selection and the -# `_add_outage_monitored_irreducible_buses!` bus-pinning (N3) logic. +# `_pin_outage_buses!` bus-pinning (N3) logic. # # Adaptation notes: # * internal symbols are namespaced `POM.` / `IOM.` / `PNM.`; expression and @@ -55,7 +55,6 @@ NetworkModel( PTDFNetworkModel; use_slacks = false, - contingency_matrix = PNM.VirtualMODF(sys), ), ) set_device_model!( @@ -132,7 +131,7 @@ end @testset "Outage pinning includes outaged-component buses (N3)" begin - # Regression: `_add_outage_monitored_irreducible_buses!` must pin both the + # Regression: `_pin_outage_buses!` must pin both the # MONITORED components' buses AND the OUTAGED (associated) components' # buses. If only the monitored set is pinned, a degree-two reduction # between the outaged arc's endpoints can collapse the contingency arc out @@ -163,8 +162,10 @@ end ) branch_models[nameof(PSY.Line)] = dm - irreducible_buses = Set{Int64}() - POM._add_outage_monitored_irreducible_buses!(irreducible_buses, sys, branch_models) + irreducible_buses = Set{Int}() + for m in values(branch_models) + POM._pin_outage_buses!(irreducible_buses, m, sys) + end monitored_arc = PSY.get_arc(monitored_only_line) outaged_arc = PSY.get_arc(outaged_line) @@ -199,11 +200,13 @@ end branch_models = IOM.BranchModelContainer() dm = DeviceModel(PSY.Line, POM.StaticBranch) # Even if an outage dict were present, StaticBranch is not outage-aware so - # `_add_outage_monitored_irreducible_buses!` skips it. + # `_pin_outage_buses!` skips it. branch_models[nameof(PSY.Line)] = dm - irreducible_buses = Set{Int64}() - POM._add_outage_monitored_irreducible_buses!(irreducible_buses, sys, branch_models) + irreducible_buses = Set{Int}() + for m in values(branch_models) + POM._pin_outage_buses!(irreducible_buses, m, sys) + end @test isempty(irreducible_buses) end diff --git a/test/test_power_flow_in_the_loop.jl b/test/test_power_flow_in_the_loop.jl index 1f71fef7..917570ca 100644 --- a/test/test_power_flow_in_the_loop.jl +++ b/test/test_power_flow_in_the_loop.jl @@ -4,7 +4,6 @@ template = get_template_dispatch_with_network( NetworkModel( PTDFNetworkModel; - network_matrix = PTDF(system), evaluations = power_flow_evaluations( ACPowerFlow(; distribute_slack_proportional_to_headroom = true, @@ -87,7 +86,6 @@ end template = get_template_dispatch_with_network( NetworkModel( PTDFNetworkModel; - network_matrix = PTDF(system), use_slacks = true, evaluations = power_flow_evaluations( ACPowerFlow(; @@ -132,7 +130,6 @@ end template = get_template_dispatch_with_network( NetworkModel( PTDFNetworkModel; - network_matrix = PTDF(system), use_slacks = true, evaluations = power_flow_evaluations( ACPowerFlow(; @@ -284,7 +281,6 @@ end template = get_template_dispatch_with_network( NetworkModel( PTDFNetworkModel; - network_matrix = PTDF(system), evaluations = power_flow_evaluations(ACPowerFlow()), ), ) @@ -339,7 +335,6 @@ end template = get_template_dispatch_with_network( NetworkModel( PTDFNetworkModel; - network_matrix = PTDF(system), evaluations = power_flow_evaluations(ACPowerFlow()), ), ) @@ -377,7 +372,6 @@ end template = get_template_dispatch_with_network( NetworkModel( PTDFNetworkModel; - network_matrix = PTDF(system), evaluations = power_flow_evaluations(ACPowerFlow()), ), ) @@ -408,7 +402,6 @@ end template = get_template_dispatch_with_network( NetworkModel( PTDFNetworkModel; - network_matrix = PTDF(system), evaluations = power_flow_evaluations(ACPowerFlow()), ), ) @@ -447,7 +440,6 @@ end template = get_template_dispatch_with_network( NetworkModel( PTDFNetworkModel; - network_matrix = PTDF(system), use_slacks = true, evaluations = power_flow_evaluations(DCPowerFlow()), ), @@ -483,7 +475,6 @@ end template = get_template_dispatch_with_network( NetworkModel( PTDFNetworkModel; - network_matrix = PTDF(system), use_slacks = true, evaluations = power_flow_evaluations(ACPowerFlow()), ), @@ -703,7 +694,6 @@ end template = get_template_dispatch_with_network( NetworkModel( PTDFNetworkModel; - network_matrix = PTDF(system), evaluations = power_flow_evaluations(pf_eval), ), ) diff --git a/test/test_services_constructor.jl b/test/test_services_constructor.jl index e94a6053..84863e83 100644 --- a/test/test_services_constructor.jl +++ b/test/test_services_constructor.jl @@ -1133,6 +1133,12 @@ end end end +# Which kind of reduction entry a branch resolved to. Dispatched rather than type-checked +# so a new PNM entry kind is a missing method instead of a silently wrong branch. +_reduced_entry_kind(::PSY.ACTransmission) = :single +_reduced_entry_kind(::PNM.BranchesSeries) = :series +_reduced_entry_kind(::PNM.BranchesParallel) = :parallel + @testset "Test bad data for interfaces with reductions" begin sys_rts_da = build_system(PSISystems, "modified_RTS_GMLC_DA_sys") transform_single_time_series!(sys_rts_da, Hour(24), Hour(1)) @@ -1157,15 +1163,31 @@ end violation_penalty = 1000.0, direction_mapping = Dict("CA-1" => -1, "C35" => -1), ) - # Order matters: compute the ptdf before adding the service so the interface lines - # are reduced (to test the bad-data checking). - ptdf = PTDF(sys_rts_da; network_reductions = NetworkReduction[DegreeTwoReduction()]) + #= + Built before the series interface exists, so its degree-two reduction merges "CA-1" + and "C35" into one BranchesSeries entry. That merged-chain-inside-an-interface state + is what makes conflicting per-segment direction data ambiguous, and it is the ONLY + state the series-chain direction validations can fire from. + + A build-time reduction can no longer produce it: PNM's DegreeTwoReduction protects the + arc buses of every TransmissionInterface contributing device, so the bus the chain + would eliminate is pinned and both lines stay separate direct entries. Two independent + contributors with opposite signs is valid data, so there is nothing left to reject. + The state is only reachable from a reduction that predates the interface, which is + what this matrix is kept for — and that route is rejected by the source's own + reduction-reproducibility guard. Do not "restore" a `build!`-returns-FAILED assertion + on the spec source below without first removing that PNM protection. + =# + pre_service_ptdf = PNM.VirtualPTDF( + sys_rts_da; + tol = POM.PTDF_ZERO_TOL, + network_reductions = PNM.NetworkReduction[PNM.DegreeTwoReduction()], + ) add_service!(sys_rts_da, interface_series_chain, [series_chain_1, series_chain_2]) template = PowerOperationsProblemTemplate( NetworkModel( AreaPTDFNetworkModel; - network_matrix = ptdf, - reduce_degree_two_branches = true, + network_source = NetworkReductionSpec(PNM.DegreeTwoReduction()), use_slacks = true, ), ) @@ -1183,6 +1205,7 @@ end template, ServiceModel(TransmissionInterface, ConstantMaxInterfaceFlow), ) + spec_network_model = get_network_model(template) ps_model = DecisionModel( template, sys_rts_da; @@ -1193,8 +1216,24 @@ end @test build!(ps_model; output_dir = mktempdir(; cleanup = true)) == IOM.ModelBuildStatus.BUILT - # Bad direction data for interface on series chain: + # Bad direction data for interface on series chain: only reachable from a reduction + # that predates the interface, and that reduction is now rejected outright because it + # describes a network the system no longer has. PSY.set_direction_mapping!(interface_series_chain, Dict("CA-1" => 1, "C35" => -1)) + stale_source = PrebuiltMatrixSource(pre_service_ptdf) + @test_throws IS.ConflictingInputsError POM._source_ybus( + stale_source, + sys_rts_da, + Int[], + ) + set_network_model!( + template, + NetworkModel( + AreaPTDFNetworkModel; + network_source = stale_source, + use_slacks = true, + ), + ) ps_model = DecisionModel( template, sys_rts_da; @@ -1207,6 +1246,7 @@ end console_level = Logging.AboveMaxLevel, output_dir = mktempdir(; cleanup = true), ) == IOM.ModelBuildStatus.FAILED + set_network_model!(template, spec_network_model) # Bad direction data for interface on double circuit: PSY.set_direction_mapping!(interface_series_chain, Dict("CA-1" => 1, "C35" => 1)) @@ -1240,21 +1280,26 @@ end output_dir = mktempdir(; cleanup = true), ) == IOM.ModelBuildStatus.FAILED - # Only including part of a series chain in an interface: + # Only including part of a series chain in an interface: there is no merged chain to + # partially specify, because the remaining contributor's endpoints pin the bus the + # merge would eliminate. Asserting that structural fact — rather than a rejection that + # can no longer fire — is what keeps the requirement covered. push!(PSY.get_services(double_circuit_1), interface_double_circuit) pop!(PSY.get_services(series_chain_1)) - ps_model = DecisionModel( - template, - sys_rts_da; - resolution = Hour(1), - optimizer = HiGHS_optimizer, - store_variable_names = true, + partial_reduction = PNM.get_network_reduction_data( + PNM.Ybus( + sys_rts_da; + network_reductions = PNM.NetworkReduction[PNM.DegreeTwoReduction()], + ), ) - @test build!( - ps_model; - console_level = Logging.AboveMaxLevel, - output_dir = mktempdir(; cleanup = true), - ) == IOM.ModelBuildStatus.FAILED + PNM.populate_branch_maps_by_type!(partial_reduction) + partial_name_to_arc = PNM.get_name_to_arc_maps(partial_reduction)[Line] + partial_maps = PNM.get_all_branch_maps_by_type(partial_reduction) + for name in ("CA-1", "C35") + arc, bucket = partial_name_to_arc[name] + @test bucket == "direct_branch_map" + @test _reduced_entry_kind(partial_maps[bucket][Line][arc]) == :single + end end @testset "GroupReserve requirement sums only its contributing services" begin diff --git a/test/test_sos_duals_validation.jl b/test/test_sos_duals_validation.jl index 9bb8c509..e6a77d48 100644 --- a/test/test_sos_duals_validation.jl +++ b/test/test_sos_duals_validation.jl @@ -59,7 +59,6 @@ end template = PowerOperationsProblemTemplate( NetworkModel( PTDFNetworkModel; - network_matrix = PTDF(sys), duals = [CopperPlateBalanceConstraint], ), ) diff --git a/test/test_utils/mock_operation_models.jl b/test/test_utils/mock_operation_models.jl index 8b6380cb..802f3399 100644 --- a/test/test_utils/mock_operation_models.jl +++ b/test/test_utils/mock_operation_models.jl @@ -159,8 +159,14 @@ function mock_construct_devices!( # Mirror the real build (`build_problem!`): device `add_to_expression!` maps every # bus through `get_mapped_bus_number(network_reduction, ...)`, so the network model # must carry a reduction. The mock applies none, so use the identity reduction. - IOM.get_network_model(template).network_reduction = - PNM.get_network_reduction_data(PNM.Ybus(IOM.get_system(problem))) + mock_ybus = PNM.Ybus(IOM.get_system(problem)) + IOM.set_network_data!( + IOM.get_network_model(template), + POM.YbusNetworkData( + mock_ybus, + PNM.get_network_reduction_data(mock_ybus), + ), + ) IOM.get_optimization_container(problem).built_for_recurrent_solves = built_for_recurrent_solves POM.initialize_system_expressions!(