Skip to content

Commit 41c348f

Browse files
committed
chore: improve restart logic
1 parent 746e1ed commit 41c348f

File tree

7 files changed

+53
-197
lines changed

7 files changed

+53
-197
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance.LateAcceptanceAcceptor;
1818
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.simulatedannealing.SimulatedAnnealingAcceptor;
1919
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stepcountinghillclimbing.StepCountingHillClimbingAcceptor;
20+
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.DiminishedReturnsStuckCriterion;
2021
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.StuckCriterion;
21-
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.UnimprovedTimeStuckCriterion;
2222
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.tabu.EntityTabuAcceptor;
2323
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.tabu.MoveTabuAcceptor;
2424
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.tabu.ValueTabuAcceptor;
@@ -222,7 +222,7 @@ private Optional<MoveTabuAcceptor<Solution_>> buildMoveTabuAcceptor(HeuristicCon
222222
if (acceptorTypeListsContainsAcceptorType(AcceptorType.LATE_ACCEPTANCE)
223223
|| (!acceptorTypeListsContainsAcceptorType(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE)
224224
&& acceptorConfig.getLateAcceptanceSize() != null)) {
225-
StuckCriterion<Solution_> strategy = new UnimprovedTimeStuckCriterion<>();
225+
StuckCriterion<Solution_> strategy = new DiminishedReturnsStuckCriterion<>();
226226
var acceptor = new LateAcceptanceAcceptor<>(strategy);
227227
acceptor.setLateAcceptanceSize(Objects.requireNonNullElse(acceptorConfig.getLateAcceptanceSize(), 400));
228228
return Optional.of(acceptor);

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,29 @@ public void stepEnded(LocalSearchStepScope<Solution_> stepScope) {
5252
@Override
5353
public boolean isAccepted(LocalSearchMoveScope<Solution_> moveScope) {
5454
if (stuckCriterion.isSolverStuck(moveScope)) {
55-
moveScope.getStepScope().getPhaseScope().setSolverStuck(true);
56-
restartTriggered = true;
57-
return true;
55+
if (rejectRestartEvent()) {
56+
// We need to reset the criterion,
57+
// or it will trigger the restart event in the next evaluation
58+
stuckCriterion.reset(moveScope.getStepScope());
59+
} else {
60+
moveScope.getStepScope().getPhaseScope().setSolverStuck(true);
61+
restartTriggered = true;
62+
return true;
63+
}
5864
}
5965
return accept(moveScope);
6066
}
6167

6268
protected abstract boolean accept(LocalSearchMoveScope<Solution_> moveScope);
6369

70+
/**
71+
* The stuck criterion may trigger a restart event, but the acceptor might choose to delay it.
72+
* This method rechecks the restart event condition on the acceptor side.
73+
*/
74+
public abstract boolean rejectRestartEvent();
75+
76+
/**
77+
* Run the restart event logic on the acceptor side.
78+
*/
6479
public abstract void restart(LocalSearchStepScope<Solution_> stepScope);
6580
}

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

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -125,33 +125,35 @@ public void stepEnded(LocalSearchStepScope<Solution_> stepScope) {
125125
}
126126

127127
@Override
128-
public void restart(LocalSearchStepScope<Solution_> stepScope) {
129-
countRestartWithoutImprovement++;
128+
public boolean rejectRestartEvent() {
130129
var distinctElements = Arrays.stream(previousScores).distinct().count();
131130
var diversity = distinctElements == 1 ? 0 : distinctElements / (double) lateAcceptanceSize;
132-
if (coefficient == 0 && (diversity > MIN_DIVERSITY_RATIO || bestScoreQueue.size() == 1)) {
133-
// We prefer not to restart until the first event has passed (about 10:30 minutes).
134-
// We have observed that this approach works better for more complex datasets.
135-
// However, when the diversity is zero, it indicates that the LA may be stuck in a local minimum,
136-
// and in such cases, we should restart before the first event.
137-
// Additionally, when there is only one best score,
138-
// it does not make sense to restart as nothing would change
139-
// and the proposed approach requires some diversity to reseed the scores.
131+
// We prefer not to restart until the first event has passed (about 10:30 minutes).
132+
// We have observed that this approach works better for more complex datasets.
133+
// However, when the diversity is zero, it indicates that the LA may be stuck in a local minimum,
134+
// and in such cases, we should restart before the first event.
135+
// Additionally, when there is only one best score,
136+
// it does not make sense to restart as nothing would change
137+
// and the proposed approach requires some diversity to reseed the scores.
138+
var reject = coefficient == 0 && (diversity > MIN_DIVERSITY_RATIO || bestScoreQueue.size() == 1);
139+
if (reject) {
140140
logger.info(
141141
"Restart event delayed. Diversity ({}), Count best scores ({}), Distinct Elements ({}), Restart without Improvement ({})",
142-
bestScoreQueue.size(), diversity, distinctElements, countRestartWithoutImprovement);
143-
return;
142+
diversity, bestScoreQueue.size(), distinctElements, countRestartWithoutImprovement);
144143
}
144+
return reject;
145+
}
146+
147+
@Override
148+
public void restart(LocalSearchStepScope<Solution_> stepScope) {
149+
countRestartWithoutImprovement++;
145150
coefficient++;
146151
var newLateAcceptanceSize = defaultLateAcceptanceSize * coefficient * SCALING_FACTOR;
147152
if (logger.isInfoEnabled()) {
148153
if (lateAcceptanceSize == newLateAcceptanceSize) {
149-
logger.info("Keeping the lateAcceptanceSize as {}. Diversity ({}), Distinct Elements ({})", lateAcceptanceSize,
150-
diversity, distinctElements);
154+
logger.info("Keeping the lateAcceptanceSize as {}.", lateAcceptanceSize);
151155
} else {
152-
logger.info(
153-
"Changing the lateAcceptanceSize from {} to {}. Diversity ({}), Distinct Elements ({})",
154-
lateAcceptanceSize, newLateAcceptanceSize, diversity, distinctElements);
156+
logger.info("Changing the lateAcceptanceSize from {} to {}.", lateAcceptanceSize, newLateAcceptanceSize);
155157
}
156158
}
157159
rebuildLateElementsList(newLateAcceptanceSize);

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,21 @@ boolean evaluateCriterion(LocalSearchMoveScope<Solution_> moveScope) {
3838
return triggered;
3939
}
4040

41+
@Override
42+
public void reset(LocalSearchStepScope<Solution_> stepScope) {
43+
// We need to recreate the termination criterion as the time window has changed
44+
// After the first restart we use a higher time window
45+
setScalingFactor(REGULAR_TIME_WINDOW_MILLIS);
46+
diminishedReturnsCriterion = new DiminishedReturnsTermination<>(nextRestart, MINIMAL_IMPROVEMENT);
47+
diminishedReturnsCriterion.start(System.nanoTime(), stepScope.getPhaseScope().getBestScore());
48+
triggered = false;
49+
}
50+
4151
@Override
4252
public void stepStarted(LocalSearchStepScope<Solution_> stepScope) {
4353
currentBestScore = stepScope.getPhaseScope().getBestScore();
4454
if (triggered) {
45-
// We need to recreate the termination criterion as the time window has changed
46-
// After the first restart we use a higher time window
47-
setScalingFactor(REGULAR_TIME_WINDOW_MILLIS);
48-
diminishedReturnsCriterion = new DiminishedReturnsTermination<>(nextRestart, MINIMAL_IMPROVEMENT);
49-
diminishedReturnsCriterion.start(System.nanoTime(), stepScope.getPhaseScope().getBestScore());
50-
triggered = false;
55+
reset(stepScope);
5156
}
5257
}
5358

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import ai.timefold.solver.core.api.solver.Solver;
44
import ai.timefold.solver.core.impl.localsearch.event.LocalSearchPhaseLifecycleListener;
55
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope;
6+
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope;
67

78
/**
89
* Allow defining strategies that identify when the {@link Solver solver} is stuck.
@@ -15,7 +16,8 @@ public interface StuckCriterion<Solution_> extends LocalSearchPhaseLifecycleList
1516
* Main logic that applies a specific metric to determine if a solver is stuck in a local optimum.
1617
*
1718
* @param moveScope cannot be null
18-
* @return
1919
*/
2020
boolean isSolverStuck(LocalSearchMoveScope<Solution_> moveScope);
21+
22+
void reset(LocalSearchStepScope<Solution_> stepScope);
2123
}

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

Lines changed: 0 additions & 90 deletions
This file was deleted.

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

Lines changed: 0 additions & 78 deletions
This file was deleted.

0 commit comments

Comments
 (0)