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 extends Termination> 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));
}
}