diff --git a/benchmark/src/main/resources/benchmark.xsd b/benchmark/src/main/resources/benchmark.xsd index 556dbdd286..50feb9d165 100644 --- a/benchmark/src/main/resources/benchmark.xsd +++ b/benchmark/src/main/resources/benchmark.xsd @@ -587,7 +587,10 @@ - + + + + diff --git a/core/src/build/revapi-differences.json b/core/src/build/revapi-differences.json index 77d0508e97..73ec6bd04b 100644 --- a/core/src/build/revapi-differences.json +++ b/core/src/build/revapi-differences.json @@ -89,7 +89,7 @@ "annotationType": "jakarta.xml.bind.annotation.XmlType", "attribute": "propOrder", "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\", \"spentLimit\", \"millisecondsSpentLimit\", \"secondsSpentLimit\", \"minutesSpentLimit\", \"hoursSpentLimit\", \"daysSpentLimit\", \"unimprovedSpentLimit\", \"unimprovedMillisecondsSpentLimit\", \"unimprovedSecondsSpentLimit\", \"unimprovedMinutesSpentLimit\", \"unimprovedHoursSpentLimit\", \"unimprovedDaysSpentLimit\", \"unimprovedScoreDifferenceThreshold\", \"bestScoreLimit\", \"bestScoreFeasible\", \"stepCountLimit\", \"unimprovedStepCountLimit\", \"scoreCalculationCountLimit\", \"moveCountLimit\", \"unimprovedBestSolutionLimit\", \"terminationConfigList\"}", + "newValue": "{\"terminationClass\", \"terminationCompositionStyle\", \"spentLimit\", \"millisecondsSpentLimit\", \"secondsSpentLimit\", \"minutesSpentLimit\", \"hoursSpentLimit\", \"daysSpentLimit\", \"unimprovedSpentLimit\", \"unimprovedMillisecondsSpentLimit\", \"unimprovedSecondsSpentLimit\", \"unimprovedMinutesSpentLimit\", \"unimprovedHoursSpentLimit\", \"unimprovedDaysSpentLimit\", \"unimprovedScoreDifferenceThreshold\", \"bestScoreLimit\", \"bestScoreFeasible\", \"stepCountLimit\", \"unimprovedStepCountLimit\", \"scoreCalculationCountLimit\", \"moveCountLimit\", \"flatLineDetectionRatio\", \"newCurveDetectionRatio\", \"terminationConfigList\"}", "justification": "Add new termination config" } ] diff --git a/core/src/main/java/ai/timefold/solver/core/config/solver/termination/TerminationConfig.java b/core/src/main/java/ai/timefold/solver/core/config/solver/termination/TerminationConfig.java index 6b77601d5a..4fa83c492f 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/solver/termination/TerminationConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/solver/termination/TerminationConfig.java @@ -39,7 +39,8 @@ "unimprovedStepCountLimit", "scoreCalculationCountLimit", "moveCountLimit", - "unimprovedBestSolutionLimit", + "flatLineDetectionRatio", + "newCurveDetectionRatio", "terminationConfigList" }) public class TerminationConfig extends AbstractConfig { @@ -79,7 +80,8 @@ public class TerminationConfig extends AbstractConfig { private Long moveCountLimit = null; - private Double unimprovedBestSolutionLimit = null; + private Double flatLineDetectionRatio = null; + private Double newCurveDetectionRatio = null; @XmlElement(name = "termination") private List terminationConfigList = null; @@ -260,12 +262,20 @@ public void setMoveCountLimit(@Nullable Long moveCountLimit) { this.moveCountLimit = moveCountLimit; } - public @Nullable Double getUnimprovedBestSolutionLimit() { - return unimprovedBestSolutionLimit; + public @Nullable Double getFlatLineDetectionRatio() { + return flatLineDetectionRatio; } - public void setUnimprovedBestSolutionLimit(@Nullable Double unimprovedBestSolutionLimit) { - this.unimprovedBestSolutionLimit = unimprovedBestSolutionLimit; + public void setFlatLineDetectionRatio(@Nullable Double flatLineDetectionRatio) { + this.flatLineDetectionRatio = flatLineDetectionRatio; + } + + public @Nullable Double getNewCurveDetectionRatio() { + return newCurveDetectionRatio; + } + + public void setNewCurveDetectionRatio(@Nullable Double newCurveDetectionRatio) { + this.newCurveDetectionRatio = newCurveDetectionRatio; } public @Nullable List<@NonNull TerminationConfig> getTerminationConfigList() { @@ -391,8 +401,13 @@ public TerminationConfig withTerminationClass(Class termi return this; } - public @NonNull TerminationConfig withUnimprovedBestSolutionLimit(@NonNull Double unimprovedBestSolutionLimit) { - this.unimprovedBestSolutionLimit = unimprovedBestSolutionLimit; + public @NonNull TerminationConfig withFlatLineDetectionRatio(@NonNull Double flatLineDetectionRatio) { + this.flatLineDetectionRatio = flatLineDetectionRatio; + return this; + } + + public @NonNull TerminationConfig withNewCurveDetectionRatio(@NonNull Double newCurveDetectionRatio) { + this.newCurveDetectionRatio = newCurveDetectionRatio; return this; } @@ -505,7 +520,8 @@ public boolean isConfigured() { unimprovedStepCountLimit != null || scoreCalculationCountLimit != null || moveCountLimit != null || - unimprovedBestSolutionLimit != null || + flatLineDetectionRatio != null || + newCurveDetectionRatio != null || isTerminationListConfigured(); } @@ -546,8 +562,10 @@ private boolean isTerminationListConfigured() { inheritedConfig.getScoreCalculationCountLimit()); moveCountLimit = ConfigUtils.inheritOverwritableProperty(moveCountLimit, inheritedConfig.getMoveCountLimit()); - unimprovedBestSolutionLimit = ConfigUtils.inheritOverwritableProperty(unimprovedBestSolutionLimit, - inheritedConfig.getUnimprovedBestSolutionLimit()); + flatLineDetectionRatio = ConfigUtils.inheritOverwritableProperty(flatLineDetectionRatio, + inheritedConfig.getFlatLineDetectionRatio()); + newCurveDetectionRatio = ConfigUtils.inheritOverwritableProperty(newCurveDetectionRatio, + inheritedConfig.getNewCurveDetectionRatio()); terminationConfigList = ConfigUtils.inheritMergeableListConfig( terminationConfigList, inheritedConfig.getTerminationConfigList()); return this; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/TerminationFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/TerminationFactory.java index de41767fe4..385b2483c7 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/TerminationFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/TerminationFactory.java @@ -88,8 +88,9 @@ The termination with bestScoreFeasible (%s) can only be used with a score type \ if (terminationConfig.getMoveCountLimit() != null) { terminationList.add(new MoveCountTermination<>(terminationConfig.getMoveCountLimit())); } - if (terminationConfig.getUnimprovedBestSolutionLimit() != null) { - terminationList.add(new UnimprovedBestSolutionTermination<>(terminationConfig.getUnimprovedBestSolutionLimit())); + if (terminationConfig.getFlatLineDetectionRatio() != null) { + terminationList.add(new UnimprovedBestSolutionTermination<>(terminationConfig.getFlatLineDetectionRatio(), + terminationConfig.getNewCurveDetectionRatio())); } terminationList.addAll(buildInnerTermination(configPolicy)); return buildTerminationFromList(terminationList); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/UnimprovedBestSolutionTermination.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/UnimprovedBestSolutionTermination.java index c8eff28551..4be7ceb578 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/UnimprovedBestSolutionTermination.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/UnimprovedBestSolutionTermination.java @@ -1,5 +1,8 @@ package ai.timefold.solver.core.impl.solver.termination; +import java.time.Clock; +import java.util.Objects; + 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; @@ -9,42 +12,58 @@ public final class UnimprovedBestSolutionTermination extends AbstractTermination { // Minimal interval of time to avoid early conclusions - protected static final long MINIMAL_INTERVAL_TIME = 10L; - // The ratio specifies the minimum criteria to determine a flat line between two move count values. - // A value of 0.2 represents 20% of the execution time of the current growth curve. - // For example, the first best solution is found at 0 seconds, - // while the last best solution is found at 60 seconds. - // Given the total time of 60 seconds, - // we will identify a flat line between the last best solution and the discovered new best solution - // if the time difference exceeds 12 seconds. - private final double maxUnimprovedBestSolutionLimit; - // Similar to unimprovedBestSolutionLimit, - // this criterion is specifically used to identify flat lines among multiple curves before the termination. - // The goal is to adjust the stop criterion based on the latest curve found when there are several. - private final double minUnimprovedBestSolutionLimit; - // The field stores the first best solution move count found in the curve growth chart. + protected static final long MINIMAL_INTERVAL_TIME_MILLIS = 10_000L; + // This setting determines the amount of time + // that is allowed without any improvements since the last best solution was identified. + // For example, if the last solution was found at 10 seconds and the setting is configured to 0.5, + // the solver will stop if no improvement is made within 5 seconds. + private final double flatLineDetectionRatio; + // This criterion functions similarly to the flatLineDetectionRatio, + // as it is also used to identify periods without improvement. + // However, the key difference is that it focuses on detecting "flat lines" between solution improvements. + // When a flat line is detected after the solution has improved, + // it indicates that the previous duration was not enough to terminate the process. + // However, it also indicates that the solver will begin + // re-evaluating the termination criterion from the last improvement before the recent improvement. + private final double newCurveDetectionRatio; + private final Clock clock; + // The field stores the time of the first best solution of the current curve. // If a solving process involves multiple curves, // the value is tied to the growth of the last curve analyzed. - protected long initialImprovementMoveCount; - protected long lastImprovementMoveCount; - protected long lastMoveEvaluationSpeed; + protected long initialCurvePointMillis; + protected long lastImprovementMillis; private Score previousBest; protected Score currentBest; protected boolean waitForFirstBestScore; protected Boolean terminate; - public UnimprovedBestSolutionTermination(double unimprovedBestSolutionLimit) { - this.maxUnimprovedBestSolutionLimit = unimprovedBestSolutionLimit; - // 80% of the max unimproved limit - this.minUnimprovedBestSolutionLimit = unimprovedBestSolutionLimit * 0.8; - if (unimprovedBestSolutionLimit < 0) { + public UnimprovedBestSolutionTermination(Double flatLineDetectionRatio, Double newCurveDetectionRatio) { + this(flatLineDetectionRatio, newCurveDetectionRatio, Clock.systemUTC()); + } + + public UnimprovedBestSolutionTermination(Double flatLineDetectionRatio, Double newCurveDetectionRatio, Clock clock) { + this.flatLineDetectionRatio = Objects.requireNonNull(flatLineDetectionRatio, + "The field flatLineDetectionRatio is required for the termination UnimprovedBestSolutionTermination"); + this.newCurveDetectionRatio = Objects.requireNonNull(newCurveDetectionRatio, + "The field newCurveDetectionRatio is required for the termination UnimprovedBestSolutionTermination"); + this.clock = Objects.requireNonNull(clock); + if (flatLineDetectionRatio < 0) { + throw new IllegalArgumentException( + "The flatLineDetectionRatio (%.2f) cannot be negative.".formatted(flatLineDetectionRatio)); + } + if (newCurveDetectionRatio < 0) { throw new IllegalArgumentException( - "The unimprovedBestSolutionLimit (%.2f) cannot be negative.".formatted(unimprovedBestSolutionLimit)); + "The newCurveDetectionRatio (%.2f) cannot be negative.".formatted(newCurveDetectionRatio)); + } + if (newCurveDetectionRatio > flatLineDetectionRatio) { + throw new IllegalArgumentException( + "The newCurveDetectionRatio (%.2f) cannot be greater than flatLineDetectionRatio (%.2f)." + .formatted(newCurveDetectionRatio, flatLineDetectionRatio)); } } - public double getUnimprovedBestSolutionLimit() { - return maxUnimprovedBestSolutionLimit; + public double getFlatLineDetectionRatio() { + return flatLineDetectionRatio; } // ************************************************************************ @@ -55,9 +74,8 @@ public double getUnimprovedBestSolutionLimit() { @SuppressWarnings("unchecked") public void phaseStarted(AbstractPhaseScope phaseScope) { super.phaseStarted(phaseScope); - initialImprovementMoveCount = 0L; - lastImprovementMoveCount = 0L; - lastMoveEvaluationSpeed = 0L; + initialCurvePointMillis = clock.millis(); + lastImprovementMillis = 0L; currentBest = phaseScope.getBestScore(); previousBest = currentBest; waitForFirstBestScore = true; @@ -98,37 +116,35 @@ public boolean isPhaseTerminated(AbstractPhaseScope phaseScope) { if (waitForFirstBestScore) { return false; } - var currentMoveCount = phaseScope.getSolverScope().getMoveEvaluationCount(); + var currentTimeMillis = clock.millis(); var improved = currentBest.compareTo(phaseScope.getBestScore()) < 0; - lastMoveEvaluationSpeed = phaseScope.getSolverScope().getMoveEvaluationSpeed(); - var interval = calculateInterval(initialImprovementMoveCount, currentMoveCount); + var completeInterval = currentTimeMillis - initialCurvePointMillis; + var newInterval = currentTimeMillis - lastImprovementMillis; if (improved) { // If there is a flat line between the last and new best solutions, // the initial value becomes the most recent best score, // as it would be the starting point for the new curve. - var minInterval = Math.floor(interval * minUnimprovedBestSolutionLimit); - var maxInterval = Math.floor(interval * maxUnimprovedBestSolutionLimit); - var newInterval = calculateInterval(lastImprovementMoveCount, currentMoveCount); - if (lastImprovementMoveCount > 0 && interval >= MINIMAL_INTERVAL_TIME && newInterval > minInterval + var minInterval = Math.floor(completeInterval * newCurveDetectionRatio); + var maxInterval = Math.floor(completeInterval * flatLineDetectionRatio); + if (lastImprovementMillis > 0 && completeInterval >= MINIMAL_INTERVAL_TIME_MILLIS && newInterval > minInterval && newInterval < maxInterval) { - initialImprovementMoveCount = lastImprovementMoveCount; + initialCurvePointMillis = lastImprovementMillis; previousBest = currentBest; if (logger.isInfoEnabled()) { - logger.info("Starting a new curve with ({}), estimated time interval ({}s)", + logger.debug("Starting a new curve with ({}), estimated time interval ({}s)", previousBest, - String.format("%.2f", calculateInterval(0, initialImprovementMoveCount))); + String.format("%.2f", completeInterval / 1000.0)); } } - lastImprovementMoveCount = currentMoveCount; + lastImprovementMillis = currentTimeMillis; currentBest = phaseScope.getBestScore(); terminate = null; return false; } else { - if (interval < MINIMAL_INTERVAL_TIME) { + if (completeInterval < MINIMAL_INTERVAL_TIME_MILLIS) { return false; } - var maxInterval = Math.floor(interval * maxUnimprovedBestSolutionLimit); - var newInterval = calculateInterval(lastImprovementMoveCount, currentMoveCount); + var maxInterval = Math.floor(completeInterval * flatLineDetectionRatio); if (newInterval > maxInterval) { terminate = true; return true; @@ -160,19 +176,14 @@ public double calculatePhaseTimeGradient(AbstractPhaseScope phaseScop // Other methods // ************************************************************************ - private double calculateInterval(long startMoveCount, long endMoveCount) { - return (double) (endMoveCount - startMoveCount) / lastMoveEvaluationSpeed; - } - @Override public UnimprovedBestSolutionTermination createChildThreadTermination(SolverScope solverScope, ChildThreadType childThreadType) { - return new UnimprovedBestSolutionTermination<>(maxUnimprovedBestSolutionLimit); + return new UnimprovedBestSolutionTermination<>(flatLineDetectionRatio, newCurveDetectionRatio, clock); } @Override public String toString() { - return "UnimprovedMoveCountRatio(%.2f)".formatted(maxUnimprovedBestSolutionLimit); + return "UnimprovedBestSolutionTermination(%.2f, %.2f)".formatted(flatLineDetectionRatio, newCurveDetectionRatio); } - } diff --git a/core/src/main/resources/solver.xsd b/core/src/main/resources/solver.xsd index 11ca549a9d..7ad1ecedd1 100644 --- a/core/src/main/resources/solver.xsd +++ b/core/src/main/resources/solver.xsd @@ -197,7 +197,9 @@ - + + + diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/UnimprovedBestSolutionTerminationTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/UnimprovedBestSolutionTerminationTest.java index ee184309a7..3d8d529551 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/UnimprovedBestSolutionTerminationTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/UnimprovedBestSolutionTerminationTest.java @@ -4,6 +4,8 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; 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.LocalSearchPhaseScope; import ai.timefold.solver.core.impl.solver.scope.SolverScope; @@ -16,7 +18,10 @@ class UnimprovedBestSolutionTerminationTest { @Test void testTermination() { - var termination = new UnimprovedBestSolutionTermination(0.5); + Clock clock = Mockito.mock(Clock.class); + var currentTime = Clock.systemUTC().millis(); + var termination = new UnimprovedBestSolutionTermination(0.5, 0.4, + clock); var solverScope = Mockito.mock(SolverScope.class); var phaseScope = Mockito.mock(LocalSearchPhaseScope.class); when(phaseScope.getSolverScope()).thenReturn(solverScope); @@ -25,25 +30,29 @@ void testTermination() { termination.phaseStarted(phaseScope); termination.waitForFirstBestScore = false; - // First curve + // Terminate termination.currentBest = SimpleScore.of(2); - termination.initialImprovementMoveCount = 0L; - termination.lastImprovementMoveCount = 5L; - when(solverScope.getMoveEvaluationCount()).thenReturn(11L); + termination.initialCurvePointMillis = currentTime; + termination.lastImprovementMillis = currentTime + 10000; + when(clock.millis()).thenReturn(currentTime + 21000); assertThat(termination.isPhaseTerminated(phaseScope)).isTrue(); - // Second curve + // Don't terminate termination.terminate = null; termination.currentBest = SimpleScore.of(2); - termination.initialImprovementMoveCount = 10L; - termination.lastImprovementMoveCount = 15L; - when(solverScope.getMoveEvaluationCount()).thenReturn(21L); - assertThat(termination.isPhaseTerminated(phaseScope)).isTrue(); + termination.initialCurvePointMillis = currentTime; + termination.lastImprovementMillis = currentTime + 10000; + when(clock.millis()).thenReturn(currentTime + 20000); + assertThat(termination.isPhaseTerminated(phaseScope)).isFalse(); + } @Test - void testGrowthCurves() { - var termination = new UnimprovedBestSolutionTermination(0.5); + void testStartNewCurve() { + Clock clock = Mockito.mock(Clock.class); + var currentTime = Clock.systemUTC().millis(); + var termination = new UnimprovedBestSolutionTermination(0.5, 0.4, + clock); var solverScope = Mockito.mock(SolverScope.class); var phaseScope = Mockito.mock(LocalSearchPhaseScope.class); when(phaseScope.getSolverScope()).thenReturn(solverScope); @@ -54,56 +63,68 @@ void testGrowthCurves() { // Adding a new curve termination.currentBest = SimpleScore.of(1); - termination.initialImprovementMoveCount = 0L; - termination.lastImprovementMoveCount = 11L; - assertThat(termination.initialImprovementMoveCount).isZero(); - when(solverScope.getMoveEvaluationCount()).thenReturn(20L); + termination.initialCurvePointMillis = currentTime; + termination.lastImprovementMillis = currentTime + 10000; + when(clock.millis()).thenReturn(currentTime + 19000); assertThat(termination.isPhaseTerminated(phaseScope)).isFalse(); - assertThat(termination.initialImprovementMoveCount).isEqualTo(11L); + assertThat(termination.initialCurvePointMillis).isEqualTo(currentTime + 10000); // Not adding a new curve - flat line smaller than the minimum termination.terminate = null; termination.currentBest = SimpleScore.of(1); - termination.initialImprovementMoveCount = 0L; - termination.lastImprovementMoveCount = 11L; - assertThat(termination.initialImprovementMoveCount).isZero(); - when(solverScope.getMoveEvaluationCount()).thenReturn(15L); + termination.initialCurvePointMillis = currentTime; + termination.lastImprovementMillis = currentTime + 10000; + when(clock.millis()).thenReturn(currentTime + 11000); assertThat(termination.isPhaseTerminated(phaseScope)).isFalse(); - assertThat(termination.initialImprovementMoveCount).isZero(); + assertThat(termination.initialCurvePointMillis).isEqualTo(currentTime); // Not adding a new curve - flat line larger than the minimum termination.terminate = null; termination.currentBest = SimpleScore.of(1); - termination.initialImprovementMoveCount = 0L; - termination.lastImprovementMoveCount = 11L; - assertThat(termination.initialImprovementMoveCount).isZero(); - when(solverScope.getMoveEvaluationCount()).thenReturn(30L); + termination.initialCurvePointMillis = currentTime; + termination.lastImprovementMillis = currentTime + 10000; + when(clock.millis()).thenReturn(currentTime + 20000); assertThat(termination.isPhaseTerminated(phaseScope)).isFalse(); - assertThat(termination.initialImprovementMoveCount).isZero(); + assertThat(termination.initialCurvePointMillis).isEqualTo(currentTime); } @Test void testMinimalInterval() { - var termination = new UnimprovedBestSolutionTermination(0.5); + Clock clock = Mockito.mock(Clock.class); + var currentTime = Clock.systemUTC().millis(); + var termination = new UnimprovedBestSolutionTermination(0.5, 0.4, + clock); var solverScope = Mockito.mock(SolverScope.class); var phaseScope = Mockito.mock(LocalSearchPhaseScope.class); when(phaseScope.getSolverScope()).thenReturn(solverScope); when(phaseScope.getBestScore()).thenReturn(SimpleScore.of(2)); when(solverScope.getMoveEvaluationSpeed()).thenReturn(1L); termination.phaseStarted(phaseScope); + termination.waitForFirstBestScore = false; - termination.lastImprovementMoveCount = 7L; + // Don't terminate termination.currentBest = SimpleScore.of(2); - when(solverScope.getMoveEvaluationCount()).thenReturn(UnimprovedBestSolutionTermination.MINIMAL_INTERVAL_TIME - 1); + termination.initialCurvePointMillis = currentTime; + termination.lastImprovementMillis = currentTime + UnimprovedBestSolutionTermination.MINIMAL_INTERVAL_TIME_MILLIS; + when(clock.millis()).thenReturn(currentTime + UnimprovedBestSolutionTermination.MINIMAL_INTERVAL_TIME_MILLIS * 2 - 1); assertThat(termination.isPhaseTerminated(phaseScope)).isFalse(); + // Don't terminate termination.terminate = null; - when(solverScope.getMoveEvaluationCount()).thenReturn(UnimprovedBestSolutionTermination.MINIMAL_INTERVAL_TIME + 1); - assertThat(termination.isPhaseTerminated(phaseScope)).isFalse(); + termination.currentBest = SimpleScore.of(2); + termination.initialCurvePointMillis = currentTime; + termination.lastImprovementMillis = currentTime + UnimprovedBestSolutionTermination.MINIMAL_INTERVAL_TIME_MILLIS; + when(clock.millis()).thenReturn(currentTime + UnimprovedBestSolutionTermination.MINIMAL_INTERVAL_TIME_MILLIS * 2 + 1); + assertThat(termination.isPhaseTerminated(phaseScope)).isTrue(); } @Test void invalidTermination() { - assertThatIllegalArgumentException().isThrownBy(() -> new UnimprovedBestSolutionTermination(-1)); + assertThatIllegalArgumentException() + .isThrownBy(() -> new UnimprovedBestSolutionTermination(-1.0, 0.0)); + assertThatIllegalArgumentException() + .isThrownBy(() -> new UnimprovedBestSolutionTermination(0.0, -1.0)); + assertThatIllegalArgumentException() + .isThrownBy(() -> new UnimprovedBestSolutionTermination(0.0, 1.0)); } }