Skip to content

Commit 746e1ed

Browse files
committed
feat: new stuck criterion
1 parent 01f580c commit 746e1ed

File tree

3 files changed

+170
-2
lines changed

3 files changed

+170
-2
lines changed

core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance.LateAcceptanceAcceptor;
1818
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.simulatedannealing.SimulatedAnnealingAcceptor;
1919
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stepcountinghillclimbing.StepCountingHillClimbingAcceptor;
20-
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.DiminishedReturnsStuckCriterion;
2120
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.StuckCriterion;
21+
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.UnimprovedTimeStuckCriterion;
2222
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.tabu.EntityTabuAcceptor;
2323
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.tabu.MoveTabuAcceptor;
2424
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.tabu.ValueTabuAcceptor;
@@ -222,7 +222,7 @@ private Optional<MoveTabuAcceptor<Solution_>> buildMoveTabuAcceptor(HeuristicCon
222222
if (acceptorTypeListsContainsAcceptorType(AcceptorType.LATE_ACCEPTANCE)
223223
|| (!acceptorTypeListsContainsAcceptorType(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE)
224224
&& acceptorConfig.getLateAcceptanceSize() != null)) {
225-
StuckCriterion<Solution_> strategy = new DiminishedReturnsStuckCriterion<>();
225+
StuckCriterion<Solution_> strategy = new UnimprovedTimeStuckCriterion<>();
226226
var acceptor = new LateAcceptanceAcceptor<>(strategy);
227227
acceptor.setLateAcceptanceSize(Objects.requireNonNullElse(acceptorConfig.getLateAcceptanceSize(), 400));
228228
return Optional.of(acceptor);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion;
2+
3+
import ai.timefold.solver.core.api.score.Score;
4+
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope;
5+
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope;
6+
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope;
7+
import ai.timefold.solver.core.impl.solver.scope.SolverScope;
8+
import ai.timefold.solver.core.impl.solver.termination.UnimprovedTimeMillisSpentTermination;
9+
10+
public class UnimprovedTimeStuckCriterion<Solution_, Score_ extends Score<Score_>>
11+
extends AbstractGeometricStuckCriterion<Solution_> {
12+
protected static final long START_TIME_WINDOW_MILLIS = 30_000;
13+
protected static final long REGULAR_TIME_WINDOW_MILLIS = 300_000;
14+
15+
private UnimprovedTimeMillisSpentTermination<Solution_> unimprovedTimeStuckCriterion;
16+
17+
private boolean triggered;
18+
private Score_ currentBestScore;
19+
20+
public UnimprovedTimeStuckCriterion() {
21+
this(new UnimprovedTimeMillisSpentTermination<>(START_TIME_WINDOW_MILLIS));
22+
}
23+
24+
protected UnimprovedTimeStuckCriterion(UnimprovedTimeMillisSpentTermination<Solution_> unimprovedTimeStuckCriterion) {
25+
super(START_TIME_WINDOW_MILLIS);
26+
this.unimprovedTimeStuckCriterion = unimprovedTimeStuckCriterion;
27+
}
28+
29+
@Override
30+
boolean evaluateCriterion(LocalSearchMoveScope<Solution_> moveScope) {
31+
triggered = unimprovedTimeStuckCriterion.isPhaseTerminated(moveScope.getStepScope().getPhaseScope());
32+
return triggered;
33+
}
34+
35+
@Override
36+
public void stepStarted(LocalSearchStepScope<Solution_> stepScope) {
37+
currentBestScore = stepScope.getPhaseScope().getBestScore();
38+
if (triggered) {
39+
// We need to recreate the termination criterion as the time window has changed
40+
// After the first restart we use a higher time window
41+
setScalingFactor(REGULAR_TIME_WINDOW_MILLIS);
42+
unimprovedTimeStuckCriterion = new UnimprovedTimeMillisSpentTermination<>(nextRestart);
43+
// The criterion must be initialized; otherwise, no restart will occur
44+
unimprovedTimeStuckCriterion.phaseStarted(stepScope.getPhaseScope());
45+
triggered = false;
46+
}
47+
}
48+
49+
@Override
50+
public void stepEnded(LocalSearchStepScope<Solution_> stepScope) {
51+
unimprovedTimeStuckCriterion.stepEnded(stepScope);
52+
if (currentBestScore.compareTo(stepScope.getPhaseScope().getBestScore()) < 0
53+
&& nextRestart > START_TIME_WINDOW_MILLIS) {
54+
// If the solution has been improved after a restart,
55+
// we reset the criterion and restart the evaluation of the metric
56+
setScalingFactor(START_TIME_WINDOW_MILLIS);
57+
super.solvingStarted(stepScope.getPhaseScope().getSolverScope());
58+
unimprovedTimeStuckCriterion = new UnimprovedTimeMillisSpentTermination<>(nextRestart);
59+
// The criterion must be initialized; otherwise, no restart will occur
60+
unimprovedTimeStuckCriterion.phaseStarted(stepScope.getPhaseScope());
61+
logger.info("Stuck criterion reset, next restart ({}), previous best score({}), new best score ({})", nextRestart,
62+
currentBestScore, stepScope.getPhaseScope().getBestScore());
63+
}
64+
}
65+
66+
@Override
67+
public void phaseStarted(LocalSearchPhaseScope<Solution_> phaseScope) {
68+
super.phaseStarted(phaseScope);
69+
unimprovedTimeStuckCriterion.phaseStarted(phaseScope);
70+
triggered = false;
71+
}
72+
73+
@Override
74+
public void phaseEnded(LocalSearchPhaseScope<Solution_> phaseScope) {
75+
super.phaseEnded(phaseScope);
76+
unimprovedTimeStuckCriterion.phaseEnded(phaseScope);
77+
}
78+
79+
@Override
80+
public void solvingStarted(SolverScope<Solution_> solverScope) {
81+
super.solvingStarted(solverScope);
82+
unimprovedTimeStuckCriterion.solvingStarted(solverScope);
83+
}
84+
85+
@Override
86+
public void solvingEnded(SolverScope<Solution_> solverScope) {
87+
super.solvingEnded(solverScope);
88+
unimprovedTimeStuckCriterion.solvingEnded(solverScope);
89+
}
90+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion;
2+
3+
import static ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.DiminishedReturnsStuckCriterion.START_TIME_WINDOW_MILLIS;
4+
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
5+
import static org.mockito.ArgumentMatchers.any;
6+
import static org.mockito.Mockito.mock;
7+
import static org.mockito.Mockito.when;
8+
9+
import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore;
10+
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope;
11+
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope;
12+
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope;
13+
import ai.timefold.solver.core.impl.solver.scope.SolverScope;
14+
import ai.timefold.solver.core.impl.solver.termination.UnimprovedTimeMillisSpentTermination;
15+
16+
import org.junit.jupiter.api.Test;
17+
18+
class UnimprovedTimeStuckCriterionTest {
19+
20+
@Test
21+
void isSolverStuck() {
22+
var solverScope = mock(SolverScope.class);
23+
var phaseScope = mock(LocalSearchPhaseScope.class);
24+
var stepScope = mock(LocalSearchStepScope.class);
25+
var moveScope = mock(LocalSearchMoveScope.class);
26+
var termination = mock(UnimprovedTimeMillisSpentTermination.class);
27+
28+
when(moveScope.getStepScope()).thenReturn(stepScope);
29+
when(stepScope.getPhaseScope()).thenReturn(phaseScope);
30+
when(phaseScope.getSolverScope()).thenReturn(solverScope);
31+
when(moveScope.getScore()).thenReturn(SimpleScore.of(1));
32+
when(phaseScope.getBestScore()).thenReturn(SimpleScore.of(1));
33+
when(termination.isPhaseTerminated(any())).thenReturn(false, true);
34+
35+
// No restart
36+
var strategy = new UnimprovedTimeStuckCriterion<>(termination);
37+
strategy.solvingStarted(null);
38+
strategy.phaseStarted(phaseScope);
39+
assertThat(strategy.isSolverStuck(moveScope)).isFalse();
40+
41+
// First restart
42+
assertThat(strategy.isSolverStuck(moveScope)).isTrue();
43+
assertThat(strategy.nextRestart).isEqualTo(2L * START_TIME_WINDOW_MILLIS);
44+
45+
// Second restart
46+
assertThat(strategy.isSolverStuck(moveScope)).isTrue();
47+
assertThat(strategy.nextRestart).isEqualTo(3L * START_TIME_WINDOW_MILLIS);
48+
}
49+
50+
@Test
51+
void reset() {
52+
var solverScope = mock(SolverScope.class);
53+
var phaseScope = mock(LocalSearchPhaseScope.class);
54+
var stepScope = mock(LocalSearchStepScope.class);
55+
var moveScope = mock(LocalSearchMoveScope.class);
56+
var termination = mock(UnimprovedTimeMillisSpentTermination.class);
57+
58+
when(moveScope.getStepScope()).thenReturn(stepScope);
59+
when(stepScope.getPhaseScope()).thenReturn(phaseScope);
60+
when(phaseScope.getSolverScope()).thenReturn(solverScope);
61+
when(moveScope.getScore()).thenReturn(SimpleScore.of(1));
62+
when(phaseScope.getBestScore()).thenReturn(SimpleScore.of(1));
63+
when(termination.isPhaseTerminated(any())).thenReturn(true);
64+
65+
// Restart
66+
var strategy = new UnimprovedTimeStuckCriterion<>(termination);
67+
strategy.solvingStarted(null);
68+
strategy.phaseStarted(phaseScope);
69+
assertThat(strategy.isSolverStuck(moveScope)).isTrue();
70+
assertThat(strategy.nextRestart).isEqualTo(2L * START_TIME_WINDOW_MILLIS);
71+
72+
// Reset
73+
strategy.stepStarted(stepScope);
74+
when(phaseScope.getBestScore()).thenReturn(SimpleScore.of(2));
75+
strategy.stepEnded(stepScope);
76+
assertThat(strategy.nextRestart).isEqualTo(START_TIME_WINDOW_MILLIS);
77+
}
78+
}

0 commit comments

Comments
 (0)