From 8d5da47c2f74dd6ba1a3f948e4d4f2a155954010 Mon Sep 17 00:00:00 2001 From: Luke Kiernan Date: Fri, 4 Sep 2026 14:28:40 -0600 Subject: [PATCH] fix: keep a zero-impedance TRANSFORMER classified as a transformer Port of PowerSystems.jl 00003f06d. _branch_type_psse checked for zero r/x before checking the transformer flag, so a TRANSFORMER record with R1-2 = X1-2 = 0 was routed to make_switch_from_zero_impedance_branch! and became a DiscreteControlledACBranch. It also slipped past the "transformer=true but detected as a Line" guard, which only catches :line. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01HmGuSdKu4g7a3uqgPXZ4NC --- src/openapi/branch.jl | 4 +++- test/test_openapi_branch.jl | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/openapi/branch.jl b/src/openapi/branch.jl index 8c39fd8..5524d8a 100644 --- a/src/openapi/branch.jl +++ b/src/openapi/branch.jl @@ -370,7 +370,9 @@ function _branch_type_matpower(d::Dict) end function _branch_type_psse(d::Dict, name::AbstractString) - if iszero(d["br_r"]) && iszero(d["br_x"]) + # A zero-impedance TRANSFORMER record is still a transformer: only a plain branch + # with no impedance is the modeled switching device this shortcut is looking for. + if !d["transformer"] && iszero(d["br_r"]) && iszero(d["br_x"]) return :switch end is_transformer = d["transformer"] diff --git a/test/test_openapi_branch.jl b/test/test_openapi_branch.jl index 8352962..feda5e2 100644 --- a/test/test_openapi_branch.jl +++ b/test/test_openapi_branch.jl @@ -271,6 +271,42 @@ end (PFP.get_value(switch, :available) ? "CLOSED" : "OPEN") end +@testset "zero-impedance TRANSFORMER stays a transformer, not a switch" begin + # The zero-impedance shortcut above must not fire on a TRANSFORMER record: a + # transformer with R1-2 = X1-2 = 0 is still a transformer, and misrouting it to + # `make_switch_from_zero_impedance_branch!` also slips past the `transformer=true but + # detected as a Line` guard, which only checks for `:line`. Port of + # PowerSystems.jl 00003f06d. + data = fourteen_bus_pm_data().data + d = first(v for v in values(data["branch"]) if v["transformer"]) + @test PFP._branch_type_psse(d, "as_parsed") == :transformer + + zero_z = merge( + Dict{String, Any}(k => v for (k, v) in d), + Dict{String, Any}("br_r" => 0.0, "br_x" => 0.0, "index" => 998), + ) + @test PFP._branch_type_psse(zero_z, "zero_z_transformer") == :transformer + + synthetic = deepcopy(data) + synthetic["branch"] = Dict{String, Any}("998" => zero_z) + for key in + ("3w_transformer", "dcline", "vscline", "interarea_transfer", "shunt", + "switched_shunt", "facts") + delete!(synthetic, key) + end + + sys = PFP.OpenAPISystem(Float64(synthetic["baseMVA"])) + PFP.read_loadzones!(sys, synthetic) + PFP.read_bus!(sys, synthetic) + PFP.read_branches!(sys, synthetic) + + @test isempty(PFP.get_components(sys, "DiscreteControlledACBranch")) + circuit = only(PFP.get_components(sys, "TransformerCircuit")) + @test PFP.get_value(circuit, :r) == 0.0 + @test PFP.get_value(circuit, :x) == 0.0 + @test !isnothing(_two_winding_transformer_for(sys, circuit)) +end + @testset "TransformerCircuit.controlled_quantity_limits passes through UNSCALED under COMPONENT_BASE for a power-flow-family control_objective" begin # Regression: `controlled_quantity_limits`'s schema quantity DOES switch with # `control_objective` (pu for VOLTAGE-family objectives, MW/MVAr for ACTIVE_POWER_FLOW/