Skip to content

Commit 7b60e22

Browse files
committed
feat: iterated local search
1 parent 8e06dc7 commit 7b60e22

File tree

19 files changed

+395
-75
lines changed

19 files changed

+395
-75
lines changed

benchmark/src/main/resources/benchmark.xsd

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3146,6 +3146,9 @@
31463146
<xs:enumeration value="DIVERSIFIED_LATE_ACCEPTANCE"/>
31473147

31483148

3149+
<xs:enumeration value="ITERATED_LOCAL_SEARCH"/>
3150+
3151+
31493152
<xs:enumeration value="GREAT_DELUGE"/>
31503153

31513154

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public enum AcceptorType {
1212
SIMULATED_ANNEALING,
1313
LATE_ACCEPTANCE,
1414
DIVERSIFIED_LATE_ACCEPTANCE,
15+
ITERATED_LOCAL_SEARCH,
1516
GREAT_DELUGE,
1617
STEP_COUNTING_HILL_CLIMBING
1718
}

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

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,26 @@
55

66
import ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType;
77
import ai.timefold.solver.core.config.heuristic.selector.common.SelectionOrder;
8+
import ai.timefold.solver.core.config.heuristic.selector.entity.pillar.PillarSelectorConfig;
9+
import ai.timefold.solver.core.config.heuristic.selector.list.SubListSelectorConfig;
810
import ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig;
911
import ai.timefold.solver.core.config.heuristic.selector.move.NearbyAutoConfigurationEnabled;
1012
import ai.timefold.solver.core.config.heuristic.selector.move.composite.UnionMoveSelectorConfig;
1113
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;
16+
import ai.timefold.solver.core.config.heuristic.selector.move.generic.RuinRecreateMoveSelectorConfig;
1217
import ai.timefold.solver.core.config.heuristic.selector.move.generic.SwapMoveSelectorConfig;
18+
import ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.SubChainChangeMoveSelectorConfig;
19+
import ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.SubChainSwapMoveSelectorConfig;
1320
import ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.TailChainSwapMoveSelectorConfig;
1421
import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListChangeMoveSelectorConfig;
22+
import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListRuinRecreateMoveSelectorConfig;
1523
import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListSwapMoveSelectorConfig;
24+
import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.SubListChangeMoveSelectorConfig;
25+
import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.SubListSwapMoveSelectorConfig;
1626
import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.kopt.KOptListMoveSelectorConfig;
27+
import ai.timefold.solver.core.config.heuristic.selector.value.chained.SubChainSelectorConfig;
1728
import ai.timefold.solver.core.config.localsearch.LocalSearchPhaseConfig;
1829
import ai.timefold.solver.core.config.localsearch.LocalSearchType;
1930
import ai.timefold.solver.core.config.localsearch.decider.acceptor.AcceptorType;
@@ -60,8 +71,9 @@ phaseTermination, buildDecider(phaseConfigPolicy, phaseTermination))
6071
private LocalSearchDecider<Solution_> buildDecider(HeuristicConfigPolicy<Solution_> configPolicy,
6172
PhaseTermination<Solution_> termination) {
6273
var moveSelector = buildMoveSelector(configPolicy);
74+
var perturbationMoveSelector = buildPerturbationMoveSelector(configPolicy);
6375
var acceptor = buildAcceptor(configPolicy);
64-
RestartStrategy<Solution_> restartStrategy = new AcceptorRestartStrategy<>(acceptor);
76+
RestartStrategy<Solution_> restartStrategy = new AcceptorRestartStrategy<>(perturbationMoveSelector, acceptor);
6577
var forager = buildForager(configPolicy);
6678
if (moveSelector.isNeverEnding() && !forager.supportsNeverEndingMoveSelector()) {
6779
throw new IllegalStateException("The moveSelector (" + moveSelector
@@ -188,6 +200,15 @@ protected MoveSelector<Solution_> buildMoveSelector(HeuristicConfigPolicy<Soluti
188200
return moveSelector;
189201
}
190202

203+
@SuppressWarnings("rawtypes")
204+
protected MoveSelector<Solution_> buildPerturbationMoveSelector(HeuristicConfigPolicy<Solution_> configPolicy) {
205+
var defaultCacheType = SelectionCacheType.JUST_IN_TIME;
206+
SelectionOrder defaultSelectionOrder;
207+
defaultSelectionOrder = SelectionOrder.RANDOM;
208+
return new UnionMoveSelectorFactory<Solution_>(determinePerturbationMoveSelectorConfig(configPolicy))
209+
.buildMoveSelector(configPolicy, defaultCacheType, defaultSelectionOrder, true);
210+
}
211+
191212
private UnionMoveSelectorConfig determineDefaultMoveSelectorConfig(HeuristicConfigPolicy<Solution_> configPolicy) {
192213
var solutionDescriptor = configPolicy.getSolutionDescriptor();
193214
var basicVariableDescriptorList = solutionDescriptor.getEntityDescriptors().stream()
@@ -234,4 +255,48 @@ private UnionMoveSelectorConfig determineDefaultMoveSelectorConfig(HeuristicConf
234255
.withMoveSelectors(new ChangeMoveSelectorConfig(), new SwapMoveSelectorConfig());
235256
}
236257
}
258+
259+
private UnionMoveSelectorConfig determinePerturbationMoveSelectorConfig(HeuristicConfigPolicy<Solution_> configPolicy) {
260+
var solutionDescriptor = configPolicy.getSolutionDescriptor();
261+
var basicVariableDescriptorList = solutionDescriptor.getEntityDescriptors().stream()
262+
.flatMap(entityDescriptor -> entityDescriptor.getGenuineVariableDescriptorList().stream())
263+
.filter(variableDescriptor -> !variableDescriptor.isListVariable())
264+
.distinct()
265+
.toList();
266+
var hasChainedVariable = basicVariableDescriptorList.stream()
267+
.filter(v -> v instanceof BasicVariableDescriptor<Solution_>)
268+
.anyMatch(v -> ((BasicVariableDescriptor<?>) v).isChained());
269+
var listVariableDescriptor = solutionDescriptor.getListVariableDescriptor();
270+
if (basicVariableDescriptorList.isEmpty()) { // We only have the one list variable.
271+
return new UnionMoveSelectorConfig()
272+
.withMoveSelectors(new ListChangeMoveSelectorConfig(), new ListSwapMoveSelectorConfig(),
273+
new KOptListMoveSelectorConfig().withMinimumK(2).withMaximumK(2),
274+
new SubListChangeMoveSelectorConfig().withSubListSelectorConfig(
275+
new SubListSelectorConfig().withMinimumSubListSize(2).withMaximumSubListSize(5)),
276+
new SubListSwapMoveSelectorConfig().withSubListSelectorConfig(
277+
new SubListSelectorConfig().withMinimumSubListSize(2).withMaximumSubListSize(5)),
278+
new ListRuinRecreateMoveSelectorConfig().withMinimumRuinedCount(1).withMaximumRuinedCount(5));
279+
} else if (listVariableDescriptor == null) { // We only have basic variables.
280+
if (hasChainedVariable && basicVariableDescriptorList.size() == 1) {
281+
return new UnionMoveSelectorConfig()
282+
.withMoveSelectors(new ChangeMoveSelectorConfig(), new SwapMoveSelectorConfig(),
283+
new SubChainChangeMoveSelectorConfig().withSubChainSelectorConfig(
284+
new SubChainSelectorConfig().withMinimumSubChainSize(2).withMaximumSubChainSize(5)),
285+
new SubChainSwapMoveSelectorConfig().withSubChainSelectorConfig(
286+
new SubChainSelectorConfig().withMinimumSubChainSize(2).withMaximumSubChainSize(5)),
287+
new TailChainSwapMoveSelectorConfig());
288+
} else {
289+
return new UnionMoveSelectorConfig()
290+
.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)),
295+
new RuinRecreateMoveSelectorConfig().withMinimumRuinedCount(1).withMaximumRuinedCount(5));
296+
}
297+
} else {
298+
return new UnionMoveSelectorConfig()
299+
.withMoveSelectors(new ChangeMoveSelectorConfig(), new SwapMoveSelectorConfig());
300+
}
301+
}
237302
}

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

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,15 +82,16 @@ public void solvingStarted(SolverScope<Solution_> solverScope) {
8282
forager.solvingStarted(solverScope);
8383
}
8484

85-
public void moveSelectorPhaseStarted(LocalSearchPhaseScope<Solution_> phaseScope) {
86-
moveSelector.phaseStarted(phaseScope);
87-
}
88-
8985
public void phaseStarted(LocalSearchPhaseScope<Solution_> phaseScope) {
9086
restartStrategy.phaseStarted(phaseScope);
9187
moveSelectorPhaseStarted(phaseScope);
9288
acceptor.phaseStarted(phaseScope);
9389
forager.phaseStarted(phaseScope);
90+
phaseScope.setDecider(this);
91+
}
92+
93+
public void moveSelectorPhaseStarted(LocalSearchPhaseScope<Solution_> phaseScope) {
94+
moveSelector.phaseStarted(phaseScope);
9495
}
9596

9697
public void stepStarted(LocalSearchStepScope<Solution_> stepScope) {
@@ -157,6 +158,17 @@ protected void pickMove(LocalSearchStepScope<Solution_> stepScope) {
157158
}
158159
}
159160

161+
public void doMoveOnly(LocalSearchPhaseScope<Solution_> phaseScope,
162+
ai.timefold.solver.core.impl.heuristic.move.Move<Solution_> move) {
163+
MoveDirector<Solution_> moveDirector = phaseScope.getScoreDirector().getMoveDirector();
164+
var adaptedMove = new LegacyMoveAdapter<>(move);
165+
adaptedMove.execute(moveDirector);
166+
LocalSearchStepScope lastStepScope = new LocalSearchStepScope(phaseScope);
167+
lastStepScope.setStep(adaptedMove);
168+
lastStepScope.setScore(phaseScope.getScoreDirector().calculateScore());
169+
phaseScope.setLastCompletedStepScope(lastStepScope);
170+
}
171+
160172
public void stepEnded(LocalSearchStepScope<Solution_> stepScope) {
161173
if (restartStrategy.isSolverStuck(stepScope)) {
162174
restartStrategy.applyRestart(stepScope);
@@ -181,6 +193,11 @@ public void solvingEnded(SolverScope<Solution_> solverScope) {
181193
forager.solvingEnded(solverScope);
182194
}
183195

196+
public void restoreCurrentBestSolution(LocalSearchPhaseScope<Solution_> phaseScope) {
197+
phaseScope.getSolverScope().setWorkingSolutionFromBestSolution();
198+
moveSelectorPhaseStarted(phaseScope);
199+
}
200+
184201
public void solvingError(SolverScope<Solution_> solverScope, Exception exception) {
185202
// Overridable by a subclass.
186203
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@
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;
1617
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance.DiversifiedLateAcceptanceAcceptor;
1718
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance.LateAcceptanceAcceptor;
1819
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.simulatedannealing.SimulatedAnnealingAcceptor;
1920
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stepcountinghillclimbing.StepCountingHillClimbingAcceptor;
2021
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.DiminishedReturnsStuckCriterion;
2122
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.StuckCriterion;
23+
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.UnimprovedStepCountStuckCriterion;
2224
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.tabu.EntityTabuAcceptor;
2325
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.tabu.MoveTabuAcceptor;
2426
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.tabu.ValueTabuAcceptor;
@@ -50,6 +52,7 @@ public Acceptor<Solution_> buildAcceptor(HeuristicConfigPolicy<Solution_> config
5052
buildSimulatedAnnealingAcceptor(configPolicy),
5153
buildLateAcceptanceAcceptor(),
5254
buildDiversifiedLateAcceptanceAcceptor(configPolicy),
55+
buildIteratedLocalSearchAcceptor(),
5356
buildGreatDelugeAcceptor(configPolicy))
5457
.filter(Optional::isPresent)
5558
.map(Optional::get)
@@ -241,6 +244,16 @@ private Optional<MoveTabuAcceptor<Solution_>> buildMoveTabuAcceptor(HeuristicCon
241244
return Optional.empty();
242245
}
243246

247+
private Optional<IteratedLocalSearchAcceptor<Solution_>>
248+
buildIteratedLocalSearchAcceptor() {
249+
if (acceptorTypeListsContainsAcceptorType(AcceptorType.ITERATED_LOCAL_SEARCH)) {
250+
StuckCriterion<Solution_> strategy = new UnimprovedStepCountStuckCriterion<>();
251+
var acceptor = new IteratedLocalSearchAcceptor<>(5, strategy);
252+
return Optional.of(acceptor);
253+
}
254+
return Optional.empty();
255+
}
256+
244257
private Optional<GreatDelugeAcceptor<Solution_>> buildGreatDelugeAcceptor(HeuristicConfigPolicy<Solution_> configPolicy) {
245258
if (acceptorTypeListsContainsAcceptorType(AcceptorType.GREAT_DELUGE)
246259
|| acceptorConfig.getGreatDelugeWaterLevelIncrementScore() != null

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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,56 +11,56 @@
1111
*/
1212
public abstract class RestartableAcceptor<Solution_> extends AbstractAcceptor<Solution_> {
1313

14-
private final StuckCriterion<Solution_> stuckCriterion;
14+
protected final StuckCriterion<Solution_> stuckCriterion;
1515
protected boolean restartTriggered;
16-
private boolean enabled;
16+
protected boolean enableRestart;
1717

18-
protected RestartableAcceptor(boolean enabled, StuckCriterion<Solution_> stuckCriterion) {
19-
this.enabled = enabled;
18+
protected RestartableAcceptor(boolean enableRestart, StuckCriterion<Solution_> stuckCriterion) {
19+
this.enableRestart = enableRestart;
2020
this.stuckCriterion = stuckCriterion;
2121
}
2222

2323
@Override
2424
public void solvingStarted(SolverScope<Solution_> solverScope) {
2525
super.solvingStarted(solverScope);
26-
if (enabled) {
26+
if (enableRestart) {
2727
stuckCriterion.solvingStarted(solverScope);
2828
}
2929
}
3030

3131
@Override
3232
public void phaseStarted(LocalSearchPhaseScope<Solution_> phaseScope) {
3333
super.phaseStarted(phaseScope);
34-
if (enabled) {
34+
if (enableRestart) {
3535
stuckCriterion.phaseStarted(phaseScope);
3636
}
3737
}
3838

3939
@Override
4040
public void phaseEnded(LocalSearchPhaseScope<Solution_> phaseScope) {
4141
super.phaseEnded(phaseScope);
42-
if (enabled) {
42+
if (enableRestart) {
4343
stuckCriterion.phaseEnded(phaseScope);
4444
}
4545
}
4646

4747
@Override
4848
public void stepStarted(LocalSearchStepScope<Solution_> stepScope) {
4949
super.stepStarted(stepScope);
50-
if (enabled) {
50+
if (enableRestart) {
5151
stuckCriterion.stepStarted(stepScope);
5252
}
5353
}
5454

5555
@Override
5656
public void stepEnded(LocalSearchStepScope<Solution_> stepScope) {
5757
super.stepEnded(stepScope);
58-
if (enabled) {
58+
if (enableRestart) {
5959
if (stuckCriterion.isSolverStuck(stepScope)) {
6060
if (rejectRestartEvent()) {
6161
// We need to reset the criterion,
6262
// or it will trigger the restart event in the next evaluation
63-
stuckCriterion.reset(stepScope);
63+
stuckCriterion.reset(stepScope.getPhaseScope());
6464
restartTriggered = false;
6565
} else {
6666
stepScope.getPhaseScope().setSolverStuck(true);

0 commit comments

Comments
 (0)