Skip to content

Commit 24151c7

Browse files
committed
feat: different restart strategies
1 parent 4b749eb commit 24151c7

19 files changed

+489
-165
lines changed

benchmark/src/main/resources/benchmark.xsd

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2366,7 +2366,7 @@
23662366
<xs:element maxOccurs="unbounded" minOccurs="0" name="acceptorType" type="tns:acceptorType"/>
23672367

23682368

2369-
<xs:element minOccurs="0" name="enableReconfiguration" type="xs:boolean"/>
2369+
<xs:element minOccurs="0" name="reconfigurationRestartType" type="tns:restartType"/>
23702370

23712371

23722372
<xs:element minOccurs="0" name="entityTabuSize" type="xs:int"/>
@@ -3107,6 +3107,24 @@
31073107
</xs:simpleType>
31083108

31093109

3110+
<xs:simpleType name="restartType">
3111+
3112+
3113+
<xs:restriction base="xs:string">
3114+
3115+
3116+
<xs:enumeration value="UNIMPROVED_TIME"/>
3117+
3118+
3119+
<xs:enumeration value="UNIMPROVED_MOVE_COUNT"/>
3120+
3121+
3122+
</xs:restriction>
3123+
3124+
3125+
</xs:simpleType>
3126+
3127+
31103128
<xs:simpleType name="stepCountingHillClimbingType">
31113129

31123130

core/src/build/revapi-differences.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@
100100
"annotationType": "jakarta.xml.bind.annotation.XmlType",
101101
"attribute": "propOrder",
102102
"oldValue": "{\"acceptorTypeList\", \"entityTabuSize\", \"entityTabuRatio\", \"fadingEntityTabuSize\", \"fadingEntityTabuRatio\", \"valueTabuSize\", \"valueTabuRatio\", \"fadingValueTabuSize\", \"fadingValueTabuRatio\", \"moveTabuSize\", \"fadingMoveTabuSize\", \"undoMoveTabuSize\", \"fadingUndoMoveTabuSize\", \"simulatedAnnealingStartingTemperature\", \"lateAcceptanceSize\", \"greatDelugeWaterLevelIncrementScore\", \"greatDelugeWaterLevelIncrementRatio\", \"stepCountingHillClimbingSize\", \"stepCountingHillClimbingType\"}",
103-
"newValue": "{\"acceptorTypeList\", \"enableReconfiguration\", \"entityTabuSize\", \"entityTabuRatio\", \"fadingEntityTabuSize\", \"fadingEntityTabuRatio\", \"valueTabuSize\", \"valueTabuRatio\", \"fadingValueTabuSize\", \"fadingValueTabuRatio\", \"moveTabuSize\", \"fadingMoveTabuSize\", \"undoMoveTabuSize\", \"fadingUndoMoveTabuSize\", \"simulatedAnnealingStartingTemperature\", \"lateAcceptanceSize\", \"greatDelugeWaterLevelIncrementScore\", \"greatDelugeWaterLevelIncrementRatio\", \"stepCountingHillClimbingSize\", \"stepCountingHillClimbingType\"}",
103+
"newValue": "{\"acceptorTypeList\", \"reconfigurationRestartType\", \"entityTabuSize\", \"entityTabuRatio\", \"fadingEntityTabuSize\", \"fadingEntityTabuRatio\", \"valueTabuSize\", \"valueTabuRatio\", \"fadingValueTabuSize\", \"fadingValueTabuRatio\", \"moveTabuSize\", \"fadingMoveTabuSize\", \"undoMoveTabuSize\", \"fadingUndoMoveTabuSize\", \"simulatedAnnealingStartingTemperature\", \"lateAcceptanceSize\", \"greatDelugeWaterLevelIncrementScore\", \"greatDelugeWaterLevelIncrementRatio\", \"stepCountingHillClimbingSize\", \"stepCountingHillClimbingType\"}",
104104
"justification": "Add the acceptor reconfiguration setting"
105105
}
106106
]

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

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
@XmlType(propOrder = {
1717
"acceptorTypeList",
18-
"enableReconfiguration",
18+
"reconfigurationRestartType",
1919
"entityTabuSize",
2020
"entityTabuRatio",
2121
"fadingEntityTabuSize",
@@ -39,7 +39,7 @@ public class LocalSearchAcceptorConfig extends AbstractConfig<LocalSearchAccepto
3939

4040
@XmlElement(name = "acceptorType")
4141
private List<AcceptorType> acceptorTypeList = null;
42-
private Boolean enableReconfiguration;
42+
private RestartType reconfigurationRestartType = null;
4343

4444
protected Integer entityTabuSize = null;
4545
protected Double entityTabuRatio = null;
@@ -80,12 +80,12 @@ public void setAcceptorTypeList(@Nullable List<AcceptorType> acceptorTypeList) {
8080
this.acceptorTypeList = acceptorTypeList;
8181
}
8282

83-
public @Nullable Boolean getEnableReconfiguration() {
84-
return enableReconfiguration;
83+
public @Nullable RestartType getReconfigurationRestartType() {
84+
return reconfigurationRestartType;
8585
}
8686

87-
public void setEnableReconfiguration(@Nullable Boolean enableReconfiguration) {
88-
this.enableReconfiguration = enableReconfiguration;
87+
public void setReconfigurationRestartType(@Nullable RestartType reconfigurationRestartType) {
88+
this.reconfigurationRestartType = reconfigurationRestartType;
8989
}
9090

9191
public @Nullable Integer getEntityTabuSize() {
@@ -273,9 +273,8 @@ public void setStepCountingHillClimbingType(@Nullable StepCountingHillClimbingTy
273273
return this;
274274
}
275275

276-
public @NonNull LocalSearchAcceptorConfig
277-
withEnableReconfiguration(@NonNull Boolean enableReconfiguration) {
278-
this.enableReconfiguration = enableReconfiguration;
276+
public @NonNull LocalSearchAcceptorConfig withRestartType(@NonNull RestartType restartType) {
277+
this.reconfigurationRestartType = restartType;
279278
return this;
280279
}
281280

@@ -383,8 +382,8 @@ public LocalSearchAcceptorConfig withFadingUndoMoveTabuSize(Integer fadingUndoMo
383382
}
384383
}
385384
}
386-
enableReconfiguration =
387-
ConfigUtils.inheritOverwritableProperty(enableReconfiguration, inheritedConfig.getEnableReconfiguration());
385+
reconfigurationRestartType = ConfigUtils.inheritOverwritableProperty(reconfigurationRestartType,
386+
inheritedConfig.getReconfigurationRestartType());
388387
entityTabuSize = ConfigUtils.inheritOverwritableProperty(entityTabuSize, inheritedConfig.getEntityTabuSize());
389388
entityTabuRatio = ConfigUtils.inheritOverwritableProperty(entityTabuRatio, inheritedConfig.getEntityTabuRatio());
390389
fadingEntityTabuSize = ConfigUtils.inheritOverwritableProperty(fadingEntityTabuSize,
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package ai.timefold.solver.core.config.localsearch.decider.acceptor;
2+
3+
import jakarta.xml.bind.annotation.XmlEnum;
4+
5+
@XmlEnum
6+
public enum RestartType {
7+
UNIMPROVED_TIME,
8+
UNIMPROVED_MOVE_COUNT
9+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ private ReconfigurationStrategy<Solution_> buildReconfigurationStrategy(Heuristi
208208
var acceptorConfig = phaseConfig.getAcceptorConfig();
209209
if (acceptorConfig != null) {
210210
var enableReconfiguration =
211-
acceptorConfig.getEnableReconfiguration() != null && acceptorConfig.getEnableReconfiguration();
211+
acceptorConfig.getReconfigurationRestartType() != null;
212212
if (enableReconfiguration) {
213213
configPolicy.ensurePreviewFeature(PreviewFeature.RECONFIGURATION);
214214
return new RestoreBestSolutionReconfigurationStrategy<>(moveSelector, acceptor);

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

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance.LateAcceptanceAcceptor;
1818
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.NoOpRestartStrategy;
1919
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.RestartStrategy;
20-
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.UnimprovedTimeGeometricRestartStrategy;
20+
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.UnimprovedMoveCountRestartStrategy;
2121
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.simulatedannealing.SimulatedAnnealingAcceptor;
2222
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stepcountinghillclimbing.StepCountingHillClimbingAcceptor;
2323
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.tabu.EntityTabuAcceptor;
@@ -223,13 +223,9 @@ private Optional<MoveTabuAcceptor<Solution_>> buildMoveTabuAcceptor(HeuristicCon
223223
if (acceptorTypeListsContainsAcceptorType(AcceptorType.LATE_ACCEPTANCE)
224224
|| (!acceptorTypeListsContainsAcceptorType(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE)
225225
&& acceptorConfig.getLateAcceptanceSize() != null)) {
226-
RestartStrategy<Solution_> restartStrategy = new NoOpRestartStrategy<>();
227-
var enableReconfiguration =
228-
acceptorConfig.getEnableReconfiguration() != null && acceptorConfig.getEnableReconfiguration();
229-
if (enableReconfiguration) {
230-
restartStrategy = new UnimprovedTimeGeometricRestartStrategy<>();
231-
}
232-
var acceptor = new LateAcceptanceAcceptor<>(enableReconfiguration, restartStrategy);
226+
var restartStrategy = buildRestartStrategy();
227+
var acceptor =
228+
new LateAcceptanceAcceptor<>(!(restartStrategy instanceof NoOpRestartStrategy<Solution_>), restartStrategy);
233229
acceptor.setLateAcceptanceSize(Objects.requireNonNullElse(acceptorConfig.getLateAcceptanceSize(), 400));
234230
return Optional.of(acceptor);
235231
}
@@ -240,19 +236,27 @@ private Optional<MoveTabuAcceptor<Solution_>> buildMoveTabuAcceptor(HeuristicCon
240236
buildDiversifiedLateAcceptanceAcceptor(HeuristicConfigPolicy<Solution_> configPolicy) {
241237
if (acceptorTypeListsContainsAcceptorType(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE)) {
242238
configPolicy.ensurePreviewFeature(PreviewFeature.DIVERSIFIED_LATE_ACCEPTANCE);
243-
RestartStrategy<Solution_> restartStrategy = new NoOpRestartStrategy<>();
244-
var enableReconfiguration =
245-
acceptorConfig.getEnableReconfiguration() != null && acceptorConfig.getEnableReconfiguration();
246-
if (enableReconfiguration) {
247-
restartStrategy = new UnimprovedTimeGeometricRestartStrategy<>();
248-
}
249-
var acceptor = new DiversifiedLateAcceptanceAcceptor<>(enableReconfiguration, restartStrategy);
239+
var restartStrategy = buildRestartStrategy();
240+
var acceptor = new DiversifiedLateAcceptanceAcceptor<>(!(restartStrategy instanceof NoOpRestartStrategy<Solution_>),
241+
restartStrategy);
250242
acceptor.setLateAcceptanceSize(Objects.requireNonNullElse(acceptorConfig.getLateAcceptanceSize(), 5));
251243
return Optional.of(acceptor);
252244
}
253245
return Optional.empty();
254246
}
255247

248+
private RestartStrategy<Solution_> buildRestartStrategy() {
249+
RestartStrategy<Solution_> restartStrategy = new NoOpRestartStrategy<>();
250+
var enableReconfiguration = acceptorConfig.getReconfigurationRestartType() != null;
251+
if (enableReconfiguration) {
252+
return switch (acceptorConfig.getReconfigurationRestartType()) {
253+
case UNIMPROVED_TIME -> new UnimprovedMoveCountRestartStrategy<>();
254+
case UNIMPROVED_MOVE_COUNT -> new UnimprovedMoveCountRestartStrategy<>();
255+
};
256+
}
257+
return restartStrategy;
258+
}
259+
256260
private Optional<GreatDelugeAcceptor<Solution_>> buildGreatDelugeAcceptor(HeuristicConfigPolicy<Solution_> configPolicy) {
257261
if (acceptorTypeListsContainsAcceptorType(AcceptorType.GREAT_DELUGE)
258262
|| acceptorConfig.getGreatDelugeWaterLevelIncrementScore() != null

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public boolean isAccepted(LocalSearchMoveScope<Solution_> moveScope) {
6565
var accepted = evaluate(moveScope);
6666
var improved = enabled && moveScope.getScore().compareTo(moveScope.getStepScope().getPhaseScope().getBestScore()) > 0;
6767
if (improved) {
68-
restartStrategy.reset();
68+
restartStrategy.reset(moveScope);
6969
}
7070
return accepted;
7171
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart;
2+
3+
import java.time.Clock;
4+
5+
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope;
6+
import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope;
7+
import ai.timefold.solver.core.impl.solver.scope.SolverScope;
8+
9+
import org.slf4j.Logger;
10+
import org.slf4j.LoggerFactory;
11+
12+
/**
13+
* Restart strategy, which exponentially increases the metric that triggers the restart process.
14+
* The first restart occurs after the {@code grace period + 1 * scalingFactor} metric.
15+
* Following that, the metric increases exponentially: 1, 2, 3, 5, 7, 10, 14...
16+
* <p>
17+
* The strategy is based on the work: Search in a Small World by Toby Walsh
18+
*
19+
* @param <Solution_> the solution type
20+
*/
21+
public abstract class AbstractGeometricRestartStrategy<Solution_> implements RestartStrategy<Solution_> {
22+
private static final double GEOMETRIC_FACTOR = 1.4; // Value extracted from the cited paper
23+
protected final Clock clock;
24+
protected final Logger logger = LoggerFactory.getLogger(AbstractGeometricRestartStrategy.class);
25+
protected final double scalingFactor;
26+
27+
private boolean gracePeriodFinished;
28+
private boolean restartTriggered;
29+
private long gracePeriodMillis;
30+
protected long nextRestart;
31+
protected double currentGeometricGrowFactor;
32+
33+
protected AbstractGeometricRestartStrategy(Clock clock, double scalingFactor) {
34+
this.clock = clock;
35+
this.scalingFactor = scalingFactor;
36+
}
37+
38+
@Override
39+
public void phaseStarted(LocalSearchPhaseScope<Solution_> phaseScope) {
40+
restartTriggered = false;
41+
if (gracePeriodMillis == 0) {
42+
// 10 seconds of grace period
43+
gracePeriodMillis = clock.millis() + 10_000;
44+
}
45+
}
46+
47+
@Override
48+
public void phaseEnded(LocalSearchPhaseScope<Solution_> phaseScope) {
49+
// Do nothing
50+
}
51+
52+
@Override
53+
public void solvingStarted(SolverScope<Solution_> solverScope) {
54+
currentGeometricGrowFactor = 1;
55+
gracePeriodMillis = 0;
56+
gracePeriodFinished = false;
57+
nextRestart = (long) Math.ceil(currentGeometricGrowFactor * scalingFactor);
58+
}
59+
60+
@Override
61+
public void solvingEnded(SolverScope<Solution_> solverScope) {
62+
// Do nothing
63+
}
64+
65+
@Override
66+
public boolean isTriggered(LocalSearchMoveScope<Solution_> moveScope) {
67+
if (!restartTriggered && (gracePeriodFinished || clock.millis() >= gracePeriodMillis)) {
68+
gracePeriodFinished = true;
69+
var triggered = process(moveScope);
70+
if (triggered) {
71+
currentGeometricGrowFactor = Math.ceil(currentGeometricGrowFactor * GEOMETRIC_FACTOR);
72+
nextRestart = (long) Math.ceil(currentGeometricGrowFactor * scalingFactor);
73+
restartTriggered = true;
74+
}
75+
}
76+
return restartTriggered;
77+
}
78+
79+
protected boolean isGracePeriodFinished() {
80+
return gracePeriodFinished;
81+
}
82+
83+
protected void disableTriggerFlag() {
84+
restartTriggered = false;
85+
}
86+
87+
abstract boolean process(LocalSearchMoveScope<Solution_> moveScope);
88+
89+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public boolean isTriggered(LocalSearchMoveScope<Solution_> moveScope) {
1313
}
1414

1515
@Override
16-
public void reset() {
16+
public void reset(LocalSearchMoveScope<Solution_> moveScope) {
1717
// Do nothing
1818
}
1919

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ public interface RestartStrategy<Solution_> extends LocalSearchPhaseLifecycleLis
77

88
boolean isTriggered(LocalSearchMoveScope<Solution_> moveScope);
99

10-
void reset();
10+
void reset(LocalSearchMoveScope<Solution_> moveScope);
1111
}

0 commit comments

Comments
 (0)