Description
When a PhaseShiftingTransformer (PST) shares an arc with a Line or Transformer2W, one of the two branches is silently dropped from all NetworkReductionData maps in add_to_branch_maps! (src/Ybus.jl). The Ybus is built correctly (both admittances are summed), but the dropped branch is never passed to set_power_flow!, leaving its flow at zero after the solve.
Root cause
add_to_branch_maps! routes each branch into direct_branch_map or parallel_branch_map. The condition that promotes two branches on the same arc to parallel_branch_map requires both to be outside SKIP_PARALLEL_REDUCTION_TYPES. When one is a PST (which is in SKIP), that condition is false, and the code falls through to the else clause, which unconditionally overwrites direct_branch_map[arc_tuple] with the incoming branch, losing the previous one.
| Step |
Branch |
Promoted to parallel? |
Result |
| 1 |
Line |
— |
direct[arc] = Line |
| 2 |
PST |
No — PST is in SKIP |
direct[arc] = PST (Line lost) |
The same displacement occurs if the PST arrives first.
Fix
Add an elseif haskey(direct_branch_map, arc_tuple) clause between condition B and the else. When the arc is already occupied and one branch is a SKIP type, move both into parallel_branch_map so neither is lost.
# existing condition B (both branches outside SKIP)
elseif haskey(direct_branch_map, arc_tuple) &&
typeof(direct_branch_map[arc_tuple]) ∉ SKIP_PARALLEL_REDUCTION_TYPES &&
typeof(br) ∉ SKIP_PARALLEL_REDUCTION_TYPES
corresponding_branch = direct_branch_map[arc_tuple]
delete!(direct_branch_map, arc_tuple)
delete!(reverse_direct_branch_map, corresponding_branch)
parallel_branch_map[arc_tuple] = BranchesParallel([corresponding_branch, br])
reverse_parallel_branch_map[corresponding_branch] = arc_tuple
reverse_parallel_branch_map[br] = arc_tuple
# NEW: one branch is a SKIP type — still store both so neither is lost
elseif haskey(direct_branch_map, arc_tuple)
corresponding_branch = direct_branch_map[arc_tuple]
delete!(direct_branch_map, arc_tuple)
delete!(reverse_direct_branch_map, corresponding_branch)
parallel_branch_map[arc_tuple] = BranchesParallel([corresponding_branch, br])
reverse_parallel_branch_map[corresponding_branch] = arc_tuple
reverse_parallel_branch_map[br] = arc_tuple
else
direct_branch_map[arc_tuple] = br
reverse_direct_branch_map[br] = arc_tuple
end
This should be sufficient because SKIP_PARALLEL_REDUCTION_TYPES only prohibits grouping a PST with another PST. A PST alongside a Line or symmetric transformer is handled correctly by _segment_flow_entry, which evaluates each branch independently using its own admittance matrix.
Impact
- Solver: unaffected — the Ybus sums all admittances regardless of the NRD maps.
- Branch flow output: broken for every displaced branch — active and reactive power flows are incorrect.
- Affected systems: any system with a PST parallel to a regular branch on the same arc (e.g.,
case6470rte).
Reproduction (before fix)
Note: The open-source case6470rte.m can be obtained from case6470rte.m
using PowerSystems, PowerNetworkMatrices, Logging
# Silence warnings/info from PowerSystems and PowerNetworkMatrices
quiet = ConsoleLogger(stderr, Logging.Error)
# --- Step 1: Load system and build Ybus ---
sys = with_logger(quiet) do
System("./case6470rte.m")
end
ybus = with_logger(quiet) do
Ybus(sys; make_arc_admittance_matrices = true)
end
nrd = with_logger(quiet) do
PowerNetworkMatrices.get_network_reduction_data(ybus)
end
# --- Step 2: Find arcs where only a PST ended up in direct_branch_map ---
direct = PowerNetworkMatrices.get_direct_branch_map(nrd)
pst_only = [(arc, br) for (arc, br) in direct if br isa PhaseShiftingTransformer]
# --- Step 3: For each of those arcs, find other system branches on the same arc ---
function siblings_on_arc(sys, arc, exclude)
result = []
for b in get_components(ACTransmission, sys)
if !get_available(b)
continue
end
if b === exclude
continue
end
from = get_number(get_from(get_arc(b)))
to = get_number(get_to(get_arc(b)))
if (from, to) == arc
push!(result, b)
end
end
return result
end
# --- Step 4: Collect all dropped branches ---
dropped_report = []
for (arc, br) in pst_only
dropped = siblings_on_arc(sys, arc, br)
if !isempty(dropped)
push!(dropped_report, (arc, br, dropped))
end
end
# --- Step 5: Report results ---
if isempty(dropped_report)
@info "✓ All system branches are accounted for in NRD — no branches dropped."
else
for (arc, br, dropped) in dropped_report
@info "Arc $arc ── $(length(dropped)) branch(es) dropped"
@info "BEFORE (all system branches on this arc):"
@info " (--kept--) $(get_name(br)) :: $(typeof(br))"
for d in dropped
@info " (--dropped--) $(get_name(d)) :: $(typeof(d))"
end
@info "AFTER (what NRD is tracking for this arc):"
@info " direct_branch_map → $(get_name(br)) :: $(typeof(br))"
end
end
After the fix, every system branch appears in NRD map.
Description
When a
PhaseShiftingTransformer(PST) shares an arc with aLineorTransformer2W, one of the two branches is silently dropped from allNetworkReductionDatamaps inadd_to_branch_maps!(src/Ybus.jl). The Ybus is built correctly (both admittances are summed), but the dropped branch is never passed toset_power_flow!, leaving its flow at zero after the solve.Root cause
add_to_branch_maps!routes each branch intodirect_branch_maporparallel_branch_map. The condition that promotes two branches on the same arc toparallel_branch_maprequires both to be outsideSKIP_PARALLEL_REDUCTION_TYPES. When one is a PST (which is inSKIP), that condition is false, and the code falls through to theelseclause, which unconditionally overwritesdirect_branch_map[arc_tuple]with the incoming branch, losing the previous one.Linedirect[arc] = LinePSTSKIPdirect[arc] = PST(Line lost)The same displacement occurs if the PST arrives first.
Fix
Add an
elseif haskey(direct_branch_map, arc_tuple)clause between condition B and theelse. When the arc is already occupied and one branch is a SKIP type, move both intoparallel_branch_mapso neither is lost.This should be sufficient because
SKIP_PARALLEL_REDUCTION_TYPESonly prohibits grouping a PST with another PST. A PST alongside aLineor symmetric transformer is handled correctly by_segment_flow_entry, which evaluates each branch independently using its own admittance matrix.Impact
case6470rte).Reproduction (before fix)
Note: The open-source
case6470rte.mcan be obtained from case6470rte.mAfter the fix, every system branch appears in NRD map.