Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
988efdd
Refactor graph traversal methods to use new `getConnectedComponents` …
clementleclercRTE Feb 3, 2026
30cc260
Refactor graph traversal methods to use new `getConnectedComponents` …
clementleclercRTE Feb 3, 2026
10555c9
Merge remote-tracking branch 'origin/graph-connected-components' into…
clementleclercRTE Feb 3, 2026
e522228
Merge branch 'main' into graph-connected-components
clementleclercRTE Feb 3, 2026
caa146e
Remove `isComponentValid` method and inline validation logic in graph…
clementleclercRTE Feb 4, 2026
b54a1c6
Refactor `getConnectedComponents` API to use `Traverser` instead of `…
clementleclercRTE Feb 4, 2026
8eb3d6c
Deprecate old traversal method, simplify `ConnectedComponentCollector…
clementleclercRTE Feb 4, 2026
c562c12
Refactor `getConnectedComponents` usage
clementleclercRTE Feb 4, 2026
c9cf18d
Merge branch 'main' into graph-connected-components
clementleclercRTE Feb 4, 2026
df1cf80
Refactor traversal methods and update internal connection tests.
clementleclercRTE Feb 9, 2026
1b58640
Merge branch 'main' into graph-connected-components
clementleclercRTE Feb 9, 2026
c5a2ef6
Add unit tests for getConnectedComponents and update traversal logic
clementleclercRTE Feb 9, 2026
7cd03d4
Merge remote-tracking branch 'origin/graph-connected-components' into…
clementleclercRTE Feb 9, 2026
16a1dd0
Refactor topology models to replace `Set` with `List` and fix review …
clementleclercRTE Feb 9, 2026
e3e8b9e
Refactor topology models to replace `Set` with `List` and fix review …
clementleclercRTE Feb 9, 2026
037e516
Merge remote-tracking branch 'origin/graph-connected-components' into…
clementleclercRTE Feb 10, 2026
b173fb9
Merge branch 'main' into graph-connected-components
clementleclercRTE Feb 16, 2026
201fb81
Fix indentation
clementleclercRTE Feb 16, 2026
f73819e
Merge branch 'main' into graph-connected-components
clementleclercRTE Feb 18, 2026
583b71a
Merge branch 'main' into graph-connected-components
clementleclercRTE Feb 18, 2026
f85d061
Merge branch 'main' into graph-connected-components
clementleclercRTE Feb 23, 2026
dd565cb
Merge remote-tracking branch 'origin/graph-connected-components' into…
clementleclercRTE Feb 23, 2026
9557084
Refactor `getConnectedComponents` to `computeTraversalPartitions` and…
clementleclercRTE Feb 24, 2026
8f96dea
Merge remote-tracking branch 'origin/main' into graph-connected-compo…
clementleclercRTE Mar 2, 2026
b6b5556
Merge branch 'main' into graph-connected-components
flo-dup Mar 3, 2026
6c094d8
Merge remote-tracking branch 'origin/graph-connected-components' into…
clementleclercRTE Mar 9, 2026
e7cbba1
Merge remote-tracking branch 'origin/main' into graph-connected-compo…
clementleclercRTE Mar 9, 2026
8f84c9a
review fix
clementleclercRTE Mar 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@
import com.powsybl.iidm.network.util.Identifiables;
import com.powsybl.iidm.network.util.Networks;
import com.powsybl.iidm.network.util.ShortIdDictionary;
import com.powsybl.math.graph.TraversalType;
import com.powsybl.math.graph.TraverseResult;
import com.powsybl.math.graph.UndirectedGraphImpl;
import com.powsybl.math.graph.UndirectedGraphListener;
import com.powsybl.math.graph.*;
import org.anarres.graphviz.builder.GraphVizAttribute;
import org.anarres.graphviz.builder.GraphVizEdge;
import org.anarres.graphviz.builder.GraphVizGraph;
Expand Down Expand Up @@ -197,9 +194,9 @@ private MergedBus getMergedBus(ConfiguredBus cfgBus) {
*/
class CalculatedBusTopology {

protected boolean isBusValid(Set<ConfiguredBus> busSet) {
protected boolean isBusValid(List<ConfiguredBus> buses) {
int feederCount = 0;
for (TerminalExt terminal : FluentIterable.from(busSet).transformAndConcat(ConfiguredBus::getConnectedTerminals)) {
for (TerminalExt terminal : FluentIterable.from(buses).transformAndConcat(ConfiguredBus::getConnectedTerminals)) {
AbstractConnectable connectable = terminal.getConnectable();
switch (connectable.getType()) {
case LINE, TWO_WINDINGS_TRANSFORMER, THREE_WINDINGS_TRANSFORMER, HVDC_CONVERTER_STATION,
Expand All @@ -214,11 +211,11 @@ protected boolean isBusValid(Set<ConfiguredBus> busSet) {
return Networks.isBusValid(feederCount);
}

private MergedBus createMergedBus(int busNum, Set<ConfiguredBus> busSet) {
private MergedBus createMergedBus(int busNum, List<ConfiguredBus> buses) {
String suffix = "_" + busNum;
String mergedBusId = Identifiables.getUniqueId(voltageLevel.getId() + suffix, getNetwork().getIndex()::contains);
String mergedBusName = voltageLevel.getOptionalName().map(name -> name + suffix).orElse(null);
return new MergedBus(mergedBusId, mergedBusName, voltageLevel.isFictitious(), busSet);
return new MergedBus(mergedBusId, mergedBusName, voltageLevel.isFictitious(), buses);
}

private void updateCache() {
Expand All @@ -230,27 +227,26 @@ private void updateCache() {

// mapping between configured buses and merged buses
Map<ConfiguredBus, MergedBus> mapping = new IdentityHashMap<>();

boolean[] encountered = new boolean[graph.getVertexCapacity()];
Arrays.fill(encountered, false);
int busNum = 0;
for (int v : graph.getVertices()) {
if (!encountered[v]) {
final Set<ConfiguredBus> busSet = new LinkedHashSet<>(1);
busSet.add(graph.getVertexObject(v));
graph.traverse(v, TraversalType.DEPTH_FIRST, (v1, e, v2) -> {
SwitchImpl aSwitch = graph.getEdgeObject(e);
if (aSwitch.isOpen()) {
return TraverseResult.TERMINATE_PATH;
} else {
busSet.add(graph.getVertexObject(v2));
return TraverseResult.CONTINUE;
}
}, encountered);
if (isBusValid(busSet)) {
MergedBus mergedBus = createMergedBus(busNum++, busSet);
mergedBuses.put(mergedBus.getId(), mergedBus);
busSet.forEach(bus -> mapping.put(bus, mergedBus));

List<List<ConfiguredBus>> connectedComponents = graph.computeTraversalPartitions(
(v1, e, v2) -> {
SwitchImpl sw = graph.getEdgeObject(e);
if (sw != null && sw.isOpen()) {
return TraverseResult.TERMINATE_PATH;
}
return TraverseResult.CONTINUE;
},
ArrayList::new,
(component, vertexIndex) -> component.add(graph.getVertexObject(vertexIndex))
);

for (List<ConfiguredBus> component : connectedComponents) {
if (isBusValid(component)) {
MergedBus mergedBus = createMergedBus(busNum++, component);
mergedBuses.put(mergedBus.getId(), mergedBus);
for (ConfiguredBus bus : component) {
mapping.put(bus, mergedBus);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ public class DcBusImpl extends AbstractDcTopologyVisitable<DcBus> implements DcB

private final Ref<NetworkImpl> networkRef;
private final Ref<SubnetworkImpl> subnetworkRef;
private final Set<DcNodeImpl> dcNodes;
private final List<DcNodeImpl> dcNodes;

private boolean valid = true;

DcBusImpl(Ref<NetworkImpl> ref, Ref<SubnetworkImpl> subnetworkRef, String id, String name, Set<DcNodeImpl> dcNodes) {
DcBusImpl(Ref<NetworkImpl> ref, Ref<SubnetworkImpl> subnetworkRef, String id, String name, List<DcNodeImpl> dcNodes) {
super(id, name);
this.networkRef = Objects.requireNonNull(ref);
this.subnetworkRef = subnetworkRef;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@
import com.powsybl.commons.ref.RefChain;
import com.powsybl.iidm.network.*;
import com.powsybl.iidm.network.util.Identifiables;
import com.powsybl.math.graph.TraversalType;
import com.powsybl.math.graph.TraverseResult;
import com.powsybl.math.graph.UndirectedGraphImpl;
import com.powsybl.math.graph.UndirectedGraphListener;
import com.powsybl.math.graph.*;

import java.util.*;
import java.util.function.Function;
Expand Down Expand Up @@ -213,19 +210,19 @@ public DcBusImpl getDcBusOfDcNode(String dcNodeId) {
*/
class CalculatedDcBusTopology {

private static boolean isDcBusValid(Set<DcNodeImpl> dcNodeSet) {
private static boolean isDcBusValid(List<DcNodeImpl> dcNodes) {
// DcBus is valid if at least one DcConnectable connected, i.e. there is at least one connected DcTerminal
return dcNodeSet.stream().flatMap(DcNode::getConnectedDcTerminalStream).findAny().isPresent();
return dcNodes.stream().flatMap(DcNode::getConnectedDcTerminalStream).findAny().isPresent();
}

private DcBusImpl createDcBus(Set<DcNodeImpl> dcNodeSet) {
if (dcNodeSet == null || dcNodeSet.isEmpty()) {
private DcBusImpl createDcBus(List<DcNodeImpl> dcNodes) {
if (dcNodes == null || dcNodes.isEmpty()) {
throw new PowsyblException("DC Node set is null or empty");
}
var node = dcNodeSet.stream().min(Comparator.comparing(DcNodeImpl::getId)).orElseThrow();
var node = dcNodes.stream().min(Comparator.comparing(DcNodeImpl::getId)).orElseThrow();
String dcBusId = Identifiables.getUniqueId(node.getId() + "_dcBus", getNetwork().getIndex()::contains);
String dcBusName = node.getOptionalName().orElse(null);
return new DcBusImpl(networkRef, subnetworkRef, dcBusId, dcBusName, dcNodeSet);
return new DcBusImpl(networkRef, subnetworkRef, dcBusId, dcBusName, dcNodes);
}

private DcBusCache getCache() {
Expand All @@ -239,27 +236,20 @@ private DcBusCache getCache() {
// mapping between DC nodes ID and DC buses
Map<String, DcBusImpl> dcNodeIdToDcBus = new HashMap<>();

boolean[] encountered = new boolean[graph.getVertexCapacity()];
Arrays.fill(encountered, false);
for (int v : graph.getVertices()) {
if (!encountered[v]) {
final Set<DcNodeImpl> dcNodeSet = new LinkedHashSet<>(1);
dcNodeSet.add(graph.getVertexObject(v));
graph.traverse(v, TraversalType.DEPTH_FIRST, (v1, e, v2) -> {
DcSwitchImpl dcSwitch = graph.getEdgeObject(e);
if (dcSwitch.isOpen()) {
return TraverseResult.TERMINATE_PATH;
} else {
dcNodeSet.add(graph.getVertexObject(v2));
return TraverseResult.CONTINUE;
}
}, encountered);

if (isDcBusValid(dcNodeSet)) {
DcBusImpl dcBus = createDcBus(dcNodeSet);
dcBuses.put(dcBus.getId(), dcBus);
dcNodeSet.forEach(dcNode -> dcNodeIdToDcBus.put(dcNode.getId(), dcBus));
}
List<List<DcNodeImpl>> components = graph.computeTraversalPartitions(
(v1, e, v2) -> {
DcSwitchImpl sw = graph.getEdgeObject(e);
return sw.isOpen() ? TraverseResult.TERMINATE_PATH : TraverseResult.CONTINUE;
},
ArrayList::new,
(component, vertexIndex) -> component.add(graph.getVertexObject(vertexIndex))
);

for (List<DcNodeImpl> component : components) {
if (isDcBusValid(component)) {
DcBusImpl dcBus = createDcBus(component);
dcBuses.put(dcBus.getId(), dcBus);
component.forEach(dcNode -> dcNodeIdToDcBus.put(dcNode.getId(), dcBus));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@
*/
class MergedBus extends AbstractIdentifiable<Bus> implements CalculatedBus {

private final Set<ConfiguredBus> buses;
private final List<ConfiguredBus> buses;

private boolean valid = true;

MergedBus(String id, String name, boolean fictitious, Set<ConfiguredBus> buses) {
MergedBus(String id, String name, boolean fictitious, List<ConfiguredBus> buses) {
super(id, name, fictitious);
if (buses.isEmpty()) {
throw new IllegalArgumentException("buses is empty");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,45 +259,17 @@ protected BusChecker getBusChecker() {
return CALCULATED_BUS_CHECKER;
}

private void traverse(int n, boolean[] encountered, Predicate<SwitchImpl> terminate, Map<String, CalculatedBus> id2bus, CalculatedBus[] node2bus) {
if (!encountered[n]) {
final TIntArrayList nodes = new TIntArrayList(1);
nodes.add(n);
Traverser traverser = (n1, e, n2) -> {
SwitchImpl aSwitch = graph.getEdgeObject(e);
if (aSwitch != null && terminate.test(aSwitch)) {
return TraverseResult.TERMINATE_PATH;
}

if (!encountered[n2]) {
// We need to check this as the traverser might be called twice with the same n2 but with different edges.
// Note that the "encountered" array is used and maintained inside graph::traverse method, hence we should not update it.
nodes.add(n2);
}
return TraverseResult.CONTINUE;
};
graph.traverse(n, TraversalType.DEPTH_FIRST, traverser, encountered);

// check that the component is a bus
String busId = Identifiables.getUniqueId(NAMING_STRATEGY.getId(voltageLevel, nodes), getNetwork().getIndex()::contains);
CopyOnWriteArrayList<NodeTerminal> terminals = new CopyOnWriteArrayList<>();
for (int i = 0; i < nodes.size(); i++) {
int n2 = nodes.getQuick(i);
NodeTerminal terminal2 = graph.getVertexObject(n2);
if (terminal2 != null) {
terminals.add(terminal2);
}
}
if (getBusChecker().isValid(graph, nodes, terminals)) {
addBus(nodes, id2bus, node2bus, busId, terminals);
}
}
}

private void addBus(TIntArrayList nodes, Map<String, CalculatedBus> id2bus, CalculatedBus[] node2bus,
String busId, CopyOnWriteArrayList<NodeTerminal> terminals) {
String busId) {
String busName = NAMING_STRATEGY.getName(voltageLevel, nodes);
Function<Terminal, Bus> getBusFromTerminal = getBusChecker() == CALCULATED_BUS_CHECKER ? t -> t.getBusView().getBus() : t -> t.getBusBreakerView().getBus();
CopyOnWriteArrayList<NodeTerminal> terminals = new CopyOnWriteArrayList<>();
for (int i = 0; i < nodes.size(); i++) {
NodeTerminal terminal = graph.getVertexObject(nodes.getQuick(i));
if (terminal != null) {
terminals.add(terminal);
}
}
CalculatedBusImpl bus = new CalculatedBusImpl(busId, busName, voltageLevel.isFictitious(), voltageLevel, nodes, terminals, getBusFromTerminal);
id2bus.put(busId, bus);
for (int i = 0; i < nodes.size(); i++) {
Expand All @@ -312,10 +284,25 @@ protected void updateCache(final Predicate<SwitchImpl> terminate) {
LOGGER.trace("Update bus topology of voltage level {}", voltageLevel.getId());
Map<String, CalculatedBus> id2bus = new LinkedHashMap<>();
CalculatedBus[] node2bus = new CalculatedBus[graph.getVertexCapacity()];
boolean[] encountered = new boolean[graph.getVertexCapacity()];
for (int v : graph.getVertices()) {
traverse(v, encountered, terminate, id2bus, node2bus);

List<TIntArrayList> components = graph.computeTraversalPartitions((v1, e, v2) -> {
SwitchImpl sw = graph.getEdgeObject(e);
if (sw != null && terminate.test(sw)) {
return TraverseResult.TERMINATE_PATH;
}
return TraverseResult.CONTINUE;
});

for (TIntArrayList nodes : components) {
if (getBusChecker().isValid(graph, nodes)) {
String busId = Identifiables.getUniqueId(
NAMING_STRATEGY.getId(voltageLevel, nodes),
getNetwork().getIndex()::contains
);
addBus(nodes, id2bus, node2bus, busId);
}
}

busCache = new BusCache(node2bus, id2bus);
LOGGER.trace("Found buses {}", id2bus.values());
}
Expand Down Expand Up @@ -457,13 +444,13 @@ SwitchImpl getSwitch(String switchId, boolean throwException) {

private interface BusChecker {

boolean isValid(UndirectedGraph<? extends TerminalExt, SwitchImpl> graph, TIntArrayList nodes, List<NodeTerminal> terminals);
boolean isValid(UndirectedGraph<? extends TerminalExt, SwitchImpl> graph, TIntArrayList nodes);
}

private static final class CalculatedBusChecker implements BusChecker {

@Override
public boolean isValid(UndirectedGraph<? extends TerminalExt, SwitchImpl> graph, TIntArrayList nodes, List<NodeTerminal> terminals) {
public boolean isValid(UndirectedGraph<? extends TerminalExt, SwitchImpl> graph, TIntArrayList nodes) {
int feederCount = 0;
int branchCount = 0;
int busbarSectionCount = 0;
Expand Down Expand Up @@ -494,7 +481,7 @@ public boolean isValid(UndirectedGraph<? extends TerminalExt, SwitchImpl> graph,

private static final class CalculatedBusBreakerChecker implements BusChecker {
@Override
public boolean isValid(UndirectedGraph<? extends TerminalExt, SwitchImpl> graph, TIntArrayList nodes, List<NodeTerminal> terminals) {
public boolean isValid(UndirectedGraph<? extends TerminalExt, SwitchImpl> graph, TIntArrayList nodes) {
return !nodes.isEmpty();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,9 @@ public void testTraversalInternalConnections() {

assertEquals(new InternalConnections().add(0, 7), findFirstInternalConnections(vl));

// Find the internal connections encountered before encountering a terminal, starting from every node
// Only internal connections connecting two nodes having both a terminal are expected to be missing
InternalConnections icConnectedToAtMostOneTerminal = findInternalConnectionsTraverseStoppingAtTerminals(vl);
InternalConnections expected = new InternalConnections();
expected.add(7, 0).add(6, 3).add(4, 3).add(5, 2).add(9, 2).add(8, 1);
expected.add(7, 0).add(5, 2).add(8, 1);
assertEquals(expected, icConnectedToAtMostOneTerminal);

assertEquals(all, findInternalConnections(vl));
Expand Down
46 changes: 46 additions & 0 deletions math/src/main/java/com/powsybl/math/graph/UndirectedGraph.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
import java.util.Comparator;
import java.util.List;
import java.util.function.Function;
import java.util.function.ObjIntConsumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.IntStream;
import java.util.stream.Stream;

Expand Down Expand Up @@ -328,9 +330,53 @@
* </ul>
* @return false if the whole traversing has to stop, meaning that a {@link TraverseResult#TERMINATE_TRAVERSER}
* has been returned from the traverser, true otherwise
* @deprecated Use {@link #computeTraversalPartitions(Traverser)} or {@link #traverse(int, TraversalType, Traverser)} instead.
*/
@Deprecated(since = "7.2.0")
boolean traverse(int v, TraversalType traversalType, Traverser traverser, boolean[] verticesEncountered);

Check warning on line 336 in math/src/main/java/com/powsybl/math/graph/UndirectedGraph.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=com.powsybl%3Apowsybl-core&issues=AZwoALjyhjawVvmMZSR3&open=AZwoALjyhjawVvmMZSR3&pullRequest=3753

/**
* Computes the connected components of the graph based on the given traversal rules,
* collecting visited vertex indices into {@link TIntArrayList} partitions.
* <p>
* This is a convenience overload of {@link #computeTraversalPartitions(Traverser, Supplier, ObjIntConsumer)}
* using {@link TIntArrayList} as the partition type.
* </p>
*
* @param traverser the traversal logic that controls which edges to follow and when to stop
* @return a list of {@link TIntArrayList}, one per connected component found;
* may be incomplete if traversal is terminated early by {@link TraverseResult#TERMINATE_TRAVERSER}
* @see #computeTraversalPartitions(Traverser, Supplier, ObjIntConsumer)
*/
default List<TIntArrayList> computeTraversalPartitions(Traverser traverser) {
return computeTraversalPartitions(traverser, TIntArrayList::new, TIntArrayList::add);
}

/**
* Computes the partitions (i.e. connected components) of the graph based on the given traversal rules.
* <p>
* This method iterates over all vertices. For every vertex that has not been encountered yet,
* it creates a new partition using the {@code partitionFactory} and performs a breadth-first traversal.
* During the traversal, each visited vertex index is passed to the {@code vertexCollector} to be
* added to the current partition.
* </p>
* <p>
* If the {@code traverser} returns {@link TraverseResult#TERMINATE_TRAVERSER} during any traversal,
* the computation stops immediately and the returned list may not contain all partitions.
* </p>
*
* @param <C> the type of the partition
* @param traverser the traversal logic that controls which edges to follow and when to stop;
* returning {@link TraverseResult#TERMINATE_PATH} skips a branch while
* {@link TraverseResult#TERMINATE_TRAVERSER} stops the entire computation
* @param partitionFactory a supplier used to create a new empty partition for each connected component
* @param vertexCollector a consumer that receives the current partition and a visited vertex index,
* used to accumulate vertices into the partition
* @return a list of partitions, one per connected component found; may be incomplete if the traversal
* is terminated early by {@link TraverseResult#TERMINATE_TRAVERSER}
*/
<C> List<C> computeTraversalPartitions(Traverser traverser, Supplier<C> partitionFactory, ObjIntConsumer<C> vertexCollector);

/**
* Traverse the entire graph, starting at the specified vertex v.
* This method allocates a boolean array and calls {@link #traverse(int, TraversalType, Traverser, boolean[])}.
Expand Down
Loading