Skip to content

Commit dbec0f5

Browse files
committed
feat: relative stuck criterion definition
1 parent 99f72c2 commit dbec0f5

File tree

4 files changed

+26
-7
lines changed

4 files changed

+26
-7
lines changed

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,21 @@ public abstract class AbstractGeometricStuckCriterion<Solution_> implements Stuc
2424
private static final double GEOMETRIC_FACTOR = 1.4; // Value extracted from the cited paper
2525
private static final long GRACE_PERIOD_MILLIS = 30_000; // 30s by default
2626

27-
private final double scalingFactor;
2827
private final Clock clock;
28+
private double scalingFactor;
2929
private boolean gracePeriodFinished;
3030
private Instant gracePeriodEnd;
3131
protected long nextRestart;
3232
private double currentGeometricGrowFactor;
3333

34-
protected AbstractGeometricStuckCriterion(Clock clock, double scalingFactor) {
34+
protected AbstractGeometricStuckCriterion(Clock clock) {
3535
this.clock = clock;
36+
this.scalingFactor = -1;
37+
}
38+
39+
protected void setScalingFactor(double scalingFactor) {
3640
this.scalingFactor = scalingFactor;
41+
this.nextRestart = calculateNextRestart();
3742
}
3843

3944
@Override
@@ -67,6 +72,9 @@ public boolean isSolverStuck(LocalSearchMoveScope<Solution_> moveScope) {
6772
if (isGracePeriodFinished()) {
6873
var triggered = evaluateCriterion(moveScope);
6974
if (triggered) {
75+
if (scalingFactor == -1) {
76+
throw new IllegalStateException("The scaling factor is not defined for this criterion.");
77+
}
7078
logger.trace(
7179
"Restart triggered with geometric factor {}, scaling factor of {}, best score ({}), move count ({})",
7280
currentGeometricGrowFactor,

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
*/
1616
public class UnimprovedMoveCountStuckCriterion<Solution_> extends AbstractGeometricStuckCriterion<Solution_> {
1717

18-
// Multiplier defined through experiments
19-
protected static final long UNIMPROVED_MOVE_COUNT_MULTIPLIER = 300_000;
18+
// The multiplier will lead to an unimproved move count near a 10-second window without any improvements.
19+
// In this manner, the first restart will occur after approximately 10 seconds without any improvement,
20+
// then after 20 seconds, and so on.
21+
protected static final int UNIMPROVED_MOVE_COUNT_MULTIPLIER = 10;
2022
// Last checkpoint of a solution improvement or the restart process
2123
protected long lastCheckpoint;
2224
private Score<?> currentBestScore;
@@ -26,7 +28,7 @@ public UnimprovedMoveCountStuckCriterion() {
2628
}
2729

2830
protected UnimprovedMoveCountStuckCriterion(Clock clock) {
29-
super(clock, UNIMPROVED_MOVE_COUNT_MULTIPLIER);
31+
super(clock);
3032
}
3133

3234
@Override
@@ -54,6 +56,12 @@ public boolean evaluateCriterion(LocalSearchMoveScope<Solution_> moveScope) {
5456
var currentMoveCount = moveScope.getStepScope().getPhaseScope().getSolverScope().getMoveEvaluationCount();
5557
if (lastCheckpoint == 0) {
5658
lastCheckpoint = currentMoveCount;
59+
// Grace period is finished
60+
// Now we use the current move evaluation speed to define the scaling factor
61+
var scalingFactor = (double) (moveScope.getStepScope().getPhaseScope().getSolverScope().getMoveEvaluationSpeed()
62+
* UNIMPROVED_MOVE_COUNT_MULTIPLIER);
63+
logger.trace("Scaling factor set to {}.", scalingFactor);
64+
setScalingFactor(scalingFactor);
5765
return false;
5866
}
5967
if (currentMoveCount - lastCheckpoint >= nextRestart) {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ void isSolverStuck() {
3434
when(instant.plusMillis(anyLong())).thenReturn(Instant.ofEpochMilli(UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1000L));
3535
when(clock.millis()).thenReturn(UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1000L);
3636
when(solverScope.getMoveEvaluationCount()).thenReturn(1000L);
37+
when(solverScope.getMoveEvaluationSpeed()).thenReturn(1L);
3738
when(phaseScope.getBestScore()).thenReturn(SimpleScore.of(1000));
3839

3940
// Finish grace period

core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestartStrategyTest.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ class RestartStrategyTest {
1616
void restoreBestSolution() {
1717
// Requires the decider
1818
var badStrategy = new RestoreBestSolutionRestartStrategy<>();
19-
assertThatThrownBy(() -> badStrategy.solvingStarted(null)).isInstanceOf(NullPointerException.class);
19+
assertThatThrownBy(() -> badStrategy.phaseStarted(null)).isInstanceOf(NullPointerException.class);
2020

2121
// Restore the best solution
2222
var strategy = new RestoreBestSolutionRestartStrategy<>();
@@ -25,9 +25,11 @@ void restoreBestSolution() {
2525
var solverScope = mock(SolverScope.class);
2626
var phaseScope = mock(LocalSearchPhaseScope.class);
2727
when(phaseScope.getSolverScope()).thenReturn(solverScope);
28+
when(phaseScope.getDecider()).thenReturn(decider);
2829
var stepScope = mock(LocalSearchStepScope.class);
2930
when(stepScope.getPhaseScope()).thenReturn(phaseScope);
30-
strategy.solvingStarted(new AdaptedSolverScope<>(solverScope, decider));
31+
strategy.solvingStarted(solverScope);
32+
strategy.phaseStarted(phaseScope);
3133
strategy.applyRestart(stepScope);
3234
// Restore the best solution
3335
verify(decider, times(1)).setWorkingSolutionFromBestSolution(any());

0 commit comments

Comments
 (0)