From f10ca871e411f99a6f2fd17ec7196710fcb65017 Mon Sep 17 00:00:00 2001 From: Fred Date: Wed, 4 Sep 2024 17:43:32 -0300 Subject: [PATCH 01/26] feat: add move count metric --- .../core/config/solver/SolverConfig.java | 6 ++-- .../solver/monitoring/SolverMetric.java | 3 ++ .../DefaultConstructionHeuristicPhase.java | 5 +-- .../decider/ConstructionHeuristicDecider.java | 5 +++ .../impl/heuristic/move/AbstractMove.java | 2 +- .../move/AbstractSimplifiedMove.java | 2 +- .../heuristic/move/AbstractWeightMove.java | 25 +++++++++++++++ .../impl/heuristic/move/CompositeMove.java | 5 +++ .../solver/core/impl/heuristic/move/Move.java | 10 ++++++ .../impl/heuristic/move/RecordedUndoMove.java | 4 +++ ...nRecreateConstructionHeuristicDecider.java | 10 ++++++ .../localsearch/DefaultLocalSearchPhase.java | 3 +- .../impl/phase/scope/AbstractPhaseScope.java | 31 +++++++++++++++++-- .../score/director/AbstractScoreDirector.java | 17 ++++++++++ .../score/director/InnerScoreDirector.java | 6 ++++ .../core/impl/solver/DefaultSolver.java | 4 ++- .../core/impl/solver/scope/SolverScope.java | 22 +++++++++++-- core/src/main/resources/solver.xsd | 2 ++ .../core/impl/solver/DefaultSolverTest.java | 12 +++++++ 19 files changed, 160 insertions(+), 14 deletions(-) create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractWeightMove.java diff --git a/core/src/main/java/ai/timefold/solver/core/config/solver/SolverConfig.java b/core/src/main/java/ai/timefold/solver/core/config/solver/SolverConfig.java index 0198907be8..6570afe089 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/solver/SolverConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/solver/SolverConfig.java @@ -631,9 +631,9 @@ public DomainAccessType determineDomainAccessType() { public MonitoringConfig determineMetricConfig() { return Objects.requireNonNullElse(monitoringConfig, new MonitoringConfig().withSolverMetricList(Arrays.asList(SolverMetric.SOLVE_DURATION, SolverMetric.ERROR_COUNT, - SolverMetric.SCORE_CALCULATION_COUNT, SolverMetric.PROBLEM_ENTITY_COUNT, - SolverMetric.PROBLEM_VARIABLE_COUNT, SolverMetric.PROBLEM_VALUE_COUNT, - SolverMetric.PROBLEM_SIZE_LOG))); + SolverMetric.SCORE_CALCULATION_COUNT, SolverMetric.MOVE_CALCULATION_COUNT, + SolverMetric.PROBLEM_ENTITY_COUNT, SolverMetric.PROBLEM_VARIABLE_COUNT, + SolverMetric.PROBLEM_VALUE_COUNT, SolverMetric.PROBLEM_SIZE_LOG))); } // ************************************************************************ diff --git a/core/src/main/java/ai/timefold/solver/core/config/solver/monitoring/SolverMetric.java b/core/src/main/java/ai/timefold/solver/core/config/solver/monitoring/SolverMetric.java index 77b80b4624..d13bab0c66 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/solver/monitoring/SolverMetric.java +++ b/core/src/main/java/ai/timefold/solver/core/config/solver/monitoring/SolverMetric.java @@ -31,6 +31,9 @@ public enum SolverMetric { SCORE_CALCULATION_COUNT("timefold.solver.score.calculation.count", SolverScope::getScoreCalculationCount, false), + MOVE_CALCULATION_COUNT("timefold.solver.move.calculation.count", + SolverScope::getMoveCalculationCount, + false), PROBLEM_ENTITY_COUNT("timefold.solver.problem.entities", solverScope -> solverScope.getProblemSizeStatistics().entityCount(), false), diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java index e2103045f4..935e85bc19 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java @@ -146,13 +146,14 @@ public void phaseEnded(ConstructionHeuristicPhaseScope phaseScope) { decider.phaseEnded(phaseScope); phaseScope.endingNow(); if (decider.isLoggingEnabled() && logger.isInfoEnabled()) { - logger.info( - "{}Construction Heuristic phase ({}) ended: time spent ({}), best score ({}), score calculation speed ({}/sec), step total ({}).", + logger.info("{}Construction Heuristic phase ({}) ended: time spent ({}), best score ({})," + + " score calculation speed ({}/sec), move calculation speed ({}/sec), step total ({}).", logIndentation, phaseIndex, phaseScope.calculateSolverTimeMillisSpentUpToNow(), phaseScope.getBestScore(), phaseScope.getPhaseScoreCalculationSpeed(), + phaseScope.getPhaseMoveCalculationSpeed(), phaseScope.getNextStepIndex()); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/decider/ConstructionHeuristicDecider.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/decider/ConstructionHeuristicDecider.java index a36024b6c4..17842f8e42 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/decider/ConstructionHeuristicDecider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/decider/ConstructionHeuristicDecider.java @@ -103,6 +103,7 @@ public void decideNextStep(ConstructionHeuristicStepScope stepScope, // It will never do anything more complex than that. continue; } + prepare(move); var moveScope = new ConstructionHeuristicMoveScope<>(stepScope, moveIndex, move); moveIndex++; doMove(moveScope); @@ -146,4 +147,8 @@ protected > void doMove(ConstructionHeuristicMoveSc } } + protected void prepare(Move move) { + // Ignored by default + } + } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractMove.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractMove.java index 7f00c75282..d7600c3666 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractMove.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractMove.java @@ -15,7 +15,7 @@ * @param the solution type, the class with the {@link PlanningSolution} annotation * @see Move */ -public abstract class AbstractMove implements Move { +public abstract class AbstractMove extends AbstractWeightMove { @Override public final Move doMove(ScoreDirector scoreDirector) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractSimplifiedMove.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractSimplifiedMove.java index 764f4d0075..b689cd20bb 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractSimplifiedMove.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractSimplifiedMove.java @@ -11,7 +11,7 @@ * * @param */ -public abstract class AbstractSimplifiedMove implements Move { +public abstract class AbstractSimplifiedMove extends AbstractWeightMove { @Override public final Move doMove(ScoreDirector scoreDirector) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractWeightMove.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractWeightMove.java new file mode 100644 index 0000000000..7069f71d52 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractWeightMove.java @@ -0,0 +1,25 @@ +package ai.timefold.solver.core.impl.heuristic.move; + +import ai.timefold.solver.core.api.domain.solution.PlanningSolution; + +/** + * Abstract superclass for {@link Move} which includes the ability to add weight count that is used by the move count + * speed metric. + * + * @param the solution type, the class with the {@link PlanningSolution} annotation + * @see Move + * @see ai.timefold.solver.core.config.solver.monitoring.SolverMetric + */ +public abstract class AbstractWeightMove implements Move { + + private int weightCount = 1; + + @Override + public int getMoveWeightCount() { + return weightCount; + } + + public void setWeightCount(int weightCount) { + this.weightCount = weightCount; + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/CompositeMove.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/CompositeMove.java index 17ddb4385c..56cf5b4bab 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/CompositeMove.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/CompositeMove.java @@ -113,6 +113,11 @@ public CompositeMove rebase(ScoreDirector destinationScore return new CompositeMove<>(rebasedMoves); } + @Override + public int getMoveWeightCount() { + return moves.length; + } + // ************************************************************************ // Introspection methods // ************************************************************************ diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/Move.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/Move.java index 9e02afedbe..72b7675616 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/Move.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/Move.java @@ -173,4 +173,14 @@ default Collection getPlanningValues() { .formatted(getClass())); } + /** + * Return the weight count of a given move that contributes to the speed metric. + * Every move counts as one single move by default. + * + * @return 1 by default + */ + default int getMoveWeightCount() { + return 1; + } + } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/RecordedUndoMove.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/RecordedUndoMove.java index 4b35cbb385..54328c4987 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/RecordedUndoMove.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/RecordedUndoMove.java @@ -24,4 +24,8 @@ protected void doMoveOnGenuineVariables(ScoreDirector scoreDirector) scoreDirector.triggerVariableListeners(); } + @Override + public int getMoveWeightCount() { + return 0; + } } \ No newline at end of file diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicDecider.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicDecider.java index 653d03defb..3ba0d9871e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicDecider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicDecider.java @@ -3,6 +3,8 @@ import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.constructionheuristic.decider.ConstructionHeuristicDecider; import ai.timefold.solver.core.impl.constructionheuristic.decider.forager.ConstructionHeuristicForager; +import ai.timefold.solver.core.impl.heuristic.move.AbstractWeightMove; +import ai.timefold.solver.core.impl.heuristic.move.Move; import ai.timefold.solver.core.impl.solver.termination.Termination; final class RuinRecreateConstructionHeuristicDecider @@ -13,6 +15,14 @@ final class RuinRecreateConstructionHeuristicDecider super("", termination, forager); } + @Override + protected void prepare(Move move) { + if (move instanceof AbstractWeightMove weightMove) { + // The moves of R&R construction won't count as completed moves + weightMove.setWeightCount(0); + } + } + @Override public boolean isLoggingEnabled() { return false; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java index 79ff87f128..4f3d1b714e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java @@ -197,12 +197,13 @@ public void phaseEnded(LocalSearchPhaseScope phaseScope) { decider.phaseEnded(phaseScope); phaseScope.endingNow(); logger.info("{}Local Search phase ({}) ended: time spent ({}), best score ({})," - + " score calculation speed ({}/sec), step total ({}).", + + " score calculation speed ({}/sec), move calculation speed ({}/sec), step total ({}).", logIndentation, phaseIndex, phaseScope.calculateSolverTimeMillisSpentUpToNow(), phaseScope.getBestScore(), phaseScope.getPhaseScoreCalculationSpeed(), + phaseScope.getPhaseMoveCalculationSpeed(), phaseScope.getNextStepIndex()); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java index 85e94d6ee1..e1461e0344 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java @@ -24,10 +24,13 @@ public abstract class AbstractPhaseScope { protected Long startingSystemTimeMillis; protected Long startingScoreCalculationCount; + protected Long startingMoveCalculationCount; protected Score startingScore; protected Long endingSystemTimeMillis; protected Long endingScoreCalculationCount; - protected long childThreadsScoreCalculationCount = 0; + protected Long endingMoveCalculationCount; + protected long childThreadsScoreCalculationCount = 0L; + protected long childThreadsMoveCalculationCount = 0L; protected int bestSolutionStepIndex; @@ -104,11 +107,13 @@ public void reset() { public void startingNow() { startingSystemTimeMillis = System.currentTimeMillis(); startingScoreCalculationCount = getScoreDirector().getCalculationCount(); + startingMoveCalculationCount = getScoreDirector().getMoveCalculationCount(); } public void endingNow() { endingSystemTimeMillis = System.currentTimeMillis(); - endingScoreCalculationCount = getScoreDirector().getCalculationCount(); + endingScoreCalculationCount = getScoreDirector().getCalculationCount() + childThreadsScoreCalculationCount; + endingMoveCalculationCount = getScoreDirector().getMoveCalculationCount() + childThreadsMoveCalculationCount; } public SolutionDescriptor getSolutionDescriptor() { @@ -133,17 +138,37 @@ public void addChildThreadsScoreCalculationCount(long addition) { childThreadsScoreCalculationCount += addition; } + public void addChildThreadsMoveCalculationCount(long addition) { + solverScope.addChildThreadsMoveCalculationCount(addition); + childThreadsMoveCalculationCount += addition; + } + public long getPhaseScoreCalculationCount() { return endingScoreCalculationCount - startingScoreCalculationCount + childThreadsScoreCalculationCount; } + public long getPhaseMoveCalculationCount() { + return endingMoveCalculationCount - startingMoveCalculationCount + childThreadsMoveCalculationCount; + } + /** * @return at least 0, per second */ public long getPhaseScoreCalculationSpeed() { + return getMetricCalculationSpeed(getPhaseScoreCalculationCount()); + } + + /** + * @return at least 0, per second + */ + public long getPhaseMoveCalculationSpeed() { + return getMetricCalculationSpeed(getPhaseMoveCalculationCount()); + } + + private long getMetricCalculationSpeed(long metric) { long timeMillisSpent = getPhaseTimeMillisSpent(); // Avoid divide by zero exception on a fast CPU - return getPhaseScoreCalculationCount() * 1000L / (timeMillisSpent == 0L ? 1L : timeMillisSpent); + return metric * 1000L / (timeMillisSpent == 0L ? 1L : timeMillisSpent); } public > InnerScoreDirector getScoreDirector() { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java index fc332e5f53..e7c4096fe1 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java @@ -66,6 +66,7 @@ public abstract class AbstractScoreDirector move, boolean assertMoveScoreFrom } Move undoMove = move.doMove(this); Score_ score = calculateScore(); + incrementMoveCalculationCount(move.getMoveWeightCount()); if (assertMoveScoreFromScratch) { undoMoveText = undoMove.toString(); if (trackingWorkingSolution) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java index 68d7004831..7e4221e8a4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java @@ -225,6 +225,12 @@ default Solution_ cloneWorkingSolution() { void incrementCalculationCount(); + long getMoveCalculationCount(); + + void resetMoveCalculationCount(); + + void incrementMoveCalculationCount(int addition); + /** * @return never null */ diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java index 463d906f74..5fe3cc5f64 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java @@ -216,6 +216,7 @@ public void solvingStarted(SolverScope solverScope) { assertCorrectSolutionState(); solverScope.startingNow(); solverScope.getScoreDirector().resetCalculationCount(); + solverScope.getScoreDirector().resetMoveCalculationCount(); super.solvingStarted(solverScope); var startingSolverCount = solverScope.getStartingSolverCount() + 1; solverScope.setStartingSolverCount(startingSolverCount); @@ -306,10 +307,11 @@ public void solvingEnded(SolverScope solverScope) { public void outerSolvingEnded(SolverScope solverScope) { logger.info("Solving ended: time spent ({}), best score ({}), score calculation speed ({}/sec), " - + "phase total ({}), environment mode ({}), move thread count ({}).", + + "move calculation speed ({}/sec), phase total ({}), environment mode ({}), move thread count ({}).", solverScope.getTimeMillisSpent(), solverScope.getBestScore(), solverScope.getScoreCalculationSpeed(), + solverScope.getMoveCalculationSpeed(), phaseList.size(), environmentMode.name(), moveThreadCountDescription); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java index 9d46316927..259f6ffb2c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java @@ -53,6 +53,8 @@ public class SolverScope { private long childThreadsScoreCalculationCount = 0; private long moveEvaluationCount = 0; + private long childThreadsMoveCalculationCount = 0L; + private Score startingInitializedScore; private Long bestSolutionTimeMillis; @@ -187,6 +189,14 @@ public long getScoreCalculationCount() { return scoreDirector.getCalculationCount() + childThreadsScoreCalculationCount; } + public void addChildThreadsMoveCalculationCount(long addition) { + childThreadsMoveCalculationCount += addition; + } + + public long getMoveCalculationCount() { + return scoreDirector.getMoveCalculationCount() + childThreadsMoveCalculationCount; + } + public void addMoveEvaluationCount(long addition) { moveEvaluationCount += addition; } @@ -288,9 +298,17 @@ public long getScoreCalculationSpeed() { return getSpeed(getScoreCalculationCount(), timeMillisSpent); } - public static long getSpeed(long scoreCalculationCount, long timeMillisSpent) { + /** + * @return at least 0, per second + */ + public long getMoveCalculationSpeed() { + long timeMillisSpent = getTimeMillisSpent(); + return getSpeed(getMoveCalculationCount(), timeMillisSpent); + } + + public static long getSpeed(long metric, long timeMillisSpent) { // Avoid divide by zero exception on a fast CPU - return scoreCalculationCount * 1000L / (timeMillisSpent == 0L ? 1L : timeMillisSpent); + return metric * 1000L / (timeMillisSpent == 0L ? 1L : timeMillisSpent); } public void setWorkingSolutionFromBestSolution() { diff --git a/core/src/main/resources/solver.xsd b/core/src/main/resources/solver.xsd index d4ed3c1b5c..1258f1db24 100644 --- a/core/src/main/resources/solver.xsd +++ b/core/src/main/resources/solver.xsd @@ -1573,6 +1573,8 @@ + + diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java index 79ab951a93..59b6a9aa76 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java @@ -246,6 +246,11 @@ void checkDefaultMeters() { null, null, Meter.Type.GAUGE), + new Meter.Id(SolverMetric.MOVE_CALCULATION_COUNT.getMeterId(), + Tags.empty(), + null, + null, + Meter.Type.GAUGE), new Meter.Id(SolverMetric.PROBLEM_ENTITY_COUNT.getMeterId(), Tags.empty(), null, @@ -328,6 +333,11 @@ void checkDefaultMetersTags() { null, null, Meter.Type.GAUGE), + new Meter.Id(SolverMetric.MOVE_CALCULATION_COUNT.getMeterId(), + Tags.of("tag.key", "tag.value"), + null, + null, + Meter.Type.GAUGE), new Meter.Id(SolverMetric.PROBLEM_ENTITY_COUNT.getMeterId(), Tags.of("tag.key", "tag.value"), null, @@ -419,6 +429,8 @@ void solveMetrics() { assertThat(meterRegistry.getMeasurement(SolverMetric.SOLVE_DURATION.getMeterId(), "DURATION")).isZero(); assertThat(meterRegistry.getMeasurement(SolverMetric.SOLVE_DURATION.getMeterId(), "ACTIVE_TASKS")).isZero(); assertThat(meterRegistry.getMeasurement(SolverMetric.ERROR_COUNT.getMeterId(), "COUNT")).isZero(); + assertThat(meterRegistry.getMeasurement(SolverMetric.SCORE_CALCULATION_COUNT.getMeterId(), "VALUE")).isPositive(); + assertThat(meterRegistry.getMeasurement(SolverMetric.MOVE_CALCULATION_COUNT.getMeterId(), "VALUE")).isPositive(); } @Test From da89485839854322e64fba0f916713b100aa6199 Mon Sep 17 00:00:00 2001 From: Fred Date: Thu, 5 Sep 2024 17:32:31 -0300 Subject: [PATCH 02/26] feat: simplify count metric and add benchmark support --- .../statistic/ProblemStatisticType.java | 4 + .../impl/result/ProblemBenchmarkResult.java | 2 + .../impl/statistic/ProblemStatistic.java | 2 + .../MoveCalculationSpeedProblemStatistic.java | 56 ++++++++++++ .../MoveCalculationSpeedStatisticPoint.java | 28 ++++++ ...oveCalculationSpeedSubSingleStatistic.java | 91 +++++++++++++++++++ benchmark/src/main/resources/benchmark.xsd | 6 ++ ...alculationSpeedSubSingleStatisticTest.java | 38 ++++++++ .../decider/ConstructionHeuristicDecider.java | 9 +- .../scope/ExhaustiveSearchPhaseScope.java | 6 ++ .../heuristic/move/AbstractMetricMove.java | 22 +++++ .../impl/heuristic/move/AbstractMove.java | 2 +- .../move/AbstractSimplifiedMove.java | 2 +- .../heuristic/move/AbstractWeightMove.java | 25 ----- .../impl/heuristic/move/CompositeMove.java | 6 -- .../solver/core/impl/heuristic/move/Move.java | 10 +- .../impl/heuristic/move/RecordedUndoMove.java | 4 +- ...nRecreateConstructionHeuristicDecider.java | 10 -- ...eateConstructionHeuristicPhaseBuilder.java | 1 + .../solver/core/impl/phase/AbstractPhase.java | 14 ++- .../impl/phase/scope/AbstractPhaseScope.java | 11 +++ .../score/director/AbstractScoreDirector.java | 8 +- .../score/director/InnerScoreDirector.java | 2 +- .../DefaultExhaustiveSearchPhaseTest.java | 54 +++++++++++ .../generic/RuinRecreateMoveSelectorTest.java | 37 ++++++++ .../ListRuinRecreateMoveSelectorTest.java | 37 ++++++++ 26 files changed, 429 insertions(+), 58 deletions(-) create mode 100644 benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedProblemStatistic.java create mode 100644 benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedStatisticPoint.java create mode 100644 benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedSubSingleStatistic.java create mode 100644 benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedSubSingleStatisticTest.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractMetricMove.java delete mode 100644 core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractWeightMove.java diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/config/statistic/ProblemStatisticType.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/config/statistic/ProblemStatisticType.java index 1410e9a267..b44536f51c 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/config/statistic/ProblemStatisticType.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/config/statistic/ProblemStatisticType.java @@ -10,6 +10,7 @@ import ai.timefold.solver.benchmark.impl.statistic.bestscore.BestScoreProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.bestsolutionmutation.BestSolutionMutationProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.memoryuse.MemoryUseProblemStatistic; +import ai.timefold.solver.benchmark.impl.statistic.movecalculationspeed.MoveCalculationSpeedProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.movecountperstep.MoveCountPerStepProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.scorecalculationspeed.ScoreCalculationSpeedProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.stepscore.StepScoreProblemStatistic; @@ -19,6 +20,7 @@ public enum ProblemStatisticType implements StatisticType { BEST_SCORE, STEP_SCORE, SCORE_CALCULATION_SPEED, + MOVE_CALCULATION_SPEED, BEST_SOLUTION_MUTATION, MOVE_COUNT_PER_STEP, MEMORY_USE; @@ -31,6 +33,8 @@ public ProblemStatistic buildProblemStatistic(ProblemBenchmarkResult problemBenc return new StepScoreProblemStatistic(problemBenchmarkResult); case SCORE_CALCULATION_SPEED: return new ScoreCalculationSpeedProblemStatistic(problemBenchmarkResult); + case MOVE_CALCULATION_SPEED: + return new MoveCalculationSpeedProblemStatistic(problemBenchmarkResult); case BEST_SOLUTION_MUTATION: return new BestSolutionMutationProblemStatistic(problemBenchmarkResult); case MOVE_COUNT_PER_STEP: diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/ProblemBenchmarkResult.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/ProblemBenchmarkResult.java index 58ebf28564..1768e05894 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/ProblemBenchmarkResult.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/ProblemBenchmarkResult.java @@ -32,6 +32,7 @@ import ai.timefold.solver.benchmark.impl.statistic.bestscore.BestScoreProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.bestsolutionmutation.BestSolutionMutationProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.memoryuse.MemoryUseProblemStatistic; +import ai.timefold.solver.benchmark.impl.statistic.movecalculationspeed.MoveCalculationSpeedProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.movecountperstep.MoveCountPerStepProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.scorecalculationspeed.ScoreCalculationSpeedProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.stepscore.StepScoreProblemStatistic; @@ -67,6 +68,7 @@ public class ProblemBenchmarkResult { @XmlElement(name = "bestScoreProblemStatistic", type = BestScoreProblemStatistic.class), @XmlElement(name = "stepScoreProblemStatistic", type = StepScoreProblemStatistic.class), @XmlElement(name = "scoreCalculationSpeedProblemStatistic", type = ScoreCalculationSpeedProblemStatistic.class), + @XmlElement(name = "moveCalculationSpeedProblemStatistic", type = MoveCalculationSpeedProblemStatistic.class), @XmlElement(name = "bestSolutionMutationProblemStatistic", type = BestSolutionMutationProblemStatistic.class), @XmlElement(name = "moveCountPerStepProblemStatistic", type = MoveCountPerStepProblemStatistic.class), @XmlElement(name = "memoryUseProblemStatistic", type = MemoryUseProblemStatistic.class), diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/ProblemStatistic.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/ProblemStatistic.java index 000207b402..db6b7ad73d 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/ProblemStatistic.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/ProblemStatistic.java @@ -19,6 +19,7 @@ import ai.timefold.solver.benchmark.impl.statistic.bestscore.BestScoreProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.bestsolutionmutation.BestSolutionMutationProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.memoryuse.MemoryUseProblemStatistic; +import ai.timefold.solver.benchmark.impl.statistic.movecalculationspeed.MoveCalculationSpeedProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.movecountperstep.MoveCountPerStepProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.scorecalculationspeed.ScoreCalculationSpeedProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.stepscore.StepScoreProblemStatistic; @@ -31,6 +32,7 @@ BestScoreProblemStatistic.class, StepScoreProblemStatistic.class, ScoreCalculationSpeedProblemStatistic.class, + MoveCalculationSpeedProblemStatistic.class, BestSolutionMutationProblemStatistic.class, MoveCountPerStepProblemStatistic.class, MemoryUseProblemStatistic.class diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedProblemStatistic.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedProblemStatistic.java new file mode 100644 index 0000000000..525b49834e --- /dev/null +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedProblemStatistic.java @@ -0,0 +1,56 @@ +package ai.timefold.solver.benchmark.impl.statistic.movecalculationspeed; + +import static java.util.Collections.singletonList; + +import java.util.List; + +import ai.timefold.solver.benchmark.config.statistic.ProblemStatisticType; +import ai.timefold.solver.benchmark.impl.report.BenchmarkReport; +import ai.timefold.solver.benchmark.impl.report.LineChart; +import ai.timefold.solver.benchmark.impl.result.ProblemBenchmarkResult; +import ai.timefold.solver.benchmark.impl.result.SingleBenchmarkResult; +import ai.timefold.solver.benchmark.impl.result.SubSingleBenchmarkResult; +import ai.timefold.solver.benchmark.impl.statistic.ProblemStatistic; +import ai.timefold.solver.benchmark.impl.statistic.SubSingleStatistic; + +public class MoveCalculationSpeedProblemStatistic extends ProblemStatistic> { + + private MoveCalculationSpeedProblemStatistic() { + // For JAXB. + } + + public MoveCalculationSpeedProblemStatistic(ProblemBenchmarkResult problemBenchmarkResult) { + super(problemBenchmarkResult, ProblemStatisticType.MOVE_CALCULATION_SPEED); + } + + @Override + public SubSingleStatistic createSubSingleStatistic(SubSingleBenchmarkResult subSingleBenchmarkResult) { + return new MoveCalculationSpeedSubSingleStatistic<>(subSingleBenchmarkResult); + } + + /** + * @return never null + */ + @Override + protected List> generateCharts(BenchmarkReport benchmarkReport) { + LineChart.Builder builder = new LineChart.Builder<>(); + for (SingleBenchmarkResult singleBenchmarkResult : problemBenchmarkResult.getSingleBenchmarkResultList()) { + String solverLabel = singleBenchmarkResult.getSolverBenchmarkResult().getNameWithFavoriteSuffix(); + if (singleBenchmarkResult.hasAllSuccess()) { + var subSingleStatistic = singleBenchmarkResult.getSubSingleStatistic(problemStatisticType); + List points = subSingleStatistic.getPointList(); + for (MoveCalculationSpeedStatisticPoint point : points) { + long timeMillisSpent = point.getTimeMillisSpent(); + long moveCalculationSpeed = point.getMoveCalculationSpeed(); + builder.add(solverLabel, timeMillisSpent, moveCalculationSpeed); + } + } + if (singleBenchmarkResult.getSolverBenchmarkResult().isFavorite()) { + builder.markFavorite(solverLabel); + } + } + return singletonList(builder.build("moveCalculationSpeedProblemStatisticChart", + problemBenchmarkResult.getName() + " move calculation speed statistic", "Time spent", + "Move calculation speed per second", false, true, false)); + } +} diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedStatisticPoint.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedStatisticPoint.java new file mode 100644 index 0000000000..dbd95f615f --- /dev/null +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedStatisticPoint.java @@ -0,0 +1,28 @@ +package ai.timefold.solver.benchmark.impl.statistic.movecalculationspeed; + +import ai.timefold.solver.benchmark.impl.statistic.StatisticPoint; + +public class MoveCalculationSpeedStatisticPoint extends StatisticPoint { + + private final long timeMillisSpent; + private final long moveCalculationSpeed; + + public MoveCalculationSpeedStatisticPoint(long timeMillisSpent, long moveCalculationSpeed) { + this.timeMillisSpent = timeMillisSpent; + this.moveCalculationSpeed = moveCalculationSpeed; + } + + public long getTimeMillisSpent() { + return timeMillisSpent; + } + + public long getMoveCalculationSpeed() { + return moveCalculationSpeed; + } + + @Override + public String toCsvLine() { + return buildCsvLineWithLongs(timeMillisSpent, moveCalculationSpeed); + } + +} diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedSubSingleStatistic.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedSubSingleStatistic.java new file mode 100644 index 0000000000..7119ebe766 --- /dev/null +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedSubSingleStatistic.java @@ -0,0 +1,91 @@ +package ai.timefold.solver.benchmark.impl.statistic.movecalculationspeed; + +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; + +import ai.timefold.solver.benchmark.config.statistic.ProblemStatisticType; +import ai.timefold.solver.benchmark.impl.result.SubSingleBenchmarkResult; +import ai.timefold.solver.benchmark.impl.statistic.ProblemBasedSubSingleStatistic; +import ai.timefold.solver.benchmark.impl.statistic.StatisticPoint; +import ai.timefold.solver.benchmark.impl.statistic.StatisticRegistry; +import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; +import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; + +import io.micrometer.core.instrument.Tags; + +public class MoveCalculationSpeedSubSingleStatistic + extends ProblemBasedSubSingleStatistic { + + private long timeMillisThresholdInterval; + + private MoveCalculationSpeedSubSingleStatistic() { + // For JAXB. + } + + public MoveCalculationSpeedSubSingleStatistic(SubSingleBenchmarkResult subSingleBenchmarkResult) { + this(subSingleBenchmarkResult, 1000L); + } + + public MoveCalculationSpeedSubSingleStatistic(SubSingleBenchmarkResult benchmarkResult, long timeMillisThresholdInterval) { + super(benchmarkResult, ProblemStatisticType.MOVE_CALCULATION_SPEED); + if (timeMillisThresholdInterval <= 0L) { + throw new IllegalArgumentException("The timeMillisThresholdInterval (" + timeMillisThresholdInterval + + ") must be bigger than 0."); + } + this.timeMillisThresholdInterval = timeMillisThresholdInterval; + } + + // ************************************************************************ + // Lifecycle methods + // ************************************************************************ + + @Override + public void open(StatisticRegistry registry, Tags runTag) { + registry.addListener(SolverMetric.MOVE_CALCULATION_COUNT, new Consumer<>() { + long nextTimeMillisThreshold = timeMillisThresholdInterval; + long lastTimeMillisSpent = 0; + final AtomicLong lastMoveCalculationCount = new AtomicLong(0); + + @Override + public void accept(Long timeMillisSpent) { + if (timeMillisSpent >= nextTimeMillisThreshold) { + registry.getGaugeValue(SolverMetric.MOVE_CALCULATION_COUNT, runTag, moveCalculationCountNumber -> { + long moveCalculationCount = moveCalculationCountNumber.longValue(); + long calculationCountInterval = moveCalculationCount - lastMoveCalculationCount.get(); + long timeMillisSpentInterval = timeMillisSpent - lastTimeMillisSpent; + if (timeMillisSpentInterval == 0L) { + // Avoid divide by zero exception on a fast CPU + timeMillisSpentInterval = 1L; + } + long moveCalculationSpeed = calculationCountInterval * 1000L / timeMillisSpentInterval; + pointList.add(new MoveCalculationSpeedStatisticPoint(timeMillisSpent, moveCalculationSpeed)); + lastMoveCalculationCount.set(moveCalculationCount); + }); + lastTimeMillisSpent = timeMillisSpent; + nextTimeMillisThreshold += timeMillisThresholdInterval; + if (nextTimeMillisThreshold < timeMillisSpent) { + nextTimeMillisThreshold = timeMillisSpent; + } + } + } + }); + } + + // ************************************************************************ + // CSV methods + // ************************************************************************ + + @Override + protected String getCsvHeader() { + return StatisticPoint.buildCsvLine("timeMillisSpent", "moveCalculationSpeed"); + } + + @Override + protected MoveCalculationSpeedStatisticPoint createPointFromCsvLine(ScoreDefinition scoreDefinition, + List csvLine) { + return new MoveCalculationSpeedStatisticPoint(Long.parseLong(csvLine.get(0)), + Long.parseLong(csvLine.get(1))); + } + +} diff --git a/benchmark/src/main/resources/benchmark.xsd b/benchmark/src/main/resources/benchmark.xsd index 63e9f945a8..269a6deab4 100644 --- a/benchmark/src/main/resources/benchmark.xsd +++ b/benchmark/src/main/resources/benchmark.xsd @@ -223,6 +223,9 @@ + + + @@ -2615,6 +2618,9 @@ + + + diff --git a/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedSubSingleStatisticTest.java b/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedSubSingleStatisticTest.java new file mode 100644 index 0000000000..0851435613 --- /dev/null +++ b/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedSubSingleStatisticTest.java @@ -0,0 +1,38 @@ + +package ai.timefold.solver.benchmark.impl.statistic.movecalculationspeed; + +import java.util.Collections; +import java.util.List; +import java.util.function.Function; + +import ai.timefold.solver.benchmark.impl.result.SubSingleBenchmarkResult; +import ai.timefold.solver.benchmark.impl.statistic.AbstractSubSingleStatisticTest; +import ai.timefold.solver.core.impl.testdata.domain.TestdataSolution; + +import org.assertj.core.api.SoftAssertions; + +public final class MoveCalculationSpeedSubSingleStatisticTest + extends + AbstractSubSingleStatisticTest> { + + @Override + protected Function> + getSubSingleStatisticConstructor() { + return MoveCalculationSpeedSubSingleStatistic::new; + } + + @Override + protected List getInputPoints() { + return Collections.singletonList(new MoveCalculationSpeedStatisticPoint(Long.MAX_VALUE, Long.MAX_VALUE)); + } + + @Override + protected void runTest(SoftAssertions assertions, List outputPoints) { + assertions.assertThat(outputPoints) + .hasSize(1) + .first() + .matches(s -> s.getMoveCalculationSpeed() == Long.MAX_VALUE, "Move calculation speeds do not match.") + .matches(s -> s.getTimeMillisSpent() == Long.MAX_VALUE, "Millis do not match."); + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/decider/ConstructionHeuristicDecider.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/decider/ConstructionHeuristicDecider.java index 17842f8e42..96d4e820fc 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/decider/ConstructionHeuristicDecider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/decider/ConstructionHeuristicDecider.java @@ -8,6 +8,7 @@ import ai.timefold.solver.core.impl.constructionheuristic.scope.ConstructionHeuristicMoveScope; import ai.timefold.solver.core.impl.constructionheuristic.scope.ConstructionHeuristicPhaseScope; import ai.timefold.solver.core.impl.constructionheuristic.scope.ConstructionHeuristicStepScope; +import ai.timefold.solver.core.impl.heuristic.move.AbstractMetricMove; import ai.timefold.solver.core.impl.heuristic.move.Move; import ai.timefold.solver.core.impl.heuristic.move.NoChangeMove; import ai.timefold.solver.core.impl.heuristic.selector.move.generic.ChangeMove; @@ -103,7 +104,7 @@ public void decideNextStep(ConstructionHeuristicStepScope stepScope, // It will never do anything more complex than that. continue; } - prepare(move); + prepare(stepScope, move); var moveScope = new ConstructionHeuristicMoveScope<>(stepScope, moveIndex, move); moveIndex++; doMove(moveScope); @@ -147,8 +148,10 @@ protected > void doMove(ConstructionHeuristicMoveSc } } - protected void prepare(Move move) { - // Ignored by default + private void prepare(ConstructionHeuristicStepScope stepScope, Move move) { + if (move instanceof AbstractMetricMove metricMove) { + metricMove.setCollectMetricEnabled(stepScope.getPhaseScope().isEnableCollectMetrics()); + } } } 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 f57c97e6f6..314fb21931 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 @@ -59,6 +59,12 @@ public void setLastCompletedStepScope(ExhaustiveSearchStepScope lastC this.lastCompletedStepScope = lastCompletedStepScope; } + @Override + public > Score_ calculateScore() { + getScoreDirector().incrementMoveCalculationCount(); + return super.calculateScore(); + } + // ************************************************************************ // Calculated methods // ************************************************************************ diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractMetricMove.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractMetricMove.java new file mode 100644 index 0000000000..e9953c00cc --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractMetricMove.java @@ -0,0 +1,22 @@ +package ai.timefold.solver.core.impl.heuristic.move; + +import ai.timefold.solver.core.api.domain.solution.PlanningSolution; + +/** + * Abstract superclass for {@link Move} which includes the ability to enable or disable the move metric collection. + * + * @param the solution type, the class with the {@link PlanningSolution} annotation + */ +public abstract class AbstractMetricMove implements Move { + + private boolean collectMetricEnabled = true; + + @Override + public boolean isCollectMetricEnabled() { + return collectMetricEnabled; + } + + public void setCollectMetricEnabled(boolean collectMetricEnabled) { + this.collectMetricEnabled = collectMetricEnabled; + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractMove.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractMove.java index d7600c3666..990a70db09 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractMove.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractMove.java @@ -15,7 +15,7 @@ * @param the solution type, the class with the {@link PlanningSolution} annotation * @see Move */ -public abstract class AbstractMove extends AbstractWeightMove { +public abstract class AbstractMove extends AbstractMetricMove { @Override public final Move doMove(ScoreDirector scoreDirector) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractSimplifiedMove.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractSimplifiedMove.java index b689cd20bb..25e0e489f9 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractSimplifiedMove.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractSimplifiedMove.java @@ -11,7 +11,7 @@ * * @param */ -public abstract class AbstractSimplifiedMove extends AbstractWeightMove { +public abstract class AbstractSimplifiedMove extends AbstractMetricMove { @Override public final Move doMove(ScoreDirector scoreDirector) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractWeightMove.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractWeightMove.java deleted file mode 100644 index 7069f71d52..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractWeightMove.java +++ /dev/null @@ -1,25 +0,0 @@ -package ai.timefold.solver.core.impl.heuristic.move; - -import ai.timefold.solver.core.api.domain.solution.PlanningSolution; - -/** - * Abstract superclass for {@link Move} which includes the ability to add weight count that is used by the move count - * speed metric. - * - * @param the solution type, the class with the {@link PlanningSolution} annotation - * @see Move - * @see ai.timefold.solver.core.config.solver.monitoring.SolverMetric - */ -public abstract class AbstractWeightMove implements Move { - - private int weightCount = 1; - - @Override - public int getMoveWeightCount() { - return weightCount; - } - - public void setWeightCount(int weightCount) { - this.weightCount = weightCount; - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/CompositeMove.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/CompositeMove.java index 56cf5b4bab..d3f84f5691 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/CompositeMove.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/CompositeMove.java @@ -112,12 +112,6 @@ public CompositeMove rebase(ScoreDirector destinationScore } return new CompositeMove<>(rebasedMoves); } - - @Override - public int getMoveWeightCount() { - return moves.length; - } - // ************************************************************************ // Introspection methods // ************************************************************************ diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/Move.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/Move.java index 72b7675616..0398425978 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/Move.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/Move.java @@ -174,13 +174,13 @@ default Collection getPlanningValues() { } /** - * Return the weight count of a given move that contributes to the speed metric. - * Every move counts as one single move by default. + * Flag that enables metric collection for the move. + * In cases like Ruin & Recreate or undo moves, metrics such as move count should not be considered. * - * @return 1 by default + * @return true by default, which means this move updates the related metrics. */ - default int getMoveWeightCount() { - return 1; + default boolean isCollectMetricEnabled() { + return true; } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/RecordedUndoMove.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/RecordedUndoMove.java index 54328c4987..6ca770a037 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/RecordedUndoMove.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/RecordedUndoMove.java @@ -25,7 +25,7 @@ protected void doMoveOnGenuineVariables(ScoreDirector scoreDirector) } @Override - public int getMoveWeightCount() { - return 0; + public boolean isCollectMetricEnabled() { + return false; } } \ No newline at end of file diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicDecider.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicDecider.java index 3ba0d9871e..653d03defb 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicDecider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicDecider.java @@ -3,8 +3,6 @@ import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.constructionheuristic.decider.ConstructionHeuristicDecider; import ai.timefold.solver.core.impl.constructionheuristic.decider.forager.ConstructionHeuristicForager; -import ai.timefold.solver.core.impl.heuristic.move.AbstractWeightMove; -import ai.timefold.solver.core.impl.heuristic.move.Move; import ai.timefold.solver.core.impl.solver.termination.Termination; final class RuinRecreateConstructionHeuristicDecider @@ -15,14 +13,6 @@ final class RuinRecreateConstructionHeuristicDecider super("", termination, forager); } - @Override - protected void prepare(Move move) { - if (move instanceof AbstractWeightMove weightMove) { - // The moves of R&R construction won't count as completed moves - weightMove.setWeightCount(0); - } - } - @Override public boolean isLoggingEnabled() { return false; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseBuilder.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseBuilder.java index ccb5bbd0f1..8c289fbac8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseBuilder.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseBuilder.java @@ -58,6 +58,7 @@ public EntityPlacer getEntityPlacer() { @Override public DefaultConstructionHeuristicPhase build() { + this.setEnableCollectMetrics(false); return new RuinRecreateConstructionHeuristicPhase<>(this); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java index ed8e9e9ab7..1687731220 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java @@ -34,6 +34,8 @@ public abstract class AbstractPhase implements Phase { protected final boolean assertShadowVariablesAreNotStaleAfterStep; protected final boolean triggerFirstInitializedSolutionEvent; + protected final boolean enableCollectMetrics; + /** Used for {@link #addPhaseLifecycleListener(PhaseLifecycleListener)}. */ protected PhaseLifecycleSupport phaseLifecycleSupport = new PhaseLifecycleSupport<>(); @@ -46,6 +48,7 @@ protected AbstractPhase(Builder builder) { assertStepScoreFromScratch = builder.assertStepScoreFromScratch; assertExpectedStepScore = builder.assertExpectedStepScore; assertShadowVariablesAreNotStaleAfterStep = builder.assertShadowVariablesAreNotStaleAfterStep; + enableCollectMetrics = builder.enableCollectMetrics; triggerFirstInitializedSolutionEvent = builder.triggerFirstInitializedSolutionEvent; } @@ -104,6 +107,7 @@ public void solvingEnded(SolverScope solverScope) { public void phaseStarted(AbstractPhaseScope phaseScope) { phaseScope.startingNow(); phaseScope.reset(); + phaseScope.setEnableCollectMetrics(enableCollectMetrics); solver.phaseStarted(phaseScope); phaseTermination.phaseStarted(phaseScope); phaseLifecycleSupport.firePhaseStarted(phaseScope); @@ -156,7 +160,9 @@ protected > void predictWorkingStepScore(AbstractSt @Override public void stepEnded(AbstractStepScope stepScope) { solver.stepEnded(stepScope); - collectMetrics(stepScope); + if (enableCollectMetrics) { + collectMetrics(stepScope); + } phaseTermination.stepEnded(stepScope); phaseLifecycleSupport.fireStepEnded(stepScope); } @@ -225,6 +231,8 @@ protected abstract static class Builder { private boolean assertExpectedStepScore = false; private boolean assertShadowVariablesAreNotStaleAfterStep = false; + private boolean enableCollectMetrics = true; + protected Builder(int phaseIndex, String logIndentation, Termination phaseTermination) { this(phaseIndex, false, logIndentation, phaseTermination); } @@ -249,6 +257,10 @@ public void setAssertShadowVariablesAreNotStaleAfterStep(boolean assertShadowVar this.assertShadowVariablesAreNotStaleAfterStep = assertShadowVariablesAreNotStaleAfterStep; } + public void setEnableCollectMetrics(boolean enableCollectMetrics) { + this.enableCollectMetrics = enableCollectMetrics; + } + protected abstract AbstractPhase build(); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java index e1461e0344..cd395c31bf 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java @@ -34,6 +34,8 @@ public abstract class AbstractPhaseScope { protected int bestSolutionStepIndex; + protected boolean enableCollectMetrics = true; + /** * As defined by #AbstractPhaseScope(SolverScope, int, boolean) * with the phaseSendingBestSolutionEvents parameter set to true. @@ -91,12 +93,21 @@ public void setBestSolutionStepIndex(int bestSolutionStepIndex) { public abstract AbstractStepScope getLastCompletedStepScope(); + public boolean isEnableCollectMetrics() { + return enableCollectMetrics; + } + + public void setEnableCollectMetrics(boolean enableCollectMetrics) { + this.enableCollectMetrics = enableCollectMetrics; + } + // ************************************************************************ // Calculated methods // ************************************************************************ public void reset() { bestSolutionStepIndex = -1; + enableCollectMetrics = true; // solverScope.getBestScore() is null with an uninitialized score startingScore = solverScope.getBestScore() == null ? solverScope.calculateScore() : solverScope.getBestScore(); if (getLastCompletedStepScope().getStepIndex() < 0) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java index e7c4096fe1..95451f3966 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java @@ -173,8 +173,8 @@ public void resetMoveCalculationCount() { } @Override - public void incrementMoveCalculationCount(int addition) { - this.moveCalculationCount += addition; + public void incrementMoveCalculationCount() { + this.moveCalculationCount++; } @Override @@ -240,7 +240,9 @@ public Score_ doAndProcessMove(Move move, boolean assertMoveScoreFrom } Move undoMove = move.doMove(this); Score_ score = calculateScore(); - incrementMoveCalculationCount(move.getMoveWeightCount()); + if (move.isCollectMetricEnabled()) { + incrementMoveCalculationCount(); + } if (assertMoveScoreFromScratch) { undoMoveText = undoMove.toString(); if (trackingWorkingSolution) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java index 7e4221e8a4..91d2aaeb0d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java @@ -229,7 +229,7 @@ default Solution_ cloneWorkingSolution() { void resetMoveCalculationCount(); - void incrementMoveCalculationCount(int addition); + void incrementMoveCalculationCount(); /** * @return never null 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 e723d3a0c6..36cb6baebf 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 @@ -11,10 +11,14 @@ import java.util.Arrays; import java.util.Collections; +import java.util.concurrent.atomic.AtomicReference; import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.score.director.ScoreDirector; +import ai.timefold.solver.core.api.solver.Solver; +import ai.timefold.solver.core.api.solver.SolverFactory; import ai.timefold.solver.core.config.exhaustivesearch.ExhaustiveSearchPhaseConfig; +import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; import ai.timefold.solver.core.impl.exhaustivesearch.decider.ExhaustiveSearchDecider; import ai.timefold.solver.core.impl.exhaustivesearch.node.ExhaustiveSearchLayer; import ai.timefold.solver.core.impl.exhaustivesearch.node.ExhaustiveSearchNode; @@ -31,9 +35,12 @@ import ai.timefold.solver.core.impl.testdata.domain.pinned.allows_unassigned.TestdataPinnedAllowsUnassignedEntity; import ai.timefold.solver.core.impl.testdata.domain.pinned.allows_unassigned.TestdataPinnedAllowsUnassignedSolution; import ai.timefold.solver.core.impl.testdata.util.PlannerTestUtils; +import ai.timefold.solver.core.impl.testutil.TestMeterRegistry; import org.junit.jupiter.api.Test; +import io.micrometer.core.instrument.Metrics; + class DefaultExhaustiveSearchPhaseTest { @Test @@ -133,6 +140,53 @@ void solveWithInitializedEntities() { assertThat(solution.getScore().initScore()).isEqualTo(0); } + @Test + void solveWithInitializedEntitiesAndMetric() { + var meterRegistry = new TestMeterRegistry(); + Metrics.addRegistry(meterRegistry); + + var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, + TestdataEntity.class); + solverConfig.setPhaseConfigList(Collections.singletonList(new ExhaustiveSearchPhaseConfig())); + + var problem = new TestdataSolution("s1"); + var v1 = new TestdataValue("v1"); + var v2 = new TestdataValue("v2"); + var v3 = new TestdataValue("v3"); + problem.setValueList(Arrays.asList(v1, v2, v3)); + problem.setEntityList(Arrays.asList( + new TestdataEntity("e1", null), + new TestdataEntity("e2", v2), + new TestdataEntity("e3", v1))); + + SolverFactory solverFactory = SolverFactory.create(solverConfig); + Solver solver = solverFactory.buildSolver(); + AtomicReference eventBestSolutionRef = new AtomicReference<>(); + solver.addEventListener(event -> eventBestSolutionRef.set(event.getNewBestSolution())); + TestdataSolution solution = solver.solve(problem); + assertThat(eventBestSolutionRef).doesNotHaveNullValue(); + + assertThat(solution).isNotNull(); + var solvedE1 = solution.getEntityList().get(0); + assertCode("e1", solvedE1); + assertThat(solvedE1.getValue()).isNotNull(); + var solvedE2 = solution.getEntityList().get(1); + assertCode("e2", solvedE2); + assertThat(solvedE2.getValue()).isEqualTo(v2); + var solvedE3 = solution.getEntityList().get(2); + assertCode("e3", solvedE3); + assertThat(solvedE3.getValue()).isEqualTo(v1); + assertThat(solution.getScore().initScore()).isEqualTo(0); + + SolverMetric.MOVE_CALCULATION_COUNT.register(solver); + SolverMetric.SCORE_CALCULATION_COUNT.register(solver); + meterRegistry.publish(solver); + var scoreCount = meterRegistry.getMeasurement(SolverMetric.SCORE_CALCULATION_COUNT.getMeterId(), "VALUE"); + var moveCount = meterRegistry.getMeasurement(SolverMetric.MOVE_CALCULATION_COUNT.getMeterId(), "VALUE"); + assertThat(scoreCount).isPositive(); + assertThat(moveCount).isPositive(); + } + @Test void solveWithPinnedEntities() { var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataPinnedSolution.class, TestdataPinnedEntity.class) diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateMoveSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateMoveSelectorTest.java index 52ded2ac07..b7d0584486 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateMoveSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateMoveSelectorTest.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.heuristic.selector.move.generic; +import static org.assertj.core.api.Assertions.assertThat; + import java.util.List; import ai.timefold.solver.core.api.solver.SolverFactory; @@ -8,6 +10,7 @@ import ai.timefold.solver.core.config.localsearch.LocalSearchPhaseConfig; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.config.solver.SolverConfig; +import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; import ai.timefold.solver.core.config.solver.termination.TerminationConfig; import ai.timefold.solver.core.impl.testdata.domain.TestdataConstraintProvider; import ai.timefold.solver.core.impl.testdata.domain.TestdataEntity; @@ -15,11 +18,14 @@ import ai.timefold.solver.core.impl.testdata.domain.allows_unassigned.TestdataAllowsUnassignedEasyScoreCalculator; import ai.timefold.solver.core.impl.testdata.domain.allows_unassigned.TestdataAllowsUnassignedEntity; import ai.timefold.solver.core.impl.testdata.domain.allows_unassigned.TestdataAllowsUnassignedSolution; +import ai.timefold.solver.core.impl.testutil.TestMeterRegistry; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; +import io.micrometer.core.instrument.Metrics; + @Execution(ExecutionMode.CONCURRENT) class RuinRecreateMoveSelectorTest { @@ -41,6 +47,37 @@ void testRuining() { solver.solve(problem); } + @Test + void testRuiningWithMetric() { + var meterRegistry = new TestMeterRegistry(); + Metrics.addRegistry(meterRegistry); + + var solverConfig = new SolverConfig() + .withEnvironmentMode(EnvironmentMode.TRACKED_FULL_ASSERT) + .withSolutionClass(TestdataSolution.class) + .withEntityClasses(TestdataEntity.class) + .withConstraintProviderClass(TestdataConstraintProvider.class) + .withPhaseList(List.of( + new ConstructionHeuristicPhaseConfig(), + new LocalSearchPhaseConfig() + .withMoveSelectorConfig(new RuinRecreateMoveSelectorConfig()) + .withTerminationConfig(new TerminationConfig() + .withStepCountLimit(100)))); + var problem = TestdataSolution.generateSolution(5, 30); + var solver = SolverFactory.create(solverConfig).buildSolver(); + solver.addEventListener(event -> meterRegistry.publish(solver)); + solver.solve(problem); + + SolverMetric.MOVE_CALCULATION_COUNT.register(solver); + SolverMetric.SCORE_CALCULATION_COUNT.register(solver); + meterRegistry.publish(solver); + var scoreCount = meterRegistry.getMeasurement(SolverMetric.SCORE_CALCULATION_COUNT.getMeterId(), "VALUE"); + var moveCount = meterRegistry.getMeasurement(SolverMetric.MOVE_CALCULATION_COUNT.getMeterId(), "VALUE"); + assertThat(scoreCount).isPositive(); + assertThat(moveCount).isPositive(); + assertThat(scoreCount).isGreaterThan(moveCount); + } + @Test void testRuiningAllowsUnassigned() { var solverConfig = new SolverConfig() diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListRuinRecreateMoveSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListRuinRecreateMoveSelectorTest.java index 7b2ca4b4a0..54e67b8391 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListRuinRecreateMoveSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListRuinRecreateMoveSelectorTest.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.heuristic.selector.move.generic.list; +import static org.assertj.core.api.Assertions.assertThat; + import java.util.List; import java.util.Objects; import java.util.stream.IntStream; @@ -18,6 +20,7 @@ import ai.timefold.solver.core.config.localsearch.LocalSearchPhaseConfig; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.config.solver.SolverConfig; +import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; import ai.timefold.solver.core.config.solver.termination.TerminationConfig; import ai.timefold.solver.core.impl.testdata.domain.list.TestdataListEntity; import ai.timefold.solver.core.impl.testdata.domain.list.TestdataListSolution; @@ -25,11 +28,14 @@ import ai.timefold.solver.core.impl.testdata.domain.list.allows_unassigned.TestdataAllowsUnassignedValuesListEntity; import ai.timefold.solver.core.impl.testdata.domain.list.allows_unassigned.TestdataAllowsUnassignedValuesListSolution; import ai.timefold.solver.core.impl.testdata.domain.list.allows_unassigned.TestdataAllowsUnassignedValuesListValue; +import ai.timefold.solver.core.impl.testutil.TestMeterRegistry; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; +import io.micrometer.core.instrument.Metrics; + @Execution(ExecutionMode.CONCURRENT) class ListRuinRecreateMoveSelectorTest { @@ -66,6 +72,37 @@ void testRuining() { solver.solve(problem); } + @Test + void testRuiningWithMetric() { + var meterRegistry = new TestMeterRegistry(); + Metrics.addRegistry(meterRegistry); + + var solverConfig = new SolverConfig() + .withEnvironmentMode(EnvironmentMode.TRACKED_FULL_ASSERT) + .withSolutionClass(TestdataListSolution.class) + .withEntityClasses(TestdataListEntity.class, TestdataListValue.class) + .withConstraintProviderClass(TestdataListConstraintProvider.class) + .withPhaseList(List.of( + new ConstructionHeuristicPhaseConfig(), + new LocalSearchPhaseConfig() + .withMoveSelectorConfig(new ListRuinRecreateMoveSelectorConfig()) + .withTerminationConfig(new TerminationConfig() + .withStepCountLimit(100)))); + var problem = TestdataListSolution.generateUninitializedSolution(10, 3); + var solver = SolverFactory.create(solverConfig).buildSolver(); + solver.addEventListener(event -> meterRegistry.publish(solver)); + solver.solve(problem); + + SolverMetric.MOVE_CALCULATION_COUNT.register(solver); + SolverMetric.SCORE_CALCULATION_COUNT.register(solver); + meterRegistry.publish(solver); + var scoreCount = meterRegistry.getMeasurement(SolverMetric.SCORE_CALCULATION_COUNT.getMeterId(), "VALUE"); + var moveCount = meterRegistry.getMeasurement(SolverMetric.MOVE_CALCULATION_COUNT.getMeterId(), "VALUE"); + assertThat(scoreCount).isPositive(); + assertThat(moveCount).isPositive(); + assertThat(scoreCount).isGreaterThan(moveCount); + } + public static final class TestdataAllowsUnassignedValuesListMixedConstraintProvider implements ConstraintProvider { From f854f4bffd4038a43bb8434749b5a46ea6327215 Mon Sep 17 00:00:00 2001 From: Fred Date: Thu, 5 Sep 2024 17:39:07 -0300 Subject: [PATCH 03/26] chore: fix doc --- .../java/ai/timefold/solver/core/impl/heuristic/move/Move.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/Move.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/Move.java index 0398425978..03ab74a675 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/Move.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/Move.java @@ -175,7 +175,7 @@ default Collection getPlanningValues() { /** * Flag that enables metric collection for the move. - * In cases like Ruin & Recreate or undo moves, metrics such as move count should not be considered. + * In cases like Ruin and Recreate or undo moves, metrics such as move count should not be considered. * * @return true by default, which means this move updates the related metrics. */ From 303cd3a6d54bc3e39be7f80f7643a26a3d31610b Mon Sep 17 00:00:00 2001 From: Fred Date: Thu, 5 Sep 2024 18:48:23 -0300 Subject: [PATCH 04/26] feat: support benchmark report --- .../impl/SolverBenchmarkFactory.java | 2 + .../impl/SubSingleBenchmarkRunner.java | 1 + .../impl/report/BenchmarkReport.java | 14 ++++ .../impl/result/SingleBenchmarkResult.java | 19 +++++ .../impl/result/SolverBenchmarkResult.java | 10 +++ .../impl/result/SubSingleBenchmarkResult.java | 20 ++++++ .../impl/report/benchmarkReport.html.ftl | 71 +++++++++++++++++++ 7 files changed, 137 insertions(+) diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/SolverBenchmarkFactory.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/SolverBenchmarkFactory.java index a9eb248023..1fb26c1442 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/SolverBenchmarkFactory.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/SolverBenchmarkFactory.java @@ -91,6 +91,8 @@ protected List getSolverMetrics(ProblemBenchmarksConfig config) { .orElseGet(ProblemStatisticType::defaultList)) { if (problemStatisticType == ProblemStatisticType.SCORE_CALCULATION_SPEED) { out.add(SolverMetric.SCORE_CALCULATION_COUNT); + } else if (problemStatisticType == ProblemStatisticType.MOVE_CALCULATION_SPEED) { + out.add(SolverMetric.MOVE_CALCULATION_COUNT); } else { out.add(SolverMetric.valueOf(problemStatisticType.name())); } diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/SubSingleBenchmarkRunner.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/SubSingleBenchmarkRunner.java index 4c3bee97cc..19e257f1a7 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/SubSingleBenchmarkRunner.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/SubSingleBenchmarkRunner.java @@ -125,6 +125,7 @@ public SubSingleBenchmarkRunner call() { subSingleBenchmarkResult.setScore(solutionDescriptor.getScore(solution)); subSingleBenchmarkResult.setTimeMillisSpent(timeMillisSpent); subSingleBenchmarkResult.setScoreCalculationCount(solverScope.getScoreCalculationCount()); + subSingleBenchmarkResult.setMoveCalculationCount(solverScope.getMoveCalculationCount()); SolutionManager solutionManager = SolutionManager.create(solverFactory); boolean isConstraintMatchEnabled = solver.getSolverScope().getScoreDirector().isConstraintMatchEnabled(); diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/report/BenchmarkReport.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/report/BenchmarkReport.java index a088877329..4bb60a5983 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/report/BenchmarkReport.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/report/BenchmarkReport.java @@ -63,6 +63,7 @@ public static Configuration createFreeMarkerConfiguration() { private List> winningScoreDifferenceSummaryChartList = null; private List> worstScoreDifferencePercentageSummaryChartList = null; private LineChart scoreCalculationSpeedSummaryChart; + private LineChart moveCalculationSpeedSummaryChart; private BarChart worstScoreCalculationSpeedDifferencePercentageSummaryChart = null; private BarChart timeSpentSummaryChart = null; private LineChart timeSpentScalabilitySummaryChart = null; @@ -142,6 +143,11 @@ public LineChart getScoreCalculationSpeedSummaryChart() { return scoreCalculationSpeedSummaryChart; } + @SuppressWarnings("unused") // Used by FreeMarker. + public LineChart getMoveCalculationSpeedSummaryChart() { + return moveCalculationSpeedSummaryChart; + } + @SuppressWarnings("unused") // Used by FreeMarker. public BarChart getWorstScoreCalculationSpeedDifferencePercentageSummaryChart() { return worstScoreCalculationSpeedDifferencePercentageSummaryChart; @@ -200,6 +206,7 @@ public void writeReport() { worstScoreDifferencePercentageSummaryChartList = createWorstScoreDifferencePercentageSummaryChart(); bestScoreDistributionSummaryChartList = createBestScoreDistributionSummaryChart(); scoreCalculationSpeedSummaryChart = createScoreCalculationSpeedSummaryChart(); + moveCalculationSpeedSummaryChart = createMoveCalculationSpeedSummaryChart(); worstScoreCalculationSpeedDifferencePercentageSummaryChart = createWorstScoreCalculationSpeedDifferencePercentageSummaryChart(); timeSpentSummaryChart = createTimeSpentSummaryChart(); @@ -239,6 +246,7 @@ public void writeReport() { chartsToWrite.addAll(worstScoreDifferencePercentageSummaryChartList); chartsToWrite.addAll(bestScoreDistributionSummaryChartList); chartsToWrite.add(scoreCalculationSpeedSummaryChart); + chartsToWrite.add(moveCalculationSpeedSummaryChart); chartsToWrite.add(worstScoreCalculationSpeedDifferencePercentageSummaryChart); chartsToWrite.add(timeSpentSummaryChart); chartsToWrite.add(timeSpentScalabilitySummaryChart); @@ -494,6 +502,12 @@ private LineChart createScoreCalculationSpeedSummaryChart() { "Score calculation speed per second", false); } + private LineChart createMoveCalculationSpeedSummaryChart() { + return createScalabilitySummaryChart(SingleBenchmarkResult::getMoveCalculationSpeed, + "moveCalculationSpeedSummaryChart", "Move calculation speed summary (higher is better)", + "Move calculation speed per second", false); + } + private BarChart createWorstScoreCalculationSpeedDifferencePercentageSummaryChart() { return createSummaryBarChart(result -> result.getWorstScoreCalculationSpeedDifferencePercentage() * 100, "worstScoreCalculationSpeedDifferencePercentageSummaryChart", diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SingleBenchmarkResult.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SingleBenchmarkResult.java index 324052bd39..cd4ebf7180 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SingleBenchmarkResult.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SingleBenchmarkResult.java @@ -58,6 +58,7 @@ public class SingleBenchmarkResult implements BenchmarkResult { private double[] standardDeviationDoubles = null; private long timeMillisSpent = -1L; private long scoreCalculationCount = -1L; + private long moveCalculationCount = -1L; private String scoreExplanationSummary = null; // ************************************************************************ @@ -151,6 +152,14 @@ public void setScoreCalculationCount(long scoreCalculationCount) { this.scoreCalculationCount = scoreCalculationCount; } + public long getMoveCalculationCount() { + return moveCalculationCount; + } + + public void setMoveCalculationCount(long moveCalculationCount) { + this.moveCalculationCount = moveCalculationCount; + } + @SuppressWarnings("unused") // Used by FreeMarker. public String getScoreExplanationSummary() { return scoreExplanationSummary; @@ -258,6 +267,15 @@ public Long getScoreCalculationSpeed() { return scoreCalculationCount * 1000L / timeMillisSpent; } + public Long getMoveCalculationSpeed() { + long timeMillisSpent = this.timeMillisSpent; + if (timeMillisSpent == 0L) { + // Avoid divide by zero exception on a fast CPU + timeMillisSpent = 1L; + } + return moveCalculationCount * 1000L / timeMillisSpent; + } + @SuppressWarnings("unused") // Used By FreeMarker. public boolean isWinner() { return ranking != null && ranking.intValue() == 0; @@ -326,6 +344,7 @@ private void determineRepresentativeSubSingleBenchmarkResult() { usedMemoryAfterInputSolution = median.getUsedMemoryAfterInputSolution(); timeMillisSpent = median.getTimeMillisSpent(); scoreCalculationCount = median.getScoreCalculationCount(); + moveCalculationCount = median.getMoveCalculationCount(); scoreExplanationSummary = median.getScoreExplanationSummary(); } diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SolverBenchmarkResult.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SolverBenchmarkResult.java index 1e446b67a9..20fdd2b0c4 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SolverBenchmarkResult.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SolverBenchmarkResult.java @@ -56,6 +56,7 @@ public class SolverBenchmarkResult { private ScoreDifferencePercentage averageWorstScoreDifferencePercentage = null; // The average of the average is not just the overall average if the SingleBenchmarkResult's timeMillisSpent differ private Long averageScoreCalculationSpeed = null; + private Long averageMoveCalculationSpeed = null; private Long averageTimeMillisSpent = null; private Double averageWorstScoreCalculationSpeedDifferencePercentage = null; @@ -159,6 +160,11 @@ public Long getAverageScoreCalculationSpeed() { return averageScoreCalculationSpeed; } + @SuppressWarnings("unused") // Used by FreeMarker. + public Long getAverageMoveCalculationSpeed() { + return averageMoveCalculationSpeed; + } + public Long getAverageTimeMillisSpent() { return averageTimeMillisSpent; } @@ -282,6 +288,7 @@ protected void determineTotalsAndAverages() { totalWinningScoreDifference = null; ScoreDifferencePercentage totalWorstScoreDifferencePercentage = null; long totalScoreCalculationSpeed = 0L; + long totalMoveCalculationSpeed = 0L; long totalTimeMillisSpent = 0L; double totalWorstScoreCalculationSpeedDifferencePercentage = 0.0; uninitializedSolutionCount = 0; @@ -300,6 +307,7 @@ protected void determineTotalsAndAverages() { totalWinningScoreDifference = singleBenchmarkResult.getWinningScoreDifference(); totalWorstScoreDifferencePercentage = singleBenchmarkResult.getWorstScoreDifferencePercentage(); totalScoreCalculationSpeed = singleBenchmarkResult.getScoreCalculationSpeed(); + totalMoveCalculationSpeed = singleBenchmarkResult.getMoveCalculationSpeed(); totalTimeMillisSpent = singleBenchmarkResult.getTimeMillisSpent(); totalWorstScoreCalculationSpeedDifferencePercentage = singleBenchmarkResult .getWorstScoreCalculationSpeedDifferencePercentage(); @@ -311,6 +319,7 @@ protected void determineTotalsAndAverages() { totalWorstScoreDifferencePercentage = totalWorstScoreDifferencePercentage.add( singleBenchmarkResult.getWorstScoreDifferencePercentage()); totalScoreCalculationSpeed += singleBenchmarkResult.getScoreCalculationSpeed(); + totalMoveCalculationSpeed += singleBenchmarkResult.getMoveCalculationSpeed(); totalTimeMillisSpent += singleBenchmarkResult.getTimeMillisSpent(); totalWorstScoreCalculationSpeedDifferencePercentage += singleBenchmarkResult .getWorstScoreCalculationSpeedDifferencePercentage(); @@ -322,6 +331,7 @@ protected void determineTotalsAndAverages() { averageScore = totalScore.divide(successCount); averageWorstScoreDifferencePercentage = totalWorstScoreDifferencePercentage.divide(successCount); averageScoreCalculationSpeed = totalScoreCalculationSpeed / successCount; + averageMoveCalculationSpeed = totalMoveCalculationSpeed / successCount; averageTimeMillisSpent = totalTimeMillisSpent / successCount; averageWorstScoreCalculationSpeedDifferencePercentage = totalWorstScoreCalculationSpeedDifferencePercentage / successCount; diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SubSingleBenchmarkResult.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SubSingleBenchmarkResult.java index 77aaba7edb..16ad24757d 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SubSingleBenchmarkResult.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SubSingleBenchmarkResult.java @@ -58,6 +58,7 @@ public class SubSingleBenchmarkResult implements BenchmarkResult { private Score score = null; private long timeMillisSpent = -1L; private long scoreCalculationCount = -1L; + private long moveCalculationCount = -1L; private String scoreExplanationSummary = null; // ************************************************************************ @@ -161,6 +162,14 @@ public void setScoreCalculationCount(long scoreCalculationCount) { this.scoreCalculationCount = scoreCalculationCount; } + public long getMoveCalculationCount() { + return moveCalculationCount; + } + + public void setMoveCalculationCount(long moveCalculationCount) { + this.moveCalculationCount = moveCalculationCount; + } + public String getScoreExplanationSummary() { return scoreExplanationSummary; } @@ -217,6 +226,16 @@ public Long getScoreCalculationSpeed() { return scoreCalculationCount * 1000L / timeMillisSpent; } + @SuppressWarnings("unused") // Used by FreeMarker. + public Long getMoveCalculationSpeed() { + long timeMillisSpent = this.timeMillisSpent; + if (timeMillisSpent == 0L) { + // Avoid divide by zero exception on a fast CPU + timeMillisSpent = 1L; + } + return moveCalculationCount * 1000L / timeMillisSpent; + } + @SuppressWarnings("unused") // Used by FreeMarker. public boolean isWinner() { return ranking != null && ranking.intValue() == 0; @@ -293,6 +312,7 @@ protected static SubSingleBenchmarkResult createMerge( newResult.score = oldResult.score; newResult.timeMillisSpent = oldResult.timeMillisSpent; newResult.scoreCalculationCount = oldResult.scoreCalculationCount; + newResult.moveCalculationCount = oldResult.moveCalculationCount; singleBenchmarkResult.getSubSingleBenchmarkResultList().add(newResult); return newResult; diff --git a/benchmark/src/main/resources/ai/timefold/solver/benchmark/impl/report/benchmarkReport.html.ftl b/benchmark/src/main/resources/ai/timefold/solver/benchmark/impl/report/benchmarkReport.html.ftl index 789858dcf1..caebb7e3af 100644 --- a/benchmark/src/main/resources/ai/timefold/solver/benchmark/impl/report/benchmarkReport.html.ftl +++ b/benchmark/src/main/resources/ai/timefold/solver/benchmark/impl/report/benchmarkReport.html.ftl @@ -362,6 +362,9 @@
  • +
  • + +
  • @@ -444,6 +447,74 @@ +
    +

    Move calculation speed summary

    +

    + Useful for comparing different score calculators and/or constraint implementations + (presuming that the solver configurations do not differ otherwise). + Also useful to measure the scalability cost of an extra constraint. +

    + <@addChart chart=benchmarkReport.moveCalculationSpeedSummaryChart /> +
    + + + + + + + + + <#list benchmarkReport.plannerBenchmarkResult.unifiedProblemBenchmarkResultList as problemBenchmarkResult> + + + + + + + <#list benchmarkReport.plannerBenchmarkResult.unifiedProblemBenchmarkResultList as problemBenchmarkResult> + + + + + + <#list benchmarkReport.plannerBenchmarkResult.solverBenchmarkResultList as solverBenchmarkResult> + class="table-success"> + + + <#list benchmarkReport.plannerBenchmarkResult.unifiedProblemBenchmarkResultList as problemBenchmarkResult> + <#if !solverBenchmarkResult.findSingleBenchmark(problemBenchmarkResult)??> + + <#else> + <#assign singleBenchmarkResult = solverBenchmarkResult.findSingleBenchmark(problemBenchmarkResult)> + <#if !singleBenchmarkResult.hasAllSuccess()> + + <#else> + <#if solverBenchmarkResult.subSingleCount lte 1> + + <#else> + + + + + + + + +
    SolverAverageProblem
    ${problemBenchmarkResult.name}
    Problem scale${benchmarkReport.plannerBenchmarkResult.averageProblemScale!""}${problemBenchmarkResult.problemScale!""}
    ${solverBenchmarkResult.name} <@addSolverBenchmarkBadges solverBenchmarkResult=solverBenchmarkResult/>${solverBenchmarkResult.averageMoveCalculationSpeed!""}/sFailed${singleBenchmarkResult.moveCalculationSpeed}/s + + + + +
    +
    +

    Worst score calculation speed difference percentage

    From 1027683f83dab87492706f6b555ddf15155ed268 Mon Sep 17 00:00:00 2001 From: Fred Date: Fri, 6 Sep 2024 11:35:15 -0300 Subject: [PATCH 05/26] feat: add termination config --- core/src/build/revapi-differences.json | 11 +++ .../solver/termination/TerminationConfig.java | 37 +++++++++ .../solver/core/impl/phase/AbstractPhase.java | 1 + .../impl/phase/scope/AbstractPhaseScope.java | 10 +++ .../impl/phase/scope/AbstractStepScope.java | 10 +++ .../solver/recaller/BestSolutionRecaller.java | 3 + .../termination/AbstractTermination.java | 3 +- .../termination/MoveCountTermination.java | 76 +++++++++++++++++++ .../termination/TerminationFactory.java | 7 +- .../UnimprovedMoveCountTermination.java | 73 ++++++++++++++++++ core/src/main/resources/solver.xsd | 4 + .../termination/MoveCountTerminationTest.java | 42 ++++++++++ .../UnimprovedMoveCountTerminationTest.java | 43 +++++++++++ 13 files changed, 318 insertions(+), 2 deletions(-) create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/solver/termination/MoveCountTermination.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/solver/termination/UnimprovedMoveCountTermination.java create mode 100644 core/src/test/java/ai/timefold/solver/core/impl/solver/termination/MoveCountTerminationTest.java create mode 100644 core/src/test/java/ai/timefold/solver/core/impl/solver/termination/UnimprovedMoveCountTerminationTest.java diff --git a/core/src/build/revapi-differences.json b/core/src/build/revapi-differences.json index 0b30ca6d69..01f71c4848 100644 --- a/core/src/build/revapi-differences.json +++ b/core/src/build/revapi-differences.json @@ -46,6 +46,17 @@ "oldValue": "{ai.timefold.solver.core.config.heuristic.selector.move.composite.CartesianProductMoveSelectorConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.generic.ChangeMoveSelectorConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.generic.list.kopt.KOptListMoveSelectorConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListChangeMoveSelectorConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListSwapMoveSelectorConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.factory.MoveIteratorFactoryConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.factory.MoveListFactoryConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.generic.PillarChangeMoveSelectorConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.generic.PillarSwapMoveSelectorConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.SubChainChangeMoveSelectorConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.SubChainSwapMoveSelectorConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.generic.list.SubListChangeMoveSelectorConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.generic.list.SubListSwapMoveSelectorConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.generic.SwapMoveSelectorConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.TailChainSwapMoveSelectorConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.composite.UnionMoveSelectorConfig.class}", "newValue": "{ai.timefold.solver.core.config.heuristic.selector.move.composite.CartesianProductMoveSelectorConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.generic.ChangeMoveSelectorConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.generic.list.kopt.KOptListMoveSelectorConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListChangeMoveSelectorConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListSwapMoveSelectorConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.factory.MoveIteratorFactoryConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.factory.MoveListFactoryConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.generic.PillarChangeMoveSelectorConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.generic.PillarSwapMoveSelectorConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.generic.RuinRecreateMoveSelectorConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListRuinRecreateMoveSelectorConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.SubChainChangeMoveSelectorConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.SubChainSwapMoveSelectorConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.generic.list.SubListChangeMoveSelectorConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.generic.list.SubListSwapMoveSelectorConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.generic.SwapMoveSelectorConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.TailChainSwapMoveSelectorConfig.class, ai.timefold.solver.core.config.heuristic.selector.move.composite.UnionMoveSelectorConfig.class}", "justification": "Add support for list ruin moves" + }, + { + "ignore": true, + "code": "java.annotation.attributeValueChanged", + "old": "class ai.timefold.solver.core.config.solver.termination.TerminationConfig", + "new": "class ai.timefold.solver.core.config.solver.termination.TerminationConfig", + "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\", \"unimprovedMoveCountLimit\", \"terminationConfigList\"}", + "justification": "Add new termination configuration" } ] } 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 ad9e8e520e..a4b46495f3 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 @@ -35,6 +35,8 @@ "stepCountLimit", "unimprovedStepCountLimit", "scoreCalculationCountLimit", + "moveCountLimit", + "unimprovedMoveCountLimit", "terminationConfigList" }) public class TerminationConfig extends AbstractConfig { @@ -72,6 +74,9 @@ public class TerminationConfig extends AbstractConfig { private Long scoreCalculationCountLimit = null; + private Long moveCountLimit = null; + private Long unimprovedMoveCountLimit = null; + @XmlElement(name = "termination") private List terminationConfigList = null; @@ -243,6 +248,22 @@ public void setScoreCalculationCountLimit(Long scoreCalculationCountLimit) { this.scoreCalculationCountLimit = scoreCalculationCountLimit; } + public Long getMoveCountLimit() { + return moveCountLimit; + } + + public void setMoveCountLimit(Long moveCountLimit) { + this.moveCountLimit = moveCountLimit; + } + + public Long getUnimprovedMoveCountLimit() { + return unimprovedMoveCountLimit; + } + + public void setUnimprovedMoveCountLimit(Long unimprovedMoveCountLimit) { + this.unimprovedMoveCountLimit = unimprovedMoveCountLimit; + } + public List getTerminationConfigList() { return terminationConfigList; } @@ -359,6 +380,16 @@ public TerminationConfig withScoreCalculationCountLimit(Long scoreCalculationCou return this; } + public TerminationConfig withMoveCountLimit(Long moveCountLimit) { + this.moveCountLimit = moveCountLimit; + return this; + } + + public TerminationConfig withUnimprovedMoveCountLimit(Long unimprovedMoveCountLimit) { + this.unimprovedMoveCountLimit = unimprovedMoveCountLimit; + return this; + } + public TerminationConfig withTerminationConfigList(List terminationConfigList) { this.terminationConfigList = terminationConfigList; return this; @@ -466,6 +497,8 @@ public boolean isConfigured() { stepCountLimit != null || unimprovedStepCountLimit != null || scoreCalculationCountLimit != null || + moveCountLimit != null || + unimprovedMoveCountLimit != null || isTerminationListConfigured(); } @@ -508,6 +541,10 @@ public TerminationConfig inherit(TerminationConfig inheritedConfig) { inheritedConfig.getUnimprovedStepCountLimit()); scoreCalculationCountLimit = ConfigUtils.inheritOverwritableProperty(scoreCalculationCountLimit, inheritedConfig.getScoreCalculationCountLimit()); + moveCountLimit = ConfigUtils.inheritOverwritableProperty(moveCountLimit, + inheritedConfig.getMoveCountLimit()); + unimprovedMoveCountLimit = ConfigUtils.inheritOverwritableProperty(unimprovedMoveCountLimit, + inheritedConfig.getUnimprovedMoveCountLimit()); terminationConfigList = ConfigUtils.inheritMergeableListConfig( terminationConfigList, inheritedConfig.getTerminationConfigList()); return this; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java index 1687731220..c643dc2e2e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java @@ -160,6 +160,7 @@ protected > void predictWorkingStepScore(AbstractSt @Override public void stepEnded(AbstractStepScope stepScope) { solver.stepEnded(stepScope); + stepScope.setMoveCalculationCount(stepScope.getPhaseScope().getPhaseMoveCalculationCount()); if (enableCollectMetrics) { collectMetrics(stepScope); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java index cd395c31bf..f32a4064b0 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java @@ -34,6 +34,8 @@ public abstract class AbstractPhaseScope { protected int bestSolutionStepIndex; + protected long bestSolutionMoveCalculationCount = 0L; + protected boolean enableCollectMetrics = true; /** @@ -91,6 +93,14 @@ public void setBestSolutionStepIndex(int bestSolutionStepIndex) { this.bestSolutionStepIndex = bestSolutionStepIndex; } + public long getBestSolutionMoveCalculationCount() { + return bestSolutionMoveCalculationCount; + } + + public void setBestSolutionMoveCalculationCount(long bestSolutionMoveCalculationCount) { + this.bestSolutionMoveCalculationCount = bestSolutionMoveCalculationCount; + } + public abstract AbstractStepScope getLastCompletedStepScope(); public boolean isEnableCollectMetrics() { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractStepScope.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractStepScope.java index c2cfe39d55..b1a7dbea3b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractStepScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractStepScope.java @@ -13,6 +13,8 @@ public abstract class AbstractStepScope { protected final int stepIndex; + protected long moveCalculationCount = 0L; + protected Score score = null; protected boolean bestScoreImproved = false; // Stays null if there is no need to clone it @@ -28,6 +30,14 @@ public int getStepIndex() { return stepIndex; } + public long getMoveCalculationCount() { + return moveCalculationCount; + } + + public void setMoveCalculationCount(long moveCalculationCount) { + this.moveCalculationCount = moveCalculationCount; + } + public Score getScore() { return score; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/recaller/BestSolutionRecaller.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/recaller/BestSolutionRecaller.java index bc05f55fe2..291994aba6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/recaller/BestSolutionRecaller.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/recaller/BestSolutionRecaller.java @@ -70,6 +70,7 @@ public void processWorkingSolutionDuringConstructionHeuristicsStep(AbstractStepS SolverScope solverScope = phaseScope.getSolverScope(); stepScope.setBestScoreImproved(true); phaseScope.setBestSolutionStepIndex(stepScope.getStepIndex()); + phaseScope.setBestSolutionMoveCalculationCount(phaseScope.getPhaseMoveCalculationCount()); Solution_ newBestSolution = stepScope.getWorkingSolution(); // Construction heuristics don't fire intermediate best solution changed events. // But the best solution and score are updated, so that unimproved* terminations work correctly. @@ -84,6 +85,7 @@ public void processWorkingSolutionDuringStep(AbstractStepScope stepSc stepScope.setBestScoreImproved(bestScoreImproved); if (bestScoreImproved) { phaseScope.setBestSolutionStepIndex(stepScope.getStepIndex()); + phaseScope.setBestSolutionMoveCalculationCount(phaseScope.getPhaseMoveCalculationCount()); Solution_ newBestSolution = stepScope.createOrGetClonedSolution(); updateBestSolutionAndFire(solverScope, score, newBestSolution); } else if (assertBestScoreIsUnmodified) { @@ -102,6 +104,7 @@ public void processWorkingSolutionDuringMove(Score score, AbstractStepScope permits AbstractCompositeTermination, BasicPlumbingTermination, BestScoreFeasibleTermination, BestScoreTermination, ChildThreadPlumbingTermination, PhaseToSolverTerminationBridge, ScoreCalculationCountTermination, StepCountTermination, TimeMillisSpentTermination, UnimprovedStepCountTermination, - UnimprovedTimeMillisSpentScoreDifferenceThresholdTermination, UnimprovedTimeMillisSpentTermination { + UnimprovedTimeMillisSpentScoreDifferenceThresholdTermination, UnimprovedTimeMillisSpentTermination, + MoveCountTermination, UnimprovedMoveCountTermination { protected final transient Logger logger = LoggerFactory.getLogger(getClass()); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/MoveCountTermination.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/MoveCountTermination.java new file mode 100644 index 0000000000..2dcfb8efdd --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/MoveCountTermination.java @@ -0,0 +1,76 @@ +package ai.timefold.solver.core.impl.solver.termination; + +import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; +import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; +import ai.timefold.solver.core.impl.solver.thread.ChildThreadType; + +public final class MoveCountTermination extends AbstractTermination { + + private final long moveCountLimit; + + public MoveCountTermination(long moveCountLimit) { + this.moveCountLimit = moveCountLimit; + if (moveCountLimit < 0L) { + throw new IllegalArgumentException("The moveCountLimit (%d) cannot be negative.".formatted(moveCountLimit)); + } + } + + public long getMoveCountLimit() { + return moveCountLimit; + } + + // ************************************************************************ + // Terminated methods + // ************************************************************************ + + @Override + public boolean isSolverTerminated(SolverScope solverScope) { + return isTerminated(solverScope.getScoreDirector()); + } + + @Override + public boolean isPhaseTerminated(AbstractPhaseScope phaseScope) { + return isTerminated(phaseScope.getScoreDirector()); + } + + private boolean isTerminated(InnerScoreDirector scoreDirector) { + long moveCalculationCount = scoreDirector.getMoveCalculationCount(); + return moveCalculationCount >= moveCountLimit; + } + + // ************************************************************************ + // Time gradient methods + // ************************************************************************ + + @Override + public double calculateSolverTimeGradient(SolverScope solverScope) { + return calculateTimeGradient(solverScope.getScoreDirector()); + } + + @Override + public double calculatePhaseTimeGradient(AbstractPhaseScope phaseScope) { + return calculateTimeGradient(phaseScope.getScoreDirector()); + } + + private double calculateTimeGradient(InnerScoreDirector scoreDirector) { + var moveCalculationCount = scoreDirector.getMoveCalculationCount(); + var timeGradient = moveCalculationCount / ((double) moveCountLimit); + return Math.min(timeGradient, 1.0); + } + // ************************************************************************ + // Other methods + // ************************************************************************ + + @Override + public MoveCountTermination createChildThreadTermination(SolverScope solverScope, + ChildThreadType childThreadType) { + return new MoveCountTermination<>(moveCountLimit); + } + + @Override + public String toString() { + return "MoveCount(%d)".formatted(moveCountLimit); + } + +} 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 f5fae18224..7c3e8c9261 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 @@ -83,7 +83,12 @@ public > Termination buildTermination( if (terminationConfig.getUnimprovedStepCountLimit() != null) { terminationList.add(new UnimprovedStepCountTermination<>(terminationConfig.getUnimprovedStepCountLimit())); } - + if (terminationConfig.getMoveCountLimit() != null) { + terminationList.add(new MoveCountTermination<>(terminationConfig.getMoveCountLimit())); + } + if (terminationConfig.getUnimprovedMoveCountLimit() != null) { + terminationList.add(new UnimprovedMoveCountTermination<>(terminationConfig.getUnimprovedMoveCountLimit())); + } terminationList.addAll(buildInnerTermination(configPolicy)); return buildTerminationFromList(terminationList); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/UnimprovedMoveCountTermination.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/UnimprovedMoveCountTermination.java new file mode 100644 index 0000000000..e2823326a4 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/UnimprovedMoveCountTermination.java @@ -0,0 +1,73 @@ +package ai.timefold.solver.core.impl.solver.termination; + +import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; +import ai.timefold.solver.core.impl.solver.thread.ChildThreadType; + +public final class UnimprovedMoveCountTermination extends AbstractTermination { + + private final long unimprovedMoveCountLimit; + + public UnimprovedMoveCountTermination(long unimprovedMoveCountLimit) { + this.unimprovedMoveCountLimit = unimprovedMoveCountLimit; + if (unimprovedMoveCountLimit < 0) { + throw new IllegalArgumentException("The unimprovedMoveCountLimit (%d) cannot be negative." + .formatted(unimprovedMoveCountLimit)); + } + } + + // ************************************************************************ + // Terminated methods + // ************************************************************************ + + @Override + public boolean isSolverTerminated(SolverScope solverScope) { + throw new UnsupportedOperationException( + "%s can only be used for phase termination.".formatted(getClass().getSimpleName())); + } + + @Override + public boolean isPhaseTerminated(AbstractPhaseScope phaseScope) { + var unimprovedMoveCount = calculateUnimprovedMoveCount(phaseScope); + return unimprovedMoveCount >= unimprovedMoveCountLimit; + } + + private static long calculateUnimprovedMoveCount(AbstractPhaseScope phaseScope) { + var bestSolutionMoveCount = phaseScope.getBestSolutionMoveCalculationCount(); + var lastStepMoveCalculationCount = phaseScope.getLastCompletedStepScope().getMoveCalculationCount(); + return lastStepMoveCalculationCount - bestSolutionMoveCount; + } + + // ************************************************************************ + // Time gradient methods + // ************************************************************************ + + @Override + public double calculateSolverTimeGradient(SolverScope solverScope) { + throw new UnsupportedOperationException( + "%s can only be used for phase termination.".formatted(getClass().getSimpleName())); + } + + @Override + public double calculatePhaseTimeGradient(AbstractPhaseScope phaseScope) { + var unimprovedMoveCount = calculateUnimprovedMoveCount(phaseScope); + var timeGradient = unimprovedMoveCount / ((double) unimprovedMoveCountLimit); + return Math.min(timeGradient, 1.0); + } + + // ************************************************************************ + // Other methods + // ************************************************************************ + + @Override + public UnimprovedMoveCountTermination createChildThreadTermination(SolverScope solverScope, + ChildThreadType childThreadType) { + return new UnimprovedMoveCountTermination<>(unimprovedMoveCountLimit); + } + + @Override + public String toString() { + return "unimprovedMoveCountTermination(%d)".formatted(unimprovedMoveCountLimit); + } + +} diff --git a/core/src/main/resources/solver.xsd b/core/src/main/resources/solver.xsd index 1258f1db24..ae1c065011 100644 --- a/core/src/main/resources/solver.xsd +++ b/core/src/main/resources/solver.xsd @@ -195,6 +195,10 @@ + + + + diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/MoveCountTerminationTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/MoveCountTerminationTest.java new file mode 100644 index 0000000000..f74558a986 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/MoveCountTerminationTest.java @@ -0,0 +1,42 @@ +package ai.timefold.solver.core.impl.solver.termination; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.data.Offset.offset; +import static org.mockito.Mockito.when; + +import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; +import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; +import ai.timefold.solver.core.impl.testdata.domain.TestdataSolution; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +class MoveCountTerminationTest { + + @Test + void phaseTermination() { + var termination = new MoveCountTermination(4); + var phaseScope = Mockito.mock(AbstractPhaseScope.class); + var scoreDirector = Mockito.mock(InnerScoreDirector.class); + when(phaseScope.getScoreDirector()).thenReturn(scoreDirector); + + when(scoreDirector.getMoveCalculationCount()).thenReturn(0L); + assertThat(termination.isPhaseTerminated(phaseScope)).isFalse(); + assertThat(termination.calculatePhaseTimeGradient(phaseScope)).isEqualTo(0.0, offset(0.0)); + when(scoreDirector.getMoveCalculationCount()).thenReturn(1L); + assertThat(termination.isPhaseTerminated(phaseScope)).isFalse(); + assertThat(termination.calculatePhaseTimeGradient(phaseScope)).isEqualTo(0.25, offset(0.0)); + when(scoreDirector.getMoveCalculationCount()).thenReturn(2L); + assertThat(termination.isPhaseTerminated(phaseScope)).isFalse(); + assertThat(termination.calculatePhaseTimeGradient(phaseScope)).isEqualTo(0.5, offset(0.0)); + when(scoreDirector.getMoveCalculationCount()).thenReturn(3L); + assertThat(termination.isPhaseTerminated(phaseScope)).isFalse(); + assertThat(termination.calculatePhaseTimeGradient(phaseScope)).isEqualTo(0.75, offset(0.0)); + when(scoreDirector.getMoveCalculationCount()).thenReturn(4L); + assertThat(termination.isPhaseTerminated(phaseScope)).isTrue(); + assertThat(termination.calculatePhaseTimeGradient(phaseScope)).isEqualTo(1.0, offset(0.0)); + when(scoreDirector.getMoveCalculationCount()).thenReturn(5L); + assertThat(termination.isPhaseTerminated(phaseScope)).isTrue(); + assertThat(termination.calculatePhaseTimeGradient(phaseScope)).isEqualTo(1.0, offset(0.0)); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/UnimprovedMoveCountTerminationTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/UnimprovedMoveCountTerminationTest.java new file mode 100644 index 0000000000..44c800e924 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/UnimprovedMoveCountTerminationTest.java @@ -0,0 +1,43 @@ +package ai.timefold.solver.core.impl.solver.termination; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.data.Offset.offset; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; +import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; +import ai.timefold.solver.core.impl.testdata.domain.TestdataSolution; + +import org.junit.jupiter.api.Test; + +class UnimprovedMoveCountTerminationTest { + + @Test + void phaseTermination() { + var termination = new UnimprovedMoveCountTermination(4); + var phaseScope = mock(AbstractPhaseScope.class); + var lastCompletedStepScope = mock(AbstractStepScope.class); + when(phaseScope.getLastCompletedStepScope()).thenReturn(lastCompletedStepScope); + + when(phaseScope.getBestSolutionMoveCalculationCount()).thenReturn(10L); + when(lastCompletedStepScope.getMoveCalculationCount()).thenReturn(10L); + assertThat(termination.isPhaseTerminated(phaseScope)).isFalse(); + assertThat(termination.calculatePhaseTimeGradient(phaseScope)).isEqualTo(0.0, offset(0.0)); + when(lastCompletedStepScope.getMoveCalculationCount()).thenReturn(11L); + assertThat(termination.isPhaseTerminated(phaseScope)).isFalse(); + assertThat(termination.calculatePhaseTimeGradient(phaseScope)).isEqualTo(0.25, offset(0.0)); + when(lastCompletedStepScope.getMoveCalculationCount()).thenReturn(12L); + assertThat(termination.isPhaseTerminated(phaseScope)).isFalse(); + assertThat(termination.calculatePhaseTimeGradient(phaseScope)).isEqualTo(0.5, offset(0.0)); + when(lastCompletedStepScope.getMoveCalculationCount()).thenReturn(13L); + assertThat(termination.isPhaseTerminated(phaseScope)).isFalse(); + assertThat(termination.calculatePhaseTimeGradient(phaseScope)).isEqualTo(0.75, offset(0.0)); + when(lastCompletedStepScope.getMoveCalculationCount()).thenReturn(14L); + assertThat(termination.isPhaseTerminated(phaseScope)).isTrue(); + assertThat(termination.calculatePhaseTimeGradient(phaseScope)).isEqualTo(1.0, offset(0.0)); + when(lastCompletedStepScope.getMoveCalculationCount()).thenReturn(16L); + assertThat(termination.isPhaseTerminated(phaseScope)).isTrue(); + assertThat(termination.calculatePhaseTimeGradient(phaseScope)).isEqualTo(1.0, offset(0.0)); + } +} From 261b46b7cca9ae44beb25b9ef585c834a1b1c297 Mon Sep 17 00:00:00 2001 From: Fred Date: Fri, 6 Sep 2024 11:47:23 -0300 Subject: [PATCH 06/26] fix: NPE when checking the move count --- .../solver/core/impl/phase/scope/AbstractPhaseScope.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java index f32a4064b0..55c8fc2a67 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java @@ -169,7 +169,11 @@ public long getPhaseScoreCalculationCount() { } public long getPhaseMoveCalculationCount() { - return endingMoveCalculationCount - startingMoveCalculationCount + childThreadsMoveCalculationCount; + var currentMoveCalculationCount = endingMoveCalculationCount; + if (endingMoveCalculationCount == null) { + currentMoveCalculationCount = getScoreDirector().getMoveCalculationCount(); + } + return currentMoveCalculationCount - startingMoveCalculationCount + childThreadsMoveCalculationCount; } /** From de34ea494925bb7bc72119d54d8b316b50d647b3 Mon Sep 17 00:00:00 2001 From: Fred Date: Fri, 6 Sep 2024 14:21:56 -0300 Subject: [PATCH 07/26] chore: minor improvement --- .../MoveCalculationSpeedProblemStatistic.java | 12 ++++++------ .../MoveCalculationSpeedSubSingleStatistic.java | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedProblemStatistic.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedProblemStatistic.java index 525b49834e..e5f4862e47 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedProblemStatistic.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedProblemStatistic.java @@ -33,15 +33,15 @@ public MoveCalculationSpeedProblemStatistic(ProblemBenchmarkResult problemBen */ @Override protected List> generateCharts(BenchmarkReport benchmarkReport) { - LineChart.Builder builder = new LineChart.Builder<>(); - for (SingleBenchmarkResult singleBenchmarkResult : problemBenchmarkResult.getSingleBenchmarkResultList()) { - String solverLabel = singleBenchmarkResult.getSolverBenchmarkResult().getNameWithFavoriteSuffix(); + var builder = new LineChart.Builder(); + for (var singleBenchmarkResult : problemBenchmarkResult.getSingleBenchmarkResultList()) { + var solverLabel = singleBenchmarkResult.getSolverBenchmarkResult().getNameWithFavoriteSuffix(); if (singleBenchmarkResult.hasAllSuccess()) { var subSingleStatistic = singleBenchmarkResult.getSubSingleStatistic(problemStatisticType); List points = subSingleStatistic.getPointList(); - for (MoveCalculationSpeedStatisticPoint point : points) { - long timeMillisSpent = point.getTimeMillisSpent(); - long moveCalculationSpeed = point.getMoveCalculationSpeed(); + for (var point : points) { + var timeMillisSpent = point.getTimeMillisSpent(); + var moveCalculationSpeed = point.getMoveCalculationSpeed(); builder.add(solverLabel, timeMillisSpent, moveCalculationSpeed); } } diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedSubSingleStatistic.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedSubSingleStatistic.java index 7119ebe766..764c3e0539 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedSubSingleStatistic.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedSubSingleStatistic.java @@ -44,21 +44,21 @@ public MoveCalculationSpeedSubSingleStatistic(SubSingleBenchmarkResult benchmark public void open(StatisticRegistry registry, Tags runTag) { registry.addListener(SolverMetric.MOVE_CALCULATION_COUNT, new Consumer<>() { long nextTimeMillisThreshold = timeMillisThresholdInterval; - long lastTimeMillisSpent = 0; + long lastTimeMillisSpent = 0L; final AtomicLong lastMoveCalculationCount = new AtomicLong(0); @Override public void accept(Long timeMillisSpent) { if (timeMillisSpent >= nextTimeMillisThreshold) { registry.getGaugeValue(SolverMetric.MOVE_CALCULATION_COUNT, runTag, moveCalculationCountNumber -> { - long moveCalculationCount = moveCalculationCountNumber.longValue(); - long calculationCountInterval = moveCalculationCount - lastMoveCalculationCount.get(); - long timeMillisSpentInterval = timeMillisSpent - lastTimeMillisSpent; + var moveCalculationCount = moveCalculationCountNumber.longValue(); + var calculationCountInterval = moveCalculationCount - lastMoveCalculationCount.get(); + var timeMillisSpentInterval = timeMillisSpent - lastTimeMillisSpent; if (timeMillisSpentInterval == 0L) { // Avoid divide by zero exception on a fast CPU timeMillisSpentInterval = 1L; } - long moveCalculationSpeed = calculationCountInterval * 1000L / timeMillisSpentInterval; + var moveCalculationSpeed = calculationCountInterval * 1000L / timeMillisSpentInterval; pointList.add(new MoveCalculationSpeedStatisticPoint(timeMillisSpent, moveCalculationSpeed)); lastMoveCalculationCount.set(moveCalculationCount); }); From 1bfbd3e5d1e34d55bcd721cc247dd6b67932a08a Mon Sep 17 00:00:00 2001 From: Fred Date: Fri, 6 Sep 2024 15:17:33 -0300 Subject: [PATCH 08/26] chore: PR comments --- .../statistic/ProblemStatisticType.java | 4 +- .../impl/result/ProblemBenchmarkResult.java | 4 +- .../impl/statistic/ProblemStatistic.java | 4 +- ...actCalculationSpeedSubSingleStatistic.java | 78 +++++++++++++++++++ ...AbstractTimeLineChartProblemStatistic.java | 50 ++++++++++++ .../statistic/common/LongStatisticPoint.java | 28 +++++++ .../MoveCalculationSpeedProblemStatistic.java | 56 ------------- ...eCalculationSpeedProblemStatisticTime.java | 25 ++++++ .../MoveCalculationSpeedStatisticPoint.java | 28 ------- ...oveCalculationSpeedSubSingleStatistic.java | 68 ++-------------- ...ScoreCalculationSpeedProblemStatistic.java | 42 ++-------- .../ScoreCalculationSpeedStatisticPoint.java | 28 ------- ...oreCalculationSpeedSubSingleStatistic.java | 68 ++-------------- benchmark/src/main/resources/benchmark.xsd | 6 ++ ...alculationSpeedSubSingleStatisticTest.java | 12 +-- ...alculationSpeedSubSingleStatisticTest.java | 11 +-- .../result/testPlannerBenchmarkResult.xml | 24 ++++++ 17 files changed, 245 insertions(+), 291 deletions(-) create mode 100644 benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/common/AbstractCalculationSpeedSubSingleStatistic.java create mode 100644 benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/common/AbstractTimeLineChartProblemStatistic.java create mode 100644 benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/common/LongStatisticPoint.java delete mode 100644 benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedProblemStatistic.java create mode 100644 benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedProblemStatisticTime.java delete mode 100644 benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedStatisticPoint.java delete mode 100644 benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/scorecalculationspeed/ScoreCalculationSpeedStatisticPoint.java diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/config/statistic/ProblemStatisticType.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/config/statistic/ProblemStatisticType.java index b44536f51c..2f54c0c266 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/config/statistic/ProblemStatisticType.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/config/statistic/ProblemStatisticType.java @@ -10,7 +10,7 @@ import ai.timefold.solver.benchmark.impl.statistic.bestscore.BestScoreProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.bestsolutionmutation.BestSolutionMutationProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.memoryuse.MemoryUseProblemStatistic; -import ai.timefold.solver.benchmark.impl.statistic.movecalculationspeed.MoveCalculationSpeedProblemStatistic; +import ai.timefold.solver.benchmark.impl.statistic.movecalculationspeed.MoveCalculationSpeedProblemStatisticTime; import ai.timefold.solver.benchmark.impl.statistic.movecountperstep.MoveCountPerStepProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.scorecalculationspeed.ScoreCalculationSpeedProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.stepscore.StepScoreProblemStatistic; @@ -34,7 +34,7 @@ public ProblemStatistic buildProblemStatistic(ProblemBenchmarkResult problemBenc case SCORE_CALCULATION_SPEED: return new ScoreCalculationSpeedProblemStatistic(problemBenchmarkResult); case MOVE_CALCULATION_SPEED: - return new MoveCalculationSpeedProblemStatistic(problemBenchmarkResult); + return new MoveCalculationSpeedProblemStatisticTime(problemBenchmarkResult); case BEST_SOLUTION_MUTATION: return new BestSolutionMutationProblemStatistic(problemBenchmarkResult); case MOVE_COUNT_PER_STEP: diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/ProblemBenchmarkResult.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/ProblemBenchmarkResult.java index 1768e05894..d6bcb36224 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/ProblemBenchmarkResult.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/ProblemBenchmarkResult.java @@ -32,7 +32,7 @@ import ai.timefold.solver.benchmark.impl.statistic.bestscore.BestScoreProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.bestsolutionmutation.BestSolutionMutationProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.memoryuse.MemoryUseProblemStatistic; -import ai.timefold.solver.benchmark.impl.statistic.movecalculationspeed.MoveCalculationSpeedProblemStatistic; +import ai.timefold.solver.benchmark.impl.statistic.movecalculationspeed.MoveCalculationSpeedProblemStatisticTime; import ai.timefold.solver.benchmark.impl.statistic.movecountperstep.MoveCountPerStepProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.scorecalculationspeed.ScoreCalculationSpeedProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.stepscore.StepScoreProblemStatistic; @@ -68,7 +68,7 @@ public class ProblemBenchmarkResult { @XmlElement(name = "bestScoreProblemStatistic", type = BestScoreProblemStatistic.class), @XmlElement(name = "stepScoreProblemStatistic", type = StepScoreProblemStatistic.class), @XmlElement(name = "scoreCalculationSpeedProblemStatistic", type = ScoreCalculationSpeedProblemStatistic.class), - @XmlElement(name = "moveCalculationSpeedProblemStatistic", type = MoveCalculationSpeedProblemStatistic.class), + @XmlElement(name = "moveCalculationSpeedProblemStatistic", type = MoveCalculationSpeedProblemStatisticTime.class), @XmlElement(name = "bestSolutionMutationProblemStatistic", type = BestSolutionMutationProblemStatistic.class), @XmlElement(name = "moveCountPerStepProblemStatistic", type = MoveCountPerStepProblemStatistic.class), @XmlElement(name = "memoryUseProblemStatistic", type = MemoryUseProblemStatistic.class), diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/ProblemStatistic.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/ProblemStatistic.java index db6b7ad73d..33e3d6167a 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/ProblemStatistic.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/ProblemStatistic.java @@ -19,7 +19,7 @@ import ai.timefold.solver.benchmark.impl.statistic.bestscore.BestScoreProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.bestsolutionmutation.BestSolutionMutationProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.memoryuse.MemoryUseProblemStatistic; -import ai.timefold.solver.benchmark.impl.statistic.movecalculationspeed.MoveCalculationSpeedProblemStatistic; +import ai.timefold.solver.benchmark.impl.statistic.movecalculationspeed.MoveCalculationSpeedProblemStatisticTime; import ai.timefold.solver.benchmark.impl.statistic.movecountperstep.MoveCountPerStepProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.scorecalculationspeed.ScoreCalculationSpeedProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.stepscore.StepScoreProblemStatistic; @@ -32,7 +32,7 @@ BestScoreProblemStatistic.class, StepScoreProblemStatistic.class, ScoreCalculationSpeedProblemStatistic.class, - MoveCalculationSpeedProblemStatistic.class, + MoveCalculationSpeedProblemStatisticTime.class, BestSolutionMutationProblemStatistic.class, MoveCountPerStepProblemStatistic.class, MemoryUseProblemStatistic.class diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/common/AbstractCalculationSpeedSubSingleStatistic.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/common/AbstractCalculationSpeedSubSingleStatistic.java new file mode 100644 index 0000000000..e678978f21 --- /dev/null +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/common/AbstractCalculationSpeedSubSingleStatistic.java @@ -0,0 +1,78 @@ +package ai.timefold.solver.benchmark.impl.statistic.common; + +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; + +import ai.timefold.solver.benchmark.config.statistic.ProblemStatisticType; +import ai.timefold.solver.benchmark.impl.result.SubSingleBenchmarkResult; +import ai.timefold.solver.benchmark.impl.statistic.ProblemBasedSubSingleStatistic; +import ai.timefold.solver.benchmark.impl.statistic.StatisticRegistry; +import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; +import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; + +import io.micrometer.core.instrument.Tags; + +public abstract class AbstractCalculationSpeedSubSingleStatistic + extends ProblemBasedSubSingleStatistic { + + private final SolverMetric solverMetric; + private final long timeMillisThresholdInterval; + + protected AbstractCalculationSpeedSubSingleStatistic(SolverMetric solverMetric, ProblemStatisticType statisticType, + SubSingleBenchmarkResult benchmarkResult, long timeMillisThresholdInterval) { + super(benchmarkResult, statisticType); + if (timeMillisThresholdInterval <= 0L) { + throw new IllegalArgumentException("The timeMillisThresholdInterval (" + timeMillisThresholdInterval + + ") must be bigger than 0."); + } + this.solverMetric = solverMetric; + this.timeMillisThresholdInterval = timeMillisThresholdInterval; + } + + // ************************************************************************ + // Lifecycle methods + // ************************************************************************ + + @Override + public void open(StatisticRegistry registry, Tags runTag) { + registry.addListener(solverMetric, new Consumer<>() { + long nextTimeMillisThreshold = timeMillisThresholdInterval; + long lastTimeMillisSpent = 0L; + final AtomicLong lastCalculationCount = new AtomicLong(0); + + @Override + public void accept(Long timeMillisSpent) { + if (timeMillisSpent >= nextTimeMillisThreshold) { + registry.getGaugeValue(solverMetric, runTag, calculationCountNumber -> { + var moveCalculationCount = calculationCountNumber.longValue(); + var calculationCountInterval = moveCalculationCount - lastCalculationCount.get(); + var timeMillisSpentInterval = timeMillisSpent - lastTimeMillisSpent; + if (timeMillisSpentInterval == 0L) { + // Avoid divide by zero exception on a fast CPU + timeMillisSpentInterval = 1L; + } + var calculationSpeed = calculationCountInterval * 1000L / timeMillisSpentInterval; + pointList.add(new LongStatisticPoint(timeMillisSpent, calculationSpeed)); + lastCalculationCount.set(moveCalculationCount); + }); + lastTimeMillisSpent = timeMillisSpent; + nextTimeMillisThreshold += timeMillisThresholdInterval; + if (nextTimeMillisThreshold < timeMillisSpent) { + nextTimeMillisThreshold = timeMillisSpent; + } + } + } + }); + } + + // ************************************************************************ + // CSV methods + // ************************************************************************ + + @Override + protected LongStatisticPoint createPointFromCsvLine(ScoreDefinition scoreDefinition, + List csvLine) { + return new LongStatisticPoint(Long.parseLong(csvLine.get(0)), Long.parseLong(csvLine.get(1))); + } +} diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/common/AbstractTimeLineChartProblemStatistic.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/common/AbstractTimeLineChartProblemStatistic.java new file mode 100644 index 0000000000..92ecae108b --- /dev/null +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/common/AbstractTimeLineChartProblemStatistic.java @@ -0,0 +1,50 @@ +package ai.timefold.solver.benchmark.impl.statistic.common; + +import static java.util.Collections.singletonList; + +import java.util.List; + +import ai.timefold.solver.benchmark.config.statistic.ProblemStatisticType; +import ai.timefold.solver.benchmark.impl.report.BenchmarkReport; +import ai.timefold.solver.benchmark.impl.report.LineChart; +import ai.timefold.solver.benchmark.impl.result.ProblemBenchmarkResult; +import ai.timefold.solver.benchmark.impl.statistic.ProblemStatistic; + +public abstract class AbstractTimeLineChartProblemStatistic extends ProblemStatistic> { + + private final String reportFileName; + private final String reportTitle; + private final String yLabel; + + protected AbstractTimeLineChartProblemStatistic(ProblemStatisticType statisticType, + ProblemBenchmarkResult problemBenchmarkResult, String reportFileName, String reportTitle, String yLabel) { + super(problemBenchmarkResult, statisticType); + this.reportFileName = reportFileName; + this.reportTitle = reportTitle; + this.yLabel = yLabel; + } + + /** + * @return never null + */ + @Override + protected List> generateCharts(BenchmarkReport benchmarkReport) { + var builder = new LineChart.Builder(); + for (var singleBenchmarkResult : problemBenchmarkResult.getSingleBenchmarkResultList()) { + var solverLabel = singleBenchmarkResult.getSolverBenchmarkResult().getNameWithFavoriteSuffix(); + if (singleBenchmarkResult.hasAllSuccess()) { + var subSingleStatistic = singleBenchmarkResult.getSubSingleStatistic(problemStatisticType); + List points = subSingleStatistic.getPointList(); + for (var point : points) { + var timeMillisSpent = point.getTimeMillisSpent(); + var calculationSpeed = point.getValue(); + builder.add(solverLabel, timeMillisSpent, calculationSpeed); + } + } + if (singleBenchmarkResult.getSolverBenchmarkResult().isFavorite()) { + builder.markFavorite(solverLabel); + } + } + return singletonList(builder.build(reportFileName, reportTitle, "Time spent", yLabel, false, true, false)); + } +} diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/common/LongStatisticPoint.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/common/LongStatisticPoint.java new file mode 100644 index 0000000000..973a00332a --- /dev/null +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/common/LongStatisticPoint.java @@ -0,0 +1,28 @@ +package ai.timefold.solver.benchmark.impl.statistic.common; + +import ai.timefold.solver.benchmark.impl.statistic.StatisticPoint; + +public class LongStatisticPoint extends StatisticPoint { + + private final long timeMillisSpent; + private final long value; + + public LongStatisticPoint(long timeMillisSpent, long value) { + this.timeMillisSpent = timeMillisSpent; + this.value = value; + } + + public long getTimeMillisSpent() { + return timeMillisSpent; + } + + public long getValue() { + return value; + } + + @Override + public String toCsvLine() { + return buildCsvLineWithLongs(timeMillisSpent, value); + } + +} diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedProblemStatistic.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedProblemStatistic.java deleted file mode 100644 index e5f4862e47..0000000000 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedProblemStatistic.java +++ /dev/null @@ -1,56 +0,0 @@ -package ai.timefold.solver.benchmark.impl.statistic.movecalculationspeed; - -import static java.util.Collections.singletonList; - -import java.util.List; - -import ai.timefold.solver.benchmark.config.statistic.ProblemStatisticType; -import ai.timefold.solver.benchmark.impl.report.BenchmarkReport; -import ai.timefold.solver.benchmark.impl.report.LineChart; -import ai.timefold.solver.benchmark.impl.result.ProblemBenchmarkResult; -import ai.timefold.solver.benchmark.impl.result.SingleBenchmarkResult; -import ai.timefold.solver.benchmark.impl.result.SubSingleBenchmarkResult; -import ai.timefold.solver.benchmark.impl.statistic.ProblemStatistic; -import ai.timefold.solver.benchmark.impl.statistic.SubSingleStatistic; - -public class MoveCalculationSpeedProblemStatistic extends ProblemStatistic> { - - private MoveCalculationSpeedProblemStatistic() { - // For JAXB. - } - - public MoveCalculationSpeedProblemStatistic(ProblemBenchmarkResult problemBenchmarkResult) { - super(problemBenchmarkResult, ProblemStatisticType.MOVE_CALCULATION_SPEED); - } - - @Override - public SubSingleStatistic createSubSingleStatistic(SubSingleBenchmarkResult subSingleBenchmarkResult) { - return new MoveCalculationSpeedSubSingleStatistic<>(subSingleBenchmarkResult); - } - - /** - * @return never null - */ - @Override - protected List> generateCharts(BenchmarkReport benchmarkReport) { - var builder = new LineChart.Builder(); - for (var singleBenchmarkResult : problemBenchmarkResult.getSingleBenchmarkResultList()) { - var solverLabel = singleBenchmarkResult.getSolverBenchmarkResult().getNameWithFavoriteSuffix(); - if (singleBenchmarkResult.hasAllSuccess()) { - var subSingleStatistic = singleBenchmarkResult.getSubSingleStatistic(problemStatisticType); - List points = subSingleStatistic.getPointList(); - for (var point : points) { - var timeMillisSpent = point.getTimeMillisSpent(); - var moveCalculationSpeed = point.getMoveCalculationSpeed(); - builder.add(solverLabel, timeMillisSpent, moveCalculationSpeed); - } - } - if (singleBenchmarkResult.getSolverBenchmarkResult().isFavorite()) { - builder.markFavorite(solverLabel); - } - } - return singletonList(builder.build("moveCalculationSpeedProblemStatisticChart", - problemBenchmarkResult.getName() + " move calculation speed statistic", "Time spent", - "Move calculation speed per second", false, true, false)); - } -} diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedProblemStatisticTime.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedProblemStatisticTime.java new file mode 100644 index 0000000000..896cd9656b --- /dev/null +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedProblemStatisticTime.java @@ -0,0 +1,25 @@ +package ai.timefold.solver.benchmark.impl.statistic.movecalculationspeed; + +import ai.timefold.solver.benchmark.config.statistic.ProblemStatisticType; +import ai.timefold.solver.benchmark.impl.result.ProblemBenchmarkResult; +import ai.timefold.solver.benchmark.impl.result.SubSingleBenchmarkResult; +import ai.timefold.solver.benchmark.impl.statistic.SubSingleStatistic; +import ai.timefold.solver.benchmark.impl.statistic.common.AbstractTimeLineChartProblemStatistic; + +public class MoveCalculationSpeedProblemStatisticTime extends AbstractTimeLineChartProblemStatistic { + + private MoveCalculationSpeedProblemStatisticTime() { + // For JAXB. + this(null); + } + + public MoveCalculationSpeedProblemStatisticTime(ProblemBenchmarkResult problemBenchmarkResult) { + super(ProblemStatisticType.MOVE_CALCULATION_SPEED, problemBenchmarkResult, "moveCalculationSpeedProblemStatisticChart", + problemBenchmarkResult.getName() + " move calculation speed statistic", "Move calculation speed per second"); + } + + @Override + public SubSingleStatistic createSubSingleStatistic(SubSingleBenchmarkResult subSingleBenchmarkResult) { + return new MoveCalculationSpeedSubSingleStatistic<>(subSingleBenchmarkResult); + } +} diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedStatisticPoint.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedStatisticPoint.java deleted file mode 100644 index dbd95f615f..0000000000 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedStatisticPoint.java +++ /dev/null @@ -1,28 +0,0 @@ -package ai.timefold.solver.benchmark.impl.statistic.movecalculationspeed; - -import ai.timefold.solver.benchmark.impl.statistic.StatisticPoint; - -public class MoveCalculationSpeedStatisticPoint extends StatisticPoint { - - private final long timeMillisSpent; - private final long moveCalculationSpeed; - - public MoveCalculationSpeedStatisticPoint(long timeMillisSpent, long moveCalculationSpeed) { - this.timeMillisSpent = timeMillisSpent; - this.moveCalculationSpeed = moveCalculationSpeed; - } - - public long getTimeMillisSpent() { - return timeMillisSpent; - } - - public long getMoveCalculationSpeed() { - return moveCalculationSpeed; - } - - @Override - public String toCsvLine() { - return buildCsvLineWithLongs(timeMillisSpent, moveCalculationSpeed); - } - -} diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedSubSingleStatistic.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedSubSingleStatistic.java index 764c3e0539..3e5f0f636b 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedSubSingleStatistic.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedSubSingleStatistic.java @@ -1,26 +1,16 @@ package ai.timefold.solver.benchmark.impl.statistic.movecalculationspeed; -import java.util.List; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Consumer; - import ai.timefold.solver.benchmark.config.statistic.ProblemStatisticType; import ai.timefold.solver.benchmark.impl.result.SubSingleBenchmarkResult; -import ai.timefold.solver.benchmark.impl.statistic.ProblemBasedSubSingleStatistic; import ai.timefold.solver.benchmark.impl.statistic.StatisticPoint; -import ai.timefold.solver.benchmark.impl.statistic.StatisticRegistry; +import ai.timefold.solver.benchmark.impl.statistic.common.AbstractCalculationSpeedSubSingleStatistic; import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; -import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; - -import io.micrometer.core.instrument.Tags; -public class MoveCalculationSpeedSubSingleStatistic - extends ProblemBasedSubSingleStatistic { - - private long timeMillisThresholdInterval; +public class MoveCalculationSpeedSubSingleStatistic extends AbstractCalculationSpeedSubSingleStatistic { private MoveCalculationSpeedSubSingleStatistic() { // For JAXB. + this(null); } public MoveCalculationSpeedSubSingleStatistic(SubSingleBenchmarkResult subSingleBenchmarkResult) { @@ -28,48 +18,8 @@ public MoveCalculationSpeedSubSingleStatistic(SubSingleBenchmarkResult subSingle } public MoveCalculationSpeedSubSingleStatistic(SubSingleBenchmarkResult benchmarkResult, long timeMillisThresholdInterval) { - super(benchmarkResult, ProblemStatisticType.MOVE_CALCULATION_SPEED); - if (timeMillisThresholdInterval <= 0L) { - throw new IllegalArgumentException("The timeMillisThresholdInterval (" + timeMillisThresholdInterval - + ") must be bigger than 0."); - } - this.timeMillisThresholdInterval = timeMillisThresholdInterval; - } - - // ************************************************************************ - // Lifecycle methods - // ************************************************************************ - - @Override - public void open(StatisticRegistry registry, Tags runTag) { - registry.addListener(SolverMetric.MOVE_CALCULATION_COUNT, new Consumer<>() { - long nextTimeMillisThreshold = timeMillisThresholdInterval; - long lastTimeMillisSpent = 0L; - final AtomicLong lastMoveCalculationCount = new AtomicLong(0); - - @Override - public void accept(Long timeMillisSpent) { - if (timeMillisSpent >= nextTimeMillisThreshold) { - registry.getGaugeValue(SolverMetric.MOVE_CALCULATION_COUNT, runTag, moveCalculationCountNumber -> { - var moveCalculationCount = moveCalculationCountNumber.longValue(); - var calculationCountInterval = moveCalculationCount - lastMoveCalculationCount.get(); - var timeMillisSpentInterval = timeMillisSpent - lastTimeMillisSpent; - if (timeMillisSpentInterval == 0L) { - // Avoid divide by zero exception on a fast CPU - timeMillisSpentInterval = 1L; - } - var moveCalculationSpeed = calculationCountInterval * 1000L / timeMillisSpentInterval; - pointList.add(new MoveCalculationSpeedStatisticPoint(timeMillisSpent, moveCalculationSpeed)); - lastMoveCalculationCount.set(moveCalculationCount); - }); - lastTimeMillisSpent = timeMillisSpent; - nextTimeMillisThreshold += timeMillisThresholdInterval; - if (nextTimeMillisThreshold < timeMillisSpent) { - nextTimeMillisThreshold = timeMillisSpent; - } - } - } - }); + super(SolverMetric.MOVE_CALCULATION_COUNT, ProblemStatisticType.MOVE_CALCULATION_SPEED, benchmarkResult, + timeMillisThresholdInterval); } // ************************************************************************ @@ -80,12 +30,4 @@ public void accept(Long timeMillisSpent) { protected String getCsvHeader() { return StatisticPoint.buildCsvLine("timeMillisSpent", "moveCalculationSpeed"); } - - @Override - protected MoveCalculationSpeedStatisticPoint createPointFromCsvLine(ScoreDefinition scoreDefinition, - List csvLine) { - return new MoveCalculationSpeedStatisticPoint(Long.parseLong(csvLine.get(0)), - Long.parseLong(csvLine.get(1))); - } - } diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/scorecalculationspeed/ScoreCalculationSpeedProblemStatistic.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/scorecalculationspeed/ScoreCalculationSpeedProblemStatistic.java index 89432f5540..60a31bec53 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/scorecalculationspeed/ScoreCalculationSpeedProblemStatistic.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/scorecalculationspeed/ScoreCalculationSpeedProblemStatistic.java @@ -1,56 +1,26 @@ package ai.timefold.solver.benchmark.impl.statistic.scorecalculationspeed; -import static java.util.Collections.singletonList; - -import java.util.List; - import ai.timefold.solver.benchmark.config.statistic.ProblemStatisticType; -import ai.timefold.solver.benchmark.impl.report.BenchmarkReport; -import ai.timefold.solver.benchmark.impl.report.LineChart; import ai.timefold.solver.benchmark.impl.result.ProblemBenchmarkResult; -import ai.timefold.solver.benchmark.impl.result.SingleBenchmarkResult; import ai.timefold.solver.benchmark.impl.result.SubSingleBenchmarkResult; -import ai.timefold.solver.benchmark.impl.statistic.ProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.SubSingleStatistic; +import ai.timefold.solver.benchmark.impl.statistic.common.AbstractTimeLineChartProblemStatistic; -public class ScoreCalculationSpeedProblemStatistic extends ProblemStatistic> { +public class ScoreCalculationSpeedProblemStatistic extends AbstractTimeLineChartProblemStatistic { private ScoreCalculationSpeedProblemStatistic() { // For JAXB. + this(null); } public ScoreCalculationSpeedProblemStatistic(ProblemBenchmarkResult problemBenchmarkResult) { - super(problemBenchmarkResult, ProblemStatisticType.SCORE_CALCULATION_SPEED); + super(ProblemStatisticType.SCORE_CALCULATION_SPEED, problemBenchmarkResult, + "scoreCalculationSpeedProblemStatisticChart", + problemBenchmarkResult.getName() + " score calculation speed statistic", "Score calculation speed per second"); } @Override public SubSingleStatistic createSubSingleStatistic(SubSingleBenchmarkResult subSingleBenchmarkResult) { return new ScoreCalculationSpeedSubSingleStatistic(subSingleBenchmarkResult); } - - /** - * @return never null - */ - @Override - protected List> generateCharts(BenchmarkReport benchmarkReport) { - LineChart.Builder builder = new LineChart.Builder<>(); - for (SingleBenchmarkResult singleBenchmarkResult : problemBenchmarkResult.getSingleBenchmarkResultList()) { - String solverLabel = singleBenchmarkResult.getSolverBenchmarkResult().getNameWithFavoriteSuffix(); - if (singleBenchmarkResult.hasAllSuccess()) { - var subSingleStatistic = singleBenchmarkResult.getSubSingleStatistic(problemStatisticType); - List points = subSingleStatistic.getPointList(); - for (ScoreCalculationSpeedStatisticPoint point : points) { - long timeMillisSpent = point.getTimeMillisSpent(); - long scoreCalculationSpeed = point.getScoreCalculationSpeed(); - builder.add(solverLabel, timeMillisSpent, scoreCalculationSpeed); - } - } - if (singleBenchmarkResult.getSolverBenchmarkResult().isFavorite()) { - builder.markFavorite(solverLabel); - } - } - return singletonList(builder.build("scoreCalculationSpeedProblemStatisticChart", - problemBenchmarkResult.getName() + " score calculation speed statistic", "Time spent", - "Score calculation speed per second", false, true, false)); - } } diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/scorecalculationspeed/ScoreCalculationSpeedStatisticPoint.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/scorecalculationspeed/ScoreCalculationSpeedStatisticPoint.java deleted file mode 100644 index ce2ded8b01..0000000000 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/scorecalculationspeed/ScoreCalculationSpeedStatisticPoint.java +++ /dev/null @@ -1,28 +0,0 @@ -package ai.timefold.solver.benchmark.impl.statistic.scorecalculationspeed; - -import ai.timefold.solver.benchmark.impl.statistic.StatisticPoint; - -public class ScoreCalculationSpeedStatisticPoint extends StatisticPoint { - - private final long timeMillisSpent; - private final long scoreCalculationSpeed; - - public ScoreCalculationSpeedStatisticPoint(long timeMillisSpent, long scoreCalculationSpeed) { - this.timeMillisSpent = timeMillisSpent; - this.scoreCalculationSpeed = scoreCalculationSpeed; - } - - public long getTimeMillisSpent() { - return timeMillisSpent; - } - - public long getScoreCalculationSpeed() { - return scoreCalculationSpeed; - } - - @Override - public String toCsvLine() { - return buildCsvLineWithLongs(timeMillisSpent, scoreCalculationSpeed); - } - -} diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/scorecalculationspeed/ScoreCalculationSpeedSubSingleStatistic.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/scorecalculationspeed/ScoreCalculationSpeedSubSingleStatistic.java index e5e4de6c71..2093dcc8d2 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/scorecalculationspeed/ScoreCalculationSpeedSubSingleStatistic.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/scorecalculationspeed/ScoreCalculationSpeedSubSingleStatistic.java @@ -1,26 +1,16 @@ package ai.timefold.solver.benchmark.impl.statistic.scorecalculationspeed; -import java.util.List; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Consumer; - import ai.timefold.solver.benchmark.config.statistic.ProblemStatisticType; import ai.timefold.solver.benchmark.impl.result.SubSingleBenchmarkResult; -import ai.timefold.solver.benchmark.impl.statistic.ProblemBasedSubSingleStatistic; import ai.timefold.solver.benchmark.impl.statistic.StatisticPoint; -import ai.timefold.solver.benchmark.impl.statistic.StatisticRegistry; +import ai.timefold.solver.benchmark.impl.statistic.common.AbstractCalculationSpeedSubSingleStatistic; import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; -import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; - -import io.micrometer.core.instrument.Tags; -public class ScoreCalculationSpeedSubSingleStatistic - extends ProblemBasedSubSingleStatistic { - - private long timeMillisThresholdInterval; +public class ScoreCalculationSpeedSubSingleStatistic extends AbstractCalculationSpeedSubSingleStatistic { private ScoreCalculationSpeedSubSingleStatistic() { // For JAXB. + this(null); } public ScoreCalculationSpeedSubSingleStatistic(SubSingleBenchmarkResult subSingleBenchmarkResult) { @@ -28,48 +18,8 @@ public ScoreCalculationSpeedSubSingleStatistic(SubSingleBenchmarkResult subSingl } public ScoreCalculationSpeedSubSingleStatistic(SubSingleBenchmarkResult benchmarkResult, long timeMillisThresholdInterval) { - super(benchmarkResult, ProblemStatisticType.SCORE_CALCULATION_SPEED); - if (timeMillisThresholdInterval <= 0L) { - throw new IllegalArgumentException("The timeMillisThresholdInterval (" + timeMillisThresholdInterval - + ") must be bigger than 0."); - } - this.timeMillisThresholdInterval = timeMillisThresholdInterval; - } - - // ************************************************************************ - // Lifecycle methods - // ************************************************************************ - - @Override - public void open(StatisticRegistry registry, Tags runTag) { - registry.addListener(SolverMetric.SCORE_CALCULATION_COUNT, new Consumer() { - long nextTimeMillisThreshold = timeMillisThresholdInterval; - long lastTimeMillisSpent = 0; - final AtomicLong lastScoreCalculationCount = new AtomicLong(0); - - @Override - public void accept(Long timeMillisSpent) { - if (timeMillisSpent >= nextTimeMillisThreshold) { - registry.getGaugeValue(SolverMetric.SCORE_CALCULATION_COUNT, runTag, scoreCalculationCountNumber -> { - long scoreCalculationCount = scoreCalculationCountNumber.longValue(); - long calculationCountInterval = scoreCalculationCount - lastScoreCalculationCount.get(); - long timeMillisSpentInterval = timeMillisSpent - lastTimeMillisSpent; - if (timeMillisSpentInterval == 0L) { - // Avoid divide by zero exception on a fast CPU - timeMillisSpentInterval = 1L; - } - long scoreCalculationSpeed = calculationCountInterval * 1000L / timeMillisSpentInterval; - pointList.add(new ScoreCalculationSpeedStatisticPoint(timeMillisSpent, scoreCalculationSpeed)); - lastScoreCalculationCount.set(scoreCalculationCount); - }); - lastTimeMillisSpent = timeMillisSpent; - nextTimeMillisThreshold += timeMillisThresholdInterval; - if (nextTimeMillisThreshold < timeMillisSpent) { - nextTimeMillisThreshold = timeMillisSpent; - } - } - } - }); + super(SolverMetric.SCORE_CALCULATION_COUNT, ProblemStatisticType.SCORE_CALCULATION_SPEED, benchmarkResult, + timeMillisThresholdInterval); } // ************************************************************************ @@ -80,12 +30,4 @@ public void accept(Long timeMillisSpent) { protected String getCsvHeader() { return StatisticPoint.buildCsvLine("timeMillisSpent", "scoreCalculationSpeed"); } - - @Override - protected ScoreCalculationSpeedStatisticPoint createPointFromCsvLine(ScoreDefinition scoreDefinition, - List csvLine) { - return new ScoreCalculationSpeedStatisticPoint(Long.parseLong(csvLine.get(0)), - Long.parseLong(csvLine.get(1))); - } - } diff --git a/benchmark/src/main/resources/benchmark.xsd b/benchmark/src/main/resources/benchmark.xsd index 269a6deab4..048604f5b4 100644 --- a/benchmark/src/main/resources/benchmark.xsd +++ b/benchmark/src/main/resources/benchmark.xsd @@ -581,6 +581,12 @@ + + + + + + diff --git a/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedSubSingleStatisticTest.java b/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedSubSingleStatisticTest.java index 0851435613..d4ee0e6c20 100644 --- a/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedSubSingleStatisticTest.java +++ b/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedSubSingleStatisticTest.java @@ -7,13 +7,13 @@ import ai.timefold.solver.benchmark.impl.result.SubSingleBenchmarkResult; import ai.timefold.solver.benchmark.impl.statistic.AbstractSubSingleStatisticTest; +import ai.timefold.solver.benchmark.impl.statistic.common.LongStatisticPoint; import ai.timefold.solver.core.impl.testdata.domain.TestdataSolution; import org.assertj.core.api.SoftAssertions; public final class MoveCalculationSpeedSubSingleStatisticTest - extends - AbstractSubSingleStatisticTest> { + extends AbstractSubSingleStatisticTest> { @Override protected Function> @@ -22,16 +22,16 @@ public final class MoveCalculationSpeedSubSingleStatisticTest } @Override - protected List getInputPoints() { - return Collections.singletonList(new MoveCalculationSpeedStatisticPoint(Long.MAX_VALUE, Long.MAX_VALUE)); + protected List getInputPoints() { + return Collections.singletonList(new LongStatisticPoint(Long.MAX_VALUE, Long.MAX_VALUE)); } @Override - protected void runTest(SoftAssertions assertions, List outputPoints) { + protected void runTest(SoftAssertions assertions, List outputPoints) { assertions.assertThat(outputPoints) .hasSize(1) .first() - .matches(s -> s.getMoveCalculationSpeed() == Long.MAX_VALUE, "Move calculation speeds do not match.") + .matches(s -> s.getValue() == Long.MAX_VALUE, "Move calculation speeds do not match.") .matches(s -> s.getTimeMillisSpent() == Long.MAX_VALUE, "Millis do not match."); } diff --git a/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/scorecalculationspeed/ScoreCalculationSpeedSubSingleStatisticTest.java b/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/scorecalculationspeed/ScoreCalculationSpeedSubSingleStatisticTest.java index cc448a0927..4a1d11c2e2 100644 --- a/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/scorecalculationspeed/ScoreCalculationSpeedSubSingleStatisticTest.java +++ b/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/scorecalculationspeed/ScoreCalculationSpeedSubSingleStatisticTest.java @@ -6,13 +6,14 @@ import ai.timefold.solver.benchmark.impl.result.SubSingleBenchmarkResult; import ai.timefold.solver.benchmark.impl.statistic.AbstractSubSingleStatisticTest; +import ai.timefold.solver.benchmark.impl.statistic.common.LongStatisticPoint; import ai.timefold.solver.core.impl.testdata.domain.TestdataSolution; import org.assertj.core.api.SoftAssertions; public final class ScoreCalculationSpeedSubSingleStatisticTest extends - AbstractSubSingleStatisticTest> { + AbstractSubSingleStatisticTest> { @Override protected Function> @@ -21,16 +22,16 @@ public final class ScoreCalculationSpeedSubSingleStatisticTest } @Override - protected List getInputPoints() { - return Collections.singletonList(new ScoreCalculationSpeedStatisticPoint(Long.MAX_VALUE, Long.MAX_VALUE)); + protected List getInputPoints() { + return Collections.singletonList(new LongStatisticPoint(Long.MAX_VALUE, Long.MAX_VALUE)); } @Override - protected void runTest(SoftAssertions assertions, List outputPoints) { + protected void runTest(SoftAssertions assertions, List outputPoints) { assertions.assertThat(outputPoints) .hasSize(1) .first() - .matches(s -> s.getScoreCalculationSpeed() == Long.MAX_VALUE, "Score calculation speeds do not match.") + .matches(s -> s.getValue() == Long.MAX_VALUE, "Score calculation speeds do not match.") .matches(s -> s.getTimeMillisSpent() == Long.MAX_VALUE, "Millis do not match."); } diff --git a/benchmark/src/test/resources/ai/timefold/solver/benchmark/impl/result/testPlannerBenchmarkResult.xml b/benchmark/src/test/resources/ai/timefold/solver/benchmark/impl/result/testPlannerBenchmarkResult.xml index 40ae600675..e3438d630a 100644 --- a/benchmark/src/test/resources/ai/timefold/solver/benchmark/impl/result/testPlannerBenchmarkResult.xml +++ b/benchmark/src/test/resources/ai/timefold/solver/benchmark/impl/result/testPlannerBenchmarkResult.xml @@ -35,9 +35,11 @@ 0hard/-37858soft 276 1632 + 500 -1 -1 + -1 @@ -46,9 +48,11 @@ 0hard/-49506soft 60 5832 + 1000 -1 -1 + -1 @@ -75,9 +79,11 @@ 0hard/-22838soft 251 1668 + 1000 -1 -1 + -1 @@ -86,9 +92,11 @@ 0hard/-30857soft 61 5530 + 2000 -1 -1 + -1 @@ -133,9 +141,11 @@ 0hard/-21607soft 5000 777269 + 700000 -1 -1 + -1 @@ -144,9 +154,11 @@ 0hard/-25288soft 5000 856937 + 500000 -1 -1 + -1 @@ -191,9 +203,11 @@ 0hard/-21252soft 5000 761968 + 361000 -1 -1 + -1 @@ -202,9 +216,11 @@ 0hard/-25645soft 5000 858383 + 5580 -1 -1 + -1 @@ -271,9 +287,11 @@ 0hard/-21929soft 5000 993153 + 593000 -1 -1 + -1 @@ -282,9 +300,11 @@ 0hard/-25258soft 5000 850727 + 550000 -1 -1 + -1 @@ -351,9 +371,11 @@ 0hard/-21148soft 5000 873469 + 573000 -1 -1 + -1 @@ -362,9 +384,11 @@ 0hard/-23083soft 5000 726321 + 526000 -1 -1 + -1 From f8364f32f716c24adf91b9ef0aa2070e23072bae Mon Sep 17 00:00:00 2001 From: Fred Date: Fri, 6 Sep 2024 16:11:12 -0300 Subject: [PATCH 09/26] feat: improve the metric logic --- .../decider/ConstructionHeuristicDecider.java | 8 ------- .../heuristic/move/AbstractMetricMove.java | 22 ------------------- .../impl/heuristic/move/AbstractMove.java | 2 +- .../move/AbstractSimplifiedMove.java | 2 +- .../solver/core/impl/heuristic/move/Move.java | 11 ---------- .../impl/heuristic/move/RecordedUndoMove.java | 5 ----- .../solver/core/impl/phase/AbstractPhase.java | 4 +++- .../impl/phase/scope/AbstractPhaseScope.java | 6 ++++- .../score/director/AbstractScoreDirector.java | 15 +++++++++---- .../score/director/InnerScoreDirector.java | 2 +- .../impl/score/director/MetricCollector.java | 10 +++++++++ 11 files changed, 32 insertions(+), 55 deletions(-) delete mode 100644 core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractMetricMove.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/score/director/MetricCollector.java diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/decider/ConstructionHeuristicDecider.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/decider/ConstructionHeuristicDecider.java index 96d4e820fc..a36024b6c4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/decider/ConstructionHeuristicDecider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/decider/ConstructionHeuristicDecider.java @@ -8,7 +8,6 @@ import ai.timefold.solver.core.impl.constructionheuristic.scope.ConstructionHeuristicMoveScope; import ai.timefold.solver.core.impl.constructionheuristic.scope.ConstructionHeuristicPhaseScope; import ai.timefold.solver.core.impl.constructionheuristic.scope.ConstructionHeuristicStepScope; -import ai.timefold.solver.core.impl.heuristic.move.AbstractMetricMove; import ai.timefold.solver.core.impl.heuristic.move.Move; import ai.timefold.solver.core.impl.heuristic.move.NoChangeMove; import ai.timefold.solver.core.impl.heuristic.selector.move.generic.ChangeMove; @@ -104,7 +103,6 @@ public void decideNextStep(ConstructionHeuristicStepScope stepScope, // It will never do anything more complex than that. continue; } - prepare(stepScope, move); var moveScope = new ConstructionHeuristicMoveScope<>(stepScope, moveIndex, move); moveIndex++; doMove(moveScope); @@ -148,10 +146,4 @@ protected > void doMove(ConstructionHeuristicMoveSc } } - private void prepare(ConstructionHeuristicStepScope stepScope, Move move) { - if (move instanceof AbstractMetricMove metricMove) { - metricMove.setCollectMetricEnabled(stepScope.getPhaseScope().isEnableCollectMetrics()); - } - } - } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractMetricMove.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractMetricMove.java deleted file mode 100644 index e9953c00cc..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractMetricMove.java +++ /dev/null @@ -1,22 +0,0 @@ -package ai.timefold.solver.core.impl.heuristic.move; - -import ai.timefold.solver.core.api.domain.solution.PlanningSolution; - -/** - * Abstract superclass for {@link Move} which includes the ability to enable or disable the move metric collection. - * - * @param the solution type, the class with the {@link PlanningSolution} annotation - */ -public abstract class AbstractMetricMove implements Move { - - private boolean collectMetricEnabled = true; - - @Override - public boolean isCollectMetricEnabled() { - return collectMetricEnabled; - } - - public void setCollectMetricEnabled(boolean collectMetricEnabled) { - this.collectMetricEnabled = collectMetricEnabled; - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractMove.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractMove.java index 990a70db09..7f00c75282 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractMove.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractMove.java @@ -15,7 +15,7 @@ * @param the solution type, the class with the {@link PlanningSolution} annotation * @see Move */ -public abstract class AbstractMove extends AbstractMetricMove { +public abstract class AbstractMove implements Move { @Override public final Move doMove(ScoreDirector scoreDirector) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractSimplifiedMove.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractSimplifiedMove.java index 25e0e489f9..764f4d0075 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractSimplifiedMove.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/AbstractSimplifiedMove.java @@ -11,7 +11,7 @@ * * @param */ -public abstract class AbstractSimplifiedMove extends AbstractMetricMove { +public abstract class AbstractSimplifiedMove implements Move { @Override public final Move doMove(ScoreDirector scoreDirector) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/Move.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/Move.java index 03ab74a675..d5fffd31e4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/Move.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/Move.java @@ -172,15 +172,4 @@ default Collection getPlanningValues() { "Move class (%s) doesn't implement the getPlanningEntities() method, so Value Tabu Search is impossible." .formatted(getClass())); } - - /** - * Flag that enables metric collection for the move. - * In cases like Ruin and Recreate or undo moves, metrics such as move count should not be considered. - * - * @return true by default, which means this move updates the related metrics. - */ - default boolean isCollectMetricEnabled() { - return true; - } - } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/RecordedUndoMove.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/RecordedUndoMove.java index 6ca770a037..6ecfa8b09b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/RecordedUndoMove.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/RecordedUndoMove.java @@ -23,9 +23,4 @@ protected void doMoveOnGenuineVariables(ScoreDirector scoreDirector) } scoreDirector.triggerVariableListeners(); } - - @Override - public boolean isCollectMetricEnabled() { - return false; - } } \ No newline at end of file diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java index c643dc2e2e..444cac5a3f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java @@ -106,8 +106,8 @@ public void solvingEnded(SolverScope solverScope) { @Override public void phaseStarted(AbstractPhaseScope phaseScope) { phaseScope.startingNow(); - phaseScope.reset(); phaseScope.setEnableCollectMetrics(enableCollectMetrics); + phaseScope.reset(); solver.phaseStarted(phaseScope); phaseTermination.phaseStarted(phaseScope); phaseLifecycleSupport.firePhaseStarted(phaseScope); @@ -116,6 +116,8 @@ public void phaseStarted(AbstractPhaseScope phaseScope) { @Override public void phaseEnded(AbstractPhaseScope phaseScope) { solver.phaseEnded(phaseScope); + phaseScope.setEnableCollectMetrics(true); + phaseScope.resetScoreDirectorMetrics(); phaseTermination.phaseEnded(phaseScope); phaseLifecycleSupport.firePhaseEnded(phaseScope); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java index 55c8fc2a67..712f25fda7 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java @@ -117,12 +117,16 @@ public void setEnableCollectMetrics(boolean enableCollectMetrics) { public void reset() { bestSolutionStepIndex = -1; - enableCollectMetrics = true; // solverScope.getBestScore() is null with an uninitialized score startingScore = solverScope.getBestScore() == null ? solverScope.calculateScore() : solverScope.getBestScore(); if (getLastCompletedStepScope().getStepIndex() < 0) { getLastCompletedStepScope().setScore(startingScore); } + resetScoreDirectorMetrics(); + } + + public void resetScoreDirectorMetrics() { + getScoreDirector().setEnableMetricCollection(enableCollectMetrics); } public void startingNow() { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java index 95451f3966..88521cb46b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java @@ -75,6 +75,8 @@ public abstract class AbstractScoreDirector solutionTracker; + private boolean collectMetricEnabled = false; + protected AbstractScoreDirector(Factory_ scoreDirectorFactory, boolean lookUpEnabled, boolean constraintMatchEnabledPreference, boolean expectShadowVariablesInCorrectState) { var solutionDescriptor = scoreDirectorFactory.getSolutionDescriptor(); @@ -174,7 +176,9 @@ public void resetMoveCalculationCount() { @Override public void incrementMoveCalculationCount() { - this.moveCalculationCount++; + if (collectMetricEnabled) { + this.moveCalculationCount++; + } } @Override @@ -182,6 +186,11 @@ public SupplyManager getSupplyManager() { return variableListenerSupport; } + @Override + public void setEnableMetricCollection(boolean enable) { + this.collectMetricEnabled = enable; + } + // ************************************************************************ // Complex methods // ************************************************************************ @@ -240,9 +249,7 @@ public Score_ doAndProcessMove(Move move, boolean assertMoveScoreFrom } Move undoMove = move.doMove(this); Score_ score = calculateScore(); - if (move.isCollectMetricEnabled()) { - incrementMoveCalculationCount(); - } + incrementMoveCalculationCount(); if (assertMoveScoreFromScratch) { undoMoveText = undoMove.toString(); if (trackingWorkingSolution) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java index 91d2aaeb0d..840d64d4e9 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java @@ -38,7 +38,7 @@ * @param the score type to go with the solution */ public interface InnerScoreDirector> - extends VariableDescriptorAwareScoreDirector, AutoCloseable { + extends VariableDescriptorAwareScoreDirector, MetricCollector, AutoCloseable { static > ConstraintAnalysis getConstraintAnalysis( ConstraintMatchTotal constraintMatchTotal, boolean analyzeConstraintMatches) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/MetricCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/MetricCollector.java new file mode 100644 index 0000000000..99f780a52a --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/MetricCollector.java @@ -0,0 +1,10 @@ +package ai.timefold.solver.core.impl.score.director; + +/** + * Allows enabling or disabling the metric collection. In cases like Ruin and Recreate moves, metrics such as move count + * should not be updated. + */ +public interface MetricCollector { + + void setEnableMetricCollection(boolean enable); +} From 17c9806a382febca4559dfc3996cd98b5a0e8253 Mon Sep 17 00:00:00 2001 From: Fred Date: Fri, 6 Sep 2024 18:31:29 -0300 Subject: [PATCH 10/26] chore: address sonarcloud issues --- .../impl/SolverBenchmarkFactoryTest.java | 28 +++++++++++++++ .../solver/core/api/solver/SolverJob.java | 21 ++++++++++++ .../core/impl/solver/DefaultSolver.java | 8 +++++ .../core/impl/solver/DefaultSolverJob.java | 10 ++++++ .../core/api/solver/SolverManagerTest.java | 4 ++- .../termination/TerminationConfigTest.java | 14 ++++++++ .../termination/MoveCountTerminationTest.java | 34 +++++++++++++++++++ .../termination/TerminationFactoryTest.java | 12 +++++++ .../UnimprovedMoveCountTerminationTest.java | 12 +++++++ 9 files changed, 142 insertions(+), 1 deletion(-) diff --git a/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/SolverBenchmarkFactoryTest.java b/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/SolverBenchmarkFactoryTest.java index c89c3a96ce..66e01a7e6c 100644 --- a/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/SolverBenchmarkFactoryTest.java +++ b/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/SolverBenchmarkFactoryTest.java @@ -1,6 +1,7 @@ package ai.timefold.solver.benchmark.impl; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import java.util.List; @@ -9,6 +10,8 @@ import ai.timefold.solver.benchmark.config.SolverBenchmarkConfig; import ai.timefold.solver.benchmark.config.statistic.ProblemStatisticType; import ai.timefold.solver.benchmark.config.statistic.SingleStatisticType; +import ai.timefold.solver.core.config.solver.SolverConfig; +import ai.timefold.solver.core.config.solver.monitoring.MonitoringConfig; import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; import org.junit.jupiter.api.Test; @@ -79,6 +82,18 @@ void invalidZeroSubSingleCount() { assertThatIllegalStateException().isThrownBy(() -> validateConfig(config)); } + @Test + void invalidMonitorConfig() { + SolverConfig solverConfig = new SolverConfig(); + solverConfig.withMonitoringConfig(new MonitoringConfig().withSolverMetricList(List.of(SolverMetric.STEP_SCORE))); + SolverBenchmarkConfig config = new SolverBenchmarkConfig(); + config.setSolverConfig(solverConfig); + config.setName("name"); + config.setSubSingleCount(1); + SolverBenchmarkFactory solverBenchmarkFactory = new SolverBenchmarkFactory(config); + assertThatIllegalArgumentException().isThrownBy(() -> solverBenchmarkFactory.buildSolverBenchmark(null, null, null)); + } + @Test void defaultStatisticsAreUsedIfNotPresent() { SolverBenchmarkConfig config = new SolverBenchmarkConfig(); @@ -90,6 +105,19 @@ void defaultStatisticsAreUsedIfNotPresent() { .containsExactly(SolverMetric.BEST_SCORE); } + @Test + void convertToSolverMetric() { + SolverBenchmarkConfig config = new SolverBenchmarkConfig(); + config.setName("name"); + config.setSubSingleCount(0); + SolverBenchmarkFactory solverBenchmarkFactory = new SolverBenchmarkFactory(config); + ProblemBenchmarksConfig problemBenchmarksConfig = new ProblemBenchmarksConfig(); + problemBenchmarksConfig.setProblemStatisticTypeList( + List.of(ProblemStatisticType.SCORE_CALCULATION_SPEED, ProblemStatisticType.MOVE_CALCULATION_SPEED)); + assertThat(solverBenchmarkFactory.getSolverMetrics(problemBenchmarksConfig)) + .containsExactly(SolverMetric.SCORE_CALCULATION_COUNT, SolverMetric.MOVE_CALCULATION_COUNT); + } + @Test void problemStatisticsAreUsedIfPresent() { SolverBenchmarkConfig config = new SolverBenchmarkConfig(); diff --git a/core/src/main/java/ai/timefold/solver/core/api/solver/SolverJob.java b/core/src/main/java/ai/timefold/solver/core/api/solver/SolverJob.java index 1425fc60f8..a5bebbb677 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/solver/SolverJob.java +++ b/core/src/main/java/ai/timefold/solver/core/api/solver/SolverJob.java @@ -95,6 +95,16 @@ public interface SolverJob { */ long getScoreCalculationCount(); + /** + * Return the number of move evaluations since the last start. + * If it hasn't started yet, it returns 0. + * If it hasn't ended yet, it returns the number of moves evaluations so far. + * If it has ended already, it returns the total number of move evaluations that occurred during solving. + * + * @return the number of move evaluations that had occurred during solving since the last (re)start, at least 0 + */ + long getMoveCalculationCount(); + /** * Return the {@link ProblemSizeStatistics} for the {@link PlanningSolution problem} submitted to the * {@link SolverManager}. @@ -113,4 +123,15 @@ public interface SolverJob { * since the last (re)start, at least 0 */ long getScoreCalculationSpeed(); + + /** + * Return the average number of move evaluations per second since the last start. + * If it hasn't started yet, it returns 0. + * If it hasn't ended yet, it returns the average number of move evaluations per second so far. + * If it has ended already, it returns the average number of move evaluations per second during solving. + * + * @return the average number of move evaluations per second that had occurred during solving + * since the last (re)start, at least 0 + */ + long getMoveCalculationSpeed(); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java index 5fe3cc5f64..c6b82a5338 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java @@ -96,10 +96,18 @@ public long getScoreCalculationCount() { return solverScope.getScoreCalculationCount(); } + public long getMoveCalculationCount() { + return solverScope.getMoveCalculationCount(); + } + public long getScoreCalculationSpeed() { return solverScope.getScoreCalculationSpeed(); } + public long getMoveCalculationSpeed() { + return solverScope.getMoveCalculationSpeed(); + } + @Override public boolean isSolving() { return solving.get(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJob.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJob.java index db89aaebd7..ae34417fa9 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJob.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJob.java @@ -230,11 +230,21 @@ public long getScoreCalculationCount() { return solver.getScoreCalculationCount(); } + @Override + public long getMoveCalculationCount() { + return solver.getMoveCalculationCount(); + } + @Override public long getScoreCalculationSpeed() { return solver.getScoreCalculationSpeed(); } + @Override + public long getMoveCalculationSpeed() { + return solver.getMoveCalculationSpeed(); + } + @Override public ProblemSizeStatistics getProblemSizeStatistics() { var problemSizeStatistics = solver.getSolverScope().getProblemSizeStatistics(); diff --git a/core/src/test/java/ai/timefold/solver/core/api/solver/SolverManagerTest.java b/core/src/test/java/ai/timefold/solver/core/api/solver/SolverManagerTest.java index 862a6316b3..706f1b2209 100644 --- a/core/src/test/java/ai/timefold/solver/core/api/solver/SolverManagerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/api/solver/SolverManagerTest.java @@ -538,13 +538,15 @@ void testScoreCalculationCountForFinishedJob() throws ExecutionException, Interr solverJob.getFinalBestSolution(); assertThat(solverJob.getScoreCalculationCount()).isEqualTo(5L); + assertThat(solverJob.getMoveCalculationCount()).isEqualTo(4L); // Score calculation speed and solve duration are non-deterministic. // On an exceptionally fast machine, getSolvingDuration() can return Duration.ZERO. // On an exceptionally slow machine, getScoreCalculationSpeed() can be 0 due to flooring // (i.e. by taking more than 5 seconds to finish solving). assertThat(solverJob.getSolvingDuration()).isGreaterThanOrEqualTo(Duration.ZERO); - assertThat(solverJob.getScoreCalculationSpeed()).isGreaterThanOrEqualTo(0L); + assertThat(solverJob.getScoreCalculationSpeed()).isNotNegative(); + assertThat(solverJob.getMoveCalculationSpeed()).isNotNegative(); } @Test diff --git a/core/src/test/java/ai/timefold/solver/core/config/solver/termination/TerminationConfigTest.java b/core/src/test/java/ai/timefold/solver/core/config/solver/termination/TerminationConfigTest.java index 1019911544..8acd0559e5 100644 --- a/core/src/test/java/ai/timefold/solver/core/config/solver/termination/TerminationConfigTest.java +++ b/core/src/test/java/ai/timefold/solver/core/config/solver/termination/TerminationConfigTest.java @@ -84,4 +84,18 @@ void childWithTimeSpentLimitShouldNotInheritTimeSpentLimitFromParent() { assertThat(child.getMinutesSpentLimit()).isNull(); } + @Test + void checkMoveCountMetrics() { + TerminationConfig parent = new TerminationConfig() + .withMoveCountLimit(2L) + .withUnimprovedMoveCountLimit(3L); + + TerminationConfig child = new TerminationConfig(); + child.inherit(parent); + + assertThat(child.getMoveCountLimit()).isEqualTo(2L); + assertThat(child.getUnimprovedMoveCountLimit()).isEqualTo(3L); + assertThat(parent.isConfigured()).isTrue(); + } + } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/MoveCountTerminationTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/MoveCountTerminationTest.java index f74558a986..d46bc6952f 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/MoveCountTerminationTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/MoveCountTerminationTest.java @@ -1,11 +1,13 @@ package ai.timefold.solver.core.impl.solver.termination; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.data.Offset.offset; import static org.mockito.Mockito.when; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; import ai.timefold.solver.core.impl.testdata.domain.TestdataSolution; import org.junit.jupiter.api.Test; @@ -39,4 +41,36 @@ void phaseTermination() { assertThat(termination.isPhaseTerminated(phaseScope)).isTrue(); assertThat(termination.calculatePhaseTimeGradient(phaseScope)).isEqualTo(1.0, offset(0.0)); } + + @Test + void solverTermination() { + var termination = new MoveCountTermination(4); + var solverScope = Mockito.mock(SolverScope.class); + var scoreDirector = Mockito.mock(InnerScoreDirector.class); + when(solverScope.getScoreDirector()).thenReturn(scoreDirector); + + when(scoreDirector.getMoveCalculationCount()).thenReturn(0L); + assertThat(termination.isSolverTerminated(solverScope)).isFalse(); + assertThat(termination.calculateSolverTimeGradient(solverScope)).isEqualTo(0.0, offset(0.0)); + when(scoreDirector.getMoveCalculationCount()).thenReturn(1L); + assertThat(termination.isSolverTerminated(solverScope)).isFalse(); + assertThat(termination.calculateSolverTimeGradient(solverScope)).isEqualTo(0.25, offset(0.0)); + when(scoreDirector.getMoveCalculationCount()).thenReturn(2L); + assertThat(termination.isSolverTerminated(solverScope)).isFalse(); + assertThat(termination.calculateSolverTimeGradient(solverScope)).isEqualTo(0.5, offset(0.0)); + when(scoreDirector.getMoveCalculationCount()).thenReturn(3L); + assertThat(termination.isSolverTerminated(solverScope)).isFalse(); + assertThat(termination.calculateSolverTimeGradient(solverScope)).isEqualTo(0.75, offset(0.0)); + when(scoreDirector.getMoveCalculationCount()).thenReturn(4L); + assertThat(termination.isSolverTerminated(solverScope)).isTrue(); + assertThat(termination.calculateSolverTimeGradient(solverScope)).isEqualTo(1.0, offset(0.0)); + when(scoreDirector.getMoveCalculationCount()).thenReturn(5L); + assertThat(termination.isSolverTerminated(solverScope)).isTrue(); + assertThat(termination.calculateSolverTimeGradient(solverScope)).isEqualTo(1.0, offset(0.0)); + } + + @Test + void invalidTermination() { + assertThatIllegalArgumentException().isThrownBy(() -> new MoveCountTermination(-1L)); + } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/TerminationFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/TerminationFactoryTest.java index 80bb8825fd..daf6bbf038 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/TerminationFactoryTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/TerminationFactoryTest.java @@ -167,6 +167,18 @@ TerminationFactory. create(terminationConfig) UnimprovedTimeMillisSpentTermination.class); } + @Test + void buildWithMoveCount() { + TerminationConfig terminationConfig = new TerminationConfig() + .withMoveCountLimit(1L) + .withUnimprovedMoveCountLimit(2L); + List> terminationList = + TerminationFactory. create(terminationConfig) + .buildTimeBasedTermination(mock(HeuristicConfigPolicy.class)); + assertThat(terminationList).hasOnlyElementsOfTypes(MoveCountTermination.class, + UnimprovedMoveCountTermination.class); + } + @Test void scoreDifferenceThreshold_mustBeUsedWithUnimprovedTimeSpent() { HeuristicConfigPolicy heuristicConfigPolicy = mock(HeuristicConfigPolicy.class); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/UnimprovedMoveCountTerminationTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/UnimprovedMoveCountTerminationTest.java index 44c800e924..2114d0c01f 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/UnimprovedMoveCountTerminationTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/UnimprovedMoveCountTerminationTest.java @@ -1,6 +1,8 @@ package ai.timefold.solver.core.impl.solver.termination; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.data.Offset.offset; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -40,4 +42,14 @@ void phaseTermination() { assertThat(termination.isPhaseTerminated(phaseScope)).isTrue(); assertThat(termination.calculatePhaseTimeGradient(phaseScope)).isEqualTo(1.0, offset(0.0)); } + + @Test + void invalidTermination() { + assertThatIllegalArgumentException().isThrownBy(() -> new UnimprovedMoveCountTermination(-1L)); + var termination = new UnimprovedMoveCountTermination(4); + assertThatThrownBy(() -> termination.isSolverTerminated(null)) + .isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(() -> termination.calculateSolverTimeGradient(null)) + .isInstanceOf(UnsupportedOperationException.class); + } } From 96f134d4c57d928b392a952821e421845d5b7fd7 Mon Sep 17 00:00:00 2001 From: Fred Date: Mon, 9 Sep 2024 12:11:52 -0300 Subject: [PATCH 11/26] feat: improve the metric logic --- .../statistic/ProblemStatisticType.java | 8 +- .../impl/SolverBenchmarkFactory.java | 4 +- .../impl/SubSingleBenchmarkRunner.java | 2 +- .../impl/report/BenchmarkReport.java | 18 ++--- .../impl/result/ProblemBenchmarkResult.java | 4 +- .../impl/result/SingleBenchmarkResult.java | 22 +++--- .../impl/result/SolverBenchmarkResult.java | 14 ++-- .../impl/result/SubSingleBenchmarkResult.java | 22 +++--- .../impl/statistic/ProblemStatistic.java | 4 +- ...actCalculationSpeedSubSingleStatistic.java | 12 +-- ...eCalculationSpeedProblemStatisticTime.java | 25 ------- ...veEvaluationSpeedProblemStatisticTime.java | 25 +++++++ ...oveEvaluationSpeedSubSingleStatistic.java} | 14 ++-- .../impl/report/benchmarkReport.html.ftl | 16 ++-- benchmark/src/main/resources/benchmark.xsd | 7 +- .../impl/SolverBenchmarkFactoryTest.java | 4 +- ...valuationSpeedSubSingleStatisticTest.java} | 12 +-- .../result/testPlannerBenchmarkResult.xml | 48 ++++++------ core/src/build/revapi-differences.json | 2 +- .../solver/core/api/solver/SolverJob.java | 4 +- .../core/config/solver/SolverConfig.java | 2 +- .../solver/monitoring/SolverMetric.java | 4 +- .../solver/termination/TerminationConfig.java | 18 ----- .../DefaultConstructionHeuristicPhase.java | 4 +- .../DefaultConstructionHeuristicForager.java | 1 + .../decider/ExhaustiveSearchDecider.java | 1 + .../scope/ExhaustiveSearchPhaseScope.java | 1 - .../localsearch/DefaultLocalSearchPhase.java | 8 +- .../forager/AcceptedLocalSearchForager.java | 1 + .../solver/core/impl/phase/AbstractPhase.java | 3 - .../impl/phase/scope/AbstractPhaseScope.java | 47 +++++------- .../impl/phase/scope/AbstractStepScope.java | 20 ++--- .../score/director/AbstractScoreDirector.java | 26 ------- .../score/director/InnerScoreDirector.java | 8 +- .../impl/score/director/MetricCollector.java | 10 --- .../core/impl/solver/DefaultSolver.java | 13 ++-- .../core/impl/solver/DefaultSolverJob.java | 8 +- .../solver/recaller/BestSolutionRecaller.java | 3 - .../core/impl/solver/scope/SolverScope.java | 28 +++---- .../termination/AbstractTermination.java | 2 +- .../termination/MoveCountTermination.java | 21 +++--- .../termination/TerminationFactory.java | 3 - .../UnimprovedMoveCountTermination.java | 73 ------------------- core/src/main/resources/solver.xsd | 4 +- .../core/api/solver/SolverManagerTest.java | 4 +- .../termination/TerminationConfigTest.java | 4 +- .../DefaultExhaustiveSearchPhaseTest.java | 6 +- .../generic/RuinRecreateMoveSelectorTest.java | 4 +- .../ListRuinRecreateMoveSelectorTest.java | 4 +- .../core/impl/solver/DefaultSolverTest.java | 6 +- .../termination/MoveCountTerminationTest.java | 31 ++++---- .../termination/TerminationFactoryTest.java | 6 +- .../UnimprovedMoveCountTerminationTest.java | 55 -------------- 53 files changed, 234 insertions(+), 462 deletions(-) delete mode 100644 benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedProblemStatisticTime.java create mode 100644 benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/moveevaluationspeed/MoveEvaluationSpeedProblemStatisticTime.java rename benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/{movecalculationspeed/MoveCalculationSpeedSubSingleStatistic.java => moveevaluationspeed/MoveEvaluationSpeedSubSingleStatistic.java} (57%) rename benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/{movecalculationspeed/MoveCalculationSpeedSubSingleStatisticTest.java => moveevaluationspeed/MoveEvaluationSpeedSubSingleStatisticTest.java} (73%) delete mode 100644 core/src/main/java/ai/timefold/solver/core/impl/score/director/MetricCollector.java delete mode 100644 core/src/main/java/ai/timefold/solver/core/impl/solver/termination/UnimprovedMoveCountTermination.java delete mode 100644 core/src/test/java/ai/timefold/solver/core/impl/solver/termination/UnimprovedMoveCountTerminationTest.java diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/config/statistic/ProblemStatisticType.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/config/statistic/ProblemStatisticType.java index 2f54c0c266..25bfddeeb4 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/config/statistic/ProblemStatisticType.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/config/statistic/ProblemStatisticType.java @@ -10,8 +10,8 @@ import ai.timefold.solver.benchmark.impl.statistic.bestscore.BestScoreProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.bestsolutionmutation.BestSolutionMutationProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.memoryuse.MemoryUseProblemStatistic; -import ai.timefold.solver.benchmark.impl.statistic.movecalculationspeed.MoveCalculationSpeedProblemStatisticTime; import ai.timefold.solver.benchmark.impl.statistic.movecountperstep.MoveCountPerStepProblemStatistic; +import ai.timefold.solver.benchmark.impl.statistic.moveevaluationspeed.MoveEvaluationSpeedProblemStatisticTime; import ai.timefold.solver.benchmark.impl.statistic.scorecalculationspeed.ScoreCalculationSpeedProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.stepscore.StepScoreProblemStatistic; @@ -20,7 +20,7 @@ public enum ProblemStatisticType implements StatisticType { BEST_SCORE, STEP_SCORE, SCORE_CALCULATION_SPEED, - MOVE_CALCULATION_SPEED, + MOVE_EVALUATION_SPEED, BEST_SOLUTION_MUTATION, MOVE_COUNT_PER_STEP, MEMORY_USE; @@ -33,8 +33,8 @@ public ProblemStatistic buildProblemStatistic(ProblemBenchmarkResult problemBenc return new StepScoreProblemStatistic(problemBenchmarkResult); case SCORE_CALCULATION_SPEED: return new ScoreCalculationSpeedProblemStatistic(problemBenchmarkResult); - case MOVE_CALCULATION_SPEED: - return new MoveCalculationSpeedProblemStatisticTime(problemBenchmarkResult); + case MOVE_EVALUATION_SPEED: + return new MoveEvaluationSpeedProblemStatisticTime(problemBenchmarkResult); case BEST_SOLUTION_MUTATION: return new BestSolutionMutationProblemStatistic(problemBenchmarkResult); case MOVE_COUNT_PER_STEP: diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/SolverBenchmarkFactory.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/SolverBenchmarkFactory.java index 1fb26c1442..9e33a06a0f 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/SolverBenchmarkFactory.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/SolverBenchmarkFactory.java @@ -91,8 +91,8 @@ protected List getSolverMetrics(ProblemBenchmarksConfig config) { .orElseGet(ProblemStatisticType::defaultList)) { if (problemStatisticType == ProblemStatisticType.SCORE_CALCULATION_SPEED) { out.add(SolverMetric.SCORE_CALCULATION_COUNT); - } else if (problemStatisticType == ProblemStatisticType.MOVE_CALCULATION_SPEED) { - out.add(SolverMetric.MOVE_CALCULATION_COUNT); + } else if (problemStatisticType == ProblemStatisticType.MOVE_EVALUATION_SPEED) { + out.add(SolverMetric.MOVE_EVALUATION_COUNT); } else { out.add(SolverMetric.valueOf(problemStatisticType.name())); } diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/SubSingleBenchmarkRunner.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/SubSingleBenchmarkRunner.java index 19e257f1a7..7e9d6435a6 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/SubSingleBenchmarkRunner.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/SubSingleBenchmarkRunner.java @@ -125,7 +125,7 @@ public SubSingleBenchmarkRunner call() { subSingleBenchmarkResult.setScore(solutionDescriptor.getScore(solution)); subSingleBenchmarkResult.setTimeMillisSpent(timeMillisSpent); subSingleBenchmarkResult.setScoreCalculationCount(solverScope.getScoreCalculationCount()); - subSingleBenchmarkResult.setMoveCalculationCount(solverScope.getMoveCalculationCount()); + subSingleBenchmarkResult.setMoveEvaluationCount(solverScope.getMoveEvaluationCount()); SolutionManager solutionManager = SolutionManager.create(solverFactory); boolean isConstraintMatchEnabled = solver.getSolverScope().getScoreDirector().isConstraintMatchEnabled(); diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/report/BenchmarkReport.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/report/BenchmarkReport.java index 4bb60a5983..41dd0c0051 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/report/BenchmarkReport.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/report/BenchmarkReport.java @@ -63,7 +63,7 @@ public static Configuration createFreeMarkerConfiguration() { private List> winningScoreDifferenceSummaryChartList = null; private List> worstScoreDifferencePercentageSummaryChartList = null; private LineChart scoreCalculationSpeedSummaryChart; - private LineChart moveCalculationSpeedSummaryChart; + private LineChart moveEvaluationSpeedSummaryChart; private BarChart worstScoreCalculationSpeedDifferencePercentageSummaryChart = null; private BarChart timeSpentSummaryChart = null; private LineChart timeSpentScalabilitySummaryChart = null; @@ -144,8 +144,8 @@ public LineChart getScoreCalculationSpeedSummaryChart() { } @SuppressWarnings("unused") // Used by FreeMarker. - public LineChart getMoveCalculationSpeedSummaryChart() { - return moveCalculationSpeedSummaryChart; + public LineChart getMoveEvaluationSpeedSummaryChart() { + return moveEvaluationSpeedSummaryChart; } @SuppressWarnings("unused") // Used by FreeMarker. @@ -206,7 +206,7 @@ public void writeReport() { worstScoreDifferencePercentageSummaryChartList = createWorstScoreDifferencePercentageSummaryChart(); bestScoreDistributionSummaryChartList = createBestScoreDistributionSummaryChart(); scoreCalculationSpeedSummaryChart = createScoreCalculationSpeedSummaryChart(); - moveCalculationSpeedSummaryChart = createMoveCalculationSpeedSummaryChart(); + moveEvaluationSpeedSummaryChart = createMoveEvaluationSpeedSummaryChart(); worstScoreCalculationSpeedDifferencePercentageSummaryChart = createWorstScoreCalculationSpeedDifferencePercentageSummaryChart(); timeSpentSummaryChart = createTimeSpentSummaryChart(); @@ -246,7 +246,7 @@ public void writeReport() { chartsToWrite.addAll(worstScoreDifferencePercentageSummaryChartList); chartsToWrite.addAll(bestScoreDistributionSummaryChartList); chartsToWrite.add(scoreCalculationSpeedSummaryChart); - chartsToWrite.add(moveCalculationSpeedSummaryChart); + chartsToWrite.add(moveEvaluationSpeedSummaryChart); chartsToWrite.add(worstScoreCalculationSpeedDifferencePercentageSummaryChart); chartsToWrite.add(timeSpentSummaryChart); chartsToWrite.add(timeSpentScalabilitySummaryChart); @@ -502,10 +502,10 @@ private LineChart createScoreCalculationSpeedSummaryChart() { "Score calculation speed per second", false); } - private LineChart createMoveCalculationSpeedSummaryChart() { - return createScalabilitySummaryChart(SingleBenchmarkResult::getMoveCalculationSpeed, - "moveCalculationSpeedSummaryChart", "Move calculation speed summary (higher is better)", - "Move calculation speed per second", false); + private LineChart createMoveEvaluationSpeedSummaryChart() { + return createScalabilitySummaryChart(SingleBenchmarkResult::getMoveEvaluationSpeed, + "moveEvaluationSpeedSummaryChart", "Move evaluation speed summary (higher is better)", + "Move evaluation speed per second", false); } private BarChart createWorstScoreCalculationSpeedDifferencePercentageSummaryChart() { diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/ProblemBenchmarkResult.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/ProblemBenchmarkResult.java index d6bcb36224..ea7d6e313f 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/ProblemBenchmarkResult.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/ProblemBenchmarkResult.java @@ -32,8 +32,8 @@ import ai.timefold.solver.benchmark.impl.statistic.bestscore.BestScoreProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.bestsolutionmutation.BestSolutionMutationProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.memoryuse.MemoryUseProblemStatistic; -import ai.timefold.solver.benchmark.impl.statistic.movecalculationspeed.MoveCalculationSpeedProblemStatisticTime; import ai.timefold.solver.benchmark.impl.statistic.movecountperstep.MoveCountPerStepProblemStatistic; +import ai.timefold.solver.benchmark.impl.statistic.moveevaluationspeed.MoveEvaluationSpeedProblemStatisticTime; import ai.timefold.solver.benchmark.impl.statistic.scorecalculationspeed.ScoreCalculationSpeedProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.stepscore.StepScoreProblemStatistic; import ai.timefold.solver.core.api.domain.solution.PlanningScore; @@ -68,7 +68,7 @@ public class ProblemBenchmarkResult { @XmlElement(name = "bestScoreProblemStatistic", type = BestScoreProblemStatistic.class), @XmlElement(name = "stepScoreProblemStatistic", type = StepScoreProblemStatistic.class), @XmlElement(name = "scoreCalculationSpeedProblemStatistic", type = ScoreCalculationSpeedProblemStatistic.class), - @XmlElement(name = "moveCalculationSpeedProblemStatistic", type = MoveCalculationSpeedProblemStatisticTime.class), + @XmlElement(name = "moveEvaluationSpeedProblemStatistic", type = MoveEvaluationSpeedProblemStatisticTime.class), @XmlElement(name = "bestSolutionMutationProblemStatistic", type = BestSolutionMutationProblemStatistic.class), @XmlElement(name = "moveCountPerStepProblemStatistic", type = MoveCountPerStepProblemStatistic.class), @XmlElement(name = "memoryUseProblemStatistic", type = MemoryUseProblemStatistic.class), diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SingleBenchmarkResult.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SingleBenchmarkResult.java index cd4ebf7180..723755cac2 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SingleBenchmarkResult.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SingleBenchmarkResult.java @@ -58,7 +58,7 @@ public class SingleBenchmarkResult implements BenchmarkResult { private double[] standardDeviationDoubles = null; private long timeMillisSpent = -1L; private long scoreCalculationCount = -1L; - private long moveCalculationCount = -1L; + private long moveEvaluationCount = -1L; private String scoreExplanationSummary = null; // ************************************************************************ @@ -152,12 +152,12 @@ public void setScoreCalculationCount(long scoreCalculationCount) { this.scoreCalculationCount = scoreCalculationCount; } - public long getMoveCalculationCount() { - return moveCalculationCount; + public long getMoveEvaluationCount() { + return moveEvaluationCount; } - public void setMoveCalculationCount(long moveCalculationCount) { - this.moveCalculationCount = moveCalculationCount; + public void setMoveEvaluationCount(long moveEvaluationCount) { + this.moveEvaluationCount = moveEvaluationCount; } @SuppressWarnings("unused") // Used by FreeMarker. @@ -267,13 +267,13 @@ public Long getScoreCalculationSpeed() { return scoreCalculationCount * 1000L / timeMillisSpent; } - public Long getMoveCalculationSpeed() { - long timeMillisSpent = this.timeMillisSpent; - if (timeMillisSpent == 0L) { + public Long getMoveEvaluationSpeed() { + long timeSpent = this.timeMillisSpent; + if (timeSpent == 0L) { // Avoid divide by zero exception on a fast CPU - timeMillisSpent = 1L; + timeSpent = 1L; } - return moveCalculationCount * 1000L / timeMillisSpent; + return moveEvaluationCount * 1000L / timeSpent; } @SuppressWarnings("unused") // Used By FreeMarker. @@ -344,7 +344,7 @@ private void determineRepresentativeSubSingleBenchmarkResult() { usedMemoryAfterInputSolution = median.getUsedMemoryAfterInputSolution(); timeMillisSpent = median.getTimeMillisSpent(); scoreCalculationCount = median.getScoreCalculationCount(); - moveCalculationCount = median.getMoveCalculationCount(); + moveEvaluationCount = median.getMoveEvaluationCount(); scoreExplanationSummary = median.getScoreExplanationSummary(); } diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SolverBenchmarkResult.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SolverBenchmarkResult.java index 20fdd2b0c4..0c8c54f8fe 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SolverBenchmarkResult.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SolverBenchmarkResult.java @@ -56,7 +56,7 @@ public class SolverBenchmarkResult { private ScoreDifferencePercentage averageWorstScoreDifferencePercentage = null; // The average of the average is not just the overall average if the SingleBenchmarkResult's timeMillisSpent differ private Long averageScoreCalculationSpeed = null; - private Long averageMoveCalculationSpeed = null; + private Long averageMoveEvaluationSpeed = null; private Long averageTimeMillisSpent = null; private Double averageWorstScoreCalculationSpeedDifferencePercentage = null; @@ -161,8 +161,8 @@ public Long getAverageScoreCalculationSpeed() { } @SuppressWarnings("unused") // Used by FreeMarker. - public Long getAverageMoveCalculationSpeed() { - return averageMoveCalculationSpeed; + public Long getAverageMoveEvaluationSpeed() { + return averageMoveEvaluationSpeed; } public Long getAverageTimeMillisSpent() { @@ -288,7 +288,7 @@ protected void determineTotalsAndAverages() { totalWinningScoreDifference = null; ScoreDifferencePercentage totalWorstScoreDifferencePercentage = null; long totalScoreCalculationSpeed = 0L; - long totalMoveCalculationSpeed = 0L; + long totalMoveEvaluationSpeed = 0L; long totalTimeMillisSpent = 0L; double totalWorstScoreCalculationSpeedDifferencePercentage = 0.0; uninitializedSolutionCount = 0; @@ -307,7 +307,7 @@ protected void determineTotalsAndAverages() { totalWinningScoreDifference = singleBenchmarkResult.getWinningScoreDifference(); totalWorstScoreDifferencePercentage = singleBenchmarkResult.getWorstScoreDifferencePercentage(); totalScoreCalculationSpeed = singleBenchmarkResult.getScoreCalculationSpeed(); - totalMoveCalculationSpeed = singleBenchmarkResult.getMoveCalculationSpeed(); + totalMoveEvaluationSpeed = singleBenchmarkResult.getMoveEvaluationSpeed(); totalTimeMillisSpent = singleBenchmarkResult.getTimeMillisSpent(); totalWorstScoreCalculationSpeedDifferencePercentage = singleBenchmarkResult .getWorstScoreCalculationSpeedDifferencePercentage(); @@ -319,7 +319,7 @@ protected void determineTotalsAndAverages() { totalWorstScoreDifferencePercentage = totalWorstScoreDifferencePercentage.add( singleBenchmarkResult.getWorstScoreDifferencePercentage()); totalScoreCalculationSpeed += singleBenchmarkResult.getScoreCalculationSpeed(); - totalMoveCalculationSpeed += singleBenchmarkResult.getMoveCalculationSpeed(); + totalMoveEvaluationSpeed += singleBenchmarkResult.getMoveEvaluationSpeed(); totalTimeMillisSpent += singleBenchmarkResult.getTimeMillisSpent(); totalWorstScoreCalculationSpeedDifferencePercentage += singleBenchmarkResult .getWorstScoreCalculationSpeedDifferencePercentage(); @@ -331,7 +331,7 @@ protected void determineTotalsAndAverages() { averageScore = totalScore.divide(successCount); averageWorstScoreDifferencePercentage = totalWorstScoreDifferencePercentage.divide(successCount); averageScoreCalculationSpeed = totalScoreCalculationSpeed / successCount; - averageMoveCalculationSpeed = totalMoveCalculationSpeed / successCount; + averageMoveEvaluationSpeed = totalMoveEvaluationSpeed / successCount; averageTimeMillisSpent = totalTimeMillisSpent / successCount; averageWorstScoreCalculationSpeedDifferencePercentage = totalWorstScoreCalculationSpeedDifferencePercentage / successCount; diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SubSingleBenchmarkResult.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SubSingleBenchmarkResult.java index 16ad24757d..d5699f6344 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SubSingleBenchmarkResult.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SubSingleBenchmarkResult.java @@ -58,7 +58,7 @@ public class SubSingleBenchmarkResult implements BenchmarkResult { private Score score = null; private long timeMillisSpent = -1L; private long scoreCalculationCount = -1L; - private long moveCalculationCount = -1L; + private long moveEvaluationCount = -1L; private String scoreExplanationSummary = null; // ************************************************************************ @@ -162,12 +162,12 @@ public void setScoreCalculationCount(long scoreCalculationCount) { this.scoreCalculationCount = scoreCalculationCount; } - public long getMoveCalculationCount() { - return moveCalculationCount; + public long getMoveEvaluationCount() { + return moveEvaluationCount; } - public void setMoveCalculationCount(long moveCalculationCount) { - this.moveCalculationCount = moveCalculationCount; + public void setMoveEvaluationCount(long moveEvaluationCount) { + this.moveEvaluationCount = moveEvaluationCount; } public String getScoreExplanationSummary() { @@ -227,13 +227,13 @@ public Long getScoreCalculationSpeed() { } @SuppressWarnings("unused") // Used by FreeMarker. - public Long getMoveCalculationSpeed() { - long timeMillisSpent = this.timeMillisSpent; - if (timeMillisSpent == 0L) { + public Long getMoveEvaluationSpeed() { + long timeSpent = this.timeMillisSpent; + if (timeSpent == 0L) { // Avoid divide by zero exception on a fast CPU - timeMillisSpent = 1L; + timeSpent = 1L; } - return moveCalculationCount * 1000L / timeMillisSpent; + return moveEvaluationCount * 1000L / timeSpent; } @SuppressWarnings("unused") // Used by FreeMarker. @@ -312,7 +312,7 @@ protected static SubSingleBenchmarkResult createMerge( newResult.score = oldResult.score; newResult.timeMillisSpent = oldResult.timeMillisSpent; newResult.scoreCalculationCount = oldResult.scoreCalculationCount; - newResult.moveCalculationCount = oldResult.moveCalculationCount; + newResult.moveEvaluationCount = oldResult.moveEvaluationCount; singleBenchmarkResult.getSubSingleBenchmarkResultList().add(newResult); return newResult; diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/ProblemStatistic.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/ProblemStatistic.java index 33e3d6167a..4d0400e361 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/ProblemStatistic.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/ProblemStatistic.java @@ -19,8 +19,8 @@ import ai.timefold.solver.benchmark.impl.statistic.bestscore.BestScoreProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.bestsolutionmutation.BestSolutionMutationProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.memoryuse.MemoryUseProblemStatistic; -import ai.timefold.solver.benchmark.impl.statistic.movecalculationspeed.MoveCalculationSpeedProblemStatisticTime; import ai.timefold.solver.benchmark.impl.statistic.movecountperstep.MoveCountPerStepProblemStatistic; +import ai.timefold.solver.benchmark.impl.statistic.moveevaluationspeed.MoveEvaluationSpeedProblemStatisticTime; import ai.timefold.solver.benchmark.impl.statistic.scorecalculationspeed.ScoreCalculationSpeedProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.stepscore.StepScoreProblemStatistic; @@ -32,7 +32,7 @@ BestScoreProblemStatistic.class, StepScoreProblemStatistic.class, ScoreCalculationSpeedProblemStatistic.class, - MoveCalculationSpeedProblemStatisticTime.class, + MoveEvaluationSpeedProblemStatisticTime.class, BestSolutionMutationProblemStatistic.class, MoveCountPerStepProblemStatistic.class, MemoryUseProblemStatistic.class diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/common/AbstractCalculationSpeedSubSingleStatistic.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/common/AbstractCalculationSpeedSubSingleStatistic.java index e678978f21..feffe454cd 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/common/AbstractCalculationSpeedSubSingleStatistic.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/common/AbstractCalculationSpeedSubSingleStatistic.java @@ -44,17 +44,17 @@ public void open(StatisticRegistry registry, Tags runTag) { @Override public void accept(Long timeMillisSpent) { if (timeMillisSpent >= nextTimeMillisThreshold) { - registry.getGaugeValue(solverMetric, runTag, calculationCountNumber -> { - var moveCalculationCount = calculationCountNumber.longValue(); - var calculationCountInterval = moveCalculationCount - lastCalculationCount.get(); + registry.getGaugeValue(solverMetric, runTag, countNumber -> { + var moveEvaluationCount = countNumber.longValue(); + var countInterval = moveEvaluationCount - lastCalculationCount.get(); var timeMillisSpentInterval = timeMillisSpent - lastTimeMillisSpent; if (timeMillisSpentInterval == 0L) { // Avoid divide by zero exception on a fast CPU timeMillisSpentInterval = 1L; } - var calculationSpeed = calculationCountInterval * 1000L / timeMillisSpentInterval; - pointList.add(new LongStatisticPoint(timeMillisSpent, calculationSpeed)); - lastCalculationCount.set(moveCalculationCount); + var speed = countInterval * 1000L / timeMillisSpentInterval; + pointList.add(new LongStatisticPoint(timeMillisSpent, speed)); + lastCalculationCount.set(moveEvaluationCount); }); lastTimeMillisSpent = timeMillisSpent; nextTimeMillisThreshold += timeMillisThresholdInterval; diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedProblemStatisticTime.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedProblemStatisticTime.java deleted file mode 100644 index 896cd9656b..0000000000 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedProblemStatisticTime.java +++ /dev/null @@ -1,25 +0,0 @@ -package ai.timefold.solver.benchmark.impl.statistic.movecalculationspeed; - -import ai.timefold.solver.benchmark.config.statistic.ProblemStatisticType; -import ai.timefold.solver.benchmark.impl.result.ProblemBenchmarkResult; -import ai.timefold.solver.benchmark.impl.result.SubSingleBenchmarkResult; -import ai.timefold.solver.benchmark.impl.statistic.SubSingleStatistic; -import ai.timefold.solver.benchmark.impl.statistic.common.AbstractTimeLineChartProblemStatistic; - -public class MoveCalculationSpeedProblemStatisticTime extends AbstractTimeLineChartProblemStatistic { - - private MoveCalculationSpeedProblemStatisticTime() { - // For JAXB. - this(null); - } - - public MoveCalculationSpeedProblemStatisticTime(ProblemBenchmarkResult problemBenchmarkResult) { - super(ProblemStatisticType.MOVE_CALCULATION_SPEED, problemBenchmarkResult, "moveCalculationSpeedProblemStatisticChart", - problemBenchmarkResult.getName() + " move calculation speed statistic", "Move calculation speed per second"); - } - - @Override - public SubSingleStatistic createSubSingleStatistic(SubSingleBenchmarkResult subSingleBenchmarkResult) { - return new MoveCalculationSpeedSubSingleStatistic<>(subSingleBenchmarkResult); - } -} diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/moveevaluationspeed/MoveEvaluationSpeedProblemStatisticTime.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/moveevaluationspeed/MoveEvaluationSpeedProblemStatisticTime.java new file mode 100644 index 0000000000..5c99975821 --- /dev/null +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/moveevaluationspeed/MoveEvaluationSpeedProblemStatisticTime.java @@ -0,0 +1,25 @@ +package ai.timefold.solver.benchmark.impl.statistic.moveevaluationspeed; + +import ai.timefold.solver.benchmark.config.statistic.ProblemStatisticType; +import ai.timefold.solver.benchmark.impl.result.ProblemBenchmarkResult; +import ai.timefold.solver.benchmark.impl.result.SubSingleBenchmarkResult; +import ai.timefold.solver.benchmark.impl.statistic.SubSingleStatistic; +import ai.timefold.solver.benchmark.impl.statistic.common.AbstractTimeLineChartProblemStatistic; + +public class MoveEvaluationSpeedProblemStatisticTime extends AbstractTimeLineChartProblemStatistic { + + private MoveEvaluationSpeedProblemStatisticTime() { + // For JAXB. + this(null); + } + + public MoveEvaluationSpeedProblemStatisticTime(ProblemBenchmarkResult problemBenchmarkResult) { + super(ProblemStatisticType.MOVE_EVALUATION_SPEED, problemBenchmarkResult, "moveEvaluationSpeedProblemStatisticChart", + problemBenchmarkResult.getName() + " move evaluation speed statistic", "Move evaluation speed per second"); + } + + @Override + public SubSingleStatistic createSubSingleStatistic(SubSingleBenchmarkResult subSingleBenchmarkResult) { + return new MoveEvaluationSpeedSubSingleStatistic<>(subSingleBenchmarkResult); + } +} diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedSubSingleStatistic.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/moveevaluationspeed/MoveEvaluationSpeedSubSingleStatistic.java similarity index 57% rename from benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedSubSingleStatistic.java rename to benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/moveevaluationspeed/MoveEvaluationSpeedSubSingleStatistic.java index 3e5f0f636b..82db93e8c7 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedSubSingleStatistic.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/moveevaluationspeed/MoveEvaluationSpeedSubSingleStatistic.java @@ -1,4 +1,4 @@ -package ai.timefold.solver.benchmark.impl.statistic.movecalculationspeed; +package ai.timefold.solver.benchmark.impl.statistic.moveevaluationspeed; import ai.timefold.solver.benchmark.config.statistic.ProblemStatisticType; import ai.timefold.solver.benchmark.impl.result.SubSingleBenchmarkResult; @@ -6,19 +6,19 @@ import ai.timefold.solver.benchmark.impl.statistic.common.AbstractCalculationSpeedSubSingleStatistic; import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; -public class MoveCalculationSpeedSubSingleStatistic extends AbstractCalculationSpeedSubSingleStatistic { +public class MoveEvaluationSpeedSubSingleStatistic extends AbstractCalculationSpeedSubSingleStatistic { - private MoveCalculationSpeedSubSingleStatistic() { + private MoveEvaluationSpeedSubSingleStatistic() { // For JAXB. this(null); } - public MoveCalculationSpeedSubSingleStatistic(SubSingleBenchmarkResult subSingleBenchmarkResult) { + public MoveEvaluationSpeedSubSingleStatistic(SubSingleBenchmarkResult subSingleBenchmarkResult) { this(subSingleBenchmarkResult, 1000L); } - public MoveCalculationSpeedSubSingleStatistic(SubSingleBenchmarkResult benchmarkResult, long timeMillisThresholdInterval) { - super(SolverMetric.MOVE_CALCULATION_COUNT, ProblemStatisticType.MOVE_CALCULATION_SPEED, benchmarkResult, + public MoveEvaluationSpeedSubSingleStatistic(SubSingleBenchmarkResult benchmarkResult, long timeMillisThresholdInterval) { + super(SolverMetric.MOVE_EVALUATION_COUNT, ProblemStatisticType.MOVE_EVALUATION_SPEED, benchmarkResult, timeMillisThresholdInterval); } @@ -28,6 +28,6 @@ public MoveCalculationSpeedSubSingleStatistic(SubSingleBenchmarkResult benchmark @Override protected String getCsvHeader() { - return StatisticPoint.buildCsvLine("timeMillisSpent", "moveCalculationSpeed"); + return StatisticPoint.buildCsvLine("timeMillisSpent", "moveEvaluationSpeed"); } } diff --git a/benchmark/src/main/resources/ai/timefold/solver/benchmark/impl/report/benchmarkReport.html.ftl b/benchmark/src/main/resources/ai/timefold/solver/benchmark/impl/report/benchmarkReport.html.ftl index caebb7e3af..5e86ae8022 100644 --- a/benchmark/src/main/resources/ai/timefold/solver/benchmark/impl/report/benchmarkReport.html.ftl +++ b/benchmark/src/main/resources/ai/timefold/solver/benchmark/impl/report/benchmarkReport.html.ftl @@ -363,7 +363,7 @@

  • - +
  • @@ -447,14 +447,14 @@
  • -
    -

    Move calculation speed summary

    +
    +

    Move evaluation speed summary

    Useful for comparing different score calculators and/or constraint implementations (presuming that the solver configurations do not differ otherwise). Also useful to measure the scalability cost of an extra constraint.

    - <@addChart chart=benchmarkReport.moveCalculationSpeedSummaryChart /> + <@addChart chart=benchmarkReport.moveEvaluationSpeedSummaryChart />
    @@ -480,7 +480,7 @@ <#list benchmarkReport.plannerBenchmarkResult.solverBenchmarkResultList as solverBenchmarkResult> class="table-success"> - + <#list benchmarkReport.plannerBenchmarkResult.unifiedProblemBenchmarkResultList as problemBenchmarkResult> <#if !solverBenchmarkResult.findSingleBenchmark(problemBenchmarkResult)??> @@ -490,17 +490,17 @@ <#else> <#if solverBenchmarkResult.subSingleCount lte 1> - + <#else>
    ${solverBenchmarkResult.name} <@addSolverBenchmarkBadges solverBenchmarkResult=solverBenchmarkResult/>${solverBenchmarkResult.averageMoveCalculationSpeed!""}/s${solverBenchmarkResult.averageMoveEvaluationSpeed!""}/sFailed${singleBenchmarkResult.moveCalculationSpeed}/s${singleBenchmarkResult.moveEvaluationSpeed}/s diff --git a/benchmark/src/main/resources/benchmark.xsd b/benchmark/src/main/resources/benchmark.xsd index 048604f5b4..8673f62765 100644 --- a/benchmark/src/main/resources/benchmark.xsd +++ b/benchmark/src/main/resources/benchmark.xsd @@ -223,7 +223,7 @@ - + @@ -584,9 +584,6 @@ - - - @@ -2624,7 +2621,7 @@ - + diff --git a/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/SolverBenchmarkFactoryTest.java b/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/SolverBenchmarkFactoryTest.java index 66e01a7e6c..e323625b94 100644 --- a/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/SolverBenchmarkFactoryTest.java +++ b/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/SolverBenchmarkFactoryTest.java @@ -113,9 +113,9 @@ void convertToSolverMetric() { SolverBenchmarkFactory solverBenchmarkFactory = new SolverBenchmarkFactory(config); ProblemBenchmarksConfig problemBenchmarksConfig = new ProblemBenchmarksConfig(); problemBenchmarksConfig.setProblemStatisticTypeList( - List.of(ProblemStatisticType.SCORE_CALCULATION_SPEED, ProblemStatisticType.MOVE_CALCULATION_SPEED)); + List.of(ProblemStatisticType.SCORE_CALCULATION_SPEED, ProblemStatisticType.MOVE_EVALUATION_SPEED)); assertThat(solverBenchmarkFactory.getSolverMetrics(problemBenchmarksConfig)) - .containsExactly(SolverMetric.SCORE_CALCULATION_COUNT, SolverMetric.MOVE_CALCULATION_COUNT); + .containsExactly(SolverMetric.SCORE_CALCULATION_COUNT, SolverMetric.MOVE_EVALUATION_COUNT); } @Test diff --git a/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedSubSingleStatisticTest.java b/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/moveevaluationspeed/MoveEvaluationSpeedSubSingleStatisticTest.java similarity index 73% rename from benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedSubSingleStatisticTest.java rename to benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/moveevaluationspeed/MoveEvaluationSpeedSubSingleStatisticTest.java index d4ee0e6c20..f737ac6655 100644 --- a/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/movecalculationspeed/MoveCalculationSpeedSubSingleStatisticTest.java +++ b/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/moveevaluationspeed/MoveEvaluationSpeedSubSingleStatisticTest.java @@ -1,5 +1,5 @@ -package ai.timefold.solver.benchmark.impl.statistic.movecalculationspeed; +package ai.timefold.solver.benchmark.impl.statistic.moveevaluationspeed; import java.util.Collections; import java.util.List; @@ -12,13 +12,13 @@ import org.assertj.core.api.SoftAssertions; -public final class MoveCalculationSpeedSubSingleStatisticTest - extends AbstractSubSingleStatisticTest> { +public final class MoveEvaluationSpeedSubSingleStatisticTest + extends AbstractSubSingleStatisticTest> { @Override - protected Function> + protected Function> getSubSingleStatisticConstructor() { - return MoveCalculationSpeedSubSingleStatistic::new; + return MoveEvaluationSpeedSubSingleStatistic::new; } @Override @@ -31,7 +31,7 @@ protected void runTest(SoftAssertions assertions, List outpu assertions.assertThat(outputPoints) .hasSize(1) .first() - .matches(s -> s.getValue() == Long.MAX_VALUE, "Move calculation speeds do not match.") + .matches(s -> s.getValue() == Long.MAX_VALUE, "Move evaluation speeds do not match.") .matches(s -> s.getTimeMillisSpent() == Long.MAX_VALUE, "Millis do not match."); } diff --git a/benchmark/src/test/resources/ai/timefold/solver/benchmark/impl/result/testPlannerBenchmarkResult.xml b/benchmark/src/test/resources/ai/timefold/solver/benchmark/impl/result/testPlannerBenchmarkResult.xml index e3438d630a..12ca07a3f6 100644 --- a/benchmark/src/test/resources/ai/timefold/solver/benchmark/impl/result/testPlannerBenchmarkResult.xml +++ b/benchmark/src/test/resources/ai/timefold/solver/benchmark/impl/result/testPlannerBenchmarkResult.xml @@ -35,11 +35,11 @@ 0hard/-37858soft 276 1632 - 500 + 500 -1 -1 - -1 + -1 @@ -48,11 +48,11 @@ 0hard/-49506soft 60 5832 - 1000 + 1000 -1 -1 - -1 + -1 @@ -79,11 +79,11 @@ 0hard/-22838soft 251 1668 - 1000 + 1000 -1 -1 - -1 + -1 @@ -92,11 +92,11 @@ 0hard/-30857soft 61 5530 - 2000 + 2000 -1 -1 - -1 + -1 @@ -141,11 +141,11 @@ 0hard/-21607soft 5000 777269 - 700000 + 700000 -1 -1 - -1 + -1 @@ -154,11 +154,11 @@ 0hard/-25288soft 5000 856937 - 500000 + 500000 -1 -1 - -1 + -1 @@ -203,11 +203,11 @@ 0hard/-21252soft 5000 761968 - 361000 + 361000 -1 -1 - -1 + -1 @@ -216,11 +216,11 @@ 0hard/-25645soft 5000 858383 - 5580 + 5580 -1 -1 - -1 + -1 @@ -287,11 +287,11 @@ 0hard/-21929soft 5000 993153 - 593000 + 593000 -1 -1 - -1 + -1 @@ -300,11 +300,11 @@ 0hard/-25258soft 5000 850727 - 550000 + 550000 -1 -1 - -1 + -1 @@ -371,11 +371,11 @@ 0hard/-21148soft 5000 873469 - 573000 + 573000 -1 -1 - -1 + -1 @@ -384,11 +384,11 @@ 0hard/-23083soft 5000 726321 - 526000 + 526000 -1 -1 - -1 + -1 diff --git a/core/src/build/revapi-differences.json b/core/src/build/revapi-differences.json index 01f71c4848..87255f0389 100644 --- a/core/src/build/revapi-differences.json +++ b/core/src/build/revapi-differences.json @@ -55,7 +55,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\", \"unimprovedMoveCountLimit\", \"terminationConfigList\"}", + "newValue": "{\"terminationClass\", \"terminationCompositionStyle\", \"spentLimit\", \"millisecondsSpentLimit\", \"secondsSpentLimit\", \"minutesSpentLimit\", \"hoursSpentLimit\", \"daysSpentLimit\", \"unimprovedSpentLimit\", \"unimprovedMillisecondsSpentLimit\", \"unimprovedSecondsSpentLimit\", \"unimprovedMinutesSpentLimit\", \"unimprovedHoursSpentLimit\", \"unimprovedDaysSpentLimit\", \"unimprovedScoreDifferenceThreshold\", \"bestScoreLimit\", \"bestScoreFeasible\", \"stepCountLimit\", \"unimprovedStepCountLimit\", \"scoreCalculationCountLimit\", \"moveCountLimit\", \"terminationConfigList\"}", "justification": "Add new termination configuration" } ] diff --git a/core/src/main/java/ai/timefold/solver/core/api/solver/SolverJob.java b/core/src/main/java/ai/timefold/solver/core/api/solver/SolverJob.java index a5bebbb677..dd4cfbcdbb 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/solver/SolverJob.java +++ b/core/src/main/java/ai/timefold/solver/core/api/solver/SolverJob.java @@ -103,7 +103,7 @@ public interface SolverJob { * * @return the number of move evaluations that had occurred during solving since the last (re)start, at least 0 */ - long getMoveCalculationCount(); + long getMoveEvaluationCount(); /** * Return the {@link ProblemSizeStatistics} for the {@link PlanningSolution problem} submitted to the @@ -133,5 +133,5 @@ public interface SolverJob { * @return the average number of move evaluations per second that had occurred during solving * since the last (re)start, at least 0 */ - long getMoveCalculationSpeed(); + long getMoveEvaluationSpeed(); } diff --git a/core/src/main/java/ai/timefold/solver/core/config/solver/SolverConfig.java b/core/src/main/java/ai/timefold/solver/core/config/solver/SolverConfig.java index 6570afe089..fa6788a9c6 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/solver/SolverConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/solver/SolverConfig.java @@ -631,7 +631,7 @@ public DomainAccessType determineDomainAccessType() { public MonitoringConfig determineMetricConfig() { return Objects.requireNonNullElse(monitoringConfig, new MonitoringConfig().withSolverMetricList(Arrays.asList(SolverMetric.SOLVE_DURATION, SolverMetric.ERROR_COUNT, - SolverMetric.SCORE_CALCULATION_COUNT, SolverMetric.MOVE_CALCULATION_COUNT, + SolverMetric.SCORE_CALCULATION_COUNT, SolverMetric.MOVE_EVALUATION_COUNT, SolverMetric.PROBLEM_ENTITY_COUNT, SolverMetric.PROBLEM_VARIABLE_COUNT, SolverMetric.PROBLEM_VALUE_COUNT, SolverMetric.PROBLEM_SIZE_LOG))); } diff --git a/core/src/main/java/ai/timefold/solver/core/config/solver/monitoring/SolverMetric.java b/core/src/main/java/ai/timefold/solver/core/config/solver/monitoring/SolverMetric.java index d13bab0c66..05421b0fb0 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/solver/monitoring/SolverMetric.java +++ b/core/src/main/java/ai/timefold/solver/core/config/solver/monitoring/SolverMetric.java @@ -31,8 +31,8 @@ public enum SolverMetric { SCORE_CALCULATION_COUNT("timefold.solver.score.calculation.count", SolverScope::getScoreCalculationCount, false), - MOVE_CALCULATION_COUNT("timefold.solver.move.calculation.count", - SolverScope::getMoveCalculationCount, + MOVE_EVALUATION_COUNT("timefold.solver.move.evaluation.count", + SolverScope::getMoveEvaluationCount, false), PROBLEM_ENTITY_COUNT("timefold.solver.problem.entities", solverScope -> solverScope.getProblemSizeStatistics().entityCount(), 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 a4b46495f3..45c83e5c75 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 @@ -36,7 +36,6 @@ "unimprovedStepCountLimit", "scoreCalculationCountLimit", "moveCountLimit", - "unimprovedMoveCountLimit", "terminationConfigList" }) public class TerminationConfig extends AbstractConfig { @@ -75,7 +74,6 @@ public class TerminationConfig extends AbstractConfig { private Long scoreCalculationCountLimit = null; private Long moveCountLimit = null; - private Long unimprovedMoveCountLimit = null; @XmlElement(name = "termination") private List terminationConfigList = null; @@ -256,14 +254,6 @@ public void setMoveCountLimit(Long moveCountLimit) { this.moveCountLimit = moveCountLimit; } - public Long getUnimprovedMoveCountLimit() { - return unimprovedMoveCountLimit; - } - - public void setUnimprovedMoveCountLimit(Long unimprovedMoveCountLimit) { - this.unimprovedMoveCountLimit = unimprovedMoveCountLimit; - } - public List getTerminationConfigList() { return terminationConfigList; } @@ -385,11 +375,6 @@ public TerminationConfig withMoveCountLimit(Long moveCountLimit) { return this; } - public TerminationConfig withUnimprovedMoveCountLimit(Long unimprovedMoveCountLimit) { - this.unimprovedMoveCountLimit = unimprovedMoveCountLimit; - return this; - } - public TerminationConfig withTerminationConfigList(List terminationConfigList) { this.terminationConfigList = terminationConfigList; return this; @@ -498,7 +483,6 @@ public boolean isConfigured() { unimprovedStepCountLimit != null || scoreCalculationCountLimit != null || moveCountLimit != null || - unimprovedMoveCountLimit != null || isTerminationListConfigured(); } @@ -543,8 +527,6 @@ public TerminationConfig inherit(TerminationConfig inheritedConfig) { inheritedConfig.getScoreCalculationCountLimit()); moveCountLimit = ConfigUtils.inheritOverwritableProperty(moveCountLimit, inheritedConfig.getMoveCountLimit()); - unimprovedMoveCountLimit = ConfigUtils.inheritOverwritableProperty(unimprovedMoveCountLimit, - inheritedConfig.getUnimprovedMoveCountLimit()); terminationConfigList = ConfigUtils.inheritMergeableListConfig( terminationConfigList, inheritedConfig.getTerminationConfigList()); return this; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java index 935e85bc19..d29dd1d446 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java @@ -147,13 +147,13 @@ public void phaseEnded(ConstructionHeuristicPhaseScope phaseScope) { phaseScope.endingNow(); if (decider.isLoggingEnabled() && logger.isInfoEnabled()) { logger.info("{}Construction Heuristic phase ({}) ended: time spent ({}), best score ({})," - + " score calculation speed ({}/sec), move calculation speed ({}/sec), step total ({}).", + + " score calculation speed ({}/sec), move evaluation speed ({}/sec), step total ({}).", logIndentation, phaseIndex, phaseScope.calculateSolverTimeMillisSpentUpToNow(), phaseScope.getBestScore(), phaseScope.getPhaseScoreCalculationSpeed(), - phaseScope.getPhaseMoveCalculationSpeed(), + phaseScope.getPhaseMoveEvaluationSpeed(), phaseScope.getNextStepIndex()); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/decider/forager/DefaultConstructionHeuristicForager.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/decider/forager/DefaultConstructionHeuristicForager.java index 4cac422b3c..2c97b89020 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/decider/forager/DefaultConstructionHeuristicForager.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/decider/forager/DefaultConstructionHeuristicForager.java @@ -39,6 +39,7 @@ public void stepEnded(ConstructionHeuristicStepScope stepScope) { @Override public void addMove(ConstructionHeuristicMoveScope moveScope) { selectedMoveCount++; + moveScope.getStepScope().incrementMoveEvaluationCount(); checkPickEarly(moveScope); if (maxScoreMoveScope == null || moveScope.getScore().compareTo(maxScoreMoveScope.getScore()) > 0) { maxScoreMoveScope = moveScope; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/ExhaustiveSearchDecider.java b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/ExhaustiveSearchDecider.java index b0c6d10bc9..f0ad5ea6f3 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/ExhaustiveSearchDecider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/ExhaustiveSearchDecider.java @@ -114,6 +114,7 @@ public void expandNode(ExhaustiveSearchStepScope stepScope) { // If the original value is null and the variable allows unassigned values, // the move to null must be done too. doMove(stepScope, moveNode); + stepScope.incrementMoveEvaluationCount(); // TODO in the lowest level (and only in that level) QuitEarly can be useful // No QuitEarly because lower layers might be promising stepScope.getPhaseScope().getSolverScope().checkYielding(); 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 314fb21931..298fe5830c 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 @@ -61,7 +61,6 @@ public void setLastCompletedStepScope(ExhaustiveSearchStepScope lastC @Override public > Score_ calculateScore() { - getScoreDirector().incrementMoveCalculationCount(); return super.calculateScore(); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java index 4f3d1b714e..63bd760b2b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java @@ -130,7 +130,9 @@ public void stepStarted(LocalSearchStepScope stepScope) { public void stepEnded(LocalSearchStepScope stepScope) { super.stepEnded(stepScope); decider.stepEnded(stepScope); - collectMetrics(stepScope); + if (stepScope.isPhaseEnableCollectMetrics()) { + collectMetrics(stepScope); + } LocalSearchPhaseScope phaseScope = stepScope.getPhaseScope(); if (logger.isDebugEnabled()) { logger.debug("{} LS step ({}), time spent ({}), score ({}), {} best score ({})," + @@ -197,13 +199,13 @@ public void phaseEnded(LocalSearchPhaseScope phaseScope) { decider.phaseEnded(phaseScope); phaseScope.endingNow(); logger.info("{}Local Search phase ({}) ended: time spent ({}), best score ({})," - + " score calculation speed ({}/sec), move calculation speed ({}/sec), step total ({}).", + + " score calculation speed ({}/sec), move evaluation speed ({}/sec), step total ({}).", logIndentation, phaseIndex, phaseScope.calculateSolverTimeMillisSpentUpToNow(), phaseScope.getBestScore(), phaseScope.getPhaseScoreCalculationSpeed(), - phaseScope.getPhaseMoveCalculationSpeed(), + phaseScope.getPhaseMoveEvaluationSpeed(), phaseScope.getNextStepIndex()); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/forager/AcceptedLocalSearchForager.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/forager/AcceptedLocalSearchForager.java index a9af793de4..3edfa93d1d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/forager/AcceptedLocalSearchForager.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/forager/AcceptedLocalSearchForager.java @@ -74,6 +74,7 @@ public boolean supportsNeverEndingMoveSelector() { @Override public void addMove(LocalSearchMoveScope moveScope) { selectedMoveCount++; + moveScope.getStepScope().incrementMoveEvaluationCount(); if (moveScope.getAccepted()) { acceptedMoveCount++; checkPickEarly(moveScope); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java index 444cac5a3f..2bc064d76d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java @@ -116,8 +116,6 @@ public void phaseStarted(AbstractPhaseScope phaseScope) { @Override public void phaseEnded(AbstractPhaseScope phaseScope) { solver.phaseEnded(phaseScope); - phaseScope.setEnableCollectMetrics(true); - phaseScope.resetScoreDirectorMetrics(); phaseTermination.phaseEnded(phaseScope); phaseLifecycleSupport.firePhaseEnded(phaseScope); } @@ -162,7 +160,6 @@ protected > void predictWorkingStepScore(AbstractSt @Override public void stepEnded(AbstractStepScope stepScope) { solver.stepEnded(stepScope); - stepScope.setMoveCalculationCount(stepScope.getPhaseScope().getPhaseMoveCalculationCount()); if (enableCollectMetrics) { collectMetrics(stepScope); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java index 712f25fda7..07800f0f60 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java @@ -24,17 +24,17 @@ public abstract class AbstractPhaseScope { protected Long startingSystemTimeMillis; protected Long startingScoreCalculationCount; - protected Long startingMoveCalculationCount; + protected Long startingMoveEvaluationCount; protected Score startingScore; protected Long endingSystemTimeMillis; protected Long endingScoreCalculationCount; - protected Long endingMoveCalculationCount; + protected Long endingMoveEvaluationCount; protected long childThreadsScoreCalculationCount = 0L; - protected long childThreadsMoveCalculationCount = 0L; + protected long childThreadsMoveEvaluationCount = 0L; protected int bestSolutionStepIndex; - protected long bestSolutionMoveCalculationCount = 0L; + protected long bestSolutionMoveEvaluationCount = 0L; protected boolean enableCollectMetrics = true; @@ -93,14 +93,6 @@ public void setBestSolutionStepIndex(int bestSolutionStepIndex) { this.bestSolutionStepIndex = bestSolutionStepIndex; } - public long getBestSolutionMoveCalculationCount() { - return bestSolutionMoveCalculationCount; - } - - public void setBestSolutionMoveCalculationCount(long bestSolutionMoveCalculationCount) { - this.bestSolutionMoveCalculationCount = bestSolutionMoveCalculationCount; - } - public abstract AbstractStepScope getLastCompletedStepScope(); public boolean isEnableCollectMetrics() { @@ -122,23 +114,18 @@ public void reset() { if (getLastCompletedStepScope().getStepIndex() < 0) { getLastCompletedStepScope().setScore(startingScore); } - resetScoreDirectorMetrics(); - } - - public void resetScoreDirectorMetrics() { - getScoreDirector().setEnableMetricCollection(enableCollectMetrics); } public void startingNow() { startingSystemTimeMillis = System.currentTimeMillis(); startingScoreCalculationCount = getScoreDirector().getCalculationCount(); - startingMoveCalculationCount = getScoreDirector().getMoveCalculationCount(); + startingMoveEvaluationCount = getSolverScope().getMoveEvaluationCount(); } public void endingNow() { endingSystemTimeMillis = System.currentTimeMillis(); - endingScoreCalculationCount = getScoreDirector().getCalculationCount() + childThreadsScoreCalculationCount; - endingMoveCalculationCount = getScoreDirector().getMoveCalculationCount() + childThreadsMoveCalculationCount; + endingScoreCalculationCount = getScoreDirector().getCalculationCount(); + endingMoveEvaluationCount = getSolverScope().getMoveEvaluationCount(); } public SolutionDescriptor getSolutionDescriptor() { @@ -163,21 +150,21 @@ public void addChildThreadsScoreCalculationCount(long addition) { childThreadsScoreCalculationCount += addition; } - public void addChildThreadsMoveCalculationCount(long addition) { - solverScope.addChildThreadsMoveCalculationCount(addition); - childThreadsMoveCalculationCount += addition; + public void addChildThreadsMoveEvaluationCount(long addition) { + solverScope.addChildThreadsMoveEvaluationCount(addition); + childThreadsMoveEvaluationCount += addition; } public long getPhaseScoreCalculationCount() { return endingScoreCalculationCount - startingScoreCalculationCount + childThreadsScoreCalculationCount; } - public long getPhaseMoveCalculationCount() { - var currentMoveCalculationCount = endingMoveCalculationCount; - if (endingMoveCalculationCount == null) { - currentMoveCalculationCount = getScoreDirector().getMoveCalculationCount(); + public long getPhaseMoveEvaluationCount() { + var currentMoveEvaluationCount = endingMoveEvaluationCount; + if (endingMoveEvaluationCount == null) { + currentMoveEvaluationCount = getSolverScope().getMoveEvaluationCount(); } - return currentMoveCalculationCount - startingMoveCalculationCount + childThreadsMoveCalculationCount; + return currentMoveEvaluationCount - startingMoveEvaluationCount + childThreadsMoveEvaluationCount; } /** @@ -190,8 +177,8 @@ public long getPhaseScoreCalculationSpeed() { /** * @return at least 0, per second */ - public long getPhaseMoveCalculationSpeed() { - return getMetricCalculationSpeed(getPhaseMoveCalculationCount()); + public long getPhaseMoveEvaluationSpeed() { + return getMetricCalculationSpeed(getPhaseMoveEvaluationCount()); } private long getMetricCalculationSpeed(long metric) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractStepScope.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractStepScope.java index b1a7dbea3b..cbdf45b92b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractStepScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractStepScope.java @@ -13,8 +13,6 @@ public abstract class AbstractStepScope { protected final int stepIndex; - protected long moveCalculationCount = 0L; - protected Score score = null; protected boolean bestScoreImproved = false; // Stays null if there is no need to clone it @@ -30,14 +28,6 @@ public int getStepIndex() { return stepIndex; } - public long getMoveCalculationCount() { - return moveCalculationCount; - } - - public void setMoveCalculationCount(long moveCalculationCount) { - this.moveCalculationCount = moveCalculationCount; - } - public Score getScore() { return score; } @@ -54,10 +44,20 @@ public void setBestScoreImproved(Boolean bestScoreImproved) { this.bestScoreImproved = bestScoreImproved; } + public void incrementMoveEvaluationCount() { + if (isPhaseEnableCollectMetrics()) { + getPhaseScope().getSolverScope().addMoveEvaluationCount(1L); + } + } + // ************************************************************************ // Calculated methods // ************************************************************************ + public boolean isPhaseEnableCollectMetrics() { + return getPhaseScope().isEnableCollectMetrics(); + } + public > InnerScoreDirector getScoreDirector() { return getPhaseScope().getScoreDirector(); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java index 88521cb46b..fc332e5f53 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java @@ -66,7 +66,6 @@ public abstract class AbstractScoreDirector solutionTracker; - private boolean collectMetricEnabled = false; - protected AbstractScoreDirector(Factory_ scoreDirectorFactory, boolean lookUpEnabled, boolean constraintMatchEnabledPreference, boolean expectShadowVariablesInCorrectState) { var solutionDescriptor = scoreDirectorFactory.getSolutionDescriptor(); @@ -164,33 +161,11 @@ public void incrementCalculationCount() { this.calculationCount++; } - @Override - public long getMoveCalculationCount() { - return moveCalculationCount; - } - - @Override - public void resetMoveCalculationCount() { - this.moveCalculationCount = 0L; - } - - @Override - public void incrementMoveCalculationCount() { - if (collectMetricEnabled) { - this.moveCalculationCount++; - } - } - @Override public SupplyManager getSupplyManager() { return variableListenerSupport; } - @Override - public void setEnableMetricCollection(boolean enable) { - this.collectMetricEnabled = enable; - } - // ************************************************************************ // Complex methods // ************************************************************************ @@ -249,7 +224,6 @@ public Score_ doAndProcessMove(Move move, boolean assertMoveScoreFrom } Move undoMove = move.doMove(this); Score_ score = calculateScore(); - incrementMoveCalculationCount(); if (assertMoveScoreFromScratch) { undoMoveText = undoMove.toString(); if (trackingWorkingSolution) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java index 840d64d4e9..68d7004831 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java @@ -38,7 +38,7 @@ * @param the score type to go with the solution */ public interface InnerScoreDirector> - extends VariableDescriptorAwareScoreDirector, MetricCollector, AutoCloseable { + extends VariableDescriptorAwareScoreDirector, AutoCloseable { static > ConstraintAnalysis getConstraintAnalysis( ConstraintMatchTotal constraintMatchTotal, boolean analyzeConstraintMatches) { @@ -225,12 +225,6 @@ default Solution_ cloneWorkingSolution() { void incrementCalculationCount(); - long getMoveCalculationCount(); - - void resetMoveCalculationCount(); - - void incrementMoveCalculationCount(); - /** * @return never null */ diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/MetricCollector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/MetricCollector.java deleted file mode 100644 index 99f780a52a..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/MetricCollector.java +++ /dev/null @@ -1,10 +0,0 @@ -package ai.timefold.solver.core.impl.score.director; - -/** - * Allows enabling or disabling the metric collection. In cases like Ruin and Recreate moves, metrics such as move count - * should not be updated. - */ -public interface MetricCollector { - - void setEnableMetricCollection(boolean enable); -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java index c6b82a5338..f99905026e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java @@ -96,16 +96,16 @@ public long getScoreCalculationCount() { return solverScope.getScoreCalculationCount(); } - public long getMoveCalculationCount() { - return solverScope.getMoveCalculationCount(); + public long getMoveEvaluationCount() { + return solverScope.getMoveEvaluationCount(); } public long getScoreCalculationSpeed() { return solverScope.getScoreCalculationSpeed(); } - public long getMoveCalculationSpeed() { - return solverScope.getMoveCalculationSpeed(); + public long getMoveEvaluationSpeed() { + return solverScope.getMoveEvaluationSpeed(); } @Override @@ -224,7 +224,6 @@ public void solvingStarted(SolverScope solverScope) { assertCorrectSolutionState(); solverScope.startingNow(); solverScope.getScoreDirector().resetCalculationCount(); - solverScope.getScoreDirector().resetMoveCalculationCount(); super.solvingStarted(solverScope); var startingSolverCount = solverScope.getStartingSolverCount() + 1; solverScope.setStartingSolverCount(startingSolverCount); @@ -315,11 +314,11 @@ public void solvingEnded(SolverScope solverScope) { public void outerSolvingEnded(SolverScope solverScope) { logger.info("Solving ended: time spent ({}), best score ({}), score calculation speed ({}/sec), " - + "move calculation speed ({}/sec), phase total ({}), environment mode ({}), move thread count ({}).", + + "move evaluation speed ({}/sec), phase total ({}), environment mode ({}), move thread count ({}).", solverScope.getTimeMillisSpent(), solverScope.getBestScore(), solverScope.getScoreCalculationSpeed(), - solverScope.getMoveCalculationSpeed(), + solverScope.getMoveEvaluationSpeed(), phaseList.size(), environmentMode.name(), moveThreadCountDescription); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJob.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJob.java index ae34417fa9..11a5cc04d0 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJob.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverJob.java @@ -231,8 +231,8 @@ public long getScoreCalculationCount() { } @Override - public long getMoveCalculationCount() { - return solver.getMoveCalculationCount(); + public long getMoveEvaluationCount() { + return solver.getMoveEvaluationCount(); } @Override @@ -241,8 +241,8 @@ public long getScoreCalculationSpeed() { } @Override - public long getMoveCalculationSpeed() { - return solver.getMoveCalculationSpeed(); + public long getMoveEvaluationSpeed() { + return solver.getMoveEvaluationSpeed(); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/recaller/BestSolutionRecaller.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/recaller/BestSolutionRecaller.java index 291994aba6..bc05f55fe2 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/recaller/BestSolutionRecaller.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/recaller/BestSolutionRecaller.java @@ -70,7 +70,6 @@ public void processWorkingSolutionDuringConstructionHeuristicsStep(AbstractStepS SolverScope solverScope = phaseScope.getSolverScope(); stepScope.setBestScoreImproved(true); phaseScope.setBestSolutionStepIndex(stepScope.getStepIndex()); - phaseScope.setBestSolutionMoveCalculationCount(phaseScope.getPhaseMoveCalculationCount()); Solution_ newBestSolution = stepScope.getWorkingSolution(); // Construction heuristics don't fire intermediate best solution changed events. // But the best solution and score are updated, so that unimproved* terminations work correctly. @@ -85,7 +84,6 @@ public void processWorkingSolutionDuringStep(AbstractStepScope stepSc stepScope.setBestScoreImproved(bestScoreImproved); if (bestScoreImproved) { phaseScope.setBestSolutionStepIndex(stepScope.getStepIndex()); - phaseScope.setBestSolutionMoveCalculationCount(phaseScope.getPhaseMoveCalculationCount()); Solution_ newBestSolution = stepScope.createOrGetClonedSolution(); updateBestSolutionAndFire(solverScope, score, newBestSolution); } else if (assertBestScoreIsUnmodified) { @@ -104,7 +102,6 @@ public void processWorkingSolutionDuringMove(Score score, AbstractStepScope { */ private Semaphore runnableThreadSemaphore = null; - private long childThreadsScoreCalculationCount = 0; - private long moveEvaluationCount = 0; + private long childThreadsScoreCalculationCount = 0L; - private long childThreadsMoveCalculationCount = 0L; + private long moveEvaluationCount = 0L; + private long childThreadsMoveEvaluationCount = 0L; private Score startingInitializedScore; @@ -189,25 +189,16 @@ public long getScoreCalculationCount() { return scoreDirector.getCalculationCount() + childThreadsScoreCalculationCount; } - public void addChildThreadsMoveCalculationCount(long addition) { - childThreadsMoveCalculationCount += addition; - } - - public long getMoveCalculationCount() { - return scoreDirector.getMoveCalculationCount() + childThreadsMoveCalculationCount; - } - public void addMoveEvaluationCount(long addition) { moveEvaluationCount += addition; } - public long getMoveEvaluationCount() { - return moveEvaluationCount; + public void addChildThreadsMoveEvaluationCount(long addition) { + childThreadsMoveEvaluationCount += addition; } - public long getMoveEvaluationSpeed() { - long timeMillisSpent = getTimeMillisSpent(); - return getSpeed(getMoveEvaluationCount(), timeMillisSpent); + public long getMoveEvaluationCount() { + return moveEvaluationCount + childThreadsMoveEvaluationCount; } public Solution_ getBestSolution() { @@ -251,6 +242,7 @@ public boolean isMetricEnabled(SolverMetric solverMetric) { public void startingNow() { startingSystemTimeMillis.set(System.currentTimeMillis()); resetAtomicLongTimeMillis(endingSystemTimeMillis); + this.moveEvaluationCount = 0L; } public Long getBestSolutionTimeMillisSpent() { @@ -301,9 +293,9 @@ public long getScoreCalculationSpeed() { /** * @return at least 0, per second */ - public long getMoveCalculationSpeed() { + public long getMoveEvaluationSpeed() { long timeMillisSpent = getTimeMillisSpent(); - return getSpeed(getMoveCalculationCount(), timeMillisSpent); + return getSpeed(getMoveEvaluationCount(), timeMillisSpent); } public static long getSpeed(long metric, long timeMillisSpent) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/AbstractTermination.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/AbstractTermination.java index 420861e313..8a6ceda572 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/AbstractTermination.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/AbstractTermination.java @@ -14,7 +14,7 @@ public abstract sealed class AbstractTermination ChildThreadPlumbingTermination, PhaseToSolverTerminationBridge, ScoreCalculationCountTermination, StepCountTermination, TimeMillisSpentTermination, UnimprovedStepCountTermination, UnimprovedTimeMillisSpentScoreDifferenceThresholdTermination, UnimprovedTimeMillisSpentTermination, - MoveCountTermination, UnimprovedMoveCountTermination { + MoveCountTermination { protected final transient Logger logger = LoggerFactory.getLogger(getClass()); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/MoveCountTermination.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/MoveCountTermination.java index 2dcfb8efdd..9136096d3c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/MoveCountTermination.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/MoveCountTermination.java @@ -1,7 +1,6 @@ package ai.timefold.solver.core.impl.solver.termination; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; -import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.solver.scope.SolverScope; import ai.timefold.solver.core.impl.solver.thread.ChildThreadType; @@ -26,17 +25,17 @@ public long getMoveCountLimit() { @Override public boolean isSolverTerminated(SolverScope solverScope) { - return isTerminated(solverScope.getScoreDirector()); + return isTerminated(solverScope); } @Override public boolean isPhaseTerminated(AbstractPhaseScope phaseScope) { - return isTerminated(phaseScope.getScoreDirector()); + return isTerminated(phaseScope.getSolverScope()); } - private boolean isTerminated(InnerScoreDirector scoreDirector) { - long moveCalculationCount = scoreDirector.getMoveCalculationCount(); - return moveCalculationCount >= moveCountLimit; + private boolean isTerminated(SolverScope solverScope) { + long moveEvaluationCount = solverScope.getMoveEvaluationCount(); + return moveEvaluationCount >= moveCountLimit; } // ************************************************************************ @@ -45,17 +44,17 @@ private boolean isTerminated(InnerScoreDirector scoreDirector) { @Override public double calculateSolverTimeGradient(SolverScope solverScope) { - return calculateTimeGradient(solverScope.getScoreDirector()); + return calculateTimeGradient(solverScope); } @Override public double calculatePhaseTimeGradient(AbstractPhaseScope phaseScope) { - return calculateTimeGradient(phaseScope.getScoreDirector()); + return calculateTimeGradient(phaseScope.getSolverScope()); } - private double calculateTimeGradient(InnerScoreDirector scoreDirector) { - var moveCalculationCount = scoreDirector.getMoveCalculationCount(); - var timeGradient = moveCalculationCount / ((double) moveCountLimit); + private double calculateTimeGradient(SolverScope solverScope) { + var moveEvaluationCount = solverScope.getMoveEvaluationCount(); + var timeGradient = moveEvaluationCount / ((double) moveCountLimit); return Math.min(timeGradient, 1.0); } // ************************************************************************ 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 7c3e8c9261..d38f46bb69 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 @@ -86,9 +86,6 @@ public > Termination buildTermination( if (terminationConfig.getMoveCountLimit() != null) { terminationList.add(new MoveCountTermination<>(terminationConfig.getMoveCountLimit())); } - if (terminationConfig.getUnimprovedMoveCountLimit() != null) { - terminationList.add(new UnimprovedMoveCountTermination<>(terminationConfig.getUnimprovedMoveCountLimit())); - } terminationList.addAll(buildInnerTermination(configPolicy)); return buildTerminationFromList(terminationList); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/UnimprovedMoveCountTermination.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/UnimprovedMoveCountTermination.java deleted file mode 100644 index e2823326a4..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/termination/UnimprovedMoveCountTermination.java +++ /dev/null @@ -1,73 +0,0 @@ -package ai.timefold.solver.core.impl.solver.termination; - -import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; -import ai.timefold.solver.core.impl.solver.scope.SolverScope; -import ai.timefold.solver.core.impl.solver.thread.ChildThreadType; - -public final class UnimprovedMoveCountTermination extends AbstractTermination { - - private final long unimprovedMoveCountLimit; - - public UnimprovedMoveCountTermination(long unimprovedMoveCountLimit) { - this.unimprovedMoveCountLimit = unimprovedMoveCountLimit; - if (unimprovedMoveCountLimit < 0) { - throw new IllegalArgumentException("The unimprovedMoveCountLimit (%d) cannot be negative." - .formatted(unimprovedMoveCountLimit)); - } - } - - // ************************************************************************ - // Terminated methods - // ************************************************************************ - - @Override - public boolean isSolverTerminated(SolverScope solverScope) { - throw new UnsupportedOperationException( - "%s can only be used for phase termination.".formatted(getClass().getSimpleName())); - } - - @Override - public boolean isPhaseTerminated(AbstractPhaseScope phaseScope) { - var unimprovedMoveCount = calculateUnimprovedMoveCount(phaseScope); - return unimprovedMoveCount >= unimprovedMoveCountLimit; - } - - private static long calculateUnimprovedMoveCount(AbstractPhaseScope phaseScope) { - var bestSolutionMoveCount = phaseScope.getBestSolutionMoveCalculationCount(); - var lastStepMoveCalculationCount = phaseScope.getLastCompletedStepScope().getMoveCalculationCount(); - return lastStepMoveCalculationCount - bestSolutionMoveCount; - } - - // ************************************************************************ - // Time gradient methods - // ************************************************************************ - - @Override - public double calculateSolverTimeGradient(SolverScope solverScope) { - throw new UnsupportedOperationException( - "%s can only be used for phase termination.".formatted(getClass().getSimpleName())); - } - - @Override - public double calculatePhaseTimeGradient(AbstractPhaseScope phaseScope) { - var unimprovedMoveCount = calculateUnimprovedMoveCount(phaseScope); - var timeGradient = unimprovedMoveCount / ((double) unimprovedMoveCountLimit); - return Math.min(timeGradient, 1.0); - } - - // ************************************************************************ - // Other methods - // ************************************************************************ - - @Override - public UnimprovedMoveCountTermination createChildThreadTermination(SolverScope solverScope, - ChildThreadType childThreadType) { - return new UnimprovedMoveCountTermination<>(unimprovedMoveCountLimit); - } - - @Override - public String toString() { - return "unimprovedMoveCountTermination(%d)".formatted(unimprovedMoveCountLimit); - } - -} diff --git a/core/src/main/resources/solver.xsd b/core/src/main/resources/solver.xsd index ae1c065011..83ea05c77f 100644 --- a/core/src/main/resources/solver.xsd +++ b/core/src/main/resources/solver.xsd @@ -197,8 +197,6 @@ - - @@ -1577,7 +1575,7 @@ - + diff --git a/core/src/test/java/ai/timefold/solver/core/api/solver/SolverManagerTest.java b/core/src/test/java/ai/timefold/solver/core/api/solver/SolverManagerTest.java index 706f1b2209..f44bf5d3d5 100644 --- a/core/src/test/java/ai/timefold/solver/core/api/solver/SolverManagerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/api/solver/SolverManagerTest.java @@ -538,7 +538,7 @@ void testScoreCalculationCountForFinishedJob() throws ExecutionException, Interr solverJob.getFinalBestSolution(); assertThat(solverJob.getScoreCalculationCount()).isEqualTo(5L); - assertThat(solverJob.getMoveCalculationCount()).isEqualTo(4L); + assertThat(solverJob.getMoveEvaluationCount()).isEqualTo(4L); // Score calculation speed and solve duration are non-deterministic. // On an exceptionally fast machine, getSolvingDuration() can return Duration.ZERO. @@ -546,7 +546,7 @@ void testScoreCalculationCountForFinishedJob() throws ExecutionException, Interr // (i.e. by taking more than 5 seconds to finish solving). assertThat(solverJob.getSolvingDuration()).isGreaterThanOrEqualTo(Duration.ZERO); assertThat(solverJob.getScoreCalculationSpeed()).isNotNegative(); - assertThat(solverJob.getMoveCalculationSpeed()).isNotNegative(); + assertThat(solverJob.getMoveEvaluationSpeed()).isNotNegative(); } @Test diff --git a/core/src/test/java/ai/timefold/solver/core/config/solver/termination/TerminationConfigTest.java b/core/src/test/java/ai/timefold/solver/core/config/solver/termination/TerminationConfigTest.java index 8acd0559e5..db0bc3fbcf 100644 --- a/core/src/test/java/ai/timefold/solver/core/config/solver/termination/TerminationConfigTest.java +++ b/core/src/test/java/ai/timefold/solver/core/config/solver/termination/TerminationConfigTest.java @@ -87,14 +87,12 @@ void childWithTimeSpentLimitShouldNotInheritTimeSpentLimitFromParent() { @Test void checkMoveCountMetrics() { TerminationConfig parent = new TerminationConfig() - .withMoveCountLimit(2L) - .withUnimprovedMoveCountLimit(3L); + .withMoveCountLimit(2L); TerminationConfig child = new TerminationConfig(); child.inherit(parent); assertThat(child.getMoveCountLimit()).isEqualTo(2L); - assertThat(child.getUnimprovedMoveCountLimit()).isEqualTo(3L); assertThat(parent.isConfigured()).isTrue(); } 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 36cb6baebf..1839054b49 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 @@ -176,13 +176,13 @@ void solveWithInitializedEntitiesAndMetric() { var solvedE3 = solution.getEntityList().get(2); assertCode("e3", solvedE3); assertThat(solvedE3.getValue()).isEqualTo(v1); - assertThat(solution.getScore().initScore()).isEqualTo(0); + assertThat(solution.getScore().initScore()).isZero(); - SolverMetric.MOVE_CALCULATION_COUNT.register(solver); + SolverMetric.MOVE_EVALUATION_COUNT.register(solver); SolverMetric.SCORE_CALCULATION_COUNT.register(solver); meterRegistry.publish(solver); var scoreCount = meterRegistry.getMeasurement(SolverMetric.SCORE_CALCULATION_COUNT.getMeterId(), "VALUE"); - var moveCount = meterRegistry.getMeasurement(SolverMetric.MOVE_CALCULATION_COUNT.getMeterId(), "VALUE"); + var moveCount = meterRegistry.getMeasurement(SolverMetric.MOVE_EVALUATION_COUNT.getMeterId(), "VALUE"); assertThat(scoreCount).isPositive(); assertThat(moveCount).isPositive(); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateMoveSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateMoveSelectorTest.java index b7d0584486..0a37c3e7b7 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateMoveSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateMoveSelectorTest.java @@ -68,11 +68,11 @@ void testRuiningWithMetric() { solver.addEventListener(event -> meterRegistry.publish(solver)); solver.solve(problem); - SolverMetric.MOVE_CALCULATION_COUNT.register(solver); + SolverMetric.MOVE_EVALUATION_COUNT.register(solver); SolverMetric.SCORE_CALCULATION_COUNT.register(solver); meterRegistry.publish(solver); var scoreCount = meterRegistry.getMeasurement(SolverMetric.SCORE_CALCULATION_COUNT.getMeterId(), "VALUE"); - var moveCount = meterRegistry.getMeasurement(SolverMetric.MOVE_CALCULATION_COUNT.getMeterId(), "VALUE"); + var moveCount = meterRegistry.getMeasurement(SolverMetric.MOVE_EVALUATION_COUNT.getMeterId(), "VALUE"); assertThat(scoreCount).isPositive(); assertThat(moveCount).isPositive(); assertThat(scoreCount).isGreaterThan(moveCount); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListRuinRecreateMoveSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListRuinRecreateMoveSelectorTest.java index 54e67b8391..61039db206 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListRuinRecreateMoveSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListRuinRecreateMoveSelectorTest.java @@ -93,11 +93,11 @@ void testRuiningWithMetric() { solver.addEventListener(event -> meterRegistry.publish(solver)); solver.solve(problem); - SolverMetric.MOVE_CALCULATION_COUNT.register(solver); + SolverMetric.MOVE_EVALUATION_COUNT.register(solver); SolverMetric.SCORE_CALCULATION_COUNT.register(solver); meterRegistry.publish(solver); var scoreCount = meterRegistry.getMeasurement(SolverMetric.SCORE_CALCULATION_COUNT.getMeterId(), "VALUE"); - var moveCount = meterRegistry.getMeasurement(SolverMetric.MOVE_CALCULATION_COUNT.getMeterId(), "VALUE"); + var moveCount = meterRegistry.getMeasurement(SolverMetric.MOVE_EVALUATION_COUNT.getMeterId(), "VALUE"); assertThat(scoreCount).isPositive(); assertThat(moveCount).isPositive(); assertThat(scoreCount).isGreaterThan(moveCount); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java index 59b6a9aa76..b597ecc149 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java @@ -246,7 +246,7 @@ void checkDefaultMeters() { null, null, Meter.Type.GAUGE), - new Meter.Id(SolverMetric.MOVE_CALCULATION_COUNT.getMeterId(), + new Meter.Id(SolverMetric.MOVE_EVALUATION_COUNT.getMeterId(), Tags.empty(), null, null, @@ -333,7 +333,7 @@ void checkDefaultMetersTags() { null, null, Meter.Type.GAUGE), - new Meter.Id(SolverMetric.MOVE_CALCULATION_COUNT.getMeterId(), + new Meter.Id(SolverMetric.MOVE_EVALUATION_COUNT.getMeterId(), Tags.of("tag.key", "tag.value"), null, null, @@ -430,7 +430,7 @@ void solveMetrics() { assertThat(meterRegistry.getMeasurement(SolverMetric.SOLVE_DURATION.getMeterId(), "ACTIVE_TASKS")).isZero(); assertThat(meterRegistry.getMeasurement(SolverMetric.ERROR_COUNT.getMeterId(), "COUNT")).isZero(); assertThat(meterRegistry.getMeasurement(SolverMetric.SCORE_CALCULATION_COUNT.getMeterId(), "VALUE")).isPositive(); - assertThat(meterRegistry.getMeasurement(SolverMetric.MOVE_CALCULATION_COUNT.getMeterId(), "VALUE")).isPositive(); + assertThat(meterRegistry.getMeasurement(SolverMetric.MOVE_EVALUATION_COUNT.getMeterId(), "VALUE")).isPositive(); } @Test diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/MoveCountTerminationTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/MoveCountTerminationTest.java index d46bc6952f..1983665576 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/MoveCountTerminationTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/MoveCountTerminationTest.java @@ -6,7 +6,6 @@ import static org.mockito.Mockito.when; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; -import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.solver.scope.SolverScope; import ai.timefold.solver.core.impl.testdata.domain.TestdataSolution; @@ -19,25 +18,25 @@ class MoveCountTerminationTest { void phaseTermination() { var termination = new MoveCountTermination(4); var phaseScope = Mockito.mock(AbstractPhaseScope.class); - var scoreDirector = Mockito.mock(InnerScoreDirector.class); - when(phaseScope.getScoreDirector()).thenReturn(scoreDirector); + var moveScope = Mockito.mock(SolverScope.class); + when(phaseScope.getSolverScope()).thenReturn(moveScope); - when(scoreDirector.getMoveCalculationCount()).thenReturn(0L); + when(moveScope.getMoveEvaluationCount()).thenReturn(0L); assertThat(termination.isPhaseTerminated(phaseScope)).isFalse(); assertThat(termination.calculatePhaseTimeGradient(phaseScope)).isEqualTo(0.0, offset(0.0)); - when(scoreDirector.getMoveCalculationCount()).thenReturn(1L); + when(moveScope.getMoveEvaluationCount()).thenReturn(1L); assertThat(termination.isPhaseTerminated(phaseScope)).isFalse(); assertThat(termination.calculatePhaseTimeGradient(phaseScope)).isEqualTo(0.25, offset(0.0)); - when(scoreDirector.getMoveCalculationCount()).thenReturn(2L); + when(moveScope.getMoveEvaluationCount()).thenReturn(2L); assertThat(termination.isPhaseTerminated(phaseScope)).isFalse(); assertThat(termination.calculatePhaseTimeGradient(phaseScope)).isEqualTo(0.5, offset(0.0)); - when(scoreDirector.getMoveCalculationCount()).thenReturn(3L); + when(moveScope.getMoveEvaluationCount()).thenReturn(3L); assertThat(termination.isPhaseTerminated(phaseScope)).isFalse(); assertThat(termination.calculatePhaseTimeGradient(phaseScope)).isEqualTo(0.75, offset(0.0)); - when(scoreDirector.getMoveCalculationCount()).thenReturn(4L); + when(moveScope.getMoveEvaluationCount()).thenReturn(4L); assertThat(termination.isPhaseTerminated(phaseScope)).isTrue(); assertThat(termination.calculatePhaseTimeGradient(phaseScope)).isEqualTo(1.0, offset(0.0)); - when(scoreDirector.getMoveCalculationCount()).thenReturn(5L); + when(moveScope.getMoveEvaluationCount()).thenReturn(5L); assertThat(termination.isPhaseTerminated(phaseScope)).isTrue(); assertThat(termination.calculatePhaseTimeGradient(phaseScope)).isEqualTo(1.0, offset(0.0)); } @@ -46,25 +45,23 @@ void phaseTermination() { void solverTermination() { var termination = new MoveCountTermination(4); var solverScope = Mockito.mock(SolverScope.class); - var scoreDirector = Mockito.mock(InnerScoreDirector.class); - when(solverScope.getScoreDirector()).thenReturn(scoreDirector); - when(scoreDirector.getMoveCalculationCount()).thenReturn(0L); + when(solverScope.getMoveEvaluationCount()).thenReturn(0L); assertThat(termination.isSolverTerminated(solverScope)).isFalse(); assertThat(termination.calculateSolverTimeGradient(solverScope)).isEqualTo(0.0, offset(0.0)); - when(scoreDirector.getMoveCalculationCount()).thenReturn(1L); + when(solverScope.getMoveEvaluationCount()).thenReturn(1L); assertThat(termination.isSolverTerminated(solverScope)).isFalse(); assertThat(termination.calculateSolverTimeGradient(solverScope)).isEqualTo(0.25, offset(0.0)); - when(scoreDirector.getMoveCalculationCount()).thenReturn(2L); + when(solverScope.getMoveEvaluationCount()).thenReturn(2L); assertThat(termination.isSolverTerminated(solverScope)).isFalse(); assertThat(termination.calculateSolverTimeGradient(solverScope)).isEqualTo(0.5, offset(0.0)); - when(scoreDirector.getMoveCalculationCount()).thenReturn(3L); + when(solverScope.getMoveEvaluationCount()).thenReturn(3L); assertThat(termination.isSolverTerminated(solverScope)).isFalse(); assertThat(termination.calculateSolverTimeGradient(solverScope)).isEqualTo(0.75, offset(0.0)); - when(scoreDirector.getMoveCalculationCount()).thenReturn(4L); + when(solverScope.getMoveEvaluationCount()).thenReturn(4L); assertThat(termination.isSolverTerminated(solverScope)).isTrue(); assertThat(termination.calculateSolverTimeGradient(solverScope)).isEqualTo(1.0, offset(0.0)); - when(scoreDirector.getMoveCalculationCount()).thenReturn(5L); + when(solverScope.getMoveEvaluationCount()).thenReturn(5L); assertThat(termination.isSolverTerminated(solverScope)).isTrue(); assertThat(termination.calculateSolverTimeGradient(solverScope)).isEqualTo(1.0, offset(0.0)); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/TerminationFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/TerminationFactoryTest.java index daf6bbf038..ab32c6adc4 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/TerminationFactoryTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/TerminationFactoryTest.java @@ -170,13 +170,11 @@ TerminationFactory. create(terminationConfig) @Test void buildWithMoveCount() { TerminationConfig terminationConfig = new TerminationConfig() - .withMoveCountLimit(1L) - .withUnimprovedMoveCountLimit(2L); + .withMoveCountLimit(1L); List> terminationList = TerminationFactory. create(terminationConfig) .buildTimeBasedTermination(mock(HeuristicConfigPolicy.class)); - assertThat(terminationList).hasOnlyElementsOfTypes(MoveCountTermination.class, - UnimprovedMoveCountTermination.class); + assertThat(terminationList).hasOnlyElementsOfTypes(MoveCountTermination.class); } @Test diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/UnimprovedMoveCountTerminationTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/UnimprovedMoveCountTerminationTest.java deleted file mode 100644 index 2114d0c01f..0000000000 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/termination/UnimprovedMoveCountTerminationTest.java +++ /dev/null @@ -1,55 +0,0 @@ -package ai.timefold.solver.core.impl.solver.termination; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.data.Offset.offset; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; -import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; -import ai.timefold.solver.core.impl.testdata.domain.TestdataSolution; - -import org.junit.jupiter.api.Test; - -class UnimprovedMoveCountTerminationTest { - - @Test - void phaseTermination() { - var termination = new UnimprovedMoveCountTermination(4); - var phaseScope = mock(AbstractPhaseScope.class); - var lastCompletedStepScope = mock(AbstractStepScope.class); - when(phaseScope.getLastCompletedStepScope()).thenReturn(lastCompletedStepScope); - - when(phaseScope.getBestSolutionMoveCalculationCount()).thenReturn(10L); - when(lastCompletedStepScope.getMoveCalculationCount()).thenReturn(10L); - assertThat(termination.isPhaseTerminated(phaseScope)).isFalse(); - assertThat(termination.calculatePhaseTimeGradient(phaseScope)).isEqualTo(0.0, offset(0.0)); - when(lastCompletedStepScope.getMoveCalculationCount()).thenReturn(11L); - assertThat(termination.isPhaseTerminated(phaseScope)).isFalse(); - assertThat(termination.calculatePhaseTimeGradient(phaseScope)).isEqualTo(0.25, offset(0.0)); - when(lastCompletedStepScope.getMoveCalculationCount()).thenReturn(12L); - assertThat(termination.isPhaseTerminated(phaseScope)).isFalse(); - assertThat(termination.calculatePhaseTimeGradient(phaseScope)).isEqualTo(0.5, offset(0.0)); - when(lastCompletedStepScope.getMoveCalculationCount()).thenReturn(13L); - assertThat(termination.isPhaseTerminated(phaseScope)).isFalse(); - assertThat(termination.calculatePhaseTimeGradient(phaseScope)).isEqualTo(0.75, offset(0.0)); - when(lastCompletedStepScope.getMoveCalculationCount()).thenReturn(14L); - assertThat(termination.isPhaseTerminated(phaseScope)).isTrue(); - assertThat(termination.calculatePhaseTimeGradient(phaseScope)).isEqualTo(1.0, offset(0.0)); - when(lastCompletedStepScope.getMoveCalculationCount()).thenReturn(16L); - assertThat(termination.isPhaseTerminated(phaseScope)).isTrue(); - assertThat(termination.calculatePhaseTimeGradient(phaseScope)).isEqualTo(1.0, offset(0.0)); - } - - @Test - void invalidTermination() { - assertThatIllegalArgumentException().isThrownBy(() -> new UnimprovedMoveCountTermination(-1L)); - var termination = new UnimprovedMoveCountTermination(4); - assertThatThrownBy(() -> termination.isSolverTerminated(null)) - .isInstanceOf(UnsupportedOperationException.class); - assertThatThrownBy(() -> termination.calculateSolverTimeGradient(null)) - .isInstanceOf(UnsupportedOperationException.class); - } -} From eb40154aafe8cf8890a2287ae90df94381a2ca3e Mon Sep 17 00:00:00 2001 From: Fred Date: Mon, 9 Sep 2024 17:54:23 -0300 Subject: [PATCH 12/26] chore: address sonarcloud issues --- ...EvaluationSpeedSubSingleStatisticTest.java | 36 +++++++++++++++++++ ...alculationSpeedSubSingleStatisticTest.java | 36 +++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/moveevaluationspeed/MoveEvaluationSpeedSubSingleStatisticTest.java b/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/moveevaluationspeed/MoveEvaluationSpeedSubSingleStatisticTest.java index f737ac6655..3c61bfdb38 100644 --- a/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/moveevaluationspeed/MoveEvaluationSpeedSubSingleStatisticTest.java +++ b/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/moveevaluationspeed/MoveEvaluationSpeedSubSingleStatisticTest.java @@ -1,16 +1,29 @@ package ai.timefold.solver.benchmark.impl.statistic.moveevaluationspeed; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + import java.util.Collections; import java.util.List; import java.util.function.Function; +import ai.timefold.solver.benchmark.config.statistic.ProblemStatisticType; +import ai.timefold.solver.benchmark.impl.report.BenchmarkReport; +import ai.timefold.solver.benchmark.impl.result.ProblemBenchmarkResult; +import ai.timefold.solver.benchmark.impl.result.SingleBenchmarkResult; +import ai.timefold.solver.benchmark.impl.result.SolverBenchmarkResult; import ai.timefold.solver.benchmark.impl.result.SubSingleBenchmarkResult; import ai.timefold.solver.benchmark.impl.statistic.AbstractSubSingleStatisticTest; +import ai.timefold.solver.benchmark.impl.statistic.SubSingleStatistic; import ai.timefold.solver.benchmark.impl.statistic.common.LongStatisticPoint; +import ai.timefold.solver.benchmark.impl.statistic.scorecalculationspeed.ScoreCalculationSpeedProblemStatistic; import ai.timefold.solver.core.impl.testdata.domain.TestdataSolution; import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.Test; public final class MoveEvaluationSpeedSubSingleStatisticTest extends AbstractSubSingleStatisticTest> { @@ -35,4 +48,27 @@ protected void runTest(SoftAssertions assertions, List outpu .matches(s -> s.getTimeMillisSpent() == Long.MAX_VALUE, "Millis do not match."); } + @Test + void generateCharts() { + var problemBenchmarkResult = mock(ProblemBenchmarkResult.class); + var benchmarkReport = mock(BenchmarkReport.class); + var singleBenchmarkResult = mock(SingleBenchmarkResult.class); + var solverBenchmarkResult = mock(SolverBenchmarkResult.class); + var singleStatistic = mock(SubSingleStatistic.class); + doReturn("Problem_0").when(problemBenchmarkResult).getName(); + doReturn(List.of(singleBenchmarkResult)).when(problemBenchmarkResult).getSingleBenchmarkResultList(); + doReturn(solverBenchmarkResult).when(singleBenchmarkResult).getSolverBenchmarkResult(); + doReturn("label").when(solverBenchmarkResult).getNameWithFavoriteSuffix(); + doReturn(true).when(singleBenchmarkResult).hasAllSuccess(); + doReturn(singleStatistic).when(singleBenchmarkResult).getSubSingleStatistic(any(ProblemStatisticType.class)); + doReturn(List.of(new LongStatisticPoint(Long.MIN_VALUE, Long.MIN_VALUE), + new LongStatisticPoint(Long.MAX_VALUE, Long.MAX_VALUE))).when(singleStatistic).getPointList(); + var statistic = new MoveEvaluationSpeedProblemStatisticTime(problemBenchmarkResult); + statistic.createChartList(benchmarkReport); + assertThat(statistic.getChartList()).hasSize(1); + var lineChart = statistic.getChartList().get(0); + assertThat(lineChart.title()).isEqualTo("Problem_0 move evaluation speed statistic"); + assertThat(lineChart.xLabel()).isEqualTo("Time spent"); + assertThat(lineChart.yLabel()).isEqualTo("Move evaluation speed per second"); + } } diff --git a/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/scorecalculationspeed/ScoreCalculationSpeedSubSingleStatisticTest.java b/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/scorecalculationspeed/ScoreCalculationSpeedSubSingleStatisticTest.java index 4a1d11c2e2..b0a4c0b1a1 100644 --- a/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/scorecalculationspeed/ScoreCalculationSpeedSubSingleStatisticTest.java +++ b/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/scorecalculationspeed/ScoreCalculationSpeedSubSingleStatisticTest.java @@ -1,15 +1,28 @@ package ai.timefold.solver.benchmark.impl.statistic.scorecalculationspeed; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + import java.util.Collections; import java.util.List; import java.util.function.Function; +import ai.timefold.solver.benchmark.config.statistic.ProblemStatisticType; +import ai.timefold.solver.benchmark.impl.report.BenchmarkReport; +import ai.timefold.solver.benchmark.impl.report.LineChart; +import ai.timefold.solver.benchmark.impl.result.ProblemBenchmarkResult; +import ai.timefold.solver.benchmark.impl.result.SingleBenchmarkResult; +import ai.timefold.solver.benchmark.impl.result.SolverBenchmarkResult; import ai.timefold.solver.benchmark.impl.result.SubSingleBenchmarkResult; import ai.timefold.solver.benchmark.impl.statistic.AbstractSubSingleStatisticTest; +import ai.timefold.solver.benchmark.impl.statistic.SubSingleStatistic; import ai.timefold.solver.benchmark.impl.statistic.common.LongStatisticPoint; import ai.timefold.solver.core.impl.testdata.domain.TestdataSolution; import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.Test; public final class ScoreCalculationSpeedSubSingleStatisticTest extends @@ -35,4 +48,27 @@ protected void runTest(SoftAssertions assertions, List outpu .matches(s -> s.getTimeMillisSpent() == Long.MAX_VALUE, "Millis do not match."); } + @Test + void generateCharts() { + var problemBenchmarkResult = mock(ProblemBenchmarkResult.class); + var benchmarkReport = mock(BenchmarkReport.class); + var singleBenchmarkResult = mock(SingleBenchmarkResult.class); + var solverBenchmarkResult = mock(SolverBenchmarkResult.class); + var singleStatistic = mock(SubSingleStatistic.class); + doReturn("Problem_0").when(problemBenchmarkResult).getName(); + doReturn(List.of(singleBenchmarkResult)).when(problemBenchmarkResult).getSingleBenchmarkResultList(); + doReturn(solverBenchmarkResult).when(singleBenchmarkResult).getSolverBenchmarkResult(); + doReturn("label").when(solverBenchmarkResult).getNameWithFavoriteSuffix(); + doReturn(true).when(singleBenchmarkResult).hasAllSuccess(); + doReturn(singleStatistic).when(singleBenchmarkResult).getSubSingleStatistic(any(ProblemStatisticType.class)); + doReturn(List.of(new LongStatisticPoint(Long.MIN_VALUE, Long.MIN_VALUE), + new LongStatisticPoint(Long.MAX_VALUE, Long.MAX_VALUE))).when(singleStatistic).getPointList(); + var statistic = new ScoreCalculationSpeedProblemStatistic(problemBenchmarkResult); + statistic.createChartList(benchmarkReport); + assertThat(statistic.getChartList()).hasSize(1); + var lineChart = statistic.getChartList().get(0); + assertThat(lineChart.title()).isEqualTo("Problem_0 score calculation speed statistic"); + assertThat(lineChart.xLabel()).isEqualTo("Time spent"); + assertThat(lineChart.yLabel()).isEqualTo("Score calculation speed per second"); + } } From 368a9b4ecc044f16d1391d3d7296ea4687e7dc7c Mon Sep 17 00:00:00 2001 From: Fred Date: Mon, 9 Sep 2024 19:58:49 -0300 Subject: [PATCH 13/26] fix: formatting --- .../MoveEvaluationSpeedSubSingleStatisticTest.java | 1 - .../ScoreCalculationSpeedSubSingleStatisticTest.java | 1 - 2 files changed, 2 deletions(-) diff --git a/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/moveevaluationspeed/MoveEvaluationSpeedSubSingleStatisticTest.java b/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/moveevaluationspeed/MoveEvaluationSpeedSubSingleStatisticTest.java index 3c61bfdb38..5fe2124157 100644 --- a/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/moveevaluationspeed/MoveEvaluationSpeedSubSingleStatisticTest.java +++ b/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/moveevaluationspeed/MoveEvaluationSpeedSubSingleStatisticTest.java @@ -19,7 +19,6 @@ import ai.timefold.solver.benchmark.impl.statistic.AbstractSubSingleStatisticTest; import ai.timefold.solver.benchmark.impl.statistic.SubSingleStatistic; import ai.timefold.solver.benchmark.impl.statistic.common.LongStatisticPoint; -import ai.timefold.solver.benchmark.impl.statistic.scorecalculationspeed.ScoreCalculationSpeedProblemStatistic; import ai.timefold.solver.core.impl.testdata.domain.TestdataSolution; import org.assertj.core.api.SoftAssertions; diff --git a/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/scorecalculationspeed/ScoreCalculationSpeedSubSingleStatisticTest.java b/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/scorecalculationspeed/ScoreCalculationSpeedSubSingleStatisticTest.java index b0a4c0b1a1..6ffac006e8 100644 --- a/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/scorecalculationspeed/ScoreCalculationSpeedSubSingleStatisticTest.java +++ b/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/scorecalculationspeed/ScoreCalculationSpeedSubSingleStatisticTest.java @@ -11,7 +11,6 @@ import ai.timefold.solver.benchmark.config.statistic.ProblemStatisticType; import ai.timefold.solver.benchmark.impl.report.BenchmarkReport; -import ai.timefold.solver.benchmark.impl.report.LineChart; import ai.timefold.solver.benchmark.impl.result.ProblemBenchmarkResult; import ai.timefold.solver.benchmark.impl.result.SingleBenchmarkResult; import ai.timefold.solver.benchmark.impl.result.SolverBenchmarkResult; From 279df7a63fb0dc486f91975030b4679b82a3b2e3 Mon Sep 17 00:00:00 2001 From: Fred Date: Tue, 10 Sep 2024 10:13:37 -0300 Subject: [PATCH 14/26] docs: new metric doc --- .../DefaultConstructionHeuristicPhase.java | 5 +- .../localsearch/DefaultLocalSearchPhase.java | 3 +- .../core/impl/solver/DefaultSolver.java | 3 +- .../moveEvaluationSpeedStatistic.png | Bin 0 -> 81839 bytes .../constraints-and-score/performance.adoc | 12 +++++ .../benchmarking-and-tweaking.adoc | 49 ++++++++++++++++++ 6 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 docs/src/modules/ROOT/images/using-timefold-solver/benchmarking-and-tweaking/moveEvaluationSpeedStatistic.png diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java index d29dd1d446..e2103045f4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java @@ -146,14 +146,13 @@ public void phaseEnded(ConstructionHeuristicPhaseScope phaseScope) { decider.phaseEnded(phaseScope); phaseScope.endingNow(); if (decider.isLoggingEnabled() && logger.isInfoEnabled()) { - logger.info("{}Construction Heuristic phase ({}) ended: time spent ({}), best score ({})," - + " score calculation speed ({}/sec), move evaluation speed ({}/sec), step total ({}).", + logger.info( + "{}Construction Heuristic phase ({}) ended: time spent ({}), best score ({}), score calculation speed ({}/sec), step total ({}).", logIndentation, phaseIndex, phaseScope.calculateSolverTimeMillisSpentUpToNow(), phaseScope.getBestScore(), phaseScope.getPhaseScoreCalculationSpeed(), - phaseScope.getPhaseMoveEvaluationSpeed(), phaseScope.getNextStepIndex()); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java index 63bd760b2b..91b08c1c4e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java @@ -199,13 +199,12 @@ public void phaseEnded(LocalSearchPhaseScope phaseScope) { decider.phaseEnded(phaseScope); phaseScope.endingNow(); logger.info("{}Local Search phase ({}) ended: time spent ({}), best score ({})," - + " score calculation speed ({}/sec), move evaluation speed ({}/sec), step total ({}).", + + " score calculation speed ({}/sec), step total ({}).", logIndentation, phaseIndex, phaseScope.calculateSolverTimeMillisSpentUpToNow(), phaseScope.getBestScore(), phaseScope.getPhaseScoreCalculationSpeed(), - phaseScope.getPhaseMoveEvaluationSpeed(), phaseScope.getNextStepIndex()); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java index f99905026e..2bf7a2352b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java @@ -314,11 +314,10 @@ public void solvingEnded(SolverScope solverScope) { public void outerSolvingEnded(SolverScope solverScope) { logger.info("Solving ended: time spent ({}), best score ({}), score calculation speed ({}/sec), " - + "move evaluation speed ({}/sec), phase total ({}), environment mode ({}), move thread count ({}).", + + "phase total ({}), environment mode ({}), move thread count ({}).", solverScope.getTimeMillisSpent(), solverScope.getBestScore(), solverScope.getScoreCalculationSpeed(), - solverScope.getMoveEvaluationSpeed(), phaseList.size(), environmentMode.name(), moveThreadCountDescription); diff --git a/docs/src/modules/ROOT/images/using-timefold-solver/benchmarking-and-tweaking/moveEvaluationSpeedStatistic.png b/docs/src/modules/ROOT/images/using-timefold-solver/benchmarking-and-tweaking/moveEvaluationSpeedStatistic.png new file mode 100644 index 0000000000000000000000000000000000000000..ed0cddb8e546911429baeaed5496ac7c8a254f39 GIT binary patch literal 81839 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 Y$iOZ{EUV)utsmoznzm|zvU$M&0#<|84*&oF literal 0 HcmV?d00001 diff --git a/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc b/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc index 2758f17c01..3a8429a601 100644 --- a/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc +++ b/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc @@ -33,6 +33,18 @@ and still compare it with the original's calculation speed. Comparing the best score with the original's best score is pointless: it's comparing apples and oranges. ==== +The solver usually calculates the score for each move it evaluates. +This means a direct relationship exists between the score calculation and the evaluated move, typically a `1:1` ratio. +However, some moves, +such as xref:optimization-algorithms/move-selector-reference.adoc#ruinRecreateMoveSelector[Ruin and Recreate], +involve processing the score multiple times within the same move. + +The use of moves like Ruin and Recreate may increase the number of score calculations, +which makes the relationship `1:1` invalid. +In such cases, +the metric xref:using-timefold-solver/benchmarking-and-tweaking.adoc#benchmarkReportMoveEvaluationSpeedSummary[__move evaluation speed per second__] makes more sense, +as it will count the number of moves individually. + [#incrementalScoreCalculationPerformance] == Incremental score calculation (with deltas) diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/benchmarking-and-tweaking.adoc b/docs/src/modules/ROOT/pages/using-timefold-solver/benchmarking-and-tweaking.adoc index 879eea03b5..84aa00192d 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/benchmarking-and-tweaking.adoc +++ b/docs/src/modules/ROOT/pages/using-timefold-solver/benchmarking-and-tweaking.adoc @@ -596,6 +596,27 @@ Useful for comparing different score calculators and/or constraint implementatio Also useful to measure the scalability cost of an extra constraint. +[#benchmarkReportMoveEvaluationSpeedSummary] +=== Move evaluation speed summary (graph And table) + +Shows the move evaluation speed: a count per move, per second and per problem scale for each solver configuration. + +Useful for comparing different solver algorithms, +score calculators and/or constraint implementations +(presuming that the solver configurations do not differ otherwise, including the move selector configuration). +Also useful to measure the scalability cost of an extra constraint. + +[IMPORTANT] +==== +When improving your move evaluation, +it's important to note that comparing a configuration +that uses xref:optimization-algorithms/move-selector-reference.adoc#ruinRecreateMoveSelector[Ruin and Recreate] moves with one that doesn't wouldn't be fair. +This is because the configuration using Ruin and Recreate will likely execute fewer moves than one that doesn't. +On the other hand, the Ruin and Recreate moves will likely calculate the score more times, improving the __score calculation speed__. +The recreate step runs a construction heuristic, +which uses greedy logic to find a better location to assign each one of the entities removed with the ruin step. +==== + [#benchmarkReportTimeSpentSummary] === Time spent summary (graph And table) @@ -753,6 +774,34 @@ After those few seconds of initialization, the calculation speed is relatively s ==== +[#benchmarkReportMoveEvaluationSpeedOverTimeStatistic] +=== Move evaluation speed over time statistic (graph and CSV) + +To see how fast the moves are evaluated, add: + +[source,xml,options="nowrap"] +---- + + ... + MOVE_EVALUATION_SPEED + +---- + +.Move Evaluation Speed Statistic +image::using-timefold-solver/benchmarking-and-tweaking/moveEvaluationSpeedStatistic.png[align="center"] + + +[NOTE] +==== +The initial high calculation speed is typical during solution initialization: +it's far easier to calculate the score of a solution if only a handful planning entities have been initialized, +than when all the planning entities are initialized. + +After those few seconds of initialization, the evaluation speed is relatively stable, +apart from an occasional stop-the-world garbage collector disruption. +==== + + [#benchmarkReportBestSolutionMutationOverTimeStatistic] === Best solution mutation over time statistic (graph and CSV) From cea21f9dfeecfa237cd50770b309e950e0b9ed94 Mon Sep 17 00:00:00 2001 From: Fred Date: Tue, 10 Sep 2024 10:19:17 -0300 Subject: [PATCH 15/26] chore: address sonarcloud issues --- .../MoveEvaluationSpeedSubSingleStatisticTest.java | 2 +- .../ScoreCalculationSpeedSubSingleStatisticTest.java | 2 +- .../exhaustivesearch/scope/ExhaustiveSearchPhaseScope.java | 5 ----- .../selector/move/generic/RuinRecreateMoveSelectorTest.java | 5 +++-- .../move/generic/list/ListRuinRecreateMoveSelectorTest.java | 5 +++-- 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/moveevaluationspeed/MoveEvaluationSpeedSubSingleStatisticTest.java b/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/moveevaluationspeed/MoveEvaluationSpeedSubSingleStatisticTest.java index 5fe2124157..ce37d3af34 100644 --- a/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/moveevaluationspeed/MoveEvaluationSpeedSubSingleStatisticTest.java +++ b/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/moveevaluationspeed/MoveEvaluationSpeedSubSingleStatisticTest.java @@ -24,7 +24,7 @@ import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.Test; -public final class MoveEvaluationSpeedSubSingleStatisticTest +final class MoveEvaluationSpeedSubSingleStatisticTest extends AbstractSubSingleStatisticTest> { @Override diff --git a/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/scorecalculationspeed/ScoreCalculationSpeedSubSingleStatisticTest.java b/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/scorecalculationspeed/ScoreCalculationSpeedSubSingleStatisticTest.java index 6ffac006e8..7cab48d473 100644 --- a/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/scorecalculationspeed/ScoreCalculationSpeedSubSingleStatisticTest.java +++ b/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/scorecalculationspeed/ScoreCalculationSpeedSubSingleStatisticTest.java @@ -23,7 +23,7 @@ import org.assertj.core.api.SoftAssertions; import org.junit.jupiter.api.Test; -public final class ScoreCalculationSpeedSubSingleStatisticTest +final class ScoreCalculationSpeedSubSingleStatisticTest extends AbstractSubSingleStatisticTest> { 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 298fe5830c..f57c97e6f6 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 @@ -59,11 +59,6 @@ public void setLastCompletedStepScope(ExhaustiveSearchStepScope lastC this.lastCompletedStepScope = lastCompletedStepScope; } - @Override - public > Score_ calculateScore() { - return super.calculateScore(); - } - // ************************************************************************ // Calculated methods // ************************************************************************ diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateMoveSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateMoveSelectorTest.java index 0a37c3e7b7..c3200a77fa 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateMoveSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateMoveSelectorTest.java @@ -1,6 +1,7 @@ package ai.timefold.solver.core.impl.heuristic.selector.move.generic; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import java.util.List; @@ -44,7 +45,7 @@ void testRuining() { .withStepCountLimit(100)))); var problem = TestdataSolution.generateSolution(5, 30); var solver = SolverFactory.create(solverConfig).buildSolver(); - solver.solve(problem); + assertDoesNotThrow(() -> solver.solve(problem)); } @Test @@ -93,7 +94,7 @@ void testRuiningAllowsUnassigned() { .withStepCountLimit(100)))); var problem = TestdataAllowsUnassignedSolution.generateSolution(5, 30); var solver = SolverFactory.create(solverConfig).buildSolver(); - solver.solve(problem); + assertDoesNotThrow(() -> solver.solve(problem)); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListRuinRecreateMoveSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListRuinRecreateMoveSelectorTest.java index 61039db206..92f0d339bd 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListRuinRecreateMoveSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListRuinRecreateMoveSelectorTest.java @@ -1,6 +1,7 @@ package ai.timefold.solver.core.impl.heuristic.selector.move.generic.list; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import java.util.List; import java.util.Objects; @@ -69,7 +70,7 @@ void testRuining() { .withStepCountLimit(100)))); var problem = TestdataListSolution.generateUninitializedSolution(10, 3); var solver = SolverFactory.create(solverConfig).buildSolver(); - solver.solve(problem); + assertDoesNotThrow(() -> solver.solve(problem)); } @Test @@ -152,7 +153,7 @@ void testRuiningAllowsUnassignedValues() { .mapToObj(id -> new TestdataAllowsUnassignedValuesListValue("v" + id)) .toList()); var solver = SolverFactory.create(solverConfig).buildSolver(); - solver.solve(problem); + assertDoesNotThrow(() -> solver.solve(problem)); } } \ No newline at end of file From caf9dedc2daea9db01a7df7c1a2cf485cf794ced Mon Sep 17 00:00:00 2001 From: Fred Date: Mon, 16 Sep 2024 14:26:23 -0300 Subject: [PATCH 16/26] chore: address PR comments --- .../ai/timefold/solver/core/api/solver/SolverManagerTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/src/test/java/ai/timefold/solver/core/api/solver/SolverManagerTest.java b/core/src/test/java/ai/timefold/solver/core/api/solver/SolverManagerTest.java index f44bf5d3d5..5e569dda1e 100644 --- a/core/src/test/java/ai/timefold/solver/core/api/solver/SolverManagerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/api/solver/SolverManagerTest.java @@ -537,6 +537,8 @@ void testScoreCalculationCountForFinishedJob() throws ExecutionException, Interr .run(); solverJob.getFinalBestSolution(); + // The score is calculated during the solving starting phase without applying any moves. + // This explains why the count has one more unit. assertThat(solverJob.getScoreCalculationCount()).isEqualTo(5L); assertThat(solverJob.getMoveEvaluationCount()).isEqualTo(4L); From ca2674160e2228630708e9bccfb11f0b3ac3aff6 Mon Sep 17 00:00:00 2001 From: Fred Date: Thu, 19 Sep 2024 13:47:43 -0300 Subject: [PATCH 17/26] chore: log move speed by default --- .../DefaultConstructionHeuristicPhase.java | 4 ++-- .../impl/exhaustivesearch/DefaultExhaustiveSearchPhase.java | 4 ++-- .../solver/core/impl/localsearch/DefaultLocalSearchPhase.java | 4 ++-- .../solver/core/impl/phase/custom/DefaultCustomPhase.java | 4 ++-- .../ai/timefold/solver/core/impl/solver/DefaultSolver.java | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java index e2103045f4..e25ff1bd66 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java @@ -147,12 +147,12 @@ public void phaseEnded(ConstructionHeuristicPhaseScope phaseScope) { phaseScope.endingNow(); if (decider.isLoggingEnabled() && logger.isInfoEnabled()) { logger.info( - "{}Construction Heuristic phase ({}) ended: time spent ({}), best score ({}), score calculation speed ({}/sec), step total ({}).", + "{}Construction Heuristic phase ({}) ended: time spent ({}), best score ({}), move evaluation speed ({}/sec), step total ({}).", logIndentation, phaseIndex, phaseScope.calculateSolverTimeMillisSpentUpToNow(), phaseScope.getBestScore(), - phaseScope.getPhaseScoreCalculationSpeed(), + phaseScope.getPhaseMoveEvaluationSpeed(), phaseScope.getNextStepIndex()); } } 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 0248279b0d..fdea3c0eff 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 @@ -209,12 +209,12 @@ public void phaseEnded(ExhaustiveSearchPhaseScope phaseScope) { decider.phaseEnded(phaseScope); phaseScope.endingNow(); logger.info("{}Exhaustive Search phase ({}) ended: time spent ({}), best score ({})," - + " score calculation speed ({}/sec), step total ({}).", + + " move evaluation speed ({}/sec), step total ({}).", logIndentation, phaseIndex, phaseScope.calculateSolverTimeMillisSpentUpToNow(), phaseScope.getBestScore(), - phaseScope.getPhaseScoreCalculationSpeed(), + phaseScope.getPhaseMoveEvaluationSpeed(), phaseScope.getNextStepIndex()); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java index 91b08c1c4e..d18f137f26 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java @@ -199,12 +199,12 @@ public void phaseEnded(LocalSearchPhaseScope phaseScope) { decider.phaseEnded(phaseScope); phaseScope.endingNow(); logger.info("{}Local Search phase ({}) ended: time spent ({}), best score ({})," - + " score calculation speed ({}/sec), step total ({}).", + + " move evaluation speed ({}/sec), step total ({}).", logIndentation, phaseIndex, phaseScope.calculateSolverTimeMillisSpentUpToNow(), phaseScope.getBestScore(), - phaseScope.getPhaseScoreCalculationSpeed(), + phaseScope.getPhaseMoveEvaluationSpeed(), phaseScope.getNextStepIndex()); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultCustomPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultCustomPhase.java index 488503fb2c..5509242ea5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultCustomPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultCustomPhase.java @@ -76,12 +76,12 @@ public void phaseEnded(CustomPhaseScope phaseScope) { super.phaseEnded(phaseScope); phaseScope.endingNow(); logger.info("{}Custom phase ({}) ended: time spent ({}), best score ({})," - + " score calculation speed ({}/sec), step total ({}).", + + " move evaluation speed ({}/sec), step total ({}).", logIndentation, phaseIndex, phaseScope.calculateSolverTimeMillisSpentUpToNow(), phaseScope.getBestScore(), - phaseScope.getPhaseScoreCalculationSpeed(), + phaseScope.getPhaseMoveEvaluationSpeed(), phaseScope.getNextStepIndex()); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java index 2bf7a2352b..bb82457e07 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolver.java @@ -313,11 +313,11 @@ public void solvingEnded(SolverScope solverScope) { } public void outerSolvingEnded(SolverScope solverScope) { - logger.info("Solving ended: time spent ({}), best score ({}), score calculation speed ({}/sec), " + logger.info("Solving ended: time spent ({}), best score ({}), move evaluation speed ({}/sec), " + "phase total ({}), environment mode ({}), move thread count ({}).", solverScope.getTimeMillisSpent(), solverScope.getBestScore(), - solverScope.getScoreCalculationSpeed(), + solverScope.getMoveEvaluationSpeed(), phaseList.size(), environmentMode.name(), moveThreadCountDescription); From db888d8a5f6a6ff7e290666500336b1e1ca70d4c Mon Sep 17 00:00:00 2001 From: Fred Date: Mon, 23 Sep 2024 10:37:31 -0300 Subject: [PATCH 18/26] feat: new metric count per move type --- .../statistic/ProblemStatisticType.java | 4 + .../impl/statistic/StatisticRegistry.java | 17 ++++ .../MoveCountPerTypeProblemStatistic.java | 48 +++++++++++ .../MoveCountPerTypeStatisticPoint.java | 28 +++++++ .../MoveCountPerTypeSubSingleStatistic.java | 50 ++++++++++++ benchmark/src/main/resources/benchmark.xsd | 6 ++ ...oveCountPerTypeSubSingleStatisticTest.java | 72 +++++++++++++++++ .../solver/monitoring/SolverMetric.java | 2 + .../DefaultConstructionHeuristicForager.java | 2 +- .../decider/ExhaustiveSearchDecider.java | 2 +- .../forager/AcceptedLocalSearchForager.java | 2 +- .../impl/phase/scope/AbstractPhaseScope.java | 2 - .../impl/phase/scope/AbstractStepScope.java | 7 +- .../core/impl/solver/scope/SolverScope.java | 26 ++++++ .../statistic/MoveCountPerTypeStatistic.java | 69 ++++++++++++++++ core/src/main/resources/solver.xsd | 2 + ...DefaultConstructionHeuristicPhaseTest.java | 69 ++++++++++++++++ .../DefaultExhaustiveSearchPhaseTest.java | 33 ++++++++ .../DefaultLocalSearchPhaseTest.java | 79 +++++++++++++++++++ 19 files changed, 514 insertions(+), 6 deletions(-) create mode 100644 benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecountpertype/MoveCountPerTypeProblemStatistic.java create mode 100644 benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecountpertype/MoveCountPerTypeStatisticPoint.java create mode 100644 benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecountpertype/MoveCountPerTypeSubSingleStatistic.java create mode 100644 benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/movecountpertype/MoveCountPerTypeSubSingleStatisticTest.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/statistic/MoveCountPerTypeStatistic.java diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/config/statistic/ProblemStatisticType.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/config/statistic/ProblemStatisticType.java index 25bfddeeb4..e22db68de1 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/config/statistic/ProblemStatisticType.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/config/statistic/ProblemStatisticType.java @@ -11,6 +11,7 @@ import ai.timefold.solver.benchmark.impl.statistic.bestsolutionmutation.BestSolutionMutationProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.memoryuse.MemoryUseProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.movecountperstep.MoveCountPerStepProblemStatistic; +import ai.timefold.solver.benchmark.impl.statistic.movecountpertype.MoveCountPerTypeProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.moveevaluationspeed.MoveEvaluationSpeedProblemStatisticTime; import ai.timefold.solver.benchmark.impl.statistic.scorecalculationspeed.ScoreCalculationSpeedProblemStatistic; import ai.timefold.solver.benchmark.impl.statistic.stepscore.StepScoreProblemStatistic; @@ -23,6 +24,7 @@ public enum ProblemStatisticType implements StatisticType { MOVE_EVALUATION_SPEED, BEST_SOLUTION_MUTATION, MOVE_COUNT_PER_STEP, + MOVE_COUNT_PER_TYPE, MEMORY_USE; public ProblemStatistic buildProblemStatistic(ProblemBenchmarkResult problemBenchmarkResult) { @@ -39,6 +41,8 @@ public ProblemStatistic buildProblemStatistic(ProblemBenchmarkResult problemBenc return new BestSolutionMutationProblemStatistic(problemBenchmarkResult); case MOVE_COUNT_PER_STEP: return new MoveCountPerStepProblemStatistic(problemBenchmarkResult); + case MOVE_COUNT_PER_TYPE: + return new MoveCountPerTypeProblemStatistic(problemBenchmarkResult); case MEMORY_USE: return new MemoryUseProblemStatistic(problemBenchmarkResult); default: diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/StatisticRegistry.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/StatisticRegistry.java index 4172e64824..b410196b80 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/StatisticRegistry.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/StatisticRegistry.java @@ -9,6 +9,7 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.ObjLongConsumer; import java.util.stream.Collectors; import ai.timefold.solver.core.api.score.Score; @@ -32,6 +33,7 @@ public class StatisticRegistry extends SimpleMeterRegistry private static final String CONSTRAINT_PACKAGE_TAG = "constraint.package"; private static final String CONSTRAINT_NAME_TAG = "constraint.name"; + List>> solverMeterListenerList = new ArrayList<>(); List>> stepMeterListenerList = new ArrayList<>(); List>> bestSolutionMeterListenerList = new ArrayList<>(); AbstractStepScope bestSolutionStepScope = null; @@ -77,6 +79,10 @@ public void addListener(SolverMetric metric, BiConsumer> listener) { + solverMeterListenerList.add(listener); + } + public Set getMeterIds(SolverMetric metric, Tags runId) { return Search.in(this).name(name -> name.startsWith(metric.getMeterId())).tags(runId) .meters().stream().map(Meter::getId) @@ -131,6 +137,16 @@ public void getGaugeValue(String meterId, Tags runId, Consumer gaugeCons } } + public void extractMoveCountPerType(SolverScope solverScope, ObjLongConsumer gaugeConsumer) { + solverScope.getMoveCountTypes().forEach(type -> { + var gauge = this.find(SolverMetric.MOVE_COUNT_PER_TYPE.getMeterId() + "." + type) + .tags(solverScope.getMonitoringTags()).gauge(); + if (gauge != null) { + gaugeConsumer.accept(type, (long) gauge.value()); + } + }); + } + @Override protected TimeUnit getBaseTimeUnit() { return TimeUnit.MILLISECONDS; @@ -181,5 +197,6 @@ public void solvingEnded(SolverScope solverScope) { .forEach(listener -> listener.accept(bestSolutionChangedTimestamp, bestSolutionStepScope)); lastStepImprovedSolution = false; } + solverMeterListenerList.forEach(listener -> listener.accept(solverScope)); } } diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecountpertype/MoveCountPerTypeProblemStatistic.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecountpertype/MoveCountPerTypeProblemStatistic.java new file mode 100644 index 0000000000..1b1182685b --- /dev/null +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecountpertype/MoveCountPerTypeProblemStatistic.java @@ -0,0 +1,48 @@ +package ai.timefold.solver.benchmark.impl.statistic.movecountpertype; + +import static java.util.Collections.singletonList; + +import java.util.List; + +import ai.timefold.solver.benchmark.config.statistic.ProblemStatisticType; +import ai.timefold.solver.benchmark.impl.report.BarChart; +import ai.timefold.solver.benchmark.impl.report.BenchmarkReport; +import ai.timefold.solver.benchmark.impl.result.ProblemBenchmarkResult; +import ai.timefold.solver.benchmark.impl.result.SubSingleBenchmarkResult; +import ai.timefold.solver.benchmark.impl.statistic.ProblemStatistic; +import ai.timefold.solver.benchmark.impl.statistic.SubSingleStatistic; + +public class MoveCountPerTypeProblemStatistic extends ProblemStatistic> { + private MoveCountPerTypeProblemStatistic() { + // Required by JAXB + } + + @SuppressWarnings("rawtypes") + public MoveCountPerTypeProblemStatistic(ProblemBenchmarkResult problemBenchmarkResult) { + super(problemBenchmarkResult, ProblemStatisticType.MOVE_COUNT_PER_TYPE); + } + + @SuppressWarnings({ "rawtypes" }) + @Override + public SubSingleStatistic createSubSingleStatistic(SubSingleBenchmarkResult subSingleBenchmarkResult) { + return new MoveCountPerTypeSubSingleStatistic(subSingleBenchmarkResult); + } + + @Override + protected List> generateCharts(BenchmarkReport benchmarkReport) { + var builder = new BarChart.Builder(); + for (var singleBenchmarkResult : problemBenchmarkResult.getSingleBenchmarkResultList()) { + // No direct ascending lines between 2 points, but a stepping line instead + if (singleBenchmarkResult.hasAllSuccess()) { + var solverLabel = singleBenchmarkResult.getSolverBenchmarkResult().getNameWithFavoriteSuffix(); + var subSingleStatistic = singleBenchmarkResult.getSubSingleStatistic(problemStatisticType); + List points = subSingleStatistic.getPointList(); + for (var point : points) { + builder.add(solverLabel, point.getMoveType(), point.getCount()); + } + } + } + return singletonList(builder.build("moveCountPerTypeProblemStatisticChart", + problemBenchmarkResult.getName() + " move count per type statistic", "Type", "Count", false)); + } +} diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecountpertype/MoveCountPerTypeStatisticPoint.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecountpertype/MoveCountPerTypeStatisticPoint.java new file mode 100644 index 0000000000..95fecdff52 --- /dev/null +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecountpertype/MoveCountPerTypeStatisticPoint.java @@ -0,0 +1,28 @@ +package ai.timefold.solver.benchmark.impl.statistic.movecountpertype; + +import ai.timefold.solver.benchmark.impl.statistic.StatisticPoint; + +public class MoveCountPerTypeStatisticPoint extends StatisticPoint { + + private final String moveType; + private final long count; + + public MoveCountPerTypeStatisticPoint(String moveType, long count) { + this.moveType = moveType; + this.count = count; + } + + public String getMoveType() { + return moveType; + } + + public long getCount() { + return count; + } + + @Override + public String toCsvLine() { + return buildCsvLineWithStrings(0L, moveType, String.valueOf(count)); + } + +} diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecountpertype/MoveCountPerTypeSubSingleStatistic.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecountpertype/MoveCountPerTypeSubSingleStatistic.java new file mode 100644 index 0000000000..8d78078ddf --- /dev/null +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/statistic/movecountpertype/MoveCountPerTypeSubSingleStatistic.java @@ -0,0 +1,50 @@ +package ai.timefold.solver.benchmark.impl.statistic.movecountpertype; + +import java.util.List; + +import ai.timefold.solver.benchmark.config.statistic.ProblemStatisticType; +import ai.timefold.solver.benchmark.impl.result.SubSingleBenchmarkResult; +import ai.timefold.solver.benchmark.impl.statistic.ProblemBasedSubSingleStatistic; +import ai.timefold.solver.benchmark.impl.statistic.StatisticPoint; +import ai.timefold.solver.benchmark.impl.statistic.StatisticRegistry; +import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; + +import io.micrometer.core.instrument.Tags; + +public class MoveCountPerTypeSubSingleStatistic + extends ProblemBasedSubSingleStatistic { + + MoveCountPerTypeSubSingleStatistic() { + // For JAXB. + } + + public MoveCountPerTypeSubSingleStatistic(SubSingleBenchmarkResult subSingleBenchmarkResult) { + super(subSingleBenchmarkResult, ProblemStatisticType.MOVE_COUNT_PER_TYPE); + } + + // ************************************************************************ + // Lifecycle methods + // ************************************************************************ + + @Override + public void open(StatisticRegistry registry, Tags runTag) { + registry.addListener(solverScope -> registry.extractMoveCountPerType(solverScope, + (type, count) -> pointList.add(new MoveCountPerTypeStatisticPoint(type, count)))); + } + + // ************************************************************************ + // CSV methods + // ************************************************************************ + + @Override + protected String getCsvHeader() { + return StatisticPoint.buildCsvLine("_", "type", "count"); + } + + @Override + protected MoveCountPerTypeStatisticPoint createPointFromCsvLine(ScoreDefinition scoreDefinition, + List csvLine) { + return new MoveCountPerTypeStatisticPoint(csvLine.get(1), Long.parseLong(csvLine.get(2))); + } + +} diff --git a/benchmark/src/main/resources/benchmark.xsd b/benchmark/src/main/resources/benchmark.xsd index 8673f62765..1549f7cff7 100644 --- a/benchmark/src/main/resources/benchmark.xsd +++ b/benchmark/src/main/resources/benchmark.xsd @@ -232,6 +232,9 @@ + + + @@ -2648,6 +2651,9 @@ + + + diff --git a/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/movecountpertype/MoveCountPerTypeSubSingleStatisticTest.java b/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/movecountpertype/MoveCountPerTypeSubSingleStatisticTest.java new file mode 100644 index 0000000000..84019c48f0 --- /dev/null +++ b/benchmark/src/test/java/ai/timefold/solver/benchmark/impl/statistic/movecountpertype/MoveCountPerTypeSubSingleStatisticTest.java @@ -0,0 +1,72 @@ + +package ai.timefold.solver.benchmark.impl.statistic.movecountpertype; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +import java.util.List; +import java.util.function.Function; + +import ai.timefold.solver.benchmark.config.statistic.ProblemStatisticType; +import ai.timefold.solver.benchmark.impl.report.BenchmarkReport; +import ai.timefold.solver.benchmark.impl.result.ProblemBenchmarkResult; +import ai.timefold.solver.benchmark.impl.result.SingleBenchmarkResult; +import ai.timefold.solver.benchmark.impl.result.SolverBenchmarkResult; +import ai.timefold.solver.benchmark.impl.result.SubSingleBenchmarkResult; +import ai.timefold.solver.benchmark.impl.statistic.AbstractSubSingleStatisticTest; +import ai.timefold.solver.benchmark.impl.statistic.SubSingleStatistic; +import ai.timefold.solver.core.impl.testdata.domain.TestdataSolution; + +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.Test; + +final class MoveCountPerTypeSubSingleStatisticTest + extends + AbstractSubSingleStatisticTest> { + + @Override + protected Function> + getSubSingleStatisticConstructor() { + return MoveCountPerTypeSubSingleStatistic::new; + } + + @Override + protected List getInputPoints() { + return List.of(new MoveCountPerTypeStatisticPoint("Move Type 1", Long.MIN_VALUE), + new MoveCountPerTypeStatisticPoint("Move Type 2", Long.MAX_VALUE)); + } + + @Override + protected void runTest(SoftAssertions assertions, List outputPoints) { + assertions.assertThat(outputPoints) + .hasSize(2) + .anyMatch(m -> m.getCount() == Long.MIN_VALUE) + .anyMatch(m -> m.getCount() == Long.MAX_VALUE); + } + + @Test + void generateCharts() { + var problemBenchmarkResult = mock(ProblemBenchmarkResult.class); + var benchmarkReport = mock(BenchmarkReport.class); + var singleBenchmarkResult = mock(SingleBenchmarkResult.class); + var solverBenchmarkResult = mock(SolverBenchmarkResult.class); + var singleStatistic = mock(SubSingleStatistic.class); + doReturn("Problem_0").when(problemBenchmarkResult).getName(); + doReturn(List.of(singleBenchmarkResult)).when(problemBenchmarkResult).getSingleBenchmarkResultList(); + doReturn(solverBenchmarkResult).when(singleBenchmarkResult).getSolverBenchmarkResult(); + doReturn("label").when(solverBenchmarkResult).getNameWithFavoriteSuffix(); + doReturn(true).when(singleBenchmarkResult).hasAllSuccess(); + doReturn(singleStatistic).when(singleBenchmarkResult).getSubSingleStatistic(any(ProblemStatisticType.class)); + doReturn(List.of(new MoveCountPerTypeStatisticPoint("Move Type 1", Long.MIN_VALUE), + new MoveCountPerTypeStatisticPoint("Move Type 2", Long.MAX_VALUE))).when(singleStatistic).getPointList(); + var statistic = new MoveCountPerTypeProblemStatistic(problemBenchmarkResult); + statistic.createChartList(benchmarkReport); + assertThat(statistic.getChartList()).hasSize(1); + var barChart = statistic.getChartList().get(0); + assertThat(barChart.title()).isEqualTo("Problem_0 move count per type statistic"); + assertThat(barChart.xLabel()).isEqualTo("Type"); + assertThat(barChart.yLabel()).isEqualTo("Count"); + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/config/solver/monitoring/SolverMetric.java b/core/src/main/java/ai/timefold/solver/core/config/solver/monitoring/SolverMetric.java index 05421b0fb0..ca5cf231f0 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/solver/monitoring/SolverMetric.java +++ b/core/src/main/java/ai/timefold/solver/core/config/solver/monitoring/SolverMetric.java @@ -15,6 +15,7 @@ import ai.timefold.solver.core.impl.statistic.BestScoreStatistic; import ai.timefold.solver.core.impl.statistic.BestSolutionMutationCountStatistic; import ai.timefold.solver.core.impl.statistic.MemoryUseStatistic; +import ai.timefold.solver.core.impl.statistic.MoveCountPerTypeStatistic; import ai.timefold.solver.core.impl.statistic.PickedMoveBestScoreDiffStatistic; import ai.timefold.solver.core.impl.statistic.PickedMoveStepScoreDiffStatistic; import ai.timefold.solver.core.impl.statistic.SolverScopeStatistic; @@ -50,6 +51,7 @@ public enum SolverMetric { STEP_SCORE("timefold.solver.step.score", false), BEST_SOLUTION_MUTATION("timefold.solver.best.solution.mutation", new BestSolutionMutationCountStatistic<>(), true), MOVE_COUNT_PER_STEP("timefold.solver.step.move.count", false), + MOVE_COUNT_PER_TYPE("timefold.solver.move.type.count", new MoveCountPerTypeStatistic<>(), false), MEMORY_USE("jvm.memory.used", new MemoryUseStatistic<>(), false), CONSTRAINT_MATCH_TOTAL_BEST_SCORE("timefold.solver.constraint.match.best.score", true, true), CONSTRAINT_MATCH_TOTAL_STEP_SCORE("timefold.solver.constraint.match.step.score", false, true), diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/decider/forager/DefaultConstructionHeuristicForager.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/decider/forager/DefaultConstructionHeuristicForager.java index 2c97b89020..a669527105 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/decider/forager/DefaultConstructionHeuristicForager.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/decider/forager/DefaultConstructionHeuristicForager.java @@ -39,7 +39,7 @@ public void stepEnded(ConstructionHeuristicStepScope stepScope) { @Override public void addMove(ConstructionHeuristicMoveScope moveScope) { selectedMoveCount++; - moveScope.getStepScope().incrementMoveEvaluationCount(); + moveScope.getStepScope().incrementMoveEvaluationCount(moveScope.getMove()); checkPickEarly(moveScope); if (maxScoreMoveScope == null || moveScope.getScore().compareTo(maxScoreMoveScope.getScore()) > 0) { maxScoreMoveScope = moveScope; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/ExhaustiveSearchDecider.java b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/ExhaustiveSearchDecider.java index f0ad5ea6f3..1f8052635e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/ExhaustiveSearchDecider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/ExhaustiveSearchDecider.java @@ -114,7 +114,7 @@ public void expandNode(ExhaustiveSearchStepScope stepScope) { // If the original value is null and the variable allows unassigned values, // the move to null must be done too. doMove(stepScope, moveNode); - stepScope.incrementMoveEvaluationCount(); + stepScope.incrementMoveEvaluationCount(move); // TODO in the lowest level (and only in that level) QuitEarly can be useful // No QuitEarly because lower layers might be promising stepScope.getPhaseScope().getSolverScope().checkYielding(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/forager/AcceptedLocalSearchForager.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/forager/AcceptedLocalSearchForager.java index 3edfa93d1d..cdc357d934 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/forager/AcceptedLocalSearchForager.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/forager/AcceptedLocalSearchForager.java @@ -74,7 +74,7 @@ public boolean supportsNeverEndingMoveSelector() { @Override public void addMove(LocalSearchMoveScope moveScope) { selectedMoveCount++; - moveScope.getStepScope().incrementMoveEvaluationCount(); + moveScope.getStepScope().incrementMoveEvaluationCount(moveScope.getMove()); if (moveScope.getAccepted()) { acceptedMoveCount++; checkPickEarly(moveScope); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java index 07800f0f60..e1e316b752 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java @@ -34,8 +34,6 @@ public abstract class AbstractPhaseScope { protected int bestSolutionStepIndex; - protected long bestSolutionMoveEvaluationCount = 0L; - protected boolean enableCollectMetrics = true; /** diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractStepScope.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractStepScope.java index cbdf45b92b..5a5220e05a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractStepScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractStepScope.java @@ -4,6 +4,8 @@ import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; +import ai.timefold.solver.core.impl.heuristic.move.Move; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; /** @@ -44,9 +46,12 @@ public void setBestScoreImproved(Boolean bestScoreImproved) { this.bestScoreImproved = bestScoreImproved; } - public void incrementMoveEvaluationCount() { + public void incrementMoveEvaluationCount(Move move) { if (isPhaseEnableCollectMetrics()) { getPhaseScope().getSolverScope().addMoveEvaluationCount(1L); + if (getPhaseScope().getSolverScope().isMetricEnabled(SolverMetric.MOVE_COUNT_PER_TYPE)) { + getPhaseScope().getSolverScope().incrementMoveEvaluationCountPerType(move); + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java index fb47758296..d9aa7e2f6e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.solver.scope; +import static java.util.stream.Collectors.toMap; + import java.util.EnumSet; import java.util.List; import java.util.Map; @@ -16,6 +18,7 @@ import ai.timefold.solver.core.api.solver.Solver; import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.heuristic.move.Move; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; @@ -64,6 +67,11 @@ public class SolverScope { */ private final Map>> stepScoreMap = new ConcurrentHashMap<>(); + /** + * Used for tracking move count per move type + */ + private final Map moveEvaluationCountPerTypeMap = new ConcurrentHashMap<>(); + private static AtomicLong resetAtomicLongTimeMillis(AtomicLong atomicLong) { atomicLong.set(-1); return atomicLong; @@ -231,6 +239,14 @@ public void setBestSolutionTimeMillis(Long bestSolutionTimeMillis) { this.bestSolutionTimeMillis = bestSolutionTimeMillis; } + public Set getMoveCountTypes() { + return moveEvaluationCountPerTypeMap.keySet(); + } + + public Map getMoveEvaluationCountPerType() { + return moveEvaluationCountPerTypeMap.entrySet().stream().collect(toMap(Map.Entry::getKey, e -> e.getValue().get())); + } + // ************************************************************************ // Calculated methods // ************************************************************************ @@ -365,4 +381,14 @@ public void destroyYielding() { } } + public void incrementMoveEvaluationCountPerType(Move move) { + moveEvaluationCountPerTypeMap.compute(move.getSimpleMoveTypeDescription(), (key, count) -> { + if (count == null) { + count = new AtomicLong(); + } + count.incrementAndGet(); + return count; + }); + } + } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/statistic/MoveCountPerTypeStatistic.java b/core/src/main/java/ai/timefold/solver/core/impl/statistic/MoveCountPerTypeStatistic.java new file mode 100644 index 0000000000..2fe8a2dd75 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/statistic/MoveCountPerTypeStatistic.java @@ -0,0 +1,69 @@ +package ai.timefold.solver.core.impl.statistic; + +import java.util.HashMap; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +import ai.timefold.solver.core.api.solver.Solver; +import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; +import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleListenerAdapter; +import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; +import ai.timefold.solver.core.impl.solver.DefaultSolver; + +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tags; + +public class MoveCountPerTypeStatistic implements SolverStatistic { + + private final Map, PhaseLifecycleListenerAdapter> solverToPhaseLifecycleListenerMap = + new WeakHashMap<>(); + + @Override + public void unregister(Solver solver) { + PhaseLifecycleListenerAdapter listener = solverToPhaseLifecycleListenerMap.remove(solver); + if (listener != null) { + ((DefaultSolver) solver).removePhaseLifecycleListener(listener); + } + } + + @Override + public void register(Solver solver) { + var defaultSolver = (DefaultSolver) solver; + var listener = new MoveCountPerTypeStatistic.MoveCountPerTypeStatisticListener(); + solverToPhaseLifecycleListenerMap.put(solver, listener); + defaultSolver.addPhaseLifecycleListener(listener); + } + + private static class MoveCountPerTypeStatisticListener extends PhaseLifecycleListenerAdapter { + private final Map> tagsToMoveCountMap = new ConcurrentHashMap<>(); + + @Override + public void phaseEnded(AbstractPhaseScope phaseScope) { + // The metric must be collected when the phase ends instead of when the solver ends + // because there is no guarantee this listener will run the phase event before the StatisticRegistry listener + var moveCountPerType = phaseScope.getSolverScope().getMoveEvaluationCountPerType(); + var tags = phaseScope.getSolverScope().getMonitoringTags(); + moveCountPerType.forEach((type, count) -> { + var key = SolverMetric.MOVE_COUNT_PER_TYPE.getMeterId() + "." + type; + var counter = Metrics.gauge(key, tags, new AtomicLong(0L)); + if (counter != null) { + counter.set(count); + } + registerMoveCountPerType(tags, key, counter); + }); + } + + private void registerMoveCountPerType(Tags tag, String key, AtomicLong count) { + tagsToMoveCountMap.compute(tag, (tags, countMap) -> { + if (countMap == null) { + countMap = new HashMap<>(); + } + countMap.put(key, count); + return countMap; + }); + } + } + +} diff --git a/core/src/main/resources/solver.xsd b/core/src/main/resources/solver.xsd index 83ea05c77f..766a833033 100644 --- a/core/src/main/resources/solver.xsd +++ b/core/src/main/resources/solver.xsd @@ -1593,6 +1593,8 @@ + + diff --git a/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java index ca3ae44456..eb2e7b23f7 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java @@ -5,19 +5,27 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.SoftAssertions.assertSoftly; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; +import ai.timefold.solver.core.api.solver.Solver; +import ai.timefold.solver.core.api.solver.SolverFactory; import ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicPhaseConfig; import ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicType; +import ai.timefold.solver.core.config.solver.monitoring.MonitoringConfig; +import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; import ai.timefold.solver.core.impl.testdata.domain.TestdataEntity; import ai.timefold.solver.core.impl.testdata.domain.TestdataSolution; import ai.timefold.solver.core.impl.testdata.domain.TestdataValue; import ai.timefold.solver.core.impl.testdata.domain.allows_unassigned.TestdataAllowsUnassignedEasyScoreCalculator; import ai.timefold.solver.core.impl.testdata.domain.allows_unassigned.TestdataAllowsUnassignedEntity; import ai.timefold.solver.core.impl.testdata.domain.allows_unassigned.TestdataAllowsUnassignedSolution; +import ai.timefold.solver.core.impl.testdata.domain.list.TestdataListEntity; +import ai.timefold.solver.core.impl.testdata.domain.list.TestdataListSolution; +import ai.timefold.solver.core.impl.testdata.domain.list.TestdataListValue; import ai.timefold.solver.core.impl.testdata.domain.list.allows_unassigned.TestdataAllowsUnassignedValuesListEasyScoreCalculator; import ai.timefold.solver.core.impl.testdata.domain.list.allows_unassigned.TestdataAllowsUnassignedValuesListEntity; import ai.timefold.solver.core.impl.testdata.domain.list.allows_unassigned.TestdataAllowsUnassignedValuesListSolution; @@ -27,9 +35,12 @@ import ai.timefold.solver.core.impl.testdata.domain.pinned.allows_unassigned.TestdataPinnedAllowsUnassignedEntity; import ai.timefold.solver.core.impl.testdata.domain.pinned.allows_unassigned.TestdataPinnedAllowsUnassignedSolution; import ai.timefold.solver.core.impl.testdata.util.PlannerTestUtils; +import ai.timefold.solver.core.impl.testutil.TestMeterRegistry; import org.junit.jupiter.api.Test; +import io.micrometer.core.instrument.Metrics; + class DefaultConstructionHeuristicPhaseTest { @Test @@ -80,6 +91,64 @@ void solveWithInitializedSolution() { assertThat(inputProblem).isSameAs(solution); } + @Test + void solveWithCustomMetrics() { + var meterRegistry = new TestMeterRegistry(); + Metrics.addRegistry(meterRegistry); + + var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class) + .withPhases(new ConstructionHeuristicPhaseConfig()) + .withMonitoringConfig(new MonitoringConfig().withSolverMetricList(List.of(SolverMetric.MOVE_COUNT_PER_TYPE))); + + var problem = new TestdataSolution("s1"); + var v1 = new TestdataValue("v1"); + var v2 = new TestdataValue("v2"); + var v3 = new TestdataValue("v3"); + problem.setValueList(Arrays.asList(v1, v2, v3)); + problem.setEntityList(Arrays.asList( + new TestdataEntity("e1", null), + new TestdataEntity("e2", v2), + new TestdataEntity("e3", v1))); + + SolverFactory solverFactory = SolverFactory.create(solverConfig); + Solver solver = solverFactory.buildSolver(); + solver.solve(problem); + + SolverMetric.MOVE_COUNT_PER_TYPE.register(solver); + meterRegistry.publish(solver); + var moveCount = meterRegistry.getMeasurement( + SolverMetric.MOVE_COUNT_PER_TYPE.getMeterId() + "." + "ChangeMove(TestdataEntity.value)", "VALUE"); + assertThat(moveCount).isPositive(); + } + + @Test + void solveWithListVariableAndCustomMetrics() { + var meterRegistry = new TestMeterRegistry(); + Metrics.addRegistry(meterRegistry); + + var solverConfig = PlannerTestUtils + .buildSolverConfig(TestdataListSolution.class, TestdataListEntity.class, TestdataListValue.class) + .withPhases(new ConstructionHeuristicPhaseConfig()) + .withMonitoringConfig(new MonitoringConfig().withSolverMetricList(List.of(SolverMetric.MOVE_COUNT_PER_TYPE))); + + var problem = new TestdataListSolution(); + var v1 = new TestdataListValue("v1"); + var v2 = new TestdataListValue("v2"); + var v3 = new TestdataListValue("v3"); + problem.setValueList(Arrays.asList(v1, v2, v3)); + problem.setEntityList(List.of(new TestdataListEntity("e1", new ArrayList<>()))); + + SolverFactory solverFactory = SolverFactory.create(solverConfig); + Solver solver = solverFactory.buildSolver(); + solver.solve(problem); + + SolverMetric.MOVE_COUNT_PER_TYPE.register(solver); + meterRegistry.publish(solver); + var moveCount = meterRegistry.getMeasurement( + SolverMetric.MOVE_COUNT_PER_TYPE.getMeterId() + "." + "ListAssignMove(TestdataListEntity.valueList)", "VALUE"); + assertThat(moveCount).isPositive(); + } + @Test void solveWithPinnedEntities() { var solverConfig = 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 1839054b49..44217390ab 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 @@ -11,6 +11,7 @@ import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.concurrent.atomic.AtomicReference; import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; @@ -18,6 +19,7 @@ import ai.timefold.solver.core.api.solver.Solver; import ai.timefold.solver.core.api.solver.SolverFactory; import ai.timefold.solver.core.config.exhaustivesearch.ExhaustiveSearchPhaseConfig; +import ai.timefold.solver.core.config.solver.monitoring.MonitoringConfig; import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; import ai.timefold.solver.core.impl.exhaustivesearch.decider.ExhaustiveSearchDecider; import ai.timefold.solver.core.impl.exhaustivesearch.node.ExhaustiveSearchLayer; @@ -187,6 +189,37 @@ void solveWithInitializedEntitiesAndMetric() { assertThat(moveCount).isPositive(); } + @Test + void solveCustomMetrics() { + var meterRegistry = new TestMeterRegistry(); + Metrics.addRegistry(meterRegistry); + + var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, + TestdataEntity.class); + solverConfig.withPhases(new ExhaustiveSearchPhaseConfig()) + .withMonitoringConfig(new MonitoringConfig().withSolverMetricList(List.of(SolverMetric.MOVE_COUNT_PER_TYPE))); + + var problem = new TestdataSolution("s1"); + var v1 = new TestdataValue("v1"); + var v2 = new TestdataValue("v2"); + var v3 = new TestdataValue("v3"); + problem.setValueList(Arrays.asList(v1, v2, v3)); + problem.setEntityList(Arrays.asList( + new TestdataEntity("e1", null), + new TestdataEntity("e2", v2), + new TestdataEntity("e3", v1))); + + SolverFactory solverFactory = SolverFactory.create(solverConfig); + Solver solver = solverFactory.buildSolver(); + solver.solve(problem); + + SolverMetric.MOVE_COUNT_PER_TYPE.register(solver); + meterRegistry.publish(solver); + var moveCount = meterRegistry.getMeasurement( + SolverMetric.MOVE_COUNT_PER_TYPE.getMeterId() + "." + "ChangeMove(TestdataEntity.value)", "VALUE"); + assertThat(moveCount).isPositive(); + } + @Test void solveWithPinnedEntities() { var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataPinnedSolution.class, TestdataPinnedEntity.class) diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseTest.java index 396c4d8073..6c68bc3bb0 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseTest.java @@ -6,10 +6,15 @@ import java.util.Arrays; import java.util.Collections; +import java.util.List; import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; +import ai.timefold.solver.core.api.solver.Solver; +import ai.timefold.solver.core.api.solver.SolverFactory; import ai.timefold.solver.core.config.localsearch.LocalSearchPhaseConfig; import ai.timefold.solver.core.config.localsearch.LocalSearchType; +import ai.timefold.solver.core.config.solver.monitoring.MonitoringConfig; +import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; import ai.timefold.solver.core.config.solver.termination.TerminationConfig; import ai.timefold.solver.core.impl.testdata.domain.TestdataEntity; import ai.timefold.solver.core.impl.testdata.domain.TestdataSolution; @@ -26,9 +31,12 @@ import ai.timefold.solver.core.impl.testdata.domain.pinned.allows_unassigned.TestdataPinnedAllowsUnassignedEntity; import ai.timefold.solver.core.impl.testdata.domain.pinned.allows_unassigned.TestdataPinnedAllowsUnassignedSolution; import ai.timefold.solver.core.impl.testdata.util.PlannerTestUtils; +import ai.timefold.solver.core.impl.testutil.TestMeterRegistry; import org.junit.jupiter.api.Test; +import io.micrometer.core.instrument.Metrics; + class DefaultLocalSearchPhaseTest { @Test @@ -61,6 +69,77 @@ void solveWithInitializedEntities() { assertThat(solvedE3.getValue()).isNotNull(); } + @Test + void solveWithCustomMetrics() { + var meterRegistry = new TestMeterRegistry(); + Metrics.addRegistry(meterRegistry); + + var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class); + var phaseConfig = new LocalSearchPhaseConfig(); + phaseConfig.setTerminationConfig(new TerminationConfig().withScoreCalculationCountLimit(10L)); + solverConfig.withPhases(phaseConfig) + .withMonitoringConfig(new MonitoringConfig().withSolverMetricList(List.of(SolverMetric.MOVE_COUNT_PER_TYPE))); + + var problem = new TestdataSolution("s1"); + var v1 = new TestdataValue("v1"); + var v2 = new TestdataValue("v2"); + var v3 = new TestdataValue("v3"); + problem.setValueList(Arrays.asList(v1, v2, v3)); + problem.setEntityList(Arrays.asList( + new TestdataEntity("e1", v3), + new TestdataEntity("e2", v2), + new TestdataEntity("e3", v1))); + + SolverFactory solverFactory = SolverFactory.create(solverConfig); + Solver solver = solverFactory.buildSolver(); + solver.solve(problem); + + SolverMetric.MOVE_COUNT_PER_TYPE.register(solver); + meterRegistry.publish(solver); + var moveCountPerChange = meterRegistry.getMeasurement( + SolverMetric.MOVE_COUNT_PER_TYPE.getMeterId() + "." + "ChangeMove(TestdataEntity.value)", "VALUE"); + var moveCountPerSwap = meterRegistry.getMeasurement( + SolverMetric.MOVE_COUNT_PER_TYPE.getMeterId() + "." + "SwapMove(TestdataEntity.value)", "VALUE"); + assertThat(moveCountPerChange).isPositive(); + assertThat(moveCountPerSwap).isPositive(); + } + + @Test + void solveWithListVariableAndCustomMetrics() { + var meterRegistry = new TestMeterRegistry(); + Metrics.addRegistry(meterRegistry); + + var solverConfig = PlannerTestUtils + .buildSolverConfig(TestdataListSolution.class, TestdataListEntity.class, TestdataListValue.class); + var phaseConfig = new LocalSearchPhaseConfig(); + phaseConfig.setTerminationConfig(new TerminationConfig().withScoreCalculationCountLimit(10L)); + solverConfig.withPhases(phaseConfig) + .withMonitoringConfig(new MonitoringConfig().withSolverMetricList(List.of(SolverMetric.MOVE_COUNT_PER_TYPE))); + + var problem = new TestdataListSolution(); + var v1 = new TestdataListValue("v1"); + var v2 = new TestdataListValue("v2"); + var v3 = new TestdataListValue("v3"); + problem.setValueList(Arrays.asList(v1, v2, v3)); + problem.setEntityList(List.of(new TestdataListEntity("e1", v1, v2, v3))); + + SolverFactory solverFactory = SolverFactory.create(solverConfig); + Solver solver = solverFactory.buildSolver(); + solver.solve(problem); + + SolverMetric.MOVE_COUNT_PER_TYPE.register(solver); + meterRegistry.publish(solver); + var moveCountPerChange = meterRegistry.getMeasurement( + SolverMetric.MOVE_COUNT_PER_TYPE.getMeterId() + "." + "ListChangeMove(TestdataListEntity.valueList)", "VALUE"); + var moveCountPerSwap = meterRegistry.getMeasurement( + SolverMetric.MOVE_COUNT_PER_TYPE.getMeterId() + "." + "ListSwapMove(TestdataListEntity.valueList)", "VALUE"); + var moveCountPer2Opt = meterRegistry.getMeasurement( + SolverMetric.MOVE_COUNT_PER_TYPE.getMeterId() + "." + "2-Opt(TestdataListEntity.valueList)", "VALUE"); + assertThat(moveCountPerChange).isPositive(); + assertThat(moveCountPerSwap).isPositive(); + assertThat(moveCountPer2Opt).isPositive(); + } + @Test void solveWithPinnedEntities() { var solverConfig = From ca14115fdd4be7fbf01cd24b7a27891cd7a2ed2d Mon Sep 17 00:00:00 2001 From: Fred Date: Mon, 23 Sep 2024 18:41:27 -0300 Subject: [PATCH 19/26] FIX: improve tests and unregister logic --- .../core/impl/solver/scope/SolverScope.java | 15 ++-- .../statistic/MoveCountPerTypeStatistic.java | 15 +++- ...DefaultConstructionHeuristicPhaseTest.java | 44 ++++++++--- .../DefaultExhaustiveSearchPhaseTest.java | 24 ++++-- .../DefaultLocalSearchPhaseTest.java | 77 ++++++++++++++----- 5 files changed, 129 insertions(+), 46 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java index d9aa7e2f6e..0caac1c59d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java @@ -382,13 +382,16 @@ public void destroyYielding() { } public void incrementMoveEvaluationCountPerType(Move move) { - moveEvaluationCountPerTypeMap.compute(move.getSimpleMoveTypeDescription(), (key, count) -> { - if (count == null) { - count = new AtomicLong(); + addMoveEvaluationCountPerType(move.getSimpleMoveTypeDescription(), 1L); + } + + public void addMoveEvaluationCountPerType(String moveType, long count) { + moveEvaluationCountPerTypeMap.compute(moveType, (key, counter) -> { + if (counter == null) { + counter = new AtomicLong(); } - count.incrementAndGet(); - return count; + counter.addAndGet(count); + return counter; }); } - } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/statistic/MoveCountPerTypeStatistic.java b/core/src/main/java/ai/timefold/solver/core/impl/statistic/MoveCountPerTypeStatistic.java index 2fe8a2dd75..5e1db3ed45 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/statistic/MoveCountPerTypeStatistic.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/statistic/MoveCountPerTypeStatistic.java @@ -11,7 +11,9 @@ import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleListenerAdapter; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.solver.DefaultSolver; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; +import io.micrometer.core.instrument.Meter; import io.micrometer.core.instrument.Metrics; import io.micrometer.core.instrument.Tags; @@ -22,9 +24,10 @@ public class MoveCountPerTypeStatistic implements SolverStatistic solver) { - PhaseLifecycleListenerAdapter listener = solverToPhaseLifecycleListenerMap.remove(solver); + var listener = solverToPhaseLifecycleListenerMap.remove(solver); if (listener != null) { ((DefaultSolver) solver).removePhaseLifecycleListener(listener); + ((MoveCountPerTypeStatisticListener) listener).unregister(solver); } } @@ -64,6 +67,16 @@ private void registerMoveCountPerType(Tags tag, String key, AtomicLong count) { return countMap; }); } + + void unregister(Solver solver) { + SolverScope solverScope = ((DefaultSolver) solver).getSolverScope(); + tagsToMoveCountMap.values().stream().flatMap(v -> v.keySet().stream()) + .forEach(meter -> Metrics.globalRegistry.remove(new Meter.Id(meter, + solverScope.getMonitoringTags(), + null, + null, + Meter.Type.GAUGE))); + } } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java index eb2e7b23f7..aa9f7195bd 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java @@ -9,6 +9,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.concurrent.atomic.AtomicLong; import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.solver.Solver; @@ -17,6 +18,9 @@ import ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicType; import ai.timefold.solver.core.config.solver.monitoring.MonitoringConfig; import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; +import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleListenerAdapter; +import ai.timefold.solver.core.impl.solver.DefaultSolver; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; import ai.timefold.solver.core.impl.testdata.domain.TestdataEntity; import ai.timefold.solver.core.impl.testdata.domain.TestdataSolution; import ai.timefold.solver.core.impl.testdata.domain.TestdataValue; @@ -112,13 +116,21 @@ void solveWithCustomMetrics() { SolverFactory solverFactory = SolverFactory.create(solverConfig); Solver solver = solverFactory.buildSolver(); + var moveCountPerChange = new AtomicLong(); + ((DefaultSolver) solver).addPhaseLifecycleListener(new PhaseLifecycleListenerAdapter<>() { + @Override + public void solvingEnded(SolverScope solverScope) { + meterRegistry.publish(solver); + var changeMoveKey = "ChangeMove(TestdataEntity.value)"; + if (solverScope.getMoveCountTypes().contains(changeMoveKey)) { + var counter = meterRegistry + .getMeasurement(SolverMetric.MOVE_COUNT_PER_TYPE.getMeterId() + "." + changeMoveKey, "VALUE"); + moveCountPerChange.set(counter.longValue()); + } + } + }); solver.solve(problem); - - SolverMetric.MOVE_COUNT_PER_TYPE.register(solver); - meterRegistry.publish(solver); - var moveCount = meterRegistry.getMeasurement( - SolverMetric.MOVE_COUNT_PER_TYPE.getMeterId() + "." + "ChangeMove(TestdataEntity.value)", "VALUE"); - assertThat(moveCount).isPositive(); + assertThat(moveCountPerChange.get()).isPositive(); } @Test @@ -140,13 +152,21 @@ void solveWithListVariableAndCustomMetrics() { SolverFactory solverFactory = SolverFactory.create(solverConfig); Solver solver = solverFactory.buildSolver(); + var moveCount = new AtomicLong(); + ((DefaultSolver) solver).addPhaseLifecycleListener(new PhaseLifecycleListenerAdapter<>() { + @Override + public void solvingEnded(SolverScope solverScope) { + meterRegistry.publish(solver); + var changeMoveKey = "ListAssignMove(TestdataListEntity.valueList)"; + if (solverScope.getMoveCountTypes().contains(changeMoveKey)) { + var counter = meterRegistry + .getMeasurement(SolverMetric.MOVE_COUNT_PER_TYPE.getMeterId() + "." + changeMoveKey, "VALUE"); + moveCount.set(counter.longValue()); + } + } + }); solver.solve(problem); - - SolverMetric.MOVE_COUNT_PER_TYPE.register(solver); - meterRegistry.publish(solver); - var moveCount = meterRegistry.getMeasurement( - SolverMetric.MOVE_COUNT_PER_TYPE.getMeterId() + "." + "ListAssignMove(TestdataListEntity.valueList)", "VALUE"); - assertThat(moveCount).isPositive(); + assertThat(moveCount.get()).isPositive(); } @Test 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 44217390ab..4c47b9624d 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 @@ -12,6 +12,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; @@ -28,7 +29,10 @@ import ai.timefold.solver.core.impl.exhaustivesearch.scope.ExhaustiveSearchStepScope; import ai.timefold.solver.core.impl.heuristic.move.Move; import ai.timefold.solver.core.impl.heuristic.selector.entity.EntitySelector; +import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleListenerAdapter; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; +import ai.timefold.solver.core.impl.solver.DefaultSolver; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; import ai.timefold.solver.core.impl.testdata.domain.TestdataEntity; import ai.timefold.solver.core.impl.testdata.domain.TestdataSolution; import ai.timefold.solver.core.impl.testdata.domain.TestdataValue; @@ -211,13 +215,21 @@ void solveCustomMetrics() { SolverFactory solverFactory = SolverFactory.create(solverConfig); Solver solver = solverFactory.buildSolver(); + var moveCountPerChange = new AtomicLong(); + ((DefaultSolver) solver).addPhaseLifecycleListener(new PhaseLifecycleListenerAdapter<>() { + @Override + public void solvingEnded(SolverScope solverScope) { + meterRegistry.publish(solver); + var changeMoveKey = "ChangeMove(TestdataEntity.value)"; + if (solverScope.getMoveCountTypes().contains(changeMoveKey)) { + var counter = meterRegistry + .getMeasurement(SolverMetric.MOVE_COUNT_PER_TYPE.getMeterId() + "." + changeMoveKey, "VALUE"); + moveCountPerChange.set(counter.longValue()); + } + } + }); solver.solve(problem); - - SolverMetric.MOVE_COUNT_PER_TYPE.register(solver); - meterRegistry.publish(solver); - var moveCount = meterRegistry.getMeasurement( - SolverMetric.MOVE_COUNT_PER_TYPE.getMeterId() + "." + "ChangeMove(TestdataEntity.value)", "VALUE"); - assertThat(moveCount).isPositive(); + assertThat(moveCountPerChange.get()).isPositive(); } @Test diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseTest.java index 6c68bc3bb0..a0dc66f81f 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseTest.java @@ -7,6 +7,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.concurrent.atomic.AtomicLong; import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.solver.Solver; @@ -16,6 +17,9 @@ import ai.timefold.solver.core.config.solver.monitoring.MonitoringConfig; import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; import ai.timefold.solver.core.config.solver.termination.TerminationConfig; +import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleListenerAdapter; +import ai.timefold.solver.core.impl.solver.DefaultSolver; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; import ai.timefold.solver.core.impl.testdata.domain.TestdataEntity; import ai.timefold.solver.core.impl.testdata.domain.TestdataSolution; import ai.timefold.solver.core.impl.testdata.domain.TestdataValue; @@ -92,16 +96,29 @@ void solveWithCustomMetrics() { SolverFactory solverFactory = SolverFactory.create(solverConfig); Solver solver = solverFactory.buildSolver(); + var moveCountPerChange = new AtomicLong(); + var moveCountPerSwap = new AtomicLong(); + ((DefaultSolver) solver).addPhaseLifecycleListener(new PhaseLifecycleListenerAdapter<>() { + @Override + public void solvingEnded(SolverScope solverScope) { + meterRegistry.publish(solver); + var changeMoveKey = "ChangeMove(TestdataEntity.value)"; + if (solverScope.getMoveCountTypes().contains(changeMoveKey)) { + var counter = meterRegistry + .getMeasurement(SolverMetric.MOVE_COUNT_PER_TYPE.getMeterId() + "." + changeMoveKey, "VALUE"); + moveCountPerChange.set(counter.longValue()); + } + var swapMoveKey = "SwapMove(TestdataEntity.value)"; + if (solverScope.getMoveCountTypes().contains(swapMoveKey)) { + var counter = meterRegistry + .getMeasurement(SolverMetric.MOVE_COUNT_PER_TYPE.getMeterId() + "." + swapMoveKey, "VALUE"); + moveCountPerSwap.set(counter.longValue()); + } + } + }); solver.solve(problem); - - SolverMetric.MOVE_COUNT_PER_TYPE.register(solver); - meterRegistry.publish(solver); - var moveCountPerChange = meterRegistry.getMeasurement( - SolverMetric.MOVE_COUNT_PER_TYPE.getMeterId() + "." + "ChangeMove(TestdataEntity.value)", "VALUE"); - var moveCountPerSwap = meterRegistry.getMeasurement( - SolverMetric.MOVE_COUNT_PER_TYPE.getMeterId() + "." + "SwapMove(TestdataEntity.value)", "VALUE"); - assertThat(moveCountPerChange).isPositive(); - assertThat(moveCountPerSwap).isPositive(); + assertThat(moveCountPerChange.get()).isPositive(); + assertThat(moveCountPerSwap.get()).isPositive(); } @Test @@ -125,19 +142,37 @@ void solveWithListVariableAndCustomMetrics() { SolverFactory solverFactory = SolverFactory.create(solverConfig); Solver solver = solverFactory.buildSolver(); + var moveCountPerChange = new AtomicLong(); + var moveCountPerSwap = new AtomicLong(); + var moveCountPer2Opt = new AtomicLong(); + ((DefaultSolver) solver).addPhaseLifecycleListener(new PhaseLifecycleListenerAdapter<>() { + @Override + public void solvingEnded(SolverScope solverScope) { + meterRegistry.publish(solver); + var changeMoveKey = "ListChangeMove(TestdataListEntity.valueList)"; + if (solverScope.getMoveCountTypes().contains(changeMoveKey)) { + var counter = meterRegistry + .getMeasurement(SolverMetric.MOVE_COUNT_PER_TYPE.getMeterId() + "." + changeMoveKey, "VALUE"); + moveCountPerChange.set(counter.longValue()); + } + var swapMoveKey = "ListSwapMove(TestdataListEntity.valueList)"; + if (solverScope.getMoveCountTypes().contains(swapMoveKey)) { + var counter = meterRegistry + .getMeasurement(SolverMetric.MOVE_COUNT_PER_TYPE.getMeterId() + "." + swapMoveKey, "VALUE"); + moveCountPerSwap.set(counter.longValue()); + } + var twoOptMoveKey = "2-Opt(TestdataListEntity.valueList)"; + if (solverScope.getMoveCountTypes().contains(twoOptMoveKey)) { + var counter = meterRegistry + .getMeasurement(SolverMetric.MOVE_COUNT_PER_TYPE.getMeterId() + "." + twoOptMoveKey, "VALUE"); + moveCountPer2Opt.set(counter.longValue()); + } + } + }); solver.solve(problem); - - SolverMetric.MOVE_COUNT_PER_TYPE.register(solver); - meterRegistry.publish(solver); - var moveCountPerChange = meterRegistry.getMeasurement( - SolverMetric.MOVE_COUNT_PER_TYPE.getMeterId() + "." + "ListChangeMove(TestdataListEntity.valueList)", "VALUE"); - var moveCountPerSwap = meterRegistry.getMeasurement( - SolverMetric.MOVE_COUNT_PER_TYPE.getMeterId() + "." + "ListSwapMove(TestdataListEntity.valueList)", "VALUE"); - var moveCountPer2Opt = meterRegistry.getMeasurement( - SolverMetric.MOVE_COUNT_PER_TYPE.getMeterId() + "." + "2-Opt(TestdataListEntity.valueList)", "VALUE"); - assertThat(moveCountPerChange).isPositive(); - assertThat(moveCountPerSwap).isPositive(); - assertThat(moveCountPer2Opt).isPositive(); + assertThat(moveCountPerChange.get()).isPositive(); + assertThat(moveCountPerSwap.get()).isPositive(); + assertThat(moveCountPer2Opt.get()).isPositive(); } @Test From 02f55d670290c41ae0cd5fb3bc0505f6b87a8a0d Mon Sep 17 00:00:00 2001 From: Fred Date: Tue, 24 Sep 2024 14:31:54 -0300 Subject: [PATCH 20/26] doc: add new metric documentation --- .../moveCountPerTypeStatistic.png | Bin 0 -> 102852 bytes .../constraints-and-score/performance.adoc | 39 ++++++++-------- .../score-calculation.adoc | 2 +- .../enterprise-edition.adoc | 20 ++++----- .../ROOT/pages/integration/integration.adoc | 6 +-- .../optimization-algorithms/overview.adoc | 17 ++++++- .../hello-world/hello-world-quickstart.adoc | 10 ++--- .../quarkus-vehicle-routing-quickstart.adoc | 10 ++--- .../quarkus/quarkus-quickstart.adoc | 10 ++--- .../spring-boot/spring-boot-quickstart.adoc | 10 ++--- .../benchmarking-and-tweaking.adoc | 42 ++++++++++++------ .../modeling-planning-problems.adoc | 2 +- .../running-the-solver.adoc | 14 ++++-- 13 files changed, 109 insertions(+), 73 deletions(-) create mode 100644 docs/src/modules/ROOT/images/using-timefold-solver/benchmarking-and-tweaking/moveCountPerTypeStatistic.png diff --git a/docs/src/modules/ROOT/images/using-timefold-solver/benchmarking-and-tweaking/moveCountPerTypeStatistic.png b/docs/src/modules/ROOT/images/using-timefold-solver/benchmarking-and-tweaking/moveCountPerTypeStatistic.png new file mode 100644 index 0000000000000000000000000000000000000000..865ce4ca0b9ac7f5a77bc0eba3faec16e25fad45 GIT binary patch literal 102852 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 WW@o48i;qJ8aC7z8Ub^kbng0V#tuK54 literal 0 HcmV?d00001 diff --git a/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc b/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc index 3a8429a601..8a256a9ebc 100644 --- a/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc +++ b/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc @@ -4,17 +4,17 @@ [#scoreCalculationPerformanceTricksOverview] == Overview -The `Solver` will normally spend most of its execution time running score calculation, +The `Solver` will normally spend most of its execution time evaluating moves and running score calculation, which is called in its deepest loops. -Faster score calculation will return the same solution in less time with the same algorithm, +Faster move evaluation will return the same solution in less time with the same algorithm, which normally means a better solution in equal time. -[#scoreCalculationSpeed] -== Score calculation speed +[#moveEvaluationSpeed] +== Move evaluation speed -After solving a problem, the `Solver` will log the __score calculation speed per second__. -This is a good measurement of Score calculation performance, +After solving a problem, the `Solver` will log the __move evaluation speed per second__. +This is a good measurement of Move evaluation performance, despite that it is affected by non-score calculation execution time. It depends on the problem scale of the problem dataset. Normally, even for large scale problems, it is higher than ``1000``, @@ -22,14 +22,14 @@ except if you are using xref:constraints-and-score/score-calculation.adoc#easySc [IMPORTANT] ==== -When improving your score calculation, focus on maximizing the score calculation speed, +When improving your move evaluation, focus on maximizing the move evaluation speed, instead of maximizing the best score. -A big improvement in score calculation can sometimes yield little or no best score improvement, +A big improvement in move evaluation can sometimes yield little or no best score improvement, for example when the algorithm is stuck in a local or global optima. -If you are watching the calculation speed instead, score calculation improvements are far more visible. +If you are watching the move evaluation speed instead, move evaluation improvements are far more visible. -Furthermore, watching the calculation speed allows you to remove or add score constraints, -and still compare it with the original's calculation speed. +Furthermore, watching the evaluation speed allows you to remove or add score constraints, +and still compare it with the original's evaluation speed. Comparing the best score with the original's best score is pointless: it's comparing apples and oranges. ==== @@ -40,10 +40,7 @@ such as xref:optimization-algorithms/move-selector-reference.adoc#ruinRecreateMo involve processing the score multiple times within the same move. The use of moves like Ruin and Recreate may increase the number of score calculations, -which makes the relationship `1:1` invalid. -In such cases, -the metric xref:using-timefold-solver/benchmarking-and-tweaking.adoc#benchmarkReportMoveEvaluationSpeedSummary[__move evaluation speed per second__] makes more sense, -as it will count the number of moves individually. +making this ratio `N:1`. [#incrementalScoreCalculationPerformance] == Incremental score calculation (with deltas) @@ -70,7 +67,7 @@ making incremental score calculation far more scalable. Do not call remote services in your score calculation, except if you are bridging `EasyScoreCalculator` to a legacy system. -The network latency will kill your score calculation performance. +The network latency will kill your move evaluation performance. Cache the results of those remote services if possible. If some parts of a constraint can be calculated once, when the `Solver` starts, and never change during solving, @@ -108,7 +105,7 @@ Use xref:using-timefold-solver/modeling-planning-problems.adoc#valueRangeProvide or xref:optimization-algorithms/overview.adoc#filteredSelection[filtered selection] to define that Course A should only be assigned a `Room` different than X. -This can give a good performance gain in some use cases, not just because the score calculation is faster, +This can give a good performance gain in some use cases, not just because the move evaluation is faster, but mainly because most optimization algorithms will spend less time evaluating infeasible solutions. However, usually this is not a good idea because there is a real risk of trading short-term benefits for long-term harm: @@ -225,7 +222,7 @@ This shows the problem in numbers: To process a thousand lessons, the constraint first creates a cross-product of one million pairs, only to throw away pretty much all of them before penalizing. -Reducing the size of the cross-product by half will therefore double the score calculation speed. +Reducing the size of the cross-product by half will therefore double the move evaluation speed. === Filters before joins @@ -470,5 +467,7 @@ For example, move multiple items from the same container to another container. == `stepLimit` benchmark Not all score constraints have the same performance cost. -Sometimes one score constraint can kill the score calculation performance outright. -Use the xref:using-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] to do a one minute run and check what happens to the score calculation speed if you comment out all but one of the score constraints. \ No newline at end of file +Sometimes one score constraint can kill the move evaluation performance outright. +Use the xref:using-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] +to do a one minute run and check what happens to the move evaluation speed +if you comment out all but one of the score constraints. \ No newline at end of file diff --git a/docs/src/modules/ROOT/pages/constraints-and-score/score-calculation.adoc b/docs/src/modules/ROOT/pages/constraints-and-score/score-calculation.adoc index 582e97d2a0..ed20045ad6 100644 --- a/docs/src/modules/ROOT/pages/constraints-and-score/score-calculation.adoc +++ b/docs/src/modules/ROOT/pages/constraints-and-score/score-calculation.adoc @@ -1858,7 +1858,7 @@ the expensive computation will not be reevaluated again unless the `Talk` itself [NOTE] ==== There is a performance cost to `expand()`. -Always check your solver's score calculation speed to see if the cost is offset by the gains. +Always check your solver's move evaluation speed to see if the cost is offset by the gains. ==== [#constraintStreamsFlattening] diff --git a/docs/src/modules/ROOT/pages/enterprise-edition/enterprise-edition.adoc b/docs/src/modules/ROOT/pages/enterprise-edition/enterprise-edition.adoc index e81f46fec1..ee9b746bcf 100644 --- a/docs/src/modules/ROOT/pages/enterprise-edition/enterprise-edition.adoc +++ b/docs/src/modules/ROOT/pages/enterprise-edition/enterprise-edition.adoc @@ -470,7 +470,7 @@ There are several ways of doing multi-threaded solving: * *<>*: Solve 1 dataset with multiple threads without sacrificing xref:constraints-and-score/performance.adoc#incrementalScoreCalculationPerformance[incremental score calculation]. -** Donate a portion of your CPU cores to Timefold Solver to scale up the score calculation speed and get the same results in fraction of the time. +** Donate a portion of your CPU cores to Timefold Solver to scale up the move evaluation speed and get the same results in fraction of the time. * *<>*: Split 1 dataset in multiple parts and solve them independently. * *Multi bet solving*: solve 1 dataset with multiple, isolated solvers and take the best result. @@ -486,7 +486,7 @@ In this section, we will focus on multi-threaded incremental solving and partiti [NOTE] ==== A xref:using-timefold-solver/running-the-solver.adoc#logging[logging level] of `debug` or `trace` might cause congestion -and slow down the xref:constraints-and-score/performance.adoc#scoreCalculationSpeed[score calculation speed]. +and slow down the xref:constraints-and-score/performance.adoc#moveEvaluationSpeed[move evaluation speed]. ==== @@ -495,20 +495,20 @@ and slow down the xref:constraints-and-score/performance.adoc#scoreCalculationSp With this feature, the solver can run significantly faster, getting you the right solution earlier. -It has been designed to speed up the solver in cases where score calculation is the bottleneck. +It has been designed to speed up the solver in cases where move evaluation is the bottleneck. This typically happens when the constraints are computationally expensive, or when the dataset is large. -- The sweet spot for this feature is when the score calculation speed is up to 10 thousand per second. +- The sweet spot for this feature is when the move evaluation speed is up to 10 thousand per second. In this case, we have observed the algorithm to scale linearly with the number of move threads. Every additional move thread will bring a speedup, albeit with diminishing returns. -- For score calculation speeds on the order of 100 thousand per second, +- For move evaluation speeds on the order of 100 thousand per second, the algorithm no longer scales linearly, but using 4 to 8 move threads may still be beneficial. -- For even higher score calculation speeds, +- For even higher move evaluation speeds, the feature does not bring any benefit. -At these speeds, score calculation is no longer the bottleneck. +At these speeds, move evaluation is no longer the bottleneck. If the solver continues to underperform, perhaps you're suffering from xref:constraints-and-score/performance.adoc#scoreTrap[score traps] or you may benefit from xref:optimization-algorithms/overview.adoc#customMoves[custom moves] @@ -612,7 +612,7 @@ On machines or containers with little or no CPUs, this falls back to the single It is counter-effective to set a `moveThreadCount` that is higher than the number of available CPU cores, -as that will slow down the score calculation speed. +as that will slow down the move evaluation speed. [IMPORTANT] ==== @@ -722,7 +722,7 @@ plug in a <>. [IMPORTANT] ==== A xref:using-timefold-solver/running-the-solver.adoc#logging[logging level] of `debug` or `trace` causes congestion in multi-threaded Partitioned Search -and slows down the xref:constraints-and-score/performance.adoc#scoreCalculationSpeed[score calculation speed]. +and slows down the xref:constraints-and-score/performance.adoc#moveEvaluationSpeed[move evaluation speed]. ==== Just like a `` element, @@ -875,7 +875,7 @@ It is not applicable to Timefold Solver for Python. ==== When a `ConstraintProvider` does an operation for multiple constraints (such as finding all shifts corresponding to an employee), that work can be shared. -This can significantly improve score calculation speed if the repeated operation is computationally expensive: +This can significantly improve move evaluation speed if the repeated operation is computationally expensive: image::enterprise-edition/nodeSharingValueProposition.png[align="center"] diff --git a/docs/src/modules/ROOT/pages/integration/integration.adoc b/docs/src/modules/ROOT/pages/integration/integration.adoc index 8f9f714027..e86ac82b68 100644 --- a/docs/src/modules/ROOT/pages/integration/integration.adoc +++ b/docs/src/modules/ROOT/pages/integration/integration.adoc @@ -1200,14 +1200,14 @@ Understand these guidelines to decide the hardware for a Timefold Solver service *** **Insufficient**: An `OutOfMemoryException` is thrown (often because the Garbage Collector is using more than 98% of the CPU time). *** **Narrow**: The heap buffer for those short-lived instances is too small, therefore the Garbage Collector needs to run more than it would like to, which causes a performance loss. **** Profiling shows that in the heap chart, the used heap space frequently touches the max heap space during solving. It also shows that the Garbage Collector has a significant CPU usage impact. -**** Adding more heap space increases the xref:constraints-and-score/performance.adoc#scoreCalculationSpeed[score calculation speed]. +**** Adding more heap space increases the xref:constraints-and-score/performance.adoc#moveEvaluationSpeed[move evaluation speed]. *** **Plenty**: There is enough heap space. The Garbage Collector is active, but its CPU usage is low. **** Adding more heap space does _not_ increase performance. **** Usually, this is around 300 to 500MB above the dataset size, _regardless of the problem scale_, except with xref:enterprise-edition/enterprise-edition.adoc#nearbySelection[nearby selection] and caching move selector, neither of which are used by default. * **CPU power**: More is better. -** Improving CPU speed directly increases the xref:constraints-and-score/performance.adoc#scoreCalculationSpeed[score calculation speed]. +** Improving CPU speed directly increases the xref:constraints-and-score/performance.adoc#moveEvaluationSpeed[move evaluation speed]. *** If the CPU power is twice as fast, it takes half the time to find the same result. However, this does not guarantee that it finds a better result in the same time, nor that it finds a similar result for a problem twice as big in the same time. *** Increasing CPU power usually does not resolve scaling issues, because planning problems scale exponentially. Power tweaking the solver configuration has far better results for scaling issues than throwing hardware at it. ** During the `solve()` method, the CPU power will max out until it returns @@ -1224,7 +1224,7 @@ or if your xref:optimization-algorithms/overview.adoc#SolverEventListener[Solver * **I/O (network, disk, ...)**: Not used during solving. ** Timefold Solver is not a web server: a solver thread does not block (unlike a servlet thread), each one fully drains a CPU. *** A web server can handle 24 active servlets threads with eight cores without performance loss, because most servlets threads are blocking on I/O. -*** However, 24 active solver threads with eight cores will cause each solver's xref:constraints-and-score/performance.adoc#scoreCalculationSpeed[score calculation speed] to be three times slower, causing a big performance loss. +*** However, 24 active solver threads with eight cores will cause each solver's xref:constraints-and-score/performance.adoc#moveEvaluationSpeed[move evaluation speed] to be three times slower, causing a big performance loss. ** Note that calling any I/O during solving, for example a remote service in your score calculation, causes a huge performance loss because it's called thousands of times per second, so it should complete in microseconds. So no good implementation does that. Keep these guidelines in mind when selecting and configuring the software. diff --git a/docs/src/modules/ROOT/pages/optimization-algorithms/overview.adoc b/docs/src/modules/ROOT/pages/optimization-algorithms/overview.adoc index 800f2e3f46..65a5ccd934 100644 --- a/docs/src/modules/ROOT/pages/optimization-algorithms/overview.adoc +++ b/docs/src/modules/ROOT/pages/optimization-algorithms/overview.adoc @@ -159,7 +159,7 @@ Timefold Solver combines optimization algorithms (metaheuristics, ...) with score calculation by a score calculation engine. This combination is very efficient, because: -* A score calculation engine, is *great for calculating the score* of a solution of a planning problem. +* A score calculation engine is *great for calculating the score* of a solution of a planning problem. It makes it easy and scalable to add additional soft or hard constraints. It does xref:constraints-and-score/performance.adoc#incrementalScoreCalculation[incremental score calculation (deltas)] without any extra code. However it tends to be not suitable to actually find new solutions. @@ -622,6 +622,21 @@ This is useful for benchmarking. Switching xref:using-timefold-solver/running-the-solver.adoc#environmentMode[EnvironmentMode] can heavily impact when this termination ends. +[#moveCountTermination] +==== `MoveCountTermination` + +`MoveCountTermination` terminates when a number of evaluated moves have been reached. +This is useful for benchmarking. + +[source,xml,options="nowrap"] +---- + + 100000 + +---- + +Switching xref:using-timefold-solver/running-the-solver.adoc#environmentMode[EnvironmentMode] can heavily impact when this termination ends. + [#combiningMultipleTerminations] ==== Combining multiple terminations diff --git a/docs/src/modules/ROOT/pages/quickstart/hello-world/hello-world-quickstart.adoc b/docs/src/modules/ROOT/pages/quickstart/hello-world/hello-world-quickstart.adoc index 1a9824d474..5d3a81bdb0 100644 --- a/docs/src/modules/ROOT/pages/quickstart/hello-world/hello-world-quickstart.adoc +++ b/docs/src/modules/ROOT/pages/quickstart/hello-world/hello-world-quickstart.adoc @@ -1002,9 +1002,9 @@ The `info` log shows what Timefold Solver did in those five seconds: [source,options="nowrap"] ---- ... Solving started: time spent (33), best score (-8init/0hard/0soft), environment mode (REPRODUCIBLE), random (JDK with seed 0). -... Construction Heuristic phase (0) ended: time spent (73), best score (0hard/0soft), score calculation speed (459/sec), step total (4). -... Local Search phase (1) ended: time spent (5000), best score (0hard/0soft), score calculation speed (28949/sec), step total (28398). -... Solving ended: time spent (5000), best score (0hard/0soft), score calculation speed (28524/sec), phase total (2), environment mode (REPRODUCIBLE). +... Construction Heuristic phase (0) ended: time spent (73), best score (0hard/0soft), move evaluation speed (459/sec), step total (4). +... Local Search phase (1) ended: time spent (5000), best score (0hard/0soft), move evaluation speed (28949/sec), step total (28398). +... Solving ended: time spent (5000), best score (0hard/0soft), move evaluation speed (28524/sec), phase total (2), environment mode (REPRODUCIBLE). ---- === Test the application @@ -1158,12 +1158,12 @@ For more, see xref:constraints-and-score/score-calculation.adoc#constraintStream === Logging When adding constraints in your `ConstraintProvider`, -keep an eye on the _score calculation speed_ in the `info` log, +keep an eye on the _move evaluation speed_ in the `info` log, after solving for the same amount of time, to assess the performance impact: [source] ---- -... Solving ended: ..., score calculation speed (29455/sec), ... +... Solving ended: ..., move evaluation speed (29455/sec), ... ---- To understand how Timefold Solver is solving your problem internally: diff --git a/docs/src/modules/ROOT/pages/quickstart/quarkus-vehicle-routing/quarkus-vehicle-routing-quickstart.adoc b/docs/src/modules/ROOT/pages/quickstart/quarkus-vehicle-routing/quarkus-vehicle-routing-quickstart.adoc index 0d68b8652c..989028417d 100644 --- a/docs/src/modules/ROOT/pages/quickstart/quarkus-vehicle-routing/quarkus-vehicle-routing-quickstart.adoc +++ b/docs/src/modules/ROOT/pages/quickstart/quarkus-vehicle-routing/quarkus-vehicle-routing-quickstart.adoc @@ -512,9 +512,9 @@ On the server side, the `info` log shows what Timefold Solver did in those five [source,options="nowrap"] ---- ... Solving started: time spent (17), best score (-5init/0hard/0soft), environment mode (REPRODUCIBLE), move thread count (NONE), random (JDK with seed 0). -... Construction Heuristic phase (0) ended: time spent (33), best score (0hard/-18755soft), score calculation speed (2222/sec), step total (5). -... Local Search phase (1) ended: time spent (5000), best score (0hard/-18716soft), score calculation speed (89685/sec), step total (40343). -... Solving ended: time spent (5000), best score (0hard/-18716soft), score calculation speed (89079/sec), phase total (2), environment mode (REPRODUCIBLE), move thread count (NONE). +... Construction Heuristic phase (0) ended: time spent (33), best score (0hard/-18755soft), move evaluation speed (2222/sec), step total (5). +... Local Search phase (1) ended: time spent (5000), best score (0hard/-18716soft), move evaluation speed (89685/sec), step total (40343). +... Solving ended: time spent (5000), best score (0hard/-18716soft), move evaluation speed (89079/sec), phase total (2), environment mode (REPRODUCIBLE), move thread count (NONE). ---- === Test the application @@ -889,12 +889,12 @@ But it does not run a millisecond longer than it strictly must, even on fast mac === Logging When adding constraints in your `ConstraintProvider`, -keep an eye on the _score calculation speed_ in the `info` log, +keep an eye on the _move evaluation speed_ in the `info` log, after solving for the same amount of time, to assess the performance impact: [source] ---- -... Solving ended: ..., score calculation speed (29455/sec), ... +... Solving ended: ..., move evaluation speed (29455/sec), ... ---- To understand how Timefold Solver is solving your problem internally, diff --git a/docs/src/modules/ROOT/pages/quickstart/quarkus/quarkus-quickstart.adoc b/docs/src/modules/ROOT/pages/quickstart/quarkus/quarkus-quickstart.adoc index d298703575..8613cb9793 100644 --- a/docs/src/modules/ROOT/pages/quickstart/quarkus/quarkus-quickstart.adoc +++ b/docs/src/modules/ROOT/pages/quickstart/quarkus/quarkus-quickstart.adoc @@ -532,9 +532,9 @@ On the server side, the `info` log shows what Timefold Solver did in those five [source,options="nowrap"] ---- ... Solving started: time spent (33), best score (-8init/0hard/0soft), environment mode (REPRODUCIBLE), random (JDK with seed 0). -... Construction Heuristic phase (0) ended: time spent (73), best score (0hard/0soft), score calculation speed (459/sec), step total (4). -... Local Search phase (1) ended: time spent (5000), best score (0hard/0soft), score calculation speed (28949/sec), step total (28398). -... Solving ended: time spent (5000), best score (0hard/0soft), score calculation speed (28524/sec), phase total (2), environment mode (REPRODUCIBLE). +... Construction Heuristic phase (0) ended: time spent (73), best score (0hard/0soft), move evaluation speed (459/sec), step total (4). +... Local Search phase (1) ended: time spent (5000), best score (0hard/0soft), move evaluation speed (28949/sec), step total (28398). +... Solving ended: time spent (5000), best score (0hard/0soft), move evaluation speed (28524/sec), phase total (2), environment mode (REPRODUCIBLE). ---- [NOTE] @@ -871,12 +871,12 @@ But it does not run a millisecond longer than it strictly must, even on fast mac === Logging When adding constraints in your `ConstraintProvider`, -keep an eye on the _score calculation speed_ in the `info` log, +keep an eye on the _move evaluation speed_ in the `info` log, after solving for the same amount of time, to assess the performance impact: [source] ---- -... Solving ended: ..., score calculation speed (29455/sec), ... +... Solving ended: ..., move evaluation speed (29455/sec), ... ---- To understand how Timefold Solver is solving your problem internally, diff --git a/docs/src/modules/ROOT/pages/quickstart/spring-boot/spring-boot-quickstart.adoc b/docs/src/modules/ROOT/pages/quickstart/spring-boot/spring-boot-quickstart.adoc index 7ba6a00538..bb628bb86d 100644 --- a/docs/src/modules/ROOT/pages/quickstart/spring-boot/spring-boot-quickstart.adoc +++ b/docs/src/modules/ROOT/pages/quickstart/spring-boot/spring-boot-quickstart.adoc @@ -384,9 +384,9 @@ On the server side, the `info` log shows what Timefold Solver did in those five [source,options="nowrap"] ---- ... Solving started: time spent (33), best score (-8init/0hard/0soft), environment mode (REPRODUCIBLE), random (JDK with seed 0). -... Construction Heuristic phase (0) ended: time spent (73), best score (0hard/0soft), score calculation speed (459/sec), step total (4). -... Local Search phase (1) ended: time spent (5000), best score (0hard/0soft), score calculation speed (28949/sec), step total (28398). -... Solving ended: time spent (5000), best score (0hard/0soft), score calculation speed (28524/sec), phase total (2), environment mode (REPRODUCIBLE). +... Construction Heuristic phase (0) ended: time spent (73), best score (0hard/0soft), move evaluation speed (459/sec), step total (4). +... Local Search phase (1) ended: time spent (5000), best score (0hard/0soft), move evaluation speed (28949/sec), step total (28398). +... Solving ended: time spent (5000), best score (0hard/0soft), move evaluation speed (28524/sec), phase total (2), environment mode (REPRODUCIBLE). ---- [NOTE] @@ -758,12 +758,12 @@ But it does not run a millisecond longer than it strictly must, even on fast mac === Logging When adding constraints in your `ConstraintProvider`, -keep an eye on the _score calculation speed_ in the `info` log, +keep an eye on the _move evaluation speed_ in the `info` log, after solving for the same amount of time, to assess the performance impact: [source] ---- -... Solving ended: ..., score calculation speed (29455/sec), ... +... Solving ended: ..., move evaluation speed (29455/sec), ... ---- To understand how Timefold Solver is solving your problem internally, diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/benchmarking-and-tweaking.adoc b/docs/src/modules/ROOT/pages/using-timefold-solver/benchmarking-and-tweaking.adoc index 84aa00192d..de610908d6 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/benchmarking-and-tweaking.adoc +++ b/docs/src/modules/ROOT/pages/using-timefold-solver/benchmarking-and-tweaking.adoc @@ -595,9 +595,20 @@ Useful for comparing different score calculators and/or constraint implementatio (presuming that the solver configurations do not differ otherwise). Also useful to measure the scalability cost of an extra constraint. +[IMPORTANT] +==== +When improving your score speed, +it's important to note that comparing a configuration +that uses xref:optimization-algorithms/move-selector-reference.adoc#ruinRecreateMoveSelector[Ruin and Recreate] moves +with one that doesn't may not be realistic. +This is because the configuration using "Ruin and Recreate" will likely execute more score calculations than without, +but it doesn't mean it will evaluate more moves. +The Ruin and Recreate recreate step runs a construction heuristic, +which uses greedy logic to find a better location to assign each one of the entities removed with the ruin step. +==== [#benchmarkReportMoveEvaluationSpeedSummary] -=== Move evaluation speed summary (graph And table) +=== Move evaluation speed summary (graph and table) Shows the move evaluation speed: a count per move, per second and per problem scale for each solver configuration. @@ -606,17 +617,6 @@ score calculators and/or constraint implementations (presuming that the solver configurations do not differ otherwise, including the move selector configuration). Also useful to measure the scalability cost of an extra constraint. -[IMPORTANT] -==== -When improving your move evaluation, -it's important to note that comparing a configuration -that uses xref:optimization-algorithms/move-selector-reference.adoc#ruinRecreateMoveSelector[Ruin and Recreate] moves with one that doesn't wouldn't be fair. -This is because the configuration using Ruin and Recreate will likely execute fewer moves than one that doesn't. -On the other hand, the Ruin and Recreate moves will likely calculate the score more times, improving the __score calculation speed__. -The recreate step runs a construction heuristic, -which uses greedy logic to find a better location to assign each one of the entities removed with the ruin step. -==== - [#benchmarkReportTimeSpentSummary] === Time spent summary (graph And table) @@ -797,10 +797,26 @@ The initial high calculation speed is typical during solution initialization: it's far easier to calculate the score of a solution if only a handful planning entities have been initialized, than when all the planning entities are initialized. -After those few seconds of initialization, the evaluation speed is relatively stable, +After the construction heuristic phase, the evaluation speed is relatively stable, apart from an occasional stop-the-world garbage collector disruption. ==== +[#benchmarkReportMoveEvaluationCountPerTypeStastistic] +=== Move evaluation count per move type statistic (graph and CSV) + +To see how many moves are evaluated per move type, add: + +[source,xml,options="nowrap"] +---- + + ... + MOVE_COUNT_PER_TYPE + +---- + +.Evaluation Count per Move Type Summary Statistic +image::using-timefold-solver/benchmarking-and-tweaking/moveCountPerTypeStatistic.png[align="center"] + [#benchmarkReportBestSolutionMutationOverTimeStatistic] === Best solution mutation over time statistic (graph and CSV) diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc b/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc index 1b33e69295..0d565d5d39 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc +++ b/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc @@ -272,7 +272,7 @@ Having multiple planning entity classes directly raises the implementation compl [NOTE] ==== -_Do not create unnecessary planning entity classes._ This leads to difficult `Move` implementations and slower score calculation. +_Do not create unnecessary planning entity classes._ This leads to difficult `Move` implementations and slower move evaluation. For example, do not create a planning entity class to hold the total free time of a teacher, which needs to be kept up to date as the `Lecture` planning entities change. Instead, calculate the free time in the score constraints (or as a <>) and put the result per teacher into a logically inserted score object. diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc b/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc index f88ddb8ca5..81a14c92ee 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc +++ b/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc @@ -287,7 +287,7 @@ but not for slow stepping algorithms (such as Tabu Search). Both cause congestion in xref:enterprise-edition/enterprise-edition.adoc#multithreadedSolving[multi-threaded solving] with most appenders, see below. -In Eclipse, `debug` logging to the console tends to cause congestion with a score calculation speeds above 10 000 per second. +In Eclipse, `debug` logging to the console tends to cause congestion with a move evaluation speeds above 10 000 per second. Nor IntelliJ, nor the Maven command line suffer from this problem. ==== @@ -301,13 +301,13 @@ DEBUG CH step (0), time spent (47), score (-6init/0hard/0soft), selected mov DEBUG CH step (1), time spent (50), score (-4init/0hard/0soft), selected move count (4), picked move ([Physics(1) {null -> Room A}, Physics(1) {null -> MONDAY 09:30}]). DEBUG CH step (2), time spent (51), score (-2init/-1hard/-1soft), selected move count (4), picked move ([Chemistry(2) {null -> Room B}, Chemistry(2) {null -> MONDAY 08:30}]). DEBUG CH step (3), time spent (52), score (-2hard/-1soft), selected move count (4), picked move ([Biology(3) {null -> Room A}, Biology(3) {null -> MONDAY 08:30}]). -INFO Construction Heuristic phase (0) ended: time spent (53), best score (-2hard/-1soft), score calculation speed (1066/sec), step total (4). +INFO Construction Heuristic phase (0) ended: time spent (53), best score (-2hard/-1soft), move evaluation speed (1066/sec), step total (4). DEBUG LS step (0), time spent (56), score (-2hard/0soft), new best score (-2hard/0soft), accepted/selected move count (1/1), picked move (Chemistry(2) {Room B, MONDAY 08:30} <-> Physics(1) {Room A, MONDAY 09:30}). DEBUG LS step (1), time spent (60), score (-2hard/1soft), new best score (-2hard/1soft), accepted/selected move count (1/2), picked move (Math(0) {Room A, MONDAY 08:30} <-> Physics(1) {Room B, MONDAY 08:30}). DEBUG LS step (2), time spent (60), score (-2hard/0soft), best score (-2hard/1soft), accepted/selected move count (1/1), picked move (Math(0) {Room B, MONDAY 08:30} <-> Physics(1) {Room A, MONDAY 08:30}). ... -INFO Local Search phase (1) ended: time spent (100), best score (0hard/1soft), score calculation speed (2021/sec), step total (59). -INFO Solving ended: time spent (100), best score (0hard/1soft), score calculation speed (1100/sec), phase total (2), environment mode (REPRODUCIBLE), move thread count (NONE). +INFO Local Search phase (1) ended: time spent (100), best score (0hard/1soft), move evaluation speed (2021/sec), step total (59). +INFO Solving ended: time spent (100), best score (0hard/1soft), move evaluation speed (1100/sec), phase total (2), environment mode (REPRODUCIBLE), move thread count (NONE). ---- All time spent values are in milliseconds. @@ -519,6 +519,9 @@ Measures the number of errors that occur while solving. - `SCORE_CALCULATION_COUNT` (default, Micrometer meter id: "timefold.solver.score.calculation.count"): Measures the number of score calculations Timefold Solver performed. +- `MOVE_EVALUATION_COUNT` (default, Micrometer meter id: "timefold.solver.move.evaluation.count"): +Measures the number of move evaluations Timefold Solver performed. + - `PROBLEM_ENTITY_COUNT` (default, Micrometer meter id: "timefold.solver.problem.entities"): Measures the number of entities in the problem submitted to Timefold Solver. @@ -547,6 +550,9 @@ Measures the number of changed planning variables between consecutive best solut - `MOVE_COUNT_PER_STEP` (Micrometer meter id: "timefold.solver.step.move.count"): Measures the number of moves evaluated in a step. +- `MOVE_COUNT_PER_TYPE` (Micrometer meter id: "timefold.solver.move.type.count"): +Measures the number of moves evaluated per move type. + - `MEMORY_USE` (Micrometer meter id: "jvm.memory.used"): Measures the amount of memory used across the JVM. This does not measure the amount of memory used by a solver; two solvers on the same JVM will report the same value for this metric. From c7cc9c9053b968b43426b8c65c9a001aac49f513 Mon Sep 17 00:00:00 2001 From: Fred Date: Tue, 24 Sep 2024 16:03:13 -0300 Subject: [PATCH 21/26] fix: fix tests --- .../decider/forager/AcceptedLocalSearchForagerTest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/forager/AcceptedLocalSearchForagerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/forager/AcceptedLocalSearchForagerTest.java index 80ecb0db62..b0e2757e36 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/forager/AcceptedLocalSearchForagerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/localsearch/decider/forager/AcceptedLocalSearchForagerTest.java @@ -4,10 +4,12 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import java.util.EnumSet; import java.util.Random; import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.config.localsearch.decider.forager.LocalSearchPickEarlyType; +import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; import ai.timefold.solver.core.impl.heuristic.move.DummyMove; import ai.timefold.solver.core.impl.localsearch.decider.forager.finalist.HighestScoreFinalistPodium; import ai.timefold.solver.core.impl.localsearch.scope.LocalSearchMoveScope; @@ -219,6 +221,7 @@ private static LocalSearchPhaseScope createPhaseScope() { Random workingRandom = new TestRandom(1, 1); solverScope.setWorkingRandom(workingRandom); solverScope.setBestScore(SimpleScore.of(-10)); + solverScope.setSolverMetricSet(EnumSet.of(SolverMetric.MOVE_EVALUATION_COUNT)); LocalSearchStepScope lastLocalSearchStepScope = new LocalSearchStepScope<>(phaseScope); lastLocalSearchStepScope.setScore(SimpleScore.of(-100)); phaseScope.setLastCompletedStepScope(lastLocalSearchStepScope); From b24d6e909a7e67f14c3dd38b239470c759622929 Mon Sep 17 00:00:00 2001 From: Fred Date: Wed, 25 Sep 2024 11:08:50 -0300 Subject: [PATCH 22/26] chore: addressing PR comments --- .../impl/result/SubSingleBenchmarkResult.java | 16 ++++------------ ...createConstructionHeuristicPhaseBuilder.java | 2 +- .../localsearch/DefaultLocalSearchPhase.java | 2 +- .../solver/core/impl/phase/AbstractPhase.java | 4 ++-- .../impl/phase/scope/AbstractStepScope.java | 7 +------ .../core/impl/solver/scope/SolverScope.java | 15 +++++---------- .../solver/core/impl/util/MathUtils.java | 5 +++++ .../constraints-and-score/performance.adoc | 17 +++++++++-------- .../benchmarking-and-tweaking.adoc | 8 ++++---- .../running-the-solver.adoc | 2 +- 10 files changed, 33 insertions(+), 45 deletions(-) diff --git a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SubSingleBenchmarkResult.java b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SubSingleBenchmarkResult.java index d5699f6344..b0a1ee3cc8 100644 --- a/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SubSingleBenchmarkResult.java +++ b/benchmark/src/main/java/ai/timefold/solver/benchmark/impl/result/SubSingleBenchmarkResult.java @@ -1,5 +1,7 @@ package ai.timefold.solver.benchmark.impl.result; +import static ai.timefold.solver.core.impl.util.MathUtils.getSpeed; + import java.io.File; import java.util.ArrayList; import java.util.HashMap; @@ -218,22 +220,12 @@ public boolean isScoreFeasible() { @SuppressWarnings("unused") // Used by FreeMarker. public Long getScoreCalculationSpeed() { - long timeMillisSpent = this.timeMillisSpent; - if (timeMillisSpent == 0L) { - // Avoid divide by zero exception on a fast CPU - timeMillisSpent = 1L; - } - return scoreCalculationCount * 1000L / timeMillisSpent; + return getSpeed(scoreCalculationCount, this.timeMillisSpent); } @SuppressWarnings("unused") // Used by FreeMarker. public Long getMoveEvaluationSpeed() { - long timeSpent = this.timeMillisSpent; - if (timeSpent == 0L) { - // Avoid divide by zero exception on a fast CPU - timeSpent = 1L; - } - return moveEvaluationCount * 1000L / timeSpent; + return getSpeed(moveEvaluationCount, this.timeMillisSpent); } @SuppressWarnings("unused") // Used by FreeMarker. diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseBuilder.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseBuilder.java index 8c289fbac8..7c90ef5dc5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseBuilder.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseBuilder.java @@ -58,7 +58,7 @@ public EntityPlacer getEntityPlacer() { @Override public DefaultConstructionHeuristicPhase build() { - this.setEnableCollectMetrics(false); + disableMetricCollection(); return new RuinRecreateConstructionHeuristicPhase<>(this); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java index d18f137f26..b954d11ff5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java @@ -130,7 +130,7 @@ public void stepStarted(LocalSearchStepScope stepScope) { public void stepEnded(LocalSearchStepScope stepScope) { super.stepEnded(stepScope); decider.stepEnded(stepScope); - if (stepScope.isPhaseEnableCollectMetrics()) { + if (stepScope.getPhaseScope().isEnableCollectMetrics()) { collectMetrics(stepScope); } LocalSearchPhaseScope phaseScope = stepScope.getPhaseScope(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java index 2bc064d76d..fbd8a6e41d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java @@ -257,8 +257,8 @@ public void setAssertShadowVariablesAreNotStaleAfterStep(boolean assertShadowVar this.assertShadowVariablesAreNotStaleAfterStep = assertShadowVariablesAreNotStaleAfterStep; } - public void setEnableCollectMetrics(boolean enableCollectMetrics) { - this.enableCollectMetrics = enableCollectMetrics; + public void disableMetricCollection() { + this.enableCollectMetrics = false; } protected abstract AbstractPhase build(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractStepScope.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractStepScope.java index 5a5220e05a..f9116bd8b1 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractStepScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractStepScope.java @@ -47,7 +47,7 @@ public void setBestScoreImproved(Boolean bestScoreImproved) { } public void incrementMoveEvaluationCount(Move move) { - if (isPhaseEnableCollectMetrics()) { + if (getPhaseScope().isEnableCollectMetrics()) { getPhaseScope().getSolverScope().addMoveEvaluationCount(1L); if (getPhaseScope().getSolverScope().isMetricEnabled(SolverMetric.MOVE_COUNT_PER_TYPE)) { getPhaseScope().getSolverScope().incrementMoveEvaluationCountPerType(move); @@ -58,11 +58,6 @@ public void incrementMoveEvaluationCount(Move move) { // ************************************************************************ // Calculated methods // ************************************************************************ - - public boolean isPhaseEnableCollectMetrics() { - return getPhaseScope().isEnableCollectMetrics(); - } - public > InnerScoreDirector getScoreDirector() { return getPhaseScope().getScoreDirector(); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java index 0caac1c59d..b2d3d38944 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java @@ -1,6 +1,6 @@ package ai.timefold.solver.core.impl.solver.scope; -import static java.util.stream.Collectors.toMap; +import static ai.timefold.solver.core.impl.util.MathUtils.getSpeed; import java.util.EnumSet; import java.util.List; @@ -70,7 +70,7 @@ public class SolverScope { /** * Used for tracking move count per move type */ - private final Map moveEvaluationCountPerTypeMap = new ConcurrentHashMap<>(); + private final Map moveEvaluationCountPerTypeMap = new ConcurrentHashMap<>(); private static AtomicLong resetAtomicLongTimeMillis(AtomicLong atomicLong) { atomicLong.set(-1); @@ -244,7 +244,7 @@ public Set getMoveCountTypes() { } public Map getMoveEvaluationCountPerType() { - return moveEvaluationCountPerTypeMap.entrySet().stream().collect(toMap(Map.Entry::getKey, e -> e.getValue().get())); + return moveEvaluationCountPerTypeMap; } // ************************************************************************ @@ -314,11 +314,6 @@ public long getMoveEvaluationSpeed() { return getSpeed(getMoveEvaluationCount(), timeMillisSpent); } - public static long getSpeed(long metric, long timeMillisSpent) { - // Avoid divide by zero exception on a fast CPU - return metric * 1000L / (timeMillisSpent == 0L ? 1L : timeMillisSpent); - } - public void setWorkingSolutionFromBestSolution() { // The workingSolution must never be the same instance as the bestSolution. scoreDirector.setWorkingSolution(scoreDirector.cloneSolution(getBestSolution())); @@ -388,9 +383,9 @@ public void incrementMoveEvaluationCountPerType(Move move) { public void addMoveEvaluationCountPerType(String moveType, long count) { moveEvaluationCountPerTypeMap.compute(moveType, (key, counter) -> { if (counter == null) { - counter = new AtomicLong(); + counter = 0L; } - counter.addAndGet(count); + counter += count; return counter; }); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/util/MathUtils.java b/core/src/main/java/ai/timefold/solver/core/impl/util/MathUtils.java index 781e7defa7..6d812b86d9 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/util/MathUtils.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/util/MathUtils.java @@ -48,4 +48,9 @@ public static long getScaledApproximateLog(long scale, long base, long value) { public static double getLogInBase(double base, double value) { return Math.log(value) / Math.log(base); } + + public static long getSpeed(long count, long timeMillisSpent) { + // Avoid divide by zero exception on a fast CPU + return count * 1000L / (timeMillisSpent == 0L ? 1L : timeMillisSpent); + } } diff --git a/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc b/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc index 8a256a9ebc..a81d15364c 100644 --- a/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc +++ b/docs/src/modules/ROOT/pages/constraints-and-score/performance.adoc @@ -6,12 +6,12 @@ The `Solver` will normally spend most of its execution time evaluating moves and running score calculation, which is called in its deepest loops. -Faster move evaluation will return the same solution in less time with the same algorithm, +Faster score calculation will return the same solution in less time with the same algorithm, which normally means a better solution in equal time. [#moveEvaluationSpeed] -== Move evaluation speed +== Move evaluation and score calculation speed After solving a problem, the `Solver` will log the __move evaluation speed per second__. This is a good measurement of Move evaluation performance, @@ -22,14 +22,15 @@ except if you are using xref:constraints-and-score/score-calculation.adoc#easySc [IMPORTANT] ==== -When improving your move evaluation, focus on maximizing the move evaluation speed, +When improving your solver's performance, +focus on maximizing the score calculation speed while keeping the existing moves, instead of maximizing the best score. -A big improvement in move evaluation can sometimes yield little or no best score improvement, +A big improvement in score calculation can sometimes yield little or no best score improvement, for example when the algorithm is stuck in a local or global optima. -If you are watching the move evaluation speed instead, move evaluation improvements are far more visible. +If you are watching the calculation speed instead, score calculation improvements are far more visible. -Furthermore, watching the evaluation speed allows you to remove or add score constraints, -and still compare it with the original's evaluation speed. +Furthermore, watching the calculation speed allows you to remove or add score constraints, +and still compare it with the original's calculation speed. Comparing the best score with the original's best score is pointless: it's comparing apples and oranges. ==== @@ -467,7 +468,7 @@ For example, move multiple items from the same container to another container. == `stepLimit` benchmark Not all score constraints have the same performance cost. -Sometimes one score constraint can kill the move evaluation performance outright. +Sometimes one score constraint can performance outright. Use the xref:using-timefold-solver/benchmarking-and-tweaking.adoc#benchmarker[Benchmarker] to do a one minute run and check what happens to the move evaluation speed if you comment out all but one of the score constraints. \ No newline at end of file diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/benchmarking-and-tweaking.adoc b/docs/src/modules/ROOT/pages/using-timefold-solver/benchmarking-and-tweaking.adoc index de610908d6..bc6f583cf6 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/benchmarking-and-tweaking.adoc +++ b/docs/src/modules/ROOT/pages/using-timefold-solver/benchmarking-and-tweaking.adoc @@ -597,9 +597,9 @@ Also useful to measure the scalability cost of an extra constraint. [IMPORTANT] ==== -When improving your score speed, -it's important to note that comparing a configuration -that uses xref:optimization-algorithms/move-selector-reference.adoc#ruinRecreateMoveSelector[Ruin and Recreate] moves +When improving your score calculation speed, +comparing a configuration that uses +xref:optimization-algorithms/move-selector-reference.adoc#ruinRecreateMoveSelector[Ruin and Recreate] moves with one that doesn't may not be realistic. This is because the configuration using "Ruin and Recreate" will likely execute more score calculations than without, but it doesn't mean it will evaluate more moves. @@ -793,7 +793,7 @@ image::using-timefold-solver/benchmarking-and-tweaking/moveEvaluationSpeedStatis [NOTE] ==== -The initial high calculation speed is typical during solution initialization: +The initial high evaluation speed is typical during solution initialization: it's far easier to calculate the score of a solution if only a handful planning entities have been initialized, than when all the planning entities are initialized. diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc b/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc index 81a14c92ee..146d61c42a 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc +++ b/docs/src/modules/ROOT/pages/using-timefold-solver/running-the-solver.adoc @@ -287,7 +287,7 @@ but not for slow stepping algorithms (such as Tabu Search). Both cause congestion in xref:enterprise-edition/enterprise-edition.adoc#multithreadedSolving[multi-threaded solving] with most appenders, see below. -In Eclipse, `debug` logging to the console tends to cause congestion with a move evaluation speeds above 10 000 per second. +In Eclipse, `debug` logging to the console tends to cause congestion with move evaluation speeds above 10 000 per second. Nor IntelliJ, nor the Maven command line suffer from this problem. ==== From 67179d0c2bf3b2e204e3599b87e6c377bdd68169 Mon Sep 17 00:00:00 2001 From: Fred Date: Wed, 25 Sep 2024 12:17:21 -0300 Subject: [PATCH 23/26] chore: addressing PR comments --- .../solver/core/impl/localsearch/DefaultLocalSearchPhase.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java index b954d11ff5..233b74dcb9 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhase.java @@ -130,9 +130,7 @@ public void stepStarted(LocalSearchStepScope stepScope) { public void stepEnded(LocalSearchStepScope stepScope) { super.stepEnded(stepScope); decider.stepEnded(stepScope); - if (stepScope.getPhaseScope().isEnableCollectMetrics()) { - collectMetrics(stepScope); - } + collectMetrics(stepScope); LocalSearchPhaseScope phaseScope = stepScope.getPhaseScope(); if (logger.isDebugEnabled()) { logger.debug("{} LS step ({}), time spent ({}), score ({}), {} best score ({})," + From 8673c3339545f73524469f2842474eae4f9d9f68 Mon Sep 17 00:00:00 2001 From: Fred Date: Wed, 25 Sep 2024 14:09:46 -0300 Subject: [PATCH 24/26] chore: addressing PR comments --- .../solver/core/impl/phase/scope/AbstractPhaseScope.java | 7 ++++++- .../solver/core/impl/phase/scope/AbstractStepScope.java | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java index e1e316b752..629969e814 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java @@ -93,7 +93,12 @@ public void setBestSolutionStepIndex(int bestSolutionStepIndex) { public abstract AbstractStepScope getLastCompletedStepScope(); - public boolean isEnableCollectMetrics() { + /** + * @return true, if the metrics collection, such as + * {@link ai.timefold.solver.core.config.solver.monitoring.SolverMetric#MOVE_COUNT_PER_TYPE MOVE_COUNT_PER_TYPE}, + * is enabled; false, if the collection is disabled for the phase. + */ + public boolean isMetricCollectionEnabled() { return enableCollectMetrics; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractStepScope.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractStepScope.java index f9116bd8b1..da9ba5f8d7 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractStepScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractStepScope.java @@ -47,7 +47,7 @@ public void setBestScoreImproved(Boolean bestScoreImproved) { } public void incrementMoveEvaluationCount(Move move) { - if (getPhaseScope().isEnableCollectMetrics()) { + if (getPhaseScope().isMetricCollectionEnabled()) { getPhaseScope().getSolverScope().addMoveEvaluationCount(1L); if (getPhaseScope().getSolverScope().isMetricEnabled(SolverMetric.MOVE_COUNT_PER_TYPE)) { getPhaseScope().getSolverScope().incrementMoveEvaluationCountPerType(move); From 0ebfa91ae2de6a9379babbf51e02f8e1fd458b02 Mon Sep 17 00:00:00 2001 From: Fred Date: Wed, 25 Sep 2024 14:29:37 -0300 Subject: [PATCH 25/26] chore: addressing PR comments --- .../solver/core/impl/phase/scope/AbstractPhaseScope.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java index 629969e814..2f4216cb5f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java @@ -96,7 +96,8 @@ public void setBestSolutionStepIndex(int bestSolutionStepIndex) { /** * @return true, if the metrics collection, such as * {@link ai.timefold.solver.core.config.solver.monitoring.SolverMetric#MOVE_COUNT_PER_TYPE MOVE_COUNT_PER_TYPE}, - * is enabled; false, if the collection is disabled for the phase. + * is enabled. + * This is disabled for nested phases, such as Construction heuristics in Ruin and Recreate. */ public boolean isMetricCollectionEnabled() { return enableCollectMetrics; From 71f13279853debf086b74286ed4ce12bc95d1742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Thu, 26 Sep 2024 09:11:06 +0200 Subject: [PATCH 26/26] Don't proliferate the metric switching --- .../DefaultConstructionHeuristicPhase.java | 6 +++- .../DefaultConstructionHeuristicForager.java | 3 +- .../ConstructionHeuristicPhaseScope.java | 8 ++--- .../decider/ExhaustiveSearchDecider.java | 7 ++-- ...uinRecreateConstructionHeuristicPhase.java | 12 +++++++ ...eateConstructionHeuristicPhaseBuilder.java | 1 - ...createConstructionHeuristicPhaseScope.java | 23 ++++++++++++ .../forager/AcceptedLocalSearchForager.java | 2 +- .../solver/core/impl/phase/AbstractPhase.java | 18 ++-------- .../impl/phase/scope/AbstractPhaseScope.java | 35 ++++++++++--------- .../impl/phase/scope/AbstractStepScope.java | 11 ------ .../core/impl/solver/scope/SolverScope.java | 5 --- 12 files changed, 71 insertions(+), 60 deletions(-) create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseScope.java diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java index e25ff1bd66..e59dc4b36c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java @@ -40,7 +40,7 @@ public String getPhaseTypeString() { // ************************************************************************ @Override public void solve(SolverScope solverScope) { - var phaseScope = new ConstructionHeuristicPhaseScope<>(solverScope, phaseIndex); + var phaseScope = buildPhaseScope(solverScope, phaseIndex); phaseStarted(phaseScope); var solutionDescriptor = solverScope.getSolutionDescriptor(); @@ -94,6 +94,10 @@ public void solve(SolverScope solverScope) { phaseEnded(phaseScope); } + protected ConstructionHeuristicPhaseScope buildPhaseScope(SolverScope solverScope, int phaseIndex) { + return new ConstructionHeuristicPhaseScope<>(solverScope, phaseIndex); + } + private void doStep(ConstructionHeuristicStepScope stepScope) { var step = stepScope.getStep(); step.doMoveOnly(stepScope.getScoreDirector()); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/decider/forager/DefaultConstructionHeuristicForager.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/decider/forager/DefaultConstructionHeuristicForager.java index a669527105..a5dc4d7851 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/decider/forager/DefaultConstructionHeuristicForager.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/decider/forager/DefaultConstructionHeuristicForager.java @@ -39,7 +39,8 @@ public void stepEnded(ConstructionHeuristicStepScope stepScope) { @Override public void addMove(ConstructionHeuristicMoveScope moveScope) { selectedMoveCount++; - moveScope.getStepScope().incrementMoveEvaluationCount(moveScope.getMove()); + moveScope.getStepScope().getPhaseScope() + .addMoveEvaluationCount(moveScope.getMove(), 1L); checkPickEarly(moveScope); if (maxScoreMoveScope == null || moveScope.getScore().compareTo(maxScoreMoveScope.getScore()) > 0) { maxScoreMoveScope = moveScope; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/scope/ConstructionHeuristicPhaseScope.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/scope/ConstructionHeuristicPhaseScope.java index 087f70c02e..12c6d4c14d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/scope/ConstructionHeuristicPhaseScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/scope/ConstructionHeuristicPhaseScope.java @@ -7,7 +7,7 @@ /** * @param the solution type, the class with the {@link PlanningSolution} annotation */ -public final class ConstructionHeuristicPhaseScope extends AbstractPhaseScope { +public class ConstructionHeuristicPhaseScope extends AbstractPhaseScope { private ConstructionHeuristicStepScope lastCompletedStepScope; @@ -25,8 +25,4 @@ public void setLastCompletedStepScope(ConstructionHeuristicStepScope this.lastCompletedStepScope = lastCompletedStepScope; } - // ************************************************************************ - // Calculated methods - // ************************************************************************ - -} +} \ No newline at end of file diff --git a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/ExhaustiveSearchDecider.java b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/ExhaustiveSearchDecider.java index 1f8052635e..228903a8d8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/ExhaustiveSearchDecider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/decider/ExhaustiveSearchDecider.java @@ -105,7 +105,8 @@ public void expandNode(ExhaustiveSearchStepScope stepScope) { manualEntityMimicRecorder.setRecordedEntity(expandingNode.getEntity()); int moveIndex = 0; - ExhaustiveSearchLayer moveLayer = stepScope.getPhaseScope().getLayerList().get(expandingNode.getDepth() + 1); + var phaseScope = stepScope.getPhaseScope(); + ExhaustiveSearchLayer moveLayer = phaseScope.getLayerList().get(expandingNode.getDepth() + 1); for (Move move : moveSelector) { ExhaustiveSearchNode moveNode = new ExhaustiveSearchNode(moveLayer, expandingNode); moveIndex++; @@ -114,10 +115,10 @@ public void expandNode(ExhaustiveSearchStepScope stepScope) { // If the original value is null and the variable allows unassigned values, // the move to null must be done too. doMove(stepScope, moveNode); - stepScope.incrementMoveEvaluationCount(move); + phaseScope.addMoveEvaluationCount(move, 1); // TODO in the lowest level (and only in that level) QuitEarly can be useful // No QuitEarly because lower layers might be promising - stepScope.getPhaseScope().getSolverScope().checkYielding(); + phaseScope.getSolverScope().checkYielding(); if (termination.isPhaseTerminated(stepScope.getPhaseScope())) { break; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhase.java index 9d95deea75..9ce2b69aa6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhase.java @@ -4,6 +4,8 @@ import ai.timefold.solver.core.impl.constructionheuristic.DefaultConstructionHeuristicPhase; import ai.timefold.solver.core.impl.constructionheuristic.scope.ConstructionHeuristicPhaseScope; import ai.timefold.solver.core.impl.constructionheuristic.scope.ConstructionHeuristicStepScope; +import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; final class RuinRecreateConstructionHeuristicPhase extends DefaultConstructionHeuristicPhase @@ -13,6 +15,16 @@ final class RuinRecreateConstructionHeuristicPhase super(builder); } + @Override + protected void collectMetrics(AbstractStepScope stepScope) { + // Nested phase doesn't collect metrics. + } + + @Override + protected ConstructionHeuristicPhaseScope buildPhaseScope(SolverScope solverScope, int phaseIndex) { + return new RuinRecreateConstructionHeuristicPhaseScope<>(solverScope, phaseIndex); + } + @Override protected void processWorkingSolutionDuringStep(ConstructionHeuristicStepScope stepScope) { // Ruin and Recreate CH doesn't process the working solution, it is a nested phase. diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseBuilder.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseBuilder.java index 7c90ef5dc5..ccb5bbd0f1 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseBuilder.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseBuilder.java @@ -58,7 +58,6 @@ public EntityPlacer getEntityPlacer() { @Override public DefaultConstructionHeuristicPhase build() { - disableMetricCollection(); return new RuinRecreateConstructionHeuristicPhase<>(this); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseScope.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseScope.java new file mode 100644 index 0000000000..62a187d250 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/RuinRecreateConstructionHeuristicPhaseScope.java @@ -0,0 +1,23 @@ +package ai.timefold.solver.core.impl.heuristic.selector.move.generic; + +import ai.timefold.solver.core.impl.constructionheuristic.scope.ConstructionHeuristicPhaseScope; +import ai.timefold.solver.core.impl.heuristic.move.Move; +import ai.timefold.solver.core.impl.solver.scope.SolverScope; + +final class RuinRecreateConstructionHeuristicPhaseScope extends ConstructionHeuristicPhaseScope { + + public RuinRecreateConstructionHeuristicPhaseScope(SolverScope solverScope, int phaseIndex) { + super(solverScope, phaseIndex); + } + + @Override + public void addChildThreadsMoveEvaluationCount(long addition) { + // Nested phase does not count moves. + } + + @Override + public void addMoveEvaluationCount(Move move, long count) { + // Nested phase does not count moves. + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/forager/AcceptedLocalSearchForager.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/forager/AcceptedLocalSearchForager.java index cdc357d934..fa09d7fe14 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/forager/AcceptedLocalSearchForager.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/decider/forager/AcceptedLocalSearchForager.java @@ -74,7 +74,7 @@ public boolean supportsNeverEndingMoveSelector() { @Override public void addMove(LocalSearchMoveScope moveScope) { selectedMoveCount++; - moveScope.getStepScope().incrementMoveEvaluationCount(moveScope.getMove()); + moveScope.getStepScope().getPhaseScope().addMoveEvaluationCount(moveScope.getMove(), 1); if (moveScope.getAccepted()) { acceptedMoveCount++; checkPickEarly(moveScope); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java index fbd8a6e41d..eaaa016260 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/AbstractPhase.java @@ -34,8 +34,6 @@ public abstract class AbstractPhase implements Phase { protected final boolean assertShadowVariablesAreNotStaleAfterStep; protected final boolean triggerFirstInitializedSolutionEvent; - protected final boolean enableCollectMetrics; - /** Used for {@link #addPhaseLifecycleListener(PhaseLifecycleListener)}. */ protected PhaseLifecycleSupport phaseLifecycleSupport = new PhaseLifecycleSupport<>(); @@ -48,7 +46,6 @@ protected AbstractPhase(Builder builder) { assertStepScoreFromScratch = builder.assertStepScoreFromScratch; assertExpectedStepScore = builder.assertExpectedStepScore; assertShadowVariablesAreNotStaleAfterStep = builder.assertShadowVariablesAreNotStaleAfterStep; - enableCollectMetrics = builder.enableCollectMetrics; triggerFirstInitializedSolutionEvent = builder.triggerFirstInitializedSolutionEvent; } @@ -106,7 +103,6 @@ public void solvingEnded(SolverScope solverScope) { @Override public void phaseStarted(AbstractPhaseScope phaseScope) { phaseScope.startingNow(); - phaseScope.setEnableCollectMetrics(enableCollectMetrics); phaseScope.reset(); solver.phaseStarted(phaseScope); phaseTermination.phaseStarted(phaseScope); @@ -160,15 +156,13 @@ protected > void predictWorkingStepScore(AbstractSt @Override public void stepEnded(AbstractStepScope stepScope) { solver.stepEnded(stepScope); - if (enableCollectMetrics) { - collectMetrics(stepScope); - } + collectMetrics(stepScope); phaseTermination.stepEnded(stepScope); phaseLifecycleSupport.fireStepEnded(stepScope); } - private void collectMetrics(AbstractStepScope stepScope) { - SolverScope solverScope = stepScope.getPhaseScope().getSolverScope(); + protected void collectMetrics(AbstractStepScope stepScope) { + var solverScope = stepScope.getPhaseScope().getSolverScope(); if (solverScope.isMetricEnabled(SolverMetric.STEP_SCORE) && stepScope.getScore().isSolutionInitialized()) { SolverMetric.registerScoreMetrics(SolverMetric.STEP_SCORE, solverScope.getMonitoringTags(), @@ -231,8 +225,6 @@ protected abstract static class Builder { private boolean assertExpectedStepScore = false; private boolean assertShadowVariablesAreNotStaleAfterStep = false; - private boolean enableCollectMetrics = true; - protected Builder(int phaseIndex, String logIndentation, Termination phaseTermination) { this(phaseIndex, false, logIndentation, phaseTermination); } @@ -257,10 +249,6 @@ public void setAssertShadowVariablesAreNotStaleAfterStep(boolean assertShadowVar this.assertShadowVariablesAreNotStaleAfterStep = assertShadowVariablesAreNotStaleAfterStep; } - public void disableMetricCollection() { - this.enableCollectMetrics = false; - } - protected abstract AbstractPhase build(); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java index 2f4216cb5f..0077250e87 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractPhaseScope.java @@ -4,7 +4,9 @@ import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.heuristic.move.Move; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.solver.scope.SolverScope; @@ -34,8 +36,6 @@ public abstract class AbstractPhaseScope { protected int bestSolutionStepIndex; - protected boolean enableCollectMetrics = true; - /** * As defined by #AbstractPhaseScope(SolverScope, int, boolean) * with the phaseSendingBestSolutionEvents parameter set to true. @@ -93,20 +93,6 @@ public void setBestSolutionStepIndex(int bestSolutionStepIndex) { public abstract AbstractStepScope getLastCompletedStepScope(); - /** - * @return true, if the metrics collection, such as - * {@link ai.timefold.solver.core.config.solver.monitoring.SolverMetric#MOVE_COUNT_PER_TYPE MOVE_COUNT_PER_TYPE}, - * is enabled. - * This is disabled for nested phases, such as Construction heuristics in Ruin and Recreate. - */ - public boolean isMetricCollectionEnabled() { - return enableCollectMetrics; - } - - public void setEnableCollectMetrics(boolean enableCollectMetrics) { - this.enableCollectMetrics = enableCollectMetrics; - } - // ************************************************************************ // Calculated methods // ************************************************************************ @@ -159,6 +145,23 @@ public void addChildThreadsMoveEvaluationCount(long addition) { childThreadsMoveEvaluationCount += addition; } + public void addMoveEvaluationCount(Move move, long count) { + solverScope.addMoveEvaluationCount(1); + addMoveEvaluationCountPerType(move, count); + } + + public void addMoveEvaluationCountPerType(Move move, long count) { + if (solverScope.isMetricEnabled(SolverMetric.MOVE_COUNT_PER_TYPE)) { + solverScope.addMoveEvaluationCountPerType(move.getSimpleMoveTypeDescription(), count); + } + } + + public void addMoveEvaluationCountPerType(String moveDescription, long count) { + if (solverScope.isMetricEnabled(SolverMetric.MOVE_COUNT_PER_TYPE)) { + solverScope.addMoveEvaluationCountPerType(moveDescription, count); + } + } + public long getPhaseScoreCalculationCount() { return endingScoreCalculationCount - startingScoreCalculationCount + childThreadsScoreCalculationCount; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractStepScope.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractStepScope.java index da9ba5f8d7..61896a0dc5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractStepScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/scope/AbstractStepScope.java @@ -4,8 +4,6 @@ import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; -import ai.timefold.solver.core.impl.heuristic.move.Move; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; /** @@ -46,15 +44,6 @@ public void setBestScoreImproved(Boolean bestScoreImproved) { this.bestScoreImproved = bestScoreImproved; } - public void incrementMoveEvaluationCount(Move move) { - if (getPhaseScope().isMetricCollectionEnabled()) { - getPhaseScope().getSolverScope().addMoveEvaluationCount(1L); - if (getPhaseScope().getSolverScope().isMetricEnabled(SolverMetric.MOVE_COUNT_PER_TYPE)) { - getPhaseScope().getSolverScope().incrementMoveEvaluationCountPerType(move); - } - } - } - // ************************************************************************ // Calculated methods // ************************************************************************ diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java index b2d3d38944..ab4ecc3589 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java @@ -18,7 +18,6 @@ import ai.timefold.solver.core.api.solver.Solver; import ai.timefold.solver.core.config.solver.monitoring.SolverMetric; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; -import ai.timefold.solver.core.impl.heuristic.move.Move; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; @@ -376,10 +375,6 @@ public void destroyYielding() { } } - public void incrementMoveEvaluationCountPerType(Move move) { - addMoveEvaluationCountPerType(move.getSimpleMoveTypeDescription(), 1L); - } - public void addMoveEvaluationCountPerType(String moveType, long count) { moveEvaluationCountPerTypeMap.compute(moveType, (key, counter) -> { if (counter == null) {