Skip to content

Commit 7875e07

Browse files
committed
chore: filtering move selector enforcing the phase termination
1 parent 999d2bf commit 7875e07

File tree

4 files changed

+58
-4
lines changed

4 files changed

+58
-4
lines changed

core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelector.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import ai.timefold.solver.core.impl.heuristic.selector.move.AbstractMoveSelector;
1010
import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector;
1111
import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope;
12+
import ai.timefold.solver.core.impl.solver.termination.PhaseTermination;
1213

1314
public final class FilteringMoveSelector<Solution_> extends AbstractMoveSelector<Solution_> {
1415

@@ -24,6 +25,7 @@ public static <Solution_> FilteringMoveSelector<Solution_> of(MoveSelector<Solut
2425
private final MoveSelector<Solution_> childMoveSelector;
2526
private final SelectionFilter<Solution_, Move<Solution_>> filter;
2627
private final boolean bailOutEnabled;
28+
private AbstractPhaseScope<Solution_> phaseScope;
2729

2830
private ScoreDirector<Solution_> scoreDirector = null;
2931

@@ -42,13 +44,15 @@ private FilteringMoveSelector(MoveSelector<Solution_> childMoveSelector,
4244
@Override
4345
public void phaseStarted(AbstractPhaseScope<Solution_> phaseScope) {
4446
super.phaseStarted(phaseScope);
45-
scoreDirector = phaseScope.getScoreDirector();
47+
this.scoreDirector = phaseScope.getScoreDirector();
48+
this.phaseScope = phaseScope;
4649
}
4750

4851
@Override
4952
public void phaseEnded(AbstractPhaseScope<Solution_> phaseScope) {
5053
super.phaseEnded(phaseScope);
51-
scoreDirector = null;
54+
this.scoreDirector = null;
55+
this.phaseScope = null;
5256
}
5357

5458
@Override
@@ -68,17 +72,22 @@ public long getSize() {
6872

6973
@Override
7074
public Iterator<Move<Solution_>> iterator() {
71-
return new JustInTimeFilteringMoveIterator(childMoveSelector.iterator(), determineBailOutSize());
75+
return new JustInTimeFilteringMoveIterator(childMoveSelector.iterator(), determineBailOutSize(), phaseScope);
7276
}
7377

7478
private class JustInTimeFilteringMoveIterator extends UpcomingSelectionIterator<Move<Solution_>> {
7579

7680
private final Iterator<Move<Solution_>> childMoveIterator;
7781
private final long bailOutSize;
82+
private final AbstractPhaseScope<Solution_> phaseScope;
83+
private final PhaseTermination<Solution_> termination;
7884

79-
public JustInTimeFilteringMoveIterator(Iterator<Move<Solution_>> childMoveIterator, long bailOutSize) {
85+
public JustInTimeFilteringMoveIterator(Iterator<Move<Solution_>> childMoveIterator, long bailOutSize,
86+
AbstractPhaseScope<Solution_> phaseScope) {
8087
this.childMoveIterator = childMoveIterator;
8188
this.bailOutSize = bailOutSize;
89+
this.phaseScope = phaseScope;
90+
this.termination = phaseScope.getTermination();
8291
}
8392

8493
@Override
@@ -95,6 +104,11 @@ protected Move<Solution_> createUpcomingSelection() {
95104
logger.trace("Bailing out of neverEnding selector ({}) after ({}) attempts to avoid infinite loop.",
96105
FilteringMoveSelector.this, bailOutSize);
97106
return noUpcomingSelection();
107+
} else if (termination != null && termination.isPhaseTerminated(phaseScope)) {
108+
logger.trace(
109+
"Bailing out of neverEnding selector ({}) because the termination setting has been triggered.",
110+
FilteringMoveSelector.this);
111+
return noUpcomingSelection();
98112
}
99113
attemptsBeforeBailOut--;
100114
}

core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ public void phaseStarted(LocalSearchPhaseScope<Solution_> phaseScope) {
125125
super.phaseStarted(phaseScope);
126126
decider.phaseStarted(phaseScope);
127127
assertWorkingSolutionInitialized(phaseScope);
128+
phaseScope.setTermination(phaseTermination);
128129
}
129130

130131
@Override

core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import ai.timefold.solver.core.impl.score.director.InnerScore;
1010
import ai.timefold.solver.core.impl.score.director.InnerScoreDirector;
1111
import ai.timefold.solver.core.impl.solver.scope.SolverScope;
12+
import ai.timefold.solver.core.impl.solver.termination.PhaseTermination;
1213
import ai.timefold.solver.core.preview.api.move.Move;
1314

1415
import org.slf4j.Logger;
@@ -36,6 +37,11 @@ public abstract class AbstractPhaseScope<Solution_> {
3637

3738
protected int bestSolutionStepIndex;
3839

40+
/**
41+
* The solver termination configuration
42+
*/
43+
private PhaseTermination<Solution_> termination;
44+
3945
/**
4046
* As defined by #AbstractPhaseScope(SolverScope, int, boolean)
4147
* with the phaseSendingBestSolutionEvents parameter set to true.
@@ -188,6 +194,14 @@ public <Score_ extends Score<Score_>> InnerScoreDirector<Solution_, Score_> getS
188194
return solverScope.getScoreDirector();
189195
}
190196

197+
public void setTermination(PhaseTermination<Solution_> termination) {
198+
this.termination = termination;
199+
}
200+
201+
public PhaseTermination<Solution_> getTermination() {
202+
return termination;
203+
}
204+
191205
public Solution_ getWorkingSolution() {
192206
return solverScope.getWorkingSolution();
193207
}

core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/FilteringMoveSelectorTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22

33
import static ai.timefold.solver.core.testutil.PlannerAssert.assertAllCodesOfMoveSelector;
44
import static ai.timefold.solver.core.testutil.PlannerAssert.verifyPhaseLifecycle;
5+
import static org.assertj.core.api.Assertions.assertThat;
6+
import static org.mockito.ArgumentMatchers.any;
57
import static org.mockito.Mockito.mock;
68
import static org.mockito.Mockito.times;
79
import static org.mockito.Mockito.verify;
810
import static org.mockito.Mockito.when;
911

12+
import java.util.Iterator;
13+
1014
import ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType;
1115
import ai.timefold.solver.core.impl.heuristic.move.DummyMove;
1216
import ai.timefold.solver.core.impl.heuristic.selector.SelectorTestUtils;
@@ -15,6 +19,7 @@
1519
import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope;
1620
import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope;
1721
import ai.timefold.solver.core.impl.solver.scope.SolverScope;
22+
import ai.timefold.solver.core.impl.solver.termination.BasicPlumbingTermination;
1823
import ai.timefold.solver.core.testdomain.TestdataSolution;
1924

2025
import org.junit.jupiter.api.Test;
@@ -41,6 +46,26 @@ void filterCacheTypeJustInTime() {
4146
filter(SelectionCacheType.JUST_IN_TIME, 5);
4247
}
4348

49+
@Test
50+
void bailOutByTermination() {
51+
var phaseScope = mock(AbstractPhaseScope.class);
52+
var moveSelector = mock(MoveSelector.class);
53+
var termination = mock(BasicPlumbingTermination.class);
54+
var iterator = mock(Iterator.class);
55+
// We set the maximum value to force it to run many evaluations
56+
when(moveSelector.getSize()).thenReturn(Long.MAX_VALUE / 11);
57+
when(moveSelector.isNeverEnding()).thenReturn(true);
58+
when(moveSelector.iterator()).thenReturn(iterator);
59+
when(iterator.hasNext()).thenReturn(true);
60+
when(phaseScope.getTermination()).thenReturn(termination);
61+
when(termination.isPhaseTerminated(any(AbstractPhaseScope.class))).thenReturn(false, false, true);
62+
var filteredMoveSelector = FilteringMoveSelector.of(moveSelector, (scoreDirector, selection) -> false);
63+
filteredMoveSelector.phaseStarted(phaseScope);
64+
assertThat(filteredMoveSelector.iterator().hasNext()).isFalse();
65+
// The termination returns true at the third call
66+
verify(iterator, times(2)).next();
67+
}
68+
4469
public void filter(SelectionCacheType cacheType, int timesCalled) {
4570
MoveSelector childMoveSelector = SelectorTestUtils.mockMoveSelector(
4671
new DummyMove("a1"), new DummyMove("a2"), new DummyMove("a3"), new DummyMove("a4"));

0 commit comments

Comments
 (0)