Skip to content

Commit 8e06dc7

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

File tree

7 files changed

+127
-73
lines changed

7 files changed

+127
-73
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ private Optional<MoveTabuAcceptor<Solution_>> buildMoveTabuAcceptor(HeuristicCon
223223
|| (!acceptorTypeListsContainsAcceptorType(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE)
224224
&& acceptorConfig.getLateAcceptanceSize() != null)) {
225225
StuckCriterion<Solution_> strategy = new DiminishedReturnsStuckCriterion<>();
226-
var acceptor = new LateAcceptanceAcceptor<>(strategy);
226+
var acceptor = new LateAcceptanceAcceptor<>(true, strategy);
227227
acceptor.setLateAcceptanceSize(Objects.requireNonNullElse(acceptorConfig.getLateAcceptanceSize(), 400));
228228
return Optional.of(acceptor);
229229
}

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

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,50 +13,62 @@ public abstract class RestartableAcceptor<Solution_> extends AbstractAcceptor<So
1313

1414
private final StuckCriterion<Solution_> stuckCriterion;
1515
protected boolean restartTriggered;
16+
private boolean enabled;
1617

17-
protected RestartableAcceptor(StuckCriterion<Solution_> stuckCriterion) {
18+
protected RestartableAcceptor(boolean enabled, StuckCriterion<Solution_> stuckCriterion) {
19+
this.enabled = enabled;
1820
this.stuckCriterion = stuckCriterion;
1921
}
2022

2123
@Override
2224
public void solvingStarted(SolverScope<Solution_> solverScope) {
2325
super.solvingStarted(solverScope);
24-
stuckCriterion.solvingStarted(solverScope);
26+
if (enabled) {
27+
stuckCriterion.solvingStarted(solverScope);
28+
}
2529
}
2630

2731
@Override
2832
public void phaseStarted(LocalSearchPhaseScope<Solution_> phaseScope) {
2933
super.phaseStarted(phaseScope);
30-
stuckCriterion.phaseStarted(phaseScope);
34+
if (enabled) {
35+
stuckCriterion.phaseStarted(phaseScope);
36+
}
3137
}
3238

3339
@Override
3440
public void phaseEnded(LocalSearchPhaseScope<Solution_> phaseScope) {
3541
super.phaseEnded(phaseScope);
36-
stuckCriterion.phaseEnded(phaseScope);
42+
if (enabled) {
43+
stuckCriterion.phaseEnded(phaseScope);
44+
}
3745
}
3846

3947
@Override
4048
public void stepStarted(LocalSearchStepScope<Solution_> stepScope) {
4149
super.stepStarted(stepScope);
42-
stuckCriterion.stepStarted(stepScope);
50+
if (enabled) {
51+
stuckCriterion.stepStarted(stepScope);
52+
}
4353
}
4454

4555
@Override
4656
public void stepEnded(LocalSearchStepScope<Solution_> stepScope) {
4757
super.stepEnded(stepScope);
48-
if (stuckCriterion.isSolverStuck(stepScope)) {
49-
if (rejectRestartEvent()) {
50-
// We need to reset the criterion,
51-
// or it will trigger the restart event in the next evaluation
52-
stuckCriterion.reset(stepScope);
53-
restartTriggered = false;
54-
} else {
55-
stepScope.getPhaseScope().setSolverStuck(true);
56-
restartTriggered = true;
58+
if (enabled) {
59+
if (stuckCriterion.isSolverStuck(stepScope)) {
60+
if (rejectRestartEvent()) {
61+
// We need to reset the criterion,
62+
// or it will trigger the restart event in the next evaluation
63+
stuckCriterion.reset(stepScope);
64+
restartTriggered = false;
65+
} else {
66+
stepScope.getPhaseScope().setSolverStuck(true);
67+
restartTriggered = true;
68+
}
5769
}
70+
stuckCriterion.stepEnded(stepScope);
5871
}
59-
stuckCriterion.stepEnded(stepScope);
6072
}
6173

6274
/**

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

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,27 @@
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

36-
public LateAcceptanceAcceptor(StuckCriterion<Solution_> stuckCriterionDetection) {
37-
super(stuckCriterionDetection);
37+
public LateAcceptanceAcceptor(boolean enableRestart, StuckCriterion<Solution_> stuckCriterionDetection) {
38+
super(enableRestart, stuckCriterionDetection);
3839
}
3940

4041
public void setLateAcceptanceSize(int lateAcceptanceSize) {
@@ -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
}

0 commit comments

Comments
 (0)