|
7 | 7 | package org.gridsuite.network.map.dto.definition.extension;
|
8 | 8 |
|
9 | 9 | import com.powsybl.iidm.network.IdentifiableType;
|
10 |
| -import com.powsybl.iidm.network.Switch; |
11 | 10 | import com.powsybl.iidm.network.SwitchKind;
|
12 | 11 | import com.powsybl.iidm.network.Terminal;
|
13 |
| -import com.powsybl.math.graph.TraverseResult; |
| 12 | +import com.powsybl.iidm.network.VoltageLevel; |
14 | 13 |
|
15 |
| -import java.util.ArrayList; |
16 |
| -import java.util.HashSet; |
17 |
| -import java.util.List; |
18 |
| -import java.util.Set; |
| 14 | +import java.util.*; |
19 | 15 |
|
20 | 16 | /**
|
21 | 17 | * @author Slimane Amar <slimane.amar at rte-france.com>
|
22 | 18 | */
|
23 |
| -public class BusbarSectionFinderTraverser implements Terminal.TopologyTraverser { |
| 19 | +public final class BusbarSectionFinderTraverser { |
24 | 20 |
|
25 |
| - private final List<BusbarCandidate> busbarCandidates = new ArrayList<>(); |
26 |
| - private final Set<String> visitedTerminals = new HashSet<>(); |
27 |
| - private static final int MAX_VISITED = 50; |
28 |
| - private final boolean allowTraversalThroughOpenDisconnectors; |
29 |
| - |
30 |
| - public BusbarSectionFinderTraverser(boolean allowTraversalThroughOpenDisconnectors) { |
31 |
| - this.allowTraversalThroughOpenDisconnectors = allowTraversalThroughOpenDisconnectors; |
| 21 | + /** |
| 22 | + * Private constructor to prevent instantiation of this utility class. |
| 23 | + */ |
| 24 | + private BusbarSectionFinderTraverser() { |
| 25 | + throw new UnsupportedOperationException(); |
32 | 26 | }
|
33 | 27 |
|
34 |
| - @Override |
35 |
| - public TraverseResult traverse(Terminal terminal, boolean connected) { |
36 |
| - String terminalId = terminal.getConnectable().getId(); |
37 |
| - if (visitedTerminals.contains(terminalId)) { |
38 |
| - return TraverseResult.TERMINATE_PATH; |
39 |
| - } |
40 |
| - visitedTerminals.add(terminalId); |
41 |
| - if (visitedTerminals.size() > MAX_VISITED) { |
42 |
| - return TraverseResult.TERMINATE_TRAVERSER; |
| 28 | + /** |
| 29 | + * Finds the best busbar section connected to the given terminal. |
| 30 | + * Uses a breadth-first search algorithm to explore all possible paths. |
| 31 | + * |
| 32 | + * @param terminal the starting terminal |
| 33 | + * @return the best busbar result according to selection criteria, or null if none found |
| 34 | + */ |
| 35 | + public static BusbarResult findBestBusbar(Terminal terminal) { |
| 36 | + VoltageLevel.NodeBreakerView view = terminal.getVoltageLevel().getNodeBreakerView(); |
| 37 | + int startNode = terminal.getNodeBreakerView().getNode(); |
| 38 | + List<BusbarResult> allResults = searchAllBusbars(view, startNode); |
| 39 | + if (allResults.isEmpty()) { |
| 40 | + return null; |
43 | 41 | }
|
| 42 | + return selectBestBusbar(allResults); |
| 43 | + } |
44 | 44 |
|
45 |
| - // If a busbar section is found, add it as a candidate |
46 |
| - if (terminal.getConnectable().getType() == IdentifiableType.BUSBAR_SECTION) { |
47 |
| - busbarCandidates.add(new BusbarCandidate(terminalId, connected)); |
48 |
| - // CONTINUE to explore other paths to other busbars |
49 |
| - return TraverseResult.CONTINUE; |
| 45 | + /** |
| 46 | + * Selects the best busbar from a list of candidates using a priority-based approach: |
| 47 | + * Priority 1: Busbar with closed last switch (minimum depth, then minimum switches before last) |
| 48 | + * Priority 2: Busbar with open last switch (minimum depth, then minimum switches before last) |
| 49 | + * Priority 3: Busbar without switch (direct connection, minimum depth) |
| 50 | + * |
| 51 | + * @param results list of all found busbar results |
| 52 | + * @return the best busbar according to selection criteria |
| 53 | + */ |
| 54 | + private static BusbarResult selectBestBusbar(List<BusbarResult> results) { |
| 55 | + // Priority 1: Search for busbar with closed last switch |
| 56 | + List<BusbarResult> withClosedSwitch = results.stream().filter(r -> r.lastSwitch() != null && !r.lastSwitch().isOpen()).toList(); |
| 57 | + if (!withClosedSwitch.isEmpty()) { |
| 58 | + BusbarResult best = withClosedSwitch.stream().min(Comparator.comparingInt(BusbarResult::depth) |
| 59 | + .thenComparingInt(BusbarResult::switchesBeforeLast)) |
| 60 | + .get(); |
| 61 | + return best; |
50 | 62 | }
|
51 |
| - return TraverseResult.CONTINUE; |
52 |
| - } |
53 | 63 |
|
54 |
| - @Override |
55 |
| - public TraverseResult traverse(Switch aSwitch) { |
56 |
| - if (visitedTerminals.size() > MAX_VISITED) { |
57 |
| - return TraverseResult.TERMINATE_TRAVERSER; |
| 64 | + // Priority 2: Search for busbar with open last switch |
| 65 | + List<BusbarResult> withOpenSwitch = results.stream().filter(r -> r.lastSwitch() != null && r.lastSwitch().isOpen()).toList(); |
| 66 | + if (!withOpenSwitch.isEmpty()) { |
| 67 | + BusbarResult best = withOpenSwitch.stream().min(Comparator.comparingInt(BusbarResult::depth) |
| 68 | + .thenComparingInt(BusbarResult::switchesBeforeLast)) |
| 69 | + .get(); |
| 70 | + |
| 71 | + return best; |
58 | 72 | }
|
59 | 73 |
|
60 |
| - // KEY: Open disconnectors end this path but not the overall traversal |
61 |
| - // They block access to this busbar but not to the others |
62 |
| - if (aSwitch.isOpen() && aSwitch.getKind() == SwitchKind.DISCONNECTOR) { |
63 |
| - // Use the parameter to control behavior |
64 |
| - return allowTraversalThroughOpenDisconnectors ? |
65 |
| - TraverseResult.CONTINUE : |
66 |
| - TraverseResult.TERMINATE_PATH; |
| 74 | + // Priority 3: Busbars without switch (direct connection) |
| 75 | + List<BusbarResult> withoutSwitch = results.stream().filter(r -> r.lastSwitch() == null).toList(); |
| 76 | + if (!withoutSwitch.isEmpty()) { |
| 77 | + BusbarResult best = withoutSwitch.stream().min(Comparator.comparingInt(BusbarResult::depth)).get(); |
| 78 | + return best; |
67 | 79 | }
|
68 |
| - return TraverseResult.CONTINUE; |
| 80 | + |
| 81 | + // Fallback: select first busbar |
| 82 | + return results.getFirst(); |
69 | 83 | }
|
70 | 84 |
|
71 |
| - public String getBusbarWithClosedDisconnector() { |
72 |
| - // Search for a connected busbar (disconnector closed) |
73 |
| - for (BusbarCandidate candidate : busbarCandidates) { |
74 |
| - if (candidate.connected()) { |
75 |
| - return candidate.id(); |
| 85 | + /** |
| 86 | + * Searches all accessible busbars from a starting node using breadth-first search. |
| 87 | + * Explores the node-breaker topology through switches. |
| 88 | + * |
| 89 | + * @param view the node-breaker view of the voltage level |
| 90 | + * @param startNode the starting node index |
| 91 | + * @return list of all busbar results found |
| 92 | + */ |
| 93 | + private static List<BusbarResult> searchAllBusbars(VoltageLevel.NodeBreakerView view, int startNode) { |
| 94 | + List<BusbarResult> results = new ArrayList<>(); |
| 95 | + Set<Integer> visited = new HashSet<>(); |
| 96 | + Queue<NodePath> queue = new LinkedList<>(); |
| 97 | + queue.offer(new NodePath(startNode, new ArrayList<>(), null)); |
| 98 | + while (!queue.isEmpty()) { |
| 99 | + NodePath current = queue.poll(); |
| 100 | + if (visited.contains(current.node())) { |
| 101 | + continue; |
| 102 | + } |
| 103 | + visited.add(current.node()); |
| 104 | + // Check if current node is a busbar section |
| 105 | + Optional<Terminal> nodeTerminal = view.getOptionalTerminal(current.node()); |
| 106 | + if (nodeTerminal.isPresent()) { |
| 107 | + Terminal term = nodeTerminal.get(); |
| 108 | + if (term.getConnectable().getType() == IdentifiableType.BUSBAR_SECTION) { |
| 109 | + String busbarId = term.getConnectable().getId(); |
| 110 | + int depth = current.pathSwitches().size(); |
| 111 | + SwitchInfo lastSwitch = current.lastSwitch(); |
| 112 | + // Calculate number of switches BEFORE the last one |
| 113 | + int switchesBeforeLast = lastSwitch != null ? (depth - 1) : 0; |
| 114 | + results.add(new BusbarResult(busbarId, depth, switchesBeforeLast, lastSwitch, null)); |
| 115 | + continue; // Don't explore beyond busbar |
| 116 | + } |
76 | 117 | }
|
77 |
| - } |
78 | 118 |
|
79 |
| - // Return first busbar found or null if none |
80 |
| - return !busbarCandidates.isEmpty() ? busbarCandidates.getFirst().id() : null; |
| 119 | + // Explore adjacent nodes through switches |
| 120 | + view.getSwitchStream().forEach(sw -> { |
| 121 | + int node1 = view.getNode1(sw.getId()); |
| 122 | + int node2 = view.getNode2(sw.getId()); |
| 123 | + if (node1 == current.node() || node2 == current.node()) { |
| 124 | + int nextNode = (node1 == current.node()) ? node2 : node1; |
| 125 | + if (!visited.contains(nextNode)) { |
| 126 | + List<SwitchInfo> newPathSwitches = new ArrayList<>(current.pathSwitches()); |
| 127 | + SwitchInfo switchInfo = new SwitchInfo(sw.getId(), sw.getKind(), sw.isOpen(), node1, node2); |
| 128 | + newPathSwitches.add(switchInfo); |
| 129 | + queue.offer(new NodePath(nextNode, newPathSwitches, switchInfo)); |
| 130 | + } |
| 131 | + } |
| 132 | + }); |
| 133 | + } |
| 134 | + return results; |
81 | 135 | }
|
82 | 136 |
|
83 |
| - // Utility method with automatic fallback |
84 |
| - public static String findBusbar(Terminal startTerminal) { |
85 |
| - // Attempt 1: normal behavior (blocks on open disconnectors) |
86 |
| - var traverser1 = new BusbarSectionFinderTraverser(false); |
87 |
| - startTerminal.traverse(traverser1); |
88 |
| - String result = traverser1.getBusbarWithClosedDisconnector(); |
| 137 | + /** |
| 138 | + * Internal record to track the path during graph traversal. |
| 139 | + */ |
| 140 | + private record NodePath(int node, List<SwitchInfo> pathSwitches, SwitchInfo lastSwitch) { } |
89 | 141 |
|
90 |
| - if (result != null) { |
91 |
| - return result; |
92 |
| - } |
| 142 | + /** |
| 143 | + * Record containing information about a switch in the topology. |
| 144 | + */ |
| 145 | + public record SwitchInfo(String id, SwitchKind kind, boolean isOpen, int node1, int node2) { } |
93 | 146 |
|
94 |
| - // Attempt 2: if null, retry allowing traversal through open disconnectors |
95 |
| - var traverser2 = new BusbarSectionFinderTraverser(true); |
96 |
| - startTerminal.traverse(traverser2); |
97 |
| - return traverser2.getBusbarWithClosedDisconnector(); |
98 |
| - } |
| 147 | + /** |
| 148 | + * Record containing the result of a busbar search with selection metadata. |
| 149 | + */ |
| 150 | + public record BusbarResult(String busbarId, int depth, int switchesBeforeLast, SwitchInfo lastSwitch, String selectionReason) { } |
99 | 151 |
|
100 |
| - private record BusbarCandidate(String id, boolean connected) { |
| 152 | + /** |
| 153 | + * Convenience method to get only the busbar ID. |
| 154 | + * |
| 155 | + * @param terminal the starting terminal |
| 156 | + * @return the busbar ID or null if none found |
| 157 | + */ |
| 158 | + public static String findBusbarId(Terminal terminal) { |
| 159 | + BusbarResult result = findBestBusbar(terminal); |
| 160 | + return result != null ? result.busbarId() : null; |
101 | 161 | }
|
102 | 162 | }
|
0 commit comments