Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,7 @@ public void solve(SolverScope<Solution_> 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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -195,7 +195,7 @@ protected EntityDescriptor<Solution_> 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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@

public abstract sealed class AbstractExhaustiveSearchDecider<Solution_, Score_ extends Score<Score_>>
implements ExhaustiveSearchPhaseLifecycleListener<Solution_>
permits BasicExhaustiveSearchDecider, ListVariableExhaustiveSearchDecider, MixedVariableExhaustiveSearchDecider {
permits BasicVariableExhaustiveSearchDecider, ListVariableExhaustiveSearchDecider,
MixedVariableExhaustiveSearchDecider {

private static final Logger LOGGER = LoggerFactory.getLogger(AbstractExhaustiveSearchDecider.class);

Expand Down Expand Up @@ -123,9 +124,8 @@ protected void doMove(ExhaustiveSearchStepScope<Solution_> 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<Solution_> stepScope,
Expand Down Expand Up @@ -204,7 +204,7 @@ protected void fillLayerList(ExhaustiveSearchPhaseScope<Solution_> phaseScope) {

protected void initStartNode(ExhaustiveSearchPhaseScope<Solution_> phaseScope,
ExhaustiveSearchLayer layer) {
var startLayer = layer == null ? phaseScope.getLayerList().get(0) : layer;
var startLayer = layer == null ? phaseScope.getLayerList().getFirst() : layer;
var startNode = new ExhaustiveSearchNode<Solution_>(startLayer, null);

if (scoreBounderEnabled) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<Solution_, Score_ extends Score<Score_>>
public final class BasicVariableExhaustiveSearchDecider<Solution_, Score_ extends Score<Score_>>
extends AbstractExhaustiveSearchDecider<Solution_, Score_> {

public BasicExhaustiveSearchDecider(String logIndentation, BestSolutionRecaller<Solution_> bestSolutionRecaller,
public BasicVariableExhaustiveSearchDecider(String logIndentation, BestSolutionRecaller<Solution_> bestSolutionRecaller,
PhaseTermination<Solution_> termination, EntitySelector<Solution_> sourceEntitySelector,
ManualEntityMimicRecorder<Solution_> manualEntityMimicRecorder, MoveRepository<Solution_> moveRepository,
boolean scoreBounderEnabled, ScoreBounder<?> scoreBounder) {
Expand Down Expand Up @@ -65,34 +64,39 @@ public boolean isEntityReinitializable(Object entity) {
return reinitializeVariableCount > 0;
}

@SuppressWarnings("unchecked")
@Override
public void restoreWorkingSolution(ExhaustiveSearchStepScope<Solution_> stepScope,
boolean assertWorkingSolutionScoreFromScratch, boolean assertExpectedWorkingSolutionScore) {
var phaseScope = stepScope.getPhaseScope();
var oldNode = phaseScope.getLastCompletedStepScope().getExpandingNode();
var newNode = stepScope.getExpandingNode();
var oldMoveList = new ArrayList<Move<Solution_>>(oldNode.getDepth());
var newMoveList = new ArrayList<Move<Solution_>>(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<Move<Solution_>>(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.<Move<Solution_>> asList(moves);
var compositeMove = Moves.compose(restoreMoveList);
// Execute the move.
phaseScope.getScoreDirector().executeMove(compositeMove);
var startingStepScore = stepScope.<Score_> getStartingStepScore();
phaseScope.getSolutionDescriptor().setScore(phaseScope.getWorkingSolution(),
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -102,35 +101,56 @@ public boolean isEntityReinitializable(Object entity) {
public void restoreWorkingSolution(ExhaustiveSearchStepScope<Solution_> stepScope,
boolean assertWorkingSolutionScoreFromScratch, boolean assertExpectedWorkingSolutionScore) {
var phaseScope = stepScope.getPhaseScope();
//First, undo all previous changes
var undoNode = phaseScope.getLastCompletedStepScope().getExpandingNode();
var unassignMoveList = new ArrayList<Move<Solution_>>();
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<Move<Solution_>>();
while (assignNode.getMove() != null) {
assignMoveList.add(assignNode.getMove());
assignNode = assignNode.getParent();
}
Collections.reverse(assignMoveList);
var allMoves = new ArrayList<Move<Solution_>>(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());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The while loop logic seems better the current recursive method. Can we determine the size of the undo move array based on the layer of the last completed step? If we can know the size in advance, we can iterate just like the previous while loop.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sizing the array is not the main problem. Filling the array in reverse order is only possible with recursion; otherwise there reverse() call still needs to be there.

I argue recursion is not a problem here. The exhaustive algorithm will run out of heap much sooner than it runs out of stack. (I ran an experiment with TSP. 12 visits took 20 seconds, 14 visits already struggled for memory.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand your point. The ES method isn't suitable for some problems. However, I believe we can use the while loop and the array type to create the move list, which simplifies the logic.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how you want me to do that, other than to manually reverse the array at the end.

// 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.<Score_> calculateScore();
stepScope.getExpandingNode().setScore(score);
phaseScope.getSolutionDescriptor().setScore(phaseScope.getWorkingSolution(), score.raw());
}

private static <Solution_> Move<Solution_>[] listAllUndoMoves(ExhaustiveSearchNode<Solution_> node) {
return listAllUndoMoves(node, 0);
}

private static <Solution_> Move<Solution_>[] listAllUndoMoves(ExhaustiveSearchNode<Solution_> 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 <Solution_> Move<Solution_>[] listAllMovesInReverseOrder(ExhaustiveSearchNode<Solution_> node) {
return listAllMovesInReverseOrder(node, 0);
}

private static <Solution_> Move<Solution_>[] listAllMovesInReverseOrder(ExhaustiveSearchNode<Solution_> 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
// ************************************************************************
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ public class ExhaustiveSearchNode<Solution_> {
* @see ScoreBounder#calculateOptimisticBound(ScoreDirector, InnerScore)
*/
private InnerScore<?> optimisticBound;
private boolean expandable = false;

public ExhaustiveSearchNode(ExhaustiveSearchLayer layer, ExhaustiveSearchNode<Solution_> parent) {
this.layer = layer;
Expand Down Expand Up @@ -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
// ************************************************************************
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,29 +27,22 @@ public BreadthFirstNodeComparator(boolean scoreBounderEnabled) {
@Override
public int compare(ExhaustiveSearchNode<Solution_> a, ExhaustiveSearchNode<Solution_> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,16 @@ public DepthFirstNodeComparator(boolean scoreBounderEnabled) {
@Override
public int compare(ExhaustiveSearchNode<Solution_> a, ExhaustiveSearchNode<Solution_> 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
Expand All @@ -43,19 +38,14 @@ public int compare(ExhaustiveSearchNode<Solution_> a, ExhaustiveSearchNode<Solut
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;
}
}
// 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,26 +31,18 @@ public int compare(ExhaustiveSearchNode<Solution_> a, ExhaustiveSearchNode<Solut
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;
}
// 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,9 @@ public class OriginalOrderNodeComparator<Solution_> implements Comparator<Exhaus
@Override
public int compare(ExhaustiveSearchNode<Solution_> a, ExhaustiveSearchNode<Solution_> 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());
Expand Down
Loading
Loading