Skip to content

Conversation

ghazwarhili
Copy link
Contributor

@ghazwarhili ghazwarhili commented Sep 29, 2025

Overview:
Nodes represent connection points
Switches (breakers, disconnectors) connect nodes
=> The goal is to use these informations to find which busbar a terminal is connected to

Algorithm Steps:
The search starts from a terminal's node and explores the network topology
The algorithm uses a queue to explore nodes level by level (by depth)
Finds the shortest path to each busbar
The algorithm applies a priority-based selection to choose the best busbar:
Priority 1: Direct connection (no switch in the path)
Priority 2: Connection through CLOSED switches
Priority 3: Connection through OPEN switches

=> The algorithm respects the real state of switches, prioritizing closed (active) connections over open (inactive) ones

if we consider the example of voltage level MUR P6 and we will search for the busbar section linked to MUR Y632

=== Starting busbar section search ===
Starting node: 7

--- Starting traversal ---

Iteration 1: Processing node 7 (depth: 0)
-> Marked as visited
-> Not a busbar, exploring adjacent nodes...
-> Adding node 14 to queue via switch MUR P6_MUR 6TR632 DJ_OC (BREAKER, CLOSED)

Iteration 2: Processing node 14 (depth: 1)
-> Marked as visited
-> Not a busbar, exploring adjacent nodes...
-> Adding node 0 to queue via switch MUR P6_MUR 6TR632 SA.1_OC (DISCONNECTOR, CLOSED)
-> Adding node 1 to queue via switch MUR P6_MUR 6TR632 SA.2_OC (DISCONNECTOR, OPEN)

Iteration 3: Processing node 0 (depth: 2)
-> Marked as visited
-> BUSBAR FOUND: MUR P6_1 (depth: 2, last switch: MUR P6_MUR 6TR632 SA.1_OC - CLOSED)

Iteration 4: Processing node 1 (depth: 2)
-> Marked as visited
-> BUSBAR FOUND: MUR P6_2 (depth: 2, last switch: MUR P6_MUR 6TR632 SA.2_OC - OPEN)

--- BFS traversal completed ---
Total nodes visited: 4
Total busbars found: 4
Found 4 busbar section(s), selecting best one...

--- Selecting best busbar from 4 candidates ---
Found 2 busbar(s) through closed switch
Selected: MUR P6_1 (depth: 2, last switch: MUR P6_MUR 6TR632 SA.1_OC - CLOSED)
Final result: MUR P6_1
=== Search completed ===

@EtienneLt
Copy link
Contributor

Test OK on several voltage levels

public final class BusbarSectionFinderTraverser {

/**
* Private constructor to prevent instantiation of this utility class.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment to remove

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

import java.util.*;

/**
* @author Slimane Amar <slimane.amar at rte-france.com>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ghazwa

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

* @param terminal the starting terminal
* @return the busbar ID or null if none found
*/
public static String findBusbarSectionId(Terminal terminal) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move the records and this method just under the constructor

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

* @param terminal the starting terminal
* @return the best busbar result according to selection criteria, or null if none found
*/
public static BusbarSectionResult findBestBusbar(Terminal terminal) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should return the busbar section id

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, you can remove findBusbarSectionId() and name this method findBusbarSectionId()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed to getBusbarSectionResult

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer not remove findBusbarSectionId, renamed to getBusbarSectionResult

}

private final boolean onlyConnectedBbs;
// Fallback: select first busbar
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to remove

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

public String getFirstTraversedBbsId() {
return firstTraversedBbsId;
/**
* Internal record to track the path during graph traversal.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comments on records to remove ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

/**
* Record containing the result of a busbar search with selection metadata.
*/
public record BusbarSectionResult(String busbarSectionId, int depth, int switchesBeforeLast, SwitchInfo lastSwitch) { }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the difference between depth and switchesBeforeLast ? It is redondant nope ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depth is not the same as switchesBeforeLast (busbarSectionId)
but switchesBeforeLast to be removed

* Selects the best busbar from a list of candidates using a priority-based approach:
* Priority 1: Busbar with closed last switch (minimum depth, then minimum switches before last)
* Priority 2: Busbar with open last switch (minimum depth, then minimum switches before last)
* Priority 3: Busbar without switch (direct connection, minimum depth)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this one is the least prioritized strategy ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be the first one ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

/**
* Record containing information about a switch in the topology.
*/
public record SwitchInfo(String id, SwitchKind kind, boolean isOpen, int node1, int node2) { }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

int node1, int node2 are not used

Copy link
Contributor Author

@ghazwarhili ghazwarhili Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had used them for debugging purposes, but now we don't need them anymore
removed

return !visited.contains(node);
}

private static Optional<BusbarSectionResult> tryCreateBusbarResult(VoltageLevel.NodeBreakerView view, NodePath currentNodePath) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private static Optional<BusbarSectionResult> tryCreateBusbarResult(VoltageLevel.NodeBreakerView view, NodePath currentNodePath) {
private static Optional<BusbarSectionResult> getDirectlyConnectedBusbarSection(VoltageLevel.NodeBreakerView view, NodePath currentNodePath) {

Something like this ? Here, we check if a busbar section is directly connected to the node and return it if yes

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

something better than getDirectlyConnectedBusbarSection() to be found

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

findBusbarSectionAtNode maybe ?

return results;
}

private static boolean hasNotBeenVisited(int node, Set<Integer> visited) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private static boolean hasNotBeenVisited(int node, Set<Integer> visited) {
private static boolean hasBeenVisited(int node, Set<Integer> visited) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}

private static boolean hasNotBeenVisited(int node, Set<Integer> visited) {
return !visited.contains(node);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return !visited.contains(node);
return visited.contains(node);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@ghazwarhili ghazwarhili changed the title Busbar section finder traverser breadth first search algorithm Depth-first search algorithm for BusBarSection IDs Oct 1, 2025
@FranckLecuyer FranckLecuyer self-requested a review October 1, 2025 07:57
private static List<BusbarSectionResult> searchAllBusbars(VoltageLevel.NodeBreakerView view, int startNode) {
List<BusbarSectionResult> results = new ArrayList<>();
Set<Integer> visited = new HashSet<>();
Queue<NodePath> queue = new LinkedList<>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Queue<NodePath> queue = new LinkedList<>();
Queue<NodePath> pathsToVisit = new LinkedList<>();

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nodePathsToVisit maybe ?

return Optional.empty();
}

private static void exploreAdjacentNodes(VoltageLevel.NodeBreakerView view, NodePath currentNodePath, Set<Integer> visited, Queue<NodePath> queue) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private static void exploreAdjacentNodes(VoltageLevel.NodeBreakerView view, NodePath currentNodePath, Set<Integer> visited, Queue<NodePath> queue) {
private static void exploreAdjacentNodes(VoltageLevel.NodeBreakerView view, NodePath currentNodePath, Set<Integer> visited, Queue<NodePath> pathsToVisit) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}

private final boolean onlyConnectedBbs;
private record NodePath(int node, List<SwitchInfo> pathSwitches, SwitchInfo lastSwitch) { }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private record NodePath(int node, List<SwitchInfo> pathSwitches, SwitchInfo lastSwitch) { }
private record Path(int startNode, List<SwitchInfo> traversedSwitches, SwitchInfo lastSwitch) { }

Something like this ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The term 'Path' is a generic name
it is ok for startNode and traversedSwitches

Copy link
Contributor

@FranckLecuyer FranckLecuyer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests:
This does not return the right busbar section in some cases for voltage levels with 'pontages' ...

private record NodePath(int node, List<SwitchInfo> pathSwitches, SwitchInfo lastSwitch) { }

private String firstTraversedBbsId;
public record SwitchInfo(String id, SwitchKind kind, boolean isOpen) { }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SwitchKind kind is not used

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used it at the beginning for logging and debugging purposes
to be removed

if (!withOpenSwitch.isEmpty()) {
return withOpenSwitch.stream().min(Comparator.comparingInt(BusbarSectionResult::depth)).orElse(null);
}
return results.getFirst();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can be empty

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case is handled by returning result.busbarSectionId() when result is not null, and otherwise the Id of the first busbar section in the terminal’s voltage level

}

private static void exploreAdjacentNodes(VoltageLevel.NodeBreakerView view, NodePath currentNodePath, Set<Integer> visited, Queue<NodePath> queue) {
view.getSwitchStream().forEach(sw -> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not efficient. Maybe we can do something better

@ghazwarhili
Copy link
Contributor Author

ghazwarhili commented Oct 1, 2025

#293 (review)
In case of a bypass (e.g., a bridging situation where multiple busbars are found at the same depth) => we resolve the conflict by comparing busbarIndex and sectionIndex, and selecting the minimum value.

Iteration 7: Processing node 2 (depth: 3)
-> Marked as visited
-> BUSBAR FOUND: ALBERP6_2.1 (depth: 3, last switch: ALBERP6_ALBER6VEN.P.1 SA.2_OC - CLOSED)

Iteration 8: Processing node 0 (depth: 3)
-> Marked as visited
-> BUSBAR FOUND: ALBERP6_1.1 (depth: 3, last switch: ALBERP6_ALBER6VEN.P.1 SA.1_OC - CLOSED)

=> return ALBERP6_1.1

Copy link

sonarqubecloud bot commented Oct 1, 2025

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants