Skip to content

Commit 2bef09e

Browse files
committed
chore: improve ILS
1 parent 7b60e22 commit 2bef09e

File tree

12 files changed

+124
-36
lines changed

12 files changed

+124
-36
lines changed

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

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,16 @@
22

33
import java.util.Collections;
44
import java.util.Objects;
5+
import java.util.Optional;
56

7+
import ai.timefold.solver.core.config.AbstractConfig;
68
import ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType;
79
import ai.timefold.solver.core.config.heuristic.selector.common.SelectionOrder;
8-
import ai.timefold.solver.core.config.heuristic.selector.entity.pillar.PillarSelectorConfig;
910
import ai.timefold.solver.core.config.heuristic.selector.list.SubListSelectorConfig;
1011
import ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig;
1112
import ai.timefold.solver.core.config.heuristic.selector.move.NearbyAutoConfigurationEnabled;
1213
import ai.timefold.solver.core.config.heuristic.selector.move.composite.UnionMoveSelectorConfig;
1314
import ai.timefold.solver.core.config.heuristic.selector.move.generic.ChangeMoveSelectorConfig;
14-
import ai.timefold.solver.core.config.heuristic.selector.move.generic.PillarChangeMoveSelectorConfig;
15-
import ai.timefold.solver.core.config.heuristic.selector.move.generic.PillarSwapMoveSelectorConfig;
1615
import ai.timefold.solver.core.config.heuristic.selector.move.generic.RuinRecreateMoveSelectorConfig;
1716
import ai.timefold.solver.core.config.heuristic.selector.move.generic.SwapMoveSelectorConfig;
1817
import ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.SubChainChangeMoveSelectorConfig;
@@ -202,11 +201,24 @@ protected MoveSelector<Solution_> buildMoveSelector(HeuristicConfigPolicy<Soluti
202201

203202
@SuppressWarnings("rawtypes")
204203
protected MoveSelector<Solution_> buildPerturbationMoveSelector(HeuristicConfigPolicy<Solution_> configPolicy) {
204+
MoveSelector<Solution_> moveSelector;
205205
var defaultCacheType = SelectionCacheType.JUST_IN_TIME;
206206
SelectionOrder defaultSelectionOrder;
207207
defaultSelectionOrder = SelectionOrder.RANDOM;
208-
return new UnionMoveSelectorFactory<Solution_>(determinePerturbationMoveSelectorConfig(configPolicy))
209-
.buildMoveSelector(configPolicy, defaultCacheType, defaultSelectionOrder, true);
208+
var moveSelectorConfig =
209+
Optional.ofNullable(phaseConfig.getMoveSelectorConfig()).map(AbstractConfig::copyConfig).orElse(null);
210+
if (moveSelectorConfig == null) {
211+
moveSelector = new UnionMoveSelectorFactory<Solution_>(determinePerturbationMoveSelectorConfig(configPolicy))
212+
.buildMoveSelector(configPolicy, defaultCacheType, defaultSelectionOrder, true);
213+
} else {
214+
if (moveSelectorConfig instanceof UnionMoveSelectorConfig unionMoveSelectorConfig) {
215+
unionMoveSelectorConfig.getMoveSelectorList().forEach(m -> m.setFixedProbabilityWeight(null));
216+
}
217+
AbstractMoveSelectorFactory<Solution_, ?> moveSelectorFactory =
218+
MoveSelectorFactory.create((MoveSelectorConfig<?>) moveSelectorConfig);
219+
moveSelector = moveSelectorFactory.buildMoveSelector(configPolicy, defaultCacheType, defaultSelectionOrder, true);
220+
}
221+
return moveSelector;
210222
}
211223

212224
private UnionMoveSelectorConfig determineDefaultMoveSelectorConfig(HeuristicConfigPolicy<Solution_> configPolicy) {
@@ -288,10 +300,6 @@ private UnionMoveSelectorConfig determinePerturbationMoveSelectorConfig(Heuristi
288300
} else {
289301
return new UnionMoveSelectorConfig()
290302
.withMoveSelectors(new ChangeMoveSelectorConfig(), new SwapMoveSelectorConfig(),
291-
new PillarChangeMoveSelectorConfig().withPillarSelectorConfig(
292-
new PillarSelectorConfig().withMinimumSubPillarSize(2).withMaximumSubPillarSize(5)),
293-
new PillarSwapMoveSelectorConfig().withPillarSelectorConfig(
294-
new PillarSelectorConfig().withMinimumSubPillarSize(2).withMaximumSubPillarSize(5)),
295303
new RuinRecreateMoveSelectorConfig().withMinimumRuinedCount(1).withMaximumRuinedCount(5));
296304
}
297305
} else {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ public void decideNextStep(LocalSearchStepScope<Solution_> stepScope) {
110110
LocalSearchMoveScope<Solution_> moveScope = new LocalSearchMoveScope<>(stepScope, moveIndex, adaptedMove);
111111
moveIndex++;
112112
doMove(moveScope);
113-
if (forager.isQuitEarly()) {
113+
if (forager.isQuitEarly() || restartStrategy.isSolverStuck(moveScope)) {
114114
break;
115115
}
116116
stepScope.getPhaseScope().getSolverScope().checkYielding();

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stepcountinghillclimbing.StepCountingHillClimbingAcceptor;
2121
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.DiminishedReturnsStuckCriterion;
2222
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.StuckCriterion;
23-
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.UnimprovedStepCountStuckCriterion;
23+
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.UnimprovedMoveCountStuckCriterion;
2424
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.tabu.EntityTabuAcceptor;
2525
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.tabu.MoveTabuAcceptor;
2626
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.tabu.ValueTabuAcceptor;
@@ -247,8 +247,8 @@ private Optional<MoveTabuAcceptor<Solution_>> buildMoveTabuAcceptor(HeuristicCon
247247
private Optional<IteratedLocalSearchAcceptor<Solution_>>
248248
buildIteratedLocalSearchAcceptor() {
249249
if (acceptorTypeListsContainsAcceptorType(AcceptorType.ITERATED_LOCAL_SEARCH)) {
250-
StuckCriterion<Solution_> strategy = new UnimprovedStepCountStuckCriterion<>();
251-
var acceptor = new IteratedLocalSearchAcceptor<>(5, strategy);
250+
StuckCriterion<Solution_> strategy = new UnimprovedMoveCountStuckCriterion<>();
251+
var acceptor = new IteratedLocalSearchAcceptor<>(3, strategy);
252252
return Optional.of(acceptor);
253253
}
254254
return Optional.empty();

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

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

33
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.StuckCriterion;
4+
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope;
45
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope;
56
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope;
67
import ai.timefold.solver.core.impl.solver.scope.SolverScope;
@@ -71,6 +72,18 @@ public void stepEnded(LocalSearchStepScope<Solution_> stepScope) {
7172
}
7273
}
7374

75+
@Override
76+
public boolean isAccepted(LocalSearchMoveScope<Solution_> moveScope) {
77+
if (enableRestart && stuckCriterion.isSolverStuck(moveScope)) {
78+
moveScope.getStepScope().getPhaseScope().setSolverStuck(true);
79+
restartTriggered = true;
80+
return true;
81+
}
82+
return accept(moveScope);
83+
}
84+
85+
public abstract boolean accept(LocalSearchMoveScope<Solution_> moveScope);
86+
7487
/**
7588
* The stuck criterion may trigger a restart event, but the acceptor might choose to delay it.
7689
* This method rechecks the restart event condition on the acceptor side.

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,23 @@
55
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.RestartableAcceptor;
66
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance.LateAcceptanceAcceptor;
77
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.StuckCriterion;
8-
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.UnimprovedStepCountStuckCriterion;
8+
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.UnimprovedMoveCountStuckCriterion;
99
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope;
1010
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope;
1111
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope;
1212

1313
public class IteratedLocalSearchAcceptor<Solution_> extends RestartableAcceptor<Solution_> {
1414

1515
private static final int[] LATE_ELEMENTS_SIZE =
16-
new int[] { 5_000, 12_500, 25_000, 25_000 };
16+
new int[] { 12_500, 25_000, 25_000, 50_000, 50_000 };
1717
private static final int[] LATE_ELEMENTS_MAX_REJECTIONS =
18-
new int[] { 5_000, 12_500, 25_000, 50_000 };
18+
new int[] { 12_500, 25_000, 50_000, 50_000, 75_000 };
1919
private final LateAcceptanceAcceptor<Solution_> lateAcceptanceAcceptor;
2020
private MoveSelector<Solution_> perturbationMoveSelector;
2121
private final int maxPerturbationCount;
2222
private int lateIndex;
2323
private int perturbationCount;
24+
private Score<?> currentBestScore;
2425

2526
public IteratedLocalSearchAcceptor(int maxPerturbationCount, StuckCriterion<Solution_> stuckCriterion) {
2627
super(true, stuckCriterion);
@@ -40,7 +41,7 @@ public void phaseStarted(LocalSearchPhaseScope<Solution_> phaseScope) {
4041
lateAcceptanceAcceptor.setLateAcceptanceSize(LATE_ELEMENTS_SIZE[perturbationCount]);
4142
lateAcceptanceAcceptor.phaseStarted(phaseScope);
4243
stuckCriterion.reset(phaseScope);
43-
if (stuckCriterion instanceof UnimprovedStepCountStuckCriterion<Solution_> stepCountStuckCriterion) {
44+
if (stuckCriterion instanceof UnimprovedMoveCountStuckCriterion<Solution_> stepCountStuckCriterion) {
4445
stepCountStuckCriterion.setMaxRejected(LATE_ELEMENTS_MAX_REJECTIONS[lateIndex]);
4546
}
4647
}
@@ -56,12 +57,16 @@ public void phaseEnded(LocalSearchPhaseScope<Solution_> phaseScope) {
5657
public void stepStarted(LocalSearchStepScope<Solution_> stepScope) {
5758
super.stepStarted(stepScope);
5859
lateAcceptanceAcceptor.stepStarted(stepScope);
60+
currentBestScore = stepScope.getPhaseScope().getBestScore();
5961
}
6062

6163
@Override
6264
public void stepEnded(LocalSearchStepScope<Solution_> stepScope) {
6365
super.stepEnded(stepScope);
6466
lateAcceptanceAcceptor.stepEnded(stepScope);
67+
if (((Score) stepScope.getScore()).compareTo(currentBestScore) > 0) {
68+
perturbationCount = 1;
69+
}
6570
}
6671

6772
@Override
@@ -96,15 +101,15 @@ public void restart(LocalSearchStepScope<Solution_> stepScope) {
96101
stepScope.getPhaseScope().getLastCompletedStepScope().getScore());
97102
lateAcceptanceAcceptor.resetLateElementsScore(LATE_ELEMENTS_SIZE[lateIndex],
98103
(Score) stepScope.getPhaseScope().getLastCompletedStepScope().getScore());
99-
if (stuckCriterion instanceof UnimprovedStepCountStuckCriterion<Solution_> stepCountStuckCriterion) {
104+
if (stuckCriterion instanceof UnimprovedMoveCountStuckCriterion<Solution_> stepCountStuckCriterion) {
100105
stepCountStuckCriterion.setMaxRejected(LATE_ELEMENTS_MAX_REJECTIONS[lateIndex]);
101106
}
102107
stuckCriterion.reset(stepScope.getPhaseScope());
103108
perturbationCount = (perturbationCount % maxPerturbationCount) + 1;
104109
}
105110

106111
@Override
107-
public boolean isAccepted(LocalSearchMoveScope<Solution_> moveScope) {
112+
public boolean accept(LocalSearchMoveScope<Solution_> moveScope) {
108113
return lateAcceptanceAcceptor.isAccepted(moveScope);
109114
}
110115
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ private void validate() {
8787

8888
@Override
8989
@SuppressWarnings("unchecked")
90-
public boolean isAccepted(LocalSearchMoveScope<Solution_> moveScope) {
90+
public boolean accept(LocalSearchMoveScope<Solution_> moveScope) {
9191
var moveScore = moveScope.getScore();
9292
var lateScore = previousScores[lateScoreIndex];
9393
var lastStepScore = moveScope.getStepScope().getPhaseScope().getLastCompletedStepScope().getScore();

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

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

3+
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope;
34
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope;
45
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope;
56
import ai.timefold.solver.core.impl.solver.scope.SolverScope;
@@ -59,6 +60,21 @@ public void setScalingFactor(double scalingFactor) {
5960
nextRestart = calculateNextRestart();
6061
}
6162

63+
@Override
64+
public boolean isSolverStuck(LocalSearchMoveScope<Solution_> moveScope) {
65+
var triggered = evaluateCriterion(moveScope);
66+
if (triggered) {
67+
logger.info(
68+
"Restart triggered with geometric factor ({}), scaling factor of ({}), best score ({})",
69+
currentGeometricGrowFactor, scalingFactor,
70+
moveScope.getStepScope().getPhaseScope().getBestScore());
71+
currentGeometricGrowFactor = Math.ceil(currentGeometricGrowFactor * GEOMETRIC_FACTOR);
72+
nextRestart = calculateNextRestart();
73+
return true;
74+
}
75+
return false;
76+
}
77+
6278
@Override
6379
public boolean isSolverStuck(LocalSearchStepScope<Solution_> stepScope) {
6480
var triggered = evaluateCriterion(stepScope);
@@ -78,6 +94,8 @@ private long calculateNextRestart() {
7894
return (long) Math.ceil(currentGeometricGrowFactor * scalingFactor);
7995
}
8096

97+
abstract boolean evaluateCriterion(LocalSearchMoveScope<Solution_> moveScope);
98+
8199
abstract boolean evaluateCriterion(LocalSearchStepScope<Solution_> stepScope);
82100

83101
}

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

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

33
import ai.timefold.solver.core.api.score.Score;
4+
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope;
45
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope;
56
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope;
67
import ai.timefold.solver.core.impl.solver.termination.DiminishedReturnsTermination;
@@ -23,6 +24,12 @@ protected DiminishedReturnsStuckCriterion(DiminishedReturnsTermination<Solution_
2324
this.diminishedReturnsCriterion = diminishedReturnsCriterion;
2425
}
2526

27+
@Override
28+
boolean evaluateCriterion(LocalSearchMoveScope<Solution_> moveScope) {
29+
// Only evaluated per step
30+
return false;
31+
}
32+
2633
@Override
2734
@SuppressWarnings("unchecked")
2835
boolean evaluateCriterion(LocalSearchStepScope<Solution_> stepScope) {

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import ai.timefold.solver.core.api.solver.Solver;
44
import ai.timefold.solver.core.impl.localsearch.event.LocalSearchPhaseLifecycleListener;
5+
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope;
56
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope;
67
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope;
78

@@ -19,5 +20,13 @@ public interface StuckCriterion<Solution_> extends LocalSearchPhaseLifecycleList
1920
*/
2021
boolean isSolverStuck(LocalSearchStepScope<Solution_> stepScope);
2122

23+
/**
24+
* Same as {{@link #isSolverStuck(LocalSearchStepScope)}, but it is called for every move}
25+
*
26+
* @param moveScope cannot be null
27+
* @return
28+
*/
29+
boolean isSolverStuck(LocalSearchMoveScope<Solution_> moveScope);
30+
2231
void reset(LocalSearchPhaseScope<Solution_> phaseScope);
2332
}

core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/stuckcriterion/UnimprovedStepCountStuckCriterion.java renamed to core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/stuckcriterion/UnimprovedMoveCountStuckCriterion.java

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

33
import ai.timefold.solver.core.api.score.Score;
4+
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope;
45
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope;
56
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope;
67
import ai.timefold.solver.core.impl.solver.scope.SolverScope;
78

8-
public class UnimprovedStepCountStuckCriterion<Solution_> implements StuckCriterion<Solution_> {
9+
public class UnimprovedMoveCountStuckCriterion<Solution_> implements StuckCriterion<Solution_> {
910

1011
private int countRejected;
12+
private Score<?> initialBestScore;
1113
private Score<?> lastCompletedScore;
1214
private int maxRejected;
15+
private boolean waitForFirstBestScore;
1316

14-
public UnimprovedStepCountStuckCriterion() {
17+
public UnimprovedMoveCountStuckCriterion() {
1518
}
1619

1720
public void setMaxRejected(int maxRejected) {
1821
this.maxRejected = maxRejected;
1922
}
2023

2124
@Override
22-
public boolean isSolverStuck(LocalSearchStepScope<Solution_> stepScope) {
23-
if (((Score) stepScope.getScore()).compareTo(lastCompletedScore) <= 0) {
24-
countRejected++;
25+
public boolean isSolverStuck(LocalSearchMoveScope<Solution_> moveScope) {
26+
if (waitForFirstBestScore) {
27+
return false;
28+
}
29+
if (moveScope.getScore().compareTo(lastCompletedScore) > 0) {
30+
countRejected = 0;
2531
}
32+
countRejected++;
2633
return countRejected > maxRejected;
2734
}
2835

36+
@Override
37+
public boolean isSolverStuck(LocalSearchStepScope<Solution_> stepScope) {
38+
// Only evaluated per move
39+
return false;
40+
}
41+
2942
@Override
3043
public void reset(LocalSearchPhaseScope<Solution_> phaseScope) {
3144
this.countRejected = 0;
3245
}
3346

3447
@Override
3548
public void phaseStarted(LocalSearchPhaseScope<Solution_> phaseScope) {
36-
this.countRejected = 0;
49+
waitForFirstBestScore = true;
50+
countRejected = 0;
51+
initialBestScore = phaseScope.getBestScore();
3752
}
3853

3954
@Override
@@ -43,8 +58,9 @@ public void stepStarted(LocalSearchStepScope<Solution_> stepScope) {
4358

4459
@Override
4560
public void stepEnded(LocalSearchStepScope<Solution_> stepScope) {
46-
if (((Score) stepScope.getScore()).compareTo(lastCompletedScore) > 0) {
47-
reset(stepScope.getPhaseScope());
61+
// Do nothing
62+
if (waitForFirstBestScore && ((Score) stepScope.getScore()).compareTo(initialBestScore) > 0) {
63+
waitForFirstBestScore = false;
4864
}
4965
}
5066

0 commit comments

Comments
 (0)