From abdcb5932f92c94f79238ddfc2ba04ffc7d5b857 Mon Sep 17 00:00:00 2001 From: mvelasqu Date: Fri, 11 Sep 2026 20:03:59 -0600 Subject: [PATCH 1/5] fix: resolve correct reduction root for vsc components --- src/apply_zero_impedance_reduction.jl | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/apply_zero_impedance_reduction.jl b/src/apply_zero_impedance_reduction.jl index 884dedf21..490945804 100644 --- a/src/apply_zero_impedance_reduction.jl +++ b/src/apply_zero_impedance_reduction.jl @@ -69,6 +69,16 @@ function _is_zero_impedance_arc( ) end +function _find_reduction_root(bus_number, reverse_bus_search_map) + root = bus_number + while haskey(reverse_bus_search_map, root) + next_root = reverse_bus_search_map[root] + next_root == root && break + root = next_root + end + return root +end + function get_reduction( ybus::Ybus, sys::PSY.System, @@ -91,14 +101,20 @@ function get_reduction( _is_zero_impedance_arc( br, susceptance_threshold, min_x_eps, resistance_tolerance) || continue from_no, to_no = arc_key - from_irred = from_no ∈ user_irreducible - to_irred = to_no ∈ user_irreducible - if from_irred && to_irred - @warn "Zero-impedance branch between two irreducible buses $from_no and $to_no; skipping merge." + from_root = _find_reduction_root(from_no, nr.reverse_bus_search_map) + to_root = _find_reduction_root(to_no, nr.reverse_bus_search_map) + from_irred = from_root ∈ user_irreducible + to_irred = to_root ∈ user_irreducible + if from_root == to_root + continue + elseif from_irred && to_irred + @warn "Zero-impedance branch between two irreducible bus groups $from_no and $to_no; skipping merge." continue elseif to_irred # Flip so the irreducible bus survives. - from_no, to_no = to_no, from_no + from_no, to_no = to_root, from_root + else + from_no, to_no = from_root, to_root end _update_bus_maps!( From 416d1c589d353e4ac14108d5e709baa371921e59 Mon Sep 17 00:00:00 2001 From: mvelasqu Date: Mon, 14 Sep 2026 10:15:41 -0600 Subject: [PATCH 2/5] chore: use existing get() function for root bus finding --- src/apply_zero_impedance_reduction.jl | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/apply_zero_impedance_reduction.jl b/src/apply_zero_impedance_reduction.jl index 490945804..ccecd5818 100644 --- a/src/apply_zero_impedance_reduction.jl +++ b/src/apply_zero_impedance_reduction.jl @@ -69,16 +69,6 @@ function _is_zero_impedance_arc( ) end -function _find_reduction_root(bus_number, reverse_bus_search_map) - root = bus_number - while haskey(reverse_bus_search_map, root) - next_root = reverse_bus_search_map[root] - next_root == root && break - root = next_root - end - return root -end - function get_reduction( ybus::Ybus, sys::PSY.System, @@ -101,8 +91,8 @@ function get_reduction( _is_zero_impedance_arc( br, susceptance_threshold, min_x_eps, resistance_tolerance) || continue from_no, to_no = arc_key - from_root = _find_reduction_root(from_no, nr.reverse_bus_search_map) - to_root = _find_reduction_root(to_no, nr.reverse_bus_search_map) + from_root = get(nr.reverse_bus_search_map, from_no, from_no) + to_root = get(nr.reverse_bus_search_map, to_no, to_no) from_irred = from_root ∈ user_irreducible to_irred = to_root ∈ user_irreducible if from_root == to_root From 64fe5fd47cdc20ee4990c7fb1f256c960a6d1d6d Mon Sep 17 00:00:00 2001 From: mvelasqu Date: Mon, 14 Sep 2026 11:36:46 -0600 Subject: [PATCH 3/5] fix: ensure arcs with same merged bus endpoints are removed from downstream arc data --- src/apply_zero_impedance_reduction.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/apply_zero_impedance_reduction.jl b/src/apply_zero_impedance_reduction.jl index ccecd5818..32ea5e5c7 100644 --- a/src/apply_zero_impedance_reduction.jl +++ b/src/apply_zero_impedance_reduction.jl @@ -96,6 +96,8 @@ function get_reduction( from_irred = from_root ∈ user_irreducible to_irred = to_root ∈ user_irreducible if from_root == to_root + # Both endpoints are already in one merged group, so this arc is a self-loop. + push!(nr.removed_arcs, arc_key) continue elseif from_irred && to_irred @warn "Zero-impedance branch between two irreducible bus groups $from_no and $to_no; skipping merge." From 66d86f054368dd76960703762cc7a435602c4455 Mon Sep 17 00:00:00 2001 From: mvelasqu Date: Mon, 14 Sep 2026 11:47:19 -0600 Subject: [PATCH 4/5] test: include z-i branch reduction test when closing arc is dropped --- test/test_ybus_reductions.jl | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/test_ybus_reductions.jl b/test/test_ybus_reductions.jl index 863309be7..69147033f 100644 --- a/test/test_ybus_reductions.jl +++ b/test/test_ybus_reductions.jl @@ -334,6 +334,31 @@ end @test length(PNM.get_bus_axis(ybus)) == 14 - 2 end +@testset "ZeroImpedanceBranchReduction: zero-impedance cycle drops its closing arc" begin + # Line3 (2-3), Line6 (3-4) and Line4 (2-4) form a zero-impedance triangle. Whichever + # arc the map iteration reaches last has both endpoints already in one merged group: + # it is a self-loop, not a skipped merge, so it must still land in removed_arcs or it + # survives in the subnetwork arc axis on a bus that is no longer on the bus axis. + for pinned in (Set{Int}(), Set([4])) + sys = PSB.build_system(PSB.PSITestSystems, "c_sys14") + for name in ("Line3", "Line6", "Line4") + line = get_component(Line, sys, name) + set_r!(line, 0.0) + set_x!(line, 0.0) + end + ybus = Ybus(sys; irreducible_buses = pinned) + nrd = get_network_reduction_data(ybus) + bus_ax = Set(PNM.get_bus_axis(ybus)) + @test Set([(2, 3), (3, 4), (2, 4)]) ⊆ PNM.get_removed_arcs(nrd) + for (_, arcs) in ybus.arc_subnetwork_axis, arc in arcs + @test arc ∉ ((2, 3), (3, 4), (2, 4)) + end + for arc in PNM.get_arc_axis(nrd), bus in arc + @test bus ∈ bus_ax + end + end +end + @testset "ZeroImpedanceBranchReduction: transformer arcs are excluded" begin sys = PSB.build_system(PSB.PSITestSystems, "c_sys14") t = get_component(Transformer2W, sys, "Trans4") # from=7, to=8 From 0b06df61e531569dc752695432677cf23afe88d7 Mon Sep 17 00:00:00 2001 From: mvelasqu Date: Mon, 14 Sep 2026 11:59:12 -0600 Subject: [PATCH 5/5] test: pinned bus survives a chained merge from z-i branch reduction --- test/test_ybus_reductions.jl | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/test_ybus_reductions.jl b/test/test_ybus_reductions.jl index 69147033f..cdcd4276b 100644 --- a/test/test_ybus_reductions.jl +++ b/test/test_ybus_reductions.jl @@ -418,6 +418,40 @@ end @test 113 ∈ PNM.get_bus_axis(ybus_skip) end +@testset "ZeroImpedanceBranchReduction: pinned bus survives a chained merge" begin + # Zero-impedance chain 2 -> 3 -> 4 (Line3, Line6). Bus 4 is pinned, so whichever + # arc the map iteration reaches first, the merged group must collapse onto bus 4. + sys = PSB.build_system(PSB.PSITestSystems, "c_sys14") + for name in ("Line3", "Line6") + line = get_component(Line, sys, name) + set_r!(line, 0.0) + set_x!(line, 0.0) + end + ybus = Ybus(sys; irreducible_buses = Set([4])) + nrd = get_network_reduction_data(ybus) + @test 4 ∈ PNM.get_bus_axis(ybus) + @test 4 ∉ keys(nrd.reverse_bus_search_map) + @test get(nrd.reverse_bus_search_map, 2, nothing) == 4 + @test get(nrd.reverse_bus_search_map, 3, nothing) == 4 + @test get(nrd.bus_reduction_map, 4, nothing) == Set([2, 3]) + + # Both ends of the chain pinned: the collision only appears once bus 3 resolves to + # bus 2, so the skip must be decided on the resolved roots, not the raw arc numbers. + sys2 = PSB.build_system(PSB.PSITestSystems, "c_sys14") + for name in ("Line3", "Line6") + line = get_component(Line, sys2, name) + set_r!(line, 0.0) + set_x!(line, 0.0) + end + ybus2 = Ybus(sys2; irreducible_buses = Set([2, 4])) + nrd2 = get_network_reduction_data(ybus2) + @test 2 ∈ PNM.get_bus_axis(ybus2) + @test 4 ∈ PNM.get_bus_axis(ybus2) + @test 3 ∉ PNM.get_bus_axis(ybus2) + @test 2 ∉ keys(nrd2.reverse_bus_search_map) + @test 4 ∉ keys(nrd2.reverse_bus_search_map) +end + @testset "ZeroImpedanceBranchReduction: custom susceptance_threshold" begin # Line3 (bus 2 → 3) gets r=0 and a reactance giving |imag(Y)| ≈ 1e3, below the # default 1e4 threshold, so it is NOT merged by default. A custom, lower threshold