Skip to content

Commit efc0ece

Browse files
committed
breadth-first busbasrsection id search algorithm
1 parent 78f18fd commit efc0ece

File tree

2 files changed

+126
-66
lines changed

2 files changed

+126
-66
lines changed

src/main/java/org/gridsuite/network/map/dto/definition/extension/BusbarSectionFinderTraverser.java

Lines changed: 125 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -7,96 +7,156 @@
77
package org.gridsuite.network.map.dto.definition.extension;
88

99
import com.powsybl.iidm.network.IdentifiableType;
10-
import com.powsybl.iidm.network.Switch;
1110
import com.powsybl.iidm.network.SwitchKind;
1211
import com.powsybl.iidm.network.Terminal;
13-
import com.powsybl.math.graph.TraverseResult;
12+
import com.powsybl.iidm.network.VoltageLevel;
1413

15-
import java.util.ArrayList;
16-
import java.util.HashSet;
17-
import java.util.List;
18-
import java.util.Set;
14+
import java.util.*;
1915

2016
/**
2117
* @author Slimane Amar <slimane.amar at rte-france.com>
2218
*/
23-
public class BusbarSectionFinderTraverser implements Terminal.TopologyTraverser {
19+
public final class BusbarSectionFinderTraverser {
2420

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();
3226
}
3327

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;
4341
}
42+
return selectBestBusbar(allResults);
43+
}
4444

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;
5062
}
51-
return TraverseResult.CONTINUE;
52-
}
5363

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;
5872
}
5973

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;
6779
}
68-
return TraverseResult.CONTINUE;
80+
81+
// Fallback: select first busbar
82+
return results.getFirst();
6983
}
7084

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+
}
76117
}
77-
}
78118

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;
81135
}
82136

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) { }
89141

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) { }
93146

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) { }
99151

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;
101161
}
102162
}

src/main/java/org/gridsuite/network/map/dto/utils/ElementUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public static String getBusOrBusbarSection(Terminal terminal) {
4747
}
4848
} else {
4949
// NODE_BREAKER: explore all paths and choose the busbar with the closed disconnector
50-
return BusbarSectionFinderTraverser.findBusbar(terminal);
50+
return BusbarSectionFinderTraverser.findBusbarId(terminal);
5151
}
5252
}
5353

0 commit comments

Comments
 (0)