Skip to content
Open
Show file tree
Hide file tree
Changes from 20 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,36 @@ 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.getConnectedComponents(
(v1, e, v2) -> {
SwitchImpl sw = graph.getEdgeObject(e);
if (sw != null && sw.isOpen()) {
return TraverseResult.TERMINATE_PATH;
}
return TraverseResult.CONTINUE;
},
new UndirectedGraph.ConnectedComponentCollector<>() {
@Override
public List<ConfiguredBus> createComponent() {
return new ArrayList<>();
}

@Override
public void addVertex(List<ConfiguredBus> component, int vertexIndex) {
ConfiguredBus bus = graph.getVertexObject(vertexIndex);
component.add(bus);
}
}
);

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 @@ -110,9 +107,9 @@ protected NetworkImpl getNetwork() {

protected static int loadDcNodeIndexLimit(PlatformConfig platformConfig) {
return platformConfig
.getOptionalModuleConfig("iidm")
.map(moduleConfig -> moduleConfig.getIntProperty("dc-node-index-limit", DEFAULT_DC_NODE_INDEX_LIMIT))
.orElse(DEFAULT_DC_NODE_INDEX_LIMIT);
.getOptionalModuleConfig("iidm")
.map(moduleConfig -> moduleConfig.getIntProperty("dc-node-index-limit", DEFAULT_DC_NODE_INDEX_LIMIT))
.orElse(DEFAULT_DC_NODE_INDEX_LIMIT);
}

private Integer getVertex(String dcNodeId) {
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,33 @@ 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.getConnectedComponents(
(v1, e, v2) -> {
DcSwitchImpl sw = graph.getEdgeObject(e);
if (sw != null && sw.isOpen()) {
return TraverseResult.TERMINATE_PATH;
}
return TraverseResult.CONTINUE;
Copy link
Collaborator

Choose a reason for hiding this comment

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

An edge cannot be null in DcTopologyModel

Suggested change
if (sw != null && sw.isOpen()) {
return TraverseResult.TERMINATE_PATH;
}
return TraverseResult.CONTINUE;
return sw.isOpen() ? TraverseResult.TERMINATE_PATH : TraverseResult.CONTINUE;

},
new UndirectedGraph.ConnectedComponentCollector<>() {
@Override
public List<DcNodeImpl> createComponent() {
return new ArrayList<>();
}

@Override
public void addVertex(List<DcNodeImpl> component, int vertexIndex) {
DcNodeImpl dcNode = graph.getVertexObject(vertexIndex);
component.add(dcNode);
}
}
);

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.getConnectedComponents((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
24 changes: 24 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 @@ -328,9 +328,33 @@ public interface UndirectedGraph<V, E> {
* </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 #getConnectedComponents(Traverser)} or {@link #traverse(int, TraversalType, Traverser)} instead.
*/
@Deprecated(since = "7.2.0")
boolean traverse(int v, TraversalType traversalType, Traverser traverser, boolean[] verticesEncountered);

default List<TIntArrayList> getConnectedComponents(Traverser traverser) {
return getConnectedComponents(traverser, new ConnectedComponentCollector<TIntArrayList>() {
@Override
public TIntArrayList createComponent() {
return new TIntArrayList();
}

@Override
public void addVertex(TIntArrayList component, int vertexIndex) {
component.add(vertexIndex);
}
});
}

<C> List<C> getConnectedComponents(Traverser traverser, ConnectedComponentCollector<C> collector);

interface ConnectedComponentCollector<C> {
C createComponent();

void addVertex(C component, int vertexIndex);
}

/**
* 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