Skip to content

Commit d253438

Browse files
fix: DSV graph should notify updater when cycles are formed or broken
Previously, the updater only considered nodes that were changed from an edge insertion or deletion. However, an edge insertion/deletion can also change the looped status of a disconnected node. For example, if we have a -> b and b -> a, and we remove a -> b, only b is marked as changed. This is incorrect, since a changed indirectly, since it is no longer looped. Thus, the graph implementations now mark any nodes whose SCC change from singleton to multinode (looped) and vice-versa.
1 parent d052cd9 commit d253438

File tree

6 files changed

+473
-7
lines changed

6 files changed

+473
-7
lines changed

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/BaseTopologicalOrderGraph.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public interface BaseTopologicalOrderGraph {
2929

3030
/**
3131
* Returns a tuple containing node ID and a number corresponding to its topological order.
32-
* In particular, after {@link TopologicalOrderGraph#commitChanges()} is called, the following
32+
* In particular, after {@link TopologicalOrderGraph#commitChanges(java.util.BitSet)} is called, the following
3333
* must be true for any pair of nodes A, B where:
3434
* <ul>
3535
* <li>A is a predecessor of B</li>

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/DefaultTopologicalOrderGraph.java

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import java.util.Map;
88
import java.util.PrimitiveIterator;
99
import java.util.Set;
10+
import java.util.stream.Collectors;
1011

1112
import ai.timefold.solver.core.impl.util.CollectionUtils;
1213
import ai.timefold.solver.core.impl.util.MutableInt;
@@ -17,20 +18,27 @@ public class DefaultTopologicalOrderGraph implements TopologicalOrderGraph {
1718
private final Map<Integer, List<Integer>> componentMap;
1819
private final Set<Integer>[] forwardEdges;
1920
private final Set<Integer>[] backEdges;
21+
private final boolean[] isNodeInLoopedComponent;
2022

2123
@SuppressWarnings({ "unchecked" })
2224
public DefaultTopologicalOrderGraph(final int size) {
2325
this.nodeIdToTopologicalOrderMap = new NodeTopologicalOrder[size];
2426
this.componentMap = CollectionUtils.newLinkedHashMap(size);
2527
this.forwardEdges = new Set[size];
2628
this.backEdges = new Set[size];
29+
this.isNodeInLoopedComponent = new boolean[size];
2730
for (var i = 0; i < size; i++) {
2831
forwardEdges[i] = new HashSet<>();
2932
backEdges[i] = new HashSet<>();
33+
isNodeInLoopedComponent[i] = false;
3034
nodeIdToTopologicalOrderMap[i] = new NodeTopologicalOrder(i, i);
3135
}
3236
}
3337

38+
List<Integer> getComponent(int node) {
39+
return componentMap.get(node);
40+
}
41+
3442
@Override
3543
public void addEdge(int fromNode, int toNode) {
3644
forwardEdges[fromNode].add(toNode);
@@ -88,7 +96,7 @@ public NodeTopologicalOrder getTopologicalOrder(int node) {
8896
}
8997

9098
@Override
91-
public void commitChanges() {
99+
public void commitChanges(BitSet changed) {
92100
var index = new MutableInt(1);
93101
var stackIndex = new MutableInt(0);
94102
var size = forwardEdges.length;
@@ -108,12 +116,23 @@ public void commitChanges() {
108116
var ordIndex = 0;
109117
for (var i = components.size() - 1; i >= 0; i--) {
110118
var component = components.get(i);
111-
var componentNodes = new ArrayList<Integer>(component.cardinality());
119+
var componentSize = component.cardinality();
120+
var isComponentLooped = componentSize != 1;
121+
var componentNodes = new ArrayList<Integer>(componentSize);
112122
for (var node = component.nextSetBit(0); node >= 0; node = component.nextSetBit(node + 1)) {
113123
nodeIdToTopologicalOrderMap[node] = new NodeTopologicalOrder(node, ordIndex);
114124
componentNodes.add(node);
115125
componentMap.put(node, componentNodes);
126+
127+
if (isComponentLooped != isNodeInLoopedComponent[node]) {
128+
// It is enough to only mark nodes whose component
129+
// status changed; the updater will notify descendants
130+
// since a looped status change force updates descendants.
131+
isNodeInLoopedComponent[node] = isComponentLooped;
132+
changed.set(node);
133+
}
116134
ordIndex++;
135+
117136
if (node == Integer.MAX_VALUE) {
118137
break;
119138
}
@@ -160,4 +179,19 @@ private void strongConnect(int node, MutableInt index, MutableInt stackIndex, in
160179
components.add(out);
161180
}
162181
}
182+
183+
@Override
184+
public String toString() {
185+
var out = new StringBuilder();
186+
out.append("DefaultTopologicalOrderGraph{\n");
187+
for (var node = 0; node < forwardEdges.length; node++) {
188+
out.append(" ").append(node).append("(").append(nodeIdToTopologicalOrderMap[node].order()).append(") -> ")
189+
.append(forwardEdges[node].stream()
190+
.sorted()
191+
.map(Object::toString)
192+
.collect(Collectors.joining(",", "[", "]\n")));
193+
}
194+
out.append("}");
195+
return out.toString();
196+
}
163197
}

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/DefaultVariableReferenceGraph.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ public void updateChanged() {
126126
if (changed.isEmpty()) {
127127
return;
128128
}
129-
graph.commitChanges();
129+
graph.commitChanges(changed);
130130
affectedEntitiesUpdater.accept(changed);
131131
}
132132

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/TopologicalOrderGraph.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package ai.timefold.solver.core.impl.domain.variable.declarative;
22

3+
import java.util.BitSet;
34
import java.util.List;
45

56
public interface TopologicalOrderGraph extends BaseTopologicalOrderGraph {
@@ -9,7 +10,7 @@ public interface TopologicalOrderGraph extends BaseTopologicalOrderGraph {
910
* After this method returns, {@link #getTopologicalOrder(int)}
1011
* must be accurate for every node in the graph.
1112
*/
12-
void commitChanges();
13+
void commitChanges(BitSet changed);
1314

1415
/**
1516
* Called on graph creation to supply metadata about the graph nodes.
@@ -22,7 +23,7 @@ default <Solution_> void withNodeData(List<EntityVariablePair<Solution_>> nodes)
2223

2324
/**
2425
* Called when a graph edge is added.
25-
* The operation is added to a batch and only executed when {@link #commitChanges()} is called.
26+
* The operation is added to a batch and only executed when {@link #commitChanges(BitSet)} is called.
2627
* <p>
2728
* {@link #getTopologicalOrder(int)} is allowed to be invalid
2829
* when this method returns.
@@ -31,7 +32,7 @@ default <Solution_> void withNodeData(List<EntityVariablePair<Solution_>> nodes)
3132

3233
/**
3334
* Called when a graph edge is removed.
34-
* The operation is added to a batch and only executed when {@link #commitChanges()} is called.
35+
* The operation is added to a batch and only executed when {@link #commitChanges(BitSet)} is called.
3536
* <p>
3637
* {@link #getTopologicalOrder(int)} is allowed to be invalid
3738
* when this method returns.

0 commit comments

Comments
 (0)