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 4fbbe82298..c20e16d78d 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 @@ -19,7 +19,14 @@ public class LateAcceptanceAcceptor extends AbstractAcceptor lastAcceptedScore = null; + // move termination that triggers the reconfiguration when it is terminated protected MoveCountTermination moveCountTermination; public void setLateAcceptanceSize(int lateAcceptanceSize) { @@ -50,12 +57,13 @@ public void phaseStarted(LocalSearchPhaseScope phaseScope) { var initialScore = phaseScope.getBestScore(); Arrays.fill(previousScores, initialScore); lateScoreIndex = 0; - lateAcceptanceReconfigurationRatioCount = (long) (lateAcceptanceSize * moveReconfigurationRatio / 100); - var lateAcceptanceReconfigurationMoveCount = (long) (phaseScope.getMoveSelectorSize() * moveCountLimitPercentage / 100); - moveCountTermination = new MoveCountTermination<>(lateAcceptanceReconfigurationMoveCount, true); + currentReconfigurationRationCount = 1; + maxReconfigurationRatioCount = (long) (lateAcceptanceSize * moveReconfigurationRatio / 100); + maxReconfigurationMoveCount = (long) (phaseScope.getMoveSelectorSize() * moveCountLimitPercentage / 100); + moveCountTermination = new MoveCountTermination<>(maxReconfigurationMoveCount, true); moveCountTermination.phaseStarted(phaseScope); - logger.info("Late Acceptance reconfiguration move count({}), late elements count({}) ", - lateAcceptanceReconfigurationMoveCount, lateAcceptanceReconfigurationRatioCount); + logger.info("Late Acceptance reconfiguration move count({}), max inferior elements count({}) ", + maxReconfigurationMoveCount, maxReconfigurationRatioCount); } private void validate() { @@ -85,20 +93,26 @@ public boolean isAccepted(LocalSearchMoveScope moveScope) { return false; } - @SuppressWarnings({"rawtypes", "unchecked"}) @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) public void stepEnded(LocalSearchStepScope stepScope) { super.stepEnded(stepScope); Score stepScore = stepScope.getScore(); - var lateScoreCmp = stepScore.compareTo(previousScores[lateScoreIndex]); - var lastStepScore = stepScope.getPhaseScope().getLastCompletedStepScope().getScore(); - var lastStepScoreCmp = stepScore.compareTo(lastStepScore); + var lateScore = previousScores[lateScoreIndex]; previousScores[lateScoreIndex] = stepScope.getScore(); lateScoreIndex = (lateScoreIndex + 1) % lateAcceptanceSize; - if (lateScoreCmp > 0 || lastStepScoreCmp > 0) { - // The terminator is only updated when a superior solution is found. + if (maxReconfigurationMoveCount > 0) { + // The termination is only updated when a superior solution is found. // Otherwise, we continue incrementing the moves until the reconfiguration is triggered - moveCountTermination.stepEnded(stepScope); + var lateScoreCmp = lateScore != null && stepScore.compareTo(lateScore) > 0; + var lastStepScoreCmp = lastAcceptedScore != null && stepScore.compareTo(lastAcceptedScore) > 0; + if (lateScore == null || lateScoreCmp || lastStepScoreCmp) { + moveCountTermination.stepEnded(stepScope); + if (lastStepScoreCmp) { + // Reset the current number of accepted inferior solutions + currentReconfigurationRationCount = 1; + } + } } } @@ -108,24 +122,35 @@ public void phaseEnded(LocalSearchPhaseScope phaseScope) { previousScores = null; moveCountTermination = null; lateScoreIndex = -1; + currentReconfigurationRationCount = 1; + lastAcceptedScore = null; } @Override public boolean needReconfiguration(LocalSearchStepScope stepScope) { - return moveCountTermination.isSolverTerminated(stepScope.getPhaseScope().getSolverScope()); + return maxReconfigurationMoveCount > 0 + && currentReconfigurationRationCount <= maxReconfigurationRatioCount + && moveCountTermination.isSolverTerminated(stepScope.getPhaseScope().getSolverScope()); } @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) public void applyReconfiguration(LocalSearchStepScope stepScope) { var idx = lateScoreIndex; - if (previousScores[idx] == null) { - // The method still has null values from the last reconfiguration, - // and we don't need to apply any reconfiguration - return; - } - for (var i = 0; i < lateAcceptanceReconfigurationRatioCount; i++) { - previousScores[idx] = null; + for (var i = 0; i < currentReconfigurationRationCount; i++) { + // We first increment to ensure stepEnded won't replace it, and it will be used in the next iteration idx = (idx + 1) % lateAcceptanceSize; + previousScores[idx] = null; + } + Score currentBestScore = stepScope.getPhaseScope().getSolverScope().getBestScore(); + if (lastAcceptedScore == null || currentBestScore.compareTo(lastAcceptedScore) > 0) { + lastAcceptedScore = currentBestScore; } + logger.info("Reconfiguration applied to inferior elements count ({}), best current score ({}).", + currentReconfigurationRationCount, lastAcceptedScore); + currentReconfigurationRationCount++; + // Ensure that after accepting an inferior solution, + // the LS evaluates ~maxReconfigurationMoveCount neighbors again + moveCountTermination.stepEnded(stepScope); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/MoveCountTermination.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/MoveCountTermination.java index 3b62e89b9b..6da0d0bbc9 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/MoveCountTermination.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/MoveCountTermination.java @@ -49,8 +49,8 @@ public void phaseStarted(AbstractPhaseScope phaseScope) { } @Override - public void stepStarted(AbstractStepScope stepScope) { - super.stepStarted(stepScope); + public void stepEnded(AbstractStepScope stepScope) { + super.stepEnded(stepScope); if (updateMoveCountPerStep) { lastMoveCount = stepScope.getPhaseScope().getSolverScope().getMoveEvaluationCount(); } 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 30f8e21d57..402e5a4c9b 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 @@ -280,7 +280,7 @@ void applyReconfiguration() { assertThat(acceptor.isAccepted(moveScope0)).isFalse(); assertThat(acceptor.needReconfiguration(stepScope0)).isFalse(); - // Reconfiguration + // Test reconfiguration solverScope.addMoveEvaluationCount(1); assertThat(acceptor.needReconfiguration(stepScope0)).isFalse(); solverScope.addMoveEvaluationCount(1); @@ -288,16 +288,51 @@ void applyReconfiguration() { assertThat(acceptor.isAccepted(moveScope0)).isFalse(); // Apply reconfiguration + // One iteration + acceptor.phaseStarted(phaseScope); stepScope0.setScore(SimpleScore.of(-3000)); var moveScope1 = buildMoveScope(stepScope0, -3000); acceptor.applyReconfiguration(stepScope0); assertThat(acceptor.isAccepted(moveScope1)).isTrue(); acceptor.stepEnded(stepScope0); - assertThat(acceptor.isAccepted(moveScope1)).isTrue(); + assertThat(acceptor.isAccepted(moveScope1)).isFalse(); acceptor.stepEnded(stepScope0); + + // Two iterations + acceptor.phaseStarted(phaseScope); var moveScope2 = buildMoveScope(stepScope0, -4000); - acceptor.stepStarted(stepScope0); - assertThat(acceptor.needReconfiguration(stepScope0)).isFalse(); + acceptor.applyReconfiguration(stepScope0); + acceptor.applyReconfiguration(stepScope0); + assertThat(acceptor.isAccepted(moveScope2)).isTrue(); + acceptor.stepEnded(stepScope0); assertThat(acceptor.isAccepted(moveScope2)).isFalse(); + acceptor.stepEnded(stepScope0); + acceptor.applyReconfiguration(stepScope0); + assertThat(acceptor.isAccepted(moveScope2)).isTrue(); + acceptor.stepEnded(stepScope0); + assertThat(acceptor.isAccepted(moveScope2)).isTrue(); + acceptor.stepEnded(stepScope0); + assertThat(acceptor.isAccepted(moveScope2)).isFalse(); + acceptor.stepEnded(stepScope0); + + // Reset + acceptor.phaseStarted(phaseScope); + var moveScope3 = buildMoveScope(stepScope0, -4000); + acceptor.applyReconfiguration(stepScope0); + assertThat(acceptor.isAccepted(moveScope3)).isTrue(); + acceptor.stepEnded(stepScope0); + assertThat(acceptor.isAccepted(moveScope3)).isFalse(); + acceptor.stepEnded(stepScope0); + // This step will reset currentReconfigurationRationCount + var moveScope4 = buildMoveScope(stepScope0, -900); + stepScope0.setScore(SimpleScore.of(-900)); + assertThat(acceptor.isAccepted(moveScope4)).isTrue(); + acceptor.stepEnded(stepScope0); + // Rerun the reconfiguration + acceptor.applyReconfiguration(stepScope0); + assertThat(acceptor.isAccepted(moveScope3)).isTrue(); + acceptor.stepEnded(stepScope0); + assertThat(acceptor.isAccepted(moveScope3)).isFalse(); + acceptor.stepEnded(stepScope0); } }