diff --git a/core/src/build/revapi-differences.json b/core/src/build/revapi-differences.json index 4ed7705772..58e0b12894 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\", \"flatLineDetectionRatio\", \"newCurveDetectionRatio\", \"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\", \"minimalExecutionTimeSeconds\", \"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 cbd6b4a212..99b90c854a 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,8 +39,8 @@ "unimprovedStepCountLimit", "scoreCalculationCountLimit", "moveCountLimit", - "flatLineDetectionRatio", - "newCurveDetectionRatio", + "stopFlatLineDetectionRatio", + "noStopFlatLineDetectionRatio", "minimalExecutionTimeSeconds", "terminationConfigList" }) @@ -81,8 +81,8 @@ public class TerminationConfig extends AbstractConfig { private Long moveCountLimit = null; - private Double flatLineDetectionRatio = null; - private Double newCurveDetectionRatio = null; + private Double stopFlatLineDetectionRatio = null; + private Double noStopFlatLineDetectionRatio = null; private Long minimalExecutionTimeSeconds = null; @XmlElement(name = "termination") @@ -264,20 +264,20 @@ public void setMoveCountLimit(@Nullable Long moveCountLimit) { this.moveCountLimit = moveCountLimit; } - public @Nullable Double getFlatLineDetectionRatio() { - return flatLineDetectionRatio; + public @Nullable Double getStopFlatLineDetectionRatio() { + return stopFlatLineDetectionRatio; } - public void setFlatLineDetectionRatio(@Nullable Double flatLineDetectionRatio) { - this.flatLineDetectionRatio = flatLineDetectionRatio; + public void setStopFlatLineDetectionRatio(@Nullable Double stopFlatLineDetectionRatio) { + this.stopFlatLineDetectionRatio = stopFlatLineDetectionRatio; } - public @Nullable Double getNewCurveDetectionRatio() { - return newCurveDetectionRatio; + public @Nullable Double getNoStopFlatLineDetectionRatio() { + return noStopFlatLineDetectionRatio; } - public void setNewCurveDetectionRatio(@Nullable Double newCurveDetectionRatio) { - this.newCurveDetectionRatio = newCurveDetectionRatio; + public void setNoStopFlatLineDetectionRatio(@Nullable Double noStopFlatLineDetectionRatio) { + this.noStopFlatLineDetectionRatio = noStopFlatLineDetectionRatio; } public @Nullable Long getMinimalExecutionTimeSeconds() { @@ -411,13 +411,13 @@ public TerminationConfig withTerminationClass(Class termi return this; } - public @NonNull TerminationConfig withFlatLineDetectionRatio(@NonNull Double flatLineDetectionRatio) { - this.flatLineDetectionRatio = flatLineDetectionRatio; + public @NonNull TerminationConfig withStopFlatLineDetectionRatio(@NonNull Double stopFlatLineDetectionRatio) { + this.stopFlatLineDetectionRatio = stopFlatLineDetectionRatio; return this; } - public @NonNull TerminationConfig withNewCurveDetectionRatio(@NonNull Double newCurveDetectionRatio) { - this.newCurveDetectionRatio = newCurveDetectionRatio; + public @NonNull TerminationConfig withNoStopFlatLineDetectionRatio(@NonNull Double noStopFlatLineDetectionRatio) { + this.noStopFlatLineDetectionRatio = noStopFlatLineDetectionRatio; return this; } @@ -535,8 +535,8 @@ public boolean isConfigured() { unimprovedStepCountLimit != null || scoreCalculationCountLimit != null || moveCountLimit != null || - flatLineDetectionRatio != null || - newCurveDetectionRatio != null || + stopFlatLineDetectionRatio != null || + noStopFlatLineDetectionRatio != null || minimalExecutionTimeSeconds != null || isTerminationListConfigured(); } @@ -578,10 +578,10 @@ private boolean isTerminationListConfigured() { inheritedConfig.getScoreCalculationCountLimit()); moveCountLimit = ConfigUtils.inheritOverwritableProperty(moveCountLimit, inheritedConfig.getMoveCountLimit()); - flatLineDetectionRatio = ConfigUtils.inheritOverwritableProperty(flatLineDetectionRatio, - inheritedConfig.getFlatLineDetectionRatio()); - newCurveDetectionRatio = ConfigUtils.inheritOverwritableProperty(newCurveDetectionRatio, - inheritedConfig.getNewCurveDetectionRatio()); + stopFlatLineDetectionRatio = ConfigUtils.inheritOverwritableProperty(stopFlatLineDetectionRatio, + inheritedConfig.getStopFlatLineDetectionRatio()); + noStopFlatLineDetectionRatio = ConfigUtils.inheritOverwritableProperty(noStopFlatLineDetectionRatio, + inheritedConfig.getNoStopFlatLineDetectionRatio()); minimalExecutionTimeSeconds = ConfigUtils.inheritOverwritableProperty(minimalExecutionTimeSeconds, inheritedConfig.getMinimalExecutionTimeSeconds()); terminationConfigList = ConfigUtils.inheritMergeableListConfig( 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 4471931365..d3c7253b62 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 @@ -86,9 +86,11 @@ public > Termination buildTermination( if (terminationConfig.getMoveCountLimit() != null) { terminationList.add(new MoveCountTermination<>(terminationConfig.getMoveCountLimit())); } - if (terminationConfig.getFlatLineDetectionRatio() != null) { - terminationList.add(new UnimprovedBestSolutionTermination<>(terminationConfig.getFlatLineDetectionRatio(), - terminationConfig.getNewCurveDetectionRatio(), terminationConfig.getMinimalExecutionTimeSeconds())); + if (terminationConfig.getStopFlatLineDetectionRatio() != null + || terminationConfig.getNoStopFlatLineDetectionRatio() != null + || terminationConfig.getMinimalExecutionTimeSeconds() != null) { + terminationList.add(new UnimprovedBestSolutionTermination<>(terminationConfig.getStopFlatLineDetectionRatio(), + terminationConfig.getNoStopFlatLineDetectionRatio(), terminationConfig.getMinimalExecutionTimeSeconds())); } 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 5c7c5543a1..ddaf1f5ad6 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 @@ -17,15 +17,15 @@ public final class UnimprovedBestSolutionTermination extends Abstract // 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, + private final double stopFlatLineDetectionRatio; + // This criterion functions similarly to the stopFlatLineDetectionRatio, // 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 + // it indicates that the previous duration was not enough to terminate the process, + // but it 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 double noStopFlatLineDetectionRatio; 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, @@ -37,33 +37,33 @@ public final class UnimprovedBestSolutionTermination extends Abstract protected boolean waitForFirstBestScore; protected Boolean terminate; - public UnimprovedBestSolutionTermination(Double flatLineDetectionRatio, - Double newCurveDetectionRatio, Long minimalExecutionTimeSeconds) { - this(flatLineDetectionRatio, newCurveDetectionRatio, minimalExecutionTimeSeconds, Clock.systemUTC()); + public UnimprovedBestSolutionTermination(Double stopFlatLineDetectionRatio, + Double noStopFlatLineDetectionRatio, Long minimalExecutionTimeSeconds) { + this(stopFlatLineDetectionRatio, noStopFlatLineDetectionRatio, minimalExecutionTimeSeconds, Clock.systemUTC()); } - public UnimprovedBestSolutionTermination(Double flatLineDetectionRatio, Double newCurveDetectionRatio, - Long minimalExecutionTimeSeconds, 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"); + public UnimprovedBestSolutionTermination(Double stopFlatLineDetectionRatio, Double noStopFlatLineDetectionRatio, + Long minimalExecutionTimeSeconds, 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.clock = Objects.requireNonNull(clock); - if (flatLineDetectionRatio < 0) { + if (stopFlatLineDetectionRatio < 0) { throw new IllegalArgumentException( - "The flatLineDetectionRatio (%.2f) cannot be negative.".formatted(flatLineDetectionRatio)); + "The stopFlatLineDetectionRatio (%.2f) cannot be negative.".formatted(stopFlatLineDetectionRatio)); } - if (newCurveDetectionRatio < 0) { + if (noStopFlatLineDetectionRatio < 0) { throw new IllegalArgumentException( - "The newCurveDetectionRatio (%.2f) cannot be negative.".formatted(newCurveDetectionRatio)); + "The noStopFlatLineDetectionRatio (%.2f) cannot be negative.".formatted(noStopFlatLineDetectionRatio)); } - if (newCurveDetectionRatio > flatLineDetectionRatio) { + if (noStopFlatLineDetectionRatio > stopFlatLineDetectionRatio) { throw new IllegalArgumentException( - "The newCurveDetectionRatio (%.2f) cannot be greater than flatLineDetectionRatio (%.2f)." - .formatted(newCurveDetectionRatio, flatLineDetectionRatio)); + "The noStopFlatLineDetectionRatio (%.2f) cannot be greater than stopFlatLineDetectionRatio (%.2f)." + .formatted(noStopFlatLineDetectionRatio, stopFlatLineDetectionRatio)); } if (minimalExecutionTimeSeconds <= 0) { throw new IllegalArgumentException( @@ -130,8 +130,8 @@ public boolean isPhaseTerminated(AbstractPhaseScope phaseScope) { // 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(lastImprovementInterval * newCurveDetectionRatio); - var maxInterval = Math.floor(lastImprovementInterval * flatLineDetectionRatio); + var minInterval = Math.floor(lastImprovementInterval * noStopFlatLineDetectionRatio); + var maxInterval = Math.floor(lastImprovementInterval * stopFlatLineDetectionRatio); if (lastImprovementMillis > 0 && completeInterval >= minimalExecutionTimeMillis && newInterval > minInterval && newInterval < maxInterval) { initialCurvePointMillis = lastImprovementMillis; @@ -150,7 +150,7 @@ public boolean isPhaseTerminated(AbstractPhaseScope phaseScope) { if (completeInterval < minimalExecutionTimeMillis) { return false; } - var maxInterval = Math.floor(lastImprovementInterval * flatLineDetectionRatio); + var maxInterval = Math.floor(lastImprovementInterval * stopFlatLineDetectionRatio); if (newInterval > maxInterval) { terminate = true; return true; @@ -185,12 +185,12 @@ public double calculatePhaseTimeGradient(AbstractPhaseScope phaseScop @Override public UnimprovedBestSolutionTermination createChildThreadTermination(SolverScope solverScope, ChildThreadType childThreadType) { - return new UnimprovedBestSolutionTermination<>(flatLineDetectionRatio, newCurveDetectionRatio, + return new UnimprovedBestSolutionTermination<>(stopFlatLineDetectionRatio, noStopFlatLineDetectionRatio, minimalExecutionTimeMillis, clock); } @Override public String toString() { - return "UnimprovedBestSolutionTermination(%.2f, %.2f)".formatted(flatLineDetectionRatio, newCurveDetectionRatio); + return "UnimprovedBestSolutionTermination(%.2f, %.2f)".formatted(stopFlatLineDetectionRatio, noStopFlatLineDetectionRatio); } } diff --git a/docs/src/modules/ROOT/images/optimization-algorithms/overview/terminationGrowthCurve.png b/docs/src/modules/ROOT/images/optimization-algorithms/overview/terminationGrowthCurve.png deleted file mode 100644 index f6557c90f0..0000000000 Binary files a/docs/src/modules/ROOT/images/optimization-algorithms/overview/terminationGrowthCurve.png and /dev/null differ diff --git a/docs/src/modules/ROOT/images/optimization-algorithms/overview/terminationMultipleFlatLines.png b/docs/src/modules/ROOT/images/optimization-algorithms/overview/terminationMultipleFlatLines.png new file mode 100644 index 0000000000..12213deb19 Binary files /dev/null and b/docs/src/modules/ROOT/images/optimization-algorithms/overview/terminationMultipleFlatLines.png differ diff --git a/docs/src/modules/ROOT/images/optimization-algorithms/overview/terminationMultipleGrowthCurve.png b/docs/src/modules/ROOT/images/optimization-algorithms/overview/terminationMultipleGrowthCurve.png deleted file mode 100644 index b5c6006839..0000000000 Binary files a/docs/src/modules/ROOT/images/optimization-algorithms/overview/terminationMultipleGrowthCurve.png and /dev/null differ diff --git a/docs/src/modules/ROOT/images/optimization-algorithms/overview/terminationSingleFlatLine.png b/docs/src/modules/ROOT/images/optimization-algorithms/overview/terminationSingleFlatLine.png new file mode 100644 index 0000000000..6b935b7dbe Binary files /dev/null and b/docs/src/modules/ROOT/images/optimization-algorithms/overview/terminationSingleFlatLine.png differ diff --git a/docs/src/modules/ROOT/pages/optimization-algorithms/overview.adoc b/docs/src/modules/ROOT/pages/optimization-algorithms/overview.adoc index bd3e8650cf..cb0445d15b 100644 --- a/docs/src/modules/ROOT/pages/optimization-algorithms/overview.adoc +++ b/docs/src/modules/ROOT/pages/optimization-algorithms/overview.adoc @@ -568,55 +568,70 @@ This `Termination` is usually combined with other terminations. [#unimprovedBestSolutionTermination] ==== `UnimprovedBestSolutionTermination` -`UnimprovedBestSolutionTermination` terminates when the solver cannot improve the best solution in an estimated time interval. -The estimation of the termination interval is based on the last time +`UnimprovedBestSolutionTermination` terminates the process when the solver cannot enhance the best solution over a time period. +The duration for this period is determined by the last time the solver improved the solution and how much time has passed without any improvements. [source,xml,options="nowrap"] ---- - 0.5 + 0.5 + 0.1 + 120 ---- Let's consider a solution process based on the following image. -image::optimization-algorithms/overview/terminationGrowthCurve.png[align="center"] +image::optimization-algorithms/overview/terminationSingleFlatLine.png[align="center"] -In the previous image, the estimated unimproved time is represented by the flat red line. -With a configuration for `unimprovedBestSolutionLimit` set to `0.5`, +In the previous image, the unimproved time is represented by the flat red line. +With the stop condition `stopFlatLineDetectionRatio` set to `0.5`, and considering that the last best score was achieved at `10 seconds`, -the solver will terminate the process if no improvement is found within a maximum of 10 seconds. -This `10-seconds` limit is calculated -by multiplying the estimated total time (`20 seconds`) by the `unimprovedBestSolutionLimit` (`0.5`). - +the solver will terminate the process if no improvement is found within a maximum of 5 seconds. +This `5-seconds` limit is calculated +by multiplying the last improvement time (`10 seconds`) by the `stopFlatLineDetectionRatio` (`0.5`). +Similarly, +setting the `stopFlatLineDetectionRatio` to `1.0` will lead to termination after `10 seconds` if there is no improvement. [NOTE] ==== -This `Termination` configuration includes some built-in properties: - -* The `Termination` process requires the growth curve to last at least `10 seconds`, -meaning that the solver will not finish until approximately `10 seconds` after the growth curve evaluation begins. -* The estimated time is determined by the speed of move evaluation. -As the evaluation speed changes during the solving process, -the conditions for ending the solver will also be adjusted. -This means that the flat line may appear longer when analyzing a growth curve chart. +The `Termination` process requires a minimum execution time, specified by `minimalExecutionTimeSeconds`. ==== -Now, let's discuss the problem-solving process involving multiple growth curves: +The solving process may identify points for improvement after periods of no progress +that are not significant enough to trigger the termination. +These periods are referred to as non-stop periods, which are defined by the `noStopFlatLineDetectionRatio`. +Let's analyze the next image: -image::optimization-algorithms/overview/terminationMultipleGrowthCurve.png[align="center"] +image::optimization-algorithms/overview/terminationMultipleFlatLines.png[align="center"] -The termination process will track the solver's progress to identify various growth curves. -Each time a new curve is identified, the newly identified curve is used to trigger the termination process. -This means -that the previously explained interval calculation will only consider the last curve rather than the entire flow. +The orange flat line represents a non-stop period. +By setting the `stopFlatLineDetectionRatio` to `0.5` and the `noStopFlatLineDetectionRatio` to `0.1`, +we can calculate the unimproved stop period as `8 * 0.5 = 4s`, +while the unimproved non-stop period is calculated as `8 * 0.1 = ~1s`. +The difference between the solutions found at `8 seconds` and `10 seconds` is `2 seconds`. +A flat line is accepted if the time difference between the solutions is greater +than or equal to the non-stop period and less than the stop period. -The previous chart displays a red line that is smaller than the size needed to trigger termination, -but it is still significant enough to indicate the beginning of a new curve. -The termination threshold is calculated as `80%` of the size of the flat line that would end the solving process. +Accepting a non-stop flat line means adjusting the start time to the last improvement time. +In the previous example, the start time will change from `0` to `8` seconds. -This `Termination` is usually combined with other terminations. +As more time passes, the flat line must be longer to trigger termination. +Beginning at `0 seconds`, +the termination criterion will only be activated after `15 seconds` of no improvement: `30s * 0.5 = 15s`. +If we start counting from `8 seconds`, the time allowed for unimproved performance adjusts to `(30s - 8s) * 0.5 = 11s`. + +Evaluating the stopping criterion only from the beginning of the solving process may prevent the solver from terminating, +as the necessary period without improvement tends to increase. +Adjusting the start time enables the solver +to advance its termination analysis when the solution remains unchanged for a specific duration. + +[NOTE] +==== +The minimum execution time must be met whenever the start time changes, +meaning the total executed time will be calculated from the new starting point. +==== [#stepCountTermination] ==== `StepCountTermination`