diff --git a/benchmark/src/main/resources/benchmark.xsd b/benchmark/src/main/resources/benchmark.xsd index 28963b2b01..34bef398b7 100644 --- a/benchmark/src/main/resources/benchmark.xsd +++ b/benchmark/src/main/resources/benchmark.xsd @@ -593,7 +593,7 @@ - + diff --git a/core/src/build/revapi-differences.json b/core/src/build/revapi-differences.json index 58e0b12894..eea65dcb89 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\", \"stopFlatLineDetectionRatio\", \"noStopFlatLineDetectionRatio\", \"minimalExecutionTimeSeconds\", \"terminationConfigList\"}", + "newValue": "{\"terminationClass\", \"terminationCompositionStyle\", \"spentLimit\", \"millisecondsSpentLimit\", \"secondsSpentLimit\", \"minutesSpentLimit\", \"hoursSpentLimit\", \"daysSpentLimit\", \"unimprovedSpentLimit\", \"unimprovedMillisecondsSpentLimit\", \"unimprovedSecondsSpentLimit\", \"unimprovedMinutesSpentLimit\", \"unimprovedHoursSpentLimit\", \"unimprovedDaysSpentLimit\", \"unimprovedScoreDifferenceThreshold\", \"bestScoreLimit\", \"bestScoreFeasible\", \"stepCountLimit\", \"unimprovedStepCountLimit\", \"scoreCalculationCountLimit\", \"moveCountLimit\", \"stopFlatLineDetectionRatio\", \"noStopFlatLineDetectionRatio\", \"delayFlatLineSecondsSpentLimit\", \"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 99b90c854a..08396b056b 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 @@ -41,7 +41,7 @@ "moveCountLimit", "stopFlatLineDetectionRatio", "noStopFlatLineDetectionRatio", - "minimalExecutionTimeSeconds", + "delayFlatLineSecondsSpentLimit", "terminationConfigList" }) public class TerminationConfig extends AbstractConfig { @@ -83,7 +83,7 @@ public class TerminationConfig extends AbstractConfig { private Double stopFlatLineDetectionRatio = null; private Double noStopFlatLineDetectionRatio = null; - private Long minimalExecutionTimeSeconds = null; + private Long delayFlatLineSecondsSpentLimit = null; @XmlElement(name = "termination") private List terminationConfigList = null; @@ -280,12 +280,12 @@ public void setNoStopFlatLineDetectionRatio(@Nullable Double noStopFlatLineDetec this.noStopFlatLineDetectionRatio = noStopFlatLineDetectionRatio; } - public @Nullable Long getMinimalExecutionTimeSeconds() { - return minimalExecutionTimeSeconds; + public @Nullable Long getDelayFlatLineSecondsSpentLimit() { + return delayFlatLineSecondsSpentLimit; } - public void setMinimalExecutionTimeSeconds(@Nullable Long minimalExecutionTimeSeconds) { - this.minimalExecutionTimeSeconds = minimalExecutionTimeSeconds; + public void setDelayFlatLineSecondsSpentLimit(@Nullable Long delayFlatLineSecondsSpentLimit) { + this.delayFlatLineSecondsSpentLimit = delayFlatLineSecondsSpentLimit; } public @Nullable List<@NonNull TerminationConfig> getTerminationConfigList() { @@ -421,8 +421,8 @@ public TerminationConfig withTerminationClass(Class termi return this; } - public @NonNull TerminationConfig withMinimalExecutionTimeSeconds(@NonNull Long minimalExecutionTimeSeconds) { - this.minimalExecutionTimeSeconds = minimalExecutionTimeSeconds; + public @NonNull TerminationConfig withDelayFlatLineSecondsSpentLimit(@NonNull Long delayFlatLineSecondsSpentLimit) { + this.delayFlatLineSecondsSpentLimit = delayFlatLineSecondsSpentLimit; return this; } @@ -537,7 +537,7 @@ public boolean isConfigured() { moveCountLimit != null || stopFlatLineDetectionRatio != null || noStopFlatLineDetectionRatio != null || - minimalExecutionTimeSeconds != null || + delayFlatLineSecondsSpentLimit != null || isTerminationListConfigured(); } @@ -582,8 +582,8 @@ private boolean isTerminationListConfigured() { inheritedConfig.getStopFlatLineDetectionRatio()); noStopFlatLineDetectionRatio = ConfigUtils.inheritOverwritableProperty(noStopFlatLineDetectionRatio, inheritedConfig.getNoStopFlatLineDetectionRatio()); - minimalExecutionTimeSeconds = ConfigUtils.inheritOverwritableProperty(minimalExecutionTimeSeconds, - inheritedConfig.getMinimalExecutionTimeSeconds()); + delayFlatLineSecondsSpentLimit = ConfigUtils.inheritOverwritableProperty(delayFlatLineSecondsSpentLimit, + inheritedConfig.getDelayFlatLineSecondsSpentLimit()); 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 204dc6946f..8f51535218 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 @@ -90,9 +90,10 @@ The termination with bestScoreFeasible (%s) can only be used with a score type \ } if (terminationConfig.getStopFlatLineDetectionRatio() != null || terminationConfig.getNoStopFlatLineDetectionRatio() != null - || terminationConfig.getMinimalExecutionTimeSeconds() != null) { + || terminationConfig.getDelayFlatLineSecondsSpentLimit() != null) { terminationList.add(new UnimprovedBestSolutionTermination<>(terminationConfig.getStopFlatLineDetectionRatio(), - terminationConfig.getNoStopFlatLineDetectionRatio(), terminationConfig.getMinimalExecutionTimeSeconds())); + terminationConfig.getNoStopFlatLineDetectionRatio(), + terminationConfig.getDelayFlatLineSecondsSpentLimit())); } 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 dc391f12d9..7233900be7 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 @@ -11,8 +11,8 @@ public final class UnimprovedBestSolutionTermination extends AbstractTermination { - // Minimal interval of time to avoid early conclusions - private final long minimalExecutionTimeMillis; + // Evaluation delay to avoid early conclusions + private final long delayExecutionTimeMillis; // 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, @@ -38,19 +38,20 @@ public final class UnimprovedBestSolutionTermination extends Abstract protected Boolean terminate; public UnimprovedBestSolutionTermination(Double stopFlatLineDetectionRatio, - Double noStopFlatLineDetectionRatio, Long minimalExecutionTimeSeconds) { - this(stopFlatLineDetectionRatio, noStopFlatLineDetectionRatio, minimalExecutionTimeSeconds, Clock.systemUTC()); + Double noStopFlatLineDetectionRatio, Long delayFlatLineSecondsSpentLimit) { + this(stopFlatLineDetectionRatio, noStopFlatLineDetectionRatio, delayFlatLineSecondsSpentLimit, Clock.systemUTC()); } public UnimprovedBestSolutionTermination(Double stopFlatLineDetectionRatio, Double noStopFlatLineDetectionRatio, - Long minimalExecutionTimeSeconds, Clock clock) { + Long delayFlatLineSecondsSpentLimit, Clock clock) { this.stopFlatLineDetectionRatio = Objects.requireNonNull(stopFlatLineDetectionRatio, "The field stopFlatLineDetectionRatio is required for the termination UnimprovedBestSolutionTermination"); this.noStopFlatLineDetectionRatio = Objects.requireNonNull(noStopFlatLineDetectionRatio, "The field noStopFlatLineDetectionRatio is required for the termination UnimprovedBestSolutionTermination"); - this.minimalExecutionTimeMillis = Objects.requireNonNull(minimalExecutionTimeSeconds, - "The field minimalExecutionTimeSeconds is required for the termination UnimprovedBestSolutionTermination") - * 1000L; + this.delayExecutionTimeMillis = + (Objects.requireNonNull(delayFlatLineSecondsSpentLimit, + "The field delayFlatLineSecondsSpentLimit is required for the termination UnimprovedBestSolutionTermination") + * 1000L); this.clock = Objects.requireNonNull(clock); if (stopFlatLineDetectionRatio < 0) { throw new IllegalArgumentException( @@ -65,14 +66,14 @@ public UnimprovedBestSolutionTermination(Double stopFlatLineDetectionRatio, Doub "The noStopFlatLineDetectionRatio (%.2f) cannot be greater than stopFlatLineDetectionRatio (%.2f)." .formatted(noStopFlatLineDetectionRatio, stopFlatLineDetectionRatio)); } - if (minimalExecutionTimeSeconds <= 0) { + if (delayFlatLineSecondsSpentLimit < 0) { throw new IllegalArgumentException( - "The minimalExecutionTimeSeconds %d must be great than zero.".formatted(minimalExecutionTimeSeconds)); + "The delayFlatLineSecondsSpentLimit (%d) cannot be negative.".formatted(delayFlatLineSecondsSpentLimit)); } } - public long getMinimalExecutionTimeMillis() { - return minimalExecutionTimeMillis; + public long getDelayExecutionTimeMillis() { + return delayExecutionTimeMillis; } public double getStopFlatLineDetectionRatio() { @@ -129,7 +130,7 @@ public boolean isPhaseTerminated(AbstractPhaseScope phaseScope) { if (terminate != null) { return terminate; } - // Validate if there is a first best solution and the poll time + // Validate if there is a first best solution if (waitForFirstBestScore) { return false; } @@ -144,7 +145,7 @@ public boolean isPhaseTerminated(AbstractPhaseScope phaseScope) { // as it would be the starting point for the new curve. var minInterval = Math.floor(lastImprovementInterval * noStopFlatLineDetectionRatio); var maxInterval = Math.floor(lastImprovementInterval * stopFlatLineDetectionRatio); - if (lastImprovementMillis > 0 && completeInterval >= minimalExecutionTimeMillis && newInterval > minInterval + if (lastImprovementMillis > 0 && completeInterval >= delayExecutionTimeMillis && newInterval >= minInterval && newInterval < maxInterval) { initialCurvePointMillis = lastImprovementMillis; previousBest = currentBest; @@ -159,7 +160,7 @@ public boolean isPhaseTerminated(AbstractPhaseScope phaseScope) { terminate = null; return false; } else { - if (completeInterval < minimalExecutionTimeMillis) { + if (completeInterval < delayExecutionTimeMillis) { return false; } var maxInterval = Math.floor(lastImprovementInterval * stopFlatLineDetectionRatio); @@ -198,12 +199,12 @@ public double calculatePhaseTimeGradient(AbstractPhaseScope phaseScop public UnimprovedBestSolutionTermination createChildThreadTermination(SolverScope solverScope, ChildThreadType childThreadType) { return new UnimprovedBestSolutionTermination<>(stopFlatLineDetectionRatio, noStopFlatLineDetectionRatio, - minimalExecutionTimeMillis, clock); + delayExecutionTimeMillis / 1000, clock); } @Override public String toString() { - return "UnimprovedBestSolutionTermination(%.2f, %.2f)".formatted(stopFlatLineDetectionRatio, - noStopFlatLineDetectionRatio); + return "UnimprovedBestSolutionTermination(%.2f, %.2f, %d)".formatted(stopFlatLineDetectionRatio, + noStopFlatLineDetectionRatio, delayExecutionTimeMillis / 1000); } } diff --git a/core/src/main/resources/solver.xsd b/core/src/main/resources/solver.xsd index 7b817f77fd..0c0be6c341 100644 --- a/core/src/main/resources/solver.xsd +++ b/core/src/main/resources/solver.xsd @@ -201,7 +201,7 @@ - + diff --git a/core/src/test/java/ai/timefold/solver/core/config/solver/termination/TerminationConfigTest.java b/core/src/test/java/ai/timefold/solver/core/config/solver/termination/TerminationConfigTest.java index 1f600f32ef..3791f8a68d 100644 --- a/core/src/test/java/ai/timefold/solver/core/config/solver/termination/TerminationConfigTest.java +++ b/core/src/test/java/ai/timefold/solver/core/config/solver/termination/TerminationConfigTest.java @@ -90,11 +90,11 @@ void childWithUnimprovedPropertiesFromParent() { TerminationConfig parent = new TerminationConfig() .withStopFlatLineDetectionRatio(0.5) .withNoStopFlatLineDetectionRatio(0.1) - .withMinimalExecutionTimeSeconds(10L); + .withDelayFlatLineSecondsSpentLimit(10L); child.inherit(parent); assertThat(child.getStopFlatLineDetectionRatio()).isEqualTo(0.5); assertThat(child.getNoStopFlatLineDetectionRatio()).isEqualTo(0.1); - assertThat(child.getMinimalExecutionTimeSeconds()).isEqualTo(10L); + assertThat(child.getDelayFlatLineSecondsSpentLimit()).isEqualTo(10L); } @Test diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/TerminationFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/TerminationFactoryTest.java index 284de54e9b..751a611728 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/TerminationFactoryTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/TerminationFactoryTest.java @@ -240,7 +240,7 @@ void buildUnimprovedBestScoreRatio() { var terminationConfig = new TerminationConfig(); terminationConfig.setStopFlatLineDetectionRatio(0.5); terminationConfig.setNoStopFlatLineDetectionRatio(0.1); - terminationConfig.setMinimalExecutionTimeSeconds(10L); + terminationConfig.setDelayFlatLineSecondsSpentLimit(10L); var termination = TerminationFactory.create(terminationConfig) .buildTermination(mock(HeuristicConfigPolicy.class)); assertThat(termination) @@ -249,7 +249,7 @@ void buildUnimprovedBestScoreRatio() { .isEqualTo(0.5); assertThat(((UnimprovedBestSolutionTermination) termination).getNoStopFlatLineDetectionRatio()) .isEqualTo(0.1); - assertThat(((UnimprovedBestSolutionTermination) termination).getMinimalExecutionTimeMillis()) + assertThat(((UnimprovedBestSolutionTermination) termination).getDelayExecutionTimeMillis()) .isEqualTo(10000L); } } 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 2b6349a214..2bdaf510b2 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 @@ -48,10 +48,10 @@ void testTermination() { } @Test - void testStartNewCurve() { + void testStartNewPoint() { Clock clock = Mockito.mock(Clock.class); var currentTime = Clock.systemUTC().millis(); - var termination = new UnimprovedBestSolutionTermination(0.5, 0.3, 10L, clock); + var termination = new UnimprovedBestSolutionTermination(0.5, 0.4, 10L, clock); var solverScope = Mockito.mock(SolverScope.class); var phaseScope = Mockito.mock(LocalSearchPhaseScope.class); when(phaseScope.getSolverScope()).thenReturn(solverScope); @@ -60,7 +60,7 @@ void testStartNewCurve() { termination.phaseStarted(phaseScope); termination.waitForFirstBestScore = false; - // Adding a new curve + // New start point termination.currentBest = SimpleScore.of(1); termination.initialCurvePointMillis = currentTime; termination.lastImprovementMillis = currentTime + 10_000; @@ -68,7 +68,7 @@ void testStartNewCurve() { assertThat(termination.isPhaseTerminated(phaseScope)).isFalse(); assertThat(termination.initialCurvePointMillis).isEqualTo(currentTime + 10_000); - // Not adding a new curve - flat line smaller than the minimum + // Don't change start point - flat line smaller than the minimum termination.terminate = null; termination.currentBest = SimpleScore.of(1); termination.initialCurvePointMillis = currentTime; @@ -77,7 +77,7 @@ void testStartNewCurve() { assertThat(termination.isPhaseTerminated(phaseScope)).isFalse(); assertThat(termination.initialCurvePointMillis).isEqualTo(currentTime); - // Not adding a new curve - flat line larger than the minimum + // Don't change start point - flat line larger than the minimum termination.terminate = null; termination.currentBest = SimpleScore.of(1); termination.initialCurvePointMillis = currentTime; @@ -88,7 +88,7 @@ void testStartNewCurve() { } @Test - void testMinimalInterval() { + void testDelayInterval() { Clock clock = Mockito.mock(Clock.class); var currentTime = Clock.systemUTC().millis(); var termination = new UnimprovedBestSolutionTermination(0.5, 0.4, 10L, clock); @@ -119,15 +119,12 @@ void testMinimalInterval() { @Test void invalidTermination() { assertThatIllegalArgumentException() - .isThrownBy(() -> new UnimprovedBestSolutionTermination(-1.0, 0.0, 1L)); - assertThatIllegalArgumentException() - .isThrownBy(() -> new UnimprovedBestSolutionTermination(0.0, -1.0, 1L)); + .isThrownBy(() -> new UnimprovedBestSolutionTermination(-1.0, 0.0, 0L)); assertThatIllegalArgumentException() - .isThrownBy(() -> new UnimprovedBestSolutionTermination(0.0, 1.0, 1L)); + .isThrownBy(() -> new UnimprovedBestSolutionTermination(0.0, -1.0, 0L)); assertThatIllegalArgumentException() - .isThrownBy(() -> new UnimprovedBestSolutionTermination(1.0, 1.0, 0L)); + .isThrownBy(() -> new UnimprovedBestSolutionTermination(0.0, 1.0, 0L)); assertThatIllegalArgumentException() - .isThrownBy(() -> new UnimprovedBestSolutionTermination(0.1, 1.0, 1L) - .calculateSolverTimeGradient(null)); + .isThrownBy(() -> new UnimprovedBestSolutionTermination(1.0, 1.0, -1L)); } } diff --git a/docs/src/modules/ROOT/pages/optimization-algorithms/overview.adoc b/docs/src/modules/ROOT/pages/optimization-algorithms/overview.adoc index effdeb4b72..32b1842beb 100644 --- a/docs/src/modules/ROOT/pages/optimization-algorithms/overview.adoc +++ b/docs/src/modules/ROOT/pages/optimization-algorithms/overview.adoc @@ -577,7 +577,7 @@ the solver improved the solution and how much time has passed without any improv 0.5 0.1 - 120 + 120 ---- @@ -596,7 +596,7 @@ setting the `stopFlatLineDetectionRatio` to `1.0` will lead to termination after [NOTE] ==== -The `Termination` process requires a minimum execution time, specified by `minimalExecutionTimeSeconds`. +The `Termination` process requires a minimum execution time, specified by `delayFlatLineSecondsSpentLimit`. ==== The solving process may identify no improvement periods that are not significant enough to trigger the termination.