Skip to content

Commit 0ed9176

Browse files
committed
feat: enable entity value range for LS and multiple move types
1 parent d5f32bc commit 0ed9176

25 files changed

Lines changed: 324 additions & 80 deletions

core/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -884,7 +884,8 @@ public void processProblemScale(Solution_ solution, Object entity, ProblemScaleT
884884
tracker.addPinnedListValueCount(1);
885885
}
886886
// Anchors are entities
887-
var valueRange = variableDescriptor.getValueRangeDescriptor().extractValueRange(solution, entity);
887+
var valueRange = valueRangeResolver.extractValueRange(variableDescriptor.getValueRangeDescriptor(),
888+
solution, entity);
888889
if (valueRange instanceof CountableValueRange<?> countableValueRange) {
889890
var valueIterator = countableValueRange.createOriginalIterator();
890891
while (valueIterator.hasNext()) {

core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/cache/HashSetValueRangeCache.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,6 @@ public class HashSetValueRangeCache<Value_> implements ValueRangeCacheStrategy<V
1717
private final Set<Value_> cache;
1818
private final List<Value_> values;
1919

20-
public HashSetValueRangeCache() {
21-
// Initial value of 1K items
22-
this(1_000);
23-
}
24-
2520
public HashSetValueRangeCache(int size) {
2621
cache = new HashSet<>(size);
2722
values = new ArrayList<>(size);

core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/cache/IdentityValueRangeCache.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,6 @@ public class IdentityValueRangeCache<Value_> implements ValueRangeCacheStrategy<
2121
private final Map<Value_, Integer> cache;
2222
private final List<Value_> values;
2323

24-
public IdentityValueRangeCache() {
25-
// Initial value of 1K items
26-
this(1_000);
27-
}
28-
2924
public IdentityValueRangeCache(int size) {
3025
cache = new IdentityHashMap<>(size);
3126
values = new ArrayList<>(size);

core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractMove.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import ai.timefold.solver.core.api.score.director.ScoreDirector;
1111
import ai.timefold.solver.core.impl.domain.valuerange.descriptor.ValueRangeDescriptor;
1212
import ai.timefold.solver.core.impl.move.director.VariableChangeRecordingScoreDirector;
13-
import ai.timefold.solver.core.impl.score.director.InnerScoreDirector;
13+
import ai.timefold.solver.core.impl.score.director.VariableDescriptorAwareScoreDirector;
1414

1515
/**
1616
* Abstract superclass for {@link Move}, requiring implementation of undo moves.
@@ -53,8 +53,9 @@ protected Move<Solution_> createUndoMove(ScoreDirector<Solution_> scoreDirector)
5353

5454
protected <Value_> ValueRange<Value_> extractValueRange(ScoreDirector<Solution_> scoreDirector,
5555
ValueRangeDescriptor<Solution_> valueRangeDescriptor, Solution_ workingSolution, Object entity) {
56-
if (scoreDirector instanceof InnerScoreDirector<Solution_, ?> innerScoreDirector) {
57-
return innerScoreDirector.getValueRangeResolver().extractValueRange(valueRangeDescriptor, workingSolution, entity);
56+
if (scoreDirector instanceof VariableDescriptorAwareScoreDirector<Solution_> variableDescriptorAwareScoreDirector) {
57+
return variableDescriptorAwareScoreDirector.getValueRangeResolver().extractValueRange(valueRangeDescriptor,
58+
workingSolution, entity);
5859
} else {
5960
return valueRangeDescriptor.extractValueRange(workingSolution, entity);
6061
}

core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateMove.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ protected void doMoveOnGenuineVariables(ScoreDirector<Solution_> scoreDirector)
5454

5555
var nestedSolverScope = new SolverScope<Solution_>(solverScope.getClock());
5656
nestedSolverScope.setSolver(solverScope.getSolver());
57+
nestedSolverScope.setValueRangeResolver(solverScope.getValueRangeResolver());
5758
nestedSolverScope.setScoreDirector(recordingScoreDirector.getBacking());
5859
constructionHeuristicPhase.solvingStarted(nestedSolverScope);
5960
constructionHeuristicPhase.solve(nestedSolverScope);

core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListAssignMove.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import ai.timefold.solver.core.api.score.director.ScoreDirector;
88
import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor;
99
import ai.timefold.solver.core.impl.heuristic.move.AbstractMove;
10-
import ai.timefold.solver.core.impl.score.director.InnerScoreDirector;
10+
import ai.timefold.solver.core.impl.score.director.ValueRangeResolver;
1111
import ai.timefold.solver.core.impl.score.director.VariableDescriptorAwareScoreDirector;
1212

1313
public final class ListAssignMove<Solution_> extends AbstractMove<Solution_> {
@@ -52,9 +52,11 @@ public boolean isMoveDoable(ScoreDirector<Solution_> scoreDirector) {
5252
var firstPass = destinationIndex >= 0 && variableDescriptor.getListSize(destinationEntity) >= destinationIndex;
5353
var secondPass = true;
5454
// When the value range is located at the entity,
55-
// we need to check if the destination's value range accepts the upcoming value.
56-
if (!variableDescriptor.canExtractValueRangeFromSolution()) {
57-
secondPass = ((InnerScoreDirector<Solution_, ?>) scoreDirector).getValueRangeResolver()
55+
// we need to check if the destination's value range accepts the upcoming value
56+
if (firstPass && !variableDescriptor.canExtractValueRangeFromSolution()) {
57+
ValueRangeResolver<Solution_> valueRangeResolver =
58+
((VariableDescriptorAwareScoreDirector<Solution_>) scoreDirector).getValueRangeResolver();
59+
secondPass = valueRangeResolver
5860
.extractValueRange(variableDescriptor.getValueRangeDescriptor(), null, destinationEntity)
5961
.contains(planningValue);
6062
}

core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListChangeMove.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import ai.timefold.solver.core.api.score.director.ScoreDirector;
1212
import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor;
1313
import ai.timefold.solver.core.impl.heuristic.move.AbstractMove;
14-
import ai.timefold.solver.core.impl.score.director.InnerScoreDirector;
14+
import ai.timefold.solver.core.impl.score.director.ValueRangeResolver;
1515
import ai.timefold.solver.core.impl.score.director.VariableDescriptorAwareScoreDirector;
1616

1717
/**
@@ -137,10 +137,12 @@ public boolean isMoveDoable(ScoreDirector<Solution_> scoreDirector) {
137137
var secondPass = true;
138138
// When the source and destination are different,
139139
// and the value range is located at the entity,
140-
// we need to check if the destination's value range accepts the upcoming value.
141-
if (distinctEntity && !variableDescriptor.canExtractValueRangeFromSolution()) {
140+
// we need to check if the destination's value range accepts the upcoming value
141+
if (firstPass && distinctEntity && !variableDescriptor.canExtractValueRangeFromSolution()) {
142142
var value = variableDescriptor.getElement(sourceEntity, sourceIndex);
143-
secondPass = ((InnerScoreDirector<Solution_, ?>) scoreDirector).getValueRangeResolver()
143+
ValueRangeResolver<Solution_> valueRangeResolver =
144+
((VariableDescriptorAwareScoreDirector<Solution_>) scoreDirector).getValueRangeResolver();
145+
secondPass = valueRangeResolver
144146
.extractValueRange(variableDescriptor.getValueRangeDescriptor(), null, destinationEntity).contains(value);
145147
}
146148
return firstPass && secondPass;

core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListSwapMove.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import ai.timefold.solver.core.api.score.director.ScoreDirector;
1212
import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor;
1313
import ai.timefold.solver.core.impl.heuristic.move.AbstractMove;
14+
import ai.timefold.solver.core.impl.score.director.ValueRangeResolver;
1415
import ai.timefold.solver.core.impl.score.director.VariableDescriptorAwareScoreDirector;
1516

1617
/**
@@ -116,7 +117,24 @@ public Object getRightValue() {
116117
public boolean isMoveDoable(ScoreDirector<Solution_> scoreDirector) {
117118
// Do not use Object#equals on user-provided domain objects. Relying on user's implementation of Object#equals
118119
// opens the opportunity to shoot themselves in the foot if different entities can be equal.
119-
return !(rightEntity == leftEntity && leftIndex == rightIndex);
120+
var sameEntity = leftEntity == rightEntity;
121+
var firstPass = !(sameEntity && leftIndex == rightIndex);
122+
var secondPass = true;
123+
// When the left and right are different,
124+
// and the value range is located at the entity,
125+
// we need to check if the destination's value range accepts the upcoming values
126+
if (firstPass && !sameEntity && !variableDescriptor.canExtractValueRangeFromSolution()) {
127+
ValueRangeResolver<Solution_> valueRangeResolver =
128+
((VariableDescriptorAwareScoreDirector<Solution_>) scoreDirector).getValueRangeResolver();
129+
var leftElement = variableDescriptor.getElement(leftEntity, leftIndex);
130+
var leftElementValueRange =
131+
valueRangeResolver.extractValueRange(variableDescriptor.getValueRangeDescriptor(), null, leftEntity);
132+
var rightElement = variableDescriptor.getElement(rightEntity, rightIndex);
133+
var rightElementValueRange =
134+
valueRangeResolver.extractValueRange(variableDescriptor.getValueRangeDescriptor(), null, rightEntity);
135+
secondPass = leftElementValueRange.contains(rightElement) && rightElementValueRange.contains(leftElement);
136+
}
137+
return firstPass && secondPass;
120138
}
121139

122140
@Override

core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/SubListChangeMove.java

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor;
1111
import ai.timefold.solver.core.impl.heuristic.move.AbstractMove;
1212
import ai.timefold.solver.core.impl.heuristic.selector.list.SubList;
13+
import ai.timefold.solver.core.impl.score.director.ValueRangeResolver;
1314
import ai.timefold.solver.core.impl.score.director.VariableDescriptorAwareScoreDirector;
1415
import ai.timefold.solver.core.impl.util.CollectionUtils;
1516

@@ -78,13 +79,25 @@ public int getDestinationIndex() {
7879

7980
@Override
8081
public boolean isMoveDoable(ScoreDirector<Solution_> scoreDirector) {
81-
if (destinationEntity != sourceEntity) {
82-
return true;
83-
} else if (destinationIndex == sourceIndex) {
82+
var sameEntity = destinationEntity == sourceEntity;
83+
if (sameEntity && destinationIndex == sourceIndex) {
8484
return false;
85-
} else {
86-
return destinationIndex + length <= variableDescriptor.getListSize(destinationEntity);
8785
}
86+
var firstPass = !sameEntity || destinationIndex + length <= variableDescriptor.getListSize(destinationEntity);
87+
var secondPass = true;
88+
// When the first and second elements are different,
89+
// and the value range is located at the entity,
90+
// we need to check if the destination's value range accepts the upcoming values
91+
if (firstPass && !sameEntity && !variableDescriptor.canExtractValueRangeFromSolution()) {
92+
ValueRangeResolver<Solution_> valueRangeResolver =
93+
((VariableDescriptorAwareScoreDirector<Solution_>) scoreDirector).getValueRangeResolver();
94+
var destinationValueRange =
95+
valueRangeResolver.extractValueRange(variableDescriptor.getValueRangeDescriptor(), null, destinationEntity);
96+
var sourceList = variableDescriptor.getValue(sourceEntity);
97+
var subList = sourceList.subList(sourceIndex, sourceIndex + length);
98+
secondPass = subList.stream().allMatch(destinationValueRange::contains);
99+
}
100+
return firstPass && secondPass;
88101
}
89102

90103
@Override

core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/SubListSwapMove.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor;
1212
import ai.timefold.solver.core.impl.heuristic.move.AbstractMove;
1313
import ai.timefold.solver.core.impl.heuristic.selector.list.SubList;
14+
import ai.timefold.solver.core.impl.score.director.ValueRangeResolver;
1415
import ai.timefold.solver.core.impl.score.director.VariableDescriptorAwareScoreDirector;
1516
import ai.timefold.solver.core.impl.util.CollectionUtils;
1617

@@ -72,7 +73,26 @@ public boolean isReversing() {
7273
@Override
7374
public boolean isMoveDoable(ScoreDirector<Solution_> scoreDirector) {
7475
// If both subLists are on the same entity, then they must not overlap.
75-
return leftSubList.entity() != rightSubList.entity() || rightFromIndex >= leftToIndex;
76+
var firstPass = leftSubList.entity() != rightSubList.entity() || rightFromIndex >= leftToIndex;
77+
var secondPass = true;
78+
// When the left and right elements are different,
79+
// and the value range is located at the entity,
80+
// we need to check if the destination's value range accepts the upcoming values
81+
if (firstPass && !variableDescriptor.canExtractValueRangeFromSolution()) {
82+
ValueRangeResolver<Solution_> valueRangeResolver =
83+
((VariableDescriptorAwareScoreDirector<Solution_>) scoreDirector).getValueRangeResolver();
84+
var leftEntity = leftSubList.entity();
85+
var leftList = subList(leftSubList);
86+
var leftValueRange =
87+
valueRangeResolver.extractValueRange(variableDescriptor.getValueRangeDescriptor(), null, leftEntity);
88+
var rightEntity = rightSubList.entity();
89+
var rightList = subList(rightSubList);
90+
var rightValueRange =
91+
valueRangeResolver.extractValueRange(variableDescriptor.getValueRangeDescriptor(), null, rightEntity);
92+
secondPass = leftList.stream().allMatch(rightValueRange::contains)
93+
&& rightList.stream().allMatch(leftValueRange::contains);
94+
}
95+
return firstPass && secondPass;
7696
}
7797

7898
@Override

0 commit comments

Comments
 (0)