Skip to content

Commit a68d0ef

Browse files
committed
feat: adaptive late acceptance
1 parent 2bef09e commit a68d0ef

File tree

10 files changed

+38
-75
lines changed

10 files changed

+38
-75
lines changed

benchmark/src/main/resources/benchmark.xsd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3146,7 +3146,7 @@
31463146
<xs:enumeration value="DIVERSIFIED_LATE_ACCEPTANCE"/>
31473147

31483148

3149-
<xs:enumeration value="ITERATED_LOCAL_SEARCH"/>
3149+
<xs:enumeration value="ADAPTIVE_LATE_ACCEPTANCE"/>
31503150

31513151

31523152
<xs:enumeration value="GREAT_DELUGE"/>

core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/AcceptorType.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public enum AcceptorType {
1212
SIMULATED_ANNEALING,
1313
LATE_ACCEPTANCE,
1414
DIVERSIFIED_LATE_ACCEPTANCE,
15-
ITERATED_LOCAL_SEARCH,
15+
ADAPTIVE_LATE_ACCEPTANCE,
1616
GREAT_DELUGE,
1717
STEP_COUNTING_HILL_CLIMBING
1818
}

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,8 @@ phaseTermination, buildDecider(phaseConfigPolicy, phaseTermination))
7070
private LocalSearchDecider<Solution_> buildDecider(HeuristicConfigPolicy<Solution_> configPolicy,
7171
PhaseTermination<Solution_> termination) {
7272
var moveSelector = buildMoveSelector(configPolicy);
73-
var perturbationMoveSelector = buildPerturbationMoveSelector(configPolicy);
7473
var acceptor = buildAcceptor(configPolicy);
75-
RestartStrategy<Solution_> restartStrategy = new AcceptorRestartStrategy<>(perturbationMoveSelector, acceptor);
74+
RestartStrategy<Solution_> restartStrategy = new AcceptorRestartStrategy<>(acceptor);
7675
var forager = buildForager(configPolicy);
7776
if (moveSelector.isNeverEnding() && !forager.supportsNeverEndingMoveSelector()) {
7877
throw new IllegalStateException("The moveSelector (" + moveSelector

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,14 @@ public void solvingEnded(SolverScope<Solution_> solverScope) {
193193
forager.solvingEnded(solverScope);
194194
}
195195

196-
public void restoreCurrentBestSolution(LocalSearchPhaseScope<Solution_> phaseScope) {
197-
phaseScope.getSolverScope().setWorkingSolutionFromBestSolution();
198-
moveSelectorPhaseStarted(phaseScope);
196+
public void restoreCurrentBestSolution(LocalSearchStepScope<Solution_> stepScope) {
197+
stepScope.getPhaseScope().getSolverScope().setWorkingSolutionFromBestSolution();
198+
stepScope.setScore(stepScope.getPhaseScope().getBestScore());
199+
// Changing the working solution requires reinitializing the move selector.
200+
// The acceptor should not be restarted, as this may lead to an inconsistent state,
201+
// such as changing the scores of all late elements in LA and DLAS.
202+
// 1 - The move selector will reset all cached lists using old solution entity references
203+
moveSelector.phaseStarted(stepScope.getPhaseScope());
199204
}
200205

201206
public void solvingError(SolverScope<Solution_> solverScope, Exception exception) {

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy;
1414
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.greatdeluge.GreatDelugeAcceptor;
1515
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.hillclimbing.HillClimbingAcceptor;
16-
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.iteratedlocalsearch.IteratedLocalSearchAcceptor;
16+
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.iteratedlocalsearch.AdaptiveLateAcceptanceAcceptor;
1717
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance.DiversifiedLateAcceptanceAcceptor;
1818
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance.LateAcceptanceAcceptor;
1919
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.simulatedannealing.SimulatedAnnealingAcceptor;
@@ -244,11 +244,11 @@ private Optional<MoveTabuAcceptor<Solution_>> buildMoveTabuAcceptor(HeuristicCon
244244
return Optional.empty();
245245
}
246246

247-
private Optional<IteratedLocalSearchAcceptor<Solution_>>
247+
private Optional<AdaptiveLateAcceptanceAcceptor<Solution_>>
248248
buildIteratedLocalSearchAcceptor() {
249-
if (acceptorTypeListsContainsAcceptorType(AcceptorType.ITERATED_LOCAL_SEARCH)) {
249+
if (acceptorTypeListsContainsAcceptorType(AcceptorType.ADAPTIVE_LATE_ACCEPTANCE)) {
250250
StuckCriterion<Solution_> strategy = new UnimprovedMoveCountStuckCriterion<>();
251-
var acceptor = new IteratedLocalSearchAcceptor<>(3, strategy);
251+
var acceptor = new AdaptiveLateAcceptanceAcceptor<>(strategy);
252252
return Optional.of(acceptor);
253253
}
254254
return Optional.empty();

core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/iteratedlocalsearch/IteratedLocalSearchAcceptor.java renamed to core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/iteratedlocalsearch/AdaptiveLateAcceptanceAcceptor.java

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

33
import ai.timefold.solver.core.api.score.Score;
4-
import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector;
54
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.RestartableAcceptor;
65
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance.LateAcceptanceAcceptor;
76
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.StuckCriterion;
@@ -10,35 +9,27 @@
109
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope;
1110
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope;
1211

13-
public class IteratedLocalSearchAcceptor<Solution_> extends RestartableAcceptor<Solution_> {
12+
public class AdaptiveLateAcceptanceAcceptor<Solution_> extends RestartableAcceptor<Solution_> {
1413

1514
private static final int[] LATE_ELEMENTS_SIZE =
16-
new int[] { 12_500, 25_000, 25_000, 50_000, 50_000 };
15+
new int[] { 2_500, 5_000, 12_500, 25_000, 25_000 };
1716
private static final int[] LATE_ELEMENTS_MAX_REJECTIONS =
18-
new int[] { 12_500, 25_000, 50_000, 50_000, 75_000 };
17+
new int[] { 5_000, 5_000, 12_500, 25_000, 50_000 };
1918
private final LateAcceptanceAcceptor<Solution_> lateAcceptanceAcceptor;
20-
private MoveSelector<Solution_> perturbationMoveSelector;
21-
private final int maxPerturbationCount;
2219
private int lateIndex;
23-
private int perturbationCount;
24-
private Score<?> currentBestScore;
20+
private Score<?> initialScore;
2521

26-
public IteratedLocalSearchAcceptor(int maxPerturbationCount, StuckCriterion<Solution_> stuckCriterion) {
22+
public AdaptiveLateAcceptanceAcceptor(StuckCriterion<Solution_> stuckCriterion) {
2723
super(true, stuckCriterion);
28-
this.maxPerturbationCount = maxPerturbationCount;
2924
this.lateAcceptanceAcceptor = new LateAcceptanceAcceptor<>(false, false, null);
3025
}
3126

32-
public void setPerturbationMoveSelector(MoveSelector<Solution_> perturbationMoveSelector) {
33-
this.perturbationMoveSelector = perturbationMoveSelector;
34-
}
35-
3627
@Override
3728
public void phaseStarted(LocalSearchPhaseScope<Solution_> phaseScope) {
3829
super.phaseStarted(phaseScope);
39-
perturbationCount = 1;
30+
initialScore = phaseScope.getBestScore();
4031
lateIndex = 0;
41-
lateAcceptanceAcceptor.setLateAcceptanceSize(LATE_ELEMENTS_SIZE[perturbationCount]);
32+
lateAcceptanceAcceptor.setLateAcceptanceSize(LATE_ELEMENTS_SIZE[lateIndex]);
4233
lateAcceptanceAcceptor.phaseStarted(phaseScope);
4334
stuckCriterion.reset(phaseScope);
4435
if (stuckCriterion instanceof UnimprovedMoveCountStuckCriterion<Solution_> stepCountStuckCriterion) {
@@ -50,23 +41,18 @@ public void phaseStarted(LocalSearchPhaseScope<Solution_> phaseScope) {
5041
public void phaseEnded(LocalSearchPhaseScope<Solution_> phaseScope) {
5142
super.phaseEnded(phaseScope);
5243
this.lateAcceptanceAcceptor.phaseEnded(phaseScope);
53-
5444
}
5545

5646
@Override
5747
public void stepStarted(LocalSearchStepScope<Solution_> stepScope) {
5848
super.stepStarted(stepScope);
5949
lateAcceptanceAcceptor.stepStarted(stepScope);
60-
currentBestScore = stepScope.getPhaseScope().getBestScore();
6150
}
6251

6352
@Override
6453
public void stepEnded(LocalSearchStepScope<Solution_> stepScope) {
6554
super.stepEnded(stepScope);
6655
lateAcceptanceAcceptor.stepEnded(stepScope);
67-
if (((Score) stepScope.getScore()).compareTo(currentBestScore) > 0) {
68-
perturbationCount = 1;
69-
}
7056
}
7157

7258
@Override
@@ -78,34 +64,19 @@ public boolean rejectRestartEvent() {
7864
@Override
7965
public void restart(LocalSearchStepScope<Solution_> stepScope) {
8066
lateIndex = (lateIndex + 1) % LATE_ELEMENTS_SIZE.length;
81-
var lastCompletedStepScore = stepScope.getPhaseScope().getLastCompletedStepScope().getScore();
8267
var phaseScope = stepScope.getPhaseScope();
8368
var decider = phaseScope.getDecider();
8469
// Restore the current best solution
85-
decider.restoreCurrentBestSolution(phaseScope);
86-
// Apply the perturbation with perturbation move selector
87-
for (int i = 0; i < perturbationCount; i++) {
88-
perturbationMoveSelector.phaseStarted(phaseScope);
89-
var iterator = perturbationMoveSelector.iterator();
90-
if (iterator.hasNext()) {
91-
decider.doMoveOnly(phaseScope, iterator.next());
92-
}
93-
}
94-
// Reset cached entity list
95-
decider.moveSelectorPhaseStarted(phaseScope);
70+
decider.restoreCurrentBestSolution(stepScope);
9671
logger.info(
97-
"Restart event triggered, step count ({}), perturbation count ({}), late elements size ({}), max rejections ({}), best score ({}), last completed score ({}), new perturbation score ({}),",
98-
stepScope.getStepIndex(), perturbationCount, LATE_ELEMENTS_SIZE[lateIndex],
99-
LATE_ELEMENTS_MAX_REJECTIONS[lateIndex], stepScope.getPhaseScope().getBestScore(),
100-
lastCompletedStepScore,
101-
stepScope.getPhaseScope().getLastCompletedStepScope().getScore());
102-
lateAcceptanceAcceptor.resetLateElementsScore(LATE_ELEMENTS_SIZE[lateIndex],
103-
(Score) stepScope.getPhaseScope().getLastCompletedStepScope().getScore());
72+
"Restart event triggered, step count ({}), late elements size ({}), max rejections ({}), best score ({}), new perturbation score ({}),",
73+
stepScope.getStepIndex(), LATE_ELEMENTS_SIZE[lateIndex], LATE_ELEMENTS_MAX_REJECTIONS[lateIndex],
74+
stepScope.getPhaseScope().getBestScore(), initialScore);
75+
lateAcceptanceAcceptor.resetLateElementsScore(LATE_ELEMENTS_SIZE[lateIndex], (Score) initialScore);
10476
if (stuckCriterion instanceof UnimprovedMoveCountStuckCriterion<Solution_> stepCountStuckCriterion) {
10577
stepCountStuckCriterion.setMaxRejected(LATE_ELEMENTS_MAX_REJECTIONS[lateIndex]);
10678
}
10779
stuckCriterion.reset(stepScope.getPhaseScope());
108-
perturbationCount = (perturbationCount % maxPerturbationCount) + 1;
10980
}
11081

11182
@Override
Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
package ai.timefold.solver.core.impl.localsearch.decider.restart;
22

3-
import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector;
43
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.Acceptor;
54
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.RestartableAcceptor;
6-
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.iteratedlocalsearch.IteratedLocalSearchAcceptor;
75
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope;
86
import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope;
97
import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope;
108
import ai.timefold.solver.core.impl.solver.scope.SolverScope;
119

1210
public final class AcceptorRestartStrategy<Solution_> implements RestartStrategy<Solution_> {
1311

14-
private final MoveSelector<Solution_> perturbationMoveSelector;
1512
private final Acceptor<Solution_> acceptor;
1613

17-
public AcceptorRestartStrategy(MoveSelector<Solution_> perturbationMoveSelector, Acceptor<Solution_> acceptor) {
18-
this.perturbationMoveSelector = perturbationMoveSelector;
14+
public AcceptorRestartStrategy(Acceptor<Solution_> acceptor) {
1915
this.acceptor = acceptor;
2016
}
2117

@@ -30,34 +26,31 @@ public void applyRestart(AbstractStepScope<Solution_> stepScope) {
3026

3127
@Override
3228
public void stepStarted(AbstractStepScope<Solution_> stepScope) {
33-
perturbationMoveSelector.stepStarted(stepScope);
29+
// Do nothing
3430
}
3531

3632
@Override
3733
public void stepEnded(AbstractStepScope<Solution_> stepScope) {
38-
perturbationMoveSelector.stepEnded(stepScope);
34+
// Do nothing
3935
}
4036

4137
@Override
4238
public void phaseStarted(AbstractPhaseScope<Solution_> phaseScope) {
43-
perturbationMoveSelector.phaseStarted(phaseScope);
39+
// Do nothing
4440
}
4541

4642
@Override
4743
public void phaseEnded(AbstractPhaseScope<Solution_> phaseScope) {
48-
perturbationMoveSelector.phaseEnded(phaseScope);
44+
// Do nothing
4945
}
5046

5147
@Override
5248
public void solvingStarted(SolverScope<Solution_> solverScope) {
53-
perturbationMoveSelector.solvingStarted(solverScope);
54-
if (acceptor instanceof IteratedLocalSearchAcceptor<Solution_> iteratedLocalSearchAcceptor) {
55-
iteratedLocalSearchAcceptor.setPerturbationMoveSelector(perturbationMoveSelector);
56-
}
49+
// Do nothing
5750
}
5851

5952
@Override
6053
public void solvingEnded(SolverScope<Solution_> solverScope) {
61-
perturbationMoveSelector.solvingEnded(solverScope);
54+
// Do nothing
6255
}
6356
}

core/src/main/resources/solver.xsd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1923,7 +1923,7 @@
19231923

19241924
<xs:enumeration value="DIVERSIFIED_LATE_ACCEPTANCE"/>
19251925

1926-
<xs:enumeration value="ITERATED_LOCAL_SEARCH"/>
1926+
<xs:enumeration value="ADAPTIVE_LATE_ACCEPTANCE"/>
19271927

19281928
<xs:enumeration value="GREAT_DELUGE"/>
19291929

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

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

99
import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore;
10-
import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector;
1110
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.AbstractAcceptorTest;
1211
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.StuckCriterion;
1312
import ai.timefold.solver.core.impl.localsearch.decider.restart.AcceptorRestartStrategy;
@@ -390,9 +389,8 @@ void restart() {
390389
@Test
391390
void ensureDiversity() {
392391
var stuckCriterion = mock(StuckCriterion.class);
393-
var moveSelector = mock(MoveSelector.class);
394392
var acceptor = new LateAcceptanceAcceptor<>(true, stuckCriterion);
395-
var restartStrategy = new AcceptorRestartStrategy(moveSelector, acceptor);
393+
var restartStrategy = new AcceptorRestartStrategy(acceptor);
396394
acceptor.setLateAcceptanceSize(5);
397395
var solverScope = new SolverScope<>();
398396
var phaseScope = new LocalSearchPhaseScope<>(solverScope, 0);
@@ -420,9 +418,8 @@ void ensureDiversity() {
420418
@Test
421419
void reconfigureAfterImprovement() {
422420
var stuckCriterion = mock(StuckCriterion.class);
423-
var moveSelector = mock(MoveSelector.class);
424421
var acceptor = new LateAcceptanceAcceptor<>(true, stuckCriterion);
425-
var restartStrategy = new AcceptorRestartStrategy(moveSelector, acceptor);
422+
var restartStrategy = new AcceptorRestartStrategy(acceptor);
426423
acceptor.setLateAcceptanceSize(5);
427424
var solverScope = new SolverScope<>();
428425
var phaseScope = new LocalSearchPhaseScope<>(solverScope, 0);

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import static org.mockito.Mockito.verify;
77
import static org.mockito.Mockito.when;
88

9-
import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector;
109
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance.LateAcceptanceAcceptor;
1110
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope;
1211
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope;
@@ -18,12 +17,11 @@ class AcceptorRestartStrategyTest {
1817
@Test
1918
void restart() {
2019
// Restore the best solution
21-
var moveSelector = mock(MoveSelector.class);
2220
var phaseScope = mock(LocalSearchPhaseScope.class);
2321
var stepScope = mock(LocalSearchStepScope.class);
2422
when(stepScope.getPhaseScope()).thenReturn(phaseScope);
2523
var acceptor = mock(LateAcceptanceAcceptor.class);
26-
var strategy = new AcceptorRestartStrategy<>(moveSelector, acceptor);
24+
var strategy = new AcceptorRestartStrategy<>(acceptor);
2725

2826
// Call acceptor restart logic
2927
strategy.applyRestart(stepScope);

0 commit comments

Comments
 (0)