Skip to content

Commit 4a1b322

Browse files
committed
chore: improve restart logic
1 parent 6221966 commit 4a1b322

File tree

6 files changed

+35
-40
lines changed

6 files changed

+35
-40
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@ public void solvingEnded(SolverScope<Solution_> solverScope) {
181181

182182
public void setWorkingSolutionFromBestSolution(LocalSearchStepScope<Solution_> stepScope) {
183183
stepScope.getPhaseScope().getSolverScope().setWorkingSolutionFromBestSolution();
184+
// Adjust the step score to reflect the best score,
185+
// ensuring the score of the last completed step is the current best one
186+
stepScope.setScore(stepScope.getPhaseScope().getBestScore());
184187
// Changing the working solution requires reinitializing the move selector.
185188
// The acceptor should not be restarted, as this may lead to an inconsistent state,
186189
// such as changing the scores of all late elements in LA and DLAS.

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

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.StuckCriterion;
88
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope;
99
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope;
10-
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope;
1110

1211
public class DiversifiedLateAcceptanceAcceptor<Solution_> extends RestartableAcceptor<Solution_> {
1312

@@ -108,16 +107,6 @@ private void updateLateScore(Score newScore) {
108107
}
109108
}
110109

111-
@Override
112-
public void stepEnded(LocalSearchStepScope<Solution_> stepScope) {
113-
super.stepEnded(stepScope);
114-
if (restartTriggered) {
115-
// Update the current late score with the best score
116-
previousScores[lateScoreIndex] = stepScope.getPhaseScope().getBestScore();
117-
restartTriggered = false;
118-
}
119-
}
120-
121110
@Override
122111
public void phaseEnded(LocalSearchPhaseScope<Solution_> phaseScope) {
123112
super.phaseEnded(phaseScope);

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ public void stepEnded(LocalSearchStepScope<Solution_> stepScope) {
7272
previousScores[lateScoreIndex] = stepScope.getScore();
7373
lateScoreIndex = (lateScoreIndex + 1) % lateAcceptanceSize;
7474
} else {
75-
// Update the current late score with the best score, keeping the late score index the same
75+
// We only modify the current late element to raise the intensification phase,
76+
// while ensuring that the other elements remain unchanged to maintain diversity.
7677
previousScores[lateScoreIndex] = stepScope.getPhaseScope().getBestScore();
7778
restartTriggered = false;
7879
}

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

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart;
22

3-
import java.time.Instant;
3+
import java.time.Clock;
44

55
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope;
66
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope;
@@ -19,27 +19,27 @@
1919
* @param <Solution_> the solution type
2020
*/
2121
public abstract class AbstractGeometricStuckCriterion<Solution_> implements StuckCriterion<Solution_> {
22-
private static final Logger logger = LoggerFactory.getLogger(AbstractGeometricStuckCriterion.class);
22+
protected static final Logger logger = LoggerFactory.getLogger(AbstractGeometricStuckCriterion.class);
2323
private static final double GEOMETRIC_FACTOR = 1.4; // Value extracted from the cited paper
2424
private static final long GRACE_PERIOD_MILLIS = 30_000; // Same value as DiminishedReturnsTermination
2525

2626
private final double scalingFactor;
27-
private final Instant instant;
27+
private final Clock clock;
2828
private boolean gracePeriodFinished;
2929
private long gracePeriodMillis;
3030
protected long nextRestart;
3131
private double currentGeometricGrowFactor;
3232

33-
protected AbstractGeometricStuckCriterion(Instant instant, double scalingFactor) {
34-
this.instant = instant;
33+
protected AbstractGeometricStuckCriterion(Clock clock, double scalingFactor) {
34+
this.clock = clock;
3535
this.scalingFactor = scalingFactor;
3636
}
3737

3838
@Override
3939
public void phaseStarted(LocalSearchPhaseScope<Solution_> phaseScope) {
4040
if (gracePeriodMillis == 0) {
41-
// 10 seconds of grace period
42-
gracePeriodMillis = instant.toEpochMilli() + GRACE_PERIOD_MILLIS;
41+
// 30 seconds of grace period
42+
gracePeriodMillis = clock.instant().toEpochMilli() + GRACE_PERIOD_MILLIS;
4343
}
4444
}
4545

@@ -66,8 +66,11 @@ public boolean isSolverStuck(LocalSearchMoveScope<Solution_> moveScope) {
6666
if (isGracePeriodFinished()) {
6767
var triggered = evaluateCriterion(moveScope);
6868
if (triggered) {
69-
logger.trace("Restart triggered with geometric factor {}, scaling factor of {}", currentGeometricGrowFactor,
70-
scalingFactor);
69+
logger.trace(
70+
"Restart triggered with geometric factor {}, scaling factor of {}, best score ({}), move count ({})",
71+
currentGeometricGrowFactor,
72+
scalingFactor, moveScope.getStepScope().getPhaseScope().getBestScore(),
73+
moveScope.getStepScope().getPhaseScope().getSolverScope().getMoveEvaluationCount());
7174
currentGeometricGrowFactor = Math.ceil(currentGeometricGrowFactor * GEOMETRIC_FACTOR);
7275
nextRestart = calculateNextRestart();
7376
return true;
@@ -80,7 +83,7 @@ protected boolean isGracePeriodFinished() {
8083
if (gracePeriodFinished) {
8184
return true;
8285
}
83-
gracePeriodFinished = instant.toEpochMilli() >= gracePeriodMillis;
86+
gracePeriodFinished = clock.instant().toEpochMilli() >= gracePeriodMillis;
8487
return gracePeriodFinished;
8588
}
8689

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

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart;
22

33
import java.time.Clock;
4-
import java.time.Instant;
54

65
import ai.timefold.solver.core.api.score.Score;
76
import ai.timefold.solver.core.impl.localsearch.decider.reconfiguration.RestartStrategy;
87
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope;
9-
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope;
108
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope;
119
import ai.timefold.solver.core.impl.solver.scope.SolverScope;
1210

@@ -17,24 +15,18 @@
1715
*/
1816
public class UnimprovedMoveCountStuckCriterion<Solution_> extends AbstractGeometricStuckCriterion<Solution_> {
1917

20-
// 50k moves multiplier defined through experiments
21-
protected static final long UNIMPROVED_MOVE_COUNT_MULTIPLIER = 50_000;
18+
// Multiplier defined through experiments
19+
protected static final long UNIMPROVED_MOVE_COUNT_MULTIPLIER = 300_000;
2220
// Last checkpoint of a solution improvement or the restart process
2321
protected long lastCheckpoint;
2422
private Score<?> currentBestScore;
2523

2624
public UnimprovedMoveCountStuckCriterion() {
27-
this(Instant.now(Clock.systemUTC()));
25+
this(Clock.systemUTC());
2826
}
2927

30-
protected UnimprovedMoveCountStuckCriterion(Instant instant) {
31-
super(instant, UNIMPROVED_MOVE_COUNT_MULTIPLIER);
32-
}
33-
34-
@Override
35-
public void phaseStarted(LocalSearchPhaseScope<Solution_> phaseScope) {
36-
super.phaseStarted(phaseScope);
37-
currentBestScore = phaseScope.getBestScore();
28+
protected UnimprovedMoveCountStuckCriterion(Clock clock) {
29+
super(clock, UNIMPROVED_MOVE_COUNT_MULTIPLIER);
3830
}
3931

4032
@Override

core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterionTest.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import static org.mockito.Mockito.mock;
66
import static org.mockito.Mockito.when;
77

8+
import java.time.Clock;
89
import java.time.Instant;
910

1011
import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore;
@@ -20,11 +21,13 @@ class UnimprovedMoveCountStuckCriterionTest {
2021

2122
@Test
2223
void isSolverStuck() {
24+
var clock = mock(Clock.class);
2325
var instant = mock(Instant.class);
2426
var solverScope = mock(SolverScope.class);
2527
var phaseScope = mock(LocalSearchPhaseScope.class);
2628
var stepScope = mock(LocalSearchStepScope.class);
2729
var moveScope = mock(LocalSearchMoveScope.class);
30+
when(clock.instant()).thenReturn(instant);
2831
when(moveScope.getStepScope()).thenReturn(stepScope);
2932
when(stepScope.getPhaseScope()).thenReturn(phaseScope);
3033
when(phaseScope.getSolverScope()).thenReturn(solverScope);
@@ -33,7 +36,7 @@ void isSolverStuck() {
3336
when(phaseScope.getBestScore()).thenReturn(SimpleScore.of(1000));
3437

3538
// Finish grace period
36-
var strategy = new UnimprovedMoveCountStuckCriterion<>(instant);
39+
var strategy = new UnimprovedMoveCountStuckCriterion<>(clock);
3740
strategy.solvingStarted(null);
3841
strategy.phaseStarted(phaseScope);
3942
assertThat(strategy.isSolverStuck(moveScope)).isFalse();
@@ -51,15 +54,15 @@ void isSolverStuck() {
5154

5255
// Second restart
5356
Mockito.reset(instant);
54-
var secondCount = 2L * UNIMPROVED_MOVE_COUNT_MULTIPLIER + firstCount;
57+
var secondCount = 2L * UNIMPROVED_MOVE_COUNT_MULTIPLIER + firstCount + 1;
5558
when(solverScope.getMoveEvaluationCount()).thenReturn(secondCount);
5659
assertThat(strategy.isSolverStuck(moveScope)).isTrue();
5760
assertThat(strategy.lastCheckpoint).isEqualTo(secondCount);
5861
assertThat(strategy.nextRestart).isEqualTo(3L * UNIMPROVED_MOVE_COUNT_MULTIPLIER);
5962
assertThat(strategy.isSolverStuck(moveScope)).isFalse();
6063

6164
// Third restart
62-
var thirdCount = 3L * UNIMPROVED_MOVE_COUNT_MULTIPLIER + secondCount;
65+
var thirdCount = 3L * UNIMPROVED_MOVE_COUNT_MULTIPLIER + secondCount + 1;
6366
Mockito.reset(instant);
6467
when(solverScope.getMoveEvaluationCount()).thenReturn(thirdCount);
6568
assertThat(strategy.isSolverStuck(moveScope)).isTrue();
@@ -69,11 +72,13 @@ void isSolverStuck() {
6972

7073
@Test
7174
void updateBestSolution() {
75+
var clock = mock(Clock.class);
7276
var instant = mock(Instant.class);
7377
var solverScope = mock(SolverScope.class);
7478
var phaseScope = mock(LocalSearchPhaseScope.class);
7579
var stepScope = mock(LocalSearchStepScope.class);
7680
var moveScope = mock(LocalSearchMoveScope.class);
81+
when(clock.instant()).thenReturn(instant);
7782
when(phaseScope.getSolverScope()).thenReturn(solverScope);
7883
when(stepScope.getPhaseScope()).thenReturn(phaseScope);
7984
when(moveScope.getStepScope()).thenReturn(stepScope);
@@ -83,7 +88,7 @@ void updateBestSolution() {
8388
UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1);
8489
when(solverScope.getMoveEvaluationCount()).thenReturn(1000L, 1001L);
8590

86-
var strategy = new UnimprovedMoveCountStuckCriterion<>(instant);
91+
var strategy = new UnimprovedMoveCountStuckCriterion<>(clock);
8792
strategy.solvingStarted(mock(SolverScope.class));
8893
strategy.phaseStarted(phaseScope);
8994
strategy.stepStarted(stepScope);
@@ -96,18 +101,20 @@ void updateBestSolution() {
96101

97102
@Test
98103
void reset() {
104+
var clock = mock(Clock.class);
99105
var instant = mock(Instant.class);
100106
var solverScope = mock(SolverScope.class);
101107
var phaseScope = mock(LocalSearchPhaseScope.class);
102108
var stepScope = mock(LocalSearchStepScope.class);
103109
var moveScope = mock(LocalSearchMoveScope.class);
110+
when(clock.instant()).thenReturn(instant);
104111
when(moveScope.getStepScope()).thenReturn(stepScope);
105112
when(stepScope.getPhaseScope()).thenReturn(phaseScope);
106113
when(phaseScope.getSolverScope()).thenReturn(solverScope);
107114
when(instant.toEpochMilli()).thenReturn(1000L, UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1000L);
108115
when(solverScope.getMoveEvaluationCount()).thenReturn(1000L);
109116

110-
var strategy = new UnimprovedMoveCountStuckCriterion<>(instant);
117+
var strategy = new UnimprovedMoveCountStuckCriterion<>(clock);
111118
strategy.solvingStarted(mock(SolverScope.class));
112119
strategy.phaseStarted(mock(LocalSearchPhaseScope.class));
113120
// Trigger

0 commit comments

Comments
 (0)