Skip to content

Commit 17c7556

Browse files
fix: phases should always check if the solver terminated early (#1456)
Fixes a regression where terminateEarly was not checked until the end of a phase. Previously, the top level termination was always BasicPlumbingTermination, which checks for/sets the terminateEarly flag. However, with the refactor to terminations, phases use different termination instances (that are not instances of BasicPlumbingTermination). None of these terminations check terminateEarly before, since that was the job of the BasicPlumbingTermination. We fix that by making plumbing terminations always check for solver termination. Note: this PR is #1455 with additional tests. --------- Co-authored-by: Lukáš Petrovický <[email protected]>
1 parent 90f02f4 commit 17c7556

File tree

11 files changed

+240
-92
lines changed

11 files changed

+240
-92
lines changed

core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhaseFactory.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,10 @@ protected PhaseTermination<Solution_> buildPhaseTermination(HeuristicConfigPolic
4040
var phaseTermination = PhaseTermination.bridge(solverTermination);
4141
var resultingTermination = TerminationFactory.<Solution_> create(terminationConfig_)
4242
.buildTermination(configPolicy, phaseTermination);
43-
var inapplicableTerminationList = resultingTermination instanceof UniversalTermination<Solution_> universalTermination
44-
? universalTermination.getPhaseTerminationsInapplicableTo(getPhaseScopeClass())
45-
: Collections.emptyList();
43+
var inapplicableTerminationList = !(this instanceof NoChangePhaseFactory<?>) &&
44+
resultingTermination instanceof UniversalTermination<Solution_> universalTermination
45+
? universalTermination.getPhaseTerminationsInapplicableTo(getPhaseScopeClass())
46+
: Collections.emptyList();
4647
var phaseName = this.getClass().getSimpleName()
4748
.replace("PhaseFactory", "")
4849
.replace("Default", "");

core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ public <Score_ extends Score<Score_>> InnerScoreDirectorFactory<Solution_, Score
136136
var phaseList = buildPhaseList(configPolicy, bestSolutionRecaller, termination);
137137

138138
return new DefaultSolver<>(environmentMode, randomFactory, bestSolutionRecaller, basicPlumbingTermination,
139-
UniversalTermination.bridge(termination), phaseList, solverScope,
139+
(UniversalTermination<Solution_>) termination, phaseList, solverScope,
140140
moveThreadCount == null ? SolverConfig.MOVE_THREAD_COUNT_NONE : Integer.toString(moveThreadCount));
141141
}
142142

core/src/main/java/ai/timefold/solver/core/impl/solver/termination/AbstractSolverTermination.java

Lines changed: 0 additions & 22 deletions
This file was deleted.

core/src/main/java/ai/timefold/solver/core/impl/solver/termination/AbstractTermination.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
@NullMarked
88
abstract sealed class AbstractTermination<Solution_>
99
implements Termination<Solution_>
10-
permits AbstractPhaseTermination, AbstractUniversalTermination, AbstractSolverTermination {
10+
permits AbstractPhaseTermination, AbstractUniversalTermination {
1111

1212
protected final transient Logger logger = LoggerFactory.getLogger(getClass());
1313

core/src/main/java/ai/timefold/solver/core/impl/solver/termination/AbstractUniversalTermination.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
abstract sealed class AbstractUniversalTermination<Solution_>
1111
extends AbstractTermination<Solution_>
1212
implements UniversalTermination<Solution_>
13-
permits AbstractCompositeTermination, BestScoreFeasibleTermination, BestScoreTermination, MoveCountTermination,
14-
ScoreCalculationCountTermination, SolverToUniversalBridgeTermination, TimeMillisSpentTermination,
13+
permits AbstractCompositeTermination, BasicPlumbingTermination, BestScoreFeasibleTermination, BestScoreTermination,
14+
ChildThreadPlumbingTermination, MoveCountTermination, ScoreCalculationCountTermination, TimeMillisSpentTermination,
1515
UnimprovedTimeMillisSpentScoreDifferenceThresholdTermination, UnimprovedTimeMillisSpentTermination {
1616

1717
@Override

core/src/main/java/ai/timefold/solver/core/impl/solver/termination/BasicPlumbingTermination.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import java.util.concurrent.BlockingQueue;
66
import java.util.concurrent.LinkedBlockingQueue;
77

8+
import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope;
89
import ai.timefold.solver.core.impl.solver.change.ProblemChangeAdapter;
910
import ai.timefold.solver.core.impl.solver.scope.SolverScope;
1011
import ai.timefold.solver.core.impl.solver.thread.ChildThreadType;
@@ -17,7 +18,7 @@
1718
*/
1819
@NullMarked
1920
public final class BasicPlumbingTermination<Solution_>
20-
extends AbstractSolverTermination<Solution_>
21+
extends AbstractUniversalTermination<Solution_>
2122
implements ChildThreadSupportingTermination<Solution_, SolverScope<Solution_>> {
2223

2324
private final boolean daemon;
@@ -126,6 +127,16 @@ public double calculateSolverTimeGradient(SolverScope<Solution_> solverScope) {
126127
return -1.0; // Not supported
127128
}
128129

130+
@Override
131+
public boolean isPhaseTerminated(AbstractPhaseScope<Solution_> phaseScope) {
132+
return isSolverTerminated(phaseScope.getSolverScope());
133+
}
134+
135+
@Override
136+
public double calculatePhaseTimeGradient(AbstractPhaseScope<Solution_> phaseScope) {
137+
return calculateSolverTimeGradient(phaseScope.getSolverScope());
138+
}
139+
129140
@Override
130141
public Termination<Solution_> createChildThreadTermination(SolverScope<Solution_> solverScope,
131142
ChildThreadType childThreadType) {

core/src/main/java/ai/timefold/solver/core/impl/solver/termination/ChildThreadPlumbingTermination.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
package ai.timefold.solver.core.impl.solver.termination;
22

3+
import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope;
34
import ai.timefold.solver.core.impl.solver.scope.SolverScope;
45
import ai.timefold.solver.core.impl.solver.thread.ChildThreadType;
56

67
import org.jspecify.annotations.NullMarked;
78

89
@NullMarked
910
public final class ChildThreadPlumbingTermination<Solution_>
10-
extends AbstractSolverTermination<Solution_>
11+
extends AbstractUniversalTermination<Solution_>
1112
implements ChildThreadSupportingTermination<Solution_, SolverScope<Solution_>> {
1213

1314
private boolean terminateChildren = false;
@@ -38,6 +39,16 @@ public double calculateSolverTimeGradient(SolverScope<Solution_> solverScope) {
3839
return -1.0; // Not supported
3940
}
4041

42+
@Override
43+
public boolean isPhaseTerminated(AbstractPhaseScope<Solution_> phaseScope) {
44+
return isSolverTerminated(phaseScope.getSolverScope());
45+
}
46+
47+
@Override
48+
public double calculatePhaseTimeGradient(AbstractPhaseScope<Solution_> phaseScope) {
49+
return calculateSolverTimeGradient(phaseScope.getSolverScope());
50+
}
51+
4152
@Override
4253
public Termination<Solution_> createChildThreadTermination(SolverScope<Solution_> solverScope,
4354
ChildThreadType childThreadType) {

core/src/main/java/ai/timefold/solver/core/impl/solver/termination/SolverTermination.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
@NullMarked
1414
public sealed interface SolverTermination<Solution_>
1515
extends Termination<Solution_>, SolverLifecycleListener<Solution_>
16-
permits AbstractSolverTermination, MockableSolverTermination, UniversalTermination {
16+
permits MockableSolverTermination, UniversalTermination {
1717

1818
/**
1919
* Called by the {@link Solver} after every phase to determine if the search should stop.

core/src/main/java/ai/timefold/solver/core/impl/solver/termination/SolverToUniversalBridgeTermination.java

Lines changed: 0 additions & 52 deletions
This file was deleted.

core/src/main/java/ai/timefold/solver/core/impl/solver/termination/UniversalTermination.java

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,4 @@ default List<PhaseTermination<Solution_>> getPhaseTerminationList() {
5858
return new AndCompositeTermination<>(terminations);
5959
}
6060

61-
static <Solution_> UniversalTermination<Solution_> bridge(SolverTermination<Solution_> termination) {
62-
if (termination instanceof UniversalTermination<Solution_> universalTermination) {
63-
return universalTermination;
64-
} else {
65-
return new SolverToUniversalBridgeTermination<>(termination);
66-
}
67-
}
68-
6961
}

0 commit comments

Comments
 (0)