Skip to content

Commit 6c3c668

Browse files
perf: Use a single node to represent multiple shadow variables in graphs (#1673)
When there is only a single declarative entity and all non-declarative parents go in the same direction (ex: previous), then some variables can share the same node in the graph. In particular, there is a static topological order when only the variables on a single entity is considered, and as long as the next declarative shadow variable does not have any new non-declarative parents (i.e. group), it can be grouped into the same node as the previous one. All nodes share the same node if no groups are used. Also fixed a bug in the topological sort done to determine variable trigger order when groups are used (previously it added group variables, which caused it to create a cycle and make all topological orders valid). --------- Co-authored-by: Lukáš Petrovický <[email protected]>
1 parent 020775e commit 6c3c668

20 files changed

+1018
-71
lines changed

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

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public void accept(BitSet changed) {
5151
}
5252
visited.set(nextNode);
5353
var shadowVariable = instanceList.get(nextNode);
54-
var isChanged = updateShadowVariable(shadowVariable, graph.isLooped(loopedTracker, nextNode));
54+
var isChanged = updateEntityShadowVariables(shadowVariable, graph.isLooped(loopedTracker, nextNode));
5555

5656
if (isChanged) {
5757
var iterator = graph.nodeForwardEdges(nextNode);
@@ -96,7 +96,7 @@ private void updateLoopedStatusOfAffectedEntity(Object affectedEntity) {
9696
for (var node : entityVariablePairFunction.apply(affectedEntity)) {
9797
// All variables come from the same entity,
9898
// therefore all have the same looped marker.
99-
shadowVariableLoopedDescriptor = node.variableReference().shadowVariableLoopedDescriptor();
99+
shadowVariableLoopedDescriptor = node.variableReferences().get(0).shadowVariableLoopedDescriptor();
100100
if (graph.isLooped(loopedTracker, node.graphNodeId())) {
101101
isEntityLooped = true;
102102
break;
@@ -114,19 +114,31 @@ private void updateLoopedStatusOfAffectedEntity(Object affectedEntity) {
114114

115115
}
116116

117-
private boolean updateShadowVariable(EntityVariablePair<Solution_> entityVariable, boolean isLooped) {
117+
private boolean updateEntityShadowVariables(EntityVariablePair<Solution_> entityVariable, boolean isLooped) {
118118
var entity = entityVariable.entity();
119-
var shadowVariableReference = entityVariable.variableReference();
120-
var oldValue = shadowVariableReference.memberAccessor().executeGetter(entity);
121-
var loopDescriptor = shadowVariableReference.shadowVariableLoopedDescriptor();
119+
var shadowVariableReferences = entityVariable.variableReferences();
120+
var loopDescriptor = shadowVariableReferences.get(0).shadowVariableLoopedDescriptor();
121+
var anyChanged = false;
122+
122123
if (loopDescriptor != null) {
123-
var oldLooped = (boolean) loopDescriptor.getValue(entity);
124-
if (oldLooped != isLooped) {
124+
var oldLooped = loopDescriptor.getValue(entity);
125+
if (!Objects.equals(oldLooped, isLooped)) {
125126
// Loop status change; add to affected entities
126127
affectedEntities.add(entityVariable);
128+
anyChanged = true;
127129
}
128130
}
129131

132+
for (var shadowVariableReference : shadowVariableReferences) {
133+
anyChanged |= updateShadowVariable(entityVariable, isLooped, shadowVariableReference, entity);
134+
}
135+
136+
return anyChanged;
137+
}
138+
139+
private boolean updateShadowVariable(EntityVariablePair<Solution_> entityVariable, boolean isLooped,
140+
VariableUpdaterInfo<Solution_> shadowVariableReference, Object entity) {
141+
var oldValue = shadowVariableReference.memberAccessor().executeGetter(entity);
130142
if (isLooped) {
131143
if (oldValue != null) {
132144
affectedEntities.add(entityVariable);
@@ -168,7 +180,7 @@ public AffectedEntities(Consumer<Object> consumer) {
168180
}
169181

170182
public void add(EntityVariablePair<Solution_> shadowVariable) {
171-
var shadowVariableLoopedDescriptor = shadowVariable.variableReference().shadowVariableLoopedDescriptor();
183+
var shadowVariableLoopedDescriptor = shadowVariable.variableReferences().get(0).shadowVariableLoopedDescriptor();
172184
if (shadowVariableLoopedDescriptor == null) {
173185
return;
174186
}

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

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

33
import java.util.ArrayList;
4+
import java.util.Arrays;
45
import java.util.BitSet;
56
import java.util.Collections;
67
import java.util.Comparator;
@@ -15,12 +16,13 @@
1516
import java.util.Set;
1617
import java.util.function.IntFunction;
1718

19+
import ai.timefold.solver.core.api.function.TriFunction;
1820
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;
1921
import ai.timefold.solver.core.impl.domain.variable.descriptor.VariableDescriptor;
2022
import ai.timefold.solver.core.impl.score.director.InnerScoreDirector;
23+
import ai.timefold.solver.core.impl.util.MutableInt;
2124
import ai.timefold.solver.core.preview.api.domain.metamodel.VariableMetaModel;
2225

23-
import org.jspecify.annotations.NonNull;
2426
import org.jspecify.annotations.NullMarked;
2527
import org.slf4j.Logger;
2628
import org.slf4j.LoggerFactory;
@@ -59,6 +61,8 @@ yield buildSingleDirectionalParentGraph(solutionDescriptor,
5961
graphStructureAndDirection,
6062
entities);
6163
}
64+
case ARBITRARY_SINGLE_ENTITY_SINGLE_DIRECTIONAL_PARENT_TYPE ->
65+
buildArbitrarySingleEntityGraph(solutionDescriptor, variableReferenceGraphBuilder, entities, graphCreator);
6266
case NO_DYNAMIC_EDGES, ARBITRARY ->
6367
buildArbitraryGraph(solutionDescriptor, variableReferenceGraphBuilder, entities, graphCreator);
6468
};
@@ -81,7 +85,7 @@ static <Solution_> VariableReferenceGraph buildSingleDirectionalParentGraph(
8185
topologicalSorter, changedVariableNotifier, entities);
8286
}
8387

84-
private static <Solution_> @NonNull List<DeclarativeShadowVariableDescriptor<Solution_>>
88+
private static <Solution_> List<DeclarativeShadowVariableDescriptor<Solution_>>
8589
topologicallySortedDeclarativeShadowVariables(
8690
List<DeclarativeShadowVariableDescriptor<Solution_>> declarativeShadowVariables) {
8791
Map<String, Integer> nameToIndex = new LinkedHashMap<>();
@@ -94,8 +98,10 @@ static <Solution_> VariableReferenceGraph buildSingleDirectionalParentGraph(
9498
var visited = new HashSet<Integer>();
9599
for (var source : declarativeShadowVariable.getSources()) {
96100
var variableReferences = source.variableSourceReferences();
97-
if (variableReferences.size() != 1) {
98-
// variableReferences is from directional variable
101+
if (source.parentVariableType() != ParentVariableType.NO_PARENT) {
102+
// We only look at direct usage; if we also added
103+
// edges for groups/directional, we will end up creating a cycle
104+
// which makes all topological orders valid
99105
continue;
100106
}
101107
var variableReference = variableReferences.get(0);
@@ -143,7 +149,7 @@ private static <Solution_> VariableReferenceGraph buildArbitraryGraph(
143149
VariableReferenceGraphBuilder<Solution_> variableReferenceGraphBuilder, Object[] entities,
144150
IntFunction<TopologicalOrderGraph> graphCreator) {
145151
var declarativeShadowVariableDescriptors = solutionDescriptor.getDeclarativeShadowVariableDescriptors();
146-
var variableIdToUpdater = new HashMap<VariableMetaModel<?, ?, ?>, VariableUpdaterInfo<Solution_>>();
152+
var variableIdToUpdater = EntityVariableUpdaterLookup.<Solution_> entityIndependentLookup();
147153

148154
// Create graph node for each entity/declarative shadow variable pair.
149155
// Maps a variable id to its source aliases;
@@ -152,7 +158,16 @@ private static <Solution_> VariableReferenceGraph buildArbitraryGraph(
152158
// to "startTime" of some visit, and thus alias it.
153159
var declarativeShadowVariableToAliasMap = createGraphNodes(variableReferenceGraphBuilder, entities,
154160
declarativeShadowVariableDescriptors, variableIdToUpdater);
161+
return buildVariableReferenceGraph(declarativeShadowVariableDescriptors, variableReferenceGraphBuilder,
162+
declarativeShadowVariableToAliasMap,
163+
graphCreator, entities);
164+
}
155165

166+
private static <Solution_> VariableReferenceGraph buildVariableReferenceGraph(
167+
List<DeclarativeShadowVariableDescriptor<Solution_>> declarativeShadowVariableDescriptors,
168+
VariableReferenceGraphBuilder<Solution_> variableReferenceGraphBuilder,
169+
Map<VariableMetaModel<?, ?, ?>, Set<VariableSourceReference>> declarativeShadowVariableToAliasMap,
170+
IntFunction<TopologicalOrderGraph> graphCreator, Object... entities) {
156171
// Create variable processors for each declarative shadow variable descriptor
157172
for (var declarativeShadowVariable : declarativeShadowVariableDescriptors) {
158173
var fromVariableId = declarativeShadowVariable.getVariableMetaModel();
@@ -168,23 +183,130 @@ private static <Solution_> VariableReferenceGraph buildArbitraryGraph(
168183
return variableReferenceGraphBuilder.build(graphCreator);
169184
}
170185

186+
private record GroupVariableUpdaterInfo<Solution_>(
187+
List<DeclarativeShadowVariableDescriptor<Solution_>> sortedDeclarativeVariableDescriptors,
188+
List<VariableUpdaterInfo<Solution_>> allUpdaters,
189+
List<VariableUpdaterInfo<Solution_>> groupedUpdaters) {
190+
191+
public List<VariableUpdaterInfo<Solution_>> getUpdatersForEntity(Object entity) {
192+
for (var shadowVariableDescriptor : sortedDeclarativeVariableDescriptors) {
193+
for (var rootSource : shadowVariableDescriptor.getSources()) {
194+
if (rootSource.parentVariableType() == ParentVariableType.GROUP) {
195+
var visitedCount = new MutableInt();
196+
rootSource.valueEntityFunction().accept(entity, ignored -> visitedCount.increment());
197+
if (visitedCount.intValue() > 0) {
198+
return groupedUpdaters;
199+
}
200+
}
201+
}
202+
}
203+
return allUpdaters;
204+
}
205+
206+
}
207+
208+
private static <Solution_> Map<VariableMetaModel<Solution_, ?, ?>, GroupVariableUpdaterInfo<Solution_>>
209+
getGroupVariableUpdaterInfoMap(
210+
List<DeclarativeShadowVariableDescriptor<Solution_>> declarativeShadowVariableDescriptors) {
211+
var sortedDeclarativeVariableDescriptors =
212+
topologicallySortedDeclarativeShadowVariables(declarativeShadowVariableDescriptors);
213+
var groupIndexToVariables = new HashMap<Integer, List<DeclarativeShadowVariableDescriptor<Solution_>>>();
214+
var groupVariables = new ArrayList<DeclarativeShadowVariableDescriptor<Solution_>>();
215+
groupIndexToVariables.put(0, groupVariables);
216+
for (var declarativeShadowVariableDescriptor : sortedDeclarativeVariableDescriptors) {
217+
if (!groupVariables.isEmpty() && Arrays.stream(declarativeShadowVariableDescriptor.getSources())
218+
.anyMatch(rootVariableSource -> rootVariableSource.parentVariableType() == ParentVariableType.GROUP)) {
219+
// Create a new variable group, since the group might reference prior variables
220+
groupVariables = new ArrayList<>();
221+
groupIndexToVariables.put(groupIndexToVariables.size(), groupVariables);
222+
}
223+
groupVariables.add(declarativeShadowVariableDescriptor);
224+
}
225+
226+
var out = new HashMap<VariableMetaModel<Solution_, ?, ?>, GroupVariableUpdaterInfo<Solution_>>();
227+
var allUpdaters = new ArrayList<VariableUpdaterInfo<Solution_>>();
228+
for (var entryKey = 0; entryKey < groupIndexToVariables.size(); entryKey++) {
229+
var entryGroupVariables = groupIndexToVariables.get(entryKey);
230+
var updaters = new ArrayList<VariableUpdaterInfo<Solution_>>();
231+
for (var declarativeShadowVariableDescriptor : entryGroupVariables) {
232+
var updater = new VariableUpdaterInfo<>(
233+
declarativeShadowVariableDescriptor.getVariableMetaModel(),
234+
entryKey,
235+
declarativeShadowVariableDescriptor,
236+
declarativeShadowVariableDescriptor.getEntityDescriptor().getShadowVariableLoopedDescriptor(),
237+
declarativeShadowVariableDescriptor.getMemberAccessor(),
238+
declarativeShadowVariableDescriptor.getCalculator()::executeGetter);
239+
updaters.add(updater);
240+
allUpdaters.add(updater);
241+
}
242+
var groupVariableUpdaterInfo =
243+
new GroupVariableUpdaterInfo<Solution_>(sortedDeclarativeVariableDescriptors, allUpdaters, updaters);
244+
for (var declarativeShadowVariableDescriptor : entryGroupVariables) {
245+
out.put(declarativeShadowVariableDescriptor.getVariableMetaModel(), groupVariableUpdaterInfo);
246+
}
247+
}
248+
allUpdaters.replaceAll(updater -> updater.withGroupId(groupIndexToVariables.size()));
249+
return out;
250+
}
251+
252+
private static <Solution_> VariableReferenceGraph buildArbitrarySingleEntityGraph(
253+
SolutionDescriptor<Solution_> solutionDescriptor,
254+
VariableReferenceGraphBuilder<Solution_> variableReferenceGraphBuilder, Object[] entities,
255+
IntFunction<TopologicalOrderGraph> graphCreator) {
256+
var declarativeShadowVariableDescriptors = solutionDescriptor.getDeclarativeShadowVariableDescriptors();
257+
// Use a dependent lookup; if an entity does not use groups, then all variables can share the same node.
258+
// If the entity use groups, then variables must be grouped into their own nodes.
259+
var variableIdToUpdater = EntityVariableUpdaterLookup.<Solution_> entityDependentLookup();
260+
261+
// Create graph node for each entity/declarative shadow variable group pair.
262+
// Maps a variable id to the source aliases of all variables in its group;
263+
// If the variables are (in topological order)
264+
// arrivalTime, readyTime, serviceStartTime, serviceFinishTime,
265+
// where serviceStartTime depends on a group of readyTime, then
266+
// the groups are [arrivalTime, readyTime] and [serviceStartTime, serviceFinishTime]
267+
// this is because from arrivalTime, you can compute readyTime without knowing either
268+
// serviceStartTime or serviceFinishTime.
269+
var variableIdToGroupedUpdater = getGroupVariableUpdaterInfoMap(declarativeShadowVariableDescriptors);
270+
var declarativeShadowVariableToAliasMap = createGraphNodes(variableReferenceGraphBuilder, entities,
271+
declarativeShadowVariableDescriptors, variableIdToUpdater,
272+
(entity, declarativeShadowVariable, variableId) -> variableIdToGroupedUpdater.get(variableId)
273+
.getUpdatersForEntity(entity));
274+
return buildVariableReferenceGraph(declarativeShadowVariableDescriptors, variableReferenceGraphBuilder,
275+
declarativeShadowVariableToAliasMap,
276+
graphCreator, entities);
277+
}
278+
279+
private static <Solution_> Map<VariableMetaModel<?, ?, ?>, Set<VariableSourceReference>> createGraphNodes(
280+
VariableReferenceGraphBuilder<Solution_> graph, Object[] entities,
281+
List<DeclarativeShadowVariableDescriptor<Solution_>> declarativeShadowVariableDescriptors,
282+
EntityVariableUpdaterLookup<Solution_> variableIdToUpdaters) {
283+
return createGraphNodes(graph, entities, declarativeShadowVariableDescriptors, variableIdToUpdaters,
284+
(entity, declarativeShadowVariableDescriptor,
285+
variableId) -> Collections.singletonList(new VariableUpdaterInfo<>(
286+
variableId,
287+
variableIdToUpdaters.getNextId(),
288+
declarativeShadowVariableDescriptor,
289+
declarativeShadowVariableDescriptor.getEntityDescriptor().getShadowVariableLoopedDescriptor(),
290+
declarativeShadowVariableDescriptor.getMemberAccessor(),
291+
declarativeShadowVariableDescriptor.getCalculator()::executeGetter)));
292+
}
293+
171294
private static <Solution_> Map<VariableMetaModel<?, ?, ?>, Set<VariableSourceReference>> createGraphNodes(
172295
VariableReferenceGraphBuilder<Solution_> graph, Object[] entities,
173296
List<DeclarativeShadowVariableDescriptor<Solution_>> declarativeShadowVariableDescriptors,
174-
Map<VariableMetaModel<?, ?, ?>, VariableUpdaterInfo<Solution_>> variableIdToUpdater) {
297+
EntityVariableUpdaterLookup<Solution_> variableIdToUpdaters,
298+
TriFunction<Object, DeclarativeShadowVariableDescriptor<Solution_>, VariableMetaModel<Solution_, ?, ?>, List<VariableUpdaterInfo<Solution_>>> entityVariableToUpdatersMapper) {
175299
var result = new HashMap<VariableMetaModel<?, ?, ?>, Set<VariableSourceReference>>();
176300
for (var entity : entities) {
177301
for (var declarativeShadowVariableDescriptor : declarativeShadowVariableDescriptors) {
178302
var entityClass = declarativeShadowVariableDescriptor.getEntityDescriptor().getEntityClass();
179303
if (entityClass.isInstance(entity)) {
180304
var variableId = declarativeShadowVariableDescriptor.getVariableMetaModel();
181-
var updater = variableIdToUpdater.computeIfAbsent(variableId, ignored -> new VariableUpdaterInfo<>(
182-
variableId,
183-
declarativeShadowVariableDescriptor,
184-
declarativeShadowVariableDescriptor.getEntityDescriptor().getShadowVariableLoopedDescriptor(),
185-
declarativeShadowVariableDescriptor.getMemberAccessor(),
186-
declarativeShadowVariableDescriptor.getCalculator()::executeGetter));
187-
graph.addVariableReferenceEntity(entity, updater);
305+
var updaters = variableIdToUpdaters.computeUpdatersForVariableOnEntity(variableId,
306+
entity,
307+
() -> entityVariableToUpdatersMapper.apply(entity, declarativeShadowVariableDescriptor,
308+
variableId));
309+
graph.addVariableReferenceEntity(entity, updaters);
188310
for (var sourceRoot : declarativeShadowVariableDescriptor.getSources()) {
189311
for (var source : sourceRoot.variableSourceReferences()) {
190312
if (source.downstreamDeclarativeVariableMetamodel() != null) {

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

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

3+
import java.util.List;
4+
35
import org.jspecify.annotations.NullMarked;
46

57
@NullMarked
6-
public record EntityVariablePair<Solution_>(Object entity, VariableUpdaterInfo<Solution_> variableReference, int graphNodeId) {
8+
public record EntityVariablePair<Solution_>(Object entity, List<VariableUpdaterInfo<Solution_>> variableReferences,
9+
int graphNodeId) {
710
@Override
811
public boolean equals(Object object) {
912
if (!(object instanceof EntityVariablePair<?> that))
@@ -18,6 +21,6 @@ public int hashCode() {
1821

1922
@Override
2023
public String toString() {
21-
return entity + ":" + variableReference.id();
24+
return entity + ":" + variableReferences.stream().map(VariableUpdaterInfo::id).toList();
2225
}
2326
}

0 commit comments

Comments
 (0)