Skip to content

Commit 2c9bb0a

Browse files
fix: process each entity only once and in topological order (#1662)
Previously, the code relied on eventually consistency to avoid collections (that is, sometimes suppliers will be called with stale data, but will always eventually be called with correct data). Now, an initial sort is done to ensure no data is stale, and a map and comparator are utilized to prevent unnecessary computations. --------- Co-authored-by: Lukáš Petrovický <[email protected]>
1 parent 81374a6 commit 2c9bb0a

14 files changed

+372
-49
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
public abstract sealed class AbstractVariableReferenceGraph<Solution_, ChangeSet_> implements VariableReferenceGraph
2020
permits DefaultVariableReferenceGraph, FixedVariableReferenceGraph {
21+
2122
// These structures are immutable.
2223
protected final List<EntityVariablePair<Solution_>> instanceList;
2324
protected final Map<VariableMetaModel<?, ?, ?>, Map<Object, EntityVariablePair<Solution_>>> variableReferenceToInstanceMap;

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

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,14 @@
1414
import java.util.Objects;
1515
import java.util.Set;
1616
import java.util.function.IntFunction;
17-
import java.util.function.UnaryOperator;
1817

1918
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;
2019
import ai.timefold.solver.core.impl.domain.variable.descriptor.VariableDescriptor;
21-
import ai.timefold.solver.core.impl.domain.variable.inverserelation.InverseRelationShadowVariableDescriptor;
2220
import ai.timefold.solver.core.impl.score.director.InnerScoreDirector;
2321
import ai.timefold.solver.core.preview.api.domain.metamodel.VariableMetaModel;
2422

2523
import org.jspecify.annotations.NonNull;
2624
import org.jspecify.annotations.NullMarked;
27-
import org.jspecify.annotations.Nullable;
2825
import org.slf4j.Logger;
2926
import org.slf4j.LoggerFactory;
3027

@@ -44,7 +41,6 @@ public DefaultShadowVariableSessionFactory(
4441
this.graphCreator = graphCreator;
4542
}
4643

47-
@SuppressWarnings("unchecked")
4844
public static <Solution_> VariableReferenceGraph buildGraph(
4945
SolutionDescriptor<Solution_> solutionDescriptor,
5046
VariableReferenceGraphBuilder<Solution_> variableReferenceGraphBuilder, Object[] entities,
@@ -68,21 +64,21 @@ yield buildSingleDirectionalParentGraph(solutionDescriptor,
6864
};
6965
}
7066

71-
private static <Solution_> VariableReferenceGraph buildSingleDirectionalParentGraph(
67+
static <Solution_> VariableReferenceGraph buildSingleDirectionalParentGraph(
7268
SolutionDescriptor<Solution_> solutionDescriptor,
7369
ChangedVariableNotifier<Solution_> changedVariableNotifier,
7470
GraphStructure.GraphStructureAndDirection graphStructureAndDirection,
7571
Object[] entities) {
7672
var declarativeShadowVariables = solutionDescriptor.getDeclarativeShadowVariableDescriptors();
7773
var sortedDeclarativeVariables = topologicallySortedDeclarativeShadowVariables(declarativeShadowVariables);
7874

79-
var successorFunction =
80-
getSuccessorFunction(solutionDescriptor, Objects.requireNonNull(changedVariableNotifier.innerScoreDirector()),
81-
Objects.requireNonNull(graphStructureAndDirection.parentMetaModel()),
75+
var topologicalSorter =
76+
getTopologicalSorter(solutionDescriptor,
77+
Objects.requireNonNull(changedVariableNotifier.innerScoreDirector()),
8278
Objects.requireNonNull(graphStructureAndDirection.direction()));
8379

84-
return new SingleDirectionalParentVariableReferenceGraph<>(sortedDeclarativeVariables, successorFunction,
85-
changedVariableNotifier, entities);
80+
return new SingleDirectionalParentVariableReferenceGraph<>(sortedDeclarativeVariables,
81+
topologicalSorter, changedVariableNotifier, entities);
8682
}
8783

8884
private static <Solution_> @NonNull List<DeclarativeShadowVariableDescriptor<Solution_>>
@@ -120,21 +116,21 @@ private static <Solution_> VariableReferenceGraph buildSingleDirectionalParentGr
120116
return sortedDeclarativeVariables;
121117
}
122118

123-
private static <Solution_> @NonNull UnaryOperator<@Nullable Object> getSuccessorFunction(
124-
SolutionDescriptor<Solution_> solutionDescriptor, InnerScoreDirector<Solution_, ?> scoreDirector,
125-
VariableMetaModel<?, ?, ?> parentMetaModel, ParentVariableType parentVariableType) {
119+
private static <Solution_> TopologicalSorter getTopologicalSorter(SolutionDescriptor<Solution_> solutionDescriptor,
120+
InnerScoreDirector<Solution_, ?> scoreDirector, ParentVariableType parentVariableType) {
126121
return switch (parentVariableType) {
127-
case PREVIOUS ->
128-
scoreDirector.getListVariableStateSupply(solutionDescriptor.getListVariableDescriptor())::getNextElement;
129-
case NEXT ->
130-
scoreDirector.getListVariableStateSupply(solutionDescriptor.getListVariableDescriptor())::getPreviousElement;
131-
case CHAINED_NEXT -> {
132-
var entityDescriptor = solutionDescriptor.getEntityDescriptorStrict(parentMetaModel.entity().type());
133-
var inverseVariable = (InverseRelationShadowVariableDescriptor<?>) entityDescriptor
134-
.getShadowVariableDescriptor(parentMetaModel.name());
135-
var sourceVariable = inverseVariable.getSourceVariableDescriptorList().get(0);
136-
var entityType = sourceVariable.getEntityDescriptor().getEntityClass();
137-
yield old -> entityType.isInstance(old) ? sourceVariable.getValue(old) : null;
122+
case PREVIOUS -> {
123+
var listStateSupply = scoreDirector.getListVariableStateSupply(solutionDescriptor.getListVariableDescriptor());
124+
yield new TopologicalSorter(listStateSupply::getNextElement,
125+
Comparator.comparingInt(entity -> Objects.requireNonNullElse(listStateSupply.getIndex(entity), 0)),
126+
listStateSupply::getInverseSingleton);
127+
}
128+
case NEXT -> {
129+
var listStateSupply = scoreDirector.getListVariableStateSupply(solutionDescriptor.getListVariableDescriptor());
130+
yield new TopologicalSorter(listStateSupply::getPreviousElement,
131+
Comparator.comparingInt(entity -> Objects.requireNonNullElse(listStateSupply.getIndex(entity), 0))
132+
.reversed(),
133+
listStateSupply::getInverseSingleton);
138134
}
139135
default -> throw new IllegalStateException(
140136
"Impossible state: expected parentVariableType to be previous or next but was %s."
@@ -150,7 +146,7 @@ private static <Solution_> VariableReferenceGraph buildArbitraryGraph(
150146
var variableIdToUpdater = new HashMap<VariableMetaModel<?, ?, ?>, VariableUpdaterInfo<Solution_>>();
151147

152148
// Create graph node for each entity/declarative shadow variable pair.
153-
// Maps a variable id to it source aliases;
149+
// Maps a variable id to its source aliases;
154150
// For instance, "previousVisit.startTime" is a source alias of "startTime"
155151
// One way to view this concept is "previousVisit.startTime" is a pointer
156152
// to "startTime" of some visit, and thus alias it.

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@
99

1010
import org.jspecify.annotations.NonNull;
1111

12-
final class DefaultVariableReferenceGraph<Solution_> extends AbstractVariableReferenceGraph<Solution_, BitSet>
13-
implements VariableReferenceGraph {
12+
final class DefaultVariableReferenceGraph<Solution_> extends AbstractVariableReferenceGraph<Solution_, BitSet> {
1413
// These structures are mutable.
1514
private final Consumer<BitSet> affectedEntitiesUpdater;
1615

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88
import org.jspecify.annotations.NonNull;
99

1010
public final class FixedVariableReferenceGraph<Solution_>
11-
extends AbstractVariableReferenceGraph<Solution_, PriorityQueue<BaseTopologicalOrderGraph.NodeTopologicalOrder>>
12-
implements VariableReferenceGraph {
11+
extends AbstractVariableReferenceGraph<Solution_, PriorityQueue<BaseTopologicalOrderGraph.NodeTopologicalOrder>> {
1312
// These are immutable
1413
private final ChangedVariableNotifier<Solution_> changedVariableNotifier;
1514

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,12 @@ public static <Solution_> GraphStructureAndDirection determineGraphStructure(
8080
}
8181
// The group variable is unused/always empty
8282
}
83-
case INDIRECT, INVERSE, VARIABLE -> {
83+
case INDIRECT, INVERSE, VARIABLE, CHAINED_NEXT -> {
84+
// CHAINED_NEXT has a complex comparator function;
85+
// so use ARBITRARY despite the fact it can be represented using SINGLE_DIRECTIONAL_PARENT
8486
return new GraphStructureAndDirection(ARBITRARY, null, null);
8587
}
86-
case NEXT, PREVIOUS, CHAINED_NEXT -> {
88+
case NEXT, PREVIOUS -> {
8789
if (parentMetaModel == null) {
8890
parentMetaModel = variableSource.variableSourceReferences().get(0).variableMetaModel();
8991
directionalType = parentVariableType;

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

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import java.util.ArrayList;
44
import java.util.Arrays;
5+
import java.util.Comparator;
56
import java.util.HashSet;
7+
import java.util.IdentityHashMap;
68
import java.util.List;
79
import java.util.Objects;
810
import java.util.Set;
@@ -11,9 +13,12 @@
1113
import ai.timefold.solver.core.preview.api.domain.metamodel.VariableMetaModel;
1214

1315
public final class SingleDirectionalParentVariableReferenceGraph<Solution_> implements VariableReferenceGraph {
16+
1417
private final Set<VariableMetaModel<?, ?, ?>> monitoredSourceVariableSet;
1518
private final VariableUpdaterInfo<Solution_>[] sortedVariableUpdaterInfos;
1619
private final UnaryOperator<Object> successorFunction;
20+
private final Comparator<Object> topologicalOrderComparator;
21+
private final UnaryOperator<Object> keyFunction;
1722
private final ChangedVariableNotifier<Solution_> changedVariableNotifier;
1823
private final List<Object> changedEntities;
1924
private final Class<?> monitoredEntityClass;
@@ -22,7 +27,7 @@ public final class SingleDirectionalParentVariableReferenceGraph<Solution_> impl
2227
@SuppressWarnings("unchecked")
2328
public SingleDirectionalParentVariableReferenceGraph(
2429
List<DeclarativeShadowVariableDescriptor<Solution_>> sortedDeclarativeShadowVariableDescriptors,
25-
UnaryOperator<Object> successorFunction,
30+
TopologicalSorter topologicalSorter,
2631
ChangedVariableNotifier<Solution_> changedVariableNotifier,
2732
Object[] entities) {
2833
monitoredEntityClass = sortedDeclarativeShadowVariableDescriptors.get(0).getEntityDescriptor().getEntityClass();
@@ -31,9 +36,12 @@ public SingleDirectionalParentVariableReferenceGraph(
3136
changedEntities = new ArrayList<>();
3237
isUpdating = false;
3338

34-
this.successorFunction = successorFunction;
39+
this.successorFunction = topologicalSorter.successor();
40+
this.topologicalOrderComparator = topologicalSorter.comparator();
41+
this.keyFunction = topologicalSorter.key();
3542
this.changedVariableNotifier = changedVariableNotifier;
36-
var shadowEntities = Arrays.stream(entities).filter(monitoredEntityClass::isInstance).toArray();
43+
var shadowEntities = Arrays.stream(entities).filter(monitoredEntityClass::isInstance)
44+
.sorted(topologicalOrderComparator).toArray();
3745
var loopedDescriptor =
3846
sortedDeclarativeShadowVariableDescriptors.get(0).getEntityDescriptor().getShadowVariableLoopedDescriptor();
3947

@@ -55,9 +63,9 @@ public SingleDirectionalParentVariableReferenceGraph(
5563
}
5664
}
5765

58-
for (var shadowEntity : shadowEntities) {
59-
updateChanged(shadowEntity);
60-
}
66+
changedEntities.addAll(List.of(shadowEntities));
67+
updateChanged();
68+
6169
if (loopedDescriptor != null) {
6270
for (var shadowEntity : shadowEntities) {
6371
changedVariableNotifier.beforeVariableChanged().accept(loopedDescriptor, shadowEntity);
@@ -70,15 +78,29 @@ public SingleDirectionalParentVariableReferenceGraph(
7078
@Override
7179
public void updateChanged() {
7280
isUpdating = true;
81+
changedEntities.sort(topologicalOrderComparator);
82+
var processed = new IdentityHashMap<>();
7383
for (var changedEntity : changedEntities) {
74-
updateChanged(changedEntity);
84+
var key = keyFunction.apply(changedEntity);
85+
var lastProcessed = processed.get(key);
86+
if (lastProcessed == null || topologicalOrderComparator.compare(lastProcessed, changedEntity) < 0) {
87+
lastProcessed = updateChanged(changedEntity);
88+
processed.put(key, lastProcessed);
89+
}
7590
}
7691
isUpdating = false;
7792
changedEntities.clear();
7893
}
7994

80-
private void updateChanged(Object entity) {
95+
/**
96+
* Update entities and its successor until one of them does not change.
97+
*
98+
* @param entity The first entity to process.
99+
* @return The last processed entity (i.e. the first entity that did not change).
100+
*/
101+
private Object updateChanged(Object entity) {
81102
var current = entity;
103+
var previous = current;
82104
while (current != null) {
83105
var anyChanged = false;
84106
for (var updater : sortedVariableUpdaterInfos) {
@@ -92,11 +114,13 @@ private void updateChanged(Object entity) {
92114
}
93115
}
94116
if (anyChanged) {
117+
previous = current;
95118
current = successorFunction.apply(current);
96119
} else {
97-
current = null;
120+
return current;
98121
}
99122
}
123+
return previous;
100124
}
101125

102126
@Override
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package ai.timefold.solver.core.impl.domain.variable.declarative;
2+
3+
import java.util.Comparator;
4+
import java.util.function.UnaryOperator;
5+
6+
import org.jspecify.annotations.NullMarked;
7+
import org.jspecify.annotations.Nullable;
8+
9+
@NullMarked
10+
public record TopologicalSorter(UnaryOperator<@Nullable Object> successor,
11+
Comparator<Object> comparator,
12+
UnaryOperator<Object> key) {
13+
}

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
import ai.timefold.solver.core.preview.api.domain.metamodel.VariableMetaModel;
44

55
public sealed interface VariableReferenceGraph
6-
permits AbstractVariableReferenceGraph, DefaultVariableReferenceGraph, EmptyVariableReferenceGraph,
7-
FixedVariableReferenceGraph, SingleDirectionalParentVariableReferenceGraph {
6+
permits AbstractVariableReferenceGraph, EmptyVariableReferenceGraph, SingleDirectionalParentVariableReferenceGraph {
87

98
void updateChanged();
109

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

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,7 @@ public void addAfterProcessor(GraphChangeType graphChangeType, VariableMetaModel
7070
.add(consumer);
7171
}
7272

73-
@SuppressWarnings("unchecked")
7473
public VariableReferenceGraph build(IntFunction<TopologicalOrderGraph> graphCreator) {
75-
// TODO empty shows up in VRP example when using it as CVRP, not CVRPTW
76-
// In that case, TimeWindowedCustomer does not exist
77-
// and therefore Customer has no shadow variable.
78-
// Surely there has to be an earlier way to catch this?
7974
if (instanceList.isEmpty()) {
8075
return EmptyVariableReferenceGraph.INSTANCE;
8176
}

core/src/test/java/ai/timefold/solver/core/impl/domain/variable/declarative/GraphStructureTest.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,7 @@ void simpleListStructure() {
3232
void simpleChainedStructure() {
3333
assertThat(GraphStructure.determineGraphStructure(
3434
TestdataChainedSimpleVarSolution.buildSolutionDescriptor()))
35-
.hasFieldOrPropertyWithValue("structure", SINGLE_DIRECTIONAL_PARENT)
36-
.hasFieldOrPropertyWithValue("direction", ParentVariableType.CHAINED_NEXT);
35+
.hasFieldOrPropertyWithValue("structure", ARBITRARY);
3736
}
3837

3938
@Test

0 commit comments

Comments
 (0)