diff --git a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhase.java index 8aad9aea447..e59d229a952 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhase.java @@ -63,8 +63,7 @@ public void solve(SolverScope solverScope) { while (!expandableNodeQueue.isEmpty() && !phaseTermination.isPhaseTerminated(phaseScope)) { var stepScope = new ExhaustiveSearchStepScope<>(phaseScope); - var node = expandableNodeQueue.last(); - expandableNodeQueue.remove(node); + var node = expandableNodeQueue.removeLast(); stepScope.setExpandingNode(node); stepStarted(stepScope); decider.restoreWorkingSolution(stepScope, assertWorkingSolutionScoreFromScratch, diff --git a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhaseFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhaseFactory.java index 929ebf9362f..5f0997bb876 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhaseFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhaseFactory.java @@ -26,7 +26,7 @@ import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.exhaustivesearch.decider.AbstractExhaustiveSearchDecider; -import ai.timefold.solver.core.impl.exhaustivesearch.decider.BasicExhaustiveSearchDecider; +import ai.timefold.solver.core.impl.exhaustivesearch.decider.BasicVariableExhaustiveSearchDecider; import ai.timefold.solver.core.impl.exhaustivesearch.decider.ListVariableExhaustiveSearchDecider; import ai.timefold.solver.core.impl.exhaustivesearch.decider.MixedVariableExhaustiveSearchDecider; import ai.timefold.solver.core.impl.exhaustivesearch.node.bounder.TrendBasedScoreBounder; @@ -195,7 +195,7 @@ protected EntityDescriptor deduceEntityDescriptor(SolutionDescriptor< termination, sourceEntitySelector, manualEntityMimicRecorder, new MoveSelectorBasedMoveRepository<>(moveSelector), scoreBounderEnabled, scoreBounder); } else { - decider = new BasicExhaustiveSearchDecider<>(configPolicy.getLogIndentation(), bestSolutionRecaller, + decider = new BasicVariableExhaustiveSearchDecider<>(configPolicy.getLogIndentation(), bestSolutionRecaller, termination, sourceEntitySelector, manualEntityMimicRecorder, new MoveSelectorBasedMoveRepository<>(moveSelector), scoreBounderEnabled, scoreBounder); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/AbstractExhaustiveSearchDecider.java b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/AbstractExhaustiveSearchDecider.java index bf63f096699..ac5bd431722 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/AbstractExhaustiveSearchDecider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/AbstractExhaustiveSearchDecider.java @@ -24,7 +24,8 @@ public abstract sealed class AbstractExhaustiveSearchDecider> implements ExhaustiveSearchPhaseLifecycleListener - permits BasicExhaustiveSearchDecider, ListVariableExhaustiveSearchDecider, MixedVariableExhaustiveSearchDecider { + permits BasicVariableExhaustiveSearchDecider, ListVariableExhaustiveSearchDecider, + MixedVariableExhaustiveSearchDecider { private static final Logger LOGGER = LoggerFactory.getLogger(AbstractExhaustiveSearchDecider.class); @@ -123,9 +124,8 @@ protected void doMove(ExhaustiveSearchStepScope stepScope, Exhaustive } } var nodeScore = moveNode.getScore(); - LOGGER.trace("{} Move treeId ({}), score ({}), expandable ({}), move ({}).", - logIndentation, executionPoint.treeId(), nodeScore == null ? "null" : nodeScore, moveNode.isExpandable(), - moveNode.getMove()); + LOGGER.trace("{} Move treeId ({}), score ({}), move ({}).", + logIndentation, executionPoint.treeId(), nodeScore == null ? "null" : nodeScore, moveNode.getMove()); } private void processMove(ExhaustiveSearchStepScope stepScope, @@ -204,7 +204,7 @@ protected void fillLayerList(ExhaustiveSearchPhaseScope phaseScope) { protected void initStartNode(ExhaustiveSearchPhaseScope phaseScope, ExhaustiveSearchLayer layer) { - var startLayer = layer == null ? phaseScope.getLayerList().get(0) : layer; + var startLayer = layer == null ? phaseScope.getLayerList().getFirst() : layer; var startNode = new ExhaustiveSearchNode(startLayer, null); if (scoreBounderEnabled) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/BasicExhaustiveSearchDecider.java b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/BasicVariableExhaustiveSearchDecider.java similarity index 82% rename from core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/BasicExhaustiveSearchDecider.java rename to core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/BasicVariableExhaustiveSearchDecider.java index 30dec6ba1e5..beca8f35eff 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/BasicExhaustiveSearchDecider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/BasicVariableExhaustiveSearchDecider.java @@ -1,7 +1,6 @@ package ai.timefold.solver.core.impl.exhaustivesearch.decider; -import java.util.ArrayList; -import java.util.Collections; +import java.util.Arrays; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.impl.exhaustivesearch.node.ExhaustiveSearchNode; @@ -16,10 +15,10 @@ import ai.timefold.solver.core.preview.api.move.Move; import ai.timefold.solver.core.preview.api.move.builtin.Moves; -public final class BasicExhaustiveSearchDecider> +public final class BasicVariableExhaustiveSearchDecider> extends AbstractExhaustiveSearchDecider { - public BasicExhaustiveSearchDecider(String logIndentation, BestSolutionRecaller bestSolutionRecaller, + public BasicVariableExhaustiveSearchDecider(String logIndentation, BestSolutionRecaller bestSolutionRecaller, PhaseTermination termination, EntitySelector sourceEntitySelector, ManualEntityMimicRecorder manualEntityMimicRecorder, MoveRepository moveRepository, boolean scoreBounderEnabled, ScoreBounder scoreBounder) { @@ -65,34 +64,39 @@ public boolean isEntityReinitializable(Object entity) { return reinitializeVariableCount > 0; } + @SuppressWarnings("unchecked") @Override public void restoreWorkingSolution(ExhaustiveSearchStepScope stepScope, boolean assertWorkingSolutionScoreFromScratch, boolean assertExpectedWorkingSolutionScore) { var phaseScope = stepScope.getPhaseScope(); var oldNode = phaseScope.getLastCompletedStepScope().getExpandingNode(); var newNode = stepScope.getExpandingNode(); - var oldMoveList = new ArrayList>(oldNode.getDepth()); - var newMoveList = new ArrayList>(newNode.getDepth()); + var oldMoveArray = new Move[oldNode.getDepth()]; + var newMoveArray = new Move[newNode.getDepth()]; + var oldMoveCount = 0; + var newMoveCount = 0; while (oldNode != newNode) { var oldDepth = oldNode.getDepth(); var newDepth = newNode.getDepth(); if (oldDepth < newDepth) { - newMoveList.add(newNode.getMove()); + newMoveArray[newMoveArray.length - newMoveCount++ - 1] = newNode.getMove(); // Build this in reverse. newNode = newNode.getParent(); } else { - oldMoveList.add(oldNode.getUndoMove()); + oldMoveArray[oldMoveCount++] = oldNode.getUndoMove(); oldNode = oldNode.getParent(); } } - var restoreMoveList = new ArrayList>(oldMoveList.size() + newMoveList.size()); - restoreMoveList.addAll(oldMoveList); - Collections.reverse(newMoveList); - restoreMoveList.addAll(newMoveList); - if (restoreMoveList.isEmpty()) { - // No moves to restore, so the working solution is already correct + var totalCount = newMoveCount + oldMoveCount; + if (totalCount == 0) { + // No moves to restore, so the working solution is already correct. return; } + // Build a composite move of both arrays. + var moves = Arrays.copyOf(oldMoveArray, totalCount); + System.arraycopy(newMoveArray, newMoveArray.length - newMoveCount, moves, oldMoveCount, newMoveCount); + var restoreMoveList = Arrays.> asList(moves); var compositeMove = Moves.compose(restoreMoveList); + // Execute the move. phaseScope.getScoreDirector().executeMove(compositeMove); var startingStepScore = stepScope. getStartingStepScore(); phaseScope.getSolutionDescriptor().setScore(phaseScope.getWorkingSolution(), diff --git a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/ListVariableExhaustiveSearchDecider.java b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/ListVariableExhaustiveSearchDecider.java index 30b570c00be..dd72861a14c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/ListVariableExhaustiveSearchDecider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/ListVariableExhaustiveSearchDecider.java @@ -1,7 +1,6 @@ package ai.timefold.solver.core.impl.exhaustivesearch.decider; -import java.util.ArrayList; -import java.util.Collections; +import java.util.Arrays; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply; @@ -102,35 +101,56 @@ public boolean isEntityReinitializable(Object entity) { public void restoreWorkingSolution(ExhaustiveSearchStepScope stepScope, boolean assertWorkingSolutionScoreFromScratch, boolean assertExpectedWorkingSolutionScore) { var phaseScope = stepScope.getPhaseScope(); - //First, undo all previous changes - var undoNode = phaseScope.getLastCompletedStepScope().getExpandingNode(); - var unassignMoveList = new ArrayList>(); - while (undoNode.getUndoMove() != null) { - unassignMoveList.add(undoNode.getUndoMove()); - undoNode = undoNode.getParent(); - } - // Next, rebuild the solution starting from the current search element - var assignNode = stepScope.getExpandingNode(); - var assignMoveList = new ArrayList>(); - while (assignNode.getMove() != null) { - assignMoveList.add(assignNode.getMove()); - assignNode = assignNode.getParent(); - } - Collections.reverse(assignMoveList); - var allMoves = new ArrayList>(unassignMoveList.size() + assignMoveList.size()); - allMoves.addAll(unassignMoveList); - allMoves.addAll(assignMoveList); - if (allMoves.isEmpty()) { - // No moves to restore, so the working solution is already correct + //First, undo all previous changes. + var unassignMoves = listAllUndoMoves(phaseScope.getLastCompletedStepScope().getExpandingNode()); + // Next, rebuild the solution starting from the current search element. + var assignMoves = listAllMovesInReverseOrder(stepScope.getExpandingNode()); + var totalLength = unassignMoves.length + assignMoves.length; + if (totalLength == 0) { + // No moves to restore, so the working solution is already correct. return; } - var compositeMove = Moves.compose(allMoves); + // Build a composite move of both arrays. + var moves = Arrays.copyOf(unassignMoves, unassignMoves.length + assignMoves.length); + System.arraycopy(assignMoves, 0, moves, unassignMoves.length, assignMoves.length); + var compositeMove = Moves.compose(moves); + // Execute the move. phaseScope.getScoreDirector().executeMove(compositeMove); var score = phaseScope. calculateScore(); stepScope.getExpandingNode().setScore(score); phaseScope.getSolutionDescriptor().setScore(phaseScope.getWorkingSolution(), score.raw()); } + private static Move[] listAllUndoMoves(ExhaustiveSearchNode node) { + return listAllUndoMoves(node, 0); + } + + private static Move[] listAllUndoMoves(ExhaustiveSearchNode node, int depth) { + var undoMove = node.getUndoMove(); + if (undoMove != null) { + var array = listAllUndoMoves(node.getParent(), depth + 1); + array[depth] = undoMove; + return array; + } else { + return new Move[depth]; + } + } + + private static Move[] listAllMovesInReverseOrder(ExhaustiveSearchNode node) { + return listAllMovesInReverseOrder(node, 0); + } + + private static Move[] listAllMovesInReverseOrder(ExhaustiveSearchNode node, int depth) { + var move = node.getMove(); + if (move != null) { + var array = listAllMovesInReverseOrder(node.getParent(), depth + 1); + array[array.length - depth - 1] = move; + return array; + } else { + return new Move[depth]; + } + } + // ************************************************************************ // Lifecycle methods // ************************************************************************ diff --git a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/node/ExhaustiveSearchNode.java b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/node/ExhaustiveSearchNode.java index 7d83591c272..b86ec8ae332 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/node/ExhaustiveSearchNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/node/ExhaustiveSearchNode.java @@ -22,7 +22,6 @@ public class ExhaustiveSearchNode { * @see ScoreBounder#calculateOptimisticBound(ScoreDirector, InnerScore) */ private InnerScore optimisticBound; - private boolean expandable = false; public ExhaustiveSearchNode(ExhaustiveSearchLayer layer, ExhaustiveSearchNode parent) { this.layer = layer; @@ -80,14 +79,6 @@ public void setOptimisticBound(InnerScore optimisticBound) { this.optimisticBound = optimisticBound; } - public boolean isExpandable() { - return expandable; - } - - public void setExpandable(boolean expandable) { - this.expandable = expandable; - } - // ************************************************************************ // Calculated methods // ************************************************************************ diff --git a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/node/comparator/BreadthFirstNodeComparator.java b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/node/comparator/BreadthFirstNodeComparator.java index e257b9e77cc..2c4fc7207e2 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/node/comparator/BreadthFirstNodeComparator.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/node/comparator/BreadthFirstNodeComparator.java @@ -27,29 +27,22 @@ public BreadthFirstNodeComparator(boolean scoreBounderEnabled) { @Override public int compare(ExhaustiveSearchNode a, ExhaustiveSearchNode b) { // Investigate shallower nodes first - var aDepth = a.getDepth(); - var bDepth = b.getDepth(); - if (aDepth < bDepth) { - return 1; - } else if (aDepth > bDepth) { - return -1; + var depthComparison = Integer.compare(a.getDepth(), b.getDepth()); + if (depthComparison != 0) { + return -depthComparison; } // Investigate better score first (ignore initScore to avoid depth first ordering) Score aScore = a.getScore().raw(); Score bScore = b.getScore().raw(); var scoreComparison = aScore.compareTo(bScore); - if (scoreComparison < 0) { - return -1; - } else if (scoreComparison > 0) { - return 1; + if (scoreComparison != 0) { + return scoreComparison; } if (scoreBounderEnabled) { // Investigate better optimistic bound first var optimisticBoundComparison = a.getOptimisticBound().compareTo(b.getOptimisticBound()); - if (optimisticBoundComparison < 0) { - return -1; - } else if (optimisticBoundComparison > 0) { - return 1; + if (optimisticBoundComparison != 0) { + return optimisticBoundComparison; } } // No point to investigating higher parent breadth index first (no impact on the churn on workingSolution) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/node/comparator/DepthFirstNodeComparator.java b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/node/comparator/DepthFirstNodeComparator.java index 6b6489f2f35..a1f11481a66 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/node/comparator/DepthFirstNodeComparator.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/node/comparator/DepthFirstNodeComparator.java @@ -20,21 +20,16 @@ public DepthFirstNodeComparator(boolean scoreBounderEnabled) { @Override public int compare(ExhaustiveSearchNode a, ExhaustiveSearchNode b) { // Investigate deeper first - var aDepth = a.getDepth(); - var bDepth = b.getDepth(); - if (aDepth < bDepth) { - return -1; - } else if (aDepth > bDepth) { - return 1; + var depthComparison = Integer.compare(a.getDepth(), b.getDepth()); + if (depthComparison != 0) { + return depthComparison; } // Investigate better score first (ignore initScore as that's already done by investigate deeper first) Score aScore = a.getScore().raw(); Score bScore = b.getScore().raw(); var scoreComparison = aScore.compareTo(bScore); - if (scoreComparison < 0) { - return -1; - } else if (scoreComparison > 0) { - return 1; + if (scoreComparison != 0) { + return scoreComparison; } // Pitfall: score is compared before optimisticBound, because of this mixed ONLY_UP and ONLY_DOWN cases: // - Node a has score 0hard/20medium/-50soft and optimisticBound 0hard/+(infinity)medium/-50soft @@ -43,19 +38,14 @@ public int compare(ExhaustiveSearchNode a, ExhaustiveSearchNode 0) { - return 1; + if (optimisticBoundComparison != 0) { + return optimisticBoundComparison; } } // Investigate higher parent breadth index first (to reduce on the churn on workingSolution) - var aParentBreadth = a.getParentBreadth(); - var bParentBreadth = b.getParentBreadth(); - if (aParentBreadth < bParentBreadth) { - return -1; - } else if (aParentBreadth > bParentBreadth) { - return 1; + var parentBreadthComparison = Long.compare(a.getParentBreadth(), b.getParentBreadth()); + if (parentBreadthComparison != 0) { + return parentBreadthComparison; } // Investigate lower breadth index first (to respect ValueSortingManner) return Long.compare(b.getBreadth(), a.getBreadth()); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/node/comparator/OptimisticBoundFirstNodeComparator.java b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/node/comparator/OptimisticBoundFirstNodeComparator.java index 07af2abd173..b53f0c7e70b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/node/comparator/OptimisticBoundFirstNodeComparator.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/node/comparator/OptimisticBoundFirstNodeComparator.java @@ -31,26 +31,18 @@ public int compare(ExhaustiveSearchNode a, ExhaustiveSearchNode 0) { - return 1; + if (scoreComparison != 0) { + return scoreComparison; } // Investigate deeper first - var aDepth = a.getDepth(); - var bDepth = b.getDepth(); - if (aDepth < bDepth) { - return -1; - } else if (aDepth > bDepth) { - return 1; + var depthComparison = Integer.compare(a.getDepth(), b.getDepth()); + if (depthComparison != 0) { + return depthComparison; } // Investigate higher parent breadth index first (to reduce on the churn on workingSolution) - var aParentBreadth = a.getParentBreadth(); - var bParentBreadth = b.getParentBreadth(); - if (aParentBreadth < bParentBreadth) { - return -1; - } else if (aParentBreadth > bParentBreadth) { - return 1; + var parentBreadthComparison = Long.compare(a.getParentBreadth(), b.getParentBreadth()); + if (parentBreadthComparison != 0) { + return parentBreadthComparison; } // Investigate lower breadth index first (to respect ValueSortingManner) return Long.compare(b.getBreadth(), a.getBreadth()); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/node/comparator/OriginalOrderNodeComparator.java b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/node/comparator/OriginalOrderNodeComparator.java index e707769e214..488144fd92b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/node/comparator/OriginalOrderNodeComparator.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/node/comparator/OriginalOrderNodeComparator.java @@ -12,12 +12,9 @@ public class OriginalOrderNodeComparator implements Comparator a, ExhaustiveSearchNode b) { // Investigate deeper first - int aDepth = a.getDepth(); - int bDepth = b.getDepth(); - if (aDepth < bDepth) { - return -1; - } else if (aDepth > bDepth) { - return 1; + var depthComparison = Integer.compare(a.getDepth(), b.getDepth()); + if (depthComparison != 0) { + return depthComparison; } // Investigate lower breadth index first (to respect ValueSortingManner) return Long.compare(b.getBreadth(), a.getBreadth()); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/node/comparator/ScoreFirstNodeComparator.java b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/node/comparator/ScoreFirstNodeComparator.java index dc18bfc3c85..f19e4f75ae7 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/node/comparator/ScoreFirstNodeComparator.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/node/comparator/ScoreFirstNodeComparator.java @@ -24,33 +24,23 @@ public int compare(ExhaustiveSearchNode a, ExhaustiveSearchNode 0) { - return 1; + if (scoreComparison != 0) { + return scoreComparison; } // Investigate better optimistic bound first var optimisticBoundComparison = a.getOptimisticBound().compareTo(b.getOptimisticBound()); - if (optimisticBoundComparison < 0) { - return -1; - } else if (optimisticBoundComparison > 0) { - return 1; + if (optimisticBoundComparison != 0) { + return optimisticBoundComparison; } // Investigate deeper first - var aDepth = a.getDepth(); - var bDepth = b.getDepth(); - if (aDepth < bDepth) { - return -1; - } else if (aDepth > bDepth) { - return 1; + var depthComparison = Integer.compare(a.getDepth(), b.getDepth()); + if (depthComparison != 0) { + return depthComparison; } // Investigate higher parent breadth index first (to reduce on the churn on workingSolution) - var aParentBreadth = a.getParentBreadth(); - var bParentBreadth = b.getParentBreadth(); - if (aParentBreadth < bParentBreadth) { - return -1; - } else if (aParentBreadth > bParentBreadth) { - return 1; + var parentBreadth = Long.compare(a.getParentBreadth(), b.getParentBreadth()); + if (parentBreadth != 0) { + return parentBreadth; } // Investigate lower breadth index first (to respect ValueSortingManner) return Long.compare(b.getBreadth(), a.getBreadth()); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/scope/ExhaustiveSearchPhaseScope.java b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/scope/ExhaustiveSearchPhaseScope.java index 0f79d0c8caa..e6b0b5e094c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/scope/ExhaustiveSearchPhaseScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/scope/ExhaustiveSearchPhaseScope.java @@ -65,10 +65,6 @@ public void setLastCompletedStepScope(ExhaustiveSearchStepScope lastC // Calculated methods // ************************************************************************ - public int getDepthSize() { - return layerList.size(); - } - public > void registerPessimisticBound(InnerScore pessimisticBound) { var castBestPessimisticBound = this. getBestPessimisticBound(); if (pessimisticBound.compareTo(castBestPessimisticBound) > 0) { @@ -84,7 +80,6 @@ public > void registerPessimisticBound(InnerScore moveNode) { expandableNodeQueue.add(moveNode); - moveNode.setExpandable(true); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/iterator/ConcatenatingIterator.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/iterator/ConcatenatingIterator.java new file mode 100644 index 00000000000..4506a60d973 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/iterator/ConcatenatingIterator.java @@ -0,0 +1,50 @@ +package ai.timefold.solver.core.impl.heuristic.selector.common.iterator; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import org.jspecify.annotations.Nullable; + +public final class ConcatenatingIterator implements Iterator { + + private final Iterator> iterators; + private Iterator current; + private boolean hasNext = false; // Exists to support null values. + private @Nullable T next; + + @SafeVarargs + public ConcatenatingIterator(Iterator... iterators) { + this.iterators = Arrays.asList(iterators).iterator(); + this.current = Collections.emptyIterator(); + } + + @Override + public boolean hasNext() { + if (hasNext) { + return true; + } + while (!current.hasNext()) { + if (!iterators.hasNext()) { + return false; + } + current = iterators.next(); + } + hasNext = true; + next = current.next(); + return true; + } + + @Override + public T next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + var result = next; + hasNext = false; + next = null; + return result; + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/iterator/MappingIterator.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/iterator/MappingIterator.java new file mode 100644 index 00000000000..60ae0cf4208 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/iterator/MappingIterator.java @@ -0,0 +1,26 @@ +package ai.timefold.solver.core.impl.heuristic.selector.common.iterator; + +import java.util.Iterator; +import java.util.function.Function; + +public final class MappingIterator implements Iterator { + + private final Iterator source; + private final Function mapper; + + public MappingIterator(Iterator source, Function mapper) { + this.source = source; + this.mapper = mapper; + } + + @Override + public boolean hasNext() { + return source.hasNext(); + } + + @Override + public R next() { + return mapper.apply(source.next()); + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelector.java index f1abd111035..2380669f9cc 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelector.java @@ -5,15 +5,14 @@ import java.util.Collections; import java.util.Iterator; import java.util.Objects; -import java.util.Spliterators; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.heuristic.selector.AbstractSelector; +import ai.timefold.solver.core.impl.heuristic.selector.common.iterator.ConcatenatingIterator; +import ai.timefold.solver.core.impl.heuristic.selector.common.iterator.MappingIterator; import ai.timefold.solver.core.impl.heuristic.selector.entity.EntitySelector; import ai.timefold.solver.core.impl.heuristic.selector.value.IterableValueSelector; import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.FilteringValueSelector; @@ -136,27 +135,27 @@ public Iterator iterator() { } if (isExhaustiveSearch) { // The exhaustive search method requires elements to be placed only at the end of the entity's list - Stream stream = StreamSupport.stream(entitySelector.spliterator(), false) - .map(entity -> ElementPosition.of(entity, listVariableDescriptor.getListSize(entity))); - return stream.iterator(); + return new MappingIterator<>(entitySelector.iterator(), + entity -> ElementPosition.of(entity, listVariableDescriptor.getListSize(entity))); } else { // Start with the first unpinned value of each entity, or zero if no pinning. // Entity selector is guaranteed to return only unpinned entities. - Stream stream = StreamSupport.stream(entitySelector.spliterator(), false) - .map(entity -> ElementPosition.of(entity, listVariableDescriptor.getFirstUnpinnedIndex(entity))); + var entityIterator = new MappingIterator<>(entitySelector.iterator(), + entity -> ElementPosition.of(entity, listVariableDescriptor.getFirstUnpinnedIndex(entity))); // Filter guarantees that we only get values that are actually in one of the lists. // Value selector guarantees only unpinned values. - // Simplify tests. - stream = Stream.concat(stream, - StreamSupport.stream(valueSelector.spliterator(), false) - .map(v -> listVariableStateSupply.getElementPosition(v).ensureAssigned()) - .map(positionInList -> ElementPosition.of(positionInList.entity(), - positionInList.index() + 1))); - // If the list variable allows unassigned values, add the option of unassigning. + var valueIterator = new MappingIterator<>(valueSelector.iterator(), + v -> { + var pos = listVariableStateSupply.getElementPosition(v).ensureAssigned(); + return ElementPosition.of(pos.entity(), pos.index() + 1); + }); if (listVariableDescriptor.allowsUnassignedValues()) { - stream = Stream.concat(stream, Stream.of(ElementPosition.unassigned())); + // If the list variable allows unassigned values, add the option of unassigning. + return new ConcatenatingIterator<>(entityIterator, valueIterator, + Collections.singletonList(ElementPosition.unassigned()).iterator()); + } else { + return new ConcatenatingIterator<>(entityIterator, valueIterator); } - return stream.iterator(); } } } @@ -175,12 +174,7 @@ public EntityDescriptor getEntityDescriptor() { } public Iterator endingIterator() { - return Stream.concat( - StreamSupport.stream(Spliterators.spliterator(entitySelector.endingIterator(), entitySelector.getSize(), 0), - false), - StreamSupport.stream(Spliterators.spliterator(valueSelector.endingIterator(null), valueSelector.getSize(), 0), - false)) - .iterator(); + return new ConcatenatingIterator<>(entitySelector.endingIterator(), valueSelector.endingIterator(null)); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/EphemeralMoveDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/move/EphemeralMoveDirector.java index d895b665c9c..64babb707cd 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/EphemeralMoveDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/EphemeralMoveDirector.java @@ -29,7 +29,7 @@ final class EphemeralMoveDirector> } Move createUndoMove() { - return new RecordedUndoMove<>(getVariableChangeRecordingScoreDirector().copyChanges()); + return getVariableChangeRecordingScoreDirector().createUndoMove(); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/VariableChangeRecordingScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/move/VariableChangeRecordingScoreDirector.java index c7d03da58a5..0167d465715 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/VariableChangeRecordingScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/VariableChangeRecordingScoreDirector.java @@ -16,6 +16,7 @@ import ai.timefold.solver.core.impl.score.director.ScoreDirector; import ai.timefold.solver.core.impl.score.director.ValueRangeManager; import ai.timefold.solver.core.impl.score.director.VariableDescriptorCache; +import ai.timefold.solver.core.preview.api.move.Move; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -25,7 +26,7 @@ public final class VariableChangeRecordingScoreDirector { private final @Nullable InnerScoreDirector backingScoreDirector; - private final List> variableChanges; + private List> variableChanges; /* * The fromIndex of afterListVariableChanged must match the fromIndex of its beforeListVariableChanged call. @@ -66,9 +67,13 @@ private VariableChangeRecordingScoreDirector(@Nullable InnerScoreDirector> copyChanges() { - return List.copyOf(variableChanges); + public Move createUndoMove() { + // The list would normally be copied here to prevent any outside modification. + // However, copying this list on the hot path would be a major performance issue. + // Instead, the list is passed as a reference here, and instead of it being cleared by undoChanges(), + // the reference is replaced; that way, the move does not actually share the list with anyone, + // and copying of its contents can be avoided. + return new RecordedUndoMove<>(variableChanges); } @Override @@ -83,7 +88,7 @@ public void undoChanges() { changeAction.undo(backingScoreDirector); } Objects.requireNonNull(backingScoreDirector).triggerVariableListeners(); - variableChanges.clear(); + variableChanges = new LinkedList<>(); // Do not clear the list, as createUndoMove() may hold a reference to it. if (cache != null) { cache.clear(); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/RevertableScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/RevertableScoreDirector.java index 245b3c25d16..9bb59eedc4c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/RevertableScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/RevertableScoreDirector.java @@ -1,6 +1,7 @@ package ai.timefold.solver.core.impl.score.director; -import java.util.List; +import ai.timefold.solver.core.impl.move.MoveDirector; +import ai.timefold.solver.core.preview.api.move.Move; import org.jspecify.annotations.NullMarked; @@ -8,11 +9,14 @@ public interface RevertableScoreDirector extends VariableDescriptorAwareScoreDirector { /** - * Use this method to get a copy of all non-commited changes executed by the director so far. - * - * @param The action type for recorded changes + * Use this method to get a representation of the operation that will be performed by {@link #undoChanges()}. + * This operation will keep accumulating uncommitted changes until {@link #undoChanges()} is actually called. + * After that happens, any subsequent call to this method will result in a fresh move instance with only those + * operations that happened after the latest call to {@link #undoChanges()}. + * This is useful when the undo operation ever needs to be replayed manually; most use cases do not need this + * and should refer to {@link MoveDirector#executeTemporary(Move)}. */ - List copyChanges(); + Move createUndoMove(); /** * Use this method to revert all changes made by moves. diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/CompositeMove.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/CompositeMove.java index cab094cb235..60d776e468a 100644 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/CompositeMove.java +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/CompositeMove.java @@ -2,6 +2,7 @@ import java.util.Arrays; import java.util.LinkedHashSet; +import java.util.List; import java.util.Objects; import java.util.SequencedCollection; import java.util.stream.Collectors; @@ -26,28 +27,27 @@ public final class CompositeMove implements Move { /** - * @param moves never null, sometimes empty. Do not modify this argument afterwards or the CompositeMove corrupts. + * @param moveList Do not modify this argument afterwards or the CompositeMove corrupts. * @return never null */ - @SafeVarargs - static > Move buildMove(Move_... moves) { - return switch (moves.length) { + static Move buildMove(List> moveList) { + return switch (moveList.size()) { case 0 -> throw new UnsupportedOperationException("The %s cannot be built from an empty move list." .formatted(CompositeMove.class.getSimpleName())); - case 1 -> moves[0]; - default -> new CompositeMove<>(moves); + case 1 -> moveList.getFirst(); + default -> new CompositeMove<>(moveList); }; } - private final Move[] moves; + private final List> moveList; - private CompositeMove(Move[] moves) { - this.moves = moves; + private CompositeMove(List> moveList) { + this.moveList = moveList; } @Override public void execute(MutableSolutionView solutionView) { - for (var move : moves) { + for (var move : moveList) { move.execute(solutionView); } } @@ -55,17 +55,17 @@ public void execute(MutableSolutionView solutionView) { @SuppressWarnings("unchecked") @Override public Move rebase(Lookup lookup) { - Move[] rebasedMoves = new Move[moves.length]; - for (var i = 0; i < moves.length; i++) { - rebasedMoves[i] = moves[i].rebase(lookup); + Move[] rebasedMoves = new Move[moveList.size()]; + for (var i = 0; i < moveList.size(); i++) { + rebasedMoves[i] = moveList.get(i).rebase(lookup); } - return new CompositeMove<>(rebasedMoves); + return new CompositeMove<>(Arrays.asList(rebasedMoves)); } @Override public SequencedCollection getPlanningEntities() { - var entities = LinkedHashSet.newLinkedHashSet(moves.length * 2); - for (var move : moves) { + var entities = LinkedHashSet.newLinkedHashSet(moveList.size() * 2); + for (var move : moveList) { entities.addAll(move.getPlanningEntities()); } return entities; @@ -73,8 +73,8 @@ public SequencedCollection getPlanningEntities() { @Override public SequencedCollection getPlanningValues() { - var values = LinkedHashSet.newLinkedHashSet(moves.length * 2); - for (var move : moves) { + var values = LinkedHashSet.newLinkedHashSet(moveList.size() * 2); + for (var move : moveList) { values.addAll(move.getPlanningValues()); } return values; @@ -82,26 +82,26 @@ public SequencedCollection getPlanningValues() { @Override public String describe() { - return getClass().getSimpleName() + Arrays.stream(moves) + return getClass().getSimpleName() + moveList.stream() .map(Move::describe) .sorted() - .map(childMoveTypeDescription -> "* " + childMoveTypeDescription) + .map(childMoveTypeDescription -> "*" + childMoveTypeDescription) .collect(Collectors.joining(",", "(", ")")); } @Override public boolean equals(Object o) { return o instanceof CompositeMove that - && Objects.deepEquals(moves, that.moves); + && Objects.equals(moveList, that.moveList); } @Override public int hashCode() { - return Arrays.hashCode(moves); + return moveList.hashCode(); } @Override public String toString() { - return Arrays.toString(moves); + return moveList.toString(); } } diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/Moves.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/Moves.java index d88e0ee35df..c4ea208b80b 100644 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/Moves.java +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/move/builtin/Moves.java @@ -1,5 +1,6 @@ package ai.timefold.solver.core.preview.api.move.builtin; +import java.util.Arrays; import java.util.List; import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningListVariableMetaModel; @@ -72,7 +73,7 @@ public static Move compose(List> moves) { */ @SafeVarargs public static Move compose(Move... moves) { - return CompositeMove.buildMove(moves); + return CompositeMove.buildMove(Arrays.asList(moves)); } // ************************************************************************ diff --git a/core/src/test/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhaseTest.java b/core/src/test/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhaseTest.java index e3e252711ac..15ed56b7fc6 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhaseTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhaseTest.java @@ -16,7 +16,7 @@ import ai.timefold.solver.core.api.score.SimpleScore; import ai.timefold.solver.core.config.exhaustivesearch.ExhaustiveSearchPhaseConfig; -import ai.timefold.solver.core.impl.exhaustivesearch.decider.BasicExhaustiveSearchDecider; +import ai.timefold.solver.core.impl.exhaustivesearch.decider.BasicVariableExhaustiveSearchDecider; import ai.timefold.solver.core.impl.exhaustivesearch.decider.ListVariableExhaustiveSearchDecider; import ai.timefold.solver.core.impl.exhaustivesearch.node.ExhaustiveSearchLayer; import ai.timefold.solver.core.impl.exhaustivesearch.node.ExhaustiveSearchNode; @@ -88,7 +88,7 @@ void restoreWorkingSolutionForBasicVariable() { when(lastCompletedStepScope.getExpandingNode()).thenReturn(node3A); when(stepScope.getExpandingNode()).thenReturn(node4B); - var decider = new BasicExhaustiveSearchDecider("", null, null, + var decider = new BasicVariableExhaustiveSearchDecider("", null, null, mock(EntitySelector.class), null, null, false, null); decider.restoreWorkingSolution(stepScope, false, false);