Skip to content

Commit 43e52bd

Browse files
committed
fix: add rebase support to NoChangeMove and ListUnassignMove
Otherwise construction heuristics is going to fail with multi-threaded solving, when list variable with unassigned values is used.
1 parent deab101 commit 43e52bd

File tree

5 files changed

+37
-13
lines changed

5 files changed

+37
-13
lines changed

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

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,9 @@ default void doMoveOnly(ScoreDirector<Solution_> scoreDirector) {
112112
* @return never null, a new move that does the same change as this move on another solution instance
113113
*/
114114
default Move<Solution_> rebase(ScoreDirector<Solution_> destinationScoreDirector) {
115-
throw new UnsupportedOperationException("The custom move class (" + getClass()
116-
+ ") doesn't implement the rebase() method, so multithreaded solving is impossible.");
115+
throw new UnsupportedOperationException(
116+
"Move class (%s) doesn't implement the rebase() method, so multithreaded solving is impossible."
117+
.formatted(getClass()));
117118
}
118119

119120
// ************************************************************************
@@ -145,8 +146,9 @@ default String getSimpleMoveTypeDescription() {
145146
* @return never null
146147
*/
147148
default Collection<? extends Object> getPlanningEntities() {
148-
throw new UnsupportedOperationException("The custom move class (" + getClass()
149-
+ ") doesn't implement the getPlanningEntities() method, so Entity Tabu Search is impossible.");
149+
throw new UnsupportedOperationException(
150+
"Move class (%s) doesn't implement the getPlanningEntities() method, so Entity Tabu Search is impossible."
151+
.formatted(getClass()));
150152
}
151153

152154
/**
@@ -162,8 +164,9 @@ default Collection<? extends Object> getPlanningEntities() {
162164
* @return never null
163165
*/
164166
default Collection<? extends Object> getPlanningValues() {
165-
throw new UnsupportedOperationException("The custom move class (" + getClass()
166-
+ ") doesn't implement the getPlanningEntities() method, so Value Tabu Search is impossible.");
167+
throw new UnsupportedOperationException(
168+
"Move class (%s) doesn't implement the getPlanningEntities() method, so Value Tabu Search is impossible."
169+
.formatted(getClass()));
167170
}
168171

169172
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ protected void doMoveOnGenuineVariables(ScoreDirector<Solution_> scoreDirector)
3030
// Do nothing.
3131
}
3232

33+
@Override
34+
public Move<Solution_> rebase(ScoreDirector<Solution_> destinationScoreDirector) {
35+
return (Move<Solution_>) INSTANCE;
36+
}
37+
3338
@Override
3439
public String getSimpleMoveTypeDescription() {
3540
return "No change";

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import ai.timefold.solver.core.api.score.director.ScoreDirector;
66
import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor;
77
import ai.timefold.solver.core.impl.heuristic.move.AbstractSimplifiedMove;
8+
import ai.timefold.solver.core.impl.heuristic.move.Move;
89
import ai.timefold.solver.core.impl.score.director.VariableDescriptorAwareScoreDirector;
910

1011
public class ListUnassignMove<Solution_> extends AbstractSimplifiedMove<Solution_> {
@@ -57,6 +58,12 @@ protected void doMoveOnGenuineVariables(ScoreDirector<Solution_> scoreDirector)
5758
innerScoreDirector.afterListVariableElementUnassigned(variableDescriptor, element);
5859
}
5960

61+
@Override
62+
public Move<Solution_> rebase(ScoreDirector<Solution_> destinationScoreDirector) {
63+
return new ListUnassignMove<>(variableDescriptor, destinationScoreDirector.lookUpWorkingObject(sourceEntity),
64+
sourceIndex);
65+
}
66+
6067
// ************************************************************************
6168
// Introspection methods
6269
// ************************************************************************

core/core-impl/src/test/java/ai/timefold/solver/core/impl/heuristic/move/NoChangeMoveTest.java

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

33
import static ai.timefold.solver.core.impl.testdata.util.PlannerTestUtils.mockRebasingScoreDirector;
44
import static org.assertj.core.api.Assertions.assertThat;
5-
import static org.assertj.core.api.Assertions.assertThatThrownBy;
65

7-
import ai.timefold.solver.core.api.score.director.ScoreDirector;
86
import ai.timefold.solver.core.impl.testdata.domain.TestdataSolution;
97

108
import org.junit.jupiter.api.Test;
@@ -18,11 +16,10 @@ void isMoveDoable() {
1816

1917
@Test
2018
void rebase() {
21-
ScoreDirector<TestdataSolution> destinationScoreDirector = mockRebasingScoreDirector(
22-
TestdataSolution.buildSolutionDescriptor(), new Object[][] {});
23-
NoChangeMove<TestdataSolution> move = NoChangeMove.getInstance();
24-
assertThatThrownBy(() -> move.rebase(destinationScoreDirector))
25-
.isInstanceOf(UnsupportedOperationException.class);
19+
var destinationScoreDirector = mockRebasingScoreDirector(TestdataSolution.buildSolutionDescriptor(), new Object[][] {});
20+
var move = NoChangeMove.<TestdataSolution> getInstance();
21+
var rebasedMove = move.rebase(destinationScoreDirector);
22+
assertThat(rebasedMove).isSameAs(move);
2623
}
2724

2825
}

core/core-impl/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListUnassignMoveTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package ai.timefold.solver.core.impl.heuristic.selector.move.generic.list;
22

3+
import static ai.timefold.solver.core.impl.testdata.util.PlannerTestUtils.mockRebasingScoreDirector;
34
import static org.assertj.core.api.Assertions.assertThat;
45
import static org.mockito.Mockito.mock;
56
import static org.mockito.Mockito.verify;
@@ -8,6 +9,7 @@
89
import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore;
910
import ai.timefold.solver.core.impl.score.director.AbstractScoreDirector;
1011
import ai.timefold.solver.core.impl.score.director.InnerScoreDirector;
12+
import ai.timefold.solver.core.impl.testdata.domain.TestdataSolution;
1113
import ai.timefold.solver.core.impl.testdata.domain.list.TestdataListEntity;
1214
import ai.timefold.solver.core.impl.testdata.domain.list.TestdataListSolution;
1315
import ai.timefold.solver.core.impl.testdata.domain.list.TestdataListValue;
@@ -68,6 +70,16 @@ void undoMove() {
6870
verify(scoreDirector).afterListVariableElementAssigned(variableDescriptor, v3);
6971
}
7072

73+
@Test
74+
void rebase() {
75+
var solutionDescriptor = TestdataSolution.buildSolutionDescriptor();
76+
var variableDescriptor = solutionDescriptor.getListVariableDescriptor();
77+
var destinationScoreDirector = mockRebasingScoreDirector(solutionDescriptor, new Object[][] {});
78+
var move = new ListUnassignMove<TestdataSolution>(null, null, 0);
79+
var rebasedMove = move.rebase(destinationScoreDirector);
80+
assertThat(rebasedMove).isNotSameAs(move);
81+
}
82+
7183
@Test
7284
void toStringTest() {
7385
var v1 = new TestdataListValue("1");

0 commit comments

Comments
 (0)