Skip to content

Commit 1d82e9b

Browse files
committed
feat: restart when max rejections reached
1 parent 935c769 commit 1d82e9b

File tree

5 files changed

+89
-47
lines changed

5 files changed

+89
-47
lines changed

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

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,24 @@
1515
public class LateAcceptanceAcceptor<Solution_> extends RestartableAcceptor<Solution_> {
1616

1717
protected static final double MIN_DIVERSITY_RATIO = 0.05;
18-
// The goal is to increase from hundreds to thousands in the first restart event and then increment it linearly
19-
protected static final int SCALING_FACTOR = 10;
18+
protected static final int SCALE_FACTOR = 3;
19+
private static final int MAX_REJECTED_EVENTS = 3;
2020

2121
protected int lateAcceptanceSize = -1;
2222
protected boolean hillClimbingEnabled = true;
2323

2424
protected Score<?>[] previousScores;
2525
protected int lateScoreIndex = -1;
2626

27-
private int maxBestScoreSize;
2827
private Score<?> bestStepScore;
2928
private Score<?> currentBestScore;
3029
// Keep track of the best scores accumulated so far. This list will be used to reseed the later elements list.
3130
protected Deque<Score<?>> bestScoreQueue;
32-
private int defaultLateAcceptanceSize;
31+
protected int defaultLateAcceptanceSize;
32+
protected int maxBestScoreSize;
3333
protected int coefficient;
34-
private int countRestartWithoutImprovement;
34+
private boolean allowDecrease;
35+
private int countRejected;
3536

3637
public LateAcceptanceAcceptor(StuckCriterion<Solution_> stuckCriterionDetection) {
3738
super(stuckCriterionDetection);
@@ -64,13 +65,13 @@ public void phaseStarted(LocalSearchPhaseScope<Solution_> phaseScope) {
6465
var initialScore = phaseScope.getBestScore();
6566
Arrays.fill(previousScores, initialScore);
6667
lateScoreIndex = 0;
67-
coefficient = 0;
68-
countRestartWithoutImprovement = 0;
69-
defaultLateAcceptanceSize = lateAcceptanceSize;
70-
// The maximum size is three times the size of the initial element list
71-
maxBestScoreSize = defaultLateAcceptanceSize * 3 * SCALING_FACTOR;
68+
coefficient = 1;
69+
allowDecrease = false;
70+
countRejected = 0;
71+
maxBestScoreSize = lateAcceptanceSize * 3;
7272
bestScoreQueue = new ArrayDeque<>(maxBestScoreSize);
7373
bestScoreQueue.addLast(initialScore);
74+
defaultLateAcceptanceSize = lateAcceptanceSize;
7475
}
7576

7677
private void validate() {
@@ -106,14 +107,14 @@ public void stepEnded(LocalSearchStepScope<Solution_> stepScope) {
106107
previousScores[lateScoreIndex] = stepScope.getScore();
107108
lateScoreIndex = (lateScoreIndex + 1) % lateAcceptanceSize;
108109
if (((Score) currentBestScore).compareTo(stepScope.getScore()) < 0) {
109-
if (countRestartWithoutImprovement > 0 && coefficient > 0) {
110+
if (allowDecrease) {
110111
// We decrease the coefficient
111112
// if there is an improvement
112113
// to avoid altering the late element size in the next restart event.
113114
// This action is performed only once after a restart event
114-
coefficient--;
115+
coefficient /= SCALE_FACTOR;
116+
allowDecrease = false;
115117
}
116-
countRestartWithoutImprovement = 0;
117118
if (bestScoreQueue.size() < maxBestScoreSize) {
118119
bestScoreQueue.addLast(stepScope.getScore());
119120
} else {
@@ -137,18 +138,26 @@ public boolean rejectRestartEvent() {
137138
// and the proposed approach requires some diversity to reseed the scores.
138139
var reject = diversity > MIN_DIVERSITY_RATIO || bestScoreQueue.size() == 1;
139140
if (reject) {
141+
countRejected++;
142+
if (countRejected > MAX_REJECTED_EVENTS && bestScoreQueue.size() > 1) {
143+
logger.info(
144+
"Restart event not rejected. Diversity ({}), Count best scores ({}), Distinct Elements ({}), Rejection Count ({})",
145+
diversity, bestScoreQueue.size(), distinctElements, countRejected);
146+
return false;
147+
}
140148
logger.info(
141-
"Restart event delayed. Diversity ({}), Count best scores ({}), Distinct Elements ({}), Restart without Improvement ({})",
142-
diversity, bestScoreQueue.size(), distinctElements, countRestartWithoutImprovement);
149+
"Restart event delayed. Diversity ({}), Count best scores ({}), Distinct Elements ({}), Rejection Count ({})",
150+
diversity, bestScoreQueue.size(), distinctElements, countRejected);
143151
}
144152
return reject;
145153
}
146154

147155
@Override
148156
public void restart(LocalSearchStepScope<Solution_> stepScope) {
149-
countRestartWithoutImprovement++;
150-
coefficient++;
151-
var newLateAcceptanceSize = defaultLateAcceptanceSize * coefficient * SCALING_FACTOR;
157+
coefficient *= SCALE_FACTOR;
158+
allowDecrease = true;
159+
countRejected = 0;
160+
var newLateAcceptanceSize = defaultLateAcceptanceSize * coefficient;
152161
if (logger.isInfoEnabled()) {
153162
if (lateAcceptanceSize == newLateAcceptanceSize) {
154163
logger.info("Keeping the lateAcceptanceSize as {}.", lateAcceptanceSize);
@@ -171,15 +180,15 @@ public void restart(LocalSearchStepScope<Solution_> stepScope) {
171180
*/
172181
private void rebuildLateElementsList(int newLateAcceptanceSize) {
173182
var newPreviousScores = new Score[newLateAcceptanceSize];
174-
var countPerScore = newLateAcceptanceSize / bestScoreQueue.size() + 1;
175-
var count = new MutableInt(newLateAcceptanceSize - 1);
176-
var iterator = bestScoreQueue.descendingIterator();
177-
while (count.intValue() >= 0 && iterator.hasNext()) {
183+
var countPerScore = Math.min(newLateAcceptanceSize / 2, (newLateAcceptanceSize / bestScoreQueue.size()) * 2);
184+
var count = new MutableInt(0);
185+
var iterator = bestScoreQueue.iterator();
186+
while (count.intValue() < newLateAcceptanceSize && iterator.hasNext()) {
178187
var score = iterator.next();
179188
for (var i = 0; i < countPerScore; i++) {
180189
newPreviousScores[count.intValue()] = score;
181-
count.decrement();
182-
if (count.intValue() < 0) {
190+
count.increment();
191+
if (count.intValue() == newLateAcceptanceSize) {
183192
break;
184193
}
185194
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@
1010
/**
1111
* Restart strategy, which exponentially increases the metric that triggers the restart process.
1212
* The first restart occurs after the {@code scalingFactor * GEOMETRIC_FACTOR^restartCount} metric.
13-
* Following that, the metric increases exponentially: 1, 2, 3, 5, 7...
13+
* Following that, the metric increases exponentially: 1, 3, 9, 27...
1414
* <p>
1515
* The strategy is based on the work: Search in a Small World by Toby Walsh
1616
*
1717
* @param <Solution_> the solution type
1818
*/
1919
public abstract class AbstractGeometricStuckCriterion<Solution_> implements StuckCriterion<Solution_> {
2020
protected static final Logger logger = LoggerFactory.getLogger(AbstractGeometricStuckCriterion.class);
21-
private static final double GEOMETRIC_FACTOR = 1.4; // Value extracted from the cited paper
21+
private static final double GEOMETRIC_FACTOR = 3; // Value extracted from the cited paper
2222

2323
private double scalingFactor;
2424
protected long nextRestart;

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

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,19 @@
77

88
public class DiminishedReturnsStuckCriterion<Solution_, Score_ extends Score<Score_>>
99
extends AbstractGeometricStuckCriterion<Solution_> {
10-
// Time window used at the beginning of the solving process
11-
protected static final long START_TIME_WINDOW_MILLIS = 10_000;
12-
// Time window used once the first restart event is triggered and accepted
13-
protected static final long REGULAR_TIME_WINDOW_MILLIS = 600_000;
10+
protected static final long TIME_WINDOW_MILLIS = 10_000;
1411
private static final double MINIMAL_IMPROVEMENT = 0.0001;
1512

1613
private DiminishedReturnsTermination<Solution_, Score_> diminishedReturnsCriterion;
1714

18-
private boolean triggered;
15+
protected boolean triggered;
1916

2017
public DiminishedReturnsStuckCriterion() {
21-
this(new DiminishedReturnsTermination<>(START_TIME_WINDOW_MILLIS, MINIMAL_IMPROVEMENT));
18+
this(new DiminishedReturnsTermination<>(TIME_WINDOW_MILLIS, MINIMAL_IMPROVEMENT));
2219
}
2320

2421
protected DiminishedReturnsStuckCriterion(DiminishedReturnsTermination<Solution_, Score_> diminishedReturnsCriterion) {
25-
super(START_TIME_WINDOW_MILLIS);
22+
super(TIME_WINDOW_MILLIS);
2623
this.diminishedReturnsCriterion = diminishedReturnsCriterion;
2724
}
2825

@@ -47,11 +44,6 @@ public void reset(LocalSearchStepScope<Solution_> stepScope) {
4744
@Override
4845
public void stepStarted(LocalSearchStepScope<Solution_> stepScope) {
4946
if (triggered) {
50-
// Once the first restart event is triggered and accepted, we adjust the time window to the regular one.
51-
// The aim is to give the solver more time to operate after applying the restart configuration.
52-
if (getScalingFactor() == START_TIME_WINDOW_MILLIS) {
53-
setScalingFactor(REGULAR_TIME_WINDOW_MILLIS);
54-
}
5547
reset(stepScope);
5648
}
5749
}

core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptorTest.java

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,46 @@ void delayRestart() {
312312
assertThat(phaseScope.isSolverStuck()).isFalse();
313313
}
314314

315+
@Test
316+
void restartAfterDelays() {
317+
var stuckCriterion = mock(StuckCriterion.class);
318+
var acceptor = new LateAcceptanceAcceptor<>(stuckCriterion);
319+
acceptor.setLateAcceptanceSize(5);
320+
var solverScope = new SolverScope<>();
321+
var phaseScope = new LocalSearchPhaseScope<>(solverScope, 0);
322+
var stepScope0 = new LocalSearchStepScope<>(phaseScope);
323+
stepScope0.setScore(SimpleScore.of(-1000));
324+
var moveScope0 = buildMoveScope(stepScope0, -3000);
325+
phaseScope.setLastCompletedStepScope(stepScope0);
326+
solverScope.setBestScore(SimpleScore.of(-1000));
327+
when(stuckCriterion.isSolverStuck(any())).thenReturn(true);
328+
329+
// Init
330+
acceptor.solvingStarted(solverScope);
331+
acceptor.phaseStarted(phaseScope);
332+
acceptor.stepStarted(stepScope0);
333+
assertThat(acceptor.isAccepted(moveScope0)).isFalse();
334+
335+
// Delay because the diversity is still high
336+
acceptor.bestScoreQueue.addLast(SimpleScore.of(-999));
337+
acceptor.bestScoreQueue.addLast(SimpleScore.of(-998));
338+
acceptor.previousScores[0] = SimpleScore.of(-1002);
339+
acceptor.previousScores[1] = SimpleScore.of(-1001);
340+
acceptor.previousScores[2] = SimpleScore.of(-1000);
341+
acceptor.previousScores[3] = SimpleScore.of(-999);
342+
acceptor.previousScores[4] = SimpleScore.of(-998);
343+
344+
// Three delays in a row
345+
acceptor.stepEnded(stepScope0);
346+
acceptor.stepEnded(stepScope0);
347+
acceptor.stepEnded(stepScope0);
348+
assertThat(phaseScope.isSolverStuck()).isFalse();
349+
350+
// Trigger it
351+
acceptor.stepEnded(stepScope0);
352+
assertThat(phaseScope.isSolverStuck()).isTrue();
353+
}
354+
315355
@Test
316356
void restart() {
317357
var stuckCriterion = mock(StuckCriterion.class);
@@ -341,6 +381,7 @@ void restart() {
341381
acceptor.bestScoreQueue.addLast(SimpleScore.of(-1001));
342382
acceptor.bestScoreQueue.addLast(SimpleScore.of(-1002));
343383

384+
assertThat(phaseScope.isSolverStuck()).isFalse();
344385
acceptor.stepEnded(stepScope0);
345386
assertThat(phaseScope.isSolverStuck()).isTrue();
346387
}
@@ -399,12 +440,12 @@ void reconfigureAfterImprovement() {
399440
acceptor.bestScoreQueue.addLast(SimpleScore.of(-996));
400441
acceptor.bestScoreQueue.addLast(SimpleScore.of(-995));
401442
restartStrategy.applyRestart(stepScope0);
402-
assertThat(acceptor.coefficient).isOne();
443+
assertThat(acceptor.coefficient).isEqualTo(LateAcceptanceAcceptor.SCALE_FACTOR);
403444

404445
// Step that improves the best solution should not change the late elements list in the next restart
405446
var stepScope1 = new LocalSearchStepScope<>(phaseScope);
406447
stepScope1.setScore(SimpleScore.of(-900));
407448
acceptor.stepEnded(stepScope1);
408-
assertThat(acceptor.coefficient).isZero();
449+
assertThat(acceptor.coefficient).isOne();
409450
}
410451
}

core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/stuckcriterion/DiminishedReturnsStuckCriterionTest.java

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

3-
import static ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.DiminishedReturnsStuckCriterion.REGULAR_TIME_WINDOW_MILLIS;
4-
import static ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.DiminishedReturnsStuckCriterion.START_TIME_WINDOW_MILLIS;
3+
import static ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.DiminishedReturnsStuckCriterion.TIME_WINDOW_MILLIS;
54
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
65
import static org.mockito.ArgumentMatchers.any;
76
import static org.mockito.ArgumentMatchers.anyLong;
@@ -42,11 +41,11 @@ void isSolverStuck() {
4241

4342
// First restart
4443
assertThat(strategy.isSolverStuck(stepScope)).isTrue();
45-
assertThat(strategy.nextRestart).isEqualTo(2L * START_TIME_WINDOW_MILLIS);
44+
assertThat(strategy.nextRestart).isEqualTo(2L * TIME_WINDOW_MILLIS);
4645

4746
// Second restart
4847
assertThat(strategy.isSolverStuck(stepScope)).isTrue();
49-
assertThat(strategy.nextRestart).isEqualTo(3L * START_TIME_WINDOW_MILLIS);
48+
assertThat(strategy.nextRestart).isEqualTo(3L * TIME_WINDOW_MILLIS);
5049
}
5150

5251
@Test
@@ -69,12 +68,13 @@ void reset() {
6968
strategy.solvingStarted(null);
7069
strategy.phaseStarted(phaseScope);
7170
assertThat(strategy.isSolverStuck(stepScope)).isTrue();
72-
assertThat(strategy.nextRestart).isEqualTo(2L * START_TIME_WINDOW_MILLIS);
71+
assertThat(strategy.nextRestart).isEqualTo(2L * TIME_WINDOW_MILLIS);
72+
assertThat(strategy.triggered).isTrue();
7373

7474
// Reset
7575
strategy.stepStarted(stepScope);
7676
when(phaseScope.getBestScore()).thenReturn(SimpleScore.of(2));
77-
strategy.stepEnded(stepScope);
78-
assertThat(strategy.nextRestart).isEqualTo(REGULAR_TIME_WINDOW_MILLIS);
77+
strategy.reset(stepScope);
78+
assertThat(strategy.triggered).isFalse();
7979
}
8080
}

0 commit comments

Comments
 (0)