Skip to content

Bug: Branch silently dropped from NetworkReductionData when a PhaseShiftingTransformer is parallel to a regular branch #305

@orennia-juan

Description

@orennia-juan

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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions