Skip to content

Commit c60c5cd

Browse files
authored
chore: preparing for a minor release 1.22.1 (#1587)
1 parent ef242d3 commit c60c5cd

File tree

3 files changed

+56
-21
lines changed

3 files changed

+56
-21
lines changed

core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptListMoveIterator.java

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
import ai.timefold.solver.core.impl.heuristic.selector.common.iterator.UpcomingSelectionIterator;
1212
import ai.timefold.solver.core.impl.heuristic.selector.value.EntityIndependentValueSelector;
1313

14+
import org.jspecify.annotations.NullMarked;
15+
import org.jspecify.annotations.Nullable;
16+
17+
@NullMarked
1418
final class KOptListMoveIterator<Solution_, Node_> extends UpcomingSelectionIterator<Move<Solution_>> {
1519

1620
private final Random workingRandom;
@@ -96,7 +100,7 @@ private Iterator<Node_> getValuesOnSelectedEntitiesIterator(Node_[] pickedValues
96100
}
97101

98102
@SuppressWarnings("unchecked")
99-
private KOptDescriptor<Node_> pickKOptMove(int k) {
103+
private @Nullable KOptDescriptor<Node_> pickKOptMove(int k) {
100104
// The code in the paper used 1-index arrays
101105
var pickedValues = (Node_[]) new Object[2 * k + 1];
102106
var originIterator = (Iterator<Node_>) originSelector.iterator();
@@ -135,18 +139,17 @@ private KOptDescriptor<Node_> pickKOptMove(int k) {
135139
}
136140
}
137141

138-
private KOptDescriptor<Node_> pickKOptMoveRec(Iterator<Node_> valueIterator,
139-
EntityOrderInfo entityOrderInfo,
140-
Node_[] pickedValues,
141-
int pickedSoFar,
142-
int k,
143-
boolean canSelectNewEntities) {
142+
private @Nullable KOptDescriptor<Node_> pickKOptMoveRec(Iterator<Node_> valueIterator, EntityOrderInfo entityOrderInfo,
143+
Node_[] pickedValues, int pickedSoFar, int k, boolean canSelectNewEntities) {
144144
var previousRemovedEdgeEndpoint = pickedValues[2 * pickedSoFar - 2];
145145
Node_ nextRemovedEdgePoint, nextRemovedEdgeOppositePoint;
146146

147147
var remainingAttempts = (k - pickedSoFar + 3) * 2;
148148
while (remainingAttempts > 0) {
149-
nextRemovedEdgePoint = valueIterator.next();
149+
nextRemovedEdgePoint = getNextNodeOrNull(valueIterator);
150+
if (nextRemovedEdgePoint == null) {
151+
return null;
152+
}
150153
var newEntityOrderInfo =
151154
entityOrderInfo.withNewNode(nextRemovedEdgePoint, listVariableStateSupply);
152155
while (nextRemovedEdgePoint == getNodePredecessor(newEntityOrderInfo, previousRemovedEdgeEndpoint) ||
@@ -161,7 +164,10 @@ && isEdgeAlreadyDeleted(pickedValues, nextRemovedEdgePoint,
161164
if (remainingAttempts == 0) {
162165
return null;
163166
}
164-
nextRemovedEdgePoint = valueIterator.next();
167+
nextRemovedEdgePoint = getNextNodeOrNull(valueIterator);
168+
if (nextRemovedEdgePoint == null) {
169+
return null;
170+
}
165171
newEntityOrderInfo =
166172
entityOrderInfo.withNewNode(nextRemovedEdgePoint, listVariableStateSupply);
167173
remainingAttempts--;
@@ -191,24 +197,19 @@ && isEdgeAlreadyDeleted(pickedValues, nextRemovedEdgePoint,
191197
}
192198

193199
if (pickedSoFar < k) {
194-
var descriptor = pickKOptMoveRec(valueIterator, newEntityOrderInfo, pickedValues,
195-
pickedSoFar + 1, k, canSelectNewEntities);
200+
var descriptor = pickKOptMoveRec(valueIterator, newEntityOrderInfo, pickedValues, pickedSoFar + 1, k,
201+
canSelectNewEntities);
196202
if (descriptor != null && descriptor.isFeasible(minK, maxCyclesPatchedInInfeasibleMove)) {
197203
return descriptor;
198204
}
199205
} else {
200-
var descriptor = new KOptDescriptor<Node_>(pickedValues,
201-
KOptUtils.getMultiEntitySuccessorFunction(pickedValues,
202-
listVariableStateSupply),
203-
KOptUtils.getMultiEntityBetweenPredicate(pickedValues,
204-
listVariableStateSupply));
206+
var descriptor = new KOptDescriptor<>(pickedValues,
207+
KOptUtils.getMultiEntitySuccessorFunction(pickedValues, listVariableStateSupply),
208+
KOptUtils.getMultiEntityBetweenPredicate(pickedValues, listVariableStateSupply));
205209
if (descriptor.isFeasible(minK, maxCyclesPatchedInInfeasibleMove)) {
206210
return descriptor;
207211
} else {
208-
descriptor = patchCycles(
209-
descriptor,
210-
newEntityOrderInfo, pickedValues,
211-
pickedSoFar);
212+
descriptor = patchCycles(descriptor, newEntityOrderInfo, pickedValues, pickedSoFar);
212213
if (descriptor.isFeasible(minK, maxCyclesPatchedInInfeasibleMove)) {
213214
return descriptor;
214215
}
@@ -218,6 +219,16 @@ && isEdgeAlreadyDeleted(pickedValues, nextRemovedEdgePoint,
218219
return null;
219220
}
220221

222+
private @Nullable Node_ getNextNodeOrNull(Iterator<Node_> iterator) {
223+
if (!iterator.hasNext()) {
224+
return null;
225+
}
226+
// This may still be null.
227+
// Either due to filtering the underlying iterator,
228+
// or due to the underlying iterator returning null.
229+
return iterator.next();
230+
}
231+
221232
KOptDescriptor<Node_> patchCycles(KOptDescriptor<Node_> descriptor, EntityOrderInfo entityOrderInfo,
222233
Node_[] oldRemovedEdges, int k) {
223234
Node_ s1, s2;

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,14 @@ public SolverBridgePhaseTermination(SolverTermination<Solution_> solverTerminati
3737
}
3838

3939
@Override
40+
@SuppressWarnings({ "rawtypes", "unchecked" })
4041
public boolean isPhaseTerminated(AbstractPhaseScope<Solution_> phaseScope) {
41-
return solverTermination.isSolverTerminated(phaseScope.getSolverScope());
42+
var terminated = solverTermination.isSolverTerminated(phaseScope.getSolverScope());
43+
// If the solver is not finished yet, we need to check the phase termination
44+
if (!terminated && solverTermination instanceof PhaseTermination phaseTermination) {
45+
return phaseTermination.isPhaseTerminated(phaseScope);
46+
}
47+
return terminated;
4248
}
4349

4450
@Override

core/src/test/java/ai/timefold/solver/core/impl/solver/termination/TerminationTest.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,24 @@ void terminateEarlyLocalSearchInterrupted(TerminationConfig terminationConfig) {
329329
assertThat(resultingSolution).isNotNull();
330330
}
331331

332+
@Test
333+
void mixedSolverPhaseTerminations() {
334+
var solverConfig = new SolverConfig()
335+
.withSolutionClass(TestdataSolution.class)
336+
.withEntityClasses(TestdataEntity.class)
337+
.withEasyScoreCalculatorClass(TestdataEasyScoreCalculator.class)
338+
.withTerminationConfig(new TerminationConfig().withSpentLimit(Duration.ofHours(1)))
339+
.withPhases(new LocalSearchPhaseConfig()
340+
.withTerminationConfig(new TerminationConfig().withStepCountLimit(4)));
341+
var solution = TestdataSolution.generateSolution(2, 2);
342+
var solver = SolverFactory.<TestdataSolution> create(solverConfig)
343+
.buildSolver();
344+
var bestSolution = solver.solve(solution);
345+
// The global spent limit is one hour, but the phase limit is four steps.
346+
// The solver process is finished after 4 steps
347+
assertThat(bestSolution).isNotNull();
348+
}
349+
332350
static class TerminationArgumentSource implements ArgumentsProvider {
333351

334352
@Override

0 commit comments

Comments
 (0)