From 6df9fc798cdd74dc02321688947720d232da1cc2 Mon Sep 17 00:00:00 2001 From: Fred Date: Fri, 3 Jan 2025 19:08:17 -0300 Subject: [PATCH 01/31] feat: add new reconfiguration strategy --- benchmark/src/main/resources/benchmark.xsd | 12 ++- core/src/build/revapi-differences.json | 11 +++ .../acceptor/LocalSearchAcceptorConfig.java | 17 ++++ .../core/config/solver/PreviewFeature.java | 2 +- .../localsearch/DefaultLocalSearchPhase.java | 12 ++- .../decider/LocalSearchDecider.java | 6 +- .../decider/acceptor/AcceptorFactory.java | 17 +++- .../ReconfigurableAbstractAcceptor.java | 89 +++++++++++++++++++ .../DiversifiedLateAcceptanceAcceptor.java | 16 +++- .../LateAcceptanceAcceptor.java | 16 +++- ...provedSolutionReconfigurationStrategy.java | 84 +++++++++++++++++ .../ReconfigurationStrategy.java | 11 +++ .../core/impl/solver/scope/SolverScope.java | 11 +++ core/src/main/resources/solver.xsd | 8 +- ...DiversifiedLateAcceptanceAcceptorTest.java | 4 +- .../LateAcceptanceAcceptorTest.java | 8 +- 16 files changed, 298 insertions(+), 26 deletions(-) create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAbstractAcceptor.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/reconfiguration/GeometricUnimprovedSolutionReconfigurationStrategy.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/reconfiguration/ReconfigurationStrategy.java diff --git a/benchmark/src/main/resources/benchmark.xsd b/benchmark/src/main/resources/benchmark.xsd index 24305f6e35..6be5cecbc3 100644 --- a/benchmark/src/main/resources/benchmark.xsd +++ b/benchmark/src/main/resources/benchmark.xsd @@ -2414,6 +2414,9 @@ + + + @@ -2601,10 +2604,13 @@ - - + + + + + - + diff --git a/core/src/build/revapi-differences.json b/core/src/build/revapi-differences.json index 22164eafe1..50fa5f9bf2 100644 --- a/core/src/build/revapi-differences.json +++ b/core/src/build/revapi-differences.json @@ -102,6 +102,17 @@ "oldValue": "{\"terminationClass\", \"terminationCompositionStyle\", \"spentLimit\", \"millisecondsSpentLimit\", \"secondsSpentLimit\", \"minutesSpentLimit\", \"hoursSpentLimit\", \"daysSpentLimit\", \"unimprovedSpentLimit\", \"unimprovedMillisecondsSpentLimit\", \"unimprovedSecondsSpentLimit\", \"unimprovedMinutesSpentLimit\", \"unimprovedHoursSpentLimit\", \"unimprovedDaysSpentLimit\", \"unimprovedScoreDifferenceThreshold\", \"bestScoreLimit\", \"bestScoreFeasible\", \"stepCountLimit\", \"unimprovedStepCountLimit\", \"scoreCalculationCountLimit\", \"terminationConfigList\"}", "newValue": "{\"terminationClass\", \"terminationCompositionStyle\", \"diminishedReturnsConfig\", \"spentLimit\", \"millisecondsSpentLimit\", \"secondsSpentLimit\", \"minutesSpentLimit\", \"hoursSpentLimit\", \"daysSpentLimit\", \"unimprovedSpentLimit\", \"unimprovedMillisecondsSpentLimit\", \"unimprovedSecondsSpentLimit\", \"unimprovedMinutesSpentLimit\", \"unimprovedHoursSpentLimit\", \"unimprovedDaysSpentLimit\", \"unimprovedScoreDifferenceThreshold\", \"bestScoreLimit\", \"bestScoreFeasible\", \"stepCountLimit\", \"unimprovedStepCountLimit\", \"scoreCalculationCountLimit\", \"moveCountLimit\", \"terminationConfigList\"}", "justification": "Added support for the new diminished returns termination type" + }, + { + "ignore": true, + "code": "java.annotation.attributeValueChanged", + "old": "class ai.timefold.solver.core.config.localsearch.decider.acceptor.LocalSearchAcceptorConfig", + "new": "class ai.timefold.solver.core.config.localsearch.decider.acceptor.LocalSearchAcceptorConfig", + "annotationType": "jakarta.xml.bind.annotation.XmlType", + "attribute": "propOrder", + "oldValue": "{\"acceptorTypeList\", \"entityTabuSize\", \"entityTabuRatio\", \"fadingEntityTabuSize\", \"fadingEntityTabuRatio\", \"valueTabuSize\", \"valueTabuRatio\", \"fadingValueTabuSize\", \"fadingValueTabuRatio\", \"moveTabuSize\", \"fadingMoveTabuSize\", \"undoMoveTabuSize\", \"fadingUndoMoveTabuSize\", \"simulatedAnnealingStartingTemperature\", \"lateAcceptanceSize\", \"greatDelugeWaterLevelIncrementScore\", \"greatDelugeWaterLevelIncrementRatio\", \"stepCountingHillClimbingSize\", \"stepCountingHillClimbingType\"}", + "newValue": "{\"acceptorTypeList\", \"enableReconfiguration\", \"entityTabuSize\", \"entityTabuRatio\", \"fadingEntityTabuSize\", \"fadingEntityTabuRatio\", \"valueTabuSize\", \"valueTabuRatio\", \"fadingValueTabuSize\", \"fadingValueTabuRatio\", \"moveTabuSize\", \"fadingMoveTabuSize\", \"undoMoveTabuSize\", \"fadingUndoMoveTabuSize\", \"simulatedAnnealingStartingTemperature\", \"lateAcceptanceSize\", \"greatDelugeWaterLevelIncrementScore\", \"greatDelugeWaterLevelIncrementRatio\", \"stepCountingHillClimbingSize\", \"stepCountingHillClimbingType\"}", + "justification": "Enable acceptor reconfiguration" } ] } diff --git a/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/LocalSearchAcceptorConfig.java b/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/LocalSearchAcceptorConfig.java index 174e35512d..f52833ad86 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/LocalSearchAcceptorConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/LocalSearchAcceptorConfig.java @@ -15,6 +15,7 @@ @XmlType(propOrder = { "acceptorTypeList", + "enableReconfiguration", "entityTabuSize", "entityTabuRatio", "fadingEntityTabuSize", @@ -38,6 +39,7 @@ public class LocalSearchAcceptorConfig extends AbstractConfig acceptorTypeList = null; + private Boolean enableReconfiguration = null; protected Integer entityTabuSize = null; protected Double entityTabuRatio = null; @@ -78,6 +80,14 @@ public void setAcceptorTypeList(@Nullable List acceptorTypeList) { this.acceptorTypeList = acceptorTypeList; } + public @Nullable Boolean getEnableReconfiguration() { + return enableReconfiguration; + } + + public void setEnableReconfiguration(@Nullable Boolean enableReconfiguration) { + this.enableReconfiguration = enableReconfiguration; + } + public @Nullable Integer getEntityTabuSize() { return entityTabuSize; } @@ -263,6 +273,11 @@ public void setStepCountingHillClimbingType(@Nullable StepCountingHillClimbingTy return this; } + public @NonNull LocalSearchAcceptorConfig withEnableReconfiguration(@NonNull Boolean enableReconfiguration) { + this.enableReconfiguration = enableReconfiguration; + return this; + } + public @NonNull LocalSearchAcceptorConfig withEntityTabuSize(@NonNull Integer entityTabuSize) { this.entityTabuSize = entityTabuSize; return this; @@ -367,6 +382,8 @@ public LocalSearchAcceptorConfig withFadingUndoMoveTabuSize(Integer fadingUndoMo } } } + enableReconfiguration = + ConfigUtils.inheritOverwritableProperty(enableReconfiguration, inheritedConfig.getEnableReconfiguration()); entityTabuSize = ConfigUtils.inheritOverwritableProperty(entityTabuSize, inheritedConfig.getEntityTabuSize()); entityTabuRatio = ConfigUtils.inheritOverwritableProperty(entityTabuRatio, inheritedConfig.getEntityTabuRatio()); fadingEntityTabuSize = ConfigUtils.inheritOverwritableProperty(fadingEntityTabuSize, diff --git a/core/src/main/java/ai/timefold/solver/core/config/solver/PreviewFeature.java b/core/src/main/java/ai/timefold/solver/core/config/solver/PreviewFeature.java index 402bf6205f..86b4732b0b 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/solver/PreviewFeature.java +++ b/core/src/main/java/ai/timefold/solver/core/config/solver/PreviewFeature.java @@ -1,8 +1,8 @@ package ai.timefold.solver.core.config.solver; public enum PreviewFeature { - DIVERSIFIED_LATE_ACCEPTANCE, + ACCEPTOR_RECONFIGURATION, PLANNING_SOLUTION_DIFF } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java index 8bb5be4f62..c84890518b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java @@ -90,9 +90,15 @@ public void solve(SolverScope solverScope) { // Although stepStarted has been called, stepEnded is not called for this step break; } - doStep(stepScope); - stepEnded(stepScope); - phaseScope.setLastCompletedStepScope(stepScope); + if (!solverScope.isResetWorkingSolution()) { + doStep(stepScope); + stepEnded(stepScope); + phaseScope.setLastCompletedStepScope(stepScope); + } else { + logger.debug("Working solution reset, score ({})", solverScope.getBestScore()); + solverScope.setWorkingSolutionFromBestSolution(); + decider.moveSelectorPhaseStarted(phaseScope); + } } phaseEnded(phaseScope); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java index bcdb810ec2..b3eb0dcae9 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java @@ -78,8 +78,12 @@ public void solvingStarted(SolverScope solverScope) { forager.solvingStarted(solverScope); } - public void phaseStarted(LocalSearchPhaseScope phaseScope) { + public void moveSelectorPhaseStarted(LocalSearchPhaseScope phaseScope) { moveSelector.phaseStarted(phaseScope); + } + + public void phaseStarted(LocalSearchPhaseScope phaseScope) { + moveSelectorPhaseStarted(phaseScope); acceptor.phaseStarted(phaseScope); forager.phaseStarted(phaseScope); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java index f1d2232668..55c6aba5af 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java @@ -46,7 +46,7 @@ public Acceptor buildAcceptor(HeuristicConfigPolicy config buildValueTabuAcceptor(configPolicy), buildMoveTabuAcceptor(configPolicy), buildSimulatedAnnealingAcceptor(configPolicy), - buildLateAcceptanceAcceptor(), + buildLateAcceptanceAcceptor(configPolicy), buildDiversifiedLateAcceptanceAcceptor(configPolicy), buildGreatDelugeAcceptor(configPolicy)) .filter(Optional::isPresent) @@ -215,11 +215,16 @@ private Optional> buildMoveTabuAcceptor(HeuristicCon return Optional.empty(); } - private Optional> buildLateAcceptanceAcceptor() { + private Optional> + buildLateAcceptanceAcceptor(HeuristicConfigPolicy configPolicy) { if (acceptorTypeListsContainsAcceptorType(AcceptorType.LATE_ACCEPTANCE) || (!acceptorTypeListsContainsAcceptorType(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE) && acceptorConfig.getLateAcceptanceSize() != null)) { - var acceptor = new LateAcceptanceAcceptor(); + var enableReconfiguration = Objects.requireNonNullElse(acceptorConfig.getEnableReconfiguration(), false); + if (enableReconfiguration) { + configPolicy.ensurePreviewFeature(PreviewFeature.ACCEPTOR_RECONFIGURATION); + } + var acceptor = new LateAcceptanceAcceptor(enableReconfiguration); acceptor.setLateAcceptanceSize(Objects.requireNonNullElse(acceptorConfig.getLateAcceptanceSize(), 400)); return Optional.of(acceptor); } @@ -230,7 +235,11 @@ private Optional> buildLateAcceptanceAcceptor( buildDiversifiedLateAcceptanceAcceptor(HeuristicConfigPolicy configPolicy) { if (acceptorTypeListsContainsAcceptorType(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE)) { configPolicy.ensurePreviewFeature(PreviewFeature.DIVERSIFIED_LATE_ACCEPTANCE); - var acceptor = new DiversifiedLateAcceptanceAcceptor(); + var enableReconfiguration = Objects.requireNonNullElse(acceptorConfig.getEnableReconfiguration(), false); + if (enableReconfiguration) { + configPolicy.ensurePreviewFeature(PreviewFeature.ACCEPTOR_RECONFIGURATION); + } + var acceptor = new DiversifiedLateAcceptanceAcceptor(enableReconfiguration); acceptor.setLateAcceptanceSize(Objects.requireNonNullElse(acceptorConfig.getLateAcceptanceSize(), 5)); return Optional.of(acceptor); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAbstractAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAbstractAcceptor.java new file mode 100644 index 0000000000..2c00304237 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAbstractAcceptor.java @@ -0,0 +1,89 @@ +package ai.timefold.solver.core.impl.localsearch.decider.acceptor; + +import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.reconfiguration.ReconfigurationStrategy; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; + +/** + * Base class designed to provide a reconfiguration strategy for an acceptor implementation. + */ +public abstract class ReconfigurableAbstractAcceptor extends AbstractAcceptor { + + private static final int MAX_PERTURBATIONS = 2; + private final ReconfigurationStrategy reconfigurationStrategy; + private int currentPerturbationCount; + private int remainingPerturbations; + private final boolean enabled; + + protected ReconfigurableAbstractAcceptor(ReconfigurationStrategy reconfigurationStrategy, boolean enabled) { + this.reconfigurationStrategy = reconfigurationStrategy; + this.enabled = enabled; + } + + @Override + public void phaseStarted(LocalSearchPhaseScope phaseScope) { + super.phaseStarted(phaseScope); + init(); + reconfigurationStrategy.phaseStarted(phaseScope); + } + + @Override + public void phaseEnded(LocalSearchPhaseScope phaseScope) { + super.phaseEnded(phaseScope); + reconfigurationStrategy.phaseEnded(phaseScope); + } + + @Override + public void stepStarted(LocalSearchStepScope stepScope) { + super.stepStarted(stepScope); + reconfigurationStrategy.stepStarted(stepScope); + } + + @Override + public void stepEnded(LocalSearchStepScope stepScope) { + super.stepEnded(stepScope); + reconfigurationStrategy.stepEnded(stepScope); + } + + @Override + @SuppressWarnings("unchecked") + public boolean isAccepted(LocalSearchMoveScope moveScope) { + if (enabled && remainingPerturbations > 0) { + remainingPerturbations--; + if (remainingPerturbations == 0) { + reconfigurationStrategy.reset(); + reset(moveScope.getScore()); + } + return true; + } + if (enabled && reconfigurationStrategy.needReconfiguration(moveScope)) { + applyPerturbation(moveScope); + return true; + } + var accepted = evaluate(moveScope); + if (enabled && moveScope.getScore().compareTo(moveScope.getStepScope().getPhaseScope().getBestScore()) > 0) { + init(); + reconfigurationStrategy.reset(); + } + return accepted; + } + + private void applyPerturbation(LocalSearchMoveScope moveScope) { + remainingPerturbations = currentPerturbationCount; + if (currentPerturbationCount < MAX_PERTURBATIONS) { + currentPerturbationCount++; + } + moveScope.getStepScope().getPhaseScope().getSolverScope().triggerResetWorkingSolution(); + } + + private void init() { + this.currentPerturbationCount = 1; + this.remainingPerturbations = 0; + } + + protected abstract boolean evaluate(LocalSearchMoveScope moveScope); + + protected abstract > void reset(Score_ score); +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java index b87373414b..656b14dc98 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java @@ -3,11 +3,12 @@ import java.util.Arrays; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.AbstractAcceptor; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.ReconfigurableAbstractAcceptor; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.reconfiguration.GeometricUnimprovedSolutionReconfigurationStrategy; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; -public class DiversifiedLateAcceptanceAcceptor extends AbstractAcceptor { +public class DiversifiedLateAcceptanceAcceptor extends ReconfigurableAbstractAcceptor { // The worst score in the late elements list protected Score lateWorse; @@ -19,6 +20,10 @@ public class DiversifiedLateAcceptanceAcceptor extends AbstractAccept protected Score[] previousScores; protected int lateScoreIndex = -1; + public DiversifiedLateAcceptanceAcceptor(boolean enableReconfiguration) { + super(new GeometricUnimprovedSolutionReconfigurationStrategy<>(), enableReconfiguration); + } + public void setLateAcceptanceSize(int lateAcceptanceSize) { this.lateAcceptanceSize = lateAcceptanceSize; } @@ -48,7 +53,7 @@ private void validate() { @Override @SuppressWarnings({ "rawtypes", "unchecked" }) - public boolean isAccepted(LocalSearchMoveScope moveScope) { + protected boolean evaluate(LocalSearchMoveScope moveScope) { // The acceptance and replacement strategies are based on the work: // Diversified Late Acceptance Search by M. Namazi, C. Sanderson, M. A. H. Newton, M. M. A. Polash, and A. Sattar var moveScore = moveScope.getScore(); @@ -71,6 +76,11 @@ public boolean isAccepted(LocalSearchMoveScope moveScope) { return accept; } + @Override + protected > void reset(Score_ score) { + previousScores[lateScoreIndex] = score; + } + @SuppressWarnings({ "rawtypes", "unchecked" }) private void updateLateScore(Score newScore) { var newScoreWorseCmp = newScore.compareTo(lateWorse); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java index 332a1e15a1..99dcec897b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java @@ -3,12 +3,13 @@ import java.util.Arrays; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.AbstractAcceptor; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.ReconfigurableAbstractAcceptor; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.reconfiguration.GeometricUnimprovedSolutionReconfigurationStrategy; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; -public class LateAcceptanceAcceptor extends AbstractAcceptor { +public class LateAcceptanceAcceptor extends ReconfigurableAbstractAcceptor { protected int lateAcceptanceSize = -1; protected boolean hillClimbingEnabled = true; @@ -16,6 +17,10 @@ public class LateAcceptanceAcceptor extends AbstractAcceptor[] previousScores; protected int lateScoreIndex = -1; + public LateAcceptanceAcceptor(boolean enableReconfiguration) { + super(new GeometricUnimprovedSolutionReconfigurationStrategy<>(), enableReconfiguration); + } + public void setLateAcceptanceSize(int lateAcceptanceSize) { this.lateAcceptanceSize = lateAcceptanceSize; } @@ -47,7 +52,7 @@ private void validate() { @Override @SuppressWarnings("unchecked") - public boolean isAccepted(LocalSearchMoveScope moveScope) { + public boolean evaluate(LocalSearchMoveScope moveScope) { var moveScore = moveScope.getScore(); var lateScore = previousScores[lateScoreIndex]; if (moveScore.compareTo(lateScore) >= 0) { @@ -60,6 +65,11 @@ public boolean isAccepted(LocalSearchMoveScope moveScope) { return false; } + @Override + protected > void reset(Score_ score) { + previousScores[lateScoreIndex] = score; + } + @Override public void stepEnded(LocalSearchStepScope stepScope) { super.stepEnded(stepScope); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/reconfiguration/GeometricUnimprovedSolutionReconfigurationStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/reconfiguration/GeometricUnimprovedSolutionReconfigurationStrategy.java new file mode 100644 index 0000000000..ecd0413792 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/reconfiguration/GeometricUnimprovedSolutionReconfigurationStrategy.java @@ -0,0 +1,84 @@ +package ai.timefold.solver.core.impl.localsearch.decider.acceptor.reconfiguration; + +import java.time.Clock; + +import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class GeometricUnimprovedSolutionReconfigurationStrategy implements ReconfigurationStrategy { + private static final double GEOMETRIC_FACTOR = 1.3; + private static final double SCALING_FACTOR = 1.0; + + private final Logger logger = LoggerFactory.getLogger(GeometricUnimprovedSolutionReconfigurationStrategy.class); + private final Clock clock = Clock.systemUTC(); + + private long lastImprovementMillis; + private Score currentBestScore; + private boolean reconfigurationTriggered; + private double geometricGrowFactor; + private long nextReconfiguration; + + @Override + public void phaseStarted(LocalSearchPhaseScope phaseScope) { + currentBestScore = phaseScope.getBestScore(); + geometricGrowFactor = 1; + nextReconfiguration = (long) (1_000 * SCALING_FACTOR); + reconfigurationTriggered = false; + lastImprovementMillis = clock.millis(); + } + + @Override + public void phaseEnded(LocalSearchPhaseScope phaseScope) { + // Do nothing + } + + @Override + public void stepStarted(LocalSearchStepScope stepScope) { + this.currentBestScore = stepScope.getPhaseScope().getBestScore(); + } + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void stepEnded(LocalSearchStepScope stepScope) { + if (((Score) stepScope.getScore()).compareTo(currentBestScore) > 0) { + lastImprovementMillis = clock.millis(); + this.currentBestScore = stepScope.getScore(); + } + } + + @Override + public void solvingStarted(SolverScope solverScope) { + // Do nothing + } + + @Override + public void solvingEnded(SolverScope solverScope) { + // Do nothing + } + + @Override + public boolean needReconfiguration(LocalSearchMoveScope moveScope) { + if (!reconfigurationTriggered && lastImprovementMillis > 0 + && clock.millis() - lastImprovementMillis >= nextReconfiguration) { + logger.debug("Reconfiguration triggered with geometric factor {} and scaling factor of {}", geometricGrowFactor, + SCALING_FACTOR); + nextReconfiguration = (long) Math.ceil(SCALING_FACTOR * geometricGrowFactor * 1_000); + geometricGrowFactor = Math.ceil(geometricGrowFactor * GEOMETRIC_FACTOR); + lastImprovementMillis = clock.millis(); + reconfigurationTriggered = true; + } + return reconfigurationTriggered; + } + + @Override + public void reset() { + reconfigurationTriggered = false; + lastImprovementMillis = clock.millis(); + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/reconfiguration/ReconfigurationStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/reconfiguration/ReconfigurationStrategy.java new file mode 100644 index 0000000000..7381627c03 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/reconfiguration/ReconfigurationStrategy.java @@ -0,0 +1,11 @@ +package ai.timefold.solver.core.impl.localsearch.decider.acceptor.reconfiguration; + +import ai.timefold.solver.core.impl.localsearch.event.LocalSearchPhaseLifecycleListener; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; + +public interface ReconfigurationStrategy extends LocalSearchPhaseLifecycleListener { + + boolean needReconfiguration(LocalSearchMoveScope moveScope); + + void reset(); +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java index 629f6120a9..96bf426f10 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java @@ -80,6 +80,8 @@ private static Long readAtomicLongTimeMillis(AtomicLong atomicLong) { return value == -1 ? null : value; } + private boolean resetWorkingSolution = false; + // ************************************************************************ // Constructors and simple getters/setters // ************************************************************************ @@ -241,6 +243,14 @@ public Map getMoveEvaluationCountPerType() { return moveEvaluationCountPerTypeMap; } + public void triggerResetWorkingSolution() { + resetWorkingSolution = true; + } + + public boolean isResetWorkingSolution() { + return resetWorkingSolution; + } + // ************************************************************************ // Calculated methods // ************************************************************************ @@ -311,6 +321,7 @@ public long getMoveEvaluationSpeed() { public void setWorkingSolutionFromBestSolution() { // The workingSolution must never be the same instance as the bestSolution. scoreDirector.setWorkingSolution(scoreDirector.cloneSolution(getBestSolution())); + resetWorkingSolution = false; } public SolverScope createChildThreadSolverScope(ChildThreadType childThreadType) { diff --git a/core/src/main/resources/solver.xsd b/core/src/main/resources/solver.xsd index 868923ef66..89ef9703b0 100644 --- a/core/src/main/resources/solver.xsd +++ b/core/src/main/resources/solver.xsd @@ -1415,6 +1415,8 @@ + + @@ -1580,9 +1582,11 @@ - + + + - + diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java index a30690e1c3..b8d55bcc5f 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java @@ -14,7 +14,7 @@ class DiversifiedLateAcceptanceAcceptorTest extends AbstractAcceptorTest { @Test void acceptanceCriterion() { - var acceptor = new DiversifiedLateAcceptanceAcceptor<>(); + var acceptor = new DiversifiedLateAcceptanceAcceptor<>(false); acceptor.setLateAcceptanceSize(3); var solverScope = new SolverScope<>(); @@ -51,7 +51,7 @@ void acceptanceCriterion() { @Test void replacementCriterion() { - var acceptor = new DiversifiedLateAcceptanceAcceptor<>(); + var acceptor = new DiversifiedLateAcceptanceAcceptor<>(false); acceptor.setLateAcceptanceSize(3); var solverScope = new SolverScope<>(); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptorTest.java index dc245c652f..01569e7ace 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptorTest.java @@ -15,7 +15,7 @@ class LateAcceptanceAcceptorTest extends AbstractAcceptorTest { @Test void lateAcceptanceSize() { - var acceptor = new LateAcceptanceAcceptor<>(); + var acceptor = new LateAcceptanceAcceptor<>(false); acceptor.setLateAcceptanceSize(3); acceptor.setHillClimbingEnabled(false); @@ -128,7 +128,7 @@ void lateAcceptanceSize() { @Test void hillClimbingEnabled() { - var acceptor = new LateAcceptanceAcceptor<>(); + var acceptor = new LateAcceptanceAcceptor<>(false); acceptor.setLateAcceptanceSize(2); acceptor.setHillClimbingEnabled(true); @@ -241,14 +241,14 @@ void hillClimbingEnabled() { @Test void zeroLateAcceptanceSize() { - var acceptor = new LateAcceptanceAcceptor<>(); + var acceptor = new LateAcceptanceAcceptor<>(false); acceptor.setLateAcceptanceSize(0); assertThatIllegalArgumentException().isThrownBy(() -> acceptor.phaseStarted(null)); } @Test void negativeLateAcceptanceSize() { - var acceptor = new LateAcceptanceAcceptor<>(); + var acceptor = new LateAcceptanceAcceptor<>(false); acceptor.setLateAcceptanceSize(-1); assertThatIllegalArgumentException().isThrownBy(() -> acceptor.phaseStarted(null)); } From 4a7f211edcbf142b4ae54813dd433751a123a237 Mon Sep 17 00:00:00 2001 From: Fred Date: Fri, 10 Jan 2025 14:15:09 -0300 Subject: [PATCH 02/31] feat: perturbation using ruin and recreate moves --- .../localsearch/DefaultLocalSearchPhase.java | 4 --- .../decider/LocalSearchDecider.java | 34 +++++++++++++++---- .../core/impl/solver/scope/SolverScope.java | 4 +++ 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java index c84890518b..f45528f775 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java @@ -94,10 +94,6 @@ public void solve(SolverScope solverScope) { doStep(stepScope); stepEnded(stepScope); phaseScope.setLastCompletedStepScope(stepScope); - } else { - logger.debug("Working solution reset, score ({})", solverScope.getBestScore()); - solverScope.setWorkingSolutionFromBestSolution(); - decider.moveSelectorPhaseStarted(phaseScope); } } phaseEnded(phaseScope); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java index b3eb0dcae9..e38d4973e6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java @@ -29,6 +29,7 @@ public class LocalSearchDecider { protected final String logIndentation; protected final Termination termination; protected final MoveSelector moveSelector; + protected final MoveSelector perturbationMoveSelector; protected final Acceptor acceptor; protected final LocalSearchForager forager; @@ -36,10 +37,12 @@ public class LocalSearchDecider { protected boolean assertExpectedUndoMoveScore = false; public LocalSearchDecider(String logIndentation, Termination termination, - MoveSelector moveSelector, Acceptor acceptor, LocalSearchForager forager) { + MoveSelector moveSelector, MoveSelector perturbationMoveSelector, + Acceptor acceptor, LocalSearchForager forager) { this.logIndentation = logIndentation; this.termination = termination; this.moveSelector = moveSelector; + this.perturbationMoveSelector = perturbationMoveSelector; this.acceptor = acceptor; this.forager = forager; } @@ -78,12 +81,8 @@ public void solvingStarted(SolverScope solverScope) { forager.solvingStarted(solverScope); } - public void moveSelectorPhaseStarted(LocalSearchPhaseScope phaseScope) { - moveSelector.phaseStarted(phaseScope); - } - public void phaseStarted(LocalSearchPhaseScope phaseScope) { - moveSelectorPhaseStarted(phaseScope); + moveSelector.phaseStarted(phaseScope); acceptor.phaseStarted(phaseScope); forager.phaseStarted(phaseScope); } @@ -98,6 +97,9 @@ public void decideNextStep(LocalSearchStepScope stepScope) { InnerScoreDirector scoreDirector = stepScope.getScoreDirector(); scoreDirector.setAllChangesWillBeUndoneBeforeStepEnds(true); int moveIndex = 0; + if (resetWorkingSolution(stepScope)) { + return; + } for (var move : moveSelector) { var adaptedMove = new LegacyMoveAdapter<>(move); LocalSearchMoveScope moveScope = new LocalSearchMoveScope<>(stepScope, moveIndex, adaptedMove); @@ -115,6 +117,26 @@ public void decideNextStep(LocalSearchStepScope stepScope) { pickMove(stepScope); } + private boolean resetWorkingSolution(LocalSearchStepScope stepScope) { + var solverScope = stepScope.getPhaseScope().getSolverScope(); + var phaseScope = stepScope.getPhaseScope(); + if (perturbationMoveSelector != null && solverScope.isResetWorkingSolution()) { + logger.debug("Working solution reset, score ({})", solverScope.getBestScore()); + solverScope.setWorkingSolutionFromBestSolution(); + perturbationMoveSelector.phaseStarted(phaseScope); + var perturbationMove = perturbationMoveSelector.iterator().next(); + stepScope.setScore(perturbationMove); + + logger.debug("Generating a new perturbation with Ruin and Recreate: old score ({}), new score ({})", solverScope.getBestScore()); + moveSelector.phaseStarted(phaseScope); + return true; + } else if (solverScope.isResetWorkingSolution()) { + // Reset the flag + solverScope.cancelResetWorkingSolution(); + } + return false; + } + @SuppressWarnings("unchecked") protected > void doMove(LocalSearchMoveScope moveScope) { InnerScoreDirector scoreDirector = moveScope.getScoreDirector(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java index 96bf426f10..194959de05 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java @@ -247,6 +247,10 @@ public void triggerResetWorkingSolution() { resetWorkingSolution = true; } + public void cancelResetWorkingSolution() { + resetWorkingSolution = false; + } + public boolean isResetWorkingSolution() { return resetWorkingSolution; } From b64132c77b6ac1b35c83759ae5ee909a0c788fd3 Mon Sep 17 00:00:00 2001 From: Fred Date: Tue, 21 Jan 2025 09:18:53 -0300 Subject: [PATCH 03/31] feat: improve reconfiguration strategy --- benchmark/src/main/resources/benchmark.xsd | 60 ++++++++++++ core/src/build/revapi-differences.json | 4 +- .../acceptor/LocalSearchAcceptorConfig.java | 88 ++++++++++++++--- .../localsearch/DefaultLocalSearchPhase.java | 8 +- .../DefaultLocalSearchPhaseFactory.java | 24 ++++- .../decider/LocalSearchDecider.java | 42 ++++----- .../decider/acceptor/AcceptorFactory.java | 4 +- .../ReconfigurableAbstractAcceptor.java | 28 +----- .../MoveSelectorPerturbationStrategy.java | 94 +++++++++++++++++++ .../perturbation/NoPerturbationStrategy.java | 48 ++++++++++ .../perturbation/PerturbationStrategy.java | 15 +++ .../impl/phase/scope/AbstractPhaseScope.java | 13 +++ .../core/impl/solver/scope/SolverScope.java | 15 --- core/src/main/resources/solver.xsd | 40 ++++++++ 14 files changed, 396 insertions(+), 87 deletions(-) create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/perturbation/MoveSelectorPerturbationStrategy.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/perturbation/NoPerturbationStrategy.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/perturbation/PerturbationStrategy.java diff --git a/benchmark/src/main/resources/benchmark.xsd b/benchmark/src/main/resources/benchmark.xsd index 6be5cecbc3..16488ecba1 100644 --- a/benchmark/src/main/resources/benchmark.xsd +++ b/benchmark/src/main/resources/benchmark.xsd @@ -2414,6 +2414,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/src/build/revapi-differences.json b/core/src/build/revapi-differences.json index 50fa5f9bf2..1f9f58326d 100644 --- a/core/src/build/revapi-differences.json +++ b/core/src/build/revapi-differences.json @@ -111,8 +111,8 @@ "annotationType": "jakarta.xml.bind.annotation.XmlType", "attribute": "propOrder", "oldValue": "{\"acceptorTypeList\", \"entityTabuSize\", \"entityTabuRatio\", \"fadingEntityTabuSize\", \"fadingEntityTabuRatio\", \"valueTabuSize\", \"valueTabuRatio\", \"fadingValueTabuSize\", \"fadingValueTabuRatio\", \"moveTabuSize\", \"fadingMoveTabuSize\", \"undoMoveTabuSize\", \"fadingUndoMoveTabuSize\", \"simulatedAnnealingStartingTemperature\", \"lateAcceptanceSize\", \"greatDelugeWaterLevelIncrementScore\", \"greatDelugeWaterLevelIncrementRatio\", \"stepCountingHillClimbingSize\", \"stepCountingHillClimbingType\"}", - "newValue": "{\"acceptorTypeList\", \"enableReconfiguration\", \"entityTabuSize\", \"entityTabuRatio\", \"fadingEntityTabuSize\", \"fadingEntityTabuRatio\", \"valueTabuSize\", \"valueTabuRatio\", \"fadingValueTabuSize\", \"fadingValueTabuRatio\", \"moveTabuSize\", \"fadingMoveTabuSize\", \"undoMoveTabuSize\", \"fadingUndoMoveTabuSize\", \"simulatedAnnealingStartingTemperature\", \"lateAcceptanceSize\", \"greatDelugeWaterLevelIncrementScore\", \"greatDelugeWaterLevelIncrementRatio\", \"stepCountingHillClimbingSize\", \"stepCountingHillClimbingType\"}", - "justification": "Enable acceptor reconfiguration" + "newValue": "{\"acceptorTypeList\", \"perturbationSelectorConfig\", \"maxPerturbationCount\", \"entityTabuSize\", \"entityTabuRatio\", \"fadingEntityTabuSize\", \"fadingEntityTabuRatio\", \"valueTabuSize\", \"valueTabuRatio\", \"fadingValueTabuSize\", \"fadingValueTabuRatio\", \"moveTabuSize\", \"fadingMoveTabuSize\", \"undoMoveTabuSize\", \"fadingUndoMoveTabuSize\", \"simulatedAnnealingStartingTemperature\", \"lateAcceptanceSize\", \"greatDelugeWaterLevelIncrementScore\", \"greatDelugeWaterLevelIncrementRatio\", \"stepCountingHillClimbingSize\", \"stepCountingHillClimbingType\"}", + "justification": "Add perturbation config" } ] } diff --git a/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/LocalSearchAcceptorConfig.java b/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/LocalSearchAcceptorConfig.java index f52833ad86..777e80c09e 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/LocalSearchAcceptorConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/LocalSearchAcceptorConfig.java @@ -4,9 +4,28 @@ import java.util.function.Consumer; import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlElements; import jakarta.xml.bind.annotation.XmlType; import ai.timefold.solver.core.config.AbstractConfig; +import ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig; +import ai.timefold.solver.core.config.heuristic.selector.move.composite.CartesianProductMoveSelectorConfig; +import ai.timefold.solver.core.config.heuristic.selector.move.composite.UnionMoveSelectorConfig; +import ai.timefold.solver.core.config.heuristic.selector.move.factory.MoveIteratorFactoryConfig; +import ai.timefold.solver.core.config.heuristic.selector.move.factory.MoveListFactoryConfig; +import ai.timefold.solver.core.config.heuristic.selector.move.generic.ChangeMoveSelectorConfig; +import ai.timefold.solver.core.config.heuristic.selector.move.generic.PillarChangeMoveSelectorConfig; +import ai.timefold.solver.core.config.heuristic.selector.move.generic.PillarSwapMoveSelectorConfig; +import ai.timefold.solver.core.config.heuristic.selector.move.generic.RuinRecreateMoveSelectorConfig; +import ai.timefold.solver.core.config.heuristic.selector.move.generic.SwapMoveSelectorConfig; +import ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.SubChainChangeMoveSelectorConfig; +import ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.SubChainSwapMoveSelectorConfig; +import ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.TailChainSwapMoveSelectorConfig; +import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListChangeMoveSelectorConfig; +import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListRuinRecreateMoveSelectorConfig; +import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListSwapMoveSelectorConfig; +import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.SubListChangeMoveSelectorConfig; +import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.SubListSwapMoveSelectorConfig; import ai.timefold.solver.core.config.localsearch.decider.acceptor.stepcountinghillclimbing.StepCountingHillClimbingType; import ai.timefold.solver.core.config.util.ConfigUtils; @@ -15,7 +34,8 @@ @XmlType(propOrder = { "acceptorTypeList", - "enableReconfiguration", + "perturbationSelectorConfig", + "maxPerturbationCount", "entityTabuSize", "entityTabuRatio", "fadingEntityTabuSize", @@ -39,7 +59,34 @@ public class LocalSearchAcceptorConfig extends AbstractConfig acceptorTypeList = null; - private Boolean enableReconfiguration = null; + @XmlElements(value = { + @XmlElement(name = CartesianProductMoveSelectorConfig.XML_ELEMENT_NAME, + type = CartesianProductMoveSelectorConfig.class), + @XmlElement(name = ChangeMoveSelectorConfig.XML_ELEMENT_NAME, type = ChangeMoveSelectorConfig.class), + @XmlElement(name = ListChangeMoveSelectorConfig.XML_ELEMENT_NAME, type = ListChangeMoveSelectorConfig.class), + @XmlElement(name = ListSwapMoveSelectorConfig.XML_ELEMENT_NAME, type = ListSwapMoveSelectorConfig.class), + @XmlElement(name = MoveIteratorFactoryConfig.XML_ELEMENT_NAME, type = MoveIteratorFactoryConfig.class), + @XmlElement(name = MoveListFactoryConfig.XML_ELEMENT_NAME, type = MoveListFactoryConfig.class), + @XmlElement(name = PillarChangeMoveSelectorConfig.XML_ELEMENT_NAME, + type = PillarChangeMoveSelectorConfig.class), + @XmlElement(name = PillarSwapMoveSelectorConfig.XML_ELEMENT_NAME, type = PillarSwapMoveSelectorConfig.class), + @XmlElement(name = RuinRecreateMoveSelectorConfig.XML_ELEMENT_NAME, + type = RuinRecreateMoveSelectorConfig.class), + @XmlElement(name = ListRuinRecreateMoveSelectorConfig.XML_ELEMENT_NAME, + type = ListRuinRecreateMoveSelectorConfig.class), + @XmlElement(name = SubChainChangeMoveSelectorConfig.XML_ELEMENT_NAME, + type = SubChainChangeMoveSelectorConfig.class), + @XmlElement(name = SubChainSwapMoveSelectorConfig.XML_ELEMENT_NAME, + type = SubChainSwapMoveSelectorConfig.class), + @XmlElement(name = SubListChangeMoveSelectorConfig.XML_ELEMENT_NAME, type = SubListChangeMoveSelectorConfig.class), + @XmlElement(name = SubListSwapMoveSelectorConfig.XML_ELEMENT_NAME, type = SubListSwapMoveSelectorConfig.class), + @XmlElement(name = SwapMoveSelectorConfig.XML_ELEMENT_NAME, type = SwapMoveSelectorConfig.class), + @XmlElement(name = TailChainSwapMoveSelectorConfig.XML_ELEMENT_NAME, + type = TailChainSwapMoveSelectorConfig.class), + @XmlElement(name = UnionMoveSelectorConfig.XML_ELEMENT_NAME, type = UnionMoveSelectorConfig.class) + }) + private MoveSelectorConfig perturbationSelectorConfig = null; + private Integer maxPerturbationCount = null; protected Integer entityTabuSize = null; protected Double entityTabuRatio = null; @@ -80,12 +127,20 @@ public void setAcceptorTypeList(@Nullable List acceptorTypeList) { this.acceptorTypeList = acceptorTypeList; } - public @Nullable Boolean getEnableReconfiguration() { - return enableReconfiguration; + public @Nullable MoveSelectorConfig getPerturbationSelectorConfig() { + return perturbationSelectorConfig; } - public void setEnableReconfiguration(@Nullable Boolean enableReconfiguration) { - this.enableReconfiguration = enableReconfiguration; + public void setPerturbationSelectorConfig(@Nullable MoveSelectorConfig perturbationSelectorConfig) { + this.perturbationSelectorConfig = perturbationSelectorConfig; + } + + public @Nullable Integer getMaxPerturbationCount() { + return maxPerturbationCount; + } + + public void setMaxPerturbationCount(@Nullable Integer maxPerturbationCount) { + this.maxPerturbationCount = maxPerturbationCount; } public @Nullable Integer getEntityTabuSize() { @@ -273,8 +328,14 @@ public void setStepCountingHillClimbingType(@Nullable StepCountingHillClimbingTy return this; } - public @NonNull LocalSearchAcceptorConfig withEnableReconfiguration(@NonNull Boolean enableReconfiguration) { - this.enableReconfiguration = enableReconfiguration; + public @NonNull LocalSearchAcceptorConfig + withPerturbationSelectorConfig(@NonNull MoveSelectorConfig perturbationSelectorConfig) { + this.perturbationSelectorConfig = perturbationSelectorConfig; + return this; + } + + public @NonNull LocalSearchAcceptorConfig withMaxPerturbationCount(@NonNull Integer maxPerturbationCount) { + this.maxPerturbationCount = maxPerturbationCount; return this; } @@ -382,8 +443,11 @@ public LocalSearchAcceptorConfig withFadingUndoMoveTabuSize(Integer fadingUndoMo } } } - enableReconfiguration = - ConfigUtils.inheritOverwritableProperty(enableReconfiguration, inheritedConfig.getEnableReconfiguration()); + perturbationSelectorConfig = + ConfigUtils.inheritOverwritableProperty(perturbationSelectorConfig, + inheritedConfig.getPerturbationSelectorConfig()); + maxPerturbationCount = + ConfigUtils.inheritOverwritableProperty(maxPerturbationCount, inheritedConfig.getMaxPerturbationCount()); entityTabuSize = ConfigUtils.inheritOverwritableProperty(entityTabuSize, inheritedConfig.getEntityTabuSize()); entityTabuRatio = ConfigUtils.inheritOverwritableProperty(entityTabuRatio, inheritedConfig.getEntityTabuRatio()); fadingEntityTabuSize = ConfigUtils.inheritOverwritableProperty(fadingEntityTabuSize, @@ -425,7 +489,9 @@ public LocalSearchAcceptorConfig withFadingUndoMoveTabuSize(Integer fadingUndoMo @Override public void visitReferencedClasses(@NonNull Consumer> classVisitor) { - // No referenced classes + if (perturbationSelectorConfig != null) { + perturbationSelectorConfig.visitReferencedClasses(classVisitor); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java index f45528f775..8bb5be4f62 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java @@ -90,11 +90,9 @@ public void solve(SolverScope solverScope) { // Although stepStarted has been called, stepEnded is not called for this step break; } - if (!solverScope.isResetWorkingSolution()) { - doStep(stepScope); - stepEnded(stepScope); - phaseScope.setLastCompletedStepScope(stepScope); - } + doStep(stepScope); + stepEnded(stepScope); + phaseScope.setLastCompletedStepScope(stepScope); } phaseEnded(phaseScope); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java index 22bd5d9986..e69f9d230f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java @@ -32,6 +32,9 @@ import ai.timefold.solver.core.impl.localsearch.decider.acceptor.AcceptorFactory; import ai.timefold.solver.core.impl.localsearch.decider.forager.LocalSearchForager; import ai.timefold.solver.core.impl.localsearch.decider.forager.LocalSearchForagerFactory; +import ai.timefold.solver.core.impl.localsearch.decider.perturbation.MoveSelectorPerturbationStrategy; +import ai.timefold.solver.core.impl.localsearch.decider.perturbation.NoPerturbationStrategy; +import ai.timefold.solver.core.impl.localsearch.decider.perturbation.PerturbationStrategy; import ai.timefold.solver.core.impl.phase.AbstractPhaseFactory; import ai.timefold.solver.core.impl.solver.recaller.BestSolutionRecaller; import ai.timefold.solver.core.impl.solver.termination.Termination; @@ -64,6 +67,7 @@ public LocalSearchPhase buildPhase(int phaseIndex, boolean lastInitia private LocalSearchDecider buildDecider(HeuristicConfigPolicy configPolicy, Termination termination) { var moveSelector = buildMoveSelector(configPolicy); + var reconfigurationStrategy = buildPerturbationStrategy(configPolicy); var acceptor = buildAcceptor(configPolicy); var forager = buildForager(configPolicy); if (moveSelector.isNeverEnding() && !forager.supportsNeverEndingMoveSelector()) { @@ -77,7 +81,8 @@ private LocalSearchDecider buildDecider(HeuristicConfigPolicy decider; if (moveThreadCount == null) { - decider = new LocalSearchDecider<>(configPolicy.getLogIndentation(), termination, moveSelector, acceptor, forager); + decider = new LocalSearchDecider<>(configPolicy.getLogIndentation(), termination, moveSelector, + reconfigurationStrategy, acceptor, forager); } else { decider = TimefoldSolverEnterpriseService.loadOrFail(TimefoldSolverEnterpriseService.Feature.MULTITHREADED_SOLVING) .buildLocalSearch(moveThreadCount, termination, moveSelector, acceptor, forager, environmentMode, @@ -195,6 +200,23 @@ protected MoveSelector buildMoveSelector(HeuristicConfigPolicy buildPerturbationStrategy(HeuristicConfigPolicy configPolicy) { + var acceptorConfig = phaseConfig.getAcceptorConfig(); + if (acceptorConfig != null) { + var reconfigurationSelectorConfig = acceptorConfig.getPerturbationSelectorConfig(); + if (reconfigurationSelectorConfig != null) { + configPolicy.ensurePreviewFeature(PreviewFeature.ACCEPTOR_RECONFIGURATION); + AbstractMoveSelectorFactory moveSelectorFactory = + MoveSelectorFactory.create(reconfigurationSelectorConfig); + var moveSelector = moveSelectorFactory.buildMoveSelector(configPolicy, SelectionCacheType.JUST_IN_TIME, + SelectionOrder.RANDOM, true); + var maxLevels = Objects.requireNonNullElse(acceptorConfig.getMaxPerturbationCount(), 3); + return new MoveSelectorPerturbationStrategy<>(moveSelector, maxLevels); + } + } + return new NoPerturbationStrategy<>(); + } + private UnionMoveSelectorConfig determineDefaultMoveSelectorConfig(HeuristicConfigPolicy configPolicy) { var solutionDescriptor = configPolicy.getSolutionDescriptor(); var basicVariableDescriptorList = solutionDescriptor.getEntityDescriptors().stream() diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java index e38d4973e6..48ccf96d19 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java @@ -3,9 +3,11 @@ import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.impl.heuristic.move.LegacyMoveAdapter; +import ai.timefold.solver.core.impl.heuristic.move.NoChangeMove; import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.Acceptor; import ai.timefold.solver.core.impl.localsearch.decider.forager.LocalSearchForager; +import ai.timefold.solver.core.impl.localsearch.decider.perturbation.PerturbationStrategy; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; @@ -29,7 +31,7 @@ public class LocalSearchDecider { protected final String logIndentation; protected final Termination termination; protected final MoveSelector moveSelector; - protected final MoveSelector perturbationMoveSelector; + protected final PerturbationStrategy perturbationStrategy; protected final Acceptor acceptor; protected final LocalSearchForager forager; @@ -37,12 +39,12 @@ public class LocalSearchDecider { protected boolean assertExpectedUndoMoveScore = false; public LocalSearchDecider(String logIndentation, Termination termination, - MoveSelector moveSelector, MoveSelector perturbationMoveSelector, + MoveSelector moveSelector, PerturbationStrategy perturbationStrategy, Acceptor acceptor, LocalSearchForager forager) { this.logIndentation = logIndentation; this.termination = termination; this.moveSelector = moveSelector; - this.perturbationMoveSelector = perturbationMoveSelector; + this.perturbationStrategy = perturbationStrategy; this.acceptor = acceptor; this.forager = forager; } @@ -77,18 +79,21 @@ public void setAssertExpectedUndoMoveScore(boolean assertExpectedUndoMoveScore) public void solvingStarted(SolverScope solverScope) { moveSelector.solvingStarted(solverScope); + perturbationStrategy.solvingStarted(solverScope); acceptor.solvingStarted(solverScope); forager.solvingStarted(solverScope); } public void phaseStarted(LocalSearchPhaseScope phaseScope) { moveSelector.phaseStarted(phaseScope); + perturbationStrategy.phaseStarted(phaseScope); acceptor.phaseStarted(phaseScope); forager.phaseStarted(phaseScope); } public void stepStarted(LocalSearchStepScope stepScope) { moveSelector.stepStarted(stepScope); + perturbationStrategy.stepStarted(stepScope); acceptor.stepStarted(stepScope); forager.stepStarted(stepScope); } @@ -97,7 +102,13 @@ public void decideNextStep(LocalSearchStepScope stepScope) { InnerScoreDirector scoreDirector = stepScope.getScoreDirector(); scoreDirector.setAllChangesWillBeUndoneBeforeStepEnds(true); int moveIndex = 0; - if (resetWorkingSolution(stepScope)) { + if (perturbationStrategy.isTriggered(stepScope)) { + var reconfigurationScore = perturbationStrategy.apply(stepScope); + // Generate a dummy move; so the LS phase continues the process + var adaptedMove = new LegacyMoveAdapter(NoChangeMove.getInstance()); + stepScope.setStep(adaptedMove); + stepScope.setScore(reconfigurationScore); + scoreDirector.setAllChangesWillBeUndoneBeforeStepEnds(false); return; } for (var move : moveSelector) { @@ -117,26 +128,6 @@ public void decideNextStep(LocalSearchStepScope stepScope) { pickMove(stepScope); } - private boolean resetWorkingSolution(LocalSearchStepScope stepScope) { - var solverScope = stepScope.getPhaseScope().getSolverScope(); - var phaseScope = stepScope.getPhaseScope(); - if (perturbationMoveSelector != null && solverScope.isResetWorkingSolution()) { - logger.debug("Working solution reset, score ({})", solverScope.getBestScore()); - solverScope.setWorkingSolutionFromBestSolution(); - perturbationMoveSelector.phaseStarted(phaseScope); - var perturbationMove = perturbationMoveSelector.iterator().next(); - stepScope.setScore(perturbationMove); - - logger.debug("Generating a new perturbation with Ruin and Recreate: old score ({}), new score ({})", solverScope.getBestScore()); - moveSelector.phaseStarted(phaseScope); - return true; - } else if (solverScope.isResetWorkingSolution()) { - // Reset the flag - solverScope.cancelResetWorkingSolution(); - } - return false; - } - @SuppressWarnings("unchecked") protected > void doMove(LocalSearchMoveScope moveScope) { InnerScoreDirector scoreDirector = moveScope.getScoreDirector(); @@ -175,18 +166,21 @@ protected void pickMove(LocalSearchStepScope stepScope) { public void stepEnded(LocalSearchStepScope stepScope) { moveSelector.stepEnded(stepScope); + perturbationStrategy.stepEnded(stepScope); acceptor.stepEnded(stepScope); forager.stepEnded(stepScope); } public void phaseEnded(LocalSearchPhaseScope phaseScope) { moveSelector.phaseEnded(phaseScope); + perturbationStrategy.phaseEnded(phaseScope); acceptor.phaseEnded(phaseScope); forager.phaseEnded(phaseScope); } public void solvingEnded(SolverScope solverScope) { moveSelector.solvingEnded(solverScope); + perturbationStrategy.solvingEnded(solverScope); acceptor.solvingEnded(solverScope); forager.solvingEnded(solverScope); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java index 55c6aba5af..6210254b5d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java @@ -220,7 +220,7 @@ private Optional> buildMoveTabuAcceptor(HeuristicCon if (acceptorTypeListsContainsAcceptorType(AcceptorType.LATE_ACCEPTANCE) || (!acceptorTypeListsContainsAcceptorType(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE) && acceptorConfig.getLateAcceptanceSize() != null)) { - var enableReconfiguration = Objects.requireNonNullElse(acceptorConfig.getEnableReconfiguration(), false); + var enableReconfiguration = acceptorConfig.getPerturbationSelectorConfig() != null; if (enableReconfiguration) { configPolicy.ensurePreviewFeature(PreviewFeature.ACCEPTOR_RECONFIGURATION); } @@ -235,7 +235,7 @@ private Optional> buildMoveTabuAcceptor(HeuristicCon buildDiversifiedLateAcceptanceAcceptor(HeuristicConfigPolicy configPolicy) { if (acceptorTypeListsContainsAcceptorType(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE)) { configPolicy.ensurePreviewFeature(PreviewFeature.DIVERSIFIED_LATE_ACCEPTANCE); - var enableReconfiguration = Objects.requireNonNullElse(acceptorConfig.getEnableReconfiguration(), false); + var enableReconfiguration = acceptorConfig.getPerturbationSelectorConfig() != null; if (enableReconfiguration) { configPolicy.ensurePreviewFeature(PreviewFeature.ACCEPTOR_RECONFIGURATION); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAbstractAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAbstractAcceptor.java index 2c00304237..8da0116491 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAbstractAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAbstractAcceptor.java @@ -11,10 +11,7 @@ */ public abstract class ReconfigurableAbstractAcceptor extends AbstractAcceptor { - private static final int MAX_PERTURBATIONS = 2; private final ReconfigurationStrategy reconfigurationStrategy; - private int currentPerturbationCount; - private int remainingPerturbations; private final boolean enabled; protected ReconfigurableAbstractAcceptor(ReconfigurationStrategy reconfigurationStrategy, boolean enabled) { @@ -25,7 +22,6 @@ protected ReconfigurableAbstractAcceptor(ReconfigurationStrategy reco @Override public void phaseStarted(LocalSearchPhaseScope phaseScope) { super.phaseStarted(phaseScope); - init(); reconfigurationStrategy.phaseStarted(phaseScope); } @@ -50,39 +46,17 @@ public void stepEnded(LocalSearchStepScope stepScope) { @Override @SuppressWarnings("unchecked") public boolean isAccepted(LocalSearchMoveScope moveScope) { - if (enabled && remainingPerturbations > 0) { - remainingPerturbations--; - if (remainingPerturbations == 0) { - reconfigurationStrategy.reset(); - reset(moveScope.getScore()); - } - return true; - } if (enabled && reconfigurationStrategy.needReconfiguration(moveScope)) { - applyPerturbation(moveScope); + moveScope.getStepScope().getPhaseScope().triggerReconfiguration(); return true; } var accepted = evaluate(moveScope); if (enabled && moveScope.getScore().compareTo(moveScope.getStepScope().getPhaseScope().getBestScore()) > 0) { - init(); reconfigurationStrategy.reset(); } return accepted; } - private void applyPerturbation(LocalSearchMoveScope moveScope) { - remainingPerturbations = currentPerturbationCount; - if (currentPerturbationCount < MAX_PERTURBATIONS) { - currentPerturbationCount++; - } - moveScope.getStepScope().getPhaseScope().getSolverScope().triggerResetWorkingSolution(); - } - - private void init() { - this.currentPerturbationCount = 1; - this.remainingPerturbations = 0; - } - protected abstract boolean evaluate(LocalSearchMoveScope moveScope); protected abstract > void reset(Score_ score); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/perturbation/MoveSelectorPerturbationStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/perturbation/MoveSelectorPerturbationStrategy.java new file mode 100644 index 0000000000..437ba5a670 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/perturbation/MoveSelectorPerturbationStrategy.java @@ -0,0 +1,94 @@ +package ai.timefold.solver.core.impl.localsearch.decider.perturbation; + +import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.impl.heuristic.move.LegacyMoveAdapter; +import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector; +import ai.timefold.solver.core.impl.move.director.MoveDirector; +import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; +import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; +import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MoveSelectorPerturbationStrategy implements PerturbationStrategy { + + protected final Logger logger = LoggerFactory.getLogger(getClass()); + private final MoveSelector perturbationMoveSelector; + private final int maxLevels; + private Score currentBestScore; + private int level; + + public MoveSelectorPerturbationStrategy(MoveSelector perturbationMoveSelector, int maxLevels) { + this.perturbationMoveSelector = perturbationMoveSelector; + this.maxLevels = maxLevels; + } + + @Override + public > Score_ apply(AbstractStepScope stepScope) { + var phaseScope = stepScope.getPhaseScope(); + var solverScope = phaseScope.getSolverScope(); + logger.debug("Resetting working solution, score ({})", solverScope.getBestScore()); + solverScope.setWorkingSolutionFromBestSolution(); + var perturbationMoveIterator = perturbationMoveSelector.iterator(); + InnerScoreDirector scoreDirector = phaseScope.getScoreDirector(); + MoveDirector moveDirector = stepScope.getMoveDirector(); + for (int i = 0; i < level; i++) { + if (perturbationMoveIterator.hasNext()) { + var perturbation = new LegacyMoveAdapter<>(perturbationMoveIterator.next()); + if (!LegacyMoveAdapter.isDoable(moveDirector, perturbation)) { + continue; + } + perturbation.execute(moveDirector); + logger.debug("Generating a perturbation: Move ({})", perturbation); + } + } + var currentScore = scoreDirector.calculateScore(); + logger.debug("New solution generated: old score ({}), new score ({})", + phaseScope.getLastCompletedStepScope().getScore(), currentScore); + if (level + 1 < maxLevels) { + level++; + } + return currentScore; + } + + @Override + public void stepStarted(AbstractStepScope stepScope) { + this.perturbationMoveSelector.stepStarted(stepScope); + this.currentBestScore = stepScope.getPhaseScope().getBestScore(); + } + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void stepEnded(AbstractStepScope stepScope) { + this.perturbationMoveSelector.stepEnded(stepScope); + // Always cancel it at the end of the step as the perturbation is already applied + stepScope.getPhaseScope().cancelReconfiguration(); + if (((Score) stepScope.getScore()).compareTo(currentBestScore) > 0) { + // Reset it if the best solution is improved + this.level = 1; + } + } + + @Override + public void phaseStarted(AbstractPhaseScope phaseScope) { + this.level = 1; + perturbationMoveSelector.phaseStarted(phaseScope); + } + + @Override + public void phaseEnded(AbstractPhaseScope phaseScope) { + perturbationMoveSelector.phaseEnded(phaseScope); + } + + @Override + public void solvingStarted(SolverScope solverScope) { + perturbationMoveSelector.solvingStarted(solverScope); + } + + @Override + public void solvingEnded(SolverScope solverScope) { + perturbationMoveSelector.solvingEnded(solverScope); + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/perturbation/NoPerturbationStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/perturbation/NoPerturbationStrategy.java new file mode 100644 index 0000000000..cf48ae6355 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/perturbation/NoPerturbationStrategy.java @@ -0,0 +1,48 @@ +package ai.timefold.solver.core.impl.localsearch.decider.perturbation; + +import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; +import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; + +public class NoPerturbationStrategy implements PerturbationStrategy { + @Override + public > Score_ apply(AbstractStepScope stepScope) { + throw new IllegalStateException("Impossible state"); + } + + @Override + public boolean isTriggered(AbstractStepScope stepScope) { + return false; + } + + @Override + public void phaseStarted(AbstractPhaseScope phaseScope) { + // Do nothing + } + + @Override + public void stepStarted(AbstractStepScope stepScope) { + // Do nothing + } + + @Override + public void stepEnded(AbstractStepScope stepScope) { + // Do nothing + } + + @Override + public void phaseEnded(AbstractPhaseScope phaseScope) { + // Do nothing + } + + @Override + public void solvingStarted(SolverScope solverScope) { + // Do nothing + } + + @Override + public void solvingEnded(SolverScope solverScope) { + // Do nothing + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/perturbation/PerturbationStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/perturbation/PerturbationStrategy.java new file mode 100644 index 0000000000..8a08e84f14 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/perturbation/PerturbationStrategy.java @@ -0,0 +1,15 @@ +package ai.timefold.solver.core.impl.localsearch.decider.perturbation; + +import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleListener; +import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; + +public interface PerturbationStrategy extends PhaseLifecycleListener { + + > Score_ apply(AbstractStepScope stepScope); + + default boolean isTriggered(AbstractStepScope stepScope) { + return stepScope.getPhaseScope().isReconfigurationTriggered(); + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java index 02af1c1821..938f7b1f01 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java @@ -34,6 +34,7 @@ public abstract class AbstractPhaseScope { protected long childThreadsScoreCalculationCount = 0L; protected int bestSolutionStepIndex; + protected boolean reconfigurationTriggered = false; /** * As defined by #AbstractPhaseScope(SolverScope, int, boolean) @@ -247,6 +248,18 @@ public int getNextStepIndex() { return getLastCompletedStepScope().getStepIndex() + 1; } + public boolean isReconfigurationTriggered() { + return this.reconfigurationTriggered; + } + + public void triggerReconfiguration() { + this.reconfigurationTriggered = true; + } + + public void cancelReconfiguration() { + this.reconfigurationTriggered = false; + } + @Override public String toString() { return getClass().getSimpleName() + "(" + phaseIndex + ")"; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java index 194959de05..629f6120a9 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java @@ -80,8 +80,6 @@ private static Long readAtomicLongTimeMillis(AtomicLong atomicLong) { return value == -1 ? null : value; } - private boolean resetWorkingSolution = false; - // ************************************************************************ // Constructors and simple getters/setters // ************************************************************************ @@ -243,18 +241,6 @@ public Map getMoveEvaluationCountPerType() { return moveEvaluationCountPerTypeMap; } - public void triggerResetWorkingSolution() { - resetWorkingSolution = true; - } - - public void cancelResetWorkingSolution() { - resetWorkingSolution = false; - } - - public boolean isResetWorkingSolution() { - return resetWorkingSolution; - } - // ************************************************************************ // Calculated methods // ************************************************************************ @@ -325,7 +311,6 @@ public long getMoveEvaluationSpeed() { public void setWorkingSolutionFromBestSolution() { // The workingSolution must never be the same instance as the bestSolution. scoreDirector.setWorkingSolution(scoreDirector.cloneSolution(getBestSolution())); - resetWorkingSolution = false; } public SolverScope createChildThreadSolverScope(ChildThreadType childThreadType) { diff --git a/core/src/main/resources/solver.xsd b/core/src/main/resources/solver.xsd index 89ef9703b0..8f05d11f7b 100644 --- a/core/src/main/resources/solver.xsd +++ b/core/src/main/resources/solver.xsd @@ -1415,6 +1415,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From a5dacd69cc0824bef819bd987d140a6822209c4c Mon Sep 17 00:00:00 2001 From: Fred Date: Wed, 22 Jan 2025 10:13:36 -0300 Subject: [PATCH 04/31] feat: improve reconfiguration strategy --- .../localsearch/decider/LocalSearchDecider.java | 14 ++++++++------ .../MoveSelectorPerturbationStrategy.java | 15 ++++++++++----- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java index 48ccf96d19..8610abd5b8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java @@ -78,22 +78,22 @@ public void setAssertExpectedUndoMoveScore(boolean assertExpectedUndoMoveScore) // ************************************************************************ public void solvingStarted(SolverScope solverScope) { - moveSelector.solvingStarted(solverScope); perturbationStrategy.solvingStarted(solverScope); + moveSelector.solvingStarted(solverScope); acceptor.solvingStarted(solverScope); forager.solvingStarted(solverScope); } public void phaseStarted(LocalSearchPhaseScope phaseScope) { - moveSelector.phaseStarted(phaseScope); perturbationStrategy.phaseStarted(phaseScope); + moveSelector.phaseStarted(phaseScope); acceptor.phaseStarted(phaseScope); forager.phaseStarted(phaseScope); } public void stepStarted(LocalSearchStepScope stepScope) { - moveSelector.stepStarted(stepScope); perturbationStrategy.stepStarted(stepScope); + moveSelector.stepStarted(stepScope); acceptor.stepStarted(stepScope); forager.stepStarted(stepScope); } @@ -104,6 +104,8 @@ public void decideNextStep(LocalSearchStepScope stepScope) { int moveIndex = 0; if (perturbationStrategy.isTriggered(stepScope)) { var reconfigurationScore = perturbationStrategy.apply(stepScope); + // Need to update the cached entity list because the working solution was changed + moveSelector.phaseStarted(stepScope.getPhaseScope()); // Generate a dummy move; so the LS phase continues the process var adaptedMove = new LegacyMoveAdapter(NoChangeMove.getInstance()); stepScope.setStep(adaptedMove); @@ -165,22 +167,22 @@ protected void pickMove(LocalSearchStepScope stepScope) { } public void stepEnded(LocalSearchStepScope stepScope) { - moveSelector.stepEnded(stepScope); perturbationStrategy.stepEnded(stepScope); + moveSelector.stepEnded(stepScope); acceptor.stepEnded(stepScope); forager.stepEnded(stepScope); } public void phaseEnded(LocalSearchPhaseScope phaseScope) { - moveSelector.phaseEnded(phaseScope); perturbationStrategy.phaseEnded(phaseScope); + moveSelector.phaseEnded(phaseScope); acceptor.phaseEnded(phaseScope); forager.phaseEnded(phaseScope); } public void solvingEnded(SolverScope solverScope) { - moveSelector.solvingEnded(solverScope); perturbationStrategy.solvingEnded(solverScope); + moveSelector.solvingEnded(solverScope); acceptor.solvingEnded(solverScope); forager.solvingEnded(solverScope); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/perturbation/MoveSelectorPerturbationStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/perturbation/MoveSelectorPerturbationStrategy.java index 437ba5a670..d7cead2c23 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/perturbation/MoveSelectorPerturbationStrategy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/perturbation/MoveSelectorPerturbationStrategy.java @@ -28,9 +28,6 @@ public MoveSelectorPerturbationStrategy(MoveSelector perturbationMove @Override public > Score_ apply(AbstractStepScope stepScope) { var phaseScope = stepScope.getPhaseScope(); - var solverScope = phaseScope.getSolverScope(); - logger.debug("Resetting working solution, score ({})", solverScope.getBestScore()); - solverScope.setWorkingSolutionFromBestSolution(); var perturbationMoveIterator = perturbationMoveSelector.iterator(); InnerScoreDirector scoreDirector = phaseScope.getScoreDirector(); MoveDirector moveDirector = stepScope.getMoveDirector(); @@ -40,6 +37,7 @@ public > Score_ apply(AbstractStepScope if (!LegacyMoveAdapter.isDoable(moveDirector, perturbation)) { continue; } + // var perturbationRebased = perturbation.rebase(moveDirector); perturbation.execute(moveDirector); logger.debug("Generating a perturbation: Move ({})", perturbation); } @@ -50,11 +48,20 @@ public > Score_ apply(AbstractStepScope if (level + 1 < maxLevels) { level++; } + // Always cancel it at the end of the step as the perturbation is already applied + stepScope.getPhaseScope().cancelReconfiguration(); return currentScore; } @Override public void stepStarted(AbstractStepScope stepScope) { + if (isTriggered(stepScope)) { + var solverScope = stepScope.getPhaseScope().getSolverScope(); + logger.debug("Resetting working solution, score ({})", solverScope.getBestScore()); + solverScope.setWorkingSolutionFromBestSolution(); + // Need to update the cached entity list because the working solution was changed + perturbationMoveSelector.phaseStarted(stepScope.getPhaseScope()); + } this.perturbationMoveSelector.stepStarted(stepScope); this.currentBestScore = stepScope.getPhaseScope().getBestScore(); } @@ -63,8 +70,6 @@ public void stepStarted(AbstractStepScope stepScope) { @SuppressWarnings({ "rawtypes", "unchecked" }) public void stepEnded(AbstractStepScope stepScope) { this.perturbationMoveSelector.stepEnded(stepScope); - // Always cancel it at the end of the step as the perturbation is already applied - stepScope.getPhaseScope().cancelReconfiguration(); if (((Score) stepScope.getScore()).compareTo(currentBestScore) > 0) { // Reset it if the best solution is improved this.level = 1; From e592dc367d53b64de386d0b2d487d4e4326f58c0 Mon Sep 17 00:00:00 2001 From: Fred Date: Wed, 22 Jan 2025 15:09:43 -0300 Subject: [PATCH 05/31] feat: new reconfiguration strategy --- benchmark/src/main/resources/benchmark.xsd | 74 ++------------ core/src/build/revapi-differences.json | 4 +- .../acceptor/LocalSearchAcceptorConfig.java | 87 +++------------- .../core/config/solver/PreviewFeature.java | 2 +- .../DefaultLocalSearchPhaseFactory.java | 29 +++--- .../decider/LocalSearchDecider.java | 26 +++-- .../decider/acceptor/AcceptorFactory.java | 23 +++-- ...eptor.java => ReconfigurableAcceptor.java} | 32 +++--- .../DiversifiedLateAcceptanceAcceptor.java | 10 +- .../LateAcceptanceAcceptor.java | 10 +- .../acceptor/restart/NoOpRestartStrategy.java | 49 +++++++++ .../RestartStrategy.java} | 6 +- ...improvedTimeGeometricRestartStrategy.java} | 51 ++++++---- .../MoveSelectorPerturbationStrategy.java | 99 ------------------- .../NoOpReconfigurationStrategy.java} | 4 +- .../ReconfigurationStrategy.java} | 5 +- ...reBestSolutionReconfigurationStrategy.java | 66 +++++++++++++ core/src/main/resources/solver.xsd | 50 +--------- .../decider/acceptor/AcceptorFactoryTest.java | 28 ++++-- ...DiversifiedLateAcceptanceAcceptorTest.java | 41 +++++++- .../LateAcceptanceAcceptorTest.java | 45 ++++++++- .../restart/NoOpRestartStrategyTest.java | 15 +++ ...rovedTimeGeometricRestartStrategyTest.java | 94 ++++++++++++++++++ .../ReconfigurationStrategyTest.java | 45 +++++++++ 24 files changed, 508 insertions(+), 387 deletions(-) rename core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/{ReconfigurableAbstractAcceptor.java => ReconfigurableAcceptor.java} (59%) create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/NoOpRestartStrategy.java rename core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/{reconfiguration/ReconfigurationStrategy.java => restart/RestartStrategy.java} (55%) rename core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/{reconfiguration/GeometricUnimprovedSolutionReconfigurationStrategy.java => restart/UnimprovedTimeGeometricRestartStrategy.java} (57%) delete mode 100644 core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/perturbation/MoveSelectorPerturbationStrategy.java rename core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/{perturbation/NoPerturbationStrategy.java => reconfiguration/NoOpReconfigurationStrategy.java} (87%) rename core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/{perturbation/PerturbationStrategy.java => reconfiguration/ReconfigurationStrategy.java} (62%) create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionReconfigurationStrategy.java create mode 100644 core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/NoOpRestartStrategyTest.java create mode 100644 core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeGeometricRestartStrategyTest.java create mode 100644 core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategyTest.java diff --git a/benchmark/src/main/resources/benchmark.xsd b/benchmark/src/main/resources/benchmark.xsd index 16488ecba1..d552322ad2 100644 --- a/benchmark/src/main/resources/benchmark.xsd +++ b/benchmark/src/main/resources/benchmark.xsd @@ -2414,69 +2414,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - @@ -2664,13 +2604,13 @@ - - - - - + + + + + - + diff --git a/core/src/build/revapi-differences.json b/core/src/build/revapi-differences.json index 1f9f58326d..69fe8a6f74 100644 --- a/core/src/build/revapi-differences.json +++ b/core/src/build/revapi-differences.json @@ -111,8 +111,8 @@ "annotationType": "jakarta.xml.bind.annotation.XmlType", "attribute": "propOrder", "oldValue": "{\"acceptorTypeList\", \"entityTabuSize\", \"entityTabuRatio\", \"fadingEntityTabuSize\", \"fadingEntityTabuRatio\", \"valueTabuSize\", \"valueTabuRatio\", \"fadingValueTabuSize\", \"fadingValueTabuRatio\", \"moveTabuSize\", \"fadingMoveTabuSize\", \"undoMoveTabuSize\", \"fadingUndoMoveTabuSize\", \"simulatedAnnealingStartingTemperature\", \"lateAcceptanceSize\", \"greatDelugeWaterLevelIncrementScore\", \"greatDelugeWaterLevelIncrementRatio\", \"stepCountingHillClimbingSize\", \"stepCountingHillClimbingType\"}", - "newValue": "{\"acceptorTypeList\", \"perturbationSelectorConfig\", \"maxPerturbationCount\", \"entityTabuSize\", \"entityTabuRatio\", \"fadingEntityTabuSize\", \"fadingEntityTabuRatio\", \"valueTabuSize\", \"valueTabuRatio\", \"fadingValueTabuSize\", \"fadingValueTabuRatio\", \"moveTabuSize\", \"fadingMoveTabuSize\", \"undoMoveTabuSize\", \"fadingUndoMoveTabuSize\", \"simulatedAnnealingStartingTemperature\", \"lateAcceptanceSize\", \"greatDelugeWaterLevelIncrementScore\", \"greatDelugeWaterLevelIncrementRatio\", \"stepCountingHillClimbingSize\", \"stepCountingHillClimbingType\"}", - "justification": "Add perturbation config" + "newValue": "{\"acceptorTypeList\", \"enableReconfiguration\", \"entityTabuSize\", \"entityTabuRatio\", \"fadingEntityTabuSize\", \"fadingEntityTabuRatio\", \"valueTabuSize\", \"valueTabuRatio\", \"fadingValueTabuSize\", \"fadingValueTabuRatio\", \"moveTabuSize\", \"fadingMoveTabuSize\", \"undoMoveTabuSize\", \"fadingUndoMoveTabuSize\", \"simulatedAnnealingStartingTemperature\", \"lateAcceptanceSize\", \"greatDelugeWaterLevelIncrementScore\", \"greatDelugeWaterLevelIncrementRatio\", \"stepCountingHillClimbingSize\", \"stepCountingHillClimbingType\"}", + "justification": "Add the acceptor reconfiguration setting" } ] } diff --git a/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/LocalSearchAcceptorConfig.java b/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/LocalSearchAcceptorConfig.java index 777e80c09e..8b53d0ad4d 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/LocalSearchAcceptorConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/LocalSearchAcceptorConfig.java @@ -4,28 +4,9 @@ import java.util.function.Consumer; import jakarta.xml.bind.annotation.XmlElement; -import jakarta.xml.bind.annotation.XmlElements; import jakarta.xml.bind.annotation.XmlType; import ai.timefold.solver.core.config.AbstractConfig; -import ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig; -import ai.timefold.solver.core.config.heuristic.selector.move.composite.CartesianProductMoveSelectorConfig; -import ai.timefold.solver.core.config.heuristic.selector.move.composite.UnionMoveSelectorConfig; -import ai.timefold.solver.core.config.heuristic.selector.move.factory.MoveIteratorFactoryConfig; -import ai.timefold.solver.core.config.heuristic.selector.move.factory.MoveListFactoryConfig; -import ai.timefold.solver.core.config.heuristic.selector.move.generic.ChangeMoveSelectorConfig; -import ai.timefold.solver.core.config.heuristic.selector.move.generic.PillarChangeMoveSelectorConfig; -import ai.timefold.solver.core.config.heuristic.selector.move.generic.PillarSwapMoveSelectorConfig; -import ai.timefold.solver.core.config.heuristic.selector.move.generic.RuinRecreateMoveSelectorConfig; -import ai.timefold.solver.core.config.heuristic.selector.move.generic.SwapMoveSelectorConfig; -import ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.SubChainChangeMoveSelectorConfig; -import ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.SubChainSwapMoveSelectorConfig; -import ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.TailChainSwapMoveSelectorConfig; -import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListChangeMoveSelectorConfig; -import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListRuinRecreateMoveSelectorConfig; -import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListSwapMoveSelectorConfig; -import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.SubListChangeMoveSelectorConfig; -import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.SubListSwapMoveSelectorConfig; import ai.timefold.solver.core.config.localsearch.decider.acceptor.stepcountinghillclimbing.StepCountingHillClimbingType; import ai.timefold.solver.core.config.util.ConfigUtils; @@ -34,8 +15,7 @@ @XmlType(propOrder = { "acceptorTypeList", - "perturbationSelectorConfig", - "maxPerturbationCount", + "enableReconfiguration", "entityTabuSize", "entityTabuRatio", "fadingEntityTabuSize", @@ -59,34 +39,7 @@ public class LocalSearchAcceptorConfig extends AbstractConfig acceptorTypeList = null; - @XmlElements(value = { - @XmlElement(name = CartesianProductMoveSelectorConfig.XML_ELEMENT_NAME, - type = CartesianProductMoveSelectorConfig.class), - @XmlElement(name = ChangeMoveSelectorConfig.XML_ELEMENT_NAME, type = ChangeMoveSelectorConfig.class), - @XmlElement(name = ListChangeMoveSelectorConfig.XML_ELEMENT_NAME, type = ListChangeMoveSelectorConfig.class), - @XmlElement(name = ListSwapMoveSelectorConfig.XML_ELEMENT_NAME, type = ListSwapMoveSelectorConfig.class), - @XmlElement(name = MoveIteratorFactoryConfig.XML_ELEMENT_NAME, type = MoveIteratorFactoryConfig.class), - @XmlElement(name = MoveListFactoryConfig.XML_ELEMENT_NAME, type = MoveListFactoryConfig.class), - @XmlElement(name = PillarChangeMoveSelectorConfig.XML_ELEMENT_NAME, - type = PillarChangeMoveSelectorConfig.class), - @XmlElement(name = PillarSwapMoveSelectorConfig.XML_ELEMENT_NAME, type = PillarSwapMoveSelectorConfig.class), - @XmlElement(name = RuinRecreateMoveSelectorConfig.XML_ELEMENT_NAME, - type = RuinRecreateMoveSelectorConfig.class), - @XmlElement(name = ListRuinRecreateMoveSelectorConfig.XML_ELEMENT_NAME, - type = ListRuinRecreateMoveSelectorConfig.class), - @XmlElement(name = SubChainChangeMoveSelectorConfig.XML_ELEMENT_NAME, - type = SubChainChangeMoveSelectorConfig.class), - @XmlElement(name = SubChainSwapMoveSelectorConfig.XML_ELEMENT_NAME, - type = SubChainSwapMoveSelectorConfig.class), - @XmlElement(name = SubListChangeMoveSelectorConfig.XML_ELEMENT_NAME, type = SubListChangeMoveSelectorConfig.class), - @XmlElement(name = SubListSwapMoveSelectorConfig.XML_ELEMENT_NAME, type = SubListSwapMoveSelectorConfig.class), - @XmlElement(name = SwapMoveSelectorConfig.XML_ELEMENT_NAME, type = SwapMoveSelectorConfig.class), - @XmlElement(name = TailChainSwapMoveSelectorConfig.XML_ELEMENT_NAME, - type = TailChainSwapMoveSelectorConfig.class), - @XmlElement(name = UnionMoveSelectorConfig.XML_ELEMENT_NAME, type = UnionMoveSelectorConfig.class) - }) - private MoveSelectorConfig perturbationSelectorConfig = null; - private Integer maxPerturbationCount = null; + private Boolean enableReconfiguration; protected Integer entityTabuSize = null; protected Double entityTabuRatio = null; @@ -127,20 +80,12 @@ public void setAcceptorTypeList(@Nullable List acceptorTypeList) { this.acceptorTypeList = acceptorTypeList; } - public @Nullable MoveSelectorConfig getPerturbationSelectorConfig() { - return perturbationSelectorConfig; + public @Nullable Boolean getEnableReconfiguration() { + return enableReconfiguration; } - public void setPerturbationSelectorConfig(@Nullable MoveSelectorConfig perturbationSelectorConfig) { - this.perturbationSelectorConfig = perturbationSelectorConfig; - } - - public @Nullable Integer getMaxPerturbationCount() { - return maxPerturbationCount; - } - - public void setMaxPerturbationCount(@Nullable Integer maxPerturbationCount) { - this.maxPerturbationCount = maxPerturbationCount; + public void setEnableReconfiguration(@Nullable Boolean enableReconfiguration) { + this.enableReconfiguration = enableReconfiguration; } public @Nullable Integer getEntityTabuSize() { @@ -329,13 +274,8 @@ public void setStepCountingHillClimbingType(@Nullable StepCountingHillClimbingTy } public @NonNull LocalSearchAcceptorConfig - withPerturbationSelectorConfig(@NonNull MoveSelectorConfig perturbationSelectorConfig) { - this.perturbationSelectorConfig = perturbationSelectorConfig; - return this; - } - - public @NonNull LocalSearchAcceptorConfig withMaxPerturbationCount(@NonNull Integer maxPerturbationCount) { - this.maxPerturbationCount = maxPerturbationCount; + withEnableReconfiguration(@NonNull Boolean enableReconfiguration) { + this.enableReconfiguration = enableReconfiguration; return this; } @@ -443,11 +383,8 @@ public LocalSearchAcceptorConfig withFadingUndoMoveTabuSize(Integer fadingUndoMo } } } - perturbationSelectorConfig = - ConfigUtils.inheritOverwritableProperty(perturbationSelectorConfig, - inheritedConfig.getPerturbationSelectorConfig()); - maxPerturbationCount = - ConfigUtils.inheritOverwritableProperty(maxPerturbationCount, inheritedConfig.getMaxPerturbationCount()); + enableReconfiguration = + ConfigUtils.inheritOverwritableProperty(enableReconfiguration, inheritedConfig.getEnableReconfiguration()); entityTabuSize = ConfigUtils.inheritOverwritableProperty(entityTabuSize, inheritedConfig.getEntityTabuSize()); entityTabuRatio = ConfigUtils.inheritOverwritableProperty(entityTabuRatio, inheritedConfig.getEntityTabuRatio()); fadingEntityTabuSize = ConfigUtils.inheritOverwritableProperty(fadingEntityTabuSize, @@ -489,9 +426,7 @@ public LocalSearchAcceptorConfig withFadingUndoMoveTabuSize(Integer fadingUndoMo @Override public void visitReferencedClasses(@NonNull Consumer> classVisitor) { - if (perturbationSelectorConfig != null) { - perturbationSelectorConfig.visitReferencedClasses(classVisitor); - } + // No referenced classes } } diff --git a/core/src/main/java/ai/timefold/solver/core/config/solver/PreviewFeature.java b/core/src/main/java/ai/timefold/solver/core/config/solver/PreviewFeature.java index 86b4732b0b..0168a68829 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/solver/PreviewFeature.java +++ b/core/src/main/java/ai/timefold/solver/core/config/solver/PreviewFeature.java @@ -2,7 +2,7 @@ public enum PreviewFeature { DIVERSIFIED_LATE_ACCEPTANCE, - ACCEPTOR_RECONFIGURATION, + RECONFIGURATION, PLANNING_SOLUTION_DIFF } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java index e69f9d230f..446e8738ad 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java @@ -32,9 +32,9 @@ import ai.timefold.solver.core.impl.localsearch.decider.acceptor.AcceptorFactory; import ai.timefold.solver.core.impl.localsearch.decider.forager.LocalSearchForager; import ai.timefold.solver.core.impl.localsearch.decider.forager.LocalSearchForagerFactory; -import ai.timefold.solver.core.impl.localsearch.decider.perturbation.MoveSelectorPerturbationStrategy; -import ai.timefold.solver.core.impl.localsearch.decider.perturbation.NoPerturbationStrategy; -import ai.timefold.solver.core.impl.localsearch.decider.perturbation.PerturbationStrategy; +import ai.timefold.solver.core.impl.localsearch.decider.reconfiguration.NoOpReconfigurationStrategy; +import ai.timefold.solver.core.impl.localsearch.decider.reconfiguration.ReconfigurationStrategy; +import ai.timefold.solver.core.impl.localsearch.decider.reconfiguration.RestoreBestSolutionReconfigurationStrategy; import ai.timefold.solver.core.impl.phase.AbstractPhaseFactory; import ai.timefold.solver.core.impl.solver.recaller.BestSolutionRecaller; import ai.timefold.solver.core.impl.solver.termination.Termination; @@ -67,7 +67,7 @@ public LocalSearchPhase buildPhase(int phaseIndex, boolean lastInitia private LocalSearchDecider buildDecider(HeuristicConfigPolicy configPolicy, Termination termination) { var moveSelector = buildMoveSelector(configPolicy); - var reconfigurationStrategy = buildPerturbationStrategy(configPolicy); + var reconfigurationStrategy = buildReconfigurationStrategy(configPolicy); var acceptor = buildAcceptor(configPolicy); var forager = buildForager(configPolicy); if (moveSelector.isNeverEnding() && !forager.supportsNeverEndingMoveSelector()) { @@ -94,6 +94,9 @@ private LocalSearchDecider buildDecider(HeuristicConfigPolicy castReconfigurationStrategy) { + castReconfigurationStrategy.setDecider(decider); + } return decider; } @@ -200,21 +203,17 @@ protected MoveSelector buildMoveSelector(HeuristicConfigPolicy buildPerturbationStrategy(HeuristicConfigPolicy configPolicy) { + private ReconfigurationStrategy buildReconfigurationStrategy(HeuristicConfigPolicy configPolicy) { var acceptorConfig = phaseConfig.getAcceptorConfig(); if (acceptorConfig != null) { - var reconfigurationSelectorConfig = acceptorConfig.getPerturbationSelectorConfig(); - if (reconfigurationSelectorConfig != null) { - configPolicy.ensurePreviewFeature(PreviewFeature.ACCEPTOR_RECONFIGURATION); - AbstractMoveSelectorFactory moveSelectorFactory = - MoveSelectorFactory.create(reconfigurationSelectorConfig); - var moveSelector = moveSelectorFactory.buildMoveSelector(configPolicy, SelectionCacheType.JUST_IN_TIME, - SelectionOrder.RANDOM, true); - var maxLevels = Objects.requireNonNullElse(acceptorConfig.getMaxPerturbationCount(), 3); - return new MoveSelectorPerturbationStrategy<>(moveSelector, maxLevels); + var enableReconfiguration = + acceptorConfig.getEnableReconfiguration() != null && acceptorConfig.getEnableReconfiguration(); + if (enableReconfiguration) { + configPolicy.ensurePreviewFeature(PreviewFeature.RECONFIGURATION); + return new RestoreBestSolutionReconfigurationStrategy<>(); } } - return new NoPerturbationStrategy<>(); + return new NoOpReconfigurationStrategy<>(); } private UnionMoveSelectorConfig determineDefaultMoveSelectorConfig(HeuristicConfigPolicy configPolicy) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java index 8610abd5b8..4fb4911086 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java @@ -7,7 +7,7 @@ import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.Acceptor; import ai.timefold.solver.core.impl.localsearch.decider.forager.LocalSearchForager; -import ai.timefold.solver.core.impl.localsearch.decider.perturbation.PerturbationStrategy; +import ai.timefold.solver.core.impl.localsearch.decider.reconfiguration.ReconfigurationStrategy; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; @@ -31,7 +31,7 @@ public class LocalSearchDecider { protected final String logIndentation; protected final Termination termination; protected final MoveSelector moveSelector; - protected final PerturbationStrategy perturbationStrategy; + protected final ReconfigurationStrategy reconfigurationStrategy; protected final Acceptor acceptor; protected final LocalSearchForager forager; @@ -39,12 +39,12 @@ public class LocalSearchDecider { protected boolean assertExpectedUndoMoveScore = false; public LocalSearchDecider(String logIndentation, Termination termination, - MoveSelector moveSelector, PerturbationStrategy perturbationStrategy, + MoveSelector moveSelector, ReconfigurationStrategy reconfigurationStrategy, Acceptor acceptor, LocalSearchForager forager) { this.logIndentation = logIndentation; this.termination = termination; this.moveSelector = moveSelector; - this.perturbationStrategy = perturbationStrategy; + this.reconfigurationStrategy = reconfigurationStrategy; this.acceptor = acceptor; this.forager = forager; } @@ -78,21 +78,21 @@ public void setAssertExpectedUndoMoveScore(boolean assertExpectedUndoMoveScore) // ************************************************************************ public void solvingStarted(SolverScope solverScope) { - perturbationStrategy.solvingStarted(solverScope); + reconfigurationStrategy.solvingStarted(solverScope); moveSelector.solvingStarted(solverScope); acceptor.solvingStarted(solverScope); forager.solvingStarted(solverScope); } public void phaseStarted(LocalSearchPhaseScope phaseScope) { - perturbationStrategy.phaseStarted(phaseScope); + reconfigurationStrategy.phaseStarted(phaseScope); moveSelector.phaseStarted(phaseScope); acceptor.phaseStarted(phaseScope); forager.phaseStarted(phaseScope); } public void stepStarted(LocalSearchStepScope stepScope) { - perturbationStrategy.stepStarted(stepScope); + reconfigurationStrategy.stepStarted(stepScope); moveSelector.stepStarted(stepScope); acceptor.stepStarted(stepScope); forager.stepStarted(stepScope); @@ -102,10 +102,8 @@ public void decideNextStep(LocalSearchStepScope stepScope) { InnerScoreDirector scoreDirector = stepScope.getScoreDirector(); scoreDirector.setAllChangesWillBeUndoneBeforeStepEnds(true); int moveIndex = 0; - if (perturbationStrategy.isTriggered(stepScope)) { - var reconfigurationScore = perturbationStrategy.apply(stepScope); - // Need to update the cached entity list because the working solution was changed - moveSelector.phaseStarted(stepScope.getPhaseScope()); + if (reconfigurationStrategy.isTriggered(stepScope)) { + var reconfigurationScore = reconfigurationStrategy.apply(stepScope); // Generate a dummy move; so the LS phase continues the process var adaptedMove = new LegacyMoveAdapter(NoChangeMove.getInstance()); stepScope.setStep(adaptedMove); @@ -167,21 +165,21 @@ protected void pickMove(LocalSearchStepScope stepScope) { } public void stepEnded(LocalSearchStepScope stepScope) { - perturbationStrategy.stepEnded(stepScope); + reconfigurationStrategy.stepEnded(stepScope); moveSelector.stepEnded(stepScope); acceptor.stepEnded(stepScope); forager.stepEnded(stepScope); } public void phaseEnded(LocalSearchPhaseScope phaseScope) { - perturbationStrategy.phaseEnded(phaseScope); + reconfigurationStrategy.phaseEnded(phaseScope); moveSelector.phaseEnded(phaseScope); acceptor.phaseEnded(phaseScope); forager.phaseEnded(phaseScope); } public void solvingEnded(SolverScope solverScope) { - perturbationStrategy.solvingEnded(solverScope); + reconfigurationStrategy.solvingEnded(solverScope); moveSelector.solvingEnded(solverScope); acceptor.solvingEnded(solverScope); forager.solvingEnded(solverScope); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java index 6210254b5d..87f29f516f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java @@ -15,6 +15,9 @@ import ai.timefold.solver.core.impl.localsearch.decider.acceptor.hillclimbing.HillClimbingAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance.DiversifiedLateAcceptanceAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance.LateAcceptanceAcceptor; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.NoOpRestartStrategy; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.RestartStrategy; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.UnimprovedTimeGeometricRestartStrategy; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.simulatedannealing.SimulatedAnnealingAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stepcountinghillclimbing.StepCountingHillClimbingAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.tabu.EntityTabuAcceptor; @@ -46,7 +49,7 @@ public Acceptor buildAcceptor(HeuristicConfigPolicy config buildValueTabuAcceptor(configPolicy), buildMoveTabuAcceptor(configPolicy), buildSimulatedAnnealingAcceptor(configPolicy), - buildLateAcceptanceAcceptor(configPolicy), + buildLateAcceptanceAcceptor(), buildDiversifiedLateAcceptanceAcceptor(configPolicy), buildGreatDelugeAcceptor(configPolicy)) .filter(Optional::isPresent) @@ -216,15 +219,17 @@ private Optional> buildMoveTabuAcceptor(HeuristicCon } private Optional> - buildLateAcceptanceAcceptor(HeuristicConfigPolicy configPolicy) { + buildLateAcceptanceAcceptor() { if (acceptorTypeListsContainsAcceptorType(AcceptorType.LATE_ACCEPTANCE) || (!acceptorTypeListsContainsAcceptorType(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE) && acceptorConfig.getLateAcceptanceSize() != null)) { - var enableReconfiguration = acceptorConfig.getPerturbationSelectorConfig() != null; + RestartStrategy restartStrategy = new NoOpRestartStrategy<>(); + var enableReconfiguration = + acceptorConfig.getEnableReconfiguration() != null && acceptorConfig.getEnableReconfiguration(); if (enableReconfiguration) { - configPolicy.ensurePreviewFeature(PreviewFeature.ACCEPTOR_RECONFIGURATION); + restartStrategy = new UnimprovedTimeGeometricRestartStrategy<>(); } - var acceptor = new LateAcceptanceAcceptor(enableReconfiguration); + var acceptor = new LateAcceptanceAcceptor<>(enableReconfiguration, restartStrategy); acceptor.setLateAcceptanceSize(Objects.requireNonNullElse(acceptorConfig.getLateAcceptanceSize(), 400)); return Optional.of(acceptor); } @@ -235,11 +240,13 @@ private Optional> buildMoveTabuAcceptor(HeuristicCon buildDiversifiedLateAcceptanceAcceptor(HeuristicConfigPolicy configPolicy) { if (acceptorTypeListsContainsAcceptorType(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE)) { configPolicy.ensurePreviewFeature(PreviewFeature.DIVERSIFIED_LATE_ACCEPTANCE); - var enableReconfiguration = acceptorConfig.getPerturbationSelectorConfig() != null; + RestartStrategy restartStrategy = new NoOpRestartStrategy<>(); + var enableReconfiguration = + acceptorConfig.getEnableReconfiguration() != null && acceptorConfig.getEnableReconfiguration(); if (enableReconfiguration) { - configPolicy.ensurePreviewFeature(PreviewFeature.ACCEPTOR_RECONFIGURATION); + restartStrategy = new UnimprovedTimeGeometricRestartStrategy<>(); } - var acceptor = new DiversifiedLateAcceptanceAcceptor(enableReconfiguration); + var acceptor = new DiversifiedLateAcceptanceAcceptor<>(enableReconfiguration, restartStrategy); acceptor.setLateAcceptanceSize(Objects.requireNonNullElse(acceptorConfig.getLateAcceptanceSize(), 5)); return Optional.of(acceptor); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAbstractAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java similarity index 59% rename from core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAbstractAcceptor.java rename to core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java index 8da0116491..71c669647c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAbstractAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java @@ -1,58 +1,64 @@ package ai.timefold.solver.core.impl.localsearch.decider.acceptor; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.reconfiguration.ReconfigurationStrategy; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.RestartStrategy; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; /** - * Base class designed to provide a reconfiguration strategy for an acceptor implementation. + * Base class designed to analyze whether the solving process needs to be restarted. + * Additionally, it also calls a reconfiguration logic as a result of restarting the solving process. */ -public abstract class ReconfigurableAbstractAcceptor extends AbstractAcceptor { +public abstract class ReconfigurableAcceptor extends AbstractAcceptor { - private final ReconfigurationStrategy reconfigurationStrategy; + private final RestartStrategy restartStrategy; private final boolean enabled; - protected ReconfigurableAbstractAcceptor(ReconfigurationStrategy reconfigurationStrategy, boolean enabled) { - this.reconfigurationStrategy = reconfigurationStrategy; + protected ReconfigurableAcceptor(boolean enabled, RestartStrategy restartStrategy) { this.enabled = enabled; + this.restartStrategy = restartStrategy; + } + + protected boolean isEnabled() { + return enabled; } @Override public void phaseStarted(LocalSearchPhaseScope phaseScope) { super.phaseStarted(phaseScope); - reconfigurationStrategy.phaseStarted(phaseScope); + restartStrategy.phaseStarted(phaseScope); } @Override public void phaseEnded(LocalSearchPhaseScope phaseScope) { super.phaseEnded(phaseScope); - reconfigurationStrategy.phaseEnded(phaseScope); + restartStrategy.phaseEnded(phaseScope); } @Override public void stepStarted(LocalSearchStepScope stepScope) { super.stepStarted(stepScope); - reconfigurationStrategy.stepStarted(stepScope); + restartStrategy.stepStarted(stepScope); } @Override public void stepEnded(LocalSearchStepScope stepScope) { super.stepEnded(stepScope); - reconfigurationStrategy.stepEnded(stepScope); + restartStrategy.stepEnded(stepScope); } @Override @SuppressWarnings("unchecked") public boolean isAccepted(LocalSearchMoveScope moveScope) { - if (enabled && reconfigurationStrategy.needReconfiguration(moveScope)) { + if (enabled && restartStrategy.isTriggered(moveScope)) { moveScope.getStepScope().getPhaseScope().triggerReconfiguration(); return true; } var accepted = evaluate(moveScope); - if (enabled && moveScope.getScore().compareTo(moveScope.getStepScope().getPhaseScope().getBestScore()) > 0) { - reconfigurationStrategy.reset(); + var improved = enabled && moveScope.getScore().compareTo(moveScope.getStepScope().getPhaseScope().getBestScore()) > 0; + if (improved) { + restartStrategy.reset(); } return accepted; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java index 656b14dc98..586fc0d544 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java @@ -3,12 +3,12 @@ import java.util.Arrays; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.ReconfigurableAbstractAcceptor; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.reconfiguration.GeometricUnimprovedSolutionReconfigurationStrategy; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.ReconfigurableAcceptor; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.RestartStrategy; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; -public class DiversifiedLateAcceptanceAcceptor extends ReconfigurableAbstractAcceptor { +public class DiversifiedLateAcceptanceAcceptor extends ReconfigurableAcceptor { // The worst score in the late elements list protected Score lateWorse; @@ -20,8 +20,8 @@ public class DiversifiedLateAcceptanceAcceptor extends Reconfigurable protected Score[] previousScores; protected int lateScoreIndex = -1; - public DiversifiedLateAcceptanceAcceptor(boolean enableReconfiguration) { - super(new GeometricUnimprovedSolutionReconfigurationStrategy<>(), enableReconfiguration); + public DiversifiedLateAcceptanceAcceptor(boolean enableReconfiguration, RestartStrategy restartStrategy) { + super(enableReconfiguration, restartStrategy); } public void setLateAcceptanceSize(int lateAcceptanceSize) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java index 99dcec897b..c45a54540e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java @@ -3,13 +3,13 @@ import java.util.Arrays; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.ReconfigurableAbstractAcceptor; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.reconfiguration.GeometricUnimprovedSolutionReconfigurationStrategy; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.ReconfigurableAcceptor; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.RestartStrategy; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; -public class LateAcceptanceAcceptor extends ReconfigurableAbstractAcceptor { +public class LateAcceptanceAcceptor extends ReconfigurableAcceptor { protected int lateAcceptanceSize = -1; protected boolean hillClimbingEnabled = true; @@ -17,8 +17,8 @@ public class LateAcceptanceAcceptor extends ReconfigurableAbstractAcc protected Score[] previousScores; protected int lateScoreIndex = -1; - public LateAcceptanceAcceptor(boolean enableReconfiguration) { - super(new GeometricUnimprovedSolutionReconfigurationStrategy<>(), enableReconfiguration); + public LateAcceptanceAcceptor(boolean enableReconfiguration, RestartStrategy restartStrategy) { + super(enableReconfiguration, restartStrategy); } public void setLateAcceptanceSize(int lateAcceptanceSize) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/NoOpRestartStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/NoOpRestartStrategy.java new file mode 100644 index 0000000000..25848caee3 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/NoOpRestartStrategy.java @@ -0,0 +1,49 @@ +package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; + +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; + +public class NoOpRestartStrategy implements RestartStrategy { + + @Override + public boolean isTriggered(LocalSearchMoveScope moveScope) { + return false; + } + + @Override + public void reset() { + // Do nothing + } + + @Override + public void phaseStarted(LocalSearchPhaseScope phaseScope) { + // Do nothing + } + + @Override + public void stepStarted(LocalSearchStepScope stepScope) { + // Do nothing + } + + @Override + public void stepEnded(LocalSearchStepScope stepScope) { + // Do nothing + } + + @Override + public void phaseEnded(LocalSearchPhaseScope phaseScope) { + // Do nothing + } + + @Override + public void solvingStarted(SolverScope solverScope) { + // Do nothing + } + + @Override + public void solvingEnded(SolverScope solverScope) { + // Do nothing + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/reconfiguration/ReconfigurationStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/RestartStrategy.java similarity index 55% rename from core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/reconfiguration/ReconfigurationStrategy.java rename to core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/RestartStrategy.java index 7381627c03..1946a1433c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/reconfiguration/ReconfigurationStrategy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/RestartStrategy.java @@ -1,11 +1,11 @@ -package ai.timefold.solver.core.impl.localsearch.decider.acceptor.reconfiguration; +package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; import ai.timefold.solver.core.impl.localsearch.event.LocalSearchPhaseLifecycleListener; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; -public interface ReconfigurationStrategy extends LocalSearchPhaseLifecycleListener { +public interface RestartStrategy extends LocalSearchPhaseLifecycleListener { - boolean needReconfiguration(LocalSearchMoveScope moveScope); + boolean isTriggered(LocalSearchMoveScope moveScope); void reset(); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/reconfiguration/GeometricUnimprovedSolutionReconfigurationStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeGeometricRestartStrategy.java similarity index 57% rename from core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/reconfiguration/GeometricUnimprovedSolutionReconfigurationStrategy.java rename to core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeGeometricRestartStrategy.java index ecd0413792..13ba8a574a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/reconfiguration/GeometricUnimprovedSolutionReconfigurationStrategy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeGeometricRestartStrategy.java @@ -1,4 +1,4 @@ -package ai.timefold.solver.core.impl.localsearch.decider.acceptor.reconfiguration; +package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; import java.time.Clock; @@ -11,25 +11,40 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class GeometricUnimprovedSolutionReconfigurationStrategy implements ReconfigurationStrategy { - private static final double GEOMETRIC_FACTOR = 1.3; +/** + * Restart strategy which exponentially increases the restart times. + * The first restart occurs after one second without improvement. + * Following that, the unimproved timeout increases exponentially: 1s, 1s, 2s, 3s, 4s, 5s, 8s... + *

+ * The strategy is based on the work: Search in a Small World by Toby Walsh + */ +public class UnimprovedTimeGeometricRestartStrategy implements RestartStrategy { + private static final double GEOMETRIC_FACTOR = 1.4; // Value extracted from the cited paper private static final double SCALING_FACTOR = 1.0; - private final Logger logger = LoggerFactory.getLogger(GeometricUnimprovedSolutionReconfigurationStrategy.class); - private final Clock clock = Clock.systemUTC(); + private final Logger logger = LoggerFactory.getLogger(UnimprovedTimeGeometricRestartStrategy.class); + private final Clock clock; - private long lastImprovementMillis; + protected long lastImprovementMillis; private Score currentBestScore; - private boolean reconfigurationTriggered; + protected boolean restartTriggered; private double geometricGrowFactor; - private long nextReconfiguration; + protected long nextRestart; + + public UnimprovedTimeGeometricRestartStrategy() { + this(Clock.systemUTC()); + } + + protected UnimprovedTimeGeometricRestartStrategy(Clock clock) { + this.clock = clock; + } @Override public void phaseStarted(LocalSearchPhaseScope phaseScope) { currentBestScore = phaseScope.getBestScore(); geometricGrowFactor = 1; - nextReconfiguration = (long) (1_000 * SCALING_FACTOR); - reconfigurationTriggered = false; + nextRestart = (long) (1_000 * SCALING_FACTOR); + restartTriggered = false; lastImprovementMillis = clock.millis(); } @@ -63,22 +78,22 @@ public void solvingEnded(SolverScope solverScope) { } @Override - public boolean needReconfiguration(LocalSearchMoveScope moveScope) { - if (!reconfigurationTriggered && lastImprovementMillis > 0 - && clock.millis() - lastImprovementMillis >= nextReconfiguration) { - logger.debug("Reconfiguration triggered with geometric factor {} and scaling factor of {}", geometricGrowFactor, + public boolean isTriggered(LocalSearchMoveScope moveScope) { + if (!restartTriggered && lastImprovementMillis > 0 + && clock.millis() - lastImprovementMillis >= nextRestart) { + logger.debug("Restart triggered with geometric factor {} and scaling factor of {}", geometricGrowFactor, SCALING_FACTOR); - nextReconfiguration = (long) Math.ceil(SCALING_FACTOR * geometricGrowFactor * 1_000); + nextRestart = (long) Math.ceil(SCALING_FACTOR * geometricGrowFactor * 1_000); geometricGrowFactor = Math.ceil(geometricGrowFactor * GEOMETRIC_FACTOR); lastImprovementMillis = clock.millis(); - reconfigurationTriggered = true; + restartTriggered = true; } - return reconfigurationTriggered; + return restartTriggered; } @Override public void reset() { - reconfigurationTriggered = false; + restartTriggered = false; lastImprovementMillis = clock.millis(); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/perturbation/MoveSelectorPerturbationStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/perturbation/MoveSelectorPerturbationStrategy.java deleted file mode 100644 index d7cead2c23..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/perturbation/MoveSelectorPerturbationStrategy.java +++ /dev/null @@ -1,99 +0,0 @@ -package ai.timefold.solver.core.impl.localsearch.decider.perturbation; - -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.impl.heuristic.move.LegacyMoveAdapter; -import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector; -import ai.timefold.solver.core.impl.move.director.MoveDirector; -import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; -import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; -import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; -import ai.timefold.solver.core.impl.solver.scope.SolverScope; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class MoveSelectorPerturbationStrategy implements PerturbationStrategy { - - protected final Logger logger = LoggerFactory.getLogger(getClass()); - private final MoveSelector perturbationMoveSelector; - private final int maxLevels; - private Score currentBestScore; - private int level; - - public MoveSelectorPerturbationStrategy(MoveSelector perturbationMoveSelector, int maxLevels) { - this.perturbationMoveSelector = perturbationMoveSelector; - this.maxLevels = maxLevels; - } - - @Override - public > Score_ apply(AbstractStepScope stepScope) { - var phaseScope = stepScope.getPhaseScope(); - var perturbationMoveIterator = perturbationMoveSelector.iterator(); - InnerScoreDirector scoreDirector = phaseScope.getScoreDirector(); - MoveDirector moveDirector = stepScope.getMoveDirector(); - for (int i = 0; i < level; i++) { - if (perturbationMoveIterator.hasNext()) { - var perturbation = new LegacyMoveAdapter<>(perturbationMoveIterator.next()); - if (!LegacyMoveAdapter.isDoable(moveDirector, perturbation)) { - continue; - } - // var perturbationRebased = perturbation.rebase(moveDirector); - perturbation.execute(moveDirector); - logger.debug("Generating a perturbation: Move ({})", perturbation); - } - } - var currentScore = scoreDirector.calculateScore(); - logger.debug("New solution generated: old score ({}), new score ({})", - phaseScope.getLastCompletedStepScope().getScore(), currentScore); - if (level + 1 < maxLevels) { - level++; - } - // Always cancel it at the end of the step as the perturbation is already applied - stepScope.getPhaseScope().cancelReconfiguration(); - return currentScore; - } - - @Override - public void stepStarted(AbstractStepScope stepScope) { - if (isTriggered(stepScope)) { - var solverScope = stepScope.getPhaseScope().getSolverScope(); - logger.debug("Resetting working solution, score ({})", solverScope.getBestScore()); - solverScope.setWorkingSolutionFromBestSolution(); - // Need to update the cached entity list because the working solution was changed - perturbationMoveSelector.phaseStarted(stepScope.getPhaseScope()); - } - this.perturbationMoveSelector.stepStarted(stepScope); - this.currentBestScore = stepScope.getPhaseScope().getBestScore(); - } - - @Override - @SuppressWarnings({ "rawtypes", "unchecked" }) - public void stepEnded(AbstractStepScope stepScope) { - this.perturbationMoveSelector.stepEnded(stepScope); - if (((Score) stepScope.getScore()).compareTo(currentBestScore) > 0) { - // Reset it if the best solution is improved - this.level = 1; - } - } - - @Override - public void phaseStarted(AbstractPhaseScope phaseScope) { - this.level = 1; - perturbationMoveSelector.phaseStarted(phaseScope); - } - - @Override - public void phaseEnded(AbstractPhaseScope phaseScope) { - perturbationMoveSelector.phaseEnded(phaseScope); - } - - @Override - public void solvingStarted(SolverScope solverScope) { - perturbationMoveSelector.solvingStarted(solverScope); - } - - @Override - public void solvingEnded(SolverScope solverScope) { - perturbationMoveSelector.solvingEnded(solverScope); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/perturbation/NoPerturbationStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/NoOpReconfigurationStrategy.java similarity index 87% rename from core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/perturbation/NoPerturbationStrategy.java rename to core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/NoOpReconfigurationStrategy.java index cf48ae6355..cb3b787c84 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/perturbation/NoPerturbationStrategy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/NoOpReconfigurationStrategy.java @@ -1,11 +1,11 @@ -package ai.timefold.solver.core.impl.localsearch.decider.perturbation; +package ai.timefold.solver.core.impl.localsearch.decider.reconfiguration; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; import ai.timefold.solver.core.impl.solver.scope.SolverScope; -public class NoPerturbationStrategy implements PerturbationStrategy { +public final class NoOpReconfigurationStrategy implements ReconfigurationStrategy { @Override public > Score_ apply(AbstractStepScope stepScope) { throw new IllegalStateException("Impossible state"); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/perturbation/PerturbationStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategy.java similarity index 62% rename from core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/perturbation/PerturbationStrategy.java rename to core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategy.java index 8a08e84f14..23fc87d9af 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/perturbation/PerturbationStrategy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategy.java @@ -1,10 +1,11 @@ -package ai.timefold.solver.core.impl.localsearch.decider.perturbation; +package ai.timefold.solver.core.impl.localsearch.decider.reconfiguration; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleListener; import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; -public interface PerturbationStrategy extends PhaseLifecycleListener { +public sealed interface ReconfigurationStrategy extends PhaseLifecycleListener + permits NoOpReconfigurationStrategy, RestoreBestSolutionReconfigurationStrategy { > Score_ apply(AbstractStepScope stepScope); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionReconfigurationStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionReconfigurationStrategy.java new file mode 100644 index 0000000000..b27cbaccb1 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionReconfigurationStrategy.java @@ -0,0 +1,66 @@ +package ai.timefold.solver.core.impl.localsearch.decider.reconfiguration; + +import java.util.Objects; + +import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.impl.localsearch.decider.LocalSearchDecider; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; +import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; +import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class RestoreBestSolutionReconfigurationStrategy implements ReconfigurationStrategy { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + private LocalSearchDecider decider; + + public void setDecider(LocalSearchDecider decider) { + this.decider = decider; + } + + @Override + @SuppressWarnings("unchecked") + public > Score_ apply(AbstractStepScope stepScope) { + var solverScope = stepScope.getPhaseScope().getSolverScope(); + logger.debug("Resetting working solution, score ({})", solverScope.getBestScore()); + solverScope.setWorkingSolutionFromBestSolution(); + // Changing the working solution requires reinitializing the move selector and acceptor + // 1 - The move selector will reset all cached lists using old solution entity references + // 2 - The acceptor will restart its search from the updated working solution (last best solution) + decider.phaseStarted((LocalSearchPhaseScope) stepScope.getPhaseScope()); + return (Score_) solverScope.getBestScore(); + } + + @Override + public void stepStarted(AbstractStepScope stepScope) { + // Do nothing + } + + @Override + public void stepEnded(AbstractStepScope stepScope) { + // Do nothing + } + + @Override + public void phaseStarted(AbstractPhaseScope phaseScope) { + Objects.requireNonNull(decider); + } + + @Override + public void phaseEnded(AbstractPhaseScope phaseScope) { + // Do nothing + } + + @Override + public void solvingStarted(SolverScope solverScope) { + // Do nothing + } + + @Override + public void solvingEnded(SolverScope solverScope) { + // Do nothing + } +} diff --git a/core/src/main/resources/solver.xsd b/core/src/main/resources/solver.xsd index 8f05d11f7b..317fd7d637 100644 --- a/core/src/main/resources/solver.xsd +++ b/core/src/main/resources/solver.xsd @@ -1415,48 +1415,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -1622,11 +1582,11 @@ - - - + + + - + diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactoryTest.java index 97663a54f0..7408f9effa 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactoryTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactoryTest.java @@ -1,12 +1,8 @@ package ai.timefold.solver.core.impl.localsearch.decider.acceptor; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; import java.util.Arrays; import java.util.List; @@ -80,6 +76,7 @@ void lateAcceptanceAcceptor() { AcceptorFactory acceptorFactory = AcceptorFactory.create(localSearchAcceptorConfig); var acceptor = acceptorFactory.buildAcceptor(heuristicConfigPolicy); assertThat(acceptor).isExactlyInstanceOf(LateAcceptanceAcceptor.class); + assertThat(((LateAcceptanceAcceptor) acceptor).isEnabled()).isFalse(); localSearchAcceptorConfig = new LocalSearchAcceptorConfig() .withLateAcceptanceSize(10); @@ -96,6 +93,7 @@ void diversifiedLateAcceptanceAcceptor() { AcceptorFactory acceptorFactory = AcceptorFactory.create(localSearchAcceptorConfig); var acceptor = acceptorFactory.buildAcceptor(heuristicConfigPolicy); assertThat(acceptor).isExactlyInstanceOf(DiversifiedLateAcceptanceAcceptor.class); + assertThat(((DiversifiedLateAcceptanceAcceptor) acceptor).isEnabled()).isFalse(); localSearchAcceptorConfig = new LocalSearchAcceptorConfig() .withAcceptorTypeList(List.of(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE)) @@ -111,4 +109,22 @@ void diversifiedLateAcceptanceAcceptor() { AcceptorFactory badAcceptorFactory = AcceptorFactory.create(localSearchAcceptorConfig); assertThatIllegalStateException().isThrownBy(() -> badAcceptorFactory.buildAcceptor(heuristicConfigPolicy)); } + + @Test + void acceptorWithReconfiguration() { + var localSearchAcceptorConfig = new LocalSearchAcceptorConfig() + .withAcceptorTypeList(List.of(AcceptorType.LATE_ACCEPTANCE)) + .withEnableReconfiguration(true); + HeuristicConfigPolicy heuristicConfigPolicy = mock(HeuristicConfigPolicy.class); + AcceptorFactory acceptorFactory = AcceptorFactory.create(localSearchAcceptorConfig); + var acceptor = acceptorFactory.buildAcceptor(heuristicConfigPolicy); + assertThat(((LateAcceptanceAcceptor) acceptor).isEnabled()).isTrue(); + + localSearchAcceptorConfig = new LocalSearchAcceptorConfig() + .withAcceptorTypeList(List.of(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE)) + .withEnableReconfiguration(true); + acceptorFactory = AcceptorFactory.create(localSearchAcceptorConfig); + acceptor = acceptorFactory.buildAcceptor(heuristicConfigPolicy); + assertThat(((DiversifiedLateAcceptanceAcceptor) acceptor).isEnabled()).isTrue(); + } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java index b8d55bcc5f..215849faa7 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java @@ -1,9 +1,13 @@ package ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.AbstractAcceptorTest; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.NoOpRestartStrategy; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.RestartStrategy; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; import ai.timefold.solver.core.impl.solver.scope.SolverScope; @@ -14,7 +18,7 @@ class DiversifiedLateAcceptanceAcceptorTest extends AbstractAcceptorTest { @Test void acceptanceCriterion() { - var acceptor = new DiversifiedLateAcceptanceAcceptor<>(false); + var acceptor = new DiversifiedLateAcceptanceAcceptor<>(false, new NoOpRestartStrategy<>()); acceptor.setLateAcceptanceSize(3); var solverScope = new SolverScope<>(); @@ -51,7 +55,7 @@ void acceptanceCriterion() { @Test void replacementCriterion() { - var acceptor = new DiversifiedLateAcceptanceAcceptor<>(false); + var acceptor = new DiversifiedLateAcceptanceAcceptor<>(false, new NoOpRestartStrategy<>()); acceptor.setLateAcceptanceSize(3); var solverScope = new SolverScope<>(); @@ -142,4 +146,37 @@ void replacementCriterion() { acceptor.isAccepted(moveScope0); assertThat(acceptor.previousScores[0]).isEqualTo(SimpleScore.of(-2001)); } + + @Test + void triggerReconfiguration() { + var restartStrategy = mock(RestartStrategy.class); + when(restartStrategy.isTriggered(any())).thenReturn(true); + var acceptor = new DiversifiedLateAcceptanceAcceptor<>(true, restartStrategy); + acceptor.setLateAcceptanceSize(3); + var solverScope = new SolverScope<>(); + var phaseScope = new LocalSearchPhaseScope<>(solverScope, 0); + var stepScope0 = new LocalSearchStepScope<>(phaseScope); + var moveScope0 = buildMoveScope(stepScope0, -2000); + assertThat(acceptor.isAccepted(moveScope0)).isTrue(); + verify(restartStrategy, times(1)).isTriggered(any()); + assertThat(phaseScope.isReconfigurationTriggered()).isTrue(); + } + + @Test + void resetReconfiguration() { + var restartStrategy = mock(RestartStrategy.class); + var acceptor = new DiversifiedLateAcceptanceAcceptor<>(true, restartStrategy); + acceptor.setLateAcceptanceSize(3); + + var solverScope = new SolverScope<>(); + solverScope.setBestScore(SimpleScore.of(-1000)); + var phaseScope = new LocalSearchPhaseScope<>(solverScope, 0); + acceptor.phaseStarted(phaseScope); + var stepScope0 = new LocalSearchStepScope<>(phaseScope); + stepScope0.setScore(SimpleScore.of(-999)); + var moveScope0 = buildMoveScope(stepScope0, -999); + stepScope0.getPhaseScope().setLastCompletedStepScope(stepScope0); + assertThat(acceptor.isAccepted(moveScope0)).isTrue(); + verify(restartStrategy, times(1)).reset(); + } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptorTest.java index 01569e7ace..9bf9982730 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptorTest.java @@ -2,9 +2,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; +import static org.mockito.Mockito.times; import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.AbstractAcceptorTest; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.NoOpRestartStrategy; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.RestartStrategy; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; import ai.timefold.solver.core.impl.solver.scope.SolverScope; @@ -15,7 +20,7 @@ class LateAcceptanceAcceptorTest extends AbstractAcceptorTest { @Test void lateAcceptanceSize() { - var acceptor = new LateAcceptanceAcceptor<>(false); + var acceptor = new LateAcceptanceAcceptor<>(false, new NoOpRestartStrategy<>()); acceptor.setLateAcceptanceSize(3); acceptor.setHillClimbingEnabled(false); @@ -128,7 +133,7 @@ void lateAcceptanceSize() { @Test void hillClimbingEnabled() { - var acceptor = new LateAcceptanceAcceptor<>(false); + var acceptor = new LateAcceptanceAcceptor<>(false, new NoOpRestartStrategy<>()); acceptor.setLateAcceptanceSize(2); acceptor.setHillClimbingEnabled(true); @@ -241,15 +246,47 @@ void hillClimbingEnabled() { @Test void zeroLateAcceptanceSize() { - var acceptor = new LateAcceptanceAcceptor<>(false); + var acceptor = new LateAcceptanceAcceptor<>(false, new NoOpRestartStrategy<>()); acceptor.setLateAcceptanceSize(0); assertThatIllegalArgumentException().isThrownBy(() -> acceptor.phaseStarted(null)); } @Test void negativeLateAcceptanceSize() { - var acceptor = new LateAcceptanceAcceptor<>(false); + var acceptor = new LateAcceptanceAcceptor<>(false, new NoOpRestartStrategy<>()); acceptor.setLateAcceptanceSize(-1); assertThatIllegalArgumentException().isThrownBy(() -> acceptor.phaseStarted(null)); } + + @Test + void triggerReconfiguration() { + var restartStrategy = mock(RestartStrategy.class); + when(restartStrategy.isTriggered(any())).thenReturn(true); + var acceptor = new LateAcceptanceAcceptor<>(true, restartStrategy); + acceptor.setLateAcceptanceSize(3); + var solverScope = new SolverScope<>(); + var phaseScope = new LocalSearchPhaseScope<>(solverScope, 0); + var stepScope0 = new LocalSearchStepScope<>(phaseScope); + var moveScope0 = buildMoveScope(stepScope0, -2000); + assertThat(acceptor.isAccepted(moveScope0)).isTrue(); + assertThat(phaseScope.isReconfigurationTriggered()).isTrue(); + } + + @Test + void resetReconfiguration() { + var restartStrategy = mock(RestartStrategy.class); + var acceptor = new LateAcceptanceAcceptor<>(true, restartStrategy); + acceptor.setLateAcceptanceSize(3); + + var solverScope = new SolverScope<>(); + solverScope.setBestScore(SimpleScore.of(-1000)); + var phaseScope = new LocalSearchPhaseScope<>(solverScope, 0); + acceptor.phaseStarted(phaseScope); + var stepScope0 = new LocalSearchStepScope<>(phaseScope); + stepScope0.setScore(SimpleScore.of(-999)); + var moveScope0 = buildMoveScope(stepScope0, -999); + stepScope0.getPhaseScope().setLastCompletedStepScope(stepScope0); + assertThat(acceptor.isAccepted(moveScope0)).isTrue(); + verify(restartStrategy, times(1)).reset(); + } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/NoOpRestartStrategyTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/NoOpRestartStrategyTest.java new file mode 100644 index 0000000000..288f5f152e --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/NoOpRestartStrategyTest.java @@ -0,0 +1,15 @@ +package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; + +import org.junit.jupiter.api.Test; + +class NoOpRestartStrategyTest { + + @Test + void noOp() { + var strategy = new NoOpRestartStrategy<>(); + assertThat(strategy.isTriggered(any())).isFalse(); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeGeometricRestartStrategyTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeGeometricRestartStrategyTest.java new file mode 100644 index 0000000000..afda704b67 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeGeometricRestartStrategyTest.java @@ -0,0 +1,94 @@ +package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.time.Clock; + +import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class UnimprovedTimeGeometricRestartStrategyTest { + + @Test + void isTriggered() { + var clock = mock(Clock.class); + when(clock.millis()).thenReturn(1000L); + var phaseScope = mock(LocalSearchPhaseScope.class); + var stepScope = mock(LocalSearchStepScope.class); + var moveScope = mock(LocalSearchMoveScope.class); + when(stepScope.getPhaseScope()).thenReturn(phaseScope); + when(moveScope.getStepScope()).thenReturn(stepScope); + when(phaseScope.getBestScore()).thenReturn(SimpleScore.of(1000)); + + // Initial values + var strategy = new UnimprovedTimeGeometricRestartStrategy<>(clock); + strategy.phaseStarted(phaseScope); + assertThat(strategy.lastImprovementMillis).isEqualTo(1000L); + assertThat(strategy.nextRestart).isEqualTo(1000L); + assertThat(strategy.restartTriggered).isFalse(); + + // First restart + Mockito.reset(clock); + when(clock.millis()).thenReturn(2000L); + assertThat(strategy.isTriggered(moveScope)).isTrue(); + assertThat(strategy.lastImprovementMillis).isEqualTo(2000L); + assertThat(strategy.nextRestart).isEqualTo(1000L); + assertThat(strategy.restartTriggered).isTrue(); + strategy.reset(); + assertThat(strategy.isTriggered(moveScope)).isFalse(); + + // Second restart + Mockito.reset(clock); + when(clock.millis()).thenReturn(3000L); + assertThat(strategy.isTriggered(moveScope)).isTrue(); + assertThat(strategy.lastImprovementMillis).isEqualTo(3000L); + assertThat(strategy.nextRestart).isEqualTo(2000L); + assertThat(strategy.restartTriggered).isTrue(); + strategy.reset(); + + // Third restart + Mockito.reset(clock); + when(clock.millis()).thenReturn(5000L); + assertThat(strategy.isTriggered(moveScope)).isTrue(); + assertThat(strategy.lastImprovementMillis).isEqualTo(5000L); + assertThat(strategy.nextRestart).isEqualTo(3000L); + assertThat(strategy.restartTriggered).isTrue(); + strategy.reset(); + } + + @Test + void updateBestSolution() { + var clock = mock(Clock.class); + when(clock.millis()).thenReturn(1000L, 2000L); + var phaseScope = mock(LocalSearchPhaseScope.class); + var stepScope = mock(LocalSearchStepScope.class); + var moveScope = mock(LocalSearchMoveScope.class); + when(stepScope.getPhaseScope()).thenReturn(phaseScope); + when(moveScope.getStepScope()).thenReturn(stepScope); + when(phaseScope.getBestScore()).thenReturn(SimpleScore.of(1000)); + when(stepScope.getScore()).thenReturn(SimpleScore.of(2000)); + + var strategy = new UnimprovedTimeGeometricRestartStrategy<>(clock); + strategy.phaseStarted(phaseScope); + strategy.stepStarted(stepScope); + strategy.stepEnded(stepScope); + assertThat(strategy.lastImprovementMillis).isEqualTo(2000L); + } + + @Test + void reset() { + var clock = mock(Clock.class); + when(clock.millis()).thenReturn(1L); + var strategy = new UnimprovedTimeGeometricRestartStrategy<>(clock); + strategy.reset(); + assertThat(strategy.lastImprovementMillis).isEqualTo(1L); + assertThat(strategy.restartTriggered).isFalse(); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategyTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategyTest.java new file mode 100644 index 0000000000..18f062dfd0 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategyTest.java @@ -0,0 +1,45 @@ +package ai.timefold.solver.core.impl.localsearch.decider.reconfiguration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +import ai.timefold.solver.core.impl.localsearch.decider.LocalSearchDecider; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.NoOpRestartStrategy; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; + +import org.junit.jupiter.api.Test; + +class ReconfigurationStrategyTest { + + @Test + void noOp() { + var strategy = new NoOpRestartStrategy<>(); + assertThat(strategy.isTriggered(mock(LocalSearchMoveScope.class))).isFalse(); + } + + @Test + void restoreBestSolution() { + var decider = mock(LocalSearchDecider.class); + var strategy = new RestoreBestSolutionReconfigurationStrategy<>(); + + // Requires the decider + assertThatThrownBy(() -> strategy.phaseStarted(null)).isInstanceOf(NullPointerException.class); + + // Restore the best solution + var solverScope = mock(SolverScope.class); + var phaseScope = mock(LocalSearchPhaseScope.class); + when(phaseScope.getSolverScope()).thenReturn(solverScope); + var stepScope = mock(LocalSearchStepScope.class); + when(stepScope.getPhaseScope()).thenReturn(phaseScope); + strategy.setDecider(decider); + strategy.apply(stepScope); + // Restore the best solution + verify(solverScope, times(1)).setWorkingSolutionFromBestSolution(); + // Restart the phase + verify(decider, times(1)).phaseStarted(any()); + } +} From 856cd991f22f5f03f1a735eab36a0810b4218b8f Mon Sep 17 00:00:00 2001 From: Fred Date: Wed, 22 Jan 2025 15:29:45 -0300 Subject: [PATCH 06/31] fix: cancel reconfiguration when it is done --- .../RestoreBestSolutionReconfigurationStrategy.java | 2 ++ .../solver/core/impl/phase/scope/AbstractPhaseScope.java | 2 +- .../decider/reconfiguration/ReconfigurationStrategyTest.java | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionReconfigurationStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionReconfigurationStrategy.java index b27cbaccb1..b4fbb05656 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionReconfigurationStrategy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionReconfigurationStrategy.java @@ -31,6 +31,8 @@ public > Score_ apply(AbstractStepScope // 1 - The move selector will reset all cached lists using old solution entity references // 2 - The acceptor will restart its search from the updated working solution (last best solution) decider.phaseStarted((LocalSearchPhaseScope) stepScope.getPhaseScope()); + // Reset it as the best solution is already restored + stepScope.getPhaseScope().resetReconfiguration(); return (Score_) solverScope.getBestScore(); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java index 938f7b1f01..5e58ad887d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java @@ -256,7 +256,7 @@ public void triggerReconfiguration() { this.reconfigurationTriggered = true; } - public void cancelReconfiguration() { + public void resetReconfiguration() { this.reconfigurationTriggered = false; } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategyTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategyTest.java index 18f062dfd0..af7e01a084 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategyTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategyTest.java @@ -40,6 +40,7 @@ void restoreBestSolution() { // Restore the best solution verify(solverScope, times(1)).setWorkingSolutionFromBestSolution(); // Restart the phase + verify(phaseScope, times(1)).resetReconfiguration(); verify(decider, times(1)).phaseStarted(any()); } } From 245572cb4acae3106c174516432a70d40a0a6ecc Mon Sep 17 00:00:00 2001 From: Fred Date: Wed, 22 Jan 2025 18:15:27 -0300 Subject: [PATCH 07/31] feat: add reconfiguration to MT contract --- .../core/enterprise/TimefoldSolverEnterpriseService.java | 4 +++- .../core/impl/localsearch/DefaultLocalSearchPhaseFactory.java | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java b/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java index 22ebeca27b..03917debe6 100644 --- a/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java +++ b/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java @@ -29,6 +29,7 @@ import ai.timefold.solver.core.impl.localsearch.decider.LocalSearchDecider; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.Acceptor; import ai.timefold.solver.core.impl.localsearch.decider.forager.LocalSearchForager; +import ai.timefold.solver.core.impl.localsearch.decider.reconfiguration.ReconfigurationStrategy; import ai.timefold.solver.core.impl.partitionedsearch.PartitionedSearchPhase; import ai.timefold.solver.core.impl.solver.termination.Termination; @@ -100,7 +101,8 @@ ConstructionHeuristicDecider buildConstructionHeuristic(T ConstructionHeuristicForager forager, HeuristicConfigPolicy configPolicy); LocalSearchDecider buildLocalSearch(int moveThreadCount, Termination termination, - MoveSelector moveSelector, Acceptor acceptor, LocalSearchForager forager, + MoveSelector moveSelector, ReconfigurationStrategy reconfigurationStrategy, + Acceptor acceptor, LocalSearchForager forager, EnvironmentMode environmentMode, HeuristicConfigPolicy configPolicy); PartitionedSearchPhase buildPartitionedSearch(int phaseIndex, diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java index 446e8738ad..c3706691a1 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java @@ -85,8 +85,8 @@ private LocalSearchDecider buildDecider(HeuristicConfigPolicy Date: Wed, 22 Jan 2025 19:40:15 -0300 Subject: [PATCH 08/31] chore: reconfiguration strategy compatible with MT --- .../DefaultLocalSearchPhaseFactory.java | 10 ++++------ .../acceptor/ReconfigurableAcceptor.java | 7 +++++++ .../UnimprovedTimeGeometricRestartStrategy.java | 10 ++++++---- ...toreBestSolutionReconfigurationStrategy.java | 17 +++++++++++------ .../ReconfigurationStrategyTest.java | 17 ++++++++++------- 5 files changed, 38 insertions(+), 23 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java index c3706691a1..99ee6ded48 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java @@ -67,8 +67,8 @@ public LocalSearchPhase buildPhase(int phaseIndex, boolean lastInitia private LocalSearchDecider buildDecider(HeuristicConfigPolicy configPolicy, Termination termination) { var moveSelector = buildMoveSelector(configPolicy); - var reconfigurationStrategy = buildReconfigurationStrategy(configPolicy); var acceptor = buildAcceptor(configPolicy); + var reconfigurationStrategy = buildReconfigurationStrategy(configPolicy, moveSelector, acceptor); var forager = buildForager(configPolicy); if (moveSelector.isNeverEnding() && !forager.supportsNeverEndingMoveSelector()) { throw new IllegalStateException("The moveSelector (" + moveSelector @@ -94,9 +94,6 @@ private LocalSearchDecider buildDecider(HeuristicConfigPolicy castReconfigurationStrategy) { - castReconfigurationStrategy.setDecider(decider); - } return decider; } @@ -203,14 +200,15 @@ protected MoveSelector buildMoveSelector(HeuristicConfigPolicy buildReconfigurationStrategy(HeuristicConfigPolicy configPolicy) { + private ReconfigurationStrategy buildReconfigurationStrategy(HeuristicConfigPolicy configPolicy, + MoveSelector moveSelector, Acceptor acceptor) { var acceptorConfig = phaseConfig.getAcceptorConfig(); if (acceptorConfig != null) { var enableReconfiguration = acceptorConfig.getEnableReconfiguration() != null && acceptorConfig.getEnableReconfiguration(); if (enableReconfiguration) { configPolicy.ensurePreviewFeature(PreviewFeature.RECONFIGURATION); - return new RestoreBestSolutionReconfigurationStrategy<>(); + return new RestoreBestSolutionReconfigurationStrategy<>(moveSelector, acceptor); } } return new NoOpReconfigurationStrategy<>(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java index 71c669647c..b07c045053 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java @@ -5,6 +5,7 @@ import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; /** * Base class designed to analyze whether the solving process needs to be restarted. @@ -24,6 +25,12 @@ protected boolean isEnabled() { return enabled; } + @Override + public void solvingStarted(SolverScope solverScope) { + super.solvingStarted(solverScope); + restartStrategy.solvingStarted(solverScope); + } + @Override public void phaseStarted(LocalSearchPhaseScope phaseScope) { super.phaseStarted(phaseScope); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeGeometricRestartStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeGeometricRestartStrategy.java index 13ba8a574a..fdf2d52bbb 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeGeometricRestartStrategy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeGeometricRestartStrategy.java @@ -42,10 +42,10 @@ protected UnimprovedTimeGeometricRestartStrategy(Clock clock) { @Override public void phaseStarted(LocalSearchPhaseScope phaseScope) { currentBestScore = phaseScope.getBestScore(); - geometricGrowFactor = 1; - nextRestart = (long) (1_000 * SCALING_FACTOR); + if (lastImprovementMillis == 0) { + lastImprovementMillis = clock.millis(); + } restartTriggered = false; - lastImprovementMillis = clock.millis(); } @Override @@ -69,7 +69,9 @@ public void stepEnded(LocalSearchStepScope stepScope) { @Override public void solvingStarted(SolverScope solverScope) { - // Do nothing + lastImprovementMillis = 0; + geometricGrowFactor = 1; + nextRestart = (long) (1_000 * SCALING_FACTOR); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionReconfigurationStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionReconfigurationStrategy.java index b4fbb05656..5494fc1405 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionReconfigurationStrategy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionReconfigurationStrategy.java @@ -3,7 +3,8 @@ import java.util.Objects; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.impl.localsearch.decider.LocalSearchDecider; +import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.Acceptor; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; @@ -15,10 +16,12 @@ public final class RestoreBestSolutionReconfigurationStrategy implements ReconfigurationStrategy { private final Logger logger = LoggerFactory.getLogger(getClass()); - private LocalSearchDecider decider; + private final MoveSelector moveSelector; + private final Acceptor acceptor; - public void setDecider(LocalSearchDecider decider) { - this.decider = decider; + public RestoreBestSolutionReconfigurationStrategy(MoveSelector moveSelector, Acceptor acceptor) { + this.moveSelector = moveSelector; + this.acceptor = acceptor; } @Override @@ -29,8 +32,9 @@ public > Score_ apply(AbstractStepScope solverScope.setWorkingSolutionFromBestSolution(); // Changing the working solution requires reinitializing the move selector and acceptor // 1 - The move selector will reset all cached lists using old solution entity references + moveSelector.phaseStarted(stepScope.getPhaseScope()); // 2 - The acceptor will restart its search from the updated working solution (last best solution) - decider.phaseStarted((LocalSearchPhaseScope) stepScope.getPhaseScope()); + acceptor.phaseStarted((LocalSearchPhaseScope) stepScope.getPhaseScope()); // Reset it as the best solution is already restored stepScope.getPhaseScope().resetReconfiguration(); return (Score_) solverScope.getBestScore(); @@ -48,7 +52,8 @@ public void stepEnded(AbstractStepScope stepScope) { @Override public void phaseStarted(AbstractPhaseScope phaseScope) { - Objects.requireNonNull(decider); + Objects.requireNonNull(moveSelector); + Objects.requireNonNull(acceptor); } @Override diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategyTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategyTest.java index af7e01a084..1f27938d1d 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategyTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategyTest.java @@ -4,7 +4,8 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.*; -import ai.timefold.solver.core.impl.localsearch.decider.LocalSearchDecider; +import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.Acceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.NoOpRestartStrategy; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; @@ -23,24 +24,26 @@ void noOp() { @Test void restoreBestSolution() { - var decider = mock(LocalSearchDecider.class); - var strategy = new RestoreBestSolutionReconfigurationStrategy<>(); - // Requires the decider - assertThatThrownBy(() -> strategy.phaseStarted(null)).isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> new RestoreBestSolutionReconfigurationStrategy<>(null, null).phaseStarted(null)) + .isInstanceOf(NullPointerException.class); // Restore the best solution + var moveSelector = mock(MoveSelector.class); + var acceptor = mock(Acceptor.class); + var strategy = new RestoreBestSolutionReconfigurationStrategy<>(moveSelector, acceptor); + var solverScope = mock(SolverScope.class); var phaseScope = mock(LocalSearchPhaseScope.class); when(phaseScope.getSolverScope()).thenReturn(solverScope); var stepScope = mock(LocalSearchStepScope.class); when(stepScope.getPhaseScope()).thenReturn(phaseScope); - strategy.setDecider(decider); strategy.apply(stepScope); // Restore the best solution verify(solverScope, times(1)).setWorkingSolutionFromBestSolution(); // Restart the phase verify(phaseScope, times(1)).resetReconfiguration(); - verify(decider, times(1)).phaseStarted(any()); + verify(moveSelector, times(1)).phaseStarted(any()); + verify(acceptor, times(1)).phaseStarted(any()); } } From 6d7499deec4e2d003a2696ce91a8ce6f86d3925f Mon Sep 17 00:00:00 2001 From: Fred Date: Thu, 23 Jan 2025 20:10:15 -0300 Subject: [PATCH 09/31] feat: different restart strategies --- benchmark/src/main/resources/benchmark.xsd | 20 ++- core/src/build/revapi-differences.json | 2 +- .../acceptor/LocalSearchAcceptorConfig.java | 21 ++- .../decider/acceptor/RestartType.java | 9 ++ .../DefaultLocalSearchPhaseFactory.java | 2 +- .../decider/acceptor/AcceptorFactory.java | 34 ++--- .../acceptor/ReconfigurableAcceptor.java | 2 +- .../AbstractGeometricRestartStrategy.java | 89 +++++++++++++ .../acceptor/restart/NoOpRestartStrategy.java | 2 +- .../acceptor/restart/RestartStrategy.java | 2 +- .../UnimprovedMoveCountRestartStrategy.java | 78 ++++++++++++ ...nimprovedTimeGeometricRestartStrategy.java | 101 --------------- .../UnimprovedTimeRestartStrategy.java | 78 ++++++++++++ core/src/main/resources/solver.xsd | 14 +- .../decider/acceptor/AcceptorFactoryTest.java | 5 +- ...DiversifiedLateAcceptanceAcceptorTest.java | 2 +- .../LateAcceptanceAcceptorTest.java | 2 +- ...nimprovedMoveCountRestartStrategyTest.java | 120 ++++++++++++++++++ ...=> UnimprovedTimeRestartStrategyTest.java} | 71 +++++++---- 19 files changed, 489 insertions(+), 165 deletions(-) create mode 100644 core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/RestartType.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricRestartStrategy.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountRestartStrategy.java delete mode 100644 core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeGeometricRestartStrategy.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeRestartStrategy.java create mode 100644 core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountRestartStrategyTest.java rename core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/{UnimprovedTimeGeometricRestartStrategyTest.java => UnimprovedTimeRestartStrategyTest.java} (57%) diff --git a/benchmark/src/main/resources/benchmark.xsd b/benchmark/src/main/resources/benchmark.xsd index d552322ad2..453d45a246 100644 --- a/benchmark/src/main/resources/benchmark.xsd +++ b/benchmark/src/main/resources/benchmark.xsd @@ -2414,7 +2414,7 @@ - + @@ -3155,6 +3155,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/core/src/build/revapi-differences.json b/core/src/build/revapi-differences.json index 69fe8a6f74..b659d2a4de 100644 --- a/core/src/build/revapi-differences.json +++ b/core/src/build/revapi-differences.json @@ -111,7 +111,7 @@ "annotationType": "jakarta.xml.bind.annotation.XmlType", "attribute": "propOrder", "oldValue": "{\"acceptorTypeList\", \"entityTabuSize\", \"entityTabuRatio\", \"fadingEntityTabuSize\", \"fadingEntityTabuRatio\", \"valueTabuSize\", \"valueTabuRatio\", \"fadingValueTabuSize\", \"fadingValueTabuRatio\", \"moveTabuSize\", \"fadingMoveTabuSize\", \"undoMoveTabuSize\", \"fadingUndoMoveTabuSize\", \"simulatedAnnealingStartingTemperature\", \"lateAcceptanceSize\", \"greatDelugeWaterLevelIncrementScore\", \"greatDelugeWaterLevelIncrementRatio\", \"stepCountingHillClimbingSize\", \"stepCountingHillClimbingType\"}", - "newValue": "{\"acceptorTypeList\", \"enableReconfiguration\", \"entityTabuSize\", \"entityTabuRatio\", \"fadingEntityTabuSize\", \"fadingEntityTabuRatio\", \"valueTabuSize\", \"valueTabuRatio\", \"fadingValueTabuSize\", \"fadingValueTabuRatio\", \"moveTabuSize\", \"fadingMoveTabuSize\", \"undoMoveTabuSize\", \"fadingUndoMoveTabuSize\", \"simulatedAnnealingStartingTemperature\", \"lateAcceptanceSize\", \"greatDelugeWaterLevelIncrementScore\", \"greatDelugeWaterLevelIncrementRatio\", \"stepCountingHillClimbingSize\", \"stepCountingHillClimbingType\"}", + "newValue": "{\"acceptorTypeList\", \"reconfigurationRestartType\", \"entityTabuSize\", \"entityTabuRatio\", \"fadingEntityTabuSize\", \"fadingEntityTabuRatio\", \"valueTabuSize\", \"valueTabuRatio\", \"fadingValueTabuSize\", \"fadingValueTabuRatio\", \"moveTabuSize\", \"fadingMoveTabuSize\", \"undoMoveTabuSize\", \"fadingUndoMoveTabuSize\", \"simulatedAnnealingStartingTemperature\", \"lateAcceptanceSize\", \"greatDelugeWaterLevelIncrementScore\", \"greatDelugeWaterLevelIncrementRatio\", \"stepCountingHillClimbingSize\", \"stepCountingHillClimbingType\"}", "justification": "Add the acceptor reconfiguration setting" } ] diff --git a/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/LocalSearchAcceptorConfig.java b/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/LocalSearchAcceptorConfig.java index 8b53d0ad4d..ccf93163e0 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/LocalSearchAcceptorConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/LocalSearchAcceptorConfig.java @@ -15,7 +15,7 @@ @XmlType(propOrder = { "acceptorTypeList", - "enableReconfiguration", + "reconfigurationRestartType", "entityTabuSize", "entityTabuRatio", "fadingEntityTabuSize", @@ -39,7 +39,7 @@ public class LocalSearchAcceptorConfig extends AbstractConfig acceptorTypeList = null; - private Boolean enableReconfiguration; + private RestartType reconfigurationRestartType = null; protected Integer entityTabuSize = null; protected Double entityTabuRatio = null; @@ -80,12 +80,12 @@ public void setAcceptorTypeList(@Nullable List acceptorTypeList) { this.acceptorTypeList = acceptorTypeList; } - public @Nullable Boolean getEnableReconfiguration() { - return enableReconfiguration; + public @Nullable RestartType getReconfigurationRestartType() { + return reconfigurationRestartType; } - public void setEnableReconfiguration(@Nullable Boolean enableReconfiguration) { - this.enableReconfiguration = enableReconfiguration; + public void setReconfigurationRestartType(@Nullable RestartType reconfigurationRestartType) { + this.reconfigurationRestartType = reconfigurationRestartType; } public @Nullable Integer getEntityTabuSize() { @@ -273,9 +273,8 @@ public void setStepCountingHillClimbingType(@Nullable StepCountingHillClimbingTy return this; } - public @NonNull LocalSearchAcceptorConfig - withEnableReconfiguration(@NonNull Boolean enableReconfiguration) { - this.enableReconfiguration = enableReconfiguration; + public @NonNull LocalSearchAcceptorConfig withRestartType(@NonNull RestartType restartType) { + this.reconfigurationRestartType = restartType; return this; } @@ -383,8 +382,8 @@ public LocalSearchAcceptorConfig withFadingUndoMoveTabuSize(Integer fadingUndoMo } } } - enableReconfiguration = - ConfigUtils.inheritOverwritableProperty(enableReconfiguration, inheritedConfig.getEnableReconfiguration()); + reconfigurationRestartType = ConfigUtils.inheritOverwritableProperty(reconfigurationRestartType, + inheritedConfig.getReconfigurationRestartType()); entityTabuSize = ConfigUtils.inheritOverwritableProperty(entityTabuSize, inheritedConfig.getEntityTabuSize()); entityTabuRatio = ConfigUtils.inheritOverwritableProperty(entityTabuRatio, inheritedConfig.getEntityTabuRatio()); fadingEntityTabuSize = ConfigUtils.inheritOverwritableProperty(fadingEntityTabuSize, diff --git a/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/RestartType.java b/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/RestartType.java new file mode 100644 index 0000000000..7007cdf085 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/RestartType.java @@ -0,0 +1,9 @@ +package ai.timefold.solver.core.config.localsearch.decider.acceptor; + +import jakarta.xml.bind.annotation.XmlEnum; + +@XmlEnum +public enum RestartType { + UNIMPROVED_TIME, + UNIMPROVED_MOVE_COUNT +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java index 99ee6ded48..32de0b5568 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java @@ -205,7 +205,7 @@ private ReconfigurationStrategy buildReconfigurationStrategy(Heuristi var acceptorConfig = phaseConfig.getAcceptorConfig(); if (acceptorConfig != null) { var enableReconfiguration = - acceptorConfig.getEnableReconfiguration() != null && acceptorConfig.getEnableReconfiguration(); + acceptorConfig.getReconfigurationRestartType() != null; if (enableReconfiguration) { configPolicy.ensurePreviewFeature(PreviewFeature.RECONFIGURATION); return new RestoreBestSolutionReconfigurationStrategy<>(moveSelector, acceptor); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java index 87f29f516f..2b1957cec9 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java @@ -17,7 +17,7 @@ import ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance.LateAcceptanceAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.NoOpRestartStrategy; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.RestartStrategy; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.UnimprovedTimeGeometricRestartStrategy; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.UnimprovedMoveCountRestartStrategy; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.simulatedannealing.SimulatedAnnealingAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stepcountinghillclimbing.StepCountingHillClimbingAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.tabu.EntityTabuAcceptor; @@ -223,13 +223,9 @@ private Optional> buildMoveTabuAcceptor(HeuristicCon if (acceptorTypeListsContainsAcceptorType(AcceptorType.LATE_ACCEPTANCE) || (!acceptorTypeListsContainsAcceptorType(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE) && acceptorConfig.getLateAcceptanceSize() != null)) { - RestartStrategy restartStrategy = new NoOpRestartStrategy<>(); - var enableReconfiguration = - acceptorConfig.getEnableReconfiguration() != null && acceptorConfig.getEnableReconfiguration(); - if (enableReconfiguration) { - restartStrategy = new UnimprovedTimeGeometricRestartStrategy<>(); - } - var acceptor = new LateAcceptanceAcceptor<>(enableReconfiguration, restartStrategy); + var restartStrategy = buildRestartStrategy(); + var acceptor = + new LateAcceptanceAcceptor<>(!(restartStrategy instanceof NoOpRestartStrategy), restartStrategy); acceptor.setLateAcceptanceSize(Objects.requireNonNullElse(acceptorConfig.getLateAcceptanceSize(), 400)); return Optional.of(acceptor); } @@ -240,19 +236,27 @@ private Optional> buildMoveTabuAcceptor(HeuristicCon buildDiversifiedLateAcceptanceAcceptor(HeuristicConfigPolicy configPolicy) { if (acceptorTypeListsContainsAcceptorType(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE)) { configPolicy.ensurePreviewFeature(PreviewFeature.DIVERSIFIED_LATE_ACCEPTANCE); - RestartStrategy restartStrategy = new NoOpRestartStrategy<>(); - var enableReconfiguration = - acceptorConfig.getEnableReconfiguration() != null && acceptorConfig.getEnableReconfiguration(); - if (enableReconfiguration) { - restartStrategy = new UnimprovedTimeGeometricRestartStrategy<>(); - } - var acceptor = new DiversifiedLateAcceptanceAcceptor<>(enableReconfiguration, restartStrategy); + var restartStrategy = buildRestartStrategy(); + var acceptor = new DiversifiedLateAcceptanceAcceptor<>(!(restartStrategy instanceof NoOpRestartStrategy), + restartStrategy); acceptor.setLateAcceptanceSize(Objects.requireNonNullElse(acceptorConfig.getLateAcceptanceSize(), 5)); return Optional.of(acceptor); } return Optional.empty(); } + private RestartStrategy buildRestartStrategy() { + RestartStrategy restartStrategy = new NoOpRestartStrategy<>(); + var enableReconfiguration = acceptorConfig.getReconfigurationRestartType() != null; + if (enableReconfiguration) { + return switch (acceptorConfig.getReconfigurationRestartType()) { + case UNIMPROVED_TIME -> new UnimprovedMoveCountRestartStrategy<>(); + case UNIMPROVED_MOVE_COUNT -> new UnimprovedMoveCountRestartStrategy<>(); + }; + } + return restartStrategy; + } + private Optional> buildGreatDelugeAcceptor(HeuristicConfigPolicy configPolicy) { if (acceptorTypeListsContainsAcceptorType(AcceptorType.GREAT_DELUGE) || acceptorConfig.getGreatDelugeWaterLevelIncrementScore() != null diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java index b07c045053..686dfe8c38 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java @@ -65,7 +65,7 @@ public boolean isAccepted(LocalSearchMoveScope moveScope) { var accepted = evaluate(moveScope); var improved = enabled && moveScope.getScore().compareTo(moveScope.getStepScope().getPhaseScope().getBestScore()) > 0; if (improved) { - restartStrategy.reset(); + restartStrategy.reset(moveScope); } return accepted; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricRestartStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricRestartStrategy.java new file mode 100644 index 0000000000..6db9b09758 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricRestartStrategy.java @@ -0,0 +1,89 @@ +package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; + +import java.time.Clock; + +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Restart strategy, which exponentially increases the metric that triggers the restart process. + * The first restart occurs after the {@code grace period + 1 * scalingFactor} metric. + * Following that, the metric increases exponentially: 1, 2, 3, 5, 7, 10, 14... + *

+ * The strategy is based on the work: Search in a Small World by Toby Walsh + * + * @param the solution type + */ +public abstract class AbstractGeometricRestartStrategy implements RestartStrategy { + private static final double GEOMETRIC_FACTOR = 1.4; // Value extracted from the cited paper + protected final Clock clock; + protected final Logger logger = LoggerFactory.getLogger(AbstractGeometricRestartStrategy.class); + protected final double scalingFactor; + + private boolean gracePeriodFinished; + private boolean restartTriggered; + private long gracePeriodMillis; + protected long nextRestart; + protected double currentGeometricGrowFactor; + + protected AbstractGeometricRestartStrategy(Clock clock, double scalingFactor) { + this.clock = clock; + this.scalingFactor = scalingFactor; + } + + @Override + public void phaseStarted(LocalSearchPhaseScope phaseScope) { + restartTriggered = false; + if (gracePeriodMillis == 0) { + // 10 seconds of grace period + gracePeriodMillis = clock.millis() + 10_000; + } + } + + @Override + public void phaseEnded(LocalSearchPhaseScope phaseScope) { + // Do nothing + } + + @Override + public void solvingStarted(SolverScope solverScope) { + currentGeometricGrowFactor = 1; + gracePeriodMillis = 0; + gracePeriodFinished = false; + nextRestart = (long) Math.ceil(currentGeometricGrowFactor * scalingFactor); + } + + @Override + public void solvingEnded(SolverScope solverScope) { + // Do nothing + } + + @Override + public boolean isTriggered(LocalSearchMoveScope moveScope) { + if (!restartTriggered && (gracePeriodFinished || clock.millis() >= gracePeriodMillis)) { + gracePeriodFinished = true; + var triggered = process(moveScope); + if (triggered) { + currentGeometricGrowFactor = Math.ceil(currentGeometricGrowFactor * GEOMETRIC_FACTOR); + nextRestart = (long) Math.ceil(currentGeometricGrowFactor * scalingFactor); + restartTriggered = true; + } + } + return restartTriggered; + } + + protected boolean isGracePeriodFinished() { + return gracePeriodFinished; + } + + protected void disableTriggerFlag() { + restartTriggered = false; + } + + abstract boolean process(LocalSearchMoveScope moveScope); + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/NoOpRestartStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/NoOpRestartStrategy.java index 25848caee3..e113fd8895 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/NoOpRestartStrategy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/NoOpRestartStrategy.java @@ -13,7 +13,7 @@ public boolean isTriggered(LocalSearchMoveScope moveScope) { } @Override - public void reset() { + public void reset(LocalSearchMoveScope moveScope) { // Do nothing } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/RestartStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/RestartStrategy.java index 1946a1433c..e2775a6285 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/RestartStrategy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/RestartStrategy.java @@ -7,5 +7,5 @@ public interface RestartStrategy extends LocalSearchPhaseLifecycleLis boolean isTriggered(LocalSearchMoveScope moveScope); - void reset(); + void reset(LocalSearchMoveScope moveScope); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountRestartStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountRestartStrategy.java new file mode 100644 index 0000000000..0ecd001199 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountRestartStrategy.java @@ -0,0 +1,78 @@ +package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; + +import java.time.Clock; + +import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; + +/** + * Restart strategy, which exponentially increases the move count values that trigger the restart process. + */ +public class UnimprovedMoveCountRestartStrategy extends AbstractGeometricRestartStrategy { + + protected long lastImprovementMoveCount; + private Score currentBestScore; + + public UnimprovedMoveCountRestartStrategy() { + this(Clock.systemUTC()); + } + + protected UnimprovedMoveCountRestartStrategy(Clock clock) { + // 50k moves as the multiplier + super(clock, 50_000); + } + + @Override + public void phaseStarted(LocalSearchPhaseScope phaseScope) { + super.phaseStarted(phaseScope); + currentBestScore = phaseScope.getBestScore(); + } + + @Override + public void stepStarted(LocalSearchStepScope stepScope) { + this.currentBestScore = stepScope.getPhaseScope().getBestScore(); + } + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void stepEnded(LocalSearchStepScope stepScope) { + // Do not update it during the grace period + if (isGracePeriodFinished() && ((Score) stepScope.getScore()).compareTo(currentBestScore) > 0) { + lastImprovementMoveCount = stepScope.getPhaseScope().getSolverScope().getMoveEvaluationCount(); + } + } + + @Override + public void solvingStarted(SolverScope solverScope) { + super.solvingStarted(solverScope); + lastImprovementMoveCount = 0; + } + + @Override + public boolean process(LocalSearchMoveScope moveScope) { + var currentMoveCount = moveScope.getStepScope().getPhaseScope().getSolverScope().getMoveEvaluationCount(); + if (lastImprovementMoveCount == 0) { + lastImprovementMoveCount = currentMoveCount; + return false; + } + if (currentMoveCount - lastImprovementMoveCount >= nextRestart) { + logger.debug("Restart triggered with geometric factor {}, scaling factor of {}, move count ({})", + currentGeometricGrowFactor, scalingFactor, currentMoveCount); + lastImprovementMoveCount = currentMoveCount; + return true; + } + return false; + } + + @Override + public void reset(LocalSearchMoveScope moveScope) { + disableTriggerFlag(); + // Do not update it during the grace period + if (isGracePeriodFinished()) { + lastImprovementMoveCount = moveScope.getStepScope().getPhaseScope().getSolverScope().getMoveEvaluationCount(); + } + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeGeometricRestartStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeGeometricRestartStrategy.java deleted file mode 100644 index fdf2d52bbb..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeGeometricRestartStrategy.java +++ /dev/null @@ -1,101 +0,0 @@ -package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; - -import java.time.Clock; - -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; -import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; -import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; -import ai.timefold.solver.core.impl.solver.scope.SolverScope; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Restart strategy which exponentially increases the restart times. - * The first restart occurs after one second without improvement. - * Following that, the unimproved timeout increases exponentially: 1s, 1s, 2s, 3s, 4s, 5s, 8s... - *

- * The strategy is based on the work: Search in a Small World by Toby Walsh - */ -public class UnimprovedTimeGeometricRestartStrategy implements RestartStrategy { - private static final double GEOMETRIC_FACTOR = 1.4; // Value extracted from the cited paper - private static final double SCALING_FACTOR = 1.0; - - private final Logger logger = LoggerFactory.getLogger(UnimprovedTimeGeometricRestartStrategy.class); - private final Clock clock; - - protected long lastImprovementMillis; - private Score currentBestScore; - protected boolean restartTriggered; - private double geometricGrowFactor; - protected long nextRestart; - - public UnimprovedTimeGeometricRestartStrategy() { - this(Clock.systemUTC()); - } - - protected UnimprovedTimeGeometricRestartStrategy(Clock clock) { - this.clock = clock; - } - - @Override - public void phaseStarted(LocalSearchPhaseScope phaseScope) { - currentBestScore = phaseScope.getBestScore(); - if (lastImprovementMillis == 0) { - lastImprovementMillis = clock.millis(); - } - restartTriggered = false; - } - - @Override - public void phaseEnded(LocalSearchPhaseScope phaseScope) { - // Do nothing - } - - @Override - public void stepStarted(LocalSearchStepScope stepScope) { - this.currentBestScore = stepScope.getPhaseScope().getBestScore(); - } - - @Override - @SuppressWarnings({ "rawtypes", "unchecked" }) - public void stepEnded(LocalSearchStepScope stepScope) { - if (((Score) stepScope.getScore()).compareTo(currentBestScore) > 0) { - lastImprovementMillis = clock.millis(); - this.currentBestScore = stepScope.getScore(); - } - } - - @Override - public void solvingStarted(SolverScope solverScope) { - lastImprovementMillis = 0; - geometricGrowFactor = 1; - nextRestart = (long) (1_000 * SCALING_FACTOR); - } - - @Override - public void solvingEnded(SolverScope solverScope) { - // Do nothing - } - - @Override - public boolean isTriggered(LocalSearchMoveScope moveScope) { - if (!restartTriggered && lastImprovementMillis > 0 - && clock.millis() - lastImprovementMillis >= nextRestart) { - logger.debug("Restart triggered with geometric factor {} and scaling factor of {}", geometricGrowFactor, - SCALING_FACTOR); - nextRestart = (long) Math.ceil(SCALING_FACTOR * geometricGrowFactor * 1_000); - geometricGrowFactor = Math.ceil(geometricGrowFactor * GEOMETRIC_FACTOR); - lastImprovementMillis = clock.millis(); - restartTriggered = true; - } - return restartTriggered; - } - - @Override - public void reset() { - restartTriggered = false; - lastImprovementMillis = clock.millis(); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeRestartStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeRestartStrategy.java new file mode 100644 index 0000000000..bbeed717d3 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeRestartStrategy.java @@ -0,0 +1,78 @@ +package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; + +import java.time.Clock; + +import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; + +/** + * Restart strategy which exponentially increases the restart times. + */ +public class UnimprovedTimeRestartStrategy extends AbstractGeometricRestartStrategy { + + protected long lastImprovementMillis; + private Score currentBestScore; + + public UnimprovedTimeRestartStrategy() { + this(Clock.systemUTC()); + } + + protected UnimprovedTimeRestartStrategy(Clock clock) { + // 1 second as the multiplier + super(clock, 1_000); + } + + @Override + public void phaseStarted(LocalSearchPhaseScope phaseScope) { + super.phaseStarted(phaseScope); + currentBestScore = phaseScope.getBestScore(); + } + + @Override + public void stepStarted(LocalSearchStepScope stepScope) { + this.currentBestScore = stepScope.getPhaseScope().getBestScore(); + } + + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void stepEnded(LocalSearchStepScope stepScope) { + // Do not update it during the grace period + if (isGracePeriodFinished() && ((Score) stepScope.getScore()).compareTo(currentBestScore) > 0) { + lastImprovementMillis = clock.millis(); + } + } + + @Override + public void solvingStarted(SolverScope solverScope) { + super.solvingStarted(solverScope); + lastImprovementMillis = 0; + } + + @Override + public boolean process(LocalSearchMoveScope moveScope) { + var currentTime = clock.millis(); + if (lastImprovementMillis == 0) { + lastImprovementMillis = currentTime; + return false; + } + if (currentTime - lastImprovementMillis >= nextRestart) { + logger.debug("Restart triggered with geometric factor {} and scaling factor of {}", currentGeometricGrowFactor, + scalingFactor); + lastImprovementMillis = clock.millis(); + return true; + } + return false; + } + + @Override + public void reset(LocalSearchMoveScope moveScope) { + disableTriggerFlag(); + // Do not update it during the grace period + if (isGracePeriodFinished()) { + lastImprovementMillis = clock.millis(); + } + } +} diff --git a/core/src/main/resources/solver.xsd b/core/src/main/resources/solver.xsd index 317fd7d637..4af9326017 100644 --- a/core/src/main/resources/solver.xsd +++ b/core/src/main/resources/solver.xsd @@ -1415,7 +1415,7 @@ - + @@ -1929,6 +1929,18 @@ + + + + + + + + + + + + diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactoryTest.java index 7408f9effa..11eee8ad8f 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactoryTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactoryTest.java @@ -1,5 +1,6 @@ package ai.timefold.solver.core.impl.localsearch.decider.acceptor; +import static ai.timefold.solver.core.config.localsearch.decider.acceptor.RestartType.UNIMPROVED_MOVE_COUNT; import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @@ -114,7 +115,7 @@ void diversifiedLateAcceptanceAcceptor() { void acceptorWithReconfiguration() { var localSearchAcceptorConfig = new LocalSearchAcceptorConfig() .withAcceptorTypeList(List.of(AcceptorType.LATE_ACCEPTANCE)) - .withEnableReconfiguration(true); + .withRestartType(UNIMPROVED_MOVE_COUNT); HeuristicConfigPolicy heuristicConfigPolicy = mock(HeuristicConfigPolicy.class); AcceptorFactory acceptorFactory = AcceptorFactory.create(localSearchAcceptorConfig); var acceptor = acceptorFactory.buildAcceptor(heuristicConfigPolicy); @@ -122,7 +123,7 @@ void acceptorWithReconfiguration() { localSearchAcceptorConfig = new LocalSearchAcceptorConfig() .withAcceptorTypeList(List.of(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE)) - .withEnableReconfiguration(true); + .withRestartType(UNIMPROVED_MOVE_COUNT); acceptorFactory = AcceptorFactory.create(localSearchAcceptorConfig); acceptor = acceptorFactory.buildAcceptor(heuristicConfigPolicy); assertThat(((DiversifiedLateAcceptanceAcceptor) acceptor).isEnabled()).isTrue(); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java index 215849faa7..f6120e1482 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java @@ -177,6 +177,6 @@ void resetReconfiguration() { var moveScope0 = buildMoveScope(stepScope0, -999); stepScope0.getPhaseScope().setLastCompletedStepScope(stepScope0); assertThat(acceptor.isAccepted(moveScope0)).isTrue(); - verify(restartStrategy, times(1)).reset(); + verify(restartStrategy, times(1)).reset(moveScope0); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptorTest.java index 9bf9982730..e67b0b9ee0 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptorTest.java @@ -287,6 +287,6 @@ void resetReconfiguration() { var moveScope0 = buildMoveScope(stepScope0, -999); stepScope0.getPhaseScope().setLastCompletedStepScope(stepScope0); assertThat(acceptor.isAccepted(moveScope0)).isTrue(); - verify(restartStrategy, times(1)).reset(); + verify(restartStrategy, times(1)).reset(moveScope0); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountRestartStrategyTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountRestartStrategyTest.java new file mode 100644 index 0000000000..937acddc20 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountRestartStrategyTest.java @@ -0,0 +1,120 @@ +package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.time.Clock; + +import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class UnimprovedMoveCountRestartStrategyTest { + + @Test + void isTriggered() { + var clock = mock(Clock.class); + var solverScope = mock(SolverScope.class); + var phaseScope = mock(LocalSearchPhaseScope.class); + var stepScope = mock(LocalSearchStepScope.class); + var moveScope = mock(LocalSearchMoveScope.class); + when(moveScope.getStepScope()).thenReturn(stepScope); + when(stepScope.getPhaseScope()).thenReturn(phaseScope); + when(phaseScope.getSolverScope()).thenReturn(solverScope); + when(clock.millis()).thenReturn(1000L, 11000L); + when(solverScope.getMoveEvaluationCount()).thenReturn(1000L); + when(phaseScope.getBestScore()).thenReturn(SimpleScore.of(1000)); + + // Finish grace period + var strategy = new UnimprovedMoveCountRestartStrategy<>(clock); + strategy.solvingStarted(null); + strategy.phaseStarted(phaseScope); + assertThat(strategy.isTriggered(moveScope)).isFalse(); + assertThat(strategy.lastImprovementMoveCount).isEqualTo(1000L); + assertThat(strategy.nextRestart).isEqualTo(50000L); + + // First restart + Mockito.reset(clock); + var firstCount = 60000L; + when(solverScope.getMoveEvaluationCount()).thenReturn(firstCount); + assertThat(strategy.isTriggered(moveScope)).isTrue(); + assertThat(strategy.lastImprovementMoveCount).isEqualTo(firstCount); + assertThat(strategy.nextRestart).isEqualTo(2L * 50000L); + strategy.reset(moveScope); + assertThat(strategy.isTriggered(moveScope)).isFalse(); + + // Second restart + Mockito.reset(clock); + var secondCount = 2L * 50000L + firstCount; + when(solverScope.getMoveEvaluationCount()).thenReturn(secondCount); + assertThat(strategy.isTriggered(moveScope)).isTrue(); + assertThat(strategy.lastImprovementMoveCount).isEqualTo(secondCount); + assertThat(strategy.nextRestart).isEqualTo(3L * 50000L); + strategy.reset(moveScope); + assertThat(strategy.isTriggered(moveScope)).isFalse(); + + // Third restart + var thirdCount = 3L * 50000L + secondCount; + Mockito.reset(clock); + when(solverScope.getMoveEvaluationCount()).thenReturn(thirdCount); + assertThat(strategy.isTriggered(moveScope)).isTrue(); + assertThat(strategy.lastImprovementMoveCount).isEqualTo(thirdCount); + assertThat(strategy.nextRestart).isEqualTo(5L * 50000L); + strategy.reset(moveScope); + } + + @Test + void updateBestSolution() { + var clock = mock(Clock.class); + var solverScope = mock(SolverScope.class); + var phaseScope = mock(LocalSearchPhaseScope.class); + var stepScope = mock(LocalSearchStepScope.class); + var moveScope = mock(LocalSearchMoveScope.class); + when(phaseScope.getSolverScope()).thenReturn(solverScope); + when(stepScope.getPhaseScope()).thenReturn(phaseScope); + when(moveScope.getStepScope()).thenReturn(stepScope); + when(phaseScope.getBestScore()).thenReturn(SimpleScore.of(1000)); + when(stepScope.getScore()).thenReturn(SimpleScore.of(2000)); + when(clock.millis()).thenReturn(1000L, 20000L, 20000L, 20001L); + when(solverScope.getMoveEvaluationCount()).thenReturn(1000L, 1001L); + + var strategy = new UnimprovedMoveCountRestartStrategy<>(clock); + strategy.solvingStarted(mock(SolverScope.class)); + strategy.phaseStarted(phaseScope); + strategy.stepStarted(stepScope); + // Trigger + strategy.isTriggered(moveScope); + // Update the last improvement + strategy.stepEnded(stepScope); + assertThat(strategy.lastImprovementMoveCount).isEqualTo(1001L); + } + + @Test + void reset() { + var clock = mock(Clock.class); + var solverScope = mock(SolverScope.class); + var phaseScope = mock(LocalSearchPhaseScope.class); + var stepScope = mock(LocalSearchStepScope.class); + var moveScope = mock(LocalSearchMoveScope.class); + when(moveScope.getStepScope()).thenReturn(stepScope); + when(stepScope.getPhaseScope()).thenReturn(phaseScope); + when(phaseScope.getSolverScope()).thenReturn(solverScope); + when(clock.millis()).thenReturn(1000L, 11000L); + when(solverScope.getMoveEvaluationCount()).thenReturn(1000L); + + var strategy = new UnimprovedMoveCountRestartStrategy<>(clock); + strategy.solvingStarted(mock(SolverScope.class)); + strategy.phaseStarted(mock(LocalSearchPhaseScope.class)); + // Trigger + strategy.isTriggered(moveScope); + // Reset + strategy.reset(moveScope); + assertThat(strategy.lastImprovementMoveCount).isEqualTo(1000L); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeGeometricRestartStrategyTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeRestartStrategyTest.java similarity index 57% rename from core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeGeometricRestartStrategyTest.java rename to core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeRestartStrategyTest.java index afda704b67..3687a7e563 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeGeometricRestartStrategyTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeRestartStrategyTest.java @@ -10,11 +10,12 @@ import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; import org.junit.jupiter.api.Test; import org.mockito.Mockito; -class UnimprovedTimeGeometricRestartStrategyTest { +class UnimprovedTimeRestartStrategyTest { @Test void isTriggered() { @@ -28,45 +29,51 @@ void isTriggered() { when(phaseScope.getBestScore()).thenReturn(SimpleScore.of(1000)); // Initial values - var strategy = new UnimprovedTimeGeometricRestartStrategy<>(clock); + var strategy = new UnimprovedTimeRestartStrategy<>(clock); + strategy.solvingStarted(null); strategy.phaseStarted(phaseScope); - assertThat(strategy.lastImprovementMillis).isEqualTo(1000L); + assertThat(strategy.lastImprovementMillis).isZero(); assertThat(strategy.nextRestart).isEqualTo(1000L); - assertThat(strategy.restartTriggered).isFalse(); + + // Finish grace period + Mockito.reset(clock); + when(clock.millis()).thenReturn(12000L); + assertThat(strategy.isTriggered(moveScope)).isFalse(); + assertThat(strategy.lastImprovementMillis).isEqualTo(12000L); + assertThat(strategy.nextRestart).isEqualTo(1000L); + strategy.reset(moveScope); + assertThat(strategy.isTriggered(moveScope)).isFalse(); // First restart Mockito.reset(clock); - when(clock.millis()).thenReturn(2000L); + when(clock.millis()).thenReturn(13000L); assertThat(strategy.isTriggered(moveScope)).isTrue(); - assertThat(strategy.lastImprovementMillis).isEqualTo(2000L); - assertThat(strategy.nextRestart).isEqualTo(1000L); - assertThat(strategy.restartTriggered).isTrue(); - strategy.reset(); + assertThat(strategy.lastImprovementMillis).isEqualTo(13000L); + assertThat(strategy.nextRestart).isEqualTo(2000L); + strategy.reset(moveScope); assertThat(strategy.isTriggered(moveScope)).isFalse(); // Second restart Mockito.reset(clock); - when(clock.millis()).thenReturn(3000L); + when(clock.millis()).thenReturn(15000L); assertThat(strategy.isTriggered(moveScope)).isTrue(); - assertThat(strategy.lastImprovementMillis).isEqualTo(3000L); - assertThat(strategy.nextRestart).isEqualTo(2000L); - assertThat(strategy.restartTriggered).isTrue(); - strategy.reset(); + assertThat(strategy.lastImprovementMillis).isEqualTo(15000L); + assertThat(strategy.nextRestart).isEqualTo(3000L); + strategy.reset(moveScope); // Third restart Mockito.reset(clock); - when(clock.millis()).thenReturn(5000L); + when(clock.millis()).thenReturn(18000L); assertThat(strategy.isTriggered(moveScope)).isTrue(); - assertThat(strategy.lastImprovementMillis).isEqualTo(5000L); - assertThat(strategy.nextRestart).isEqualTo(3000L); - assertThat(strategy.restartTriggered).isTrue(); - strategy.reset(); + assertThat(strategy.lastImprovementMillis).isEqualTo(18000L); + assertThat(strategy.nextRestart).isEqualTo(5000L); + strategy.reset(moveScope); } @Test void updateBestSolution() { var clock = mock(Clock.class); - when(clock.millis()).thenReturn(1000L, 2000L); + when(clock.millis()).thenReturn(1000L, 20000L, 20000L, 20001L); var phaseScope = mock(LocalSearchPhaseScope.class); var stepScope = mock(LocalSearchStepScope.class); var moveScope = mock(LocalSearchMoveScope.class); @@ -75,20 +82,30 @@ void updateBestSolution() { when(phaseScope.getBestScore()).thenReturn(SimpleScore.of(1000)); when(stepScope.getScore()).thenReturn(SimpleScore.of(2000)); - var strategy = new UnimprovedTimeGeometricRestartStrategy<>(clock); + var strategy = new UnimprovedTimeRestartStrategy<>(clock); + strategy.solvingStarted(mock(SolverScope.class)); strategy.phaseStarted(phaseScope); strategy.stepStarted(stepScope); + // Trigger + strategy.isTriggered(moveScope); + // Update the last improvement strategy.stepEnded(stepScope); - assertThat(strategy.lastImprovementMillis).isEqualTo(2000L); + assertThat(strategy.lastImprovementMillis).isEqualTo(20001L); } @Test void reset() { var clock = mock(Clock.class); - when(clock.millis()).thenReturn(1L); - var strategy = new UnimprovedTimeGeometricRestartStrategy<>(clock); - strategy.reset(); - assertThat(strategy.lastImprovementMillis).isEqualTo(1L); - assertThat(strategy.restartTriggered).isFalse(); + var moveScope = mock(LocalSearchMoveScope.class); + when(clock.millis()).thenReturn(1000L, 11000L); + + var strategy = new UnimprovedTimeRestartStrategy<>(clock); + strategy.solvingStarted(mock(SolverScope.class)); + strategy.phaseStarted(mock(LocalSearchPhaseScope.class)); + // Trigger + strategy.isTriggered(moveScope); + // Reset + strategy.reset(moveScope); + assertThat(strategy.lastImprovementMillis).isEqualTo(11000L); } } From 020c3cd60648c3a9312841f4c0f793ca266adee2 Mon Sep 17 00:00:00 2001 From: Fred Date: Fri, 24 Jan 2025 08:42:20 -0300 Subject: [PATCH 10/31] chore: minor fix --- .../impl/localsearch/decider/acceptor/AcceptorFactory.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java index 2b1957cec9..df285ef364 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java @@ -18,6 +18,7 @@ import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.NoOpRestartStrategy; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.RestartStrategy; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.UnimprovedMoveCountRestartStrategy; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.UnimprovedTimeRestartStrategy; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.simulatedannealing.SimulatedAnnealingAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stepcountinghillclimbing.StepCountingHillClimbingAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.tabu.EntityTabuAcceptor; @@ -250,8 +251,8 @@ private RestartStrategy buildRestartStrategy() { var enableReconfiguration = acceptorConfig.getReconfigurationRestartType() != null; if (enableReconfiguration) { return switch (acceptorConfig.getReconfigurationRestartType()) { - case UNIMPROVED_TIME -> new UnimprovedMoveCountRestartStrategy<>(); case UNIMPROVED_MOVE_COUNT -> new UnimprovedMoveCountRestartStrategy<>(); + case UNIMPROVED_TIME -> new UnimprovedTimeRestartStrategy<>(); }; } return restartStrategy; From 8f89f7408e5f9b75ed8c90d65b779c34f47b47a7 Mon Sep 17 00:00:00 2001 From: Fred Date: Fri, 24 Jan 2025 09:06:47 -0300 Subject: [PATCH 11/31] chore: minor fix --- .../RestoreBestSolutionReconfigurationStrategy.java | 4 ++-- .../solver/core/impl/phase/scope/AbstractPhaseScope.java | 2 +- .../decider/reconfiguration/ReconfigurationStrategyTest.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionReconfigurationStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionReconfigurationStrategy.java index 5494fc1405..5945f65ec6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionReconfigurationStrategy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionReconfigurationStrategy.java @@ -35,8 +35,8 @@ public > Score_ apply(AbstractStepScope moveSelector.phaseStarted(stepScope.getPhaseScope()); // 2 - The acceptor will restart its search from the updated working solution (last best solution) acceptor.phaseStarted((LocalSearchPhaseScope) stepScope.getPhaseScope()); - // Reset it as the best solution is already restored - stepScope.getPhaseScope().resetReconfiguration(); + // Cancel it as the best solution is already restored + stepScope.getPhaseScope().cancelReconfiguration(); return (Score_) solverScope.getBestScore(); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java index 5e58ad887d..938f7b1f01 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java @@ -256,7 +256,7 @@ public void triggerReconfiguration() { this.reconfigurationTriggered = true; } - public void resetReconfiguration() { + public void cancelReconfiguration() { this.reconfigurationTriggered = false; } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategyTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategyTest.java index 1f27938d1d..0f9eaee316 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategyTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategyTest.java @@ -42,7 +42,7 @@ void restoreBestSolution() { // Restore the best solution verify(solverScope, times(1)).setWorkingSolutionFromBestSolution(); // Restart the phase - verify(phaseScope, times(1)).resetReconfiguration(); + verify(phaseScope, times(1)).cancelReconfiguration(); verify(moveSelector, times(1)).phaseStarted(any()); verify(acceptor, times(1)).phaseStarted(any()); } From 7a0fc0e6da89a32c5e4358f6514affa2886833fb Mon Sep 17 00:00:00 2001 From: Fred Date: Fri, 24 Jan 2025 10:46:20 -0300 Subject: [PATCH 12/31] chore: simplify logic --- benchmark/src/main/resources/benchmark.xsd | 21 ---- core/src/build/revapi-differences.json | 11 -- .../acceptor/LocalSearchAcceptorConfig.java | 17 --- .../decider/acceptor/RestartType.java | 9 -- .../DefaultLocalSearchPhaseFactory.java | 18 +-- .../decider/acceptor/AcceptorFactory.java | 23 +--- .../acceptor/ReconfigurableAcceptor.java | 12 +- .../DiversifiedLateAcceptanceAcceptor.java | 4 +- .../LateAcceptanceAcceptor.java | 4 +- .../acceptor/restart/NoOpRestartStrategy.java | 49 -------- .../UnimprovedTimeRestartStrategy.java | 78 ------------ core/src/main/resources/solver.xsd | 14 --- .../decider/acceptor/AcceptorFactoryTest.java | 21 ---- ...DiversifiedLateAcceptanceAcceptorTest.java | 13 +- .../LateAcceptanceAcceptorTest.java | 21 ++-- .../restart/NoOpRestartStrategyTest.java | 15 --- .../UnimprovedTimeRestartStrategyTest.java | 111 ------------------ .../ReconfigurationStrategyTest.java | 9 -- 18 files changed, 32 insertions(+), 418 deletions(-) delete mode 100644 core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/RestartType.java delete mode 100644 core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/NoOpRestartStrategy.java delete mode 100644 core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeRestartStrategy.java delete mode 100644 core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/NoOpRestartStrategyTest.java delete mode 100644 core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeRestartStrategyTest.java diff --git a/benchmark/src/main/resources/benchmark.xsd b/benchmark/src/main/resources/benchmark.xsd index 453d45a246..c015895395 100644 --- a/benchmark/src/main/resources/benchmark.xsd +++ b/benchmark/src/main/resources/benchmark.xsd @@ -2414,9 +2414,6 @@ - - - @@ -3155,24 +3152,6 @@ - - - - - - - - - - - - - - - - - - diff --git a/core/src/build/revapi-differences.json b/core/src/build/revapi-differences.json index b659d2a4de..22164eafe1 100644 --- a/core/src/build/revapi-differences.json +++ b/core/src/build/revapi-differences.json @@ -102,17 +102,6 @@ "oldValue": "{\"terminationClass\", \"terminationCompositionStyle\", \"spentLimit\", \"millisecondsSpentLimit\", \"secondsSpentLimit\", \"minutesSpentLimit\", \"hoursSpentLimit\", \"daysSpentLimit\", \"unimprovedSpentLimit\", \"unimprovedMillisecondsSpentLimit\", \"unimprovedSecondsSpentLimit\", \"unimprovedMinutesSpentLimit\", \"unimprovedHoursSpentLimit\", \"unimprovedDaysSpentLimit\", \"unimprovedScoreDifferenceThreshold\", \"bestScoreLimit\", \"bestScoreFeasible\", \"stepCountLimit\", \"unimprovedStepCountLimit\", \"scoreCalculationCountLimit\", \"terminationConfigList\"}", "newValue": "{\"terminationClass\", \"terminationCompositionStyle\", \"diminishedReturnsConfig\", \"spentLimit\", \"millisecondsSpentLimit\", \"secondsSpentLimit\", \"minutesSpentLimit\", \"hoursSpentLimit\", \"daysSpentLimit\", \"unimprovedSpentLimit\", \"unimprovedMillisecondsSpentLimit\", \"unimprovedSecondsSpentLimit\", \"unimprovedMinutesSpentLimit\", \"unimprovedHoursSpentLimit\", \"unimprovedDaysSpentLimit\", \"unimprovedScoreDifferenceThreshold\", \"bestScoreLimit\", \"bestScoreFeasible\", \"stepCountLimit\", \"unimprovedStepCountLimit\", \"scoreCalculationCountLimit\", \"moveCountLimit\", \"terminationConfigList\"}", "justification": "Added support for the new diminished returns termination type" - }, - { - "ignore": true, - "code": "java.annotation.attributeValueChanged", - "old": "class ai.timefold.solver.core.config.localsearch.decider.acceptor.LocalSearchAcceptorConfig", - "new": "class ai.timefold.solver.core.config.localsearch.decider.acceptor.LocalSearchAcceptorConfig", - "annotationType": "jakarta.xml.bind.annotation.XmlType", - "attribute": "propOrder", - "oldValue": "{\"acceptorTypeList\", \"entityTabuSize\", \"entityTabuRatio\", \"fadingEntityTabuSize\", \"fadingEntityTabuRatio\", \"valueTabuSize\", \"valueTabuRatio\", \"fadingValueTabuSize\", \"fadingValueTabuRatio\", \"moveTabuSize\", \"fadingMoveTabuSize\", \"undoMoveTabuSize\", \"fadingUndoMoveTabuSize\", \"simulatedAnnealingStartingTemperature\", \"lateAcceptanceSize\", \"greatDelugeWaterLevelIncrementScore\", \"greatDelugeWaterLevelIncrementRatio\", \"stepCountingHillClimbingSize\", \"stepCountingHillClimbingType\"}", - "newValue": "{\"acceptorTypeList\", \"reconfigurationRestartType\", \"entityTabuSize\", \"entityTabuRatio\", \"fadingEntityTabuSize\", \"fadingEntityTabuRatio\", \"valueTabuSize\", \"valueTabuRatio\", \"fadingValueTabuSize\", \"fadingValueTabuRatio\", \"moveTabuSize\", \"fadingMoveTabuSize\", \"undoMoveTabuSize\", \"fadingUndoMoveTabuSize\", \"simulatedAnnealingStartingTemperature\", \"lateAcceptanceSize\", \"greatDelugeWaterLevelIncrementScore\", \"greatDelugeWaterLevelIncrementRatio\", \"stepCountingHillClimbingSize\", \"stepCountingHillClimbingType\"}", - "justification": "Add the acceptor reconfiguration setting" } ] } diff --git a/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/LocalSearchAcceptorConfig.java b/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/LocalSearchAcceptorConfig.java index ccf93163e0..174e35512d 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/LocalSearchAcceptorConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/LocalSearchAcceptorConfig.java @@ -15,7 +15,6 @@ @XmlType(propOrder = { "acceptorTypeList", - "reconfigurationRestartType", "entityTabuSize", "entityTabuRatio", "fadingEntityTabuSize", @@ -39,7 +38,6 @@ public class LocalSearchAcceptorConfig extends AbstractConfig acceptorTypeList = null; - private RestartType reconfigurationRestartType = null; protected Integer entityTabuSize = null; protected Double entityTabuRatio = null; @@ -80,14 +78,6 @@ public void setAcceptorTypeList(@Nullable List acceptorTypeList) { this.acceptorTypeList = acceptorTypeList; } - public @Nullable RestartType getReconfigurationRestartType() { - return reconfigurationRestartType; - } - - public void setReconfigurationRestartType(@Nullable RestartType reconfigurationRestartType) { - this.reconfigurationRestartType = reconfigurationRestartType; - } - public @Nullable Integer getEntityTabuSize() { return entityTabuSize; } @@ -273,11 +263,6 @@ public void setStepCountingHillClimbingType(@Nullable StepCountingHillClimbingTy return this; } - public @NonNull LocalSearchAcceptorConfig withRestartType(@NonNull RestartType restartType) { - this.reconfigurationRestartType = restartType; - return this; - } - public @NonNull LocalSearchAcceptorConfig withEntityTabuSize(@NonNull Integer entityTabuSize) { this.entityTabuSize = entityTabuSize; return this; @@ -382,8 +367,6 @@ public LocalSearchAcceptorConfig withFadingUndoMoveTabuSize(Integer fadingUndoMo } } } - reconfigurationRestartType = ConfigUtils.inheritOverwritableProperty(reconfigurationRestartType, - inheritedConfig.getReconfigurationRestartType()); entityTabuSize = ConfigUtils.inheritOverwritableProperty(entityTabuSize, inheritedConfig.getEntityTabuSize()); entityTabuRatio = ConfigUtils.inheritOverwritableProperty(entityTabuRatio, inheritedConfig.getEntityTabuRatio()); fadingEntityTabuSize = ConfigUtils.inheritOverwritableProperty(fadingEntityTabuSize, diff --git a/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/RestartType.java b/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/RestartType.java deleted file mode 100644 index 7007cdf085..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/config/localsearch/decider/acceptor/RestartType.java +++ /dev/null @@ -1,9 +0,0 @@ -package ai.timefold.solver.core.config.localsearch.decider.acceptor; - -import jakarta.xml.bind.annotation.XmlEnum; - -@XmlEnum -public enum RestartType { - UNIMPROVED_TIME, - UNIMPROVED_MOVE_COUNT -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java index 32de0b5568..12164c554b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java @@ -32,8 +32,6 @@ import ai.timefold.solver.core.impl.localsearch.decider.acceptor.AcceptorFactory; import ai.timefold.solver.core.impl.localsearch.decider.forager.LocalSearchForager; import ai.timefold.solver.core.impl.localsearch.decider.forager.LocalSearchForagerFactory; -import ai.timefold.solver.core.impl.localsearch.decider.reconfiguration.NoOpReconfigurationStrategy; -import ai.timefold.solver.core.impl.localsearch.decider.reconfiguration.ReconfigurationStrategy; import ai.timefold.solver.core.impl.localsearch.decider.reconfiguration.RestoreBestSolutionReconfigurationStrategy; import ai.timefold.solver.core.impl.phase.AbstractPhaseFactory; import ai.timefold.solver.core.impl.solver.recaller.BestSolutionRecaller; @@ -68,7 +66,7 @@ private LocalSearchDecider buildDecider(HeuristicConfigPolicy termination) { var moveSelector = buildMoveSelector(configPolicy); var acceptor = buildAcceptor(configPolicy); - var reconfigurationStrategy = buildReconfigurationStrategy(configPolicy, moveSelector, acceptor); + var reconfigurationStrategy = new RestoreBestSolutionReconfigurationStrategy<>(moveSelector, acceptor); var forager = buildForager(configPolicy); if (moveSelector.isNeverEnding() && !forager.supportsNeverEndingMoveSelector()) { throw new IllegalStateException("The moveSelector (" + moveSelector @@ -200,20 +198,6 @@ protected MoveSelector buildMoveSelector(HeuristicConfigPolicy buildReconfigurationStrategy(HeuristicConfigPolicy configPolicy, - MoveSelector moveSelector, Acceptor acceptor) { - var acceptorConfig = phaseConfig.getAcceptorConfig(); - if (acceptorConfig != null) { - var enableReconfiguration = - acceptorConfig.getReconfigurationRestartType() != null; - if (enableReconfiguration) { - configPolicy.ensurePreviewFeature(PreviewFeature.RECONFIGURATION); - return new RestoreBestSolutionReconfigurationStrategy<>(moveSelector, acceptor); - } - } - return new NoOpReconfigurationStrategy<>(); - } - private UnionMoveSelectorConfig determineDefaultMoveSelectorConfig(HeuristicConfigPolicy configPolicy) { var solutionDescriptor = configPolicy.getSolutionDescriptor(); var basicVariableDescriptorList = solutionDescriptor.getEntityDescriptors().stream() diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java index df285ef364..c589a1fdde 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java @@ -15,10 +15,7 @@ import ai.timefold.solver.core.impl.localsearch.decider.acceptor.hillclimbing.HillClimbingAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance.DiversifiedLateAcceptanceAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance.LateAcceptanceAcceptor; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.NoOpRestartStrategy; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.RestartStrategy; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.UnimprovedMoveCountRestartStrategy; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.UnimprovedTimeRestartStrategy; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.simulatedannealing.SimulatedAnnealingAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stepcountinghillclimbing.StepCountingHillClimbingAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.tabu.EntityTabuAcceptor; @@ -224,9 +221,7 @@ private Optional> buildMoveTabuAcceptor(HeuristicCon if (acceptorTypeListsContainsAcceptorType(AcceptorType.LATE_ACCEPTANCE) || (!acceptorTypeListsContainsAcceptorType(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE) && acceptorConfig.getLateAcceptanceSize() != null)) { - var restartStrategy = buildRestartStrategy(); - var acceptor = - new LateAcceptanceAcceptor<>(!(restartStrategy instanceof NoOpRestartStrategy), restartStrategy); + var acceptor = new LateAcceptanceAcceptor<>(new UnimprovedMoveCountRestartStrategy()); acceptor.setLateAcceptanceSize(Objects.requireNonNullElse(acceptorConfig.getLateAcceptanceSize(), 400)); return Optional.of(acceptor); } @@ -237,27 +232,13 @@ private Optional> buildMoveTabuAcceptor(HeuristicCon buildDiversifiedLateAcceptanceAcceptor(HeuristicConfigPolicy configPolicy) { if (acceptorTypeListsContainsAcceptorType(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE)) { configPolicy.ensurePreviewFeature(PreviewFeature.DIVERSIFIED_LATE_ACCEPTANCE); - var restartStrategy = buildRestartStrategy(); - var acceptor = new DiversifiedLateAcceptanceAcceptor<>(!(restartStrategy instanceof NoOpRestartStrategy), - restartStrategy); + var acceptor = new DiversifiedLateAcceptanceAcceptor<>(new UnimprovedMoveCountRestartStrategy()); acceptor.setLateAcceptanceSize(Objects.requireNonNullElse(acceptorConfig.getLateAcceptanceSize(), 5)); return Optional.of(acceptor); } return Optional.empty(); } - private RestartStrategy buildRestartStrategy() { - RestartStrategy restartStrategy = new NoOpRestartStrategy<>(); - var enableReconfiguration = acceptorConfig.getReconfigurationRestartType() != null; - if (enableReconfiguration) { - return switch (acceptorConfig.getReconfigurationRestartType()) { - case UNIMPROVED_MOVE_COUNT -> new UnimprovedMoveCountRestartStrategy<>(); - case UNIMPROVED_TIME -> new UnimprovedTimeRestartStrategy<>(); - }; - } - return restartStrategy; - } - private Optional> buildGreatDelugeAcceptor(HeuristicConfigPolicy configPolicy) { if (acceptorTypeListsContainsAcceptorType(AcceptorType.GREAT_DELUGE) || acceptorConfig.getGreatDelugeWaterLevelIncrementScore() != null diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java index 686dfe8c38..70a7ed0772 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java @@ -14,17 +14,11 @@ public abstract class ReconfigurableAcceptor extends AbstractAcceptor { private final RestartStrategy restartStrategy; - private final boolean enabled; - protected ReconfigurableAcceptor(boolean enabled, RestartStrategy restartStrategy) { - this.enabled = enabled; + protected ReconfigurableAcceptor(RestartStrategy restartStrategy) { this.restartStrategy = restartStrategy; } - protected boolean isEnabled() { - return enabled; - } - @Override public void solvingStarted(SolverScope solverScope) { super.solvingStarted(solverScope); @@ -58,12 +52,12 @@ public void stepEnded(LocalSearchStepScope stepScope) { @Override @SuppressWarnings("unchecked") public boolean isAccepted(LocalSearchMoveScope moveScope) { - if (enabled && restartStrategy.isTriggered(moveScope)) { + if (restartStrategy.isTriggered(moveScope)) { moveScope.getStepScope().getPhaseScope().triggerReconfiguration(); return true; } var accepted = evaluate(moveScope); - var improved = enabled && moveScope.getScore().compareTo(moveScope.getStepScope().getPhaseScope().getBestScore()) > 0; + var improved = moveScope.getScore().compareTo(moveScope.getStepScope().getPhaseScope().getBestScore()) > 0; if (improved) { restartStrategy.reset(moveScope); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java index 586fc0d544..f6eabb38a3 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java @@ -20,8 +20,8 @@ public class DiversifiedLateAcceptanceAcceptor extends Reconfigurable protected Score[] previousScores; protected int lateScoreIndex = -1; - public DiversifiedLateAcceptanceAcceptor(boolean enableReconfiguration, RestartStrategy restartStrategy) { - super(enableReconfiguration, restartStrategy); + public DiversifiedLateAcceptanceAcceptor(RestartStrategy restartStrategy) { + super(restartStrategy); } public void setLateAcceptanceSize(int lateAcceptanceSize) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java index c45a54540e..3801208157 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java @@ -17,8 +17,8 @@ public class LateAcceptanceAcceptor extends ReconfigurableAcceptor[] previousScores; protected int lateScoreIndex = -1; - public LateAcceptanceAcceptor(boolean enableReconfiguration, RestartStrategy restartStrategy) { - super(enableReconfiguration, restartStrategy); + public LateAcceptanceAcceptor(RestartStrategy restartStrategy) { + super(restartStrategy); } public void setLateAcceptanceSize(int lateAcceptanceSize) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/NoOpRestartStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/NoOpRestartStrategy.java deleted file mode 100644 index e113fd8895..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/NoOpRestartStrategy.java +++ /dev/null @@ -1,49 +0,0 @@ -package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; - -import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; -import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; -import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; -import ai.timefold.solver.core.impl.solver.scope.SolverScope; - -public class NoOpRestartStrategy implements RestartStrategy { - - @Override - public boolean isTriggered(LocalSearchMoveScope moveScope) { - return false; - } - - @Override - public void reset(LocalSearchMoveScope moveScope) { - // Do nothing - } - - @Override - public void phaseStarted(LocalSearchPhaseScope phaseScope) { - // Do nothing - } - - @Override - public void stepStarted(LocalSearchStepScope stepScope) { - // Do nothing - } - - @Override - public void stepEnded(LocalSearchStepScope stepScope) { - // Do nothing - } - - @Override - public void phaseEnded(LocalSearchPhaseScope phaseScope) { - // Do nothing - } - - @Override - public void solvingStarted(SolverScope solverScope) { - // Do nothing - } - - @Override - public void solvingEnded(SolverScope solverScope) { - // Do nothing - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeRestartStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeRestartStrategy.java deleted file mode 100644 index bbeed717d3..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeRestartStrategy.java +++ /dev/null @@ -1,78 +0,0 @@ -package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; - -import java.time.Clock; - -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; -import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; -import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; -import ai.timefold.solver.core.impl.solver.scope.SolverScope; - -/** - * Restart strategy which exponentially increases the restart times. - */ -public class UnimprovedTimeRestartStrategy extends AbstractGeometricRestartStrategy { - - protected long lastImprovementMillis; - private Score currentBestScore; - - public UnimprovedTimeRestartStrategy() { - this(Clock.systemUTC()); - } - - protected UnimprovedTimeRestartStrategy(Clock clock) { - // 1 second as the multiplier - super(clock, 1_000); - } - - @Override - public void phaseStarted(LocalSearchPhaseScope phaseScope) { - super.phaseStarted(phaseScope); - currentBestScore = phaseScope.getBestScore(); - } - - @Override - public void stepStarted(LocalSearchStepScope stepScope) { - this.currentBestScore = stepScope.getPhaseScope().getBestScore(); - } - - @Override - @SuppressWarnings({ "rawtypes", "unchecked" }) - public void stepEnded(LocalSearchStepScope stepScope) { - // Do not update it during the grace period - if (isGracePeriodFinished() && ((Score) stepScope.getScore()).compareTo(currentBestScore) > 0) { - lastImprovementMillis = clock.millis(); - } - } - - @Override - public void solvingStarted(SolverScope solverScope) { - super.solvingStarted(solverScope); - lastImprovementMillis = 0; - } - - @Override - public boolean process(LocalSearchMoveScope moveScope) { - var currentTime = clock.millis(); - if (lastImprovementMillis == 0) { - lastImprovementMillis = currentTime; - return false; - } - if (currentTime - lastImprovementMillis >= nextRestart) { - logger.debug("Restart triggered with geometric factor {} and scaling factor of {}", currentGeometricGrowFactor, - scalingFactor); - lastImprovementMillis = clock.millis(); - return true; - } - return false; - } - - @Override - public void reset(LocalSearchMoveScope moveScope) { - disableTriggerFlag(); - // Do not update it during the grace period - if (isGracePeriodFinished()) { - lastImprovementMillis = clock.millis(); - } - } -} diff --git a/core/src/main/resources/solver.xsd b/core/src/main/resources/solver.xsd index 4af9326017..f5312eefb6 100644 --- a/core/src/main/resources/solver.xsd +++ b/core/src/main/resources/solver.xsd @@ -1415,8 +1415,6 @@ - - @@ -1929,18 +1927,6 @@ - - - - - - - - - - - - diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactoryTest.java index 11eee8ad8f..3bcf659abf 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactoryTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactoryTest.java @@ -1,6 +1,5 @@ package ai.timefold.solver.core.impl.localsearch.decider.acceptor; -import static ai.timefold.solver.core.config.localsearch.decider.acceptor.RestartType.UNIMPROVED_MOVE_COUNT; import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @@ -77,7 +76,6 @@ void lateAcceptanceAcceptor() { AcceptorFactory acceptorFactory = AcceptorFactory.create(localSearchAcceptorConfig); var acceptor = acceptorFactory.buildAcceptor(heuristicConfigPolicy); assertThat(acceptor).isExactlyInstanceOf(LateAcceptanceAcceptor.class); - assertThat(((LateAcceptanceAcceptor) acceptor).isEnabled()).isFalse(); localSearchAcceptorConfig = new LocalSearchAcceptorConfig() .withLateAcceptanceSize(10); @@ -94,7 +92,6 @@ void diversifiedLateAcceptanceAcceptor() { AcceptorFactory acceptorFactory = AcceptorFactory.create(localSearchAcceptorConfig); var acceptor = acceptorFactory.buildAcceptor(heuristicConfigPolicy); assertThat(acceptor).isExactlyInstanceOf(DiversifiedLateAcceptanceAcceptor.class); - assertThat(((DiversifiedLateAcceptanceAcceptor) acceptor).isEnabled()).isFalse(); localSearchAcceptorConfig = new LocalSearchAcceptorConfig() .withAcceptorTypeList(List.of(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE)) @@ -110,22 +107,4 @@ void diversifiedLateAcceptanceAcceptor() { AcceptorFactory badAcceptorFactory = AcceptorFactory.create(localSearchAcceptorConfig); assertThatIllegalStateException().isThrownBy(() -> badAcceptorFactory.buildAcceptor(heuristicConfigPolicy)); } - - @Test - void acceptorWithReconfiguration() { - var localSearchAcceptorConfig = new LocalSearchAcceptorConfig() - .withAcceptorTypeList(List.of(AcceptorType.LATE_ACCEPTANCE)) - .withRestartType(UNIMPROVED_MOVE_COUNT); - HeuristicConfigPolicy heuristicConfigPolicy = mock(HeuristicConfigPolicy.class); - AcceptorFactory acceptorFactory = AcceptorFactory.create(localSearchAcceptorConfig); - var acceptor = acceptorFactory.buildAcceptor(heuristicConfigPolicy); - assertThat(((LateAcceptanceAcceptor) acceptor).isEnabled()).isTrue(); - - localSearchAcceptorConfig = new LocalSearchAcceptorConfig() - .withAcceptorTypeList(List.of(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE)) - .withRestartType(UNIMPROVED_MOVE_COUNT); - acceptorFactory = AcceptorFactory.create(localSearchAcceptorConfig); - acceptor = acceptorFactory.buildAcceptor(heuristicConfigPolicy); - assertThat(((DiversifiedLateAcceptanceAcceptor) acceptor).isEnabled()).isTrue(); - } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java index f6120e1482..8993839fe1 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java @@ -6,7 +6,6 @@ import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.AbstractAcceptorTest; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.NoOpRestartStrategy; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.RestartStrategy; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; @@ -18,7 +17,9 @@ class DiversifiedLateAcceptanceAcceptorTest extends AbstractAcceptorTest { @Test void acceptanceCriterion() { - var acceptor = new DiversifiedLateAcceptanceAcceptor<>(false, new NoOpRestartStrategy<>()); + var restartStrategy = mock(RestartStrategy.class); + when(restartStrategy.isTriggered(any())).thenReturn(false); + var acceptor = new DiversifiedLateAcceptanceAcceptor<>(restartStrategy); acceptor.setLateAcceptanceSize(3); var solverScope = new SolverScope<>(); @@ -55,7 +56,9 @@ void acceptanceCriterion() { @Test void replacementCriterion() { - var acceptor = new DiversifiedLateAcceptanceAcceptor<>(false, new NoOpRestartStrategy<>()); + var restartStrategy = mock(RestartStrategy.class); + when(restartStrategy.isTriggered(any())).thenReturn(false); + var acceptor = new DiversifiedLateAcceptanceAcceptor<>(restartStrategy); acceptor.setLateAcceptanceSize(3); var solverScope = new SolverScope<>(); @@ -151,7 +154,7 @@ void replacementCriterion() { void triggerReconfiguration() { var restartStrategy = mock(RestartStrategy.class); when(restartStrategy.isTriggered(any())).thenReturn(true); - var acceptor = new DiversifiedLateAcceptanceAcceptor<>(true, restartStrategy); + var acceptor = new DiversifiedLateAcceptanceAcceptor<>(restartStrategy); acceptor.setLateAcceptanceSize(3); var solverScope = new SolverScope<>(); var phaseScope = new LocalSearchPhaseScope<>(solverScope, 0); @@ -165,7 +168,7 @@ void triggerReconfiguration() { @Test void resetReconfiguration() { var restartStrategy = mock(RestartStrategy.class); - var acceptor = new DiversifiedLateAcceptanceAcceptor<>(true, restartStrategy); + var acceptor = new DiversifiedLateAcceptanceAcceptor<>(restartStrategy); acceptor.setLateAcceptanceSize(3); var solverScope = new SolverScope<>(); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptorTest.java index e67b0b9ee0..9a0894bb07 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptorTest.java @@ -8,7 +8,6 @@ import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.AbstractAcceptorTest; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.NoOpRestartStrategy; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.RestartStrategy; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; @@ -20,7 +19,9 @@ class LateAcceptanceAcceptorTest extends AbstractAcceptorTest { @Test void lateAcceptanceSize() { - var acceptor = new LateAcceptanceAcceptor<>(false, new NoOpRestartStrategy<>()); + var restartStrategy = mock(RestartStrategy.class); + when(restartStrategy.isTriggered(any())).thenReturn(false); + var acceptor = new LateAcceptanceAcceptor<>(restartStrategy); acceptor.setLateAcceptanceSize(3); acceptor.setHillClimbingEnabled(false); @@ -133,7 +134,9 @@ void lateAcceptanceSize() { @Test void hillClimbingEnabled() { - var acceptor = new LateAcceptanceAcceptor<>(false, new NoOpRestartStrategy<>()); + var restartStrategy = mock(RestartStrategy.class); + when(restartStrategy.isTriggered(any())).thenReturn(false); + var acceptor = new LateAcceptanceAcceptor<>(restartStrategy); acceptor.setLateAcceptanceSize(2); acceptor.setHillClimbingEnabled(true); @@ -246,14 +249,18 @@ void hillClimbingEnabled() { @Test void zeroLateAcceptanceSize() { - var acceptor = new LateAcceptanceAcceptor<>(false, new NoOpRestartStrategy<>()); + var restartStrategy = mock(RestartStrategy.class); + when(restartStrategy.isTriggered(any())).thenReturn(false); + var acceptor = new LateAcceptanceAcceptor<>(restartStrategy); acceptor.setLateAcceptanceSize(0); assertThatIllegalArgumentException().isThrownBy(() -> acceptor.phaseStarted(null)); } @Test void negativeLateAcceptanceSize() { - var acceptor = new LateAcceptanceAcceptor<>(false, new NoOpRestartStrategy<>()); + var restartStrategy = mock(RestartStrategy.class); + when(restartStrategy.isTriggered(any())).thenReturn(false); + var acceptor = new LateAcceptanceAcceptor<>(restartStrategy); acceptor.setLateAcceptanceSize(-1); assertThatIllegalArgumentException().isThrownBy(() -> acceptor.phaseStarted(null)); } @@ -262,7 +269,7 @@ void negativeLateAcceptanceSize() { void triggerReconfiguration() { var restartStrategy = mock(RestartStrategy.class); when(restartStrategy.isTriggered(any())).thenReturn(true); - var acceptor = new LateAcceptanceAcceptor<>(true, restartStrategy); + var acceptor = new LateAcceptanceAcceptor<>(restartStrategy); acceptor.setLateAcceptanceSize(3); var solverScope = new SolverScope<>(); var phaseScope = new LocalSearchPhaseScope<>(solverScope, 0); @@ -275,7 +282,7 @@ void triggerReconfiguration() { @Test void resetReconfiguration() { var restartStrategy = mock(RestartStrategy.class); - var acceptor = new LateAcceptanceAcceptor<>(true, restartStrategy); + var acceptor = new LateAcceptanceAcceptor<>(restartStrategy); acceptor.setLateAcceptanceSize(3); var solverScope = new SolverScope<>(); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/NoOpRestartStrategyTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/NoOpRestartStrategyTest.java deleted file mode 100644 index 288f5f152e..0000000000 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/NoOpRestartStrategyTest.java +++ /dev/null @@ -1,15 +0,0 @@ -package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; - -import org.junit.jupiter.api.Test; - -class NoOpRestartStrategyTest { - - @Test - void noOp() { - var strategy = new NoOpRestartStrategy<>(); - assertThat(strategy.isTriggered(any())).isFalse(); - } -} diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeRestartStrategyTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeRestartStrategyTest.java deleted file mode 100644 index 3687a7e563..0000000000 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedTimeRestartStrategyTest.java +++ /dev/null @@ -1,111 +0,0 @@ -package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.time.Clock; - -import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; -import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; -import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; -import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; -import ai.timefold.solver.core.impl.solver.scope.SolverScope; - -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -class UnimprovedTimeRestartStrategyTest { - - @Test - void isTriggered() { - var clock = mock(Clock.class); - when(clock.millis()).thenReturn(1000L); - var phaseScope = mock(LocalSearchPhaseScope.class); - var stepScope = mock(LocalSearchStepScope.class); - var moveScope = mock(LocalSearchMoveScope.class); - when(stepScope.getPhaseScope()).thenReturn(phaseScope); - when(moveScope.getStepScope()).thenReturn(stepScope); - when(phaseScope.getBestScore()).thenReturn(SimpleScore.of(1000)); - - // Initial values - var strategy = new UnimprovedTimeRestartStrategy<>(clock); - strategy.solvingStarted(null); - strategy.phaseStarted(phaseScope); - assertThat(strategy.lastImprovementMillis).isZero(); - assertThat(strategy.nextRestart).isEqualTo(1000L); - - // Finish grace period - Mockito.reset(clock); - when(clock.millis()).thenReturn(12000L); - assertThat(strategy.isTriggered(moveScope)).isFalse(); - assertThat(strategy.lastImprovementMillis).isEqualTo(12000L); - assertThat(strategy.nextRestart).isEqualTo(1000L); - strategy.reset(moveScope); - assertThat(strategy.isTriggered(moveScope)).isFalse(); - - // First restart - Mockito.reset(clock); - when(clock.millis()).thenReturn(13000L); - assertThat(strategy.isTriggered(moveScope)).isTrue(); - assertThat(strategy.lastImprovementMillis).isEqualTo(13000L); - assertThat(strategy.nextRestart).isEqualTo(2000L); - strategy.reset(moveScope); - assertThat(strategy.isTriggered(moveScope)).isFalse(); - - // Second restart - Mockito.reset(clock); - when(clock.millis()).thenReturn(15000L); - assertThat(strategy.isTriggered(moveScope)).isTrue(); - assertThat(strategy.lastImprovementMillis).isEqualTo(15000L); - assertThat(strategy.nextRestart).isEqualTo(3000L); - strategy.reset(moveScope); - - // Third restart - Mockito.reset(clock); - when(clock.millis()).thenReturn(18000L); - assertThat(strategy.isTriggered(moveScope)).isTrue(); - assertThat(strategy.lastImprovementMillis).isEqualTo(18000L); - assertThat(strategy.nextRestart).isEqualTo(5000L); - strategy.reset(moveScope); - } - - @Test - void updateBestSolution() { - var clock = mock(Clock.class); - when(clock.millis()).thenReturn(1000L, 20000L, 20000L, 20001L); - var phaseScope = mock(LocalSearchPhaseScope.class); - var stepScope = mock(LocalSearchStepScope.class); - var moveScope = mock(LocalSearchMoveScope.class); - when(stepScope.getPhaseScope()).thenReturn(phaseScope); - when(moveScope.getStepScope()).thenReturn(stepScope); - when(phaseScope.getBestScore()).thenReturn(SimpleScore.of(1000)); - when(stepScope.getScore()).thenReturn(SimpleScore.of(2000)); - - var strategy = new UnimprovedTimeRestartStrategy<>(clock); - strategy.solvingStarted(mock(SolverScope.class)); - strategy.phaseStarted(phaseScope); - strategy.stepStarted(stepScope); - // Trigger - strategy.isTriggered(moveScope); - // Update the last improvement - strategy.stepEnded(stepScope); - assertThat(strategy.lastImprovementMillis).isEqualTo(20001L); - } - - @Test - void reset() { - var clock = mock(Clock.class); - var moveScope = mock(LocalSearchMoveScope.class); - when(clock.millis()).thenReturn(1000L, 11000L); - - var strategy = new UnimprovedTimeRestartStrategy<>(clock); - strategy.solvingStarted(mock(SolverScope.class)); - strategy.phaseStarted(mock(LocalSearchPhaseScope.class)); - // Trigger - strategy.isTriggered(moveScope); - // Reset - strategy.reset(moveScope); - assertThat(strategy.lastImprovementMillis).isEqualTo(11000L); - } -} diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategyTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategyTest.java index 0f9eaee316..d967127f00 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategyTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategyTest.java @@ -1,13 +1,10 @@ package ai.timefold.solver.core.impl.localsearch.decider.reconfiguration; -import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.*; import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.Acceptor; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.NoOpRestartStrategy; -import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; import ai.timefold.solver.core.impl.solver.scope.SolverScope; @@ -16,12 +13,6 @@ class ReconfigurationStrategyTest { - @Test - void noOp() { - var strategy = new NoOpRestartStrategy<>(); - assertThat(strategy.isTriggered(mock(LocalSearchMoveScope.class))).isFalse(); - } - @Test void restoreBestSolution() { // Requires the decider From cea2d3efa73211dc9a4cd13d1f91c0f053438322 Mon Sep 17 00:00:00 2001 From: Fred Date: Fri, 24 Jan 2025 11:18:49 -0300 Subject: [PATCH 13/31] chore: remove preview feature --- benchmark/src/main/resources/benchmark.xsd | 3 --- .../ai/timefold/solver/core/config/solver/PreviewFeature.java | 1 - core/src/main/resources/solver.xsd | 2 -- 3 files changed, 6 deletions(-) diff --git a/benchmark/src/main/resources/benchmark.xsd b/benchmark/src/main/resources/benchmark.xsd index c015895395..24305f6e35 100644 --- a/benchmark/src/main/resources/benchmark.xsd +++ b/benchmark/src/main/resources/benchmark.xsd @@ -2603,9 +2603,6 @@ - - - diff --git a/core/src/main/java/ai/timefold/solver/core/config/solver/PreviewFeature.java b/core/src/main/java/ai/timefold/solver/core/config/solver/PreviewFeature.java index 0168a68829..7904996492 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/solver/PreviewFeature.java +++ b/core/src/main/java/ai/timefold/solver/core/config/solver/PreviewFeature.java @@ -2,7 +2,6 @@ public enum PreviewFeature { DIVERSIFIED_LATE_ACCEPTANCE, - RECONFIGURATION, PLANNING_SOLUTION_DIFF } diff --git a/core/src/main/resources/solver.xsd b/core/src/main/resources/solver.xsd index f5312eefb6..868923ef66 100644 --- a/core/src/main/resources/solver.xsd +++ b/core/src/main/resources/solver.xsd @@ -1581,8 +1581,6 @@ - - From e4a4ea6770a0a2997a31a100bfaa981c55514c9d Mon Sep 17 00:00:00 2001 From: Fred Date: Fri, 24 Jan 2025 11:23:37 -0300 Subject: [PATCH 14/31] chore: address sonar --- .../impl/localsearch/decider/acceptor/AcceptorFactory.java | 7 +++++-- .../reconfiguration/ReconfigurationStrategyTest.java | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java index c589a1fdde..b7afb588ae 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java @@ -15,6 +15,7 @@ import ai.timefold.solver.core.impl.localsearch.decider.acceptor.hillclimbing.HillClimbingAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance.DiversifiedLateAcceptanceAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance.LateAcceptanceAcceptor; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.RestartStrategy; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.UnimprovedMoveCountRestartStrategy; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.simulatedannealing.SimulatedAnnealingAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stepcountinghillclimbing.StepCountingHillClimbingAcceptor; @@ -221,7 +222,8 @@ private Optional> buildMoveTabuAcceptor(HeuristicCon if (acceptorTypeListsContainsAcceptorType(AcceptorType.LATE_ACCEPTANCE) || (!acceptorTypeListsContainsAcceptorType(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE) && acceptorConfig.getLateAcceptanceSize() != null)) { - var acceptor = new LateAcceptanceAcceptor<>(new UnimprovedMoveCountRestartStrategy()); + RestartStrategy strategy = new UnimprovedMoveCountRestartStrategy<>(); + var acceptor = new LateAcceptanceAcceptor<>(strategy); acceptor.setLateAcceptanceSize(Objects.requireNonNullElse(acceptorConfig.getLateAcceptanceSize(), 400)); return Optional.of(acceptor); } @@ -232,7 +234,8 @@ private Optional> buildMoveTabuAcceptor(HeuristicCon buildDiversifiedLateAcceptanceAcceptor(HeuristicConfigPolicy configPolicy) { if (acceptorTypeListsContainsAcceptorType(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE)) { configPolicy.ensurePreviewFeature(PreviewFeature.DIVERSIFIED_LATE_ACCEPTANCE); - var acceptor = new DiversifiedLateAcceptanceAcceptor<>(new UnimprovedMoveCountRestartStrategy()); + RestartStrategy strategy = new UnimprovedMoveCountRestartStrategy<>(); + var acceptor = new DiversifiedLateAcceptanceAcceptor<>(strategy); acceptor.setLateAcceptanceSize(Objects.requireNonNullElse(acceptorConfig.getLateAcceptanceSize(), 5)); return Optional.of(acceptor); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategyTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategyTest.java index d967127f00..c2fdf339b7 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategyTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategyTest.java @@ -16,8 +16,8 @@ class ReconfigurationStrategyTest { @Test void restoreBestSolution() { // Requires the decider - assertThatThrownBy(() -> new RestoreBestSolutionReconfigurationStrategy<>(null, null).phaseStarted(null)) - .isInstanceOf(NullPointerException.class); + var badStrategy = new RestoreBestSolutionReconfigurationStrategy<>(null, null); + assertThatThrownBy(() -> badStrategy.phaseStarted(null)).isInstanceOf(NullPointerException.class); // Restore the best solution var moveSelector = mock(MoveSelector.class); From f89e3c0957439316aeec0efe6e70c54eadaedd52 Mon Sep 17 00:00:00 2001 From: Fred Date: Mon, 27 Jan 2025 11:43:21 -0300 Subject: [PATCH 15/31] chore: simplify the reconfiguration logic --- .../core/config/solver/PreviewFeature.java | 1 + .../decider/LocalSearchDecider.java | 13 ++--- .../acceptor/ReconfigurableAcceptor.java | 8 +--- .../AbstractGeometricRestartStrategy.java | 25 +++++----- .../acceptor/restart/RestartStrategy.java | 2 - .../UnimprovedMoveCountRestartStrategy.java | 26 ++++------ .../NoOpReconfigurationStrategy.java | 48 ------------------- .../ReconfigurationStrategy.java | 5 +- ...reBestSolutionReconfigurationStrategy.java | 12 ++--- ...DiversifiedLateAcceptanceAcceptorTest.java | 18 ------- .../LateAcceptanceAcceptorTest.java | 22 +-------- ...nimprovedMoveCountRestartStrategyTest.java | 17 +++---- 12 files changed, 42 insertions(+), 155 deletions(-) delete mode 100644 core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/NoOpReconfigurationStrategy.java diff --git a/core/src/main/java/ai/timefold/solver/core/config/solver/PreviewFeature.java b/core/src/main/java/ai/timefold/solver/core/config/solver/PreviewFeature.java index 7904996492..402bf6205f 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/solver/PreviewFeature.java +++ b/core/src/main/java/ai/timefold/solver/core/config/solver/PreviewFeature.java @@ -1,6 +1,7 @@ package ai.timefold.solver.core.config.solver; public enum PreviewFeature { + DIVERSIFIED_LATE_ACCEPTANCE, PLANNING_SOLUTION_DIFF diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java index 4fb4911086..0de6072cd1 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java @@ -3,7 +3,6 @@ import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.impl.heuristic.move.LegacyMoveAdapter; -import ai.timefold.solver.core.impl.heuristic.move.NoChangeMove; import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.Acceptor; import ai.timefold.solver.core.impl.localsearch.decider.forager.LocalSearchForager; @@ -102,15 +101,6 @@ public void decideNextStep(LocalSearchStepScope stepScope) { InnerScoreDirector scoreDirector = stepScope.getScoreDirector(); scoreDirector.setAllChangesWillBeUndoneBeforeStepEnds(true); int moveIndex = 0; - if (reconfigurationStrategy.isTriggered(stepScope)) { - var reconfigurationScore = reconfigurationStrategy.apply(stepScope); - // Generate a dummy move; so the LS phase continues the process - var adaptedMove = new LegacyMoveAdapter(NoChangeMove.getInstance()); - stepScope.setStep(adaptedMove); - stepScope.setScore(reconfigurationScore); - scoreDirector.setAllChangesWillBeUndoneBeforeStepEnds(false); - return; - } for (var move : moveSelector) { var adaptedMove = new LegacyMoveAdapter<>(move); LocalSearchMoveScope moveScope = new LocalSearchMoveScope<>(stepScope, moveIndex, adaptedMove); @@ -165,6 +155,9 @@ protected void pickMove(LocalSearchStepScope stepScope) { } public void stepEnded(LocalSearchStepScope stepScope) { + if (reconfigurationStrategy.isTriggered(stepScope)) { + reconfigurationStrategy.apply(stepScope); + } reconfigurationStrategy.stepEnded(stepScope); moveSelector.stepEnded(stepScope); acceptor.stepEnded(stepScope); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java index 70a7ed0772..d00eaaa1c4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java @@ -50,18 +50,12 @@ public void stepEnded(LocalSearchStepScope stepScope) { } @Override - @SuppressWarnings("unchecked") public boolean isAccepted(LocalSearchMoveScope moveScope) { if (restartStrategy.isTriggered(moveScope)) { moveScope.getStepScope().getPhaseScope().triggerReconfiguration(); return true; } - var accepted = evaluate(moveScope); - var improved = moveScope.getScore().compareTo(moveScope.getStepScope().getPhaseScope().getBestScore()) > 0; - if (improved) { - restartStrategy.reset(moveScope); - } - return accepted; + return evaluate(moveScope); } protected abstract boolean evaluate(LocalSearchMoveScope moveScope); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricRestartStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricRestartStrategy.java index 6db9b09758..137e73b031 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricRestartStrategy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricRestartStrategy.java @@ -25,10 +25,9 @@ public abstract class AbstractGeometricRestartStrategy implements Res protected final double scalingFactor; private boolean gracePeriodFinished; - private boolean restartTriggered; private long gracePeriodMillis; protected long nextRestart; - protected double currentGeometricGrowFactor; + private double currentGeometricGrowFactor; protected AbstractGeometricRestartStrategy(Clock clock, double scalingFactor) { this.clock = clock; @@ -37,7 +36,6 @@ protected AbstractGeometricRestartStrategy(Clock clock, double scalingFactor) { @Override public void phaseStarted(LocalSearchPhaseScope phaseScope) { - restartTriggered = false; if (gracePeriodMillis == 0) { // 10 seconds of grace period gracePeriodMillis = clock.millis() + 10_000; @@ -54,7 +52,7 @@ public void solvingStarted(SolverScope solverScope) { currentGeometricGrowFactor = 1; gracePeriodMillis = 0; gracePeriodFinished = false; - nextRestart = (long) Math.ceil(currentGeometricGrowFactor * scalingFactor); + nextRestart = calculateNextRestart(); } @Override @@ -64,24 +62,29 @@ public void solvingEnded(SolverScope solverScope) { @Override public boolean isTriggered(LocalSearchMoveScope moveScope) { - if (!restartTriggered && (gracePeriodFinished || clock.millis() >= gracePeriodMillis)) { - gracePeriodFinished = true; + if (isGracePeriodFinished()) { var triggered = process(moveScope); if (triggered) { + logger.trace("Restart triggered with geometric factor {}, scaling factor of {}", currentGeometricGrowFactor, + scalingFactor); currentGeometricGrowFactor = Math.ceil(currentGeometricGrowFactor * GEOMETRIC_FACTOR); - nextRestart = (long) Math.ceil(currentGeometricGrowFactor * scalingFactor); - restartTriggered = true; + nextRestart = calculateNextRestart(); + return true; } } - return restartTriggered; + return false; } protected boolean isGracePeriodFinished() { + if (gracePeriodFinished) { + return true; + } + gracePeriodFinished = clock.millis() >= gracePeriodMillis; return gracePeriodFinished; } - protected void disableTriggerFlag() { - restartTriggered = false; + private long calculateNextRestart() { + return (long) Math.ceil(currentGeometricGrowFactor * scalingFactor); } abstract boolean process(LocalSearchMoveScope moveScope); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/RestartStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/RestartStrategy.java index e2775a6285..c2f1c668ed 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/RestartStrategy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/RestartStrategy.java @@ -6,6 +6,4 @@ public interface RestartStrategy extends LocalSearchPhaseLifecycleListener { boolean isTriggered(LocalSearchMoveScope moveScope); - - void reset(LocalSearchMoveScope moveScope); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountRestartStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountRestartStrategy.java index 0ecd001199..a93fbafb83 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountRestartStrategy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountRestartStrategy.java @@ -13,7 +13,8 @@ */ public class UnimprovedMoveCountRestartStrategy extends AbstractGeometricRestartStrategy { - protected long lastImprovementMoveCount; + // Last checkpoint of a solution improvement or the restart process + protected long lastCheckpoint; private Score currentBestScore; public UnimprovedMoveCountRestartStrategy() { @@ -41,38 +42,27 @@ public void stepStarted(LocalSearchStepScope stepScope) { public void stepEnded(LocalSearchStepScope stepScope) { // Do not update it during the grace period if (isGracePeriodFinished() && ((Score) stepScope.getScore()).compareTo(currentBestScore) > 0) { - lastImprovementMoveCount = stepScope.getPhaseScope().getSolverScope().getMoveEvaluationCount(); + lastCheckpoint = stepScope.getPhaseScope().getSolverScope().getMoveEvaluationCount(); } } @Override public void solvingStarted(SolverScope solverScope) { super.solvingStarted(solverScope); - lastImprovementMoveCount = 0; + lastCheckpoint = 0; } @Override public boolean process(LocalSearchMoveScope moveScope) { var currentMoveCount = moveScope.getStepScope().getPhaseScope().getSolverScope().getMoveEvaluationCount(); - if (lastImprovementMoveCount == 0) { - lastImprovementMoveCount = currentMoveCount; + if (lastCheckpoint == 0) { + lastCheckpoint = currentMoveCount; return false; } - if (currentMoveCount - lastImprovementMoveCount >= nextRestart) { - logger.debug("Restart triggered with geometric factor {}, scaling factor of {}, move count ({})", - currentGeometricGrowFactor, scalingFactor, currentMoveCount); - lastImprovementMoveCount = currentMoveCount; + if (currentMoveCount - lastCheckpoint >= nextRestart) { + lastCheckpoint = currentMoveCount; return true; } return false; } - - @Override - public void reset(LocalSearchMoveScope moveScope) { - disableTriggerFlag(); - // Do not update it during the grace period - if (isGracePeriodFinished()) { - lastImprovementMoveCount = moveScope.getStepScope().getPhaseScope().getSolverScope().getMoveEvaluationCount(); - } - } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/NoOpReconfigurationStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/NoOpReconfigurationStrategy.java deleted file mode 100644 index cb3b787c84..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/NoOpReconfigurationStrategy.java +++ /dev/null @@ -1,48 +0,0 @@ -package ai.timefold.solver.core.impl.localsearch.decider.reconfiguration; - -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; -import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; -import ai.timefold.solver.core.impl.solver.scope.SolverScope; - -public final class NoOpReconfigurationStrategy implements ReconfigurationStrategy { - @Override - public > Score_ apply(AbstractStepScope stepScope) { - throw new IllegalStateException("Impossible state"); - } - - @Override - public boolean isTriggered(AbstractStepScope stepScope) { - return false; - } - - @Override - public void phaseStarted(AbstractPhaseScope phaseScope) { - // Do nothing - } - - @Override - public void stepStarted(AbstractStepScope stepScope) { - // Do nothing - } - - @Override - public void stepEnded(AbstractStepScope stepScope) { - // Do nothing - } - - @Override - public void phaseEnded(AbstractPhaseScope phaseScope) { - // Do nothing - } - - @Override - public void solvingStarted(SolverScope solverScope) { - // Do nothing - } - - @Override - public void solvingEnded(SolverScope solverScope) { - // Do nothing - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategy.java index 23fc87d9af..d79efa631f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategy.java @@ -1,13 +1,12 @@ package ai.timefold.solver.core.impl.localsearch.decider.reconfiguration; -import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleListener; import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; public sealed interface ReconfigurationStrategy extends PhaseLifecycleListener - permits NoOpReconfigurationStrategy, RestoreBestSolutionReconfigurationStrategy { + permits RestoreBestSolutionReconfigurationStrategy { - > Score_ apply(AbstractStepScope stepScope); + void apply(AbstractStepScope stepScope); default boolean isTriggered(AbstractStepScope stepScope) { return stepScope.getPhaseScope().isReconfigurationTriggered(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionReconfigurationStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionReconfigurationStrategy.java index 5945f65ec6..f7b52fde2c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionReconfigurationStrategy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionReconfigurationStrategy.java @@ -2,7 +2,6 @@ import java.util.Objects; -import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.Acceptor; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; @@ -21,14 +20,15 @@ public final class RestoreBestSolutionReconfigurationStrategy impleme public RestoreBestSolutionReconfigurationStrategy(MoveSelector moveSelector, Acceptor acceptor) { this.moveSelector = moveSelector; + Objects.requireNonNull(moveSelector); this.acceptor = acceptor; + Objects.requireNonNull(acceptor); } @Override - @SuppressWarnings("unchecked") - public > Score_ apply(AbstractStepScope stepScope) { + public void apply(AbstractStepScope stepScope) { var solverScope = stepScope.getPhaseScope().getSolverScope(); - logger.debug("Resetting working solution, score ({})", solverScope.getBestScore()); + logger.trace("Resetting working solution, score ({})", solverScope.getBestScore()); solverScope.setWorkingSolutionFromBestSolution(); // Changing the working solution requires reinitializing the move selector and acceptor // 1 - The move selector will reset all cached lists using old solution entity references @@ -37,7 +37,6 @@ public > Score_ apply(AbstractStepScope acceptor.phaseStarted((LocalSearchPhaseScope) stepScope.getPhaseScope()); // Cancel it as the best solution is already restored stepScope.getPhaseScope().cancelReconfiguration(); - return (Score_) solverScope.getBestScore(); } @Override @@ -52,8 +51,7 @@ public void stepEnded(AbstractStepScope stepScope) { @Override public void phaseStarted(AbstractPhaseScope phaseScope) { - Objects.requireNonNull(moveSelector); - Objects.requireNonNull(acceptor); + // Do nothing } @Override diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java index 8993839fe1..40fb57feab 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java @@ -164,22 +164,4 @@ void triggerReconfiguration() { verify(restartStrategy, times(1)).isTriggered(any()); assertThat(phaseScope.isReconfigurationTriggered()).isTrue(); } - - @Test - void resetReconfiguration() { - var restartStrategy = mock(RestartStrategy.class); - var acceptor = new DiversifiedLateAcceptanceAcceptor<>(restartStrategy); - acceptor.setLateAcceptanceSize(3); - - var solverScope = new SolverScope<>(); - solverScope.setBestScore(SimpleScore.of(-1000)); - var phaseScope = new LocalSearchPhaseScope<>(solverScope, 0); - acceptor.phaseStarted(phaseScope); - var stepScope0 = new LocalSearchStepScope<>(phaseScope); - stepScope0.setScore(SimpleScore.of(-999)); - var moveScope0 = buildMoveScope(stepScope0, -999); - stepScope0.getPhaseScope().setLastCompletedStepScope(stepScope0); - assertThat(acceptor.isAccepted(moveScope0)).isTrue(); - verify(restartStrategy, times(1)).reset(moveScope0); - } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptorTest.java index 9a0894bb07..979b40376d 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptorTest.java @@ -3,8 +3,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; -import static org.mockito.Mockito.times; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.AbstractAcceptorTest; @@ -278,22 +278,4 @@ void triggerReconfiguration() { assertThat(acceptor.isAccepted(moveScope0)).isTrue(); assertThat(phaseScope.isReconfigurationTriggered()).isTrue(); } - - @Test - void resetReconfiguration() { - var restartStrategy = mock(RestartStrategy.class); - var acceptor = new LateAcceptanceAcceptor<>(restartStrategy); - acceptor.setLateAcceptanceSize(3); - - var solverScope = new SolverScope<>(); - solverScope.setBestScore(SimpleScore.of(-1000)); - var phaseScope = new LocalSearchPhaseScope<>(solverScope, 0); - acceptor.phaseStarted(phaseScope); - var stepScope0 = new LocalSearchStepScope<>(phaseScope); - stepScope0.setScore(SimpleScore.of(-999)); - var moveScope0 = buildMoveScope(stepScope0, -999); - stepScope0.getPhaseScope().setLastCompletedStepScope(stepScope0); - assertThat(acceptor.isAccepted(moveScope0)).isTrue(); - verify(restartStrategy, times(1)).reset(moveScope0); - } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountRestartStrategyTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountRestartStrategyTest.java index 937acddc20..ffb999f077 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountRestartStrategyTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountRestartStrategyTest.java @@ -36,7 +36,7 @@ void isTriggered() { strategy.solvingStarted(null); strategy.phaseStarted(phaseScope); assertThat(strategy.isTriggered(moveScope)).isFalse(); - assertThat(strategy.lastImprovementMoveCount).isEqualTo(1000L); + assertThat(strategy.lastCheckpoint).isEqualTo(1000L); assertThat(strategy.nextRestart).isEqualTo(50000L); // First restart @@ -44,9 +44,8 @@ void isTriggered() { var firstCount = 60000L; when(solverScope.getMoveEvaluationCount()).thenReturn(firstCount); assertThat(strategy.isTriggered(moveScope)).isTrue(); - assertThat(strategy.lastImprovementMoveCount).isEqualTo(firstCount); + assertThat(strategy.lastCheckpoint).isEqualTo(firstCount); assertThat(strategy.nextRestart).isEqualTo(2L * 50000L); - strategy.reset(moveScope); assertThat(strategy.isTriggered(moveScope)).isFalse(); // Second restart @@ -54,9 +53,8 @@ void isTriggered() { var secondCount = 2L * 50000L + firstCount; when(solverScope.getMoveEvaluationCount()).thenReturn(secondCount); assertThat(strategy.isTriggered(moveScope)).isTrue(); - assertThat(strategy.lastImprovementMoveCount).isEqualTo(secondCount); + assertThat(strategy.lastCheckpoint).isEqualTo(secondCount); assertThat(strategy.nextRestart).isEqualTo(3L * 50000L); - strategy.reset(moveScope); assertThat(strategy.isTriggered(moveScope)).isFalse(); // Third restart @@ -64,9 +62,8 @@ void isTriggered() { Mockito.reset(clock); when(solverScope.getMoveEvaluationCount()).thenReturn(thirdCount); assertThat(strategy.isTriggered(moveScope)).isTrue(); - assertThat(strategy.lastImprovementMoveCount).isEqualTo(thirdCount); + assertThat(strategy.lastCheckpoint).isEqualTo(thirdCount); assertThat(strategy.nextRestart).isEqualTo(5L * 50000L); - strategy.reset(moveScope); } @Test @@ -92,7 +89,7 @@ void updateBestSolution() { strategy.isTriggered(moveScope); // Update the last improvement strategy.stepEnded(stepScope); - assertThat(strategy.lastImprovementMoveCount).isEqualTo(1001L); + assertThat(strategy.lastCheckpoint).isEqualTo(1001L); } @Test @@ -113,8 +110,6 @@ void reset() { strategy.phaseStarted(mock(LocalSearchPhaseScope.class)); // Trigger strategy.isTriggered(moveScope); - // Reset - strategy.reset(moveScope); - assertThat(strategy.lastImprovementMoveCount).isEqualTo(1000L); + assertThat(strategy.lastCheckpoint).isEqualTo(1000L); } } From 8bc0cc8652d6f172c0e4cca56266a68ac1ccb2e0 Mon Sep 17 00:00:00 2001 From: Fred Date: Mon, 27 Jan 2025 13:55:25 -0300 Subject: [PATCH 16/31] chore: simplify the reconfiguration logic --- .../decider/acceptor/ReconfigurableAcceptor.java | 6 +++--- .../lateacceptance/DiversifiedLateAcceptanceAcceptor.java | 4 ++-- .../acceptor/lateacceptance/LateAcceptanceAcceptor.java | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java index d00eaaa1c4..978f8d2f1b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java @@ -55,10 +55,10 @@ public boolean isAccepted(LocalSearchMoveScope moveScope) { moveScope.getStepScope().getPhaseScope().triggerReconfiguration(); return true; } - return evaluate(moveScope); + return applyAcceptanceCriteria(moveScope); } - protected abstract boolean evaluate(LocalSearchMoveScope moveScope); + protected abstract boolean applyAcceptanceCriteria(LocalSearchMoveScope moveScope); - protected abstract > void reset(Score_ score); + protected abstract > void applyReplacementCriteria(Score_ score); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java index f6eabb38a3..2c9e511f48 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java @@ -53,7 +53,7 @@ private void validate() { @Override @SuppressWarnings({ "rawtypes", "unchecked" }) - protected boolean evaluate(LocalSearchMoveScope moveScope) { + protected boolean applyAcceptanceCriteria(LocalSearchMoveScope moveScope) { // The acceptance and replacement strategies are based on the work: // Diversified Late Acceptance Search by M. Namazi, C. Sanderson, M. A. H. Newton, M. M. A. Polash, and A. Sattar var moveScore = moveScope.getScore(); @@ -77,7 +77,7 @@ protected boolean evaluate(LocalSearchMoveScope moveScope) { } @Override - protected > void reset(Score_ score) { + protected > void applyReplacementCriteria(Score_ score) { previousScores[lateScoreIndex] = score; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java index 3801208157..546ba6b8dd 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java @@ -52,7 +52,7 @@ private void validate() { @Override @SuppressWarnings("unchecked") - public boolean evaluate(LocalSearchMoveScope moveScope) { + public boolean applyAcceptanceCriteria(LocalSearchMoveScope moveScope) { var moveScore = moveScope.getScore(); var lateScore = previousScores[lateScoreIndex]; if (moveScore.compareTo(lateScore) >= 0) { @@ -66,7 +66,7 @@ public boolean evaluate(LocalSearchMoveScope moveScope) { } @Override - protected > void reset(Score_ score) { + protected > void applyReplacementCriteria(Score_ score) { previousScores[lateScoreIndex] = score; } From aea651059561962e044932176357090ebfc213f9 Mon Sep 17 00:00:00 2001 From: Fred Date: Mon, 27 Jan 2025 16:42:03 -0300 Subject: [PATCH 17/31] chore: address comments --- .../TimefoldSolverEnterpriseService.java | 4 +- .../DefaultLocalSearchPhaseFactory.java | 8 +-- .../decider/LocalSearchDecider.java | 24 ++++---- .../decider/acceptor/AcceptorFactory.java | 8 +-- .../acceptor/ReconfigurableAcceptor.java | 25 ++++---- .../DiversifiedLateAcceptanceAcceptor.java | 11 +--- .../LateAcceptanceAcceptor.java | 11 +--- ...a => AbstractGeometricStuckCriterion.java} | 25 ++++---- .../acceptor/restart/RestartStrategy.java | 9 --- .../acceptor/restart/StuckCriterion.java | 22 +++++++ ...=> UnimprovedMoveCountStuckCriterion.java} | 21 ++++--- .../ReconfigurationStrategy.java | 15 ----- .../reconfiguration/RestartStrategy.java | 38 ++++++++++++ ...> RestoreBestSolutionRestartStrategy.java} | 8 +-- .../impl/phase/scope/AbstractPhaseScope.java | 14 ++--- ...DiversifiedLateAcceptanceAcceptorTest.java | 18 +++--- .../LateAcceptanceAcceptorTest.java | 24 ++++---- ...nimprovedMoveCountStuckCriterionTest.java} | 61 ++++++++++--------- ...tegyTest.java => RestartStrategyTest.java} | 10 +-- 19 files changed, 193 insertions(+), 163 deletions(-) rename core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/{AbstractGeometricRestartStrategy.java => AbstractGeometricStuckCriterion.java} (73%) delete mode 100644 core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/RestartStrategy.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/StuckCriterion.java rename core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/{UnimprovedMoveCountRestartStrategy.java => UnimprovedMoveCountStuckCriterion.java} (70%) delete mode 100644 core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategy.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestartStrategy.java rename core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/{RestoreBestSolutionReconfigurationStrategy.java => RestoreBestSolutionRestartStrategy.java} (86%) rename core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/{UnimprovedMoveCountRestartStrategyTest.java => UnimprovedMoveCountStuckCriterionTest.java} (63%) rename core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/{ReconfigurationStrategyTest.java => RestartStrategyTest.java} (82%) diff --git a/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java b/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java index 03917debe6..59c301148b 100644 --- a/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java +++ b/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java @@ -29,7 +29,7 @@ import ai.timefold.solver.core.impl.localsearch.decider.LocalSearchDecider; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.Acceptor; import ai.timefold.solver.core.impl.localsearch.decider.forager.LocalSearchForager; -import ai.timefold.solver.core.impl.localsearch.decider.reconfiguration.ReconfigurationStrategy; +import ai.timefold.solver.core.impl.localsearch.decider.reconfiguration.RestartStrategy; import ai.timefold.solver.core.impl.partitionedsearch.PartitionedSearchPhase; import ai.timefold.solver.core.impl.solver.termination.Termination; @@ -101,7 +101,7 @@ ConstructionHeuristicDecider buildConstructionHeuristic(T ConstructionHeuristicForager forager, HeuristicConfigPolicy configPolicy); LocalSearchDecider buildLocalSearch(int moveThreadCount, Termination termination, - MoveSelector moveSelector, ReconfigurationStrategy reconfigurationStrategy, + MoveSelector moveSelector, RestartStrategy restartStrategy, Acceptor acceptor, LocalSearchForager forager, EnvironmentMode environmentMode, HeuristicConfigPolicy configPolicy); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java index 12164c554b..ce3cde0f42 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java @@ -32,7 +32,7 @@ import ai.timefold.solver.core.impl.localsearch.decider.acceptor.AcceptorFactory; import ai.timefold.solver.core.impl.localsearch.decider.forager.LocalSearchForager; import ai.timefold.solver.core.impl.localsearch.decider.forager.LocalSearchForagerFactory; -import ai.timefold.solver.core.impl.localsearch.decider.reconfiguration.RestoreBestSolutionReconfigurationStrategy; +import ai.timefold.solver.core.impl.localsearch.decider.reconfiguration.RestoreBestSolutionRestartStrategy; import ai.timefold.solver.core.impl.phase.AbstractPhaseFactory; import ai.timefold.solver.core.impl.solver.recaller.BestSolutionRecaller; import ai.timefold.solver.core.impl.solver.termination.Termination; @@ -66,7 +66,7 @@ private LocalSearchDecider buildDecider(HeuristicConfigPolicy termination) { var moveSelector = buildMoveSelector(configPolicy); var acceptor = buildAcceptor(configPolicy); - var reconfigurationStrategy = new RestoreBestSolutionReconfigurationStrategy<>(moveSelector, acceptor); + var restartStrategy = new RestoreBestSolutionRestartStrategy<>(moveSelector, acceptor); var forager = buildForager(configPolicy); if (moveSelector.isNeverEnding() && !forager.supportsNeverEndingMoveSelector()) { throw new IllegalStateException("The moveSelector (" + moveSelector @@ -80,10 +80,10 @@ private LocalSearchDecider buildDecider(HeuristicConfigPolicy decider; if (moveThreadCount == null) { decider = new LocalSearchDecider<>(configPolicy.getLogIndentation(), termination, moveSelector, - reconfigurationStrategy, acceptor, forager); + restartStrategy, acceptor, forager); } else { decider = TimefoldSolverEnterpriseService.loadOrFail(TimefoldSolverEnterpriseService.Feature.MULTITHREADED_SOLVING) - .buildLocalSearch(moveThreadCount, termination, moveSelector, reconfigurationStrategy, acceptor, forager, + .buildLocalSearch(moveThreadCount, termination, moveSelector, restartStrategy, acceptor, forager, environmentMode, configPolicy); } if (environmentMode.isNonIntrusiveFullAsserted()) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java index 0de6072cd1..b6f0156521 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java @@ -6,7 +6,7 @@ import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.Acceptor; import ai.timefold.solver.core.impl.localsearch.decider.forager.LocalSearchForager; -import ai.timefold.solver.core.impl.localsearch.decider.reconfiguration.ReconfigurationStrategy; +import ai.timefold.solver.core.impl.localsearch.decider.reconfiguration.RestartStrategy; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; @@ -30,7 +30,7 @@ public class LocalSearchDecider { protected final String logIndentation; protected final Termination termination; protected final MoveSelector moveSelector; - protected final ReconfigurationStrategy reconfigurationStrategy; + protected final RestartStrategy restartStrategy; protected final Acceptor acceptor; protected final LocalSearchForager forager; @@ -38,12 +38,12 @@ public class LocalSearchDecider { protected boolean assertExpectedUndoMoveScore = false; public LocalSearchDecider(String logIndentation, Termination termination, - MoveSelector moveSelector, ReconfigurationStrategy reconfigurationStrategy, + MoveSelector moveSelector, RestartStrategy restartStrategy, Acceptor acceptor, LocalSearchForager forager) { this.logIndentation = logIndentation; this.termination = termination; this.moveSelector = moveSelector; - this.reconfigurationStrategy = reconfigurationStrategy; + this.restartStrategy = restartStrategy; this.acceptor = acceptor; this.forager = forager; } @@ -77,21 +77,21 @@ public void setAssertExpectedUndoMoveScore(boolean assertExpectedUndoMoveScore) // ************************************************************************ public void solvingStarted(SolverScope solverScope) { - reconfigurationStrategy.solvingStarted(solverScope); + restartStrategy.solvingStarted(solverScope); moveSelector.solvingStarted(solverScope); acceptor.solvingStarted(solverScope); forager.solvingStarted(solverScope); } public void phaseStarted(LocalSearchPhaseScope phaseScope) { - reconfigurationStrategy.phaseStarted(phaseScope); + restartStrategy.phaseStarted(phaseScope); moveSelector.phaseStarted(phaseScope); acceptor.phaseStarted(phaseScope); forager.phaseStarted(phaseScope); } public void stepStarted(LocalSearchStepScope stepScope) { - reconfigurationStrategy.stepStarted(stepScope); + restartStrategy.stepStarted(stepScope); moveSelector.stepStarted(stepScope); acceptor.stepStarted(stepScope); forager.stepStarted(stepScope); @@ -155,24 +155,24 @@ protected void pickMove(LocalSearchStepScope stepScope) { } public void stepEnded(LocalSearchStepScope stepScope) { - if (reconfigurationStrategy.isTriggered(stepScope)) { - reconfigurationStrategy.apply(stepScope); + if (restartStrategy.isSolverStuck(stepScope)) { + restartStrategy.applyRestart(stepScope); } - reconfigurationStrategy.stepEnded(stepScope); + restartStrategy.stepEnded(stepScope); moveSelector.stepEnded(stepScope); acceptor.stepEnded(stepScope); forager.stepEnded(stepScope); } public void phaseEnded(LocalSearchPhaseScope phaseScope) { - reconfigurationStrategy.phaseEnded(phaseScope); + restartStrategy.phaseEnded(phaseScope); moveSelector.phaseEnded(phaseScope); acceptor.phaseEnded(phaseScope); forager.phaseEnded(phaseScope); } public void solvingEnded(SolverScope solverScope) { - reconfigurationStrategy.solvingEnded(solverScope); + restartStrategy.solvingEnded(solverScope); moveSelector.solvingEnded(solverScope); acceptor.solvingEnded(solverScope); forager.solvingEnded(solverScope); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java index b7afb588ae..de36437325 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java @@ -15,8 +15,8 @@ import ai.timefold.solver.core.impl.localsearch.decider.acceptor.hillclimbing.HillClimbingAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance.DiversifiedLateAcceptanceAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance.LateAcceptanceAcceptor; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.RestartStrategy; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.UnimprovedMoveCountRestartStrategy; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.StuckCriterion; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.UnimprovedMoveCountStuckCriterion; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.simulatedannealing.SimulatedAnnealingAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stepcountinghillclimbing.StepCountingHillClimbingAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.tabu.EntityTabuAcceptor; @@ -222,7 +222,7 @@ private Optional> buildMoveTabuAcceptor(HeuristicCon if (acceptorTypeListsContainsAcceptorType(AcceptorType.LATE_ACCEPTANCE) || (!acceptorTypeListsContainsAcceptorType(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE) && acceptorConfig.getLateAcceptanceSize() != null)) { - RestartStrategy strategy = new UnimprovedMoveCountRestartStrategy<>(); + StuckCriterion strategy = new UnimprovedMoveCountStuckCriterion<>(); var acceptor = new LateAcceptanceAcceptor<>(strategy); acceptor.setLateAcceptanceSize(Objects.requireNonNullElse(acceptorConfig.getLateAcceptanceSize(), 400)); return Optional.of(acceptor); @@ -234,7 +234,7 @@ private Optional> buildMoveTabuAcceptor(HeuristicCon buildDiversifiedLateAcceptanceAcceptor(HeuristicConfigPolicy configPolicy) { if (acceptorTypeListsContainsAcceptorType(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE)) { configPolicy.ensurePreviewFeature(PreviewFeature.DIVERSIFIED_LATE_ACCEPTANCE); - RestartStrategy strategy = new UnimprovedMoveCountRestartStrategy<>(); + StuckCriterion strategy = new UnimprovedMoveCountStuckCriterion<>(); var acceptor = new DiversifiedLateAcceptanceAcceptor<>(strategy); acceptor.setLateAcceptanceSize(Objects.requireNonNullElse(acceptorConfig.getLateAcceptanceSize(), 5)); return Optional.of(acceptor); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java index 978f8d2f1b..cc8764f810 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java @@ -1,7 +1,6 @@ package ai.timefold.solver.core.impl.localsearch.decider.acceptor; -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.RestartStrategy; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.StuckCriterion; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; @@ -13,52 +12,50 @@ */ public abstract class ReconfigurableAcceptor extends AbstractAcceptor { - private final RestartStrategy restartStrategy; + private final StuckCriterion stuckCriterionDetection; - protected ReconfigurableAcceptor(RestartStrategy restartStrategy) { - this.restartStrategy = restartStrategy; + protected ReconfigurableAcceptor(StuckCriterion stuckCriterionDetection) { + this.stuckCriterionDetection = stuckCriterionDetection; } @Override public void solvingStarted(SolverScope solverScope) { super.solvingStarted(solverScope); - restartStrategy.solvingStarted(solverScope); + stuckCriterionDetection.solvingStarted(solverScope); } @Override public void phaseStarted(LocalSearchPhaseScope phaseScope) { super.phaseStarted(phaseScope); - restartStrategy.phaseStarted(phaseScope); + stuckCriterionDetection.phaseStarted(phaseScope); } @Override public void phaseEnded(LocalSearchPhaseScope phaseScope) { super.phaseEnded(phaseScope); - restartStrategy.phaseEnded(phaseScope); + stuckCriterionDetection.phaseEnded(phaseScope); } @Override public void stepStarted(LocalSearchStepScope stepScope) { super.stepStarted(stepScope); - restartStrategy.stepStarted(stepScope); + stuckCriterionDetection.stepStarted(stepScope); } @Override public void stepEnded(LocalSearchStepScope stepScope) { super.stepEnded(stepScope); - restartStrategy.stepEnded(stepScope); + stuckCriterionDetection.stepEnded(stepScope); } @Override public boolean isAccepted(LocalSearchMoveScope moveScope) { - if (restartStrategy.isTriggered(moveScope)) { - moveScope.getStepScope().getPhaseScope().triggerReconfiguration(); + if (stuckCriterionDetection.isSolverStuck(moveScope)) { + moveScope.getStepScope().getPhaseScope().triggerSolverStuck(); return true; } return applyAcceptanceCriteria(moveScope); } protected abstract boolean applyAcceptanceCriteria(LocalSearchMoveScope moveScope); - - protected abstract > void applyReplacementCriteria(Score_ score); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java index 2c9e511f48..4022c3f226 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java @@ -4,7 +4,7 @@ import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.ReconfigurableAcceptor; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.RestartStrategy; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.StuckCriterion; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; @@ -20,8 +20,8 @@ public class DiversifiedLateAcceptanceAcceptor extends Reconfigurable protected Score[] previousScores; protected int lateScoreIndex = -1; - public DiversifiedLateAcceptanceAcceptor(RestartStrategy restartStrategy) { - super(restartStrategy); + public DiversifiedLateAcceptanceAcceptor(StuckCriterion stuckCriterionDetection) { + super(stuckCriterionDetection); } public void setLateAcceptanceSize(int lateAcceptanceSize) { @@ -76,11 +76,6 @@ protected boolean applyAcceptanceCriteria(LocalSearchMoveScope moveSc return accept; } - @Override - protected > void applyReplacementCriteria(Score_ score) { - previousScores[lateScoreIndex] = score; - } - @SuppressWarnings({ "rawtypes", "unchecked" }) private void updateLateScore(Score newScore) { var newScoreWorseCmp = newScore.compareTo(lateWorse); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java index 546ba6b8dd..5d0bc3579a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java @@ -4,7 +4,7 @@ import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.ReconfigurableAcceptor; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.RestartStrategy; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.StuckCriterion; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; @@ -17,8 +17,8 @@ public class LateAcceptanceAcceptor extends ReconfigurableAcceptor[] previousScores; protected int lateScoreIndex = -1; - public LateAcceptanceAcceptor(RestartStrategy restartStrategy) { - super(restartStrategy); + public LateAcceptanceAcceptor(StuckCriterion stuckCriterionDetection) { + super(stuckCriterionDetection); } public void setLateAcceptanceSize(int lateAcceptanceSize) { @@ -65,11 +65,6 @@ public boolean applyAcceptanceCriteria(LocalSearchMoveScope moveScope return false; } - @Override - protected > void applyReplacementCriteria(Score_ score) { - previousScores[lateScoreIndex] = score; - } - @Override public void stepEnded(LocalSearchStepScope stepScope) { super.stepEnded(stepScope); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricRestartStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java similarity index 73% rename from core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricRestartStrategy.java rename to core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java index 137e73b031..3484872780 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricRestartStrategy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java @@ -1,6 +1,6 @@ package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; -import java.time.Clock; +import java.time.Instant; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; @@ -18,19 +18,20 @@ * * @param the solution type */ -public abstract class AbstractGeometricRestartStrategy implements RestartStrategy { +public abstract class AbstractGeometricStuckCriterion implements StuckCriterion { + private static final Logger logger = LoggerFactory.getLogger(AbstractGeometricStuckCriterion.class); private static final double GEOMETRIC_FACTOR = 1.4; // Value extracted from the cited paper - protected final Clock clock; - protected final Logger logger = LoggerFactory.getLogger(AbstractGeometricRestartStrategy.class); - protected final double scalingFactor; + private static final long GRACE_PERIOD_MILLIS = 30_000; // Same value as DiminishedReturnsTermination + private final double scalingFactor; + private final Instant instant; private boolean gracePeriodFinished; private long gracePeriodMillis; protected long nextRestart; private double currentGeometricGrowFactor; - protected AbstractGeometricRestartStrategy(Clock clock, double scalingFactor) { - this.clock = clock; + protected AbstractGeometricStuckCriterion(Instant instant, double scalingFactor) { + this.instant = instant; this.scalingFactor = scalingFactor; } @@ -38,7 +39,7 @@ protected AbstractGeometricRestartStrategy(Clock clock, double scalingFactor) { public void phaseStarted(LocalSearchPhaseScope phaseScope) { if (gracePeriodMillis == 0) { // 10 seconds of grace period - gracePeriodMillis = clock.millis() + 10_000; + gracePeriodMillis = instant.toEpochMilli() + GRACE_PERIOD_MILLIS; } } @@ -61,9 +62,9 @@ public void solvingEnded(SolverScope solverScope) { } @Override - public boolean isTriggered(LocalSearchMoveScope moveScope) { + public boolean isSolverStuck(LocalSearchMoveScope moveScope) { if (isGracePeriodFinished()) { - var triggered = process(moveScope); + var triggered = evaluateCriterion(moveScope); if (triggered) { logger.trace("Restart triggered with geometric factor {}, scaling factor of {}", currentGeometricGrowFactor, scalingFactor); @@ -79,7 +80,7 @@ protected boolean isGracePeriodFinished() { if (gracePeriodFinished) { return true; } - gracePeriodFinished = clock.millis() >= gracePeriodMillis; + gracePeriodFinished = instant.toEpochMilli() >= gracePeriodMillis; return gracePeriodFinished; } @@ -87,6 +88,6 @@ private long calculateNextRestart() { return (long) Math.ceil(currentGeometricGrowFactor * scalingFactor); } - abstract boolean process(LocalSearchMoveScope moveScope); + abstract boolean evaluateCriterion(LocalSearchMoveScope moveScope); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/RestartStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/RestartStrategy.java deleted file mode 100644 index c2f1c668ed..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/RestartStrategy.java +++ /dev/null @@ -1,9 +0,0 @@ -package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; - -import ai.timefold.solver.core.impl.localsearch.event.LocalSearchPhaseLifecycleListener; -import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; - -public interface RestartStrategy extends LocalSearchPhaseLifecycleListener { - - boolean isTriggered(LocalSearchMoveScope moveScope); -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/StuckCriterion.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/StuckCriterion.java new file mode 100644 index 0000000000..9f48b927d7 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/StuckCriterion.java @@ -0,0 +1,22 @@ +package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; + +import ai.timefold.solver.core.impl.localsearch.event.LocalSearchPhaseLifecycleListener; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; + +/** + * Base contract + * for defining strategies that identify when the {@link ai.timefold.solver.core.api.solver.Solver solver} is stuck. + * + * @param + */ +public interface StuckCriterion extends LocalSearchPhaseLifecycleListener { + + /** + * Main logic that applies a specific metric to determine if a solver is stuck and cannot find better solutions + * in the current solving flow. + * + * @param moveScope cannot be null + * @return + */ + boolean isSolverStuck(LocalSearchMoveScope moveScope); +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountRestartStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterion.java similarity index 70% rename from core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountRestartStrategy.java rename to core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterion.java index a93fbafb83..41852a2d7f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountRestartStrategy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterion.java @@ -1,29 +1,34 @@ package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; import java.time.Clock; +import java.time.Instant; import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.impl.localsearch.decider.reconfiguration.RestartStrategy; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; import ai.timefold.solver.core.impl.solver.scope.SolverScope; /** - * Restart strategy, which exponentially increases the move count values that trigger the restart process. + * Criterion based on the unimproved move count. + * It exponentially increases the unimproved move count values that trigger the + * {@link RestartStrategy restart} process. */ -public class UnimprovedMoveCountRestartStrategy extends AbstractGeometricRestartStrategy { +public class UnimprovedMoveCountStuckCriterion extends AbstractGeometricStuckCriterion { + // 50k moves multiplier defined through experiments + protected static final long UNIMPROVED_MOVE_COUNT_MULTIPLIER = 50_000; // Last checkpoint of a solution improvement or the restart process protected long lastCheckpoint; private Score currentBestScore; - public UnimprovedMoveCountRestartStrategy() { - this(Clock.systemUTC()); + public UnimprovedMoveCountStuckCriterion() { + this(Instant.now(Clock.systemUTC())); } - protected UnimprovedMoveCountRestartStrategy(Clock clock) { - // 50k moves as the multiplier - super(clock, 50_000); + protected UnimprovedMoveCountStuckCriterion(Instant instant) { + super(instant, UNIMPROVED_MOVE_COUNT_MULTIPLIER); } @Override @@ -53,7 +58,7 @@ public void solvingStarted(SolverScope solverScope) { } @Override - public boolean process(LocalSearchMoveScope moveScope) { + public boolean evaluateCriterion(LocalSearchMoveScope moveScope) { var currentMoveCount = moveScope.getStepScope().getPhaseScope().getSolverScope().getMoveEvaluationCount(); if (lastCheckpoint == 0) { lastCheckpoint = currentMoveCount; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategy.java deleted file mode 100644 index d79efa631f..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategy.java +++ /dev/null @@ -1,15 +0,0 @@ -package ai.timefold.solver.core.impl.localsearch.decider.reconfiguration; - -import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleListener; -import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; - -public sealed interface ReconfigurationStrategy extends PhaseLifecycleListener - permits RestoreBestSolutionReconfigurationStrategy { - - void apply(AbstractStepScope stepScope); - - default boolean isTriggered(AbstractStepScope stepScope) { - return stepScope.getPhaseScope().isReconfigurationTriggered(); - } - -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestartStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestartStrategy.java new file mode 100644 index 0000000000..65a4cbf809 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestartStrategy.java @@ -0,0 +1,38 @@ +package ai.timefold.solver.core.impl.localsearch.decider.reconfiguration; + +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.StuckCriterion; +import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleListener; +import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; + +/** + * Base contract for defining restart strategies. + * The restart process is initiated + * when the {@link ai.timefold.solver.core.impl.localsearch.decider.LocalSearchDecider decider} identifies + * that the solver is {@link StuckCriterion stuck} + * and requires some logic to alter the current solving flow. + * + * @param the solution type + */ +public sealed interface RestartStrategy extends PhaseLifecycleListener + permits RestoreBestSolutionRestartStrategy { + + /** + * Restarts the solver to help it to get unstuck and discover new better solutions. + * + * @param stepScope cannot be null + */ + void applyRestart(AbstractStepScope stepScope); + + /** + * Evaluates whether the solver is stuck based on specific criteria + * and determines if reconfiguration logic needs to be applied. + * + * @param stepScope cannot be null + * + * @return true if the solver needs to be reconfigured; or false otherwise + */ + default boolean isSolverStuck(AbstractStepScope stepScope) { + return stepScope.getPhaseScope().isSolverStuck(); + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionReconfigurationStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionRestartStrategy.java similarity index 86% rename from core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionReconfigurationStrategy.java rename to core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionRestartStrategy.java index f7b52fde2c..7e369c20bc 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionReconfigurationStrategy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionRestartStrategy.java @@ -12,13 +12,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public final class RestoreBestSolutionReconfigurationStrategy implements ReconfigurationStrategy { +public final class RestoreBestSolutionRestartStrategy implements RestartStrategy { private final Logger logger = LoggerFactory.getLogger(getClass()); private final MoveSelector moveSelector; private final Acceptor acceptor; - public RestoreBestSolutionReconfigurationStrategy(MoveSelector moveSelector, Acceptor acceptor) { + public RestoreBestSolutionRestartStrategy(MoveSelector moveSelector, Acceptor acceptor) { this.moveSelector = moveSelector; Objects.requireNonNull(moveSelector); this.acceptor = acceptor; @@ -26,7 +26,7 @@ public RestoreBestSolutionReconfigurationStrategy(MoveSelector moveSe } @Override - public void apply(AbstractStepScope stepScope) { + public void applyRestart(AbstractStepScope stepScope) { var solverScope = stepScope.getPhaseScope().getSolverScope(); logger.trace("Resetting working solution, score ({})", solverScope.getBestScore()); solverScope.setWorkingSolutionFromBestSolution(); @@ -36,7 +36,7 @@ public void apply(AbstractStepScope stepScope) { // 2 - The acceptor will restart its search from the updated working solution (last best solution) acceptor.phaseStarted((LocalSearchPhaseScope) stepScope.getPhaseScope()); // Cancel it as the best solution is already restored - stepScope.getPhaseScope().cancelReconfiguration(); + stepScope.getPhaseScope().resetSolverStuck(); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java index 938f7b1f01..dd33741791 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java @@ -34,7 +34,7 @@ public abstract class AbstractPhaseScope { protected long childThreadsScoreCalculationCount = 0L; protected int bestSolutionStepIndex; - protected boolean reconfigurationTriggered = false; + protected boolean solverStuck = false; /** * As defined by #AbstractPhaseScope(SolverScope, int, boolean) @@ -248,16 +248,16 @@ public int getNextStepIndex() { return getLastCompletedStepScope().getStepIndex() + 1; } - public boolean isReconfigurationTriggered() { - return this.reconfigurationTriggered; + public boolean isSolverStuck() { + return this.solverStuck; } - public void triggerReconfiguration() { - this.reconfigurationTriggered = true; + public void triggerSolverStuck() { + this.solverStuck = true; } - public void cancelReconfiguration() { - this.reconfigurationTriggered = false; + public void resetSolverStuck() { + this.solverStuck = false; } @Override diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java index 40fb57feab..11a83b6568 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java @@ -6,7 +6,7 @@ import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.AbstractAcceptorTest; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.RestartStrategy; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.StuckCriterion; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; import ai.timefold.solver.core.impl.solver.scope.SolverScope; @@ -17,8 +17,8 @@ class DiversifiedLateAcceptanceAcceptorTest extends AbstractAcceptorTest { @Test void acceptanceCriterion() { - var restartStrategy = mock(RestartStrategy.class); - when(restartStrategy.isTriggered(any())).thenReturn(false); + var restartStrategy = mock(StuckCriterion.class); + when(restartStrategy.isSolverStuck(any())).thenReturn(false); var acceptor = new DiversifiedLateAcceptanceAcceptor<>(restartStrategy); acceptor.setLateAcceptanceSize(3); @@ -56,8 +56,8 @@ void acceptanceCriterion() { @Test void replacementCriterion() { - var restartStrategy = mock(RestartStrategy.class); - when(restartStrategy.isTriggered(any())).thenReturn(false); + var restartStrategy = mock(StuckCriterion.class); + when(restartStrategy.isSolverStuck(any())).thenReturn(false); var acceptor = new DiversifiedLateAcceptanceAcceptor<>(restartStrategy); acceptor.setLateAcceptanceSize(3); @@ -152,8 +152,8 @@ void replacementCriterion() { @Test void triggerReconfiguration() { - var restartStrategy = mock(RestartStrategy.class); - when(restartStrategy.isTriggered(any())).thenReturn(true); + var restartStrategy = mock(StuckCriterion.class); + when(restartStrategy.isSolverStuck(any())).thenReturn(true); var acceptor = new DiversifiedLateAcceptanceAcceptor<>(restartStrategy); acceptor.setLateAcceptanceSize(3); var solverScope = new SolverScope<>(); @@ -161,7 +161,7 @@ void triggerReconfiguration() { var stepScope0 = new LocalSearchStepScope<>(phaseScope); var moveScope0 = buildMoveScope(stepScope0, -2000); assertThat(acceptor.isAccepted(moveScope0)).isTrue(); - verify(restartStrategy, times(1)).isTriggered(any()); - assertThat(phaseScope.isReconfigurationTriggered()).isTrue(); + verify(restartStrategy, times(1)).isSolverStuck(any()); + assertThat(phaseScope.isSolverStuck()).isTrue(); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptorTest.java index 979b40376d..9a6d42bcc5 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptorTest.java @@ -8,7 +8,7 @@ import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.AbstractAcceptorTest; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.RestartStrategy; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.StuckCriterion; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; import ai.timefold.solver.core.impl.solver.scope.SolverScope; @@ -19,8 +19,8 @@ class LateAcceptanceAcceptorTest extends AbstractAcceptorTest { @Test void lateAcceptanceSize() { - var restartStrategy = mock(RestartStrategy.class); - when(restartStrategy.isTriggered(any())).thenReturn(false); + var restartStrategy = mock(StuckCriterion.class); + when(restartStrategy.isSolverStuck(any())).thenReturn(false); var acceptor = new LateAcceptanceAcceptor<>(restartStrategy); acceptor.setLateAcceptanceSize(3); acceptor.setHillClimbingEnabled(false); @@ -134,8 +134,8 @@ void lateAcceptanceSize() { @Test void hillClimbingEnabled() { - var restartStrategy = mock(RestartStrategy.class); - when(restartStrategy.isTriggered(any())).thenReturn(false); + var restartStrategy = mock(StuckCriterion.class); + when(restartStrategy.isSolverStuck(any())).thenReturn(false); var acceptor = new LateAcceptanceAcceptor<>(restartStrategy); acceptor.setLateAcceptanceSize(2); acceptor.setHillClimbingEnabled(true); @@ -249,8 +249,8 @@ void hillClimbingEnabled() { @Test void zeroLateAcceptanceSize() { - var restartStrategy = mock(RestartStrategy.class); - when(restartStrategy.isTriggered(any())).thenReturn(false); + var restartStrategy = mock(StuckCriterion.class); + when(restartStrategy.isSolverStuck(any())).thenReturn(false); var acceptor = new LateAcceptanceAcceptor<>(restartStrategy); acceptor.setLateAcceptanceSize(0); assertThatIllegalArgumentException().isThrownBy(() -> acceptor.phaseStarted(null)); @@ -258,8 +258,8 @@ void zeroLateAcceptanceSize() { @Test void negativeLateAcceptanceSize() { - var restartStrategy = mock(RestartStrategy.class); - when(restartStrategy.isTriggered(any())).thenReturn(false); + var restartStrategy = mock(StuckCriterion.class); + when(restartStrategy.isSolverStuck(any())).thenReturn(false); var acceptor = new LateAcceptanceAcceptor<>(restartStrategy); acceptor.setLateAcceptanceSize(-1); assertThatIllegalArgumentException().isThrownBy(() -> acceptor.phaseStarted(null)); @@ -267,8 +267,8 @@ void negativeLateAcceptanceSize() { @Test void triggerReconfiguration() { - var restartStrategy = mock(RestartStrategy.class); - when(restartStrategy.isTriggered(any())).thenReturn(true); + var restartStrategy = mock(StuckCriterion.class); + when(restartStrategy.isSolverStuck(any())).thenReturn(true); var acceptor = new LateAcceptanceAcceptor<>(restartStrategy); acceptor.setLateAcceptanceSize(3); var solverScope = new SolverScope<>(); @@ -276,6 +276,6 @@ void triggerReconfiguration() { var stepScope0 = new LocalSearchStepScope<>(phaseScope); var moveScope0 = buildMoveScope(stepScope0, -2000); assertThat(acceptor.isAccepted(moveScope0)).isTrue(); - assertThat(phaseScope.isReconfigurationTriggered()).isTrue(); + assertThat(phaseScope.isSolverStuck()).isTrue(); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountRestartStrategyTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterionTest.java similarity index 63% rename from core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountRestartStrategyTest.java rename to core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterionTest.java index ffb999f077..816f1510c3 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountRestartStrategyTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterionTest.java @@ -1,10 +1,11 @@ package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; +import static ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.UnimprovedMoveCountStuckCriterion.UNIMPROVED_MOVE_COUNT_MULTIPLIER; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import java.time.Clock; +import java.time.Instant; import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; @@ -15,11 +16,11 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; -class UnimprovedMoveCountRestartStrategyTest { +class UnimprovedMoveCountStuckCriterionTest { @Test - void isTriggered() { - var clock = mock(Clock.class); + void isSolverStuck() { + var instant = mock(Instant.class); var solverScope = mock(SolverScope.class); var phaseScope = mock(LocalSearchPhaseScope.class); var stepScope = mock(LocalSearchStepScope.class); @@ -27,48 +28,48 @@ void isTriggered() { when(moveScope.getStepScope()).thenReturn(stepScope); when(stepScope.getPhaseScope()).thenReturn(phaseScope); when(phaseScope.getSolverScope()).thenReturn(solverScope); - when(clock.millis()).thenReturn(1000L, 11000L); + when(instant.toEpochMilli()).thenReturn(1000L, UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1000L); when(solverScope.getMoveEvaluationCount()).thenReturn(1000L); when(phaseScope.getBestScore()).thenReturn(SimpleScore.of(1000)); // Finish grace period - var strategy = new UnimprovedMoveCountRestartStrategy<>(clock); + var strategy = new UnimprovedMoveCountStuckCriterion<>(instant); strategy.solvingStarted(null); strategy.phaseStarted(phaseScope); - assertThat(strategy.isTriggered(moveScope)).isFalse(); + assertThat(strategy.isSolverStuck(moveScope)).isFalse(); assertThat(strategy.lastCheckpoint).isEqualTo(1000L); - assertThat(strategy.nextRestart).isEqualTo(50000L); + assertThat(strategy.nextRestart).isEqualTo(UNIMPROVED_MOVE_COUNT_MULTIPLIER); // First restart - Mockito.reset(clock); - var firstCount = 60000L; + Mockito.reset(instant); + var firstCount = UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1000L; when(solverScope.getMoveEvaluationCount()).thenReturn(firstCount); - assertThat(strategy.isTriggered(moveScope)).isTrue(); + assertThat(strategy.isSolverStuck(moveScope)).isTrue(); assertThat(strategy.lastCheckpoint).isEqualTo(firstCount); - assertThat(strategy.nextRestart).isEqualTo(2L * 50000L); - assertThat(strategy.isTriggered(moveScope)).isFalse(); + assertThat(strategy.nextRestart).isEqualTo(2L * UNIMPROVED_MOVE_COUNT_MULTIPLIER); + assertThat(strategy.isSolverStuck(moveScope)).isFalse(); // Second restart - Mockito.reset(clock); - var secondCount = 2L * 50000L + firstCount; + Mockito.reset(instant); + var secondCount = 2L * UNIMPROVED_MOVE_COUNT_MULTIPLIER + firstCount; when(solverScope.getMoveEvaluationCount()).thenReturn(secondCount); - assertThat(strategy.isTriggered(moveScope)).isTrue(); + assertThat(strategy.isSolverStuck(moveScope)).isTrue(); assertThat(strategy.lastCheckpoint).isEqualTo(secondCount); - assertThat(strategy.nextRestart).isEqualTo(3L * 50000L); - assertThat(strategy.isTriggered(moveScope)).isFalse(); + assertThat(strategy.nextRestart).isEqualTo(3L * UNIMPROVED_MOVE_COUNT_MULTIPLIER); + assertThat(strategy.isSolverStuck(moveScope)).isFalse(); // Third restart - var thirdCount = 3L * 50000L + secondCount; - Mockito.reset(clock); + var thirdCount = 3L * UNIMPROVED_MOVE_COUNT_MULTIPLIER + secondCount; + Mockito.reset(instant); when(solverScope.getMoveEvaluationCount()).thenReturn(thirdCount); - assertThat(strategy.isTriggered(moveScope)).isTrue(); + assertThat(strategy.isSolverStuck(moveScope)).isTrue(); assertThat(strategy.lastCheckpoint).isEqualTo(thirdCount); - assertThat(strategy.nextRestart).isEqualTo(5L * 50000L); + assertThat(strategy.nextRestart).isEqualTo(5L * UNIMPROVED_MOVE_COUNT_MULTIPLIER); } @Test void updateBestSolution() { - var clock = mock(Clock.class); + var instant = mock(Instant.class); var solverScope = mock(SolverScope.class); var phaseScope = mock(LocalSearchPhaseScope.class); var stepScope = mock(LocalSearchStepScope.class); @@ -78,15 +79,15 @@ void updateBestSolution() { when(moveScope.getStepScope()).thenReturn(stepScope); when(phaseScope.getBestScore()).thenReturn(SimpleScore.of(1000)); when(stepScope.getScore()).thenReturn(SimpleScore.of(2000)); - when(clock.millis()).thenReturn(1000L, 20000L, 20000L, 20001L); + when(instant.toEpochMilli()).thenReturn(1000L, UNIMPROVED_MOVE_COUNT_MULTIPLIER, UNIMPROVED_MOVE_COUNT_MULTIPLIER, UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1); when(solverScope.getMoveEvaluationCount()).thenReturn(1000L, 1001L); - var strategy = new UnimprovedMoveCountRestartStrategy<>(clock); + var strategy = new UnimprovedMoveCountStuckCriterion<>(instant); strategy.solvingStarted(mock(SolverScope.class)); strategy.phaseStarted(phaseScope); strategy.stepStarted(stepScope); // Trigger - strategy.isTriggered(moveScope); + strategy.isSolverStuck(moveScope); // Update the last improvement strategy.stepEnded(stepScope); assertThat(strategy.lastCheckpoint).isEqualTo(1001L); @@ -94,7 +95,7 @@ void updateBestSolution() { @Test void reset() { - var clock = mock(Clock.class); + var instant = mock(Instant.class); var solverScope = mock(SolverScope.class); var phaseScope = mock(LocalSearchPhaseScope.class); var stepScope = mock(LocalSearchStepScope.class); @@ -102,14 +103,14 @@ void reset() { when(moveScope.getStepScope()).thenReturn(stepScope); when(stepScope.getPhaseScope()).thenReturn(phaseScope); when(phaseScope.getSolverScope()).thenReturn(solverScope); - when(clock.millis()).thenReturn(1000L, 11000L); + when(instant.toEpochMilli()).thenReturn(1000L, UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1000L); when(solverScope.getMoveEvaluationCount()).thenReturn(1000L); - var strategy = new UnimprovedMoveCountRestartStrategy<>(clock); + var strategy = new UnimprovedMoveCountStuckCriterion<>(instant); strategy.solvingStarted(mock(SolverScope.class)); strategy.phaseStarted(mock(LocalSearchPhaseScope.class)); // Trigger - strategy.isTriggered(moveScope); + strategy.isSolverStuck(moveScope); assertThat(strategy.lastCheckpoint).isEqualTo(1000L); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategyTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestartStrategyTest.java similarity index 82% rename from core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategyTest.java rename to core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestartStrategyTest.java index c2fdf339b7..0cfa963cfb 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/ReconfigurationStrategyTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestartStrategyTest.java @@ -11,29 +11,29 @@ import org.junit.jupiter.api.Test; -class ReconfigurationStrategyTest { +class RestartStrategyTest { @Test void restoreBestSolution() { // Requires the decider - var badStrategy = new RestoreBestSolutionReconfigurationStrategy<>(null, null); + var badStrategy = new RestoreBestSolutionRestartStrategy<>(null, null); assertThatThrownBy(() -> badStrategy.phaseStarted(null)).isInstanceOf(NullPointerException.class); // Restore the best solution var moveSelector = mock(MoveSelector.class); var acceptor = mock(Acceptor.class); - var strategy = new RestoreBestSolutionReconfigurationStrategy<>(moveSelector, acceptor); + var strategy = new RestoreBestSolutionRestartStrategy<>(moveSelector, acceptor); var solverScope = mock(SolverScope.class); var phaseScope = mock(LocalSearchPhaseScope.class); when(phaseScope.getSolverScope()).thenReturn(solverScope); var stepScope = mock(LocalSearchStepScope.class); when(stepScope.getPhaseScope()).thenReturn(phaseScope); - strategy.apply(stepScope); + strategy.applyRestart(stepScope); // Restore the best solution verify(solverScope, times(1)).setWorkingSolutionFromBestSolution(); // Restart the phase - verify(phaseScope, times(1)).cancelReconfiguration(); + verify(phaseScope, times(1)).resetSolverStuck(); verify(moveSelector, times(1)).phaseStarted(any()); verify(acceptor, times(1)).phaseStarted(any()); } From 8fdfd53cb9fc6e6c38c3c027f757e8d134d9e586 Mon Sep 17 00:00:00 2001 From: Fred Date: Mon, 27 Jan 2025 18:24:55 -0300 Subject: [PATCH 18/31] chore: update only the current score of the late elements list to maintain the diversity --- .../DefaultLocalSearchPhaseFactory.java | 2 +- .../decider/LocalSearchDecider.java | 12 +- ...Acceptor.java => RestartableAcceptor.java} | 6 +- .../DiversifiedLateAcceptanceAcceptor.java | 15 +- .../LateAcceptanceAcceptor.java | 14 +- .../reconfiguration/AdaptedSolverScope.java | 317 ++++++++++++++++++ .../RestoreBestSolutionRestartStrategy.java | 27 +- .../reconfiguration/RestartStrategyTest.java | 19 +- 8 files changed, 370 insertions(+), 42 deletions(-) rename core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/{ReconfigurableAcceptor.java => RestartableAcceptor.java} (89%) create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/AdaptedSolverScope.java diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java index ce3cde0f42..d1fc083447 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java @@ -66,7 +66,7 @@ private LocalSearchDecider buildDecider(HeuristicConfigPolicy termination) { var moveSelector = buildMoveSelector(configPolicy); var acceptor = buildAcceptor(configPolicy); - var restartStrategy = new RestoreBestSolutionRestartStrategy<>(moveSelector, acceptor); + var restartStrategy = new RestoreBestSolutionRestartStrategy(); var forager = buildForager(configPolicy); if (moveSelector.isNeverEnding() && !forager.supportsNeverEndingMoveSelector()) { throw new IllegalStateException("The moveSelector (" + moveSelector diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java index b6f0156521..0145289b85 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java @@ -6,6 +6,7 @@ import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.Acceptor; import ai.timefold.solver.core.impl.localsearch.decider.forager.LocalSearchForager; +import ai.timefold.solver.core.impl.localsearch.decider.reconfiguration.AdaptedSolverScope; import ai.timefold.solver.core.impl.localsearch.decider.reconfiguration.RestartStrategy; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; @@ -77,7 +78,7 @@ public void setAssertExpectedUndoMoveScore(boolean assertExpectedUndoMoveScore) // ************************************************************************ public void solvingStarted(SolverScope solverScope) { - restartStrategy.solvingStarted(solverScope); + restartStrategy.solvingStarted(new AdaptedSolverScope<>(solverScope, this)); moveSelector.solvingStarted(solverScope); acceptor.solvingStarted(solverScope); forager.solvingStarted(solverScope); @@ -178,6 +179,15 @@ public void solvingEnded(SolverScope solverScope) { forager.solvingEnded(solverScope); } + public void setWorkingSolutionFromBestSolution(LocalSearchStepScope stepScope) { + stepScope.getPhaseScope().getSolverScope().setWorkingSolutionFromBestSolution(); + // Changing the working solution requires reinitializing the move selector. + // The acceptor should not be restarted, as this may lead to an inconsistent state, + // such as changing the scores of all late elements in LA and DLAS. + // 1 - The move selector will reset all cached lists using old solution entity references + moveSelector.phaseStarted(stepScope.getPhaseScope()); + } + public void solvingError(SolverScope solverScope, Exception exception) { // Overridable by a subclass. } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/RestartableAcceptor.java similarity index 89% rename from core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java rename to core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/RestartableAcceptor.java index cc8764f810..ed06c3b128 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/ReconfigurableAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/RestartableAcceptor.java @@ -10,11 +10,12 @@ * Base class designed to analyze whether the solving process needs to be restarted. * Additionally, it also calls a reconfiguration logic as a result of restarting the solving process. */ -public abstract class ReconfigurableAcceptor extends AbstractAcceptor { +public abstract class RestartableAcceptor extends AbstractAcceptor { private final StuckCriterion stuckCriterionDetection; + protected boolean restartTriggered; - protected ReconfigurableAcceptor(StuckCriterion stuckCriterionDetection) { + protected RestartableAcceptor(StuckCriterion stuckCriterionDetection) { this.stuckCriterionDetection = stuckCriterionDetection; } @@ -52,6 +53,7 @@ public void stepEnded(LocalSearchStepScope stepScope) { public boolean isAccepted(LocalSearchMoveScope moveScope) { if (stuckCriterionDetection.isSolverStuck(moveScope)) { moveScope.getStepScope().getPhaseScope().triggerSolverStuck(); + restartTriggered = true; return true; } return applyAcceptanceCriteria(moveScope); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java index 4022c3f226..b2a99f174d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java @@ -3,12 +3,13 @@ import java.util.Arrays; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.ReconfigurableAcceptor; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.RestartableAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.StuckCriterion; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; -public class DiversifiedLateAcceptanceAcceptor extends ReconfigurableAcceptor { +public class DiversifiedLateAcceptanceAcceptor extends RestartableAcceptor { // The worst score in the late elements list protected Score lateWorse; @@ -107,6 +108,16 @@ private void updateLateScore(Score newScore) { } } + @Override + public void stepEnded(LocalSearchStepScope stepScope) { + super.stepEnded(stepScope); + if (restartTriggered) { + // Update the current late score with the best score + previousScores[lateScoreIndex] = stepScope.getPhaseScope().getBestScore(); + restartTriggered = false; + } + } + @Override public void phaseEnded(LocalSearchPhaseScope phaseScope) { super.phaseEnded(phaseScope); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java index 5d0bc3579a..03f1343102 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java @@ -3,13 +3,13 @@ import java.util.Arrays; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.ReconfigurableAcceptor; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.RestartableAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.StuckCriterion; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; -public class LateAcceptanceAcceptor extends ReconfigurableAcceptor { +public class LateAcceptanceAcceptor extends RestartableAcceptor { protected int lateAcceptanceSize = -1; protected boolean hillClimbingEnabled = true; @@ -68,8 +68,14 @@ public boolean applyAcceptanceCriteria(LocalSearchMoveScope moveScope @Override public void stepEnded(LocalSearchStepScope stepScope) { super.stepEnded(stepScope); - previousScores[lateScoreIndex] = stepScope.getScore(); - lateScoreIndex = (lateScoreIndex + 1) % lateAcceptanceSize; + if (!restartTriggered) { + previousScores[lateScoreIndex] = stepScope.getScore(); + lateScoreIndex = (lateScoreIndex + 1) % lateAcceptanceSize; + } else { + // Update the current late score with the best score, keeping the late score index the same + previousScores[lateScoreIndex] = stepScope.getPhaseScope().getBestScore(); + restartTriggered = false; + } } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/AdaptedSolverScope.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/AdaptedSolverScope.java new file mode 100644 index 0000000000..b3d195a808 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/AdaptedSolverScope.java @@ -0,0 +1,317 @@ +package ai.timefold.solver.core.impl.localsearch.decider.reconfiguration; + +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicReference; + +import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.api.solver.ProblemSizeStatistics; +import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; +import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.localsearch.decider.LocalSearchDecider; +import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; +import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; +import ai.timefold.solver.core.impl.solver.AbstractSolver; +import ai.timefold.solver.core.impl.solver.change.DefaultProblemChangeDirector; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; +import ai.timefold.solver.core.impl.solver.thread.ChildThreadType; + +import io.micrometer.core.instrument.Tags; + +public class AdaptedSolverScope extends SolverScope { + private final SolverScope solverScope; + private final LocalSearchDecider decider; + + public AdaptedSolverScope(SolverScope solverScope, LocalSearchDecider decider) { + this.solverScope = solverScope; + this.decider = decider; + } + + protected LocalSearchDecider getDecider() { + return decider; + } + + @Override + public AbstractSolver getSolver() { + return solverScope.getSolver(); + } + + @Override + public void setSolver(AbstractSolver solver) { + solverScope.setSolver(solver); + } + + @Override + public DefaultProblemChangeDirector getProblemChangeDirector() { + return solverScope.getProblemChangeDirector(); + } + + @Override + public void setProblemChangeDirector(DefaultProblemChangeDirector problemChangeDirector) { + solverScope.setProblemChangeDirector(problemChangeDirector); + } + + @Override + public Tags getMonitoringTags() { + return solverScope.getMonitoringTags(); + } + + @Override + public void setMonitoringTags(Tags monitoringTags) { + solverScope.setMonitoringTags(monitoringTags); + } + + @Override + public Map>> getStepScoreMap() { + return solverScope.getStepScoreMap(); + } + + @Override + public Set getSolverMetricSet() { + return solverScope.getSolverMetricSet(); + } + + @Override + public void setSolverMetricSet(EnumSet solverMetricSet) { + solverScope.setSolverMetricSet(solverMetricSet); + } + + @Override + public int getStartingSolverCount() { + return solverScope.getStartingSolverCount(); + } + + @Override + public void setStartingSolverCount(int startingSolverCount) { + solverScope.setStartingSolverCount(startingSolverCount); + } + + @Override + public Random getWorkingRandom() { + return solverScope.getWorkingRandom(); + } + + @Override + public void setWorkingRandom(Random workingRandom) { + solverScope.setWorkingRandom(workingRandom); + } + + @Override + public > InnerScoreDirector getScoreDirector() { + return solverScope.getScoreDirector(); + } + + @Override + public void setScoreDirector(InnerScoreDirector scoreDirector) { + solverScope.setScoreDirector(scoreDirector); + } + + @Override + public void setRunnableThreadSemaphore(Semaphore runnableThreadSemaphore) { + solverScope.setRunnableThreadSemaphore(runnableThreadSemaphore); + } + + @Override + public Long getStartingSystemTimeMillis() { + return solverScope.getStartingSystemTimeMillis(); + } + + @Override + public Long getEndingSystemTimeMillis() { + return solverScope.getEndingSystemTimeMillis(); + } + + @Override + public SolutionDescriptor getSolutionDescriptor() { + return solverScope.getSolutionDescriptor(); + } + + @Override + public ScoreDefinition getScoreDefinition() { + return solverScope.getScoreDefinition(); + } + + @Override + public Solution_ getWorkingSolution() { + return solverScope.getWorkingSolution(); + } + + @Override + public int getWorkingEntityCount() { + return solverScope.getWorkingEntityCount(); + } + + @Override + public Score calculateScore() { + return solverScope.calculateScore(); + } + + @Override + public void assertScoreFromScratch(Solution_ solution) { + solverScope.assertScoreFromScratch(solution); + } + + @Override + public Score getStartingInitializedScore() { + return solverScope.getStartingInitializedScore(); + } + + @Override + public void setStartingInitializedScore(Score startingInitializedScore) { + solverScope.setStartingInitializedScore(startingInitializedScore); + } + + @Override + public void addChildThreadsScoreCalculationCount(long addition) { + solverScope.addChildThreadsScoreCalculationCount(addition); + } + + @Override + public long getScoreCalculationCount() { + return solverScope.getScoreCalculationCount(); + } + + @Override + public void addMoveEvaluationCount(long addition) { + solverScope.addMoveEvaluationCount(addition); + } + + @Override + public void addChildThreadsMoveEvaluationCount(long addition) { + solverScope.addChildThreadsMoveEvaluationCount(addition); + } + + @Override + public long getMoveEvaluationCount() { + return solverScope.getMoveEvaluationCount(); + } + + @Override + public Solution_ getBestSolution() { + return solverScope.getBestSolution(); + } + + @Override + public void setBestSolution(Solution_ bestSolution) { + solverScope.setBestSolution(bestSolution); + } + + @Override + public Score getBestScore() { + return solverScope.getBestScore(); + } + + @Override + public void setBestScore(Score bestScore) { + solverScope.setBestScore(bestScore); + } + + @Override + public Long getBestSolutionTimeMillis() { + return solverScope.getBestSolutionTimeMillis(); + } + + @Override + public void setBestSolutionTimeMillis(Long bestSolutionTimeMillis) { + solverScope.setBestSolutionTimeMillis(bestSolutionTimeMillis); + } + + @Override + public Set getMoveCountTypes() { + return solverScope.getMoveCountTypes(); + } + + @Override + public Map getMoveEvaluationCountPerType() { + return solverScope.getMoveEvaluationCountPerType(); + } + + @Override + public boolean isMetricEnabled(SolverMetric solverMetric) { + return solverScope.isMetricEnabled(solverMetric); + } + + @Override + public void startingNow() { + solverScope.startingNow(); + } + + @Override + public Long getBestSolutionTimeMillisSpent() { + return solverScope.getBestSolutionTimeMillisSpent(); + } + + @Override + public void endingNow() { + solverScope.endingNow(); + } + + @Override + public boolean isBestSolutionInitialized() { + return solverScope.isBestSolutionInitialized(); + } + + @Override + public long calculateTimeMillisSpentUpToNow() { + return solverScope.calculateTimeMillisSpentUpToNow(); + } + + @Override + public long getTimeMillisSpent() { + return solverScope.getTimeMillisSpent(); + } + + @Override + public ProblemSizeStatistics getProblemSizeStatistics() { + return solverScope.getProblemSizeStatistics(); + } + + @Override + public void setProblemSizeStatistics(ProblemSizeStatistics problemSizeStatistics) { + solverScope.setProblemSizeStatistics(problemSizeStatistics); + } + + @Override + public long getScoreCalculationSpeed() { + return solverScope.getScoreCalculationSpeed(); + } + + @Override + public long getMoveEvaluationSpeed() { + return solverScope.getMoveEvaluationSpeed(); + } + + @Override + public void setWorkingSolutionFromBestSolution() { + solverScope.setWorkingSolutionFromBestSolution(); + } + + @Override + public SolverScope createChildThreadSolverScope(ChildThreadType childThreadType) { + return solverScope.createChildThreadSolverScope(childThreadType); + } + + @Override + public void initializeYielding() { + solverScope.initializeYielding(); + } + + @Override + public void checkYielding() { + solverScope.checkYielding(); + } + + @Override + public void destroyYielding() { + solverScope.destroyYielding(); + } + + @Override + public void addMoveEvaluationCountPerType(String moveType, long count) { + solverScope.addMoveEvaluationCountPerType(moveType, count); + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionRestartStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionRestartStrategy.java index 7e369c20bc..573512b927 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionRestartStrategy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionRestartStrategy.java @@ -2,9 +2,8 @@ import java.util.Objects; -import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.Acceptor; -import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; +import ai.timefold.solver.core.impl.localsearch.decider.LocalSearchDecider; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; import ai.timefold.solver.core.impl.solver.scope.SolverScope; @@ -15,27 +14,14 @@ public final class RestoreBestSolutionRestartStrategy implements RestartStrategy { private final Logger logger = LoggerFactory.getLogger(getClass()); - private final MoveSelector moveSelector; - private final Acceptor acceptor; - - public RestoreBestSolutionRestartStrategy(MoveSelector moveSelector, Acceptor acceptor) { - this.moveSelector = moveSelector; - Objects.requireNonNull(moveSelector); - this.acceptor = acceptor; - Objects.requireNonNull(acceptor); - } + private LocalSearchDecider decider; @Override public void applyRestart(AbstractStepScope stepScope) { var solverScope = stepScope.getPhaseScope().getSolverScope(); logger.trace("Resetting working solution, score ({})", solverScope.getBestScore()); - solverScope.setWorkingSolutionFromBestSolution(); - // Changing the working solution requires reinitializing the move selector and acceptor - // 1 - The move selector will reset all cached lists using old solution entity references - moveSelector.phaseStarted(stepScope.getPhaseScope()); - // 2 - The acceptor will restart its search from the updated working solution (last best solution) - acceptor.phaseStarted((LocalSearchPhaseScope) stepScope.getPhaseScope()); - // Cancel it as the best solution is already restored + decider.setWorkingSolutionFromBestSolution((LocalSearchStepScope) stepScope); + // Mark the solver as unstuck as the best solution is already restored stepScope.getPhaseScope().resetSolverStuck(); } @@ -61,7 +47,8 @@ public void phaseEnded(AbstractPhaseScope phaseScope) { @Override public void solvingStarted(SolverScope solverScope) { - // Do nothing + var castSolverScope = (AdaptedSolverScope) solverScope; + this.decider = Objects.requireNonNull(castSolverScope.getDecider()); } @Override diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestartStrategyTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestartStrategyTest.java index 0cfa963cfb..282eee6b57 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestartStrategyTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestartStrategyTest.java @@ -3,8 +3,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.*; -import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.Acceptor; +import ai.timefold.solver.core.impl.localsearch.decider.LocalSearchDecider; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; import ai.timefold.solver.core.impl.solver.scope.SolverScope; @@ -16,25 +15,21 @@ class RestartStrategyTest { @Test void restoreBestSolution() { // Requires the decider - var badStrategy = new RestoreBestSolutionRestartStrategy<>(null, null); - assertThatThrownBy(() -> badStrategy.phaseStarted(null)).isInstanceOf(NullPointerException.class); + var badStrategy = new RestoreBestSolutionRestartStrategy<>(); + assertThatThrownBy(() -> badStrategy.solvingStarted(null)).isInstanceOf(NullPointerException.class); // Restore the best solution - var moveSelector = mock(MoveSelector.class); - var acceptor = mock(Acceptor.class); - var strategy = new RestoreBestSolutionRestartStrategy<>(moveSelector, acceptor); + var strategy = new RestoreBestSolutionRestartStrategy<>(); + var decider = mock(LocalSearchDecider.class); var solverScope = mock(SolverScope.class); var phaseScope = mock(LocalSearchPhaseScope.class); when(phaseScope.getSolverScope()).thenReturn(solverScope); var stepScope = mock(LocalSearchStepScope.class); when(stepScope.getPhaseScope()).thenReturn(phaseScope); + strategy.solvingStarted(new AdaptedSolverScope<>(solverScope, decider)); strategy.applyRestart(stepScope); // Restore the best solution - verify(solverScope, times(1)).setWorkingSolutionFromBestSolution(); - // Restart the phase - verify(phaseScope, times(1)).resetSolverStuck(); - verify(moveSelector, times(1)).phaseStarted(any()); - verify(acceptor, times(1)).phaseStarted(any()); + verify(decider, times(1)).setWorkingSolutionFromBestSolution(any()); } } From 62219661cdd5fab2607e8d0c2ccefbb507387b5e Mon Sep 17 00:00:00 2001 From: Fred Date: Mon, 27 Jan 2025 18:33:01 -0300 Subject: [PATCH 19/31] chore: formatting --- .../restart/UnimprovedMoveCountStuckCriterionTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterionTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterionTest.java index 816f1510c3..c3f39279bc 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterionTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterionTest.java @@ -79,7 +79,8 @@ void updateBestSolution() { when(moveScope.getStepScope()).thenReturn(stepScope); when(phaseScope.getBestScore()).thenReturn(SimpleScore.of(1000)); when(stepScope.getScore()).thenReturn(SimpleScore.of(2000)); - when(instant.toEpochMilli()).thenReturn(1000L, UNIMPROVED_MOVE_COUNT_MULTIPLIER, UNIMPROVED_MOVE_COUNT_MULTIPLIER, UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1); + when(instant.toEpochMilli()).thenReturn(1000L, UNIMPROVED_MOVE_COUNT_MULTIPLIER, UNIMPROVED_MOVE_COUNT_MULTIPLIER, + UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1); when(solverScope.getMoveEvaluationCount()).thenReturn(1000L, 1001L); var strategy = new UnimprovedMoveCountStuckCriterion<>(instant); From 4a1b322935ef91a7eb75fdf162c50545fe0266c6 Mon Sep 17 00:00:00 2001 From: Fred Date: Tue, 28 Jan 2025 14:52:37 -0300 Subject: [PATCH 20/31] chore: improve restart logic --- .../decider/LocalSearchDecider.java | 3 +++ .../DiversifiedLateAcceptanceAcceptor.java | 11 --------- .../LateAcceptanceAcceptor.java | 3 ++- .../AbstractGeometricStuckCriterion.java | 23 +++++++++++-------- .../UnimprovedMoveCountStuckCriterion.java | 18 ++++----------- ...UnimprovedMoveCountStuckCriterionTest.java | 17 ++++++++++---- 6 files changed, 35 insertions(+), 40 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java index 0145289b85..9eb2fad115 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java @@ -181,6 +181,9 @@ public void solvingEnded(SolverScope solverScope) { public void setWorkingSolutionFromBestSolution(LocalSearchStepScope stepScope) { stepScope.getPhaseScope().getSolverScope().setWorkingSolutionFromBestSolution(); + // Adjust the step score to reflect the best score, + // ensuring the score of the last completed step is the current best one + stepScope.setScore(stepScope.getPhaseScope().getBestScore()); // Changing the working solution requires reinitializing the move selector. // The acceptor should not be restarted, as this may lead to an inconsistent state, // such as changing the scores of all late elements in LA and DLAS. diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java index b2a99f174d..fb9553db21 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java @@ -7,7 +7,6 @@ import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.StuckCriterion; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; -import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; public class DiversifiedLateAcceptanceAcceptor extends RestartableAcceptor { @@ -108,16 +107,6 @@ private void updateLateScore(Score newScore) { } } - @Override - public void stepEnded(LocalSearchStepScope stepScope) { - super.stepEnded(stepScope); - if (restartTriggered) { - // Update the current late score with the best score - previousScores[lateScoreIndex] = stepScope.getPhaseScope().getBestScore(); - restartTriggered = false; - } - } - @Override public void phaseEnded(LocalSearchPhaseScope phaseScope) { super.phaseEnded(phaseScope); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java index 03f1343102..da30741d09 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java @@ -72,7 +72,8 @@ public void stepEnded(LocalSearchStepScope stepScope) { previousScores[lateScoreIndex] = stepScope.getScore(); lateScoreIndex = (lateScoreIndex + 1) % lateAcceptanceSize; } else { - // Update the current late score with the best score, keeping the late score index the same + // We only modify the current late element to raise the intensification phase, + // while ensuring that the other elements remain unchanged to maintain diversity. previousScores[lateScoreIndex] = stepScope.getPhaseScope().getBestScore(); restartTriggered = false; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java index 3484872780..a9adef9079 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java @@ -1,6 +1,6 @@ package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; -import java.time.Instant; +import java.time.Clock; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; @@ -19,27 +19,27 @@ * @param the solution type */ public abstract class AbstractGeometricStuckCriterion implements StuckCriterion { - private static final Logger logger = LoggerFactory.getLogger(AbstractGeometricStuckCriterion.class); + protected static final Logger logger = LoggerFactory.getLogger(AbstractGeometricStuckCriterion.class); private static final double GEOMETRIC_FACTOR = 1.4; // Value extracted from the cited paper private static final long GRACE_PERIOD_MILLIS = 30_000; // Same value as DiminishedReturnsTermination private final double scalingFactor; - private final Instant instant; + private final Clock clock; private boolean gracePeriodFinished; private long gracePeriodMillis; protected long nextRestart; private double currentGeometricGrowFactor; - protected AbstractGeometricStuckCriterion(Instant instant, double scalingFactor) { - this.instant = instant; + protected AbstractGeometricStuckCriterion(Clock clock, double scalingFactor) { + this.clock = clock; this.scalingFactor = scalingFactor; } @Override public void phaseStarted(LocalSearchPhaseScope phaseScope) { if (gracePeriodMillis == 0) { - // 10 seconds of grace period - gracePeriodMillis = instant.toEpochMilli() + GRACE_PERIOD_MILLIS; + // 30 seconds of grace period + gracePeriodMillis = clock.instant().toEpochMilli() + GRACE_PERIOD_MILLIS; } } @@ -66,8 +66,11 @@ public boolean isSolverStuck(LocalSearchMoveScope moveScope) { if (isGracePeriodFinished()) { var triggered = evaluateCriterion(moveScope); if (triggered) { - logger.trace("Restart triggered with geometric factor {}, scaling factor of {}", currentGeometricGrowFactor, - scalingFactor); + logger.trace( + "Restart triggered with geometric factor {}, scaling factor of {}, best score ({}), move count ({})", + currentGeometricGrowFactor, + scalingFactor, moveScope.getStepScope().getPhaseScope().getBestScore(), + moveScope.getStepScope().getPhaseScope().getSolverScope().getMoveEvaluationCount()); currentGeometricGrowFactor = Math.ceil(currentGeometricGrowFactor * GEOMETRIC_FACTOR); nextRestart = calculateNextRestart(); return true; @@ -80,7 +83,7 @@ protected boolean isGracePeriodFinished() { if (gracePeriodFinished) { return true; } - gracePeriodFinished = instant.toEpochMilli() >= gracePeriodMillis; + gracePeriodFinished = clock.instant().toEpochMilli() >= gracePeriodMillis; return gracePeriodFinished; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterion.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterion.java index 41852a2d7f..ca8e8918ed 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterion.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterion.java @@ -1,12 +1,10 @@ package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; import java.time.Clock; -import java.time.Instant; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.impl.localsearch.decider.reconfiguration.RestartStrategy; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; -import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; import ai.timefold.solver.core.impl.solver.scope.SolverScope; @@ -17,24 +15,18 @@ */ public class UnimprovedMoveCountStuckCriterion extends AbstractGeometricStuckCriterion { - // 50k moves multiplier defined through experiments - protected static final long UNIMPROVED_MOVE_COUNT_MULTIPLIER = 50_000; + // Multiplier defined through experiments + protected static final long UNIMPROVED_MOVE_COUNT_MULTIPLIER = 300_000; // Last checkpoint of a solution improvement or the restart process protected long lastCheckpoint; private Score currentBestScore; public UnimprovedMoveCountStuckCriterion() { - this(Instant.now(Clock.systemUTC())); + this(Clock.systemUTC()); } - protected UnimprovedMoveCountStuckCriterion(Instant instant) { - super(instant, UNIMPROVED_MOVE_COUNT_MULTIPLIER); - } - - @Override - public void phaseStarted(LocalSearchPhaseScope phaseScope) { - super.phaseStarted(phaseScope); - currentBestScore = phaseScope.getBestScore(); + protected UnimprovedMoveCountStuckCriterion(Clock clock) { + super(clock, UNIMPROVED_MOVE_COUNT_MULTIPLIER); } @Override diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterionTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterionTest.java index c3f39279bc..76a61b033c 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterionTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterionTest.java @@ -5,6 +5,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.time.Clock; import java.time.Instant; import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; @@ -20,11 +21,13 @@ class UnimprovedMoveCountStuckCriterionTest { @Test void isSolverStuck() { + var clock = mock(Clock.class); var instant = mock(Instant.class); var solverScope = mock(SolverScope.class); var phaseScope = mock(LocalSearchPhaseScope.class); var stepScope = mock(LocalSearchStepScope.class); var moveScope = mock(LocalSearchMoveScope.class); + when(clock.instant()).thenReturn(instant); when(moveScope.getStepScope()).thenReturn(stepScope); when(stepScope.getPhaseScope()).thenReturn(phaseScope); when(phaseScope.getSolverScope()).thenReturn(solverScope); @@ -33,7 +36,7 @@ void isSolverStuck() { when(phaseScope.getBestScore()).thenReturn(SimpleScore.of(1000)); // Finish grace period - var strategy = new UnimprovedMoveCountStuckCriterion<>(instant); + var strategy = new UnimprovedMoveCountStuckCriterion<>(clock); strategy.solvingStarted(null); strategy.phaseStarted(phaseScope); assertThat(strategy.isSolverStuck(moveScope)).isFalse(); @@ -51,7 +54,7 @@ void isSolverStuck() { // Second restart Mockito.reset(instant); - var secondCount = 2L * UNIMPROVED_MOVE_COUNT_MULTIPLIER + firstCount; + var secondCount = 2L * UNIMPROVED_MOVE_COUNT_MULTIPLIER + firstCount + 1; when(solverScope.getMoveEvaluationCount()).thenReturn(secondCount); assertThat(strategy.isSolverStuck(moveScope)).isTrue(); assertThat(strategy.lastCheckpoint).isEqualTo(secondCount); @@ -59,7 +62,7 @@ void isSolverStuck() { assertThat(strategy.isSolverStuck(moveScope)).isFalse(); // Third restart - var thirdCount = 3L * UNIMPROVED_MOVE_COUNT_MULTIPLIER + secondCount; + var thirdCount = 3L * UNIMPROVED_MOVE_COUNT_MULTIPLIER + secondCount + 1; Mockito.reset(instant); when(solverScope.getMoveEvaluationCount()).thenReturn(thirdCount); assertThat(strategy.isSolverStuck(moveScope)).isTrue(); @@ -69,11 +72,13 @@ void isSolverStuck() { @Test void updateBestSolution() { + var clock = mock(Clock.class); var instant = mock(Instant.class); var solverScope = mock(SolverScope.class); var phaseScope = mock(LocalSearchPhaseScope.class); var stepScope = mock(LocalSearchStepScope.class); var moveScope = mock(LocalSearchMoveScope.class); + when(clock.instant()).thenReturn(instant); when(phaseScope.getSolverScope()).thenReturn(solverScope); when(stepScope.getPhaseScope()).thenReturn(phaseScope); when(moveScope.getStepScope()).thenReturn(stepScope); @@ -83,7 +88,7 @@ void updateBestSolution() { UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1); when(solverScope.getMoveEvaluationCount()).thenReturn(1000L, 1001L); - var strategy = new UnimprovedMoveCountStuckCriterion<>(instant); + var strategy = new UnimprovedMoveCountStuckCriterion<>(clock); strategy.solvingStarted(mock(SolverScope.class)); strategy.phaseStarted(phaseScope); strategy.stepStarted(stepScope); @@ -96,18 +101,20 @@ void updateBestSolution() { @Test void reset() { + var clock = mock(Clock.class); var instant = mock(Instant.class); var solverScope = mock(SolverScope.class); var phaseScope = mock(LocalSearchPhaseScope.class); var stepScope = mock(LocalSearchStepScope.class); var moveScope = mock(LocalSearchMoveScope.class); + when(clock.instant()).thenReturn(instant); when(moveScope.getStepScope()).thenReturn(stepScope); when(stepScope.getPhaseScope()).thenReturn(phaseScope); when(phaseScope.getSolverScope()).thenReturn(solverScope); when(instant.toEpochMilli()).thenReturn(1000L, UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1000L); when(solverScope.getMoveEvaluationCount()).thenReturn(1000L); - var strategy = new UnimprovedMoveCountStuckCriterion<>(instant); + var strategy = new UnimprovedMoveCountStuckCriterion<>(clock); strategy.solvingStarted(mock(SolverScope.class)); strategy.phaseStarted(mock(LocalSearchPhaseScope.class)); // Trigger From 99f72c22c59febb3db2848fdf0e43b96caf23253 Mon Sep 17 00:00:00 2001 From: Fred Date: Wed, 29 Jan 2025 14:07:34 -0300 Subject: [PATCH 21/31] chore: address comments --- .../decider/LocalSearchDecider.java | 4 +- .../decider/acceptor/RestartableAcceptor.java | 24 +- .../DiversifiedLateAcceptanceAcceptor.java | 2 +- .../LateAcceptanceAcceptor.java | 2 +- .../AbstractGeometricStuckCriterion.java | 13 +- .../acceptor/restart/StuckCriterion.java | 9 +- .../reconfiguration/AdaptedSolverScope.java | 317 ------------------ .../reconfiguration/RestartStrategy.java | 3 +- .../RestoreBestSolutionRestartStrategy.java | 8 +- .../scope/LocalSearchPhaseScope.java | 10 + .../impl/phase/scope/AbstractPhaseScope.java | 8 +- ...UnimprovedMoveCountStuckCriterionTest.java | 15 +- 12 files changed, 52 insertions(+), 363 deletions(-) delete mode 100644 core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/AdaptedSolverScope.java diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java index 9eb2fad115..98b6c5f478 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java @@ -6,7 +6,6 @@ import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.Acceptor; import ai.timefold.solver.core.impl.localsearch.decider.forager.LocalSearchForager; -import ai.timefold.solver.core.impl.localsearch.decider.reconfiguration.AdaptedSolverScope; import ai.timefold.solver.core.impl.localsearch.decider.reconfiguration.RestartStrategy; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; @@ -78,13 +77,14 @@ public void setAssertExpectedUndoMoveScore(boolean assertExpectedUndoMoveScore) // ************************************************************************ public void solvingStarted(SolverScope solverScope) { - restartStrategy.solvingStarted(new AdaptedSolverScope<>(solverScope, this)); + restartStrategy.solvingStarted(solverScope); moveSelector.solvingStarted(solverScope); acceptor.solvingStarted(solverScope); forager.solvingStarted(solverScope); } public void phaseStarted(LocalSearchPhaseScope phaseScope) { + phaseScope.setDecider(this); restartStrategy.phaseStarted(phaseScope); moveSelector.phaseStarted(phaseScope); acceptor.phaseStarted(phaseScope); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/RestartableAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/RestartableAcceptor.java index ed06c3b128..11fe342b7d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/RestartableAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/RestartableAcceptor.java @@ -12,52 +12,52 @@ */ public abstract class RestartableAcceptor extends AbstractAcceptor { - private final StuckCriterion stuckCriterionDetection; + private final StuckCriterion stuckCriterion; protected boolean restartTriggered; - protected RestartableAcceptor(StuckCriterion stuckCriterionDetection) { - this.stuckCriterionDetection = stuckCriterionDetection; + protected RestartableAcceptor(StuckCriterion stuckCriterion) { + this.stuckCriterion = stuckCriterion; } @Override public void solvingStarted(SolverScope solverScope) { super.solvingStarted(solverScope); - stuckCriterionDetection.solvingStarted(solverScope); + stuckCriterion.solvingStarted(solverScope); } @Override public void phaseStarted(LocalSearchPhaseScope phaseScope) { super.phaseStarted(phaseScope); - stuckCriterionDetection.phaseStarted(phaseScope); + stuckCriterion.phaseStarted(phaseScope); } @Override public void phaseEnded(LocalSearchPhaseScope phaseScope) { super.phaseEnded(phaseScope); - stuckCriterionDetection.phaseEnded(phaseScope); + stuckCriterion.phaseEnded(phaseScope); } @Override public void stepStarted(LocalSearchStepScope stepScope) { super.stepStarted(stepScope); - stuckCriterionDetection.stepStarted(stepScope); + stuckCriterion.stepStarted(stepScope); } @Override public void stepEnded(LocalSearchStepScope stepScope) { super.stepEnded(stepScope); - stuckCriterionDetection.stepEnded(stepScope); + stuckCriterion.stepEnded(stepScope); } @Override public boolean isAccepted(LocalSearchMoveScope moveScope) { - if (stuckCriterionDetection.isSolverStuck(moveScope)) { - moveScope.getStepScope().getPhaseScope().triggerSolverStuck(); + if (stuckCriterion.isSolverStuck(moveScope)) { + moveScope.getStepScope().getPhaseScope().setSolverStuck(true); restartTriggered = true; return true; } - return applyAcceptanceCriteria(moveScope); + return accept(moveScope); } - protected abstract boolean applyAcceptanceCriteria(LocalSearchMoveScope moveScope); + protected abstract boolean accept(LocalSearchMoveScope moveScope); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java index fb9553db21..4ceb91f5db 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java @@ -53,7 +53,7 @@ private void validate() { @Override @SuppressWarnings({ "rawtypes", "unchecked" }) - protected boolean applyAcceptanceCriteria(LocalSearchMoveScope moveScope) { + protected boolean accept(LocalSearchMoveScope moveScope) { // The acceptance and replacement strategies are based on the work: // Diversified Late Acceptance Search by M. Namazi, C. Sanderson, M. A. H. Newton, M. M. A. Polash, and A. Sattar var moveScore = moveScope.getScore(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java index da30741d09..457c4b4b64 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java @@ -52,7 +52,7 @@ private void validate() { @Override @SuppressWarnings("unchecked") - public boolean applyAcceptanceCriteria(LocalSearchMoveScope moveScope) { + public boolean accept(LocalSearchMoveScope moveScope) { var moveScore = moveScope.getScore(); var lateScore = previousScores[lateScoreIndex]; if (moveScore.compareTo(lateScore) >= 0) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java index a9adef9079..dd29e2e2e5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java @@ -1,6 +1,7 @@ package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; import java.time.Clock; +import java.time.Instant; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; @@ -21,12 +22,12 @@ public abstract class AbstractGeometricStuckCriterion implements StuckCriterion { protected static final Logger logger = LoggerFactory.getLogger(AbstractGeometricStuckCriterion.class); private static final double GEOMETRIC_FACTOR = 1.4; // Value extracted from the cited paper - private static final long GRACE_PERIOD_MILLIS = 30_000; // Same value as DiminishedReturnsTermination + private static final long GRACE_PERIOD_MILLIS = 30_000; // 30s by default private final double scalingFactor; private final Clock clock; private boolean gracePeriodFinished; - private long gracePeriodMillis; + private Instant gracePeriodEnd; protected long nextRestart; private double currentGeometricGrowFactor; @@ -37,9 +38,9 @@ protected AbstractGeometricStuckCriterion(Clock clock, double scalingFactor) { @Override public void phaseStarted(LocalSearchPhaseScope phaseScope) { - if (gracePeriodMillis == 0) { + if (gracePeriodEnd == null) { // 30 seconds of grace period - gracePeriodMillis = clock.instant().toEpochMilli() + GRACE_PERIOD_MILLIS; + gracePeriodEnd = clock.instant().plusMillis(GRACE_PERIOD_MILLIS); } } @@ -51,7 +52,7 @@ public void phaseEnded(LocalSearchPhaseScope phaseScope) { @Override public void solvingStarted(SolverScope solverScope) { currentGeometricGrowFactor = 1; - gracePeriodMillis = 0; + gracePeriodEnd = null; gracePeriodFinished = false; nextRestart = calculateNextRestart(); } @@ -83,7 +84,7 @@ protected boolean isGracePeriodFinished() { if (gracePeriodFinished) { return true; } - gracePeriodFinished = clock.instant().toEpochMilli() >= gracePeriodMillis; + gracePeriodFinished = clock.millis() >= gracePeriodEnd.toEpochMilli(); return gracePeriodFinished; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/StuckCriterion.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/StuckCriterion.java index 9f48b927d7..50ed41342a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/StuckCriterion.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/StuckCriterion.java @@ -1,19 +1,18 @@ package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; +import ai.timefold.solver.core.api.solver.Solver; import ai.timefold.solver.core.impl.localsearch.event.LocalSearchPhaseLifecycleListener; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; /** - * Base contract - * for defining strategies that identify when the {@link ai.timefold.solver.core.api.solver.Solver solver} is stuck. + * Allow defining strategies that identify when the {@link Solver solver} is stuck. * - * @param + * @param the solution type */ public interface StuckCriterion extends LocalSearchPhaseLifecycleListener { /** - * Main logic that applies a specific metric to determine if a solver is stuck and cannot find better solutions - * in the current solving flow. + * Main logic that applies a specific metric to determine if a solver is stuck in a local optimum. * * @param moveScope cannot be null * @return diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/AdaptedSolverScope.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/AdaptedSolverScope.java deleted file mode 100644 index b3d195a808..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/AdaptedSolverScope.java +++ /dev/null @@ -1,317 +0,0 @@ -package ai.timefold.solver.core.impl.localsearch.decider.reconfiguration; - -import java.util.EnumSet; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.Set; -import java.util.concurrent.Semaphore; -import java.util.concurrent.atomic.AtomicReference; - -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.api.solver.ProblemSizeStatistics; -import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; -import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; -import ai.timefold.solver.core.impl.localsearch.decider.LocalSearchDecider; -import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; -import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; -import ai.timefold.solver.core.impl.solver.AbstractSolver; -import ai.timefold.solver.core.impl.solver.change.DefaultProblemChangeDirector; -import ai.timefold.solver.core.impl.solver.scope.SolverScope; -import ai.timefold.solver.core.impl.solver.thread.ChildThreadType; - -import io.micrometer.core.instrument.Tags; - -public class AdaptedSolverScope extends SolverScope { - private final SolverScope solverScope; - private final LocalSearchDecider decider; - - public AdaptedSolverScope(SolverScope solverScope, LocalSearchDecider decider) { - this.solverScope = solverScope; - this.decider = decider; - } - - protected LocalSearchDecider getDecider() { - return decider; - } - - @Override - public AbstractSolver getSolver() { - return solverScope.getSolver(); - } - - @Override - public void setSolver(AbstractSolver solver) { - solverScope.setSolver(solver); - } - - @Override - public DefaultProblemChangeDirector getProblemChangeDirector() { - return solverScope.getProblemChangeDirector(); - } - - @Override - public void setProblemChangeDirector(DefaultProblemChangeDirector problemChangeDirector) { - solverScope.setProblemChangeDirector(problemChangeDirector); - } - - @Override - public Tags getMonitoringTags() { - return solverScope.getMonitoringTags(); - } - - @Override - public void setMonitoringTags(Tags monitoringTags) { - solverScope.setMonitoringTags(monitoringTags); - } - - @Override - public Map>> getStepScoreMap() { - return solverScope.getStepScoreMap(); - } - - @Override - public Set getSolverMetricSet() { - return solverScope.getSolverMetricSet(); - } - - @Override - public void setSolverMetricSet(EnumSet solverMetricSet) { - solverScope.setSolverMetricSet(solverMetricSet); - } - - @Override - public int getStartingSolverCount() { - return solverScope.getStartingSolverCount(); - } - - @Override - public void setStartingSolverCount(int startingSolverCount) { - solverScope.setStartingSolverCount(startingSolverCount); - } - - @Override - public Random getWorkingRandom() { - return solverScope.getWorkingRandom(); - } - - @Override - public void setWorkingRandom(Random workingRandom) { - solverScope.setWorkingRandom(workingRandom); - } - - @Override - public > InnerScoreDirector getScoreDirector() { - return solverScope.getScoreDirector(); - } - - @Override - public void setScoreDirector(InnerScoreDirector scoreDirector) { - solverScope.setScoreDirector(scoreDirector); - } - - @Override - public void setRunnableThreadSemaphore(Semaphore runnableThreadSemaphore) { - solverScope.setRunnableThreadSemaphore(runnableThreadSemaphore); - } - - @Override - public Long getStartingSystemTimeMillis() { - return solverScope.getStartingSystemTimeMillis(); - } - - @Override - public Long getEndingSystemTimeMillis() { - return solverScope.getEndingSystemTimeMillis(); - } - - @Override - public SolutionDescriptor getSolutionDescriptor() { - return solverScope.getSolutionDescriptor(); - } - - @Override - public ScoreDefinition getScoreDefinition() { - return solverScope.getScoreDefinition(); - } - - @Override - public Solution_ getWorkingSolution() { - return solverScope.getWorkingSolution(); - } - - @Override - public int getWorkingEntityCount() { - return solverScope.getWorkingEntityCount(); - } - - @Override - public Score calculateScore() { - return solverScope.calculateScore(); - } - - @Override - public void assertScoreFromScratch(Solution_ solution) { - solverScope.assertScoreFromScratch(solution); - } - - @Override - public Score getStartingInitializedScore() { - return solverScope.getStartingInitializedScore(); - } - - @Override - public void setStartingInitializedScore(Score startingInitializedScore) { - solverScope.setStartingInitializedScore(startingInitializedScore); - } - - @Override - public void addChildThreadsScoreCalculationCount(long addition) { - solverScope.addChildThreadsScoreCalculationCount(addition); - } - - @Override - public long getScoreCalculationCount() { - return solverScope.getScoreCalculationCount(); - } - - @Override - public void addMoveEvaluationCount(long addition) { - solverScope.addMoveEvaluationCount(addition); - } - - @Override - public void addChildThreadsMoveEvaluationCount(long addition) { - solverScope.addChildThreadsMoveEvaluationCount(addition); - } - - @Override - public long getMoveEvaluationCount() { - return solverScope.getMoveEvaluationCount(); - } - - @Override - public Solution_ getBestSolution() { - return solverScope.getBestSolution(); - } - - @Override - public void setBestSolution(Solution_ bestSolution) { - solverScope.setBestSolution(bestSolution); - } - - @Override - public Score getBestScore() { - return solverScope.getBestScore(); - } - - @Override - public void setBestScore(Score bestScore) { - solverScope.setBestScore(bestScore); - } - - @Override - public Long getBestSolutionTimeMillis() { - return solverScope.getBestSolutionTimeMillis(); - } - - @Override - public void setBestSolutionTimeMillis(Long bestSolutionTimeMillis) { - solverScope.setBestSolutionTimeMillis(bestSolutionTimeMillis); - } - - @Override - public Set getMoveCountTypes() { - return solverScope.getMoveCountTypes(); - } - - @Override - public Map getMoveEvaluationCountPerType() { - return solverScope.getMoveEvaluationCountPerType(); - } - - @Override - public boolean isMetricEnabled(SolverMetric solverMetric) { - return solverScope.isMetricEnabled(solverMetric); - } - - @Override - public void startingNow() { - solverScope.startingNow(); - } - - @Override - public Long getBestSolutionTimeMillisSpent() { - return solverScope.getBestSolutionTimeMillisSpent(); - } - - @Override - public void endingNow() { - solverScope.endingNow(); - } - - @Override - public boolean isBestSolutionInitialized() { - return solverScope.isBestSolutionInitialized(); - } - - @Override - public long calculateTimeMillisSpentUpToNow() { - return solverScope.calculateTimeMillisSpentUpToNow(); - } - - @Override - public long getTimeMillisSpent() { - return solverScope.getTimeMillisSpent(); - } - - @Override - public ProblemSizeStatistics getProblemSizeStatistics() { - return solverScope.getProblemSizeStatistics(); - } - - @Override - public void setProblemSizeStatistics(ProblemSizeStatistics problemSizeStatistics) { - solverScope.setProblemSizeStatistics(problemSizeStatistics); - } - - @Override - public long getScoreCalculationSpeed() { - return solverScope.getScoreCalculationSpeed(); - } - - @Override - public long getMoveEvaluationSpeed() { - return solverScope.getMoveEvaluationSpeed(); - } - - @Override - public void setWorkingSolutionFromBestSolution() { - solverScope.setWorkingSolutionFromBestSolution(); - } - - @Override - public SolverScope createChildThreadSolverScope(ChildThreadType childThreadType) { - return solverScope.createChildThreadSolverScope(childThreadType); - } - - @Override - public void initializeYielding() { - solverScope.initializeYielding(); - } - - @Override - public void checkYielding() { - solverScope.checkYielding(); - } - - @Override - public void destroyYielding() { - solverScope.destroyYielding(); - } - - @Override - public void addMoveEvaluationCountPerType(String moveType, long count) { - solverScope.addMoveEvaluationCountPerType(moveType, count); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestartStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestartStrategy.java index 65a4cbf809..eeb48ba18c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestartStrategy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestartStrategy.java @@ -1,5 +1,6 @@ package ai.timefold.solver.core.impl.localsearch.decider.reconfiguration; +import ai.timefold.solver.core.impl.localsearch.decider.LocalSearchDecider; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.StuckCriterion; import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleListener; import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; @@ -7,7 +8,7 @@ /** * Base contract for defining restart strategies. * The restart process is initiated - * when the {@link ai.timefold.solver.core.impl.localsearch.decider.LocalSearchDecider decider} identifies + * when the {@link LocalSearchDecider decider} identifies * that the solver is {@link StuckCriterion stuck} * and requires some logic to alter the current solving flow. * diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionRestartStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionRestartStrategy.java index 573512b927..347a6e1f55 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionRestartStrategy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionRestartStrategy.java @@ -3,6 +3,7 @@ import java.util.Objects; import ai.timefold.solver.core.impl.localsearch.decider.LocalSearchDecider; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; @@ -22,7 +23,7 @@ public void applyRestart(AbstractStepScope stepScope) { logger.trace("Resetting working solution, score ({})", solverScope.getBestScore()); decider.setWorkingSolutionFromBestSolution((LocalSearchStepScope) stepScope); // Mark the solver as unstuck as the best solution is already restored - stepScope.getPhaseScope().resetSolverStuck(); + stepScope.getPhaseScope().setSolverStuck(false); } @Override @@ -37,7 +38,7 @@ public void stepEnded(AbstractStepScope stepScope) { @Override public void phaseStarted(AbstractPhaseScope phaseScope) { - // Do nothing + this.decider = Objects.requireNonNull(((LocalSearchPhaseScope) phaseScope).getDecider()); } @Override @@ -47,8 +48,7 @@ public void phaseEnded(AbstractPhaseScope phaseScope) { @Override public void solvingStarted(SolverScope solverScope) { - var castSolverScope = (AdaptedSolverScope) solverScope; - this.decider = Objects.requireNonNull(castSolverScope.getDecider()); + // Do nothing } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/scope/LocalSearchPhaseScope.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/scope/LocalSearchPhaseScope.java index 6fa927ede5..0aecf364c5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/scope/LocalSearchPhaseScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/scope/LocalSearchPhaseScope.java @@ -1,6 +1,7 @@ package ai.timefold.solver.core.impl.localsearch.scope; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; +import ai.timefold.solver.core.impl.localsearch.decider.LocalSearchDecider; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.solver.scope.SolverScope; @@ -9,6 +10,7 @@ */ public final class LocalSearchPhaseScope extends AbstractPhaseScope { + private LocalSearchDecider decider; private LocalSearchStepScope lastCompletedStepScope; public LocalSearchPhaseScope(SolverScope solverScope, int phaseIndex) { @@ -26,6 +28,14 @@ public void setLastCompletedStepScope(LocalSearchStepScope lastComple this.lastCompletedStepScope = lastCompletedStepScope; } + public LocalSearchDecider getDecider() { + return decider; + } + + public void setDecider(LocalSearchDecider decider) { + this.decider = decider; + } + // ************************************************************************ // Calculated methods // ************************************************************************ diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java index dd33741791..9823b962b2 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java @@ -252,12 +252,8 @@ public boolean isSolverStuck() { return this.solverStuck; } - public void triggerSolverStuck() { - this.solverStuck = true; - } - - public void resetSolverStuck() { - this.solverStuck = false; + public void setSolverStuck(boolean solverStuck) { + this.solverStuck = solverStuck; } @Override diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterionTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterionTest.java index 76a61b033c..2f9c1d567a 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterionTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterionTest.java @@ -2,6 +2,7 @@ import static ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.UnimprovedMoveCountStuckCriterion.UNIMPROVED_MOVE_COUNT_MULTIPLIER; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -15,7 +16,6 @@ import ai.timefold.solver.core.impl.solver.scope.SolverScope; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; class UnimprovedMoveCountStuckCriterionTest { @@ -31,7 +31,8 @@ void isSolverStuck() { when(moveScope.getStepScope()).thenReturn(stepScope); when(stepScope.getPhaseScope()).thenReturn(phaseScope); when(phaseScope.getSolverScope()).thenReturn(solverScope); - when(instant.toEpochMilli()).thenReturn(1000L, UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1000L); + when(instant.plusMillis(anyLong())).thenReturn(Instant.ofEpochMilli(UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1000L)); + when(clock.millis()).thenReturn(UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1000L); when(solverScope.getMoveEvaluationCount()).thenReturn(1000L); when(phaseScope.getBestScore()).thenReturn(SimpleScore.of(1000)); @@ -44,7 +45,6 @@ void isSolverStuck() { assertThat(strategy.nextRestart).isEqualTo(UNIMPROVED_MOVE_COUNT_MULTIPLIER); // First restart - Mockito.reset(instant); var firstCount = UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1000L; when(solverScope.getMoveEvaluationCount()).thenReturn(firstCount); assertThat(strategy.isSolverStuck(moveScope)).isTrue(); @@ -53,7 +53,6 @@ void isSolverStuck() { assertThat(strategy.isSolverStuck(moveScope)).isFalse(); // Second restart - Mockito.reset(instant); var secondCount = 2L * UNIMPROVED_MOVE_COUNT_MULTIPLIER + firstCount + 1; when(solverScope.getMoveEvaluationCount()).thenReturn(secondCount); assertThat(strategy.isSolverStuck(moveScope)).isTrue(); @@ -63,7 +62,6 @@ void isSolverStuck() { // Third restart var thirdCount = 3L * UNIMPROVED_MOVE_COUNT_MULTIPLIER + secondCount + 1; - Mockito.reset(instant); when(solverScope.getMoveEvaluationCount()).thenReturn(thirdCount); assertThat(strategy.isSolverStuck(moveScope)).isTrue(); assertThat(strategy.lastCheckpoint).isEqualTo(thirdCount); @@ -84,8 +82,8 @@ void updateBestSolution() { when(moveScope.getStepScope()).thenReturn(stepScope); when(phaseScope.getBestScore()).thenReturn(SimpleScore.of(1000)); when(stepScope.getScore()).thenReturn(SimpleScore.of(2000)); - when(instant.toEpochMilli()).thenReturn(1000L, UNIMPROVED_MOVE_COUNT_MULTIPLIER, UNIMPROVED_MOVE_COUNT_MULTIPLIER, - UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1); + when(instant.plusMillis(anyLong())).thenReturn(Instant.ofEpochMilli(UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1000L)); + when(clock.millis()).thenReturn(UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1000L, UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1000L); when(solverScope.getMoveEvaluationCount()).thenReturn(1000L, 1001L); var strategy = new UnimprovedMoveCountStuckCriterion<>(clock); @@ -111,7 +109,8 @@ void reset() { when(moveScope.getStepScope()).thenReturn(stepScope); when(stepScope.getPhaseScope()).thenReturn(phaseScope); when(phaseScope.getSolverScope()).thenReturn(solverScope); - when(instant.toEpochMilli()).thenReturn(1000L, UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1000L); + when(instant.plusMillis(anyLong())).thenReturn(Instant.ofEpochMilli(UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1000L)); + when(clock.millis()).thenReturn(UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1000L); when(solverScope.getMoveEvaluationCount()).thenReturn(1000L); var strategy = new UnimprovedMoveCountStuckCriterion<>(clock); From dbec0f52fcb84ec815648ee3dc97951ee724aa30 Mon Sep 17 00:00:00 2001 From: Fred Date: Wed, 29 Jan 2025 18:31:49 -0300 Subject: [PATCH 22/31] feat: relative stuck criterion definition --- .../restart/AbstractGeometricStuckCriterion.java | 12 ++++++++++-- .../restart/UnimprovedMoveCountStuckCriterion.java | 14 +++++++++++--- .../UnimprovedMoveCountStuckCriterionTest.java | 1 + .../reconfiguration/RestartStrategyTest.java | 6 ++++-- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java index dd29e2e2e5..b83e0aec79 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java @@ -24,16 +24,21 @@ public abstract class AbstractGeometricStuckCriterion implements Stuc private static final double GEOMETRIC_FACTOR = 1.4; // Value extracted from the cited paper private static final long GRACE_PERIOD_MILLIS = 30_000; // 30s by default - private final double scalingFactor; private final Clock clock; + private double scalingFactor; private boolean gracePeriodFinished; private Instant gracePeriodEnd; protected long nextRestart; private double currentGeometricGrowFactor; - protected AbstractGeometricStuckCriterion(Clock clock, double scalingFactor) { + protected AbstractGeometricStuckCriterion(Clock clock) { this.clock = clock; + this.scalingFactor = -1; + } + + protected void setScalingFactor(double scalingFactor) { this.scalingFactor = scalingFactor; + this.nextRestart = calculateNextRestart(); } @Override @@ -67,6 +72,9 @@ public boolean isSolverStuck(LocalSearchMoveScope moveScope) { if (isGracePeriodFinished()) { var triggered = evaluateCriterion(moveScope); if (triggered) { + if (scalingFactor == -1) { + throw new IllegalStateException("The scaling factor is not defined for this criterion."); + } logger.trace( "Restart triggered with geometric factor {}, scaling factor of {}, best score ({}), move count ({})", currentGeometricGrowFactor, diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterion.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterion.java index ca8e8918ed..030ddcb578 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterion.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterion.java @@ -15,8 +15,10 @@ */ public class UnimprovedMoveCountStuckCriterion extends AbstractGeometricStuckCriterion { - // Multiplier defined through experiments - protected static final long UNIMPROVED_MOVE_COUNT_MULTIPLIER = 300_000; + // The multiplier will lead to an unimproved move count near a 10-second window without any improvements. + // In this manner, the first restart will occur after approximately 10 seconds without any improvement, + // then after 20 seconds, and so on. + protected static final int UNIMPROVED_MOVE_COUNT_MULTIPLIER = 10; // Last checkpoint of a solution improvement or the restart process protected long lastCheckpoint; private Score currentBestScore; @@ -26,7 +28,7 @@ public UnimprovedMoveCountStuckCriterion() { } protected UnimprovedMoveCountStuckCriterion(Clock clock) { - super(clock, UNIMPROVED_MOVE_COUNT_MULTIPLIER); + super(clock); } @Override @@ -54,6 +56,12 @@ public boolean evaluateCriterion(LocalSearchMoveScope moveScope) { var currentMoveCount = moveScope.getStepScope().getPhaseScope().getSolverScope().getMoveEvaluationCount(); if (lastCheckpoint == 0) { lastCheckpoint = currentMoveCount; + // Grace period is finished + // Now we use the current move evaluation speed to define the scaling factor + var scalingFactor = (double) (moveScope.getStepScope().getPhaseScope().getSolverScope().getMoveEvaluationSpeed() + * UNIMPROVED_MOVE_COUNT_MULTIPLIER); + logger.trace("Scaling factor set to {}.", scalingFactor); + setScalingFactor(scalingFactor); return false; } if (currentMoveCount - lastCheckpoint >= nextRestart) { diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterionTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterionTest.java index 2f9c1d567a..fee9068bf1 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterionTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterionTest.java @@ -34,6 +34,7 @@ void isSolverStuck() { when(instant.plusMillis(anyLong())).thenReturn(Instant.ofEpochMilli(UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1000L)); when(clock.millis()).thenReturn(UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1000L); when(solverScope.getMoveEvaluationCount()).thenReturn(1000L); + when(solverScope.getMoveEvaluationSpeed()).thenReturn(1L); when(phaseScope.getBestScore()).thenReturn(SimpleScore.of(1000)); // Finish grace period diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestartStrategyTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestartStrategyTest.java index 282eee6b57..3588e01210 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestartStrategyTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestartStrategyTest.java @@ -16,7 +16,7 @@ class RestartStrategyTest { void restoreBestSolution() { // Requires the decider var badStrategy = new RestoreBestSolutionRestartStrategy<>(); - assertThatThrownBy(() -> badStrategy.solvingStarted(null)).isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> badStrategy.phaseStarted(null)).isInstanceOf(NullPointerException.class); // Restore the best solution var strategy = new RestoreBestSolutionRestartStrategy<>(); @@ -25,9 +25,11 @@ void restoreBestSolution() { var solverScope = mock(SolverScope.class); var phaseScope = mock(LocalSearchPhaseScope.class); when(phaseScope.getSolverScope()).thenReturn(solverScope); + when(phaseScope.getDecider()).thenReturn(decider); var stepScope = mock(LocalSearchStepScope.class); when(stepScope.getPhaseScope()).thenReturn(phaseScope); - strategy.solvingStarted(new AdaptedSolverScope<>(solverScope, decider)); + strategy.solvingStarted(solverScope); + strategy.phaseStarted(phaseScope); strategy.applyRestart(stepScope); // Restore the best solution verify(decider, times(1)).setWorkingSolutionFromBestSolution(any()); From 4908ff54b4b3e2f5071d04daea3bb38f3c5211f7 Mon Sep 17 00:00:00 2001 From: Fred Date: Thu, 30 Jan 2025 17:53:00 -0300 Subject: [PATCH 23/31] chore: ensure initialization --- .../AbstractGeometricStuckCriterion.java | 8 +++--- .../UnimprovedMoveCountStuckCriterion.java | 14 +++++----- ...UnimprovedMoveCountStuckCriterionTest.java | 26 +++++++++---------- 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java index b83e0aec79..c6cf43f6df 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java @@ -70,17 +70,18 @@ public void solvingEnded(SolverScope solverScope) { @Override public boolean isSolverStuck(LocalSearchMoveScope moveScope) { if (isGracePeriodFinished()) { + if (scalingFactor == -1) { + scalingFactor = calculateScalingFactor(moveScope); + } var triggered = evaluateCriterion(moveScope); if (triggered) { - if (scalingFactor == -1) { - throw new IllegalStateException("The scaling factor is not defined for this criterion."); - } logger.trace( "Restart triggered with geometric factor {}, scaling factor of {}, best score ({}), move count ({})", currentGeometricGrowFactor, scalingFactor, moveScope.getStepScope().getPhaseScope().getBestScore(), moveScope.getStepScope().getPhaseScope().getSolverScope().getMoveEvaluationCount()); currentGeometricGrowFactor = Math.ceil(currentGeometricGrowFactor * GEOMETRIC_FACTOR); + scalingFactor = calculateScalingFactor(moveScope); nextRestart = calculateNextRestart(); return true; } @@ -102,4 +103,5 @@ private long calculateNextRestart() { abstract boolean evaluateCriterion(LocalSearchMoveScope moveScope); + abstract double calculateScalingFactor(LocalSearchMoveScope moveScope); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterion.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterion.java index 030ddcb578..0aca69ea21 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterion.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterion.java @@ -18,7 +18,7 @@ public class UnimprovedMoveCountStuckCriterion extends AbstractGeomet // The multiplier will lead to an unimproved move count near a 10-second window without any improvements. // In this manner, the first restart will occur after approximately 10 seconds without any improvement, // then after 20 seconds, and so on. - protected static final int UNIMPROVED_MOVE_COUNT_MULTIPLIER = 10; + protected static final double UNIMPROVED_MOVE_COUNT_MULTIPLIER = 10.0; // Last checkpoint of a solution improvement or the restart process protected long lastCheckpoint; private Score currentBestScore; @@ -56,12 +56,6 @@ public boolean evaluateCriterion(LocalSearchMoveScope moveScope) { var currentMoveCount = moveScope.getStepScope().getPhaseScope().getSolverScope().getMoveEvaluationCount(); if (lastCheckpoint == 0) { lastCheckpoint = currentMoveCount; - // Grace period is finished - // Now we use the current move evaluation speed to define the scaling factor - var scalingFactor = (double) (moveScope.getStepScope().getPhaseScope().getSolverScope().getMoveEvaluationSpeed() - * UNIMPROVED_MOVE_COUNT_MULTIPLIER); - logger.trace("Scaling factor set to {}.", scalingFactor); - setScalingFactor(scalingFactor); return false; } if (currentMoveCount - lastCheckpoint >= nextRestart) { @@ -70,4 +64,10 @@ public boolean evaluateCriterion(LocalSearchMoveScope moveScope) { } return false; } + + @Override + double calculateScalingFactor(LocalSearchMoveScope moveScope) { + return moveScope.getStepScope().getPhaseScope().getSolverScope().getMoveEvaluationSpeed() + * UNIMPROVED_MOVE_COUNT_MULTIPLIER; + } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterionTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterionTest.java index fee9068bf1..95f4d63ccd 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterionTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterionTest.java @@ -31,8 +31,8 @@ void isSolverStuck() { when(moveScope.getStepScope()).thenReturn(stepScope); when(stepScope.getPhaseScope()).thenReturn(phaseScope); when(phaseScope.getSolverScope()).thenReturn(solverScope); - when(instant.plusMillis(anyLong())).thenReturn(Instant.ofEpochMilli(UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1000L)); - when(clock.millis()).thenReturn(UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1000L); + when(instant.plusMillis(anyLong())).thenReturn(Instant.ofEpochMilli(((long) UNIMPROVED_MOVE_COUNT_MULTIPLIER) + 1000L)); + when(clock.millis()).thenReturn(((long) UNIMPROVED_MOVE_COUNT_MULTIPLIER) + 1000L); when(solverScope.getMoveEvaluationCount()).thenReturn(1000L); when(solverScope.getMoveEvaluationSpeed()).thenReturn(1L); when(phaseScope.getBestScore()).thenReturn(SimpleScore.of(1000)); @@ -43,30 +43,29 @@ void isSolverStuck() { strategy.phaseStarted(phaseScope); assertThat(strategy.isSolverStuck(moveScope)).isFalse(); assertThat(strategy.lastCheckpoint).isEqualTo(1000L); - assertThat(strategy.nextRestart).isEqualTo(UNIMPROVED_MOVE_COUNT_MULTIPLIER); // First restart - var firstCount = UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1000L; + var firstCount = ((long) UNIMPROVED_MOVE_COUNT_MULTIPLIER) + 1000L; when(solverScope.getMoveEvaluationCount()).thenReturn(firstCount); assertThat(strategy.isSolverStuck(moveScope)).isTrue(); assertThat(strategy.lastCheckpoint).isEqualTo(firstCount); - assertThat(strategy.nextRestart).isEqualTo(2L * UNIMPROVED_MOVE_COUNT_MULTIPLIER); + assertThat(strategy.nextRestart).isEqualTo(2L * ((long) UNIMPROVED_MOVE_COUNT_MULTIPLIER)); assertThat(strategy.isSolverStuck(moveScope)).isFalse(); // Second restart - var secondCount = 2L * UNIMPROVED_MOVE_COUNT_MULTIPLIER + firstCount + 1; + var secondCount = 2L * ((long) UNIMPROVED_MOVE_COUNT_MULTIPLIER) + firstCount + 1; when(solverScope.getMoveEvaluationCount()).thenReturn(secondCount); assertThat(strategy.isSolverStuck(moveScope)).isTrue(); assertThat(strategy.lastCheckpoint).isEqualTo(secondCount); - assertThat(strategy.nextRestart).isEqualTo(3L * UNIMPROVED_MOVE_COUNT_MULTIPLIER); + assertThat(strategy.nextRestart).isEqualTo(3L * ((long) UNIMPROVED_MOVE_COUNT_MULTIPLIER)); assertThat(strategy.isSolverStuck(moveScope)).isFalse(); // Third restart - var thirdCount = 3L * UNIMPROVED_MOVE_COUNT_MULTIPLIER + secondCount + 1; + var thirdCount = 3L * ((long) UNIMPROVED_MOVE_COUNT_MULTIPLIER) + secondCount + 1; when(solverScope.getMoveEvaluationCount()).thenReturn(thirdCount); assertThat(strategy.isSolverStuck(moveScope)).isTrue(); assertThat(strategy.lastCheckpoint).isEqualTo(thirdCount); - assertThat(strategy.nextRestart).isEqualTo(5L * UNIMPROVED_MOVE_COUNT_MULTIPLIER); + assertThat(strategy.nextRestart).isEqualTo(5L * ((long) UNIMPROVED_MOVE_COUNT_MULTIPLIER)); } @Test @@ -83,8 +82,9 @@ void updateBestSolution() { when(moveScope.getStepScope()).thenReturn(stepScope); when(phaseScope.getBestScore()).thenReturn(SimpleScore.of(1000)); when(stepScope.getScore()).thenReturn(SimpleScore.of(2000)); - when(instant.plusMillis(anyLong())).thenReturn(Instant.ofEpochMilli(UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1000L)); - when(clock.millis()).thenReturn(UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1000L, UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1000L); + when(instant.plusMillis(anyLong())).thenReturn(Instant.ofEpochMilli(((long) UNIMPROVED_MOVE_COUNT_MULTIPLIER) + 1000L)); + when(clock.millis()).thenReturn(((long) UNIMPROVED_MOVE_COUNT_MULTIPLIER) + 1000L, + ((long) UNIMPROVED_MOVE_COUNT_MULTIPLIER) + 1000L); when(solverScope.getMoveEvaluationCount()).thenReturn(1000L, 1001L); var strategy = new UnimprovedMoveCountStuckCriterion<>(clock); @@ -110,8 +110,8 @@ void reset() { when(moveScope.getStepScope()).thenReturn(stepScope); when(stepScope.getPhaseScope()).thenReturn(phaseScope); when(phaseScope.getSolverScope()).thenReturn(solverScope); - when(instant.plusMillis(anyLong())).thenReturn(Instant.ofEpochMilli(UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1000L)); - when(clock.millis()).thenReturn(UNIMPROVED_MOVE_COUNT_MULTIPLIER + 1000L); + when(instant.plusMillis(anyLong())).thenReturn(Instant.ofEpochMilli(((long) UNIMPROVED_MOVE_COUNT_MULTIPLIER) + 1000L)); + when(clock.millis()).thenReturn(((long) UNIMPROVED_MOVE_COUNT_MULTIPLIER) + 1000L); when(solverScope.getMoveEvaluationCount()).thenReturn(1000L); var strategy = new UnimprovedMoveCountStuckCriterion<>(clock); From bf87fbebef0532d7d1e78e3429884b11b47f772b Mon Sep 17 00:00:00 2001 From: Fred Date: Fri, 31 Jan 2025 08:42:50 -0300 Subject: [PATCH 24/31] chore: address comments --- .../acceptor/restart/AbstractGeometricStuckCriterion.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java index c6cf43f6df..e26bb9d40f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java @@ -12,7 +12,7 @@ /** * Restart strategy, which exponentially increases the metric that triggers the restart process. - * The first restart occurs after the {@code grace period + 1 * scalingFactor} metric. + * The first restart occurs after the {@code (gracePeriod + scalingFactor) * GEOMETRIC_FACTOR^restartCount} metric. * Following that, the metric increases exponentially: 1, 2, 3, 5, 7, 10, 14... *

* The strategy is based on the work: Search in a Small World by Toby Walsh From 47d76a75661647de8fb7d5edc55b9e26ac609cd2 Mon Sep 17 00:00:00 2001 From: Fred Date: Wed, 5 Feb 2025 09:38:27 -0300 Subject: [PATCH 25/31] feat: new stuck criterion --- .../decider/acceptor/AcceptorFactory.java | 6 +- .../AbstractGeometricStuckCriterion.java | 60 ++------- .../DiminishedReturnsStuckCriterion.java | 78 +++++++++++ .../UnimprovedMoveCountStuckCriterion.java | 73 ----------- .../DiminishedReturnsStuckCriterionTest.java | 50 +++++++ ...UnimprovedMoveCountStuckCriterionTest.java | 124 ------------------ 6 files changed, 144 insertions(+), 247 deletions(-) create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterion.java delete mode 100644 core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterion.java create mode 100644 core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterionTest.java delete mode 100644 core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterionTest.java diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java index de36437325..580441f954 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java @@ -15,8 +15,8 @@ import ai.timefold.solver.core.impl.localsearch.decider.acceptor.hillclimbing.HillClimbingAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance.DiversifiedLateAcceptanceAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance.LateAcceptanceAcceptor; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.DiminishedReturnsStuckCriterion; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.StuckCriterion; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.UnimprovedMoveCountStuckCriterion; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.simulatedannealing.SimulatedAnnealingAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stepcountinghillclimbing.StepCountingHillClimbingAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.tabu.EntityTabuAcceptor; @@ -222,7 +222,7 @@ private Optional> buildMoveTabuAcceptor(HeuristicCon if (acceptorTypeListsContainsAcceptorType(AcceptorType.LATE_ACCEPTANCE) || (!acceptorTypeListsContainsAcceptorType(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE) && acceptorConfig.getLateAcceptanceSize() != null)) { - StuckCriterion strategy = new UnimprovedMoveCountStuckCriterion<>(); + StuckCriterion strategy = new DiminishedReturnsStuckCriterion<>(); var acceptor = new LateAcceptanceAcceptor<>(strategy); acceptor.setLateAcceptanceSize(Objects.requireNonNullElse(acceptorConfig.getLateAcceptanceSize(), 400)); return Optional.of(acceptor); @@ -234,7 +234,7 @@ private Optional> buildMoveTabuAcceptor(HeuristicCon buildDiversifiedLateAcceptanceAcceptor(HeuristicConfigPolicy configPolicy) { if (acceptorTypeListsContainsAcceptorType(AcceptorType.DIVERSIFIED_LATE_ACCEPTANCE)) { configPolicy.ensurePreviewFeature(PreviewFeature.DIVERSIFIED_LATE_ACCEPTANCE); - StuckCriterion strategy = new UnimprovedMoveCountStuckCriterion<>(); + StuckCriterion strategy = new DiminishedReturnsStuckCriterion<>(); var acceptor = new DiversifiedLateAcceptanceAcceptor<>(strategy); acceptor.setLateAcceptanceSize(Objects.requireNonNullElse(acceptorConfig.getLateAcceptanceSize(), 5)); return Optional.of(acceptor); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java index e26bb9d40f..3e36412a0c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java @@ -1,8 +1,5 @@ package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; -import java.time.Clock; -import java.time.Instant; - import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.solver.scope.SolverScope; @@ -12,7 +9,7 @@ /** * Restart strategy, which exponentially increases the metric that triggers the restart process. - * The first restart occurs after the {@code (gracePeriod + scalingFactor) * GEOMETRIC_FACTOR^restartCount} metric. + * The first restart occurs after the {@code scalingFactor * GEOMETRIC_FACTOR^restartCount} metric. * Following that, the metric increases exponentially: 1, 2, 3, 5, 7, 10, 14... *

* The strategy is based on the work: Search in a Small World by Toby Walsh @@ -22,31 +19,18 @@ public abstract class AbstractGeometricStuckCriterion implements StuckCriterion { protected static final Logger logger = LoggerFactory.getLogger(AbstractGeometricStuckCriterion.class); private static final double GEOMETRIC_FACTOR = 1.4; // Value extracted from the cited paper - private static final long GRACE_PERIOD_MILLIS = 30_000; // 30s by default - private final Clock clock; - private double scalingFactor; - private boolean gracePeriodFinished; - private Instant gracePeriodEnd; + private final double scalingFactor; protected long nextRestart; private double currentGeometricGrowFactor; - protected AbstractGeometricStuckCriterion(Clock clock) { - this.clock = clock; - this.scalingFactor = -1; - } - - protected void setScalingFactor(double scalingFactor) { + protected AbstractGeometricStuckCriterion(double scalingFactor) { this.scalingFactor = scalingFactor; - this.nextRestart = calculateNextRestart(); } @Override public void phaseStarted(LocalSearchPhaseScope phaseScope) { - if (gracePeriodEnd == null) { - // 30 seconds of grace period - gracePeriodEnd = clock.instant().plusMillis(GRACE_PERIOD_MILLIS); - } + // Do nothing } @Override @@ -57,8 +41,6 @@ public void phaseEnded(LocalSearchPhaseScope phaseScope) { @Override public void solvingStarted(SolverScope solverScope) { currentGeometricGrowFactor = 1; - gracePeriodEnd = null; - gracePeriodFinished = false; nextRestart = calculateNextRestart(); } @@ -69,32 +51,17 @@ public void solvingEnded(SolverScope solverScope) { @Override public boolean isSolverStuck(LocalSearchMoveScope moveScope) { - if (isGracePeriodFinished()) { - if (scalingFactor == -1) { - scalingFactor = calculateScalingFactor(moveScope); - } - var triggered = evaluateCriterion(moveScope); - if (triggered) { - logger.trace( - "Restart triggered with geometric factor {}, scaling factor of {}, best score ({}), move count ({})", - currentGeometricGrowFactor, - scalingFactor, moveScope.getStepScope().getPhaseScope().getBestScore(), - moveScope.getStepScope().getPhaseScope().getSolverScope().getMoveEvaluationCount()); - currentGeometricGrowFactor = Math.ceil(currentGeometricGrowFactor * GEOMETRIC_FACTOR); - scalingFactor = calculateScalingFactor(moveScope); - nextRestart = calculateNextRestart(); - return true; - } - } - return false; - } - - protected boolean isGracePeriodFinished() { - if (gracePeriodFinished) { + var triggered = evaluateCriterion(moveScope); + if (triggered) { + logger.trace( + "Restart triggered with geometric factor {}, scaling factor of {}, best score ({})", + currentGeometricGrowFactor, + scalingFactor, moveScope.getStepScope().getPhaseScope().getBestScore()); + currentGeometricGrowFactor = Math.ceil(currentGeometricGrowFactor * GEOMETRIC_FACTOR); + nextRestart = calculateNextRestart(); return true; } - gracePeriodFinished = clock.millis() >= gracePeriodEnd.toEpochMilli(); - return gracePeriodFinished; + return false; } private long calculateNextRestart() { @@ -103,5 +70,4 @@ private long calculateNextRestart() { abstract boolean evaluateCriterion(LocalSearchMoveScope moveScope); - abstract double calculateScalingFactor(LocalSearchMoveScope moveScope); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterion.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterion.java new file mode 100644 index 0000000000..dbbf1a3dfa --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterion.java @@ -0,0 +1,78 @@ +package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; + +import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; +import ai.timefold.solver.core.impl.solver.termination.DiminishedReturnsTermination; + +public class DiminishedReturnsStuckCriterion> + extends AbstractGeometricStuckCriterion { + protected static final long TIME_WINDOW_MILLIS = 25_000; + private static final double MINIMAL_IMPROVEMENT = 0.0001; + + private DiminishedReturnsTermination diminishedReturnsCriterion; + + private boolean triggered; + + public DiminishedReturnsStuckCriterion() { + this(new DiminishedReturnsTermination<>(TIME_WINDOW_MILLIS, MINIMAL_IMPROVEMENT)); + } + + protected DiminishedReturnsStuckCriterion(DiminishedReturnsTermination diminishedReturnsCriterion) { + super(TIME_WINDOW_MILLIS); + this.diminishedReturnsCriterion = diminishedReturnsCriterion; + } + + @Override + @SuppressWarnings("unchecked") + boolean evaluateCriterion(LocalSearchMoveScope moveScope) { + var bestScore = moveScope.getStepScope().getPhaseScope().getBestScore(); + if (moveScope.getScore().compareTo(bestScore) > 0) { + bestScore = moveScope.getScore(); + } + triggered = diminishedReturnsCriterion.isTerminated(System.nanoTime(), (Score_) bestScore); + return triggered; + } + + @Override + public void stepStarted(LocalSearchStepScope stepScope) { + if (triggered) { + // We need to recreate the termination criterion as the time window has changed + diminishedReturnsCriterion = new DiminishedReturnsTermination<>(nextRestart, MINIMAL_IMPROVEMENT); + diminishedReturnsCriterion.start(System.nanoTime(), stepScope.getPhaseScope().getBestScore()); + triggered = false; + } + } + + @Override + public void stepEnded(LocalSearchStepScope stepScope) { + diminishedReturnsCriterion.stepEnded(stepScope); + } + + @Override + public void phaseStarted(LocalSearchPhaseScope phaseScope) { + super.phaseStarted(phaseScope); + diminishedReturnsCriterion.phaseStarted(phaseScope); + triggered = false; + } + + @Override + public void phaseEnded(LocalSearchPhaseScope phaseScope) { + super.phaseEnded(phaseScope); + diminishedReturnsCriterion.phaseEnded(phaseScope); + } + + @Override + public void solvingStarted(SolverScope solverScope) { + super.solvingStarted(solverScope); + diminishedReturnsCriterion.solvingStarted(solverScope); + } + + @Override + public void solvingEnded(SolverScope solverScope) { + super.solvingEnded(solverScope); + diminishedReturnsCriterion.solvingEnded(solverScope); + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterion.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterion.java deleted file mode 100644 index 0aca69ea21..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterion.java +++ /dev/null @@ -1,73 +0,0 @@ -package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; - -import java.time.Clock; - -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.impl.localsearch.decider.reconfiguration.RestartStrategy; -import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; -import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; -import ai.timefold.solver.core.impl.solver.scope.SolverScope; - -/** - * Criterion based on the unimproved move count. - * It exponentially increases the unimproved move count values that trigger the - * {@link RestartStrategy restart} process. - */ -public class UnimprovedMoveCountStuckCriterion extends AbstractGeometricStuckCriterion { - - // The multiplier will lead to an unimproved move count near a 10-second window without any improvements. - // In this manner, the first restart will occur after approximately 10 seconds without any improvement, - // then after 20 seconds, and so on. - protected static final double UNIMPROVED_MOVE_COUNT_MULTIPLIER = 10.0; - // Last checkpoint of a solution improvement or the restart process - protected long lastCheckpoint; - private Score currentBestScore; - - public UnimprovedMoveCountStuckCriterion() { - this(Clock.systemUTC()); - } - - protected UnimprovedMoveCountStuckCriterion(Clock clock) { - super(clock); - } - - @Override - public void stepStarted(LocalSearchStepScope stepScope) { - this.currentBestScore = stepScope.getPhaseScope().getBestScore(); - } - - @Override - @SuppressWarnings({ "rawtypes", "unchecked" }) - public void stepEnded(LocalSearchStepScope stepScope) { - // Do not update it during the grace period - if (isGracePeriodFinished() && ((Score) stepScope.getScore()).compareTo(currentBestScore) > 0) { - lastCheckpoint = stepScope.getPhaseScope().getSolverScope().getMoveEvaluationCount(); - } - } - - @Override - public void solvingStarted(SolverScope solverScope) { - super.solvingStarted(solverScope); - lastCheckpoint = 0; - } - - @Override - public boolean evaluateCriterion(LocalSearchMoveScope moveScope) { - var currentMoveCount = moveScope.getStepScope().getPhaseScope().getSolverScope().getMoveEvaluationCount(); - if (lastCheckpoint == 0) { - lastCheckpoint = currentMoveCount; - return false; - } - if (currentMoveCount - lastCheckpoint >= nextRestart) { - lastCheckpoint = currentMoveCount; - return true; - } - return false; - } - - @Override - double calculateScalingFactor(LocalSearchMoveScope moveScope) { - return moveScope.getStepScope().getPhaseScope().getSolverScope().getMoveEvaluationSpeed() - * UNIMPROVED_MOVE_COUNT_MULTIPLIER; - } -} diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterionTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterionTest.java new file mode 100644 index 0000000000..c1121914bb --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterionTest.java @@ -0,0 +1,50 @@ +package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; + +import static ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.DiminishedReturnsStuckCriterion.TIME_WINDOW_MILLIS; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; +import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; +import ai.timefold.solver.core.impl.solver.termination.DiminishedReturnsTermination; + +import org.junit.jupiter.api.Test; + +class DiminishedReturnsStuckCriterionTest { + + @Test + void isSolverStuck() { + var solverScope = mock(SolverScope.class); + var phaseScope = mock(LocalSearchPhaseScope.class); + var stepScope = mock(LocalSearchStepScope.class); + var moveScope = mock(LocalSearchMoveScope.class); + var termination = mock(DiminishedReturnsTermination.class); + + when(moveScope.getStepScope()).thenReturn(stepScope); + when(stepScope.getPhaseScope()).thenReturn(phaseScope); + when(phaseScope.getSolverScope()).thenReturn(solverScope); + when(moveScope.getScore()).thenReturn(SimpleScore.of(1)); + when(phaseScope.getBestScore()).thenReturn(SimpleScore.of(1)); + when(termination.isTerminated(anyLong(), any())).thenReturn(false, true); + + // No restart + var strategy = new DiminishedReturnsStuckCriterion<>(termination); + strategy.solvingStarted(null); + strategy.phaseStarted(phaseScope); + assertThat(strategy.isSolverStuck(moveScope)).isFalse(); + + // First restart + assertThat(strategy.isSolverStuck(moveScope)).isTrue(); + assertThat(strategy.nextRestart).isEqualTo(2L * TIME_WINDOW_MILLIS); + + // Second restart + assertThat(strategy.isSolverStuck(moveScope)).isTrue(); + assertThat(strategy.nextRestart).isEqualTo(3L * TIME_WINDOW_MILLIS); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterionTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterionTest.java deleted file mode 100644 index 95f4d63ccd..0000000000 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/UnimprovedMoveCountStuckCriterionTest.java +++ /dev/null @@ -1,124 +0,0 @@ -package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; - -import static ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.UnimprovedMoveCountStuckCriterion.UNIMPROVED_MOVE_COUNT_MULTIPLIER; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.time.Clock; -import java.time.Instant; - -import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; -import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; -import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; -import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; -import ai.timefold.solver.core.impl.solver.scope.SolverScope; - -import org.junit.jupiter.api.Test; - -class UnimprovedMoveCountStuckCriterionTest { - - @Test - void isSolverStuck() { - var clock = mock(Clock.class); - var instant = mock(Instant.class); - var solverScope = mock(SolverScope.class); - var phaseScope = mock(LocalSearchPhaseScope.class); - var stepScope = mock(LocalSearchStepScope.class); - var moveScope = mock(LocalSearchMoveScope.class); - when(clock.instant()).thenReturn(instant); - when(moveScope.getStepScope()).thenReturn(stepScope); - when(stepScope.getPhaseScope()).thenReturn(phaseScope); - when(phaseScope.getSolverScope()).thenReturn(solverScope); - when(instant.plusMillis(anyLong())).thenReturn(Instant.ofEpochMilli(((long) UNIMPROVED_MOVE_COUNT_MULTIPLIER) + 1000L)); - when(clock.millis()).thenReturn(((long) UNIMPROVED_MOVE_COUNT_MULTIPLIER) + 1000L); - when(solverScope.getMoveEvaluationCount()).thenReturn(1000L); - when(solverScope.getMoveEvaluationSpeed()).thenReturn(1L); - when(phaseScope.getBestScore()).thenReturn(SimpleScore.of(1000)); - - // Finish grace period - var strategy = new UnimprovedMoveCountStuckCriterion<>(clock); - strategy.solvingStarted(null); - strategy.phaseStarted(phaseScope); - assertThat(strategy.isSolverStuck(moveScope)).isFalse(); - assertThat(strategy.lastCheckpoint).isEqualTo(1000L); - - // First restart - var firstCount = ((long) UNIMPROVED_MOVE_COUNT_MULTIPLIER) + 1000L; - when(solverScope.getMoveEvaluationCount()).thenReturn(firstCount); - assertThat(strategy.isSolverStuck(moveScope)).isTrue(); - assertThat(strategy.lastCheckpoint).isEqualTo(firstCount); - assertThat(strategy.nextRestart).isEqualTo(2L * ((long) UNIMPROVED_MOVE_COUNT_MULTIPLIER)); - assertThat(strategy.isSolverStuck(moveScope)).isFalse(); - - // Second restart - var secondCount = 2L * ((long) UNIMPROVED_MOVE_COUNT_MULTIPLIER) + firstCount + 1; - when(solverScope.getMoveEvaluationCount()).thenReturn(secondCount); - assertThat(strategy.isSolverStuck(moveScope)).isTrue(); - assertThat(strategy.lastCheckpoint).isEqualTo(secondCount); - assertThat(strategy.nextRestart).isEqualTo(3L * ((long) UNIMPROVED_MOVE_COUNT_MULTIPLIER)); - assertThat(strategy.isSolverStuck(moveScope)).isFalse(); - - // Third restart - var thirdCount = 3L * ((long) UNIMPROVED_MOVE_COUNT_MULTIPLIER) + secondCount + 1; - when(solverScope.getMoveEvaluationCount()).thenReturn(thirdCount); - assertThat(strategy.isSolverStuck(moveScope)).isTrue(); - assertThat(strategy.lastCheckpoint).isEqualTo(thirdCount); - assertThat(strategy.nextRestart).isEqualTo(5L * ((long) UNIMPROVED_MOVE_COUNT_MULTIPLIER)); - } - - @Test - void updateBestSolution() { - var clock = mock(Clock.class); - var instant = mock(Instant.class); - var solverScope = mock(SolverScope.class); - var phaseScope = mock(LocalSearchPhaseScope.class); - var stepScope = mock(LocalSearchStepScope.class); - var moveScope = mock(LocalSearchMoveScope.class); - when(clock.instant()).thenReturn(instant); - when(phaseScope.getSolverScope()).thenReturn(solverScope); - when(stepScope.getPhaseScope()).thenReturn(phaseScope); - when(moveScope.getStepScope()).thenReturn(stepScope); - when(phaseScope.getBestScore()).thenReturn(SimpleScore.of(1000)); - when(stepScope.getScore()).thenReturn(SimpleScore.of(2000)); - when(instant.plusMillis(anyLong())).thenReturn(Instant.ofEpochMilli(((long) UNIMPROVED_MOVE_COUNT_MULTIPLIER) + 1000L)); - when(clock.millis()).thenReturn(((long) UNIMPROVED_MOVE_COUNT_MULTIPLIER) + 1000L, - ((long) UNIMPROVED_MOVE_COUNT_MULTIPLIER) + 1000L); - when(solverScope.getMoveEvaluationCount()).thenReturn(1000L, 1001L); - - var strategy = new UnimprovedMoveCountStuckCriterion<>(clock); - strategy.solvingStarted(mock(SolverScope.class)); - strategy.phaseStarted(phaseScope); - strategy.stepStarted(stepScope); - // Trigger - strategy.isSolverStuck(moveScope); - // Update the last improvement - strategy.stepEnded(stepScope); - assertThat(strategy.lastCheckpoint).isEqualTo(1001L); - } - - @Test - void reset() { - var clock = mock(Clock.class); - var instant = mock(Instant.class); - var solverScope = mock(SolverScope.class); - var phaseScope = mock(LocalSearchPhaseScope.class); - var stepScope = mock(LocalSearchStepScope.class); - var moveScope = mock(LocalSearchMoveScope.class); - when(clock.instant()).thenReturn(instant); - when(moveScope.getStepScope()).thenReturn(stepScope); - when(stepScope.getPhaseScope()).thenReturn(phaseScope); - when(phaseScope.getSolverScope()).thenReturn(solverScope); - when(instant.plusMillis(anyLong())).thenReturn(Instant.ofEpochMilli(((long) UNIMPROVED_MOVE_COUNT_MULTIPLIER) + 1000L)); - when(clock.millis()).thenReturn(((long) UNIMPROVED_MOVE_COUNT_MULTIPLIER) + 1000L); - when(solverScope.getMoveEvaluationCount()).thenReturn(1000L); - - var strategy = new UnimprovedMoveCountStuckCriterion<>(clock); - strategy.solvingStarted(mock(SolverScope.class)); - strategy.phaseStarted(mock(LocalSearchPhaseScope.class)); - // Trigger - strategy.isSolverStuck(moveScope); - assertThat(strategy.lastCheckpoint).isEqualTo(1000L); - } -} From 0bcd8d23d6b1c509e2a61de5399c76331d5cb0f9 Mon Sep 17 00:00:00 2001 From: Fred Date: Wed, 5 Feb 2025 09:38:50 -0300 Subject: [PATCH 26/31] chore: temp info log --- .../acceptor/restart/AbstractGeometricStuckCriterion.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java index 3e36412a0c..c824aab41f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java @@ -53,7 +53,7 @@ public void solvingEnded(SolverScope solverScope) { public boolean isSolverStuck(LocalSearchMoveScope moveScope) { var triggered = evaluateCriterion(moveScope); if (triggered) { - logger.trace( + logger.info( "Restart triggered with geometric factor {}, scaling factor of {}, best score ({})", currentGeometricGrowFactor, scalingFactor, moveScope.getStepScope().getPhaseScope().getBestScore()); From 6f568f889c4c0deef970a76127df7f462e1f367c Mon Sep 17 00:00:00 2001 From: fred Date: Thu, 6 Feb 2025 16:15:07 -0300 Subject: [PATCH 27/31] chore: increase window --- .../acceptor/restart/DiminishedReturnsStuckCriterion.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterion.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterion.java index dbbf1a3dfa..e96888d22e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterion.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterion.java @@ -9,7 +9,7 @@ public class DiminishedReturnsStuckCriterion> extends AbstractGeometricStuckCriterion { - protected static final long TIME_WINDOW_MILLIS = 25_000; + protected static final long TIME_WINDOW_MILLIS = 60_000; private static final double MINIMAL_IMPROVEMENT = 0.0001; private DiminishedReturnsTermination diminishedReturnsCriterion; From 7d2d0f3d5167acfc1feceadd2530fec50ae44a9d Mon Sep 17 00:00:00 2001 From: fred Date: Fri, 7 Feb 2025 08:28:17 -0300 Subject: [PATCH 28/31] feat: reset stuck criterion after an improvement --- .../AbstractGeometricStuckCriterion.java | 6 ++-- .../DiminishedReturnsStuckCriterion.java | 11 +++++++ .../DiminishedReturnsStuckCriterionTest.java | 29 +++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java index c824aab41f..e114b2f8ff 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java @@ -54,9 +54,9 @@ public boolean isSolverStuck(LocalSearchMoveScope moveScope) { var triggered = evaluateCriterion(moveScope); if (triggered) { logger.info( - "Restart triggered with geometric factor {}, scaling factor of {}, best score ({})", - currentGeometricGrowFactor, - scalingFactor, moveScope.getStepScope().getPhaseScope().getBestScore()); + "Restart triggered with geometric factor ({}), scaling factor of ({}), nextRestart ({}), best score ({})", + currentGeometricGrowFactor, scalingFactor, nextRestart, + moveScope.getStepScope().getPhaseScope().getBestScore()); currentGeometricGrowFactor = Math.ceil(currentGeometricGrowFactor * GEOMETRIC_FACTOR); nextRestart = calculateNextRestart(); return true; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterion.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterion.java index e96888d22e..827a115f05 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterion.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterion.java @@ -15,6 +15,7 @@ public class DiminishedReturnsStuckCriterion diminishedReturnsCriterion; private boolean triggered; + private Score_ currentBestScore; public DiminishedReturnsStuckCriterion() { this(new DiminishedReturnsTermination<>(TIME_WINDOW_MILLIS, MINIMAL_IMPROVEMENT)); @@ -38,6 +39,7 @@ boolean evaluateCriterion(LocalSearchMoveScope moveScope) { @Override public void stepStarted(LocalSearchStepScope stepScope) { + currentBestScore = stepScope.getPhaseScope().getBestScore(); if (triggered) { // We need to recreate the termination criterion as the time window has changed diminishedReturnsCriterion = new DiminishedReturnsTermination<>(nextRestart, MINIMAL_IMPROVEMENT); @@ -49,6 +51,15 @@ public void stepStarted(LocalSearchStepScope stepScope) { @Override public void stepEnded(LocalSearchStepScope stepScope) { diminishedReturnsCriterion.stepEnded(stepScope); + if (currentBestScore.compareTo(stepScope.getPhaseScope().getBestScore()) < 0 && nextRestart > TIME_WINDOW_MILLIS) { + // If the solution has been improved after a restart, + // we reset the criterion and restart the evaluation of the metric + super.solvingStarted(stepScope.getPhaseScope().getSolverScope()); + diminishedReturnsCriterion = new DiminishedReturnsTermination<>(nextRestart, MINIMAL_IMPROVEMENT); + diminishedReturnsCriterion.start(System.nanoTime(), stepScope.getPhaseScope().getBestScore()); + logger.info("Stuck criterion reset, next restart ({}), previous best score({}), new best score ({})", nextRestart, + currentBestScore, stepScope.getPhaseScope().getBestScore()); + } } @Override diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterionTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterionTest.java index c1121914bb..e4a8237970 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterionTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterionTest.java @@ -47,4 +47,33 @@ void isSolverStuck() { assertThat(strategy.isSolverStuck(moveScope)).isTrue(); assertThat(strategy.nextRestart).isEqualTo(3L * TIME_WINDOW_MILLIS); } + + @Test + void reset() { + var solverScope = mock(SolverScope.class); + var phaseScope = mock(LocalSearchPhaseScope.class); + var stepScope = mock(LocalSearchStepScope.class); + var moveScope = mock(LocalSearchMoveScope.class); + var termination = mock(DiminishedReturnsTermination.class); + + when(moveScope.getStepScope()).thenReturn(stepScope); + when(stepScope.getPhaseScope()).thenReturn(phaseScope); + when(phaseScope.getSolverScope()).thenReturn(solverScope); + when(moveScope.getScore()).thenReturn(SimpleScore.of(1)); + when(phaseScope.getBestScore()).thenReturn(SimpleScore.of(1)); + when(termination.isTerminated(anyLong(), any())).thenReturn(true); + + // Restart + var strategy = new DiminishedReturnsStuckCriterion<>(termination); + strategy.solvingStarted(null); + strategy.phaseStarted(phaseScope); + assertThat(strategy.isSolverStuck(moveScope)).isTrue(); + assertThat(strategy.nextRestart).isEqualTo(2L * TIME_WINDOW_MILLIS); + + // Reset + strategy.stepStarted(stepScope); + when(phaseScope.getBestScore()).thenReturn(SimpleScore.of(2)); + strategy.stepEnded(stepScope); + assertThat(strategy.nextRestart).isEqualTo(TIME_WINDOW_MILLIS); + } } From 139eb04bcc35b80c403636bfbafa76c0b6acc8f1 Mon Sep 17 00:00:00 2001 From: fred Date: Fri, 7 Feb 2025 11:21:44 -0300 Subject: [PATCH 29/31] feat: update initial window size --- .../acceptor/restart/DiminishedReturnsStuckCriterion.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterion.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterion.java index 827a115f05..c842beb999 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterion.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterion.java @@ -9,7 +9,7 @@ public class DiminishedReturnsStuckCriterion> extends AbstractGeometricStuckCriterion { - protected static final long TIME_WINDOW_MILLIS = 60_000; + protected static final long TIME_WINDOW_MILLIS = 25_000; private static final double MINIMAL_IMPROVEMENT = 0.0001; private DiminishedReturnsTermination diminishedReturnsCriterion; From 9497be15a91a58ab78f872b50f86340464eafde6 Mon Sep 17 00:00:00 2001 From: fred Date: Tue, 11 Feb 2025 11:08:12 -0300 Subject: [PATCH 30/31] chore: change the restart approach --- .../acceptor/lateacceptance/LateAcceptanceAcceptor.java | 3 --- .../acceptor/restart/DiminishedReturnsStuckCriterion.java | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java index 457c4b4b64..d6c3832d5c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java @@ -72,9 +72,6 @@ public void stepEnded(LocalSearchStepScope stepScope) { previousScores[lateScoreIndex] = stepScope.getScore(); lateScoreIndex = (lateScoreIndex + 1) % lateAcceptanceSize; } else { - // We only modify the current late element to raise the intensification phase, - // while ensuring that the other elements remain unchanged to maintain diversity. - previousScores[lateScoreIndex] = stepScope.getPhaseScope().getBestScore(); restartTriggered = false; } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterion.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterion.java index c842beb999..827a115f05 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterion.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterion.java @@ -9,7 +9,7 @@ public class DiminishedReturnsStuckCriterion> extends AbstractGeometricStuckCriterion { - protected static final long TIME_WINDOW_MILLIS = 25_000; + protected static final long TIME_WINDOW_MILLIS = 60_000; private static final double MINIMAL_IMPROVEMENT = 0.0001; private DiminishedReturnsTermination diminishedReturnsCriterion; From 9dd7178a14d0a93f500709e069cb9d1774973d23 Mon Sep 17 00:00:00 2001 From: fred Date: Wed, 12 Feb 2025 08:30:56 -0300 Subject: [PATCH 31/31] chore: package rename --- .../core/enterprise/TimefoldSolverEnterpriseService.java | 2 +- .../core/impl/localsearch/DefaultLocalSearchPhaseFactory.java | 2 +- .../core/impl/localsearch/decider/LocalSearchDecider.java | 2 +- .../impl/localsearch/decider/acceptor/AcceptorFactory.java | 4 ++-- .../localsearch/decider/acceptor/RestartableAcceptor.java | 2 +- .../lateacceptance/DiversifiedLateAcceptanceAcceptor.java | 2 +- .../acceptor/lateacceptance/LateAcceptanceAcceptor.java | 2 +- .../AbstractGeometricStuckCriterion.java | 2 +- .../DiminishedReturnsStuckCriterion.java | 2 +- .../acceptor/{restart => stuckcriterion}/StuckCriterion.java | 2 +- .../decider/{reconfiguration => restart}/RestartStrategy.java | 4 ++-- .../RestoreBestSolutionRestartStrategy.java | 2 +- .../lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java | 2 +- .../acceptor/lateacceptance/LateAcceptanceAcceptorTest.java | 2 +- .../DiminishedReturnsStuckCriterionTest.java | 4 ++-- .../{reconfiguration => restart}/RestartStrategyTest.java | 2 +- 16 files changed, 19 insertions(+), 19 deletions(-) rename core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/{restart => stuckcriterion}/AbstractGeometricStuckCriterion.java (99%) rename core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/{restart => stuckcriterion}/DiminishedReturnsStuckCriterion.java (99%) rename core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/{restart => stuckcriterion}/StuckCriterion.java (97%) rename core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/{reconfiguration => restart}/RestartStrategy.java (93%) rename core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/{reconfiguration => restart}/RestoreBestSolutionRestartStrategy.java (96%) rename core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/{restart => stuckcriterion}/DiminishedReturnsStuckCriterionTest.java (97%) rename core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/{reconfiguration => restart}/RestartStrategyTest.java (95%) diff --git a/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java b/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java index 59c301148b..1378722578 100644 --- a/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java +++ b/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java @@ -29,7 +29,7 @@ import ai.timefold.solver.core.impl.localsearch.decider.LocalSearchDecider; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.Acceptor; import ai.timefold.solver.core.impl.localsearch.decider.forager.LocalSearchForager; -import ai.timefold.solver.core.impl.localsearch.decider.reconfiguration.RestartStrategy; +import ai.timefold.solver.core.impl.localsearch.decider.restart.RestartStrategy; import ai.timefold.solver.core.impl.partitionedsearch.PartitionedSearchPhase; import ai.timefold.solver.core.impl.solver.termination.Termination; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java index d1fc083447..f0a98b2c38 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java @@ -32,7 +32,7 @@ import ai.timefold.solver.core.impl.localsearch.decider.acceptor.AcceptorFactory; import ai.timefold.solver.core.impl.localsearch.decider.forager.LocalSearchForager; import ai.timefold.solver.core.impl.localsearch.decider.forager.LocalSearchForagerFactory; -import ai.timefold.solver.core.impl.localsearch.decider.reconfiguration.RestoreBestSolutionRestartStrategy; +import ai.timefold.solver.core.impl.localsearch.decider.restart.RestoreBestSolutionRestartStrategy; import ai.timefold.solver.core.impl.phase.AbstractPhaseFactory; import ai.timefold.solver.core.impl.solver.recaller.BestSolutionRecaller; import ai.timefold.solver.core.impl.solver.termination.Termination; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java index 98b6c5f478..306556a797 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/LocalSearchDecider.java @@ -6,7 +6,7 @@ import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.Acceptor; import ai.timefold.solver.core.impl.localsearch.decider.forager.LocalSearchForager; -import ai.timefold.solver.core.impl.localsearch.decider.reconfiguration.RestartStrategy; +import ai.timefold.solver.core.impl.localsearch.decider.restart.RestartStrategy; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java index 580441f954..2b3561840c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/AcceptorFactory.java @@ -15,10 +15,10 @@ import ai.timefold.solver.core.impl.localsearch.decider.acceptor.hillclimbing.HillClimbingAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance.DiversifiedLateAcceptanceAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.lateacceptance.LateAcceptanceAcceptor; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.DiminishedReturnsStuckCriterion; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.StuckCriterion; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.simulatedannealing.SimulatedAnnealingAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stepcountinghillclimbing.StepCountingHillClimbingAcceptor; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.DiminishedReturnsStuckCriterion; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.StuckCriterion; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.tabu.EntityTabuAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.tabu.MoveTabuAcceptor; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.tabu.ValueTabuAcceptor; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/RestartableAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/RestartableAcceptor.java index 11fe342b7d..d914566394 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/RestartableAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/RestartableAcceptor.java @@ -1,6 +1,6 @@ package ai.timefold.solver.core.impl.localsearch.decider.acceptor; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.StuckCriterion; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.StuckCriterion; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java index 4ceb91f5db..1b5f5a70c1 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptor.java @@ -4,7 +4,7 @@ import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.RestartableAcceptor; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.StuckCriterion; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.StuckCriterion; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java index d6c3832d5c..6433f3ca3e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptor.java @@ -4,7 +4,7 @@ import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.RestartableAcceptor; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.StuckCriterion; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.StuckCriterion; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/stuckcriterion/AbstractGeometricStuckCriterion.java similarity index 99% rename from core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java rename to core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/stuckcriterion/AbstractGeometricStuckCriterion.java index e114b2f8ff..179fec99fd 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/AbstractGeometricStuckCriterion.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/stuckcriterion/AbstractGeometricStuckCriterion.java @@ -1,4 +1,4 @@ -package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; +package ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterion.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/stuckcriterion/DiminishedReturnsStuckCriterion.java similarity index 99% rename from core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterion.java rename to core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/stuckcriterion/DiminishedReturnsStuckCriterion.java index 827a115f05..18e7fa2b5b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterion.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/stuckcriterion/DiminishedReturnsStuckCriterion.java @@ -1,4 +1,4 @@ -package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; +package ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/StuckCriterion.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/stuckcriterion/StuckCriterion.java similarity index 97% rename from core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/StuckCriterion.java rename to core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/stuckcriterion/StuckCriterion.java index 50ed41342a..0d06ac35d7 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/StuckCriterion.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/stuckcriterion/StuckCriterion.java @@ -1,4 +1,4 @@ -package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; +package ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion; import ai.timefold.solver.core.api.solver.Solver; import ai.timefold.solver.core.impl.localsearch.event.LocalSearchPhaseLifecycleListener; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestartStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/restart/RestartStrategy.java similarity index 93% rename from core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestartStrategy.java rename to core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/restart/RestartStrategy.java index eeb48ba18c..135abfd8c8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestartStrategy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/restart/RestartStrategy.java @@ -1,7 +1,7 @@ -package ai.timefold.solver.core.impl.localsearch.decider.reconfiguration; +package ai.timefold.solver.core.impl.localsearch.decider.restart; import ai.timefold.solver.core.impl.localsearch.decider.LocalSearchDecider; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.StuckCriterion; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.StuckCriterion; import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleListener; import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionRestartStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/restart/RestoreBestSolutionRestartStrategy.java similarity index 96% rename from core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionRestartStrategy.java rename to core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/restart/RestoreBestSolutionRestartStrategy.java index 347a6e1f55..0f1b8c2751 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestoreBestSolutionRestartStrategy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/restart/RestoreBestSolutionRestartStrategy.java @@ -1,4 +1,4 @@ -package ai.timefold.solver.core.impl.localsearch.decider.reconfiguration; +package ai.timefold.solver.core.impl.localsearch.decider.restart; import java.util.Objects; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java index 11a83b6568..523168feab 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/DiversifiedLateAcceptanceAcceptorTest.java @@ -6,7 +6,7 @@ import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.AbstractAcceptorTest; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.StuckCriterion; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.StuckCriterion; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; import ai.timefold.solver.core.impl.solver.scope.SolverScope; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptorTest.java index 9a6d42bcc5..a0d04cb89a 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/lateacceptance/LateAcceptanceAcceptorTest.java @@ -8,7 +8,7 @@ import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.impl.localsearch.decider.acceptor.AbstractAcceptorTest; -import ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.StuckCriterion; +import ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.StuckCriterion; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchStepScope; import ai.timefold.solver.core.impl.solver.scope.SolverScope; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterionTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/stuckcriterion/DiminishedReturnsStuckCriterionTest.java similarity index 97% rename from core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterionTest.java rename to core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/stuckcriterion/DiminishedReturnsStuckCriterionTest.java index e4a8237970..1ba0c0f594 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/restart/DiminishedReturnsStuckCriterionTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/acceptor/stuckcriterion/DiminishedReturnsStuckCriterionTest.java @@ -1,6 +1,6 @@ -package ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart; +package ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion; -import static ai.timefold.solver.core.impl.localsearch.decider.acceptor.restart.DiminishedReturnsStuckCriterion.TIME_WINDOW_MILLIS; +import static ai.timefold.solver.core.impl.localsearch.decider.acceptor.stuckcriterion.DiminishedReturnsStuckCriterion.TIME_WINDOW_MILLIS; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestartStrategyTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/restart/RestartStrategyTest.java similarity index 95% rename from core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestartStrategyTest.java rename to core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/restart/RestartStrategyTest.java index 3588e01210..1614155a31 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/reconfiguration/RestartStrategyTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/restart/RestartStrategyTest.java @@ -1,4 +1,4 @@ -package ai.timefold.solver.core.impl.localsearch.decider.reconfiguration; +package ai.timefold.solver.core.impl.localsearch.decider.restart; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.*;