From 5f4c844c04c215919a711acad3bf1fd3212362ea Mon Sep 17 00:00:00 2001 From: Jose Daniel Lara Date: Sat, 30 May 2026 22:09:50 +0100 Subject: [PATCH] use the helpers to reduce reactive power --- src/AdjacencyMatrix.jl | 8 +- src/degree_two_reduction.jl | 7 +- src/reduction_helpers.jl | 18 +++- test/test_degree_two_reactive_injectors.jl | 102 +++++++++++++++++++++ 4 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 test/test_degree_two_reactive_injectors.jl diff --git a/src/AdjacencyMatrix.jl b/src/AdjacencyMatrix.jl index 4639be7aa..28efbd06a 100644 --- a/src/AdjacencyMatrix.jl +++ b/src/AdjacencyMatrix.jl @@ -68,7 +68,13 @@ function get_reduction( get_user_irreducible_buses(get_reductions(network_reduction_data)) validate_buses(A, user_irreducible) working_set = Set{Int}(user_irreducible) - union!(working_set, _system_derived_irreducible_buses(sys)) + union!( + working_set, + _system_derived_irreducible_buses( + sys, + get_reduce_reactive_power_injectors(reduction), + ), + ) exempt_bus_positions = Set(get_irreducible_indices(A, collect(working_set))) diff --git a/src/degree_two_reduction.jl b/src/degree_two_reduction.jl index 63061886f..04662fb9e 100644 --- a/src/degree_two_reduction.jl +++ b/src/degree_two_reduction.jl @@ -7,7 +7,12 @@ and `TransmissionInterface` endpoints) on top of any user set passed via `Ybus(sys; irreducible_buses=...)`. # Fields -- `reduce_reactive_power_injectors::Bool = true` +- `reduce_reactive_power_injectors::Bool = true`: when `true`, buses whose only + injectors support reactive power (e.g. a `SynchronousCondenser`, or a purely + susceptive `FixedAdmittance`) are treated as reduction candidates. When `false`, + such reactive-only injector hosts are kept. Buses hosting an active-power + injector are always kept. Capability is read from the PowerSystems + `supports_active_power` / `supports_reactive_power` traits. """ @kwdef struct DegreeTwoReduction <: NetworkReduction reduce_reactive_power_injectors::Bool = true diff --git a/src/reduction_helpers.jl b/src/reduction_helpers.jl index e06a117ae..35447d7bc 100644 --- a/src/reduction_helpers.jl +++ b/src/reduction_helpers.jl @@ -53,16 +53,30 @@ function _add_arc_buses!(buses::Set{Int}, br::PSY.ThreeWindingTransformer) end """ - _system_derived_irreducible_buses(sys) -> Set{Int} + _system_derived_irreducible_buses(sys, reduce_reactive_power_injectors) -> Set{Int} Fresh `Set{Int}` of buses the system requires kept: `StaticInjection` hosts, `TwoTerminalHVDC` endpoints, cross-area `ACTransmission` endpoints (when any `AreaInterchange` exists), and branch-typed `TransmissionInterface` contributors. + +A `StaticInjection` host is kept when the injector supports active power. When +`reduce_reactive_power_injectors` is `false`, reactive-power injectors also keep +their host bus; when `true` (the default), buses whose injectors only support +reactive power become reduction candidates. The non-injection protections are +unconditional. """ -function _system_derived_irreducible_buses(sys::PSY.System) +function _system_derived_irreducible_buses( + sys::PSY.System, + reduce_reactive_power_injectors::Bool, +) buses = Set{Int}() for c in PSY.get_components(PSY.StaticInjection, sys) PSY.get_available(c) || continue + # An injector keeps its bus if it supports active power, or (when reactive + # injectors are not being reduced) reactive power. + forces_keep = PSY.supports_active_power(c) || + (!reduce_reactive_power_injectors && PSY.supports_reactive_power(c)) + forces_keep || continue bus = PSY.get_bus(c) PSY.get_available(bus) || continue push!(buses, PSY.get_number(bus)) diff --git a/test/test_degree_two_reactive_injectors.jl b/test/test_degree_two_reactive_injectors.jl new file mode 100644 index 000000000..04d901984 --- /dev/null +++ b/test/test_degree_two_reactive_injectors.jl @@ -0,0 +1,102 @@ +# Build a standalone 4-bus chain (1-2-3-4). Buses 1 and 4 host active-power +# injectors; bus 2 hosts only a reactive SynchronousCondenser; bus 3 is bare. +# This isolates the reactive-only gating decision in the degree-2 reduction. +function _build_reactive_only_degree2_system() + sys = PSY.System(100.0) + buses = ACBus[] + for i in 1:4 + if i == 1 + bustype = ACBusTypes.REF + else + bustype = ACBusTypes.PV + end + bus = ACBus(; + number = i, + name = "Bus $i", + available = true, + bustype = bustype, + angle = 0.0, + magnitude = 1.0, + voltage_limits = (min = 0.9, max = 1.05), + base_voltage = 138.0, + ) + add_component!(sys, bus) + push!(buses, bus) + end + for (i, (b_from, b_to)) in enumerate([(1, 2), (2, 3), (3, 4)]) + line = Line(; + name = "Line $i", + available = true, + active_power_flow = 0.0, + reactive_power_flow = 0.0, + arc = Arc(; from = buses[b_from], to = buses[b_to]), + r = 0.01, + x = 0.1, + b = (from = 0.0, to = 0.0), + rating = 2.0, + angle_limits = (min = -1.5, max = 1.5), + ) + add_component!(sys, line) + end + for (bus, name) in [(buses[1], "Gen 1"), (buses[4], "Gen 4")] + gen = ThermalStandard(; + name = name, + available = true, + status = true, + bus = bus, + active_power = 0.0, + reactive_power = 0.0, + rating = 1.0, + active_power_limits = (min = 0.0, max = 1.0), + reactive_power_limits = nothing, + ramp_limits = (up = 1.0, down = 1.0), + operation_cost = ThermalGenerationCost(nothing), + base_power = 100.0, + time_limits = (up = 1.0, down = 1.0), + must_run = false, + prime_mover_type = PrimeMovers.CC, + fuel = ThermalFuels.NATURAL_GAS, + ) + add_component!(sys, gen) + end + condenser = SynchronousCondenser(; + name = "Cond 2", + available = true, + bus = buses[2], + reactive_power = 0.0, + rating = 1.0, + reactive_power_limits = (min = -1.0, max = 1.0), + base_power = 100.0, + ) + add_component!(sys, condenser) + return sys +end + +@testset "Degree-2 reactive-injector gating: irreducible-bus set" begin + sys = _build_reactive_only_degree2_system() + + kept_reduce = PNM._system_derived_irreducible_buses(sys, true) + @test 1 in kept_reduce + @test 4 in kept_reduce + @test !(2 in kept_reduce) # condenser-only bus dropped when reducing reactive + @test !(3 in kept_reduce) # bare bus never kept + + kept_keep = PNM._system_derived_irreducible_buses(sys, false) + @test 1 in kept_keep + @test 4 in kept_keep + @test 2 in kept_keep # condenser bus retained when keeping reactive + @test !(3 in kept_keep) +end + +@testset "Degree-2 reactive-injector gating: get_reduction wiring" begin + sys = _build_reactive_only_degree2_system() + adj = AdjacencyMatrix(sys) + + reduced = PNM.get_reduction(adj, sys, DegreeTwoReduction()) # default reduce = true + @test !(2 in reduced.irreducible_buses) # condenser bus is a reduction candidate + @test 2 in reduced.removed_buses # and is actually folded out of the chain + + kept = PNM.get_reduction(adj, sys, DegreeTwoReduction(; reduce_reactive_power_injectors = false)) + @test 2 in kept.irreducible_buses # condenser bus retained + @test !(2 in kept.removed_buses) # and not folded +end