Skip to content

Commit 081f207

Browse files
feat: allowing to specify an alignment key to eliminate redundant calculations (#1718)
If a calculation only involves facts/variables on indirect variables, then that calculation will result in the same value for entities that share the same indirect variables/facts. To prevent redundant recalculations, the user can specify an alignment key, and entities that share the same group key for a shadow will only be calculated once.
1 parent 72d48c2 commit 081f207

File tree

19 files changed

+2987
-112
lines changed

19 files changed

+2987
-112
lines changed

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

Lines changed: 11 additions & 22 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.Arrays;
34
import java.util.BitSet;
45
import java.util.List;
56
import java.util.Objects;
@@ -130,36 +131,19 @@ private boolean updateEntityShadowVariables(EntityVariablePair<Solution_> entity
130131
}
131132

132133
for (var shadowVariableReference : shadowVariableReferences) {
133-
anyChanged |= updateShadowVariable(entityVariable, isLooped, shadowVariableReference, entity);
134+
anyChanged |= updateShadowVariable(isLooped, shadowVariableReference, entity);
134135
}
135136

136137
return anyChanged;
137138
}
138139

139-
private boolean updateShadowVariable(EntityVariablePair<Solution_> entityVariable, boolean isLooped,
140+
private boolean updateShadowVariable(boolean isLooped,
140141
VariableUpdaterInfo<Solution_> shadowVariableReference, Object entity) {
141-
var oldValue = shadowVariableReference.memberAccessor().executeGetter(entity);
142142
if (isLooped) {
143-
if (oldValue != null) {
144-
affectedEntities.add(entityVariable);
145-
changeShadowVariableAndNotify(shadowVariableReference, entity, null);
146-
}
147-
return true;
143+
return shadowVariableReference.updateIfChanged(entity, null, changedVariableNotifier);
148144
} else {
149-
var newValue = shadowVariableReference.calculator().apply(entity);
150-
if (!Objects.equals(oldValue, newValue)) {
151-
affectedEntities.add(entityVariable);
152-
changeShadowVariableAndNotify(shadowVariableReference, entity, newValue);
153-
return true;
154-
}
145+
return shadowVariableReference.updateIfChanged(entity, changedVariableNotifier);
155146
}
156-
return false;
157-
}
158-
159-
private void changeShadowVariableAndNotify(VariableUpdaterInfo<Solution_> shadowVariableReference, Object entity,
160-
Object newValue) {
161-
var variableDescriptor = shadowVariableReference.variableDescriptor();
162-
changeShadowVariableAndNotify(variableDescriptor, entity, newValue);
163147
}
164148

165149
private void changeShadowVariableAndNotify(VariableDescriptor<Solution_> variableDescriptor, Object entity,
@@ -184,7 +168,12 @@ public void add(EntityVariablePair<Solution_> shadowVariable) {
184168
if (shadowVariableLoopedDescriptor == null) {
185169
return;
186170
}
187-
entitiesForLoopedVarUpdateSet.add(shadowVariable.entity());
171+
var entityGroup = shadowVariable.variableReferences().get(0).groupEntities();
172+
if (entityGroup == null) {
173+
entitiesForLoopedVarUpdateSet.add(shadowVariable.entity());
174+
} else {
175+
entitiesForLoopedVarUpdateSet.addAll(Arrays.asList(entityGroup));
176+
}
188177
}
189178

190179
public void processAndClear() {

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

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

3+
import java.lang.reflect.Member;
34
import java.util.Collection;
45
import java.util.Collections;
56
import java.util.List;
7+
import java.util.function.Function;
68

79
import ai.timefold.solver.core.api.domain.variable.AbstractVariableListener;
810
import ai.timefold.solver.core.api.domain.variable.ShadowVariable;
@@ -18,12 +20,15 @@
1820
import ai.timefold.solver.core.impl.domain.variable.listener.VariableListenerWithSources;
1921
import ai.timefold.solver.core.impl.domain.variable.supply.Demand;
2022
import ai.timefold.solver.core.impl.domain.variable.supply.SupplyManager;
23+
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningSolutionMetaModel;
2124
import ai.timefold.solver.core.preview.api.domain.variable.declarative.ShadowSources;
2225

2326
public class DeclarativeShadowVariableDescriptor<Solution_> extends ShadowVariableDescriptor<Solution_> {
2427
MemberAccessor calculator;
2528
RootVariableSource<?, ?>[] sources;
2629
String[] sourcePaths;
30+
String alignmentKey;
31+
Function<Object, Object> alignmentKeyMap;
2732

2833
public DeclarativeShadowVariableDescriptor(int ordinal,
2934
EntityDescriptor<Solution_> entityDescriptor,
@@ -80,6 +85,12 @@ A shadow variable must have at least one source (since otherwise it a constant).
8085
Maybe add one source?
8186
""".formatted(methodName, ShadowVariable.class.getSimpleName(), variableMemberAccessor));
8287
}
88+
89+
if (shadowVariableUpdater.alignmentKey() != null && !shadowVariableUpdater.alignmentKey().isEmpty()) {
90+
alignmentKey = shadowVariableUpdater.alignmentKey();
91+
} else {
92+
alignmentKey = null;
93+
}
8394
}
8495

8596
@Override
@@ -105,15 +116,59 @@ public Iterable<VariableListenerWithSources<Solution_>> buildVariableListeners(S
105116
@Override
106117
public void linkVariableDescriptors(DescriptorPolicy descriptorPolicy) {
107118
sources = new RootVariableSource[sourcePaths.length];
119+
var solutionMetamodel = entityDescriptor.getSolutionDescriptor().getMetaModel();
120+
var memberAccessorFactory = entityDescriptor.getSolutionDescriptor().getMemberAccessorFactory();
121+
108122
for (int i = 0; i < sources.length; i++) {
109123
sources[i] = RootVariableSource.from(
110-
entityDescriptor.getSolutionDescriptor().getMetaModel(),
124+
solutionMetamodel,
111125
entityDescriptor.getEntityClass(),
112126
variableMemberAccessor.getName(),
113127
sourcePaths[i],
114-
entityDescriptor.getSolutionDescriptor().getMemberAccessorFactory(),
128+
memberAccessorFactory,
115129
descriptorPolicy);
116130
}
131+
132+
var alignmentKeyMember = getAlignmentKeyMemberForEntityProperty(solutionMetamodel,
133+
entityDescriptor.getEntityClass(),
134+
calculator,
135+
variableName,
136+
alignmentKey);
137+
if (alignmentKeyMember != null) {
138+
alignmentKeyMap = memberAccessorFactory.buildAndCacheMemberAccessor(alignmentKeyMember,
139+
MemberAccessorFactory.MemberAccessorType.FIELD_OR_GETTER_METHOD, ShadowSources.class,
140+
descriptorPolicy.getDomainAccessType())::executeGetter;
141+
} else {
142+
alignmentKeyMap = null;
143+
}
144+
}
145+
146+
protected static Member getAlignmentKeyMemberForEntityProperty(
147+
PlanningSolutionMetaModel<?> solutionMetamodel,
148+
Class<?> entityClass,
149+
MemberAccessor calculator,
150+
String variableName,
151+
String propertyName) {
152+
if (propertyName == null) {
153+
return null;
154+
}
155+
Member member = RootVariableSource.getMember(entityClass,
156+
propertyName, entityClass,
157+
propertyName);
158+
if (RootVariableSource.isVariable(solutionMetamodel, member.getDeclaringClass(),
159+
member.getName())) {
160+
throw new IllegalArgumentException(
161+
"""
162+
The @%s-annotated supplier method (%s) for variable (%s) on class (%s) uses a alignmentKey (%s) that is a variable.
163+
A alignmentKey must be a problem fact and cannot change during solving.
164+
"""
165+
.formatted(ShadowSources.class.getSimpleName(),
166+
calculator.getName(),
167+
variableName,
168+
entityClass.getCanonicalName(),
169+
propertyName));
170+
}
171+
return member;
117172
}
118173

119174
@Override
@@ -129,6 +184,10 @@ public MemberAccessor getCalculator() {
129184
return calculator;
130185
}
131186

187+
public Function<Object, Object> getAlignmentKeyMap() {
188+
return alignmentKeyMap;
189+
}
190+
132191
public RootVariableSource<?, ?>[] getSources() {
133192
return sources;
134193
}

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

Lines changed: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.util.Map;
1515
import java.util.Objects;
1616
import java.util.Set;
17+
import java.util.function.Function;
1718
import java.util.function.IntFunction;
1819

1920
import ai.timefold.solver.core.api.function.TriFunction;
@@ -24,6 +25,7 @@
2425
import ai.timefold.solver.core.preview.api.domain.metamodel.VariableMetaModel;
2526

2627
import org.jspecify.annotations.NullMarked;
28+
import org.jspecify.annotations.Nullable;
2729
import org.slf4j.Logger;
2830
import org.slf4j.LoggerFactory;
2931

@@ -186,9 +188,17 @@ private static <Solution_> VariableReferenceGraph buildVariableReferenceGraph(
186188
private record GroupVariableUpdaterInfo<Solution_>(
187189
List<DeclarativeShadowVariableDescriptor<Solution_>> sortedDeclarativeVariableDescriptors,
188190
List<VariableUpdaterInfo<Solution_>> allUpdaters,
189-
List<VariableUpdaterInfo<Solution_>> groupedUpdaters) {
191+
List<VariableUpdaterInfo<Solution_>> groupedUpdaters,
192+
Map<DeclarativeShadowVariableDescriptor<Solution_>, Map<Object, VariableUpdaterInfo<Solution_>>> variableToEntityToGroupUpdater) {
190193

191-
public List<VariableUpdaterInfo<Solution_>> getUpdatersForEntity(Object entity) {
194+
public List<VariableUpdaterInfo<Solution_>> getUpdatersForEntityVariable(Object entity,
195+
DeclarativeShadowVariableDescriptor<Solution_> declarativeShadowVariableDescriptor) {
196+
if (variableToEntityToGroupUpdater.containsKey(declarativeShadowVariableDescriptor)) {
197+
var updater = variableToEntityToGroupUpdater.get(declarativeShadowVariableDescriptor).get(entity);
198+
if (updater != null) {
199+
return List.of(updater);
200+
}
201+
}
192202
for (var shadowVariableDescriptor : sortedDeclarativeVariableDescriptors) {
193203
for (var rootSource : shadowVariableDescriptor.getSources()) {
194204
if (rootSource.parentVariableType() == ParentVariableType.GROUP) {
@@ -207,16 +217,32 @@ public List<VariableUpdaterInfo<Solution_>> getUpdatersForEntity(Object entity)
207217

208218
private static <Solution_> Map<VariableMetaModel<Solution_, ?, ?>, GroupVariableUpdaterInfo<Solution_>>
209219
getGroupVariableUpdaterInfoMap(
210-
List<DeclarativeShadowVariableDescriptor<Solution_>> declarativeShadowVariableDescriptors) {
220+
List<DeclarativeShadowVariableDescriptor<Solution_>> declarativeShadowVariableDescriptors,
221+
Object[] entities) {
211222
var sortedDeclarativeVariableDescriptors =
212223
topologicallySortedDeclarativeShadowVariables(declarativeShadowVariableDescriptors);
213224
var groupIndexToVariables = new HashMap<Integer, List<DeclarativeShadowVariableDescriptor<Solution_>>>();
214225
var groupVariables = new ArrayList<DeclarativeShadowVariableDescriptor<Solution_>>();
215226
groupIndexToVariables.put(0, groupVariables);
216227
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
228+
// If a @ShadowSources has a group source (i.e. "visitGroup[].arrivalTimes"),
229+
// create a new group since it must wait until all members of that group are processed
230+
var hasGroupSources = Arrays.stream(declarativeShadowVariableDescriptor.getSources())
231+
.anyMatch(rootVariableSource -> rootVariableSource.parentVariableType() == ParentVariableType.GROUP);
232+
233+
// If a @ShadowSources has an alignment key,
234+
// create a new group since multiple entities must be updated for this node
235+
var hasAlignmentKey = declarativeShadowVariableDescriptor.getAlignmentKeyMap() != null;
236+
237+
// If the previous @ShadowSources has an alignment key,
238+
// create a new group since we are updating a single entity again
239+
// NOTE: Can potentially be optimized/share a node if VariableUpdaterInfo
240+
// update each group member independently after the alignmentKey
241+
var previousHasAlignmentKey = !groupVariables.isEmpty() && groupVariables.get(0).getAlignmentKeyMap() != null;
242+
243+
if (!groupVariables.isEmpty() && (hasGroupSources
244+
|| hasAlignmentKey
245+
|| previousHasAlignmentKey)) {
220246
groupVariables = new ArrayList<>();
221247
groupIndexToVariables.put(groupIndexToVariables.size(), groupVariables);
222248
}
@@ -225,25 +251,57 @@ public List<VariableUpdaterInfo<Solution_>> getUpdatersForEntity(Object entity)
225251

226252
var out = new HashMap<VariableMetaModel<Solution_, ?, ?>, GroupVariableUpdaterInfo<Solution_>>();
227253
var allUpdaters = new ArrayList<VariableUpdaterInfo<Solution_>>();
254+
var groupedUpdaters =
255+
new HashMap<DeclarativeShadowVariableDescriptor<Solution_>, Map<Object, VariableUpdaterInfo<Solution_>>>();
256+
var updaterKey = 0;
228257
for (var entryKey = 0; entryKey < groupIndexToVariables.size(); entryKey++) {
229258
var entryGroupVariables = groupIndexToVariables.get(entryKey);
230259
var updaters = new ArrayList<VariableUpdaterInfo<Solution_>>();
231260
for (var declarativeShadowVariableDescriptor : entryGroupVariables) {
232261
var updater = new VariableUpdaterInfo<>(
233262
declarativeShadowVariableDescriptor.getVariableMetaModel(),
234-
entryKey,
263+
updaterKey,
235264
declarativeShadowVariableDescriptor,
236265
declarativeShadowVariableDescriptor.getEntityDescriptor().getShadowVariableLoopedDescriptor(),
237266
declarativeShadowVariableDescriptor.getMemberAccessor(),
238267
declarativeShadowVariableDescriptor.getCalculator()::executeGetter);
239-
updaters.add(updater);
240-
allUpdaters.add(updater);
268+
if (declarativeShadowVariableDescriptor.getAlignmentKeyMap() != null) {
269+
var alignmentKeyFunction = declarativeShadowVariableDescriptor.getAlignmentKeyMap();
270+
var alignmentKeyToAlignedEntitiesMap = new HashMap<Object, List<Object>>();
271+
for (var entity : entities) {
272+
if (declarativeShadowVariableDescriptor.getEntityDescriptor().getEntityClass().isInstance(entity)) {
273+
var alignmentKey = alignmentKeyFunction.apply(entity);
274+
alignmentKeyToAlignedEntitiesMap.computeIfAbsent(alignmentKey, k -> new ArrayList<>()).add(entity);
275+
}
276+
}
277+
for (var alignmentGroup : alignmentKeyToAlignedEntitiesMap.entrySet()) {
278+
var updaterCopy = updater.withGroupId(updaterKey);
279+
if (alignmentGroup.getKey() == null) {
280+
updaters.add(updaterCopy);
281+
allUpdaters.add(updaterCopy);
282+
} else {
283+
updaterCopy = updaterCopy.withGroupEntities(alignmentGroup.getValue().toArray(new Object[0]));
284+
var variableUpdaterMap = groupedUpdaters.computeIfAbsent(declarativeShadowVariableDescriptor,
285+
ignored -> new IdentityHashMap<>());
286+
for (var entity : alignmentGroup.getValue()) {
287+
variableUpdaterMap.put(entity, updaterCopy);
288+
}
289+
}
290+
updaterKey++;
291+
}
292+
updaterKey--; // it will be incremented again at end of the loop
293+
} else {
294+
updaters.add(updater);
295+
allUpdaters.add(updater);
296+
}
241297
}
242298
var groupVariableUpdaterInfo =
243-
new GroupVariableUpdaterInfo<Solution_>(sortedDeclarativeVariableDescriptors, allUpdaters, updaters);
299+
new GroupVariableUpdaterInfo<Solution_>(sortedDeclarativeVariableDescriptors, allUpdaters, updaters,
300+
groupedUpdaters);
244301
for (var declarativeShadowVariableDescriptor : entryGroupVariables) {
245302
out.put(declarativeShadowVariableDescriptor.getVariableMetaModel(), groupVariableUpdaterInfo);
246303
}
304+
updaterKey++;
247305
}
248306
allUpdaters.replaceAll(updater -> updater.withGroupId(groupIndexToVariables.size()));
249307
return out;
@@ -256,7 +314,16 @@ private static <Solution_> VariableReferenceGraph buildArbitrarySingleEntityGrap
256314
var declarativeShadowVariableDescriptors = solutionDescriptor.getDeclarativeShadowVariableDescriptors();
257315
// Use a dependent lookup; if an entity does not use groups, then all variables can share the same node.
258316
// If the entity use groups, then variables must be grouped into their own nodes.
259-
var variableIdToUpdater = EntityVariableUpdaterLookup.<Solution_> entityDependentLookup();
317+
var alignmentKeyMappers = new HashMap<VariableMetaModel<Solution_, ?, ?>, Function<Object, @Nullable Object>>();
318+
for (var declarativeShadowVariableDescriptor : declarativeShadowVariableDescriptors) {
319+
if (declarativeShadowVariableDescriptor.getAlignmentKeyMap() != null) {
320+
alignmentKeyMappers.put(declarativeShadowVariableDescriptor.getVariableMetaModel(),
321+
declarativeShadowVariableDescriptor.getAlignmentKeyMap());
322+
}
323+
}
324+
var variableIdToUpdater =
325+
alignmentKeyMappers.isEmpty() ? EntityVariableUpdaterLookup.<Solution_> entityDependentLookup()
326+
: EntityVariableUpdaterLookup.<Solution_> groupedEntityDependentLookup(alignmentKeyMappers::get);
260327

261328
// Create graph node for each entity/declarative shadow variable group pair.
262329
// Maps a variable id to the source aliases of all variables in its group;
@@ -266,11 +333,11 @@ private static <Solution_> VariableReferenceGraph buildArbitrarySingleEntityGrap
266333
// the groups are [arrivalTime, readyTime] and [serviceStartTime, serviceFinishTime]
267334
// this is because from arrivalTime, you can compute readyTime without knowing either
268335
// serviceStartTime or serviceFinishTime.
269-
var variableIdToGroupedUpdater = getGroupVariableUpdaterInfoMap(declarativeShadowVariableDescriptors);
336+
var variableIdToGroupedUpdater = getGroupVariableUpdaterInfoMap(declarativeShadowVariableDescriptors, entities);
270337
var declarativeShadowVariableToAliasMap = createGraphNodes(variableReferenceGraphBuilder, entities,
271338
declarativeShadowVariableDescriptors, variableIdToUpdater,
272339
(entity, declarativeShadowVariable, variableId) -> variableIdToGroupedUpdater.get(variableId)
273-
.getUpdatersForEntity(entity));
340+
.getUpdatersForEntityVariable(entity, declarativeShadowVariable));
274341
return buildVariableReferenceGraph(declarativeShadowVariableDescriptors, variableReferenceGraphBuilder,
275342
declarativeShadowVariableToAliasMap,
276343
graphCreator, entities);

0 commit comments

Comments
 (0)