From 06d2c82fd17b2a2cef5bda940745c4407b015870 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Thu, 7 Nov 2024 14:17:04 +0100 Subject: [PATCH] feat: enable score analysis without justifications (#1185) Introduces an option to disable constraint match justifications, while still keeping constraint matching enabled. In some cases, the dataset is so large that justifications are pointless, and impractical to send over the wire as well. In this case, score analysis only provides constraint match count, and not the full match analysis. --- .../impl/SubSingleBenchmarkRunner.java | 3 +- .../core/api/score/ScoreExplanation.java | 1 + .../score/analysis/ConstraintAnalysis.java | 43 +++-- .../api/score/analysis/MatchAnalysis.java | 11 +- .../api/score/constraint/ConstraintMatch.java | 19 ++- .../score/stream/ConstraintJustification.java | 2 +- .../api/solver/ScoreAnalysisFetchPolicy.java | 15 +- .../core/api/solver/SolutionManager.java | 2 +- .../localsearch/DefaultLocalSearchPhase.java | 2 +- .../impl/score/DefaultScoreExplanation.java | 24 ++- .../constraint/ConstraintMatchPolicy.java | 53 ++++++ .../DefaultConstraintMatchTotal.java | 10 +- .../score/constraint/DefaultIndictment.java | 10 +- .../score/director/AbstractScoreDirector.java | 36 ++-- .../AbstractScoreDirectorFactory.java | 8 +- .../score/director/InnerScoreDirector.java | 89 +++++----- .../director/InnerScoreDirectorFactory.java | 47 ++---- .../score/director/ScoreDirectorFactory.java | 15 +- .../director/easy/EasyScoreDirector.java | 15 +- .../easy/EasyScoreDirectorFactory.java | 9 +- .../incremental/IncrementalScoreDirector.java | 43 ++--- .../IncrementalScoreDirectorFactory.java | 6 +- .../BavetConstraintStreamScoreDirector.java | 25 +-- ...tConstraintStreamScoreDirectorFactory.java | 16 +- .../stream/bavet/BavetConstraintFactory.java | 4 +- .../stream/bavet/BavetConstraintSession.java | 4 +- .../bavet/BavetConstraintSessionFactory.java | 5 +- .../bi/BavetScoringBiConstraintStream.java | 37 ++++- .../common/BavetScoringConstraintStream.java | 23 +++ .../BavetScoringQuadConstraintStream.java | 37 ++++- .../tri/BavetScoringTriConstraintStream.java | 37 ++++- .../uni/BavetScoringUniConstraintStream.java | 37 ++++- ...tConstraintStreamScoreDirectorFactory.java | 3 +- .../common/inliner/AbstractScoreInliner.java | 69 ++++---- .../BendableBigDecimalScoreContext.java | 6 +- .../BendableBigDecimalScoreInliner.java | 7 +- .../inliner/BendableLongScoreContext.java | 6 +- .../inliner/BendableLongScoreInliner.java | 6 +- .../common/inliner/BendableScoreContext.java | 6 +- .../common/inliner/BendableScoreInliner.java | 6 +- .../inliner/ConstraintMatchSupplier.java | 12 +- .../HardMediumSoftBigDecimalScoreContext.java | 8 +- .../HardMediumSoftBigDecimalScoreInliner.java | 5 +- .../HardMediumSoftLongScoreContext.java | 8 +- .../HardMediumSoftLongScoreInliner.java | 5 +- .../inliner/HardMediumSoftScoreContext.java | 8 +- .../inliner/HardMediumSoftScoreInliner.java | 6 +- .../HardSoftBigDecimalScoreContext.java | 6 +- .../HardSoftBigDecimalScoreInliner.java | 5 +- .../inliner/HardSoftLongScoreContext.java | 6 +- .../inliner/HardSoftLongScoreInliner.java | 6 +- .../common/inliner/HardSoftScoreContext.java | 6 +- .../common/inliner/HardSoftScoreInliner.java | 5 +- .../stream/common/inliner/ScoreContext.java | 9 +- .../inliner/SimpleBigDecimalScoreContext.java | 2 +- .../inliner/SimpleBigDecimalScoreInliner.java | 6 +- .../inliner/SimpleLongScoreContext.java | 2 +- .../inliner/SimpleLongScoreInliner.java | 5 +- .../common/inliner/SimpleScoreContext.java | 2 +- .../common/inliner/SimpleScoreInliner.java | 5 +- .../solver/core/impl/solver/Assigner.java | 4 +- .../core/impl/solver/AssignmentProcessor.java | 2 +- .../impl/solver/DefaultSolutionManager.java | 20 ++- .../impl/solver/DefaultSolverFactory.java | 4 +- .../api/score/analysis/ScoreAnalysisTest.java | 154 +++++++++++++++--- .../core/api/solver/SolverFactoryTest.java | 67 ++++---- .../ConstraintWeightOverridesTest.java | 3 +- .../selector/move/generic/ChangeMoveTest.java | 4 +- .../move/generic/PillarChangeMoveTest.java | 4 +- .../move/generic/PillarSwapMoveTest.java | 4 +- .../selector/move/generic/SwapMoveTest.java | 4 +- .../AbstractScoreDirectorSemanticsTest.java | 15 +- .../ScoreDirectorFactoryFactoryTest.java | 7 +- .../easy/EasyScoreDirectorFactoryTest.java | 3 +- .../easy/EasyScoreDirectorSemanticsTest.java | 4 +- .../director/easy/EasyScoreDirectorTest.java | 77 +++------ .../IncrementalScoreDirectorTest.java | 17 +- ...etAdvancedGroupByConstraintStreamTest.java | 5 +- .../BavetConstraintStreamImplSupport.java | 19 +-- .../stream/bavet/BavetRegressionTest.java | 5 +- ...avetBiConstraintStreamNodeSharingTest.java | 5 +- .../bavet/bi/BavetBiConstraintStreamTest.java | 5 +- ...etQuadConstraintStreamNodeSharingTest.java | 5 +- .../quad/BavetQuadConstraintStreamTest.java | 5 +- ...vetTriConstraintStreamNodeSharingTest.java | 5 +- .../tri/BavetTriConstraintStreamTest.java | 5 +- ...vetUniConstraintStreamNodeSharingTest.java | 5 +- .../uni/BavetUniConstraintStreamTest.java | 5 +- .../common/AbstractConstraintStreamTest.java | 35 ++-- .../common/ConstraintStreamImplSupport.java | 3 +- .../common/ConstraintStreamTestExtension.java | 13 +- .../bi/AbstractBiConstraintStreamTest.java | 4 +- .../inliner/AbstractScoreInlinerTest.java | 7 +- .../BendableBigDecimalScoreInlinerTest.java | 8 +- .../inliner/BendableLongScoreInlinerTest.java | 7 +- .../inliner/BendableScoreInlinerTest.java | 7 +- ...dMediumSoftBigDecimalScoreInlinerTest.java | 7 +- .../HardMediumSoftLongScoreInlinerTest.java | 8 +- .../HardMediumSoftScoreInlinerTest.java | 8 +- .../HardSoftBigDecimalScoreInlinerTest.java | 8 +- .../inliner/HardSoftLongScoreInlinerTest.java | 7 +- .../inliner/HardSoftScoreInlinerTest.java | 7 +- .../SimpleBigDecimalScoreInlinerTest.java | 8 +- .../inliner/SimpleLongScoreInlinerTest.java | 7 +- .../inliner/SimpleScoreInlinerTest.java | 7 +- .../AbstractQuadConstraintStreamTest.java | 4 +- .../tri/AbstractTriConstraintStreamTest.java | 4 +- .../uni/AbstractUniConstraintStreamTest.java | 4 +- .../impl/testdata/util/PlannerTestUtils.java | 3 +- .../understanding-the-score.adoc | 16 +- .../upgrade-to-latest-version.adoc | 21 +++ ...tractScoreAnalysisJacksonDeserializer.java | 9 +- .../ScoreAnalysisJacksonSerializer.java | 3 + .../api/TimefoldJacksonModuleTest.java | 32 ++-- .../DefaultMultiConstraintVerification.java | 3 +- .../DefaultSingleConstraintVerification.java | 3 +- 116 files changed, 1047 insertions(+), 582 deletions(-) create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchPolicy.java 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 7e9d6435a6..1d3555550d 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 @@ -128,7 +128,8 @@ public SubSingleBenchmarkRunner call() { subSingleBenchmarkResult.setMoveEvaluationCount(solverScope.getMoveEvaluationCount()); SolutionManager solutionManager = SolutionManager.create(solverFactory); - boolean isConstraintMatchEnabled = solver.getSolverScope().getScoreDirector().isConstraintMatchEnabled(); + boolean isConstraintMatchEnabled = solver.getSolverScope().getScoreDirector().getConstraintMatchPolicy() + .isEnabled(); if (isConstraintMatchEnabled) { // Easy calculator fails otherwise. ScoreExplanation scoreExplanation = solutionManager.explain(solution, SolutionUpdatePolicy.NO_UPDATE); diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/ScoreExplanation.java b/core/src/main/java/ai/timefold/solver/core/api/score/ScoreExplanation.java index 28091328e6..05c60a916f 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/ScoreExplanation.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/ScoreExplanation.java @@ -100,6 +100,7 @@ public interface ScoreExplanation> { * Explains the {@link Score} of {@link #getScore()} for all constraints * justified with a given {@link ConstraintJustification} type. * Otherwise, as defined by {@link #getJustificationList()}. + * May be empty, if the score explanation ran with justification support disabled. * * @return all constraint matches associated with the given justification class * @see #getIndictmentMap() diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ConstraintAnalysis.java b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ConstraintAnalysis.java index 21ffc24a63..9a717545bf 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ConstraintAnalysis.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ConstraintAnalysis.java @@ -31,15 +31,16 @@ * non-empty if constraint has matches. * This is a {@link List} to simplify access to individual elements, * but it contains no duplicates just like {@link HashSet} wouldn't. + * @param matchCount -1 if analysis not available, + * 0 if constraint has no matches, + * positive if constraint has matches. */ public record ConstraintAnalysis>(@NonNull ConstraintRef constraintRef, @NonNull Score_ weight, - @NonNull Score_ score, @Nullable List> matches) { + @NonNull Score_ score, @Nullable List> matches, int matchCount) { - static > @NonNull ConstraintAnalysis of( - @NonNull ConstraintRef constraintRef, - @NonNull Score_ constraintWeight, - @NonNull Score_ score) { - return new ConstraintAnalysis<>(constraintRef, constraintWeight, score, null); + public ConstraintAnalysis(@NonNull ConstraintRef constraintRef, @NonNull Score_ weight, @NonNull Score_ score, + @Nullable List> matches) { + this(constraintRef, weight, score, matches, matches == null ? -1 : matches.size()); } public ConstraintAnalysis { @@ -55,26 +56,14 @@ public record ConstraintAnalysis>(@NonNull Constrai .formatted(DefaultConstraintMatchTotal.class.getSimpleName(), ConstraintMatchAwareIncrementalScoreCalculator.class.getSimpleName())); Objects.requireNonNull(score); - } - - /** - * Return the match count of the constraint. - * - * @throws IllegalStateException if the {@link ConstraintAnalysis#matches()} is null - */ - public int matchCount() { - if (matches == null) { - throw new IllegalArgumentException(""" - The constraint matches must be non-null. - Maybe use ScoreAnalysisFetchPolicy.FETCH_ALL to request the score analysis - """); + if (matches != null && matchCount != matches.size()) { + throw new IllegalArgumentException("The match count must be equal to the size of the matches list."); } - return matches.size(); } ConstraintAnalysis negate() { if (matches == null) { - return ConstraintAnalysis.of(constraintRef, weight.negate(), score.negate()); + return new ConstraintAnalysis<>(constraintRef, weight.negate(), score.negate(), null, matchCount); } else { var negatedMatchAnalyses = matches.stream() .map(MatchAnalysis::negate) @@ -102,14 +91,22 @@ static > ConstraintAnalysis diff( var otherMatchAnalyses = otherConstraintAnalysis.matches(); if ((matchAnalyses == null && otherMatchAnalyses != null) || (matchAnalyses != null && otherMatchAnalyses == null)) { throw new IllegalStateException( - "Impossible state: Only one of the score analyses (%s, %s) provided match analyses for a constraint (%s)." + "Impossible state: One of the score analyses (%s, %s) provided no match analysis for a constraint (%s)." .formatted(constraintAnalysis, otherConstraintAnalysis, constraintRef)); } // Compute the diff. var constraintWeightDifference = constraintAnalysis.weight().subtract(otherConstraintAnalysis.weight()); var scoreDifference = constraintAnalysis.score().subtract(otherConstraintAnalysis.score()); if (matchAnalyses == null) { - return ConstraintAnalysis.of(constraintRef, constraintWeightDifference, scoreDifference); + var leftCount = constraintAnalysis.matchCount(); + var rightCount = otherConstraintAnalysis.matchCount(); + if ((leftCount == -1 && rightCount != -1) || (leftCount != -1 && rightCount == -1)) { + throw new IllegalStateException( + "Impossible state: One of the score analyses (%s, %s) provided no match count for a constraint (%s)." + .formatted(constraintAnalysis, otherConstraintAnalysis, constraintRef)); + } + return new ConstraintAnalysis<>(constraintRef, constraintWeightDifference, scoreDifference, null, + leftCount - rightCount); } var matchAnalysisMap = mapMatchesToJustifications(matchAnalyses); var otherMatchAnalysisMap = mapMatchesToJustifications(otherMatchAnalyses); diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/MatchAnalysis.java b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/MatchAnalysis.java index 7f9acdfe4f..f38827661f 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/MatchAnalysis.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/MatchAnalysis.java @@ -22,8 +22,10 @@ public record MatchAnalysis>(@NonNull ConstraintRef public MatchAnalysis { Objects.requireNonNull(constraintRef); Objects.requireNonNull(score); - Objects.requireNonNull(justification, """ - Received a null justification. + // Null justification is impossible; + // if the fetch policy doesn't requre match analysis, the code shouldn't even get here. + Objects.requireNonNull(justification, () -> """ + Impossible state: Received a null justification. Maybe check your %s's justifyWith() implementation for that constraint?""" .formatted(ConstraintProvider.class)); } @@ -42,8 +44,9 @@ public int compareTo(MatchAnalysis other) { if (scoreComparison != 0) { return scoreComparison; } else { - if (this.justification instanceof Comparable && other.justification instanceof Comparable) { - return ((Comparable) this.justification).compareTo(other.justification); + if (this.justification instanceof Comparable comparableJustification + && other.justification instanceof Comparable otherComparableJustification) { + return comparableJustification.compareTo(otherComparableJustification); } else { return 0; } diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/constraint/ConstraintMatch.java b/core/src/main/java/ai/timefold/solver/core/api/score/constraint/ConstraintMatch.java index c3d1a445a3..91b90549c7 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/constraint/ConstraintMatch.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/constraint/ConstraintMatch.java @@ -12,6 +12,7 @@ import ai.timefold.solver.core.api.solver.SolutionManager; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; /** * Retrievable from {@link ConstraintMatchTotal#getConstraintMatchSet()} @@ -85,10 +86,16 @@ public ConstraintMatch(String constraintId, String constraintPackage, String con this(new ConstraintRef(constraintPackage, constraintName, constraintId), justification, indictedObjectList, score); } - public ConstraintMatch(@NonNull ConstraintRef constraintRef, @NonNull ConstraintJustification justification, + /** + * @param constraintRef unique identifier of the constraint + * @param justification only null if justifications are disabled + * @param indictedObjectList never null, empty if justifications are disabled + * @param score penalty or reward associated with the constraint match + */ + public ConstraintMatch(@NonNull ConstraintRef constraintRef, @Nullable ConstraintJustification justification, @NonNull Collection indictedObjectList, @NonNull Score_ score) { this.constraintRef = requireNonNull(constraintRef); - this.justification = requireNonNull(justification); + this.justification = justification; this.indictedObjectList = requireNonNull(indictedObjectList) instanceof List list ? list : List.copyOf(indictedObjectList); this.score = requireNonNull(score); @@ -160,9 +167,10 @@ public List getJustificationList() { * (eg. [A, B] for a bi stream), unless a custom justification mapping was provided, * in which case it returns the return value of that function. *
  • For incremental score calculation, it returns what the calculator is implemented to return.
  • + *
  • It may return null, if justification support was disabled altogether.
  • * */ - public @NonNull Justification_ getJustification() { + public @Nullable Justification_ getJustification() { return (Justification_) justification; } @@ -175,6 +183,7 @@ public List getJustificationList() { * (eg. [A, B] for a bi stream), unless a custom indictment mapping was provided, * in which case it returns the return value of that function. *
  • For incremental score calculation, it returns what the calculator is implemented to return.
  • + *
  • It may return an empty list, if justification support was disabled altogether.
  • * * * @return may be empty or contain null @@ -201,6 +210,10 @@ public int compareTo(ConstraintMatch other) { return constraintRef.compareTo(other.constraintRef); } else if (!score.equals(other.score)) { return score.compareTo(other.score); + } else if (justification == null) { + return other.justification == null ? 0 : -1; + } else if (other.justification == null) { + return 1; } else if (justification instanceof Comparable comparable) { return comparable.compareTo(other.justification); } diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintJustification.java b/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintJustification.java index 6e74858715..9bb5e8b68f 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintJustification.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/stream/ConstraintJustification.java @@ -14,7 +14,7 @@ * All classes used as constraint justifications must implement this interface. * *

    - * Implementing classes ("implementations") may decide to implement {@link Comparable} + * Implementations may decide to implement {@link Comparable} * to preserve order of instances when displayed in user interfaces, logs etc. * This is entirely optional. * diff --git a/core/src/main/java/ai/timefold/solver/core/api/solver/ScoreAnalysisFetchPolicy.java b/core/src/main/java/ai/timefold/solver/core/api/solver/ScoreAnalysisFetchPolicy.java index 0a68bfbc03..73ee7b0def 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/solver/ScoreAnalysisFetchPolicy.java +++ b/core/src/main/java/ai/timefold/solver/core/api/solver/ScoreAnalysisFetchPolicy.java @@ -5,7 +5,7 @@ /** * Determines the depth of {@link SolutionManager#analyze(Object) score analysis}. - * If unsure, pick {@link #FETCH_ALL}. + * If unsure, pick {@link #FETCH_MATCH_COUNT}. * */ public enum ScoreAnalysisFetchPolicy { @@ -17,9 +17,18 @@ public enum ScoreAnalysisFetchPolicy { FETCH_ALL, /** * {@link ConstraintAnalysis} included in {@link ScoreAnalysis} - * does not include {@link ConstraintAnalysis#matches() match analysis}. + * provides neither {@link ConstraintAnalysis#matches() match analysis} + * nor {@link ConstraintAnalysis#matchCount() match count}. * This is useful for performance reasons when the match analysis is not needed. */ - FETCH_SHALLOW + FETCH_SHALLOW, + /** + * {@link ConstraintAnalysis} included in {@link ScoreAnalysis} + * does not provide {@link ConstraintAnalysis#matches() match analysis}, + * but does provide {@link ConstraintAnalysis#matchCount() match count}. + * This is useful when there are too many matches to send over the wire + * or meaningfully present to users. + */ + FETCH_MATCH_COUNT } diff --git a/core/src/main/java/ai/timefold/solver/core/api/solver/SolutionManager.java b/core/src/main/java/ai/timefold/solver/core/api/solver/SolutionManager.java index 52a15e05c4..0b972f2f99 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/solver/SolutionManager.java +++ b/core/src/main/java/ai/timefold/solver/core/api/solver/SolutionManager.java @@ -126,7 +126,7 @@ ScoreExplanation explain(@NonNull Solution_ solution, * This is a faster, JSON-friendly version of {@link #explain(Object)}. * * @param solution must be fully initialized otherwise an exception is thrown - * @param fetchPolicy if unsure, pick {@link ScoreAnalysisFetchPolicy#FETCH_ALL} + * @param fetchPolicy if unsure, pick {@link ScoreAnalysisFetchPolicy#FETCH_MATCH_COUNT} * @param solutionUpdatePolicy if unsure, pick {@link SolutionUpdatePolicy#UPDATE_ALL} * @throws IllegalStateException when constraint matching is disabled or not supported by the underlying score * calculator, such as {@link EasyScoreCalculator}. 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 ac12d9a879..51cce2ad12 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 @@ -155,7 +155,7 @@ private void collectMetrics(LocalSearchStepScope stepScope) { || solverScope.isMetricEnabled(SolverMetric.CONSTRAINT_MATCH_TOTAL_BEST_SCORE)) { InnerScoreDirector scoreDirector = stepScope.getScoreDirector(); ScoreDefinition scoreDefinition = solverScope.getScoreDefinition(); - if (scoreDirector.isConstraintMatchEnabled()) { + if (scoreDirector.getConstraintMatchPolicy().isEnabled()) { for (ConstraintMatchTotal constraintMatchTotal : scoreDirector.getConstraintMatchTotalMap() .values()) { Tags tags = solverScope.getMonitoringTags().and( diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/DefaultScoreExplanation.java b/core/src/main/java/ai/timefold/solver/core/impl/score/DefaultScoreExplanation.java index e2732d2930..112f0b72f8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/DefaultScoreExplanation.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/DefaultScoreExplanation.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; @@ -62,10 +63,18 @@ Explanation of score (%s): constraintMatchSet.stream() .sorted(constraintMatchComparator) .limit(constraintMatchLimit) - .forEach(constraintMatch -> scoreExplanation.append(""" - %s: justified with (%s) - """.formatted(constraintMatch.getScore().toShortString(), - constraintMatch.getJustification()))); + .forEach(constraintMatch -> { + if (constraintMatch.getJustification() == null) { + scoreExplanation.append(""" + %s: unjustified + """.formatted(constraintMatch.getScore().toShortString())); + } else { + scoreExplanation.append(""" + %s: justified with (%s) + """.formatted(constraintMatch.getScore().toShortString(), + constraintMatch.getJustification())); + } + }); if (constraintMatchSet.size() > constraintMatchLimit) { scoreExplanation.append(""" ... @@ -131,10 +140,13 @@ public DefaultScoreExplanation(Solution_ solution, Score_ score, for (ConstraintMatchTotal constraintMatchTotal : constraintMatchTotalMap.values()) { for (ConstraintMatch constraintMatch : constraintMatchTotal.getConstraintMatchSet()) { ConstraintJustification justification = constraintMatch.getJustification(); - workingConstraintJustificationList.add(justification); + if (justification != null) { + workingConstraintJustificationList.add(justification); + } } } - this.constraintJustificationList = workingConstraintJustificationList; + this.constraintJustificationList = + workingConstraintJustificationList.isEmpty() ? Collections.emptyList() : workingConstraintJustificationList; this.indictmentMap = indictmentMap; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchPolicy.java b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchPolicy.java new file mode 100644 index 0000000000..bac2a9ed77 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchPolicy.java @@ -0,0 +1,53 @@ +package ai.timefold.solver.core.impl.score.constraint; + +import ai.timefold.solver.core.api.solver.ScoreAnalysisFetchPolicy; + +import org.jspecify.annotations.NullMarked; + +/** + * Determines whether constraint match is enabled and whether constraint match justification is enabled. + * + * @see ai.timefold.solver.core.api.score.constraint.ConstraintMatch + * @see ai.timefold.solver.core.api.score.stream.ConstraintJustification + */ +@NullMarked +public enum ConstraintMatchPolicy { + + DISABLED(false, false), + ENABLED_WITHOUT_JUSTIFICATIONS(true, false), + ENABLED(true, true); + + /** + * To achieve the most performance out of the underlying solver, + * the policy should match whatever policy was used for score analysis. + * For example, if the fetch policy specifies that only match counts are necessary and not matches themselves + * ({@link ScoreAnalysisFetchPolicy#FETCH_MATCH_COUNT}), + * we can configure the solver to not produce justifications ({@link #ENABLED_WITHOUT_JUSTIFICATIONS}). + * + * @param scoreAnalysisFetchPolicy + * @return Match policy best suited for the given fetch policy. + */ + public static ConstraintMatchPolicy match(ScoreAnalysisFetchPolicy scoreAnalysisFetchPolicy) { + return switch (scoreAnalysisFetchPolicy) { + case FETCH_MATCH_COUNT, FETCH_SHALLOW -> ENABLED_WITHOUT_JUSTIFICATIONS; + case FETCH_ALL -> ENABLED; + }; + } + + private final boolean enabled; + private final boolean justificationEnabled; + + ConstraintMatchPolicy(boolean enabled, boolean justificationEnabled) { + this.enabled = enabled; + this.justificationEnabled = justificationEnabled; + } + + public boolean isEnabled() { + return enabled; + } + + public boolean isJustificationEnabled() { + return justificationEnabled; + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/DefaultConstraintMatchTotal.java b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/DefaultConstraintMatchTotal.java index cc2297bede..fdefddf85d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/DefaultConstraintMatchTotal.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/DefaultConstraintMatchTotal.java @@ -16,6 +16,8 @@ import ai.timefold.solver.core.api.score.stream.DefaultConstraintJustification; import ai.timefold.solver.core.api.solver.SolutionManager; +import org.jspecify.annotations.NonNull; + /** * If possible, prefer using {@link SolutionManager#analyze(Object)} instead. * @@ -71,22 +73,22 @@ public DefaultConstraintMatchTotal(ConstraintRef constraintRef, Score_ constrain } @Override - public ConstraintRef getConstraintRef() { + public @NonNull ConstraintRef getConstraintRef() { return constraintRef; } @Override - public Score_ getConstraintWeight() { + public @NonNull Score_ getConstraintWeight() { return constraintWeight; } @Override - public Set> getConstraintMatchSet() { + public @NonNull Set> getConstraintMatchSet() { return constraintMatchSet; } @Override - public Score_ getScore() { + public @NonNull Score_ getScore() { return score; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/DefaultIndictment.java b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/DefaultIndictment.java index 4f080ef44f..c73151c29f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/DefaultIndictment.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/DefaultIndictment.java @@ -11,6 +11,8 @@ import ai.timefold.solver.core.api.score.stream.ConstraintJustification; import ai.timefold.solver.core.impl.util.CollectionUtils; +import org.jspecify.annotations.NonNull; + public final class DefaultIndictment> implements Indictment { private final Object indictedObject; @@ -24,17 +26,17 @@ public DefaultIndictment(Object indictedObject, Score_ zeroScore) { } @Override - public IndictedObject_ getIndictedObject() { + public @NonNull IndictedObject_ getIndictedObject() { return (IndictedObject_) indictedObject; } @Override - public Set> getConstraintMatchSet() { + public @NonNull Set> getConstraintMatchSet() { return constraintMatchSet; } @Override - public List getJustificationList() { + public @NonNull List getJustificationList() { if (constraintJustificationList == null) { constraintJustificationList = buildConstraintJustificationList(); } @@ -61,7 +63,7 @@ private List buildConstraintJustificationList() { } @Override - public Score_ getScore() { + public @NonNull Score_ getScore() { return score; } 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 8103617232..74c1f8a2ec 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 @@ -19,6 +19,7 @@ import ai.timefold.solver.core.api.score.analysis.ConstraintAnalysis; import ai.timefold.solver.core.api.score.analysis.MatchAnalysis; import ai.timefold.solver.core.api.score.director.ScoreDirector; +import ai.timefold.solver.core.api.solver.ScoreAnalysisFetchPolicy; import ai.timefold.solver.core.api.solver.change.ProblemChange; import ai.timefold.solver.core.api.solver.change.ProblemChangeDirector; import ai.timefold.solver.core.config.solver.EnvironmentMode; @@ -34,6 +35,7 @@ import ai.timefold.solver.core.impl.domain.variable.supply.SupplyManager; import ai.timefold.solver.core.impl.move.director.MoveDirector; import ai.timefold.solver.core.impl.phase.scope.SolverLifecyclePoint; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; import ai.timefold.solver.core.impl.solver.exception.UndoScoreCorruptionException; import ai.timefold.solver.core.impl.solver.thread.ChildThreadType; @@ -62,11 +64,11 @@ public abstract class AbstractScoreDirector variableDescriptorCache; protected final VariableListenerSupport variableListenerSupport; - protected final boolean constraintMatchEnabledPreference; private long workingEntityListRevision = 0L; private int workingGenuineEntityCount = 0; @@ -85,18 +87,18 @@ public abstract class AbstractScoreDirector listVariableStateSupply; protected AbstractScoreDirector(Factory_ scoreDirectorFactory, boolean lookUpEnabled, - boolean constraintMatchEnabledPreference, boolean expectShadowVariablesInCorrectState) { + ConstraintMatchPolicy constraintMatchPolicy, boolean expectShadowVariablesInCorrectState) { var solutionDescriptor = scoreDirectorFactory.getSolutionDescriptor(); this.lookUpEnabled = lookUpEnabled; this.lookUpManager = lookUpEnabled ? new LookUpManager(solutionDescriptor.getLookUpStrategyResolver()) : null; + this.constraintMatchPolicy = constraintMatchPolicy; this.expectShadowVariablesInCorrectState = expectShadowVariablesInCorrectState; this.scoreDirectorFactory = scoreDirectorFactory; this.variableDescriptorCache = new VariableDescriptorCache<>(solutionDescriptor); this.variableListenerSupport = VariableListenerSupport.create(this); this.variableListenerSupport.linkVariableListeners(); - this.constraintMatchEnabledPreference = constraintMatchEnabledPreference; if (scoreDirectorFactory.isTrackingWorkingSolution()) { this.solutionTracker = new SolutionTracker<>(getSolutionDescriptor(), getSupplyManager()); @@ -113,6 +115,11 @@ protected AbstractScoreDirector(Factory_ scoreDirectorFactory, boolean lookUpEna } } + @Override + public final ConstraintMatchPolicy getConstraintMatchPolicy() { + return constraintMatchPolicy; + } + @Override public Factory_ getScoreDirectorFactory() { return scoreDirectorFactory; @@ -338,16 +345,14 @@ public AbstractScoreDirector clone() { public InnerScoreDirector createChildThreadScoreDirector(ChildThreadType childThreadType) { if (childThreadType == ChildThreadType.PART_THREAD) { var childThreadScoreDirector = (AbstractScoreDirector) scoreDirectorFactory - .buildDerivedScoreDirector(lookUpEnabled, constraintMatchEnabledPreference); + .buildDerivedScoreDirector(lookUpEnabled, constraintMatchPolicy); // ScoreCalculationCountTermination takes into account previous phases // but the calculationCount of partitions is maxed, not summed. childThreadScoreDirector.calculationCount = calculationCount; return childThreadScoreDirector; } else if (childThreadType == ChildThreadType.MOVE_THREAD) { - // TODO The move thread must use constraintMatchEnabledPreference in FULL_ASSERT, - // but it doesn't have to for Indictment Local Search, in which case it is a performance loss var childThreadScoreDirector = (AbstractScoreDirector) scoreDirectorFactory - .buildDerivedScoreDirector(true, constraintMatchEnabledPreference); + .buildDerivedScoreDirector(true, constraintMatchPolicy); childThreadScoreDirector.setWorkingSolution(cloneWorkingSolution()); return childThreadScoreDirector; } else { @@ -620,7 +625,8 @@ private void assertScoreFromScratch(Score_ score, Object completedAction, boolea if (assertionScoreDirectorFactory == null) { assertionScoreDirectorFactory = scoreDirectorFactory; } - try (var uncorruptedScoreDirector = assertionScoreDirectorFactory.buildDerivedScoreDirector(false, true)) { + try (var uncorruptedScoreDirector = + assertionScoreDirectorFactory.buildDerivedScoreDirector(false, ConstraintMatchPolicy.ENABLED)) { uncorruptedScoreDirector.setWorkingSolution(workingSolution); Score_ uncorruptedScore = uncorruptedScoreDirector.calculateScore(); if (!score.equals(uncorruptedScore)) { @@ -713,16 +719,18 @@ that could cause the scoreDifference (%s).""" */ protected String buildScoreCorruptionAnalysis(InnerScoreDirector uncorruptedScoreDirector, boolean predicted) { - if (!isConstraintMatchEnabled() || !uncorruptedScoreDirector.isConstraintMatchEnabled()) { + if (!getConstraintMatchPolicy().isEnabled() || !uncorruptedScoreDirector.getConstraintMatchPolicy().isEnabled()) { return """ - Score corruption analysis could not be generated because either corrupted constraintMatchEnabled (%s) \ - or uncorrupted constraintMatchEnabled (%s) is disabled. + Score corruption analysis could not be generated because either corrupted constraintMatchPolicy (%s) \ + or uncorrupted constraintMatchPolicy (%s) is %s. Check your score constraints manually.""" - .formatted(constraintMatchEnabledPreference, uncorruptedScoreDirector.isConstraintMatchEnabled()); + .formatted(constraintMatchPolicy, uncorruptedScoreDirector.getConstraintMatchPolicy(), + ConstraintMatchPolicy.DISABLED); } - var corruptedAnalysis = buildScoreAnalysis(true, ScoreAnalysisMode.SCORE_CORRUPTION); - var uncorruptedAnalysis = uncorruptedScoreDirector.buildScoreAnalysis(true, ScoreAnalysisMode.SCORE_CORRUPTION); + var corruptedAnalysis = buildScoreAnalysis(ScoreAnalysisFetchPolicy.FETCH_ALL, ScoreAnalysisMode.SCORE_CORRUPTION); + var uncorruptedAnalysis = uncorruptedScoreDirector.buildScoreAnalysis(ScoreAnalysisFetchPolicy.FETCH_ALL, + ScoreAnalysisMode.SCORE_CORRUPTION); var excessSet = new LinkedHashSet>(); var missingSet = new LinkedHashSet>(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirectorFactory.java index ec4cc2fe72..08bdcff052 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirectorFactory.java @@ -6,6 +6,7 @@ import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.BasicVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; import ai.timefold.solver.core.impl.score.trend.InitializingScoreTrend; @@ -92,16 +93,11 @@ public void setTrackingWorkingSolution(boolean trackingWorkingSolution) { // Complex methods // ************************************************************************ - @Override - public InnerScoreDirector buildScoreDirector() { - return buildScoreDirector(true, true); - } - @Override public void assertScoreFromScratch(Solution_ solution) { // Get the score before uncorruptedScoreDirector.calculateScore() modifies it Score_ score = getSolutionDescriptor().getScore(solution); - try (var uncorruptedScoreDirector = buildDerivedScoreDirector(false, true)) { + try (var uncorruptedScoreDirector = buildDerivedScoreDirector(false, ConstraintMatchPolicy.ENABLED)) { uncorruptedScoreDirector.setWorkingSolution(solution); Score_ uncorruptedScore = uncorruptedScoreDirector.calculateScore(); if (!score.equals(uncorruptedScore)) { 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 11e29f6a5c..d41d050fbb 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 @@ -1,6 +1,8 @@ package ai.timefold.solver.core.impl.score.director; -import java.util.ArrayList; +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toList; + import java.util.List; import java.util.Map; import java.util.Objects; @@ -24,6 +26,7 @@ import ai.timefold.solver.core.api.score.director.ScoreDirector; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintJustification; +import ai.timefold.solver.core.api.solver.ScoreAnalysisFetchPolicy; import ai.timefold.solver.core.api.solver.SolutionManager; import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; @@ -32,9 +35,9 @@ import ai.timefold.solver.core.impl.domain.variable.supply.SupplyManager; import ai.timefold.solver.core.impl.move.director.MoveDirector; import ai.timefold.solver.core.impl.phase.scope.SolverLifecyclePoint; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; import ai.timefold.solver.core.impl.solver.thread.ChildThreadType; -import ai.timefold.solver.core.impl.util.CollectionUtils; /** * @param the solution type, the class with the {@link PlanningSolution} annotation @@ -44,34 +47,39 @@ public interface InnerScoreDirector> extends VariableDescriptorAwareScoreDirector, AutoCloseable { static > ConstraintAnalysis getConstraintAnalysis( - ConstraintMatchTotal constraintMatchTotal, boolean analyzeConstraintMatches) { - Score_ zero = constraintMatchTotal.getScore().zero(); - if (analyzeConstraintMatches) { - // Marge all constraint matches with the same justification. - Map>> deduplicatedConstraintMatchMap = - CollectionUtils.newLinkedHashMap(constraintMatchTotal.getConstraintMatchCount()); - for (var constraintMatch : constraintMatchTotal.getConstraintMatchSet()) { - var constraintJustification = constraintMatch.getJustification(); - var constraintMatchList = deduplicatedConstraintMatchMap.computeIfAbsent(constraintJustification, - k -> new ArrayList<>(1)); - constraintMatchList.add(constraintMatch); + ConstraintMatchTotal constraintMatchTotal, ScoreAnalysisFetchPolicy scoreAnalysisFetchPolicy) { + return switch (scoreAnalysisFetchPolicy) { + case FETCH_ALL -> { + // Justification can not be null here, because they are enabled by FETCH_ALL. + var deduplicatedConstraintMatchMap = constraintMatchTotal.getConstraintMatchSet() + .stream() + .collect(groupingBy( + c -> (ConstraintJustification) c.getJustification(), + toList())); + var matchAnalyses = sumMatchesWithSameJustification(constraintMatchTotal, deduplicatedConstraintMatchMap); + yield new ConstraintAnalysis<>(constraintMatchTotal.getConstraintRef(), + constraintMatchTotal.getConstraintWeight(), constraintMatchTotal.getScore(), matchAnalyses); } - // Sum scores for each duplicate justification; this is the total score for the match. - var matchAnalyses = deduplicatedConstraintMatchMap.entrySet().stream() - .map(entry -> { - var score = entry.getValue().stream() - .map(ConstraintMatch::getScore) - .reduce(zero, Score::add); - return new MatchAnalysis<>(constraintMatchTotal.getConstraintRef(), score, entry.getKey()); - }) - .toList(); - return new ConstraintAnalysis<>(constraintMatchTotal.getConstraintRef(), constraintMatchTotal.getConstraintWeight(), - constraintMatchTotal.getScore(), - matchAnalyses); - } else { - return new ConstraintAnalysis<>(constraintMatchTotal.getConstraintRef(), constraintMatchTotal.getConstraintWeight(), - constraintMatchTotal.getScore(), null); - } + case FETCH_MATCH_COUNT -> new ConstraintAnalysis<>(constraintMatchTotal.getConstraintRef(), + constraintMatchTotal.getConstraintWeight(), constraintMatchTotal.getScore(), null, + constraintMatchTotal.getConstraintMatchCount()); + case FETCH_SHALLOW -> + new ConstraintAnalysis<>(constraintMatchTotal.getConstraintRef(), constraintMatchTotal.getConstraintWeight(), + constraintMatchTotal.getScore(), null); + }; + } + + private static > List> + sumMatchesWithSameJustification(ConstraintMatchTotal constraintMatchTotal, + Map>> deduplicatedConstraintMatchMap) { + return deduplicatedConstraintMatchMap.entrySet().stream() + .map(entry -> { + var score = entry.getValue().stream() + .map(ConstraintMatch::getScore) + .reduce(constraintMatchTotal.getScore().zero(), Score::add); + return new MatchAnalysis<>(constraintMatchTotal.getConstraintRef(), score, entry.getKey()); + }) + .toList(); } /** @@ -90,9 +98,12 @@ static > ConstraintAnalysis getConstraintAn Score_ calculateScore(); /** - * @return true if {@link #getConstraintMatchTotalMap()} and {@link #getIndictmentMap} can be called + * @return {@link ConstraintMatchPolicy#ENABLED} if {@link #getConstraintMatchTotalMap()} and {@link #getIndictmentMap()} + * can be called. + * {@link ConstraintMatchPolicy#ENABLED_WITHOUT_JUSTIFICATIONS} if only the former can be called. + * {@link ConstraintMatchPolicy#DISABLED} if neither can be called. */ - boolean isConstraintMatchEnabled(); + ConstraintMatchPolicy getConstraintMatchPolicy(); /** * Explains the {@link Score} of {@link #calculateScore()} by splitting it up per {@link Constraint}. @@ -106,7 +117,7 @@ static > ConstraintAnalysis getConstraintAn * (to create one, use {@link ConstraintRef#composeConstraintId(String, String)}). * If a constraint is present in the problem but resulted in no matches, * it will still be in the map with a {@link ConstraintMatchTotal#getConstraintMatchSet()} size of 0. - * @throws IllegalStateException if {@link #isConstraintMatchEnabled()} returns false + * @throws IllegalStateException if {@link #getConstraintMatchPolicy()} returns {@link ConstraintMatchPolicy#DISABLED}. * @see #getIndictmentMap() */ Map> getConstraintMatchTotalMap(); @@ -125,7 +136,7 @@ static > ConstraintAnalysis getConstraintAn * * @return never null, the key is a {@link ProblemFactCollectionProperty problem fact} or a * {@link PlanningEntity planning entity} - * @throws IllegalStateException if {@link #isConstraintMatchEnabled()} returns false + * @throws IllegalStateException unless {@link #getConstraintMatchPolicy()} returns {@link ConstraintMatchPolicy#ENABLED}. * @see #getConstraintMatchTotalMap() */ Map> getIndictmentMap(); @@ -327,25 +338,25 @@ default Solution_ cloneWorkingSolution() { */ void forceTriggerVariableListeners(); - default ScoreAnalysis buildScoreAnalysis(boolean analyzeConstraintMatches) { - return buildScoreAnalysis(analyzeConstraintMatches, ScoreAnalysisMode.DEFAULT); + default ScoreAnalysis buildScoreAnalysis(ScoreAnalysisFetchPolicy scoreAnalysisFetchPolicy) { + return buildScoreAnalysis(scoreAnalysisFetchPolicy, ScoreAnalysisMode.DEFAULT); } /** * - * @param analyzeConstraintMatches True if the result's {@link ConstraintAnalysis} should have its {@link MatchAnalysis} - * populated. + * @param scoreAnalysisFetchPolicy never null * @param mode Allows to tweak the behavior of this method. * @return never null */ - default ScoreAnalysis buildScoreAnalysis(boolean analyzeConstraintMatches, ScoreAnalysisMode mode) { + default ScoreAnalysis buildScoreAnalysis(ScoreAnalysisFetchPolicy scoreAnalysisFetchPolicy, + ScoreAnalysisMode mode) { var score = calculateScore(); if (Objects.requireNonNull(mode) == ScoreAnalysisMode.RECOMMENDATION_API) { score = score.withInitScore(0); } var constraintAnalysisMap = new TreeMap>(); for (var constraintMatchTotal : getConstraintMatchTotalMap().values()) { - var constraintAnalysis = getConstraintAnalysis(constraintMatchTotal, analyzeConstraintMatches); + var constraintAnalysis = getConstraintAnalysis(constraintMatchTotal, scoreAnalysisFetchPolicy); constraintAnalysisMap.put(constraintMatchTotal.getConstraintRef(), constraintAnalysis); } return new ScoreAnalysis<>(score, constraintAnalysisMap); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirectorFactory.java index 97cc080568..e73659839f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirectorFactory.java @@ -5,6 +5,7 @@ import ai.timefold.solver.core.api.score.constraint.ConstraintMatch; import ai.timefold.solver.core.api.score.director.ScoreDirector; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; import ai.timefold.solver.core.impl.score.trend.InitializingScoreTrend; @@ -26,59 +27,31 @@ public interface InnerScoreDirectorFactory getScoreDefinition(); @Override - InnerScoreDirector buildScoreDirector(); - - /** - * Like {@link #buildScoreDirector()}, but optionally disables {@link ConstraintMatch} tracking and look up - * for more performance (presuming the {@link ScoreDirector} implementation actually supports it to begin with). - * - * @param lookUpEnabled true if a {@link ScoreDirector} implementation should track all working objects - * for {@link ScoreDirector#lookUpWorkingObject(Object)} - * @param constraintMatchEnabledPreference false if a {@link ScoreDirector} implementation - * should not do {@link ConstraintMatch} tracking even if it supports it. - * @return never null - * @see InnerScoreDirector#isConstraintMatchEnabled() - * @see InnerScoreDirector#getConstraintMatchTotalMap() - */ default InnerScoreDirector buildScoreDirector(boolean lookUpEnabled, - boolean constraintMatchEnabledPreference) { - return buildScoreDirector(lookUpEnabled, constraintMatchEnabledPreference, true); + ConstraintMatchPolicy constraintMatchPolicy) { + return buildScoreDirector(lookUpEnabled, constraintMatchPolicy, true); } - /** - * Like {@link #buildScoreDirector()}, but optionally disables {@link ConstraintMatch} tracking and look up - * for more performance (presuming the {@link ScoreDirector} implementation actually supports it to begin with). - * - * @param lookUpEnabled true if a {@link ScoreDirector} implementation should track all working objects - * for {@link ScoreDirector#lookUpWorkingObject(Object)} - * @param constraintMatchEnabledPreference false if a {@link ScoreDirector} implementation - * should not do {@link ConstraintMatch} tracking even if it supports it. - * @param expectShadowVariablesInCorrectState true, unless you have an exceptional reason. - * See {@link InnerScoreDirector#expectShadowVariablesInCorrectState()} for details. - * @return never null - * @see InnerScoreDirector#isConstraintMatchEnabled() - * @see InnerScoreDirector#getConstraintMatchTotalMap() - */ - InnerScoreDirector buildScoreDirector(boolean lookUpEnabled, boolean constraintMatchEnabledPreference, + @Override + InnerScoreDirector buildScoreDirector(boolean lookUpEnabled, ConstraintMatchPolicy constraintMatchPolicy, boolean expectShadowVariablesInCorrectState); /** - * Like {@link #buildScoreDirector(boolean, boolean)}, but makes the score director a derived one. + * Like {@link #buildScoreDirector(boolean, ConstraintMatchPolicy)}, but makes the score director a derived one. * Derived score directors may make choices which the main score director can not make, such as reducing logging. * Derived score directors are typically used for multithreaded solving, testing and assert modes. * * @param lookUpEnabled true if a {@link ScoreDirector} implementation should track all working objects * for {@link ScoreDirector#lookUpWorkingObject(Object)} - * @param constraintMatchEnabledPreference false if a {@link ScoreDirector} implementation - * should not do {@link ConstraintMatch} tracking even if it supports it. + * @param constraintMatchPolicy how should the {@link ScoreDirector} implementation do {@link ConstraintMatch}, if at all. * @return never null - * @see InnerScoreDirector#isConstraintMatchEnabled() + * @see InnerScoreDirector#getConstraintMatchPolicy() * @see InnerScoreDirector#getConstraintMatchTotalMap() */ default InnerScoreDirector buildDerivedScoreDirector(boolean lookUpEnabled, - boolean constraintMatchEnabledPreference) { + ConstraintMatchPolicy constraintMatchPolicy) { // Most score directors don't need derived status; CS will override this. - return buildScoreDirector(lookUpEnabled, constraintMatchEnabledPreference, true); + return buildScoreDirector(lookUpEnabled, constraintMatchPolicy); } /** diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactory.java index 316970891b..d699253c8b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactory.java @@ -1,7 +1,9 @@ package ai.timefold.solver.core.impl.score.director; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; +import ai.timefold.solver.core.api.score.constraint.ConstraintMatch; import ai.timefold.solver.core.api.score.director.ScoreDirector; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; /** * Builds a {@link ScoreDirector}. @@ -11,10 +13,19 @@ public interface ScoreDirectorFactory { /** - * Creates a new {@link ScoreDirector} instance. + * Like {@link #buildScoreDirector(boolean, ConstraintMatchPolicy, boolean)}, + * with the final parameter set to true. * + * @param lookUpEnabled true if a {@link ScoreDirector} implementation should track all working objects + * for {@link ScoreDirector#lookUpWorkingObject(Object)} + * @param constraintMatchPolicy how should the {@link ScoreDirector} track {@link ConstraintMatch constraint matches}. * @return never null */ - ScoreDirector buildScoreDirector(); + default ScoreDirector buildScoreDirector(boolean lookUpEnabled, ConstraintMatchPolicy constraintMatchPolicy) { + return buildScoreDirector(lookUpEnabled, constraintMatchPolicy, true); + } + + ScoreDirector buildScoreDirector(boolean lookUpEnabled, ConstraintMatchPolicy constraintMatchPolicy, + boolean expectShadowVariablesInCorrectState); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirector.java index 4987b657b8..071f4d24ea 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirector.java @@ -10,6 +10,7 @@ import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; import ai.timefold.solver.core.api.score.constraint.Indictment; import ai.timefold.solver.core.api.score.director.ScoreDirector; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.director.AbstractScoreDirector; /** @@ -28,9 +29,9 @@ public final class EasyScoreDirector> private final EasyScoreCalculator easyScoreCalculator; public EasyScoreDirector(EasyScoreDirectorFactory scoreDirectorFactory, - boolean lookUpEnabled, boolean constraintMatchEnabledPreference, boolean expectShadowVariablesInCorrectState, + boolean lookUpEnabled, boolean expectShadowVariablesInCorrectState, EasyScoreCalculator easyScoreCalculator) { - super(scoreDirectorFactory, lookUpEnabled, constraintMatchEnabledPreference, expectShadowVariablesInCorrectState); + super(scoreDirectorFactory, lookUpEnabled, ConstraintMatchPolicy.DISABLED, expectShadowVariablesInCorrectState); this.easyScoreCalculator = easyScoreCalculator; } @@ -63,16 +64,6 @@ public Score_ calculateScore() { return score; } - /** - * Always false, {@link ConstraintMatchTotal}s are not supported by this {@link ScoreDirector} implementation. - * - * @return false - */ - @Override - public boolean isConstraintMatchEnabled() { - return false; - } - /** * {@link ConstraintMatch}s are not supported by this {@link ScoreDirector} implementation. * diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorFactory.java index 5225ab4fd3..f08b423af5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorFactory.java @@ -6,6 +6,7 @@ import ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig; import ai.timefold.solver.core.config.util.ConfigUtils; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.director.AbstractScoreDirectorFactory; import ai.timefold.solver.core.impl.score.director.ScoreDirectorFactory; @@ -47,11 +48,9 @@ public EasyScoreDirectorFactory(SolutionDescriptor solutionDescriptor // ************************************************************************ @Override - public EasyScoreDirector buildScoreDirector( - boolean lookUpEnabled, boolean constraintMatchEnabledPreference, boolean expectShadowVariablesInCorrectState) { - return new EasyScoreDirector<>(this, lookUpEnabled, constraintMatchEnabledPreference, - expectShadowVariablesInCorrectState, - easyScoreCalculator); + public EasyScoreDirector buildScoreDirector(boolean lookUpEnabled, + ConstraintMatchPolicy constraintMatchPolicy, boolean expectShadowVariablesInCorrectState) { + return new EasyScoreDirector<>(this, lookUpEnabled, expectShadowVariablesInCorrectState, easyScoreCalculator); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirector.java index ac1ac904c3..3476932336 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirector.java @@ -18,6 +18,7 @@ import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.VariableDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.constraint.DefaultIndictment; import ai.timefold.solver.core.impl.score.director.AbstractScoreDirector; @@ -36,12 +37,23 @@ public final class IncrementalScoreDirector incrementalScoreCalculator; public IncrementalScoreDirector(IncrementalScoreDirectorFactory scoreDirectorFactory, - boolean lookUpEnabled, boolean constraintMatchEnabledPreference, boolean expectShadowVariablesInCorrectState, + boolean lookUpEnabled, ConstraintMatchPolicy constraintMatchPolicy, boolean expectShadowVariablesInCorrectState, IncrementalScoreCalculator incrementalScoreCalculator) { - super(scoreDirectorFactory, lookUpEnabled, constraintMatchEnabledPreference, expectShadowVariablesInCorrectState); + super(scoreDirectorFactory, lookUpEnabled, + determineCorrectPolicy(constraintMatchPolicy, incrementalScoreCalculator), + expectShadowVariablesInCorrectState); this.incrementalScoreCalculator = incrementalScoreCalculator; } + private static ConstraintMatchPolicy determineCorrectPolicy(ConstraintMatchPolicy constraintMatchPolicy, + IncrementalScoreCalculator incrementalScoreCalculator) { + if (incrementalScoreCalculator instanceof ConstraintMatchAwareIncrementalScoreCalculator) { + return constraintMatchPolicy; + } else { + return ConstraintMatchPolicy.DISABLED; + } + } + public IncrementalScoreCalculator getIncrementalScoreCalculator() { return incrementalScoreCalculator; } @@ -55,7 +67,7 @@ public void setWorkingSolution(Solution_ workingSolution) { super.setWorkingSolution(workingSolution); if (incrementalScoreCalculator instanceof ConstraintMatchAwareIncrementalScoreCalculator) { ((ConstraintMatchAwareIncrementalScoreCalculator) incrementalScoreCalculator) - .resetWorkingSolution(workingSolution, constraintMatchEnabledPreference); + .resetWorkingSolution(workingSolution, getConstraintMatchPolicy().isEnabled()); } else { incrementalScoreCalculator.resetWorkingSolution(workingSolution); } @@ -64,11 +76,10 @@ public void setWorkingSolution(Solution_ workingSolution) { @Override public Score_ calculateScore() { variableListenerSupport.assertNotificationQueuesAreEmpty(); - Score_ score = incrementalScoreCalculator.calculateScore(); - if (score == null) { - throw new IllegalStateException("The incrementalScoreCalculator (" + incrementalScoreCalculator.getClass() - + ") must return a non-null score (" + score + ") in the method calculateScore()."); - } else if (!score.isSolutionInitialized()) { + Score_ score = Objects.requireNonNull(incrementalScoreCalculator.calculateScore(), + () -> "The incrementalScoreCalculator (%s) must return a non-null score in the method calculateScore()." + .formatted(incrementalScoreCalculator)); + if (!score.isSolutionInitialized()) { throw new IllegalStateException("The score (" + this + ")'s initScore (" + score.initScore() + ") should be 0.\n" + "Maybe the score calculator (" + incrementalScoreCalculator.getClass() + ") is calculating " @@ -82,16 +93,10 @@ public Score_ calculateScore() { return score; } - @Override - public boolean isConstraintMatchEnabled() { - return constraintMatchEnabledPreference - && incrementalScoreCalculator instanceof ConstraintMatchAwareIncrementalScoreCalculator; - } - @Override public Map> getConstraintMatchTotalMap() { - if (!isConstraintMatchEnabled()) { - throw new IllegalStateException("When constraintMatchEnabled (" + isConstraintMatchEnabled() + if (!constraintMatchPolicy.isEnabled()) { + throw new IllegalStateException("When constraint matching (" + constraintMatchPolicy + ") is disabled in the constructor, this method should not be called."); } // Notice that we don't trigger the variable listeners @@ -103,8 +108,8 @@ public Map> getConstraintMatchTotalMap() { @Override public Map> getIndictmentMap() { - if (!isConstraintMatchEnabled()) { - throw new IllegalStateException("When constraintMatchEnabled (" + isConstraintMatchEnabled() + if (!constraintMatchPolicy.isJustificationEnabled()) { + throw new IllegalStateException("When constraint matching with justifications (" + constraintMatchPolicy + ") is disabled in the constructor, this method should not be called."); } Map> incrementalIndictmentMap = @@ -113,7 +118,7 @@ public Map> getIndictmentMap() { if (incrementalIndictmentMap != null) { return incrementalIndictmentMap; } - Map> indictmentMap = new LinkedHashMap<>(); // TODO use entitySize + Map> indictmentMap = new LinkedHashMap<>(); Score_ zeroScore = getScoreDefinition().getZeroScore(); Map> constraintMatchTotalMap = getConstraintMatchTotalMap(); for (ConstraintMatchTotal constraintMatchTotal : constraintMatchTotalMap.values()) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorFactory.java index 39faf9cb49..57f336c459 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorFactory.java @@ -9,6 +9,7 @@ import ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig; import ai.timefold.solver.core.config.util.ConfigUtils; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.director.AbstractScoreDirectorFactory; import ai.timefold.solver.core.impl.score.director.ScoreDirectorFactory; @@ -55,9 +56,8 @@ public boolean supportsConstraintMatching() { @Override public IncrementalScoreDirector buildScoreDirector(boolean lookUpEnabled, - boolean constraintMatchEnabledPreference, boolean expectShadowVariablesInCorrectState) { - return new IncrementalScoreDirector<>(this, - lookUpEnabled, constraintMatchEnabledPreference, expectShadowVariablesInCorrectState, + ConstraintMatchPolicy constraintMatchPolicy, boolean expectShadowVariablesInCorrectState) { + return new IncrementalScoreDirector<>(this, lookUpEnabled, constraintMatchPolicy, expectShadowVariablesInCorrectState, incrementalScoreCalculatorSupplier.get()); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirector.java index c0949ad9a4..7cfa71b508 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirector.java @@ -11,6 +11,7 @@ import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.VariableDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.director.AbstractScoreDirector; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintSession; @@ -29,14 +30,14 @@ public final class BavetConstraintStreamScoreDirector session; public BavetConstraintStreamScoreDirector(BavetConstraintStreamScoreDirectorFactory scoreDirectorFactory, - boolean lookUpEnabled, boolean constraintMatchEnabledPreference, boolean expectShadowVariablesInCorrectState) { - this(scoreDirectorFactory, lookUpEnabled, constraintMatchEnabledPreference, expectShadowVariablesInCorrectState, false); + boolean lookUpEnabled, ConstraintMatchPolicy constraintMatchPolicy, boolean expectShadowVariablesInCorrectState) { + this(scoreDirectorFactory, lookUpEnabled, constraintMatchPolicy, expectShadowVariablesInCorrectState, false); } public BavetConstraintStreamScoreDirector(BavetConstraintStreamScoreDirectorFactory scoreDirectorFactory, - boolean lookUpEnabled, boolean constraintMatchEnabledPreference, boolean expectShadowVariablesInCorrectState, + boolean lookUpEnabled, ConstraintMatchPolicy constraintMatchPolicy, boolean expectShadowVariablesInCorrectState, boolean derived) { - super(scoreDirectorFactory, lookUpEnabled, constraintMatchEnabledPreference, expectShadowVariablesInCorrectState); + super(scoreDirectorFactory, lookUpEnabled, constraintMatchPolicy, expectShadowVariablesInCorrectState); this.derived = derived; } @@ -46,7 +47,7 @@ public BavetConstraintStreamScoreDirector(BavetConstraintStreamScoreDirectorFact @Override public void setWorkingSolution(Solution_ workingSolution) { - session = scoreDirectorFactory.newSession(workingSolution, constraintMatchEnabledPreference, derived); + session = scoreDirectorFactory.newSession(workingSolution, constraintMatchPolicy, derived); getSolutionDescriptor().visitAll(workingSolution, session::insert); super.setWorkingSolution(workingSolution); } @@ -59,14 +60,11 @@ public Score_ calculateScore() { return score; } - @Override - public boolean isConstraintMatchEnabled() { - return constraintMatchEnabledPreference; - } - @Override public Map> getConstraintMatchTotalMap() { - if (workingSolution == null) { + if (!constraintMatchPolicy.isEnabled()) { + throw new IllegalStateException("When constraint matching is disabled, this method should not be called."); + } else if (workingSolution == null) { throw new IllegalStateException( "The method setWorkingSolution() must be called before the method getConstraintMatchTotalMap()."); } @@ -75,7 +73,10 @@ public Map> getConstraintMatchTotalMap() { @Override public Map> getIndictmentMap() { - if (workingSolution == null) { + if (!constraintMatchPolicy.isJustificationEnabled()) { + throw new IllegalStateException( + "When constraint matching with justifications is disabled, this method should not be called."); + } else if (workingSolution == null) { throw new IllegalStateException( "The method setWorkingSolution() must be called before the method getIndictmentMap()."); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirectorFactory.java index b039665b07..7ab4f5c986 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirectorFactory.java @@ -14,6 +14,7 @@ import ai.timefold.solver.core.config.util.ConfigUtils; import ai.timefold.solver.core.enterprise.TimefoldSolverEnterpriseService; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintFactory; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintSession; @@ -69,26 +70,25 @@ public BavetConstraintStreamScoreDirectorFactory(SolutionDescriptor s @Override public BavetConstraintStreamScoreDirector buildScoreDirector(boolean lookUpEnabled, - boolean constraintMatchEnabledPreference, boolean expectShadowVariablesInCorrectState) { - return new BavetConstraintStreamScoreDirector<>(this, lookUpEnabled, constraintMatchEnabledPreference, + ConstraintMatchPolicy constraintMatchPolicy, boolean expectShadowVariablesInCorrectState) { + return new BavetConstraintStreamScoreDirector<>(this, lookUpEnabled, constraintMatchPolicy, expectShadowVariablesInCorrectState); } @Override public InnerScoreDirector buildDerivedScoreDirector(boolean lookUpEnabled, - boolean constraintMatchEnabledPreference) { - return new BavetConstraintStreamScoreDirector<>(this, lookUpEnabled, constraintMatchEnabledPreference, - true, true); + ConstraintMatchPolicy constraintMatchPolicy) { + return new BavetConstraintStreamScoreDirector<>(this, lookUpEnabled, constraintMatchPolicy, true, true); } - public BavetConstraintSession newSession(Solution_ workingSolution, boolean constraintMatchEnabled, + public BavetConstraintSession newSession(Solution_ workingSolution, ConstraintMatchPolicy constraintMatchPolicy, boolean scoreDirectorDerived) { - return constraintSessionFactory.buildSession(workingSolution, constraintMatchEnabled, scoreDirectorDerived); + return constraintSessionFactory.buildSession(workingSolution, constraintMatchPolicy, scoreDirectorDerived); } @Override public AbstractScoreInliner fireAndForget(Object... facts) { - var session = newSession(null, true, true); + var session = newSession(null, ConstraintMatchPolicy.ENABLED, true); Arrays.stream(facts).forEach(session::insert); session.calculateScore(0); return session.getScoreInliner(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintFactory.java index 74efab1007..411edcde7a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintFactory.java @@ -10,6 +10,7 @@ import ai.timefold.solver.core.api.score.stream.uni.UniConstraintStream; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.bavet.common.BavetAbstractConstraintStream; import ai.timefold.solver.core.impl.score.stream.bavet.uni.BavetForEachUniConstraintStream; import ai.timefold.solver.core.impl.score.stream.common.InnerConstraintFactory; @@ -69,7 +70,8 @@ public > Stream_ share( * If a constraint already exists in this factory, it replaces it with the old copy. * {@link BavetAbstractConstraintStream} implement equals/hashcode ignoring child streams. *

    - * {@link BavetConstraintSessionFactory#buildSession(Object, boolean, boolean)} needs this to happen for all streams. + * {@link BavetConstraintSessionFactory#buildSession(Object, ConstraintMatchPolicy, boolean)} needs this to happen for all + * streams. *

    * This must be called before the stream receives child streams. * diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintSession.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintSession.java index 965ce5ee51..42248d408a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintSession.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintSession.java @@ -6,6 +6,7 @@ import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; import ai.timefold.solver.core.api.score.constraint.Indictment; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.director.stream.BavetConstraintStreamScoreDirectorFactory; import ai.timefold.solver.core.impl.score.stream.bavet.common.PropagationQueue; import ai.timefold.solver.core.impl.score.stream.bavet.uni.AbstractForEachUniNode; @@ -13,7 +14,8 @@ /** * The type is public to make it easier for Bavet-specific minimal bug reproducers to be created. - * Instances should be created through {@link BavetConstraintStreamScoreDirectorFactory#newSession(Object, boolean, boolean)}. + * Instances should be created through + * {@link BavetConstraintStreamScoreDirectorFactory#newSession(Object, ConstraintMatchPolicy, boolean)}. * * @see PropagationQueue Description of the tuple propagation mechanism. * @param diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintSessionFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintSessionFactory.java index 027bf41135..0ca6763d8d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintSessionFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintSessionFactory.java @@ -14,6 +14,7 @@ import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintMetaModel; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.bavet.common.AbstractConcatNode; import ai.timefold.solver.core.impl.score.stream.bavet.common.AbstractIfExistsNode; import ai.timefold.solver.core.impl.score.stream.bavet.common.AbstractJoinNode; @@ -53,7 +54,7 @@ public BavetConstraintSessionFactory(SolutionDescriptor solutionDescr // ************************************************************************ @SuppressWarnings("unchecked") - public BavetConstraintSession buildSession(Solution_ workingSolution, boolean constraintMatchEnabled, + public BavetConstraintSession buildSession(Solution_ workingSolution, ConstraintMatchPolicy constraintMatchPolicy, boolean scoreDirectorDerived) { var constraintWeightSupplier = solutionDescriptor.getConstraintWeightSupplier(); var constraints = constraintMetaModel.getConstraints(); @@ -108,7 +109,7 @@ public BavetConstraintSession buildSession(Solution_ workingSolution, bo } } - var scoreInliner = AbstractScoreInliner.buildScoreInliner(scoreDefinition, constraintWeightMap, constraintMatchEnabled); + var scoreInliner = AbstractScoreInliner.buildScoreInliner(scoreDefinition, constraintWeightMap, constraintMatchPolicy); if (constraintStreamSet.isEmpty()) { LOGGER.warn("No constraints enabled for solution ({}).", workingSolution); return new BavetConstraintSession<>(scoreInliner); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetScoringBiConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetScoringBiConstraintStream.java index 6e5d567f2a..e99a94c5b4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetScoringBiConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetScoringBiConstraintStream.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.score.stream.bavet.bi; +import static ai.timefold.solver.core.impl.score.stream.bavet.common.BavetScoringConstraintStream.impactWithConstraintMatchNoJustifications; + import java.math.BigDecimal; import java.util.function.BiFunction; import java.util.function.ToIntBiFunction; @@ -7,6 +9,7 @@ import ai.timefold.solver.core.api.function.TriFunction; import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraint; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintFactory; import ai.timefold.solver.core.impl.score.stream.bavet.common.BavetScoringConstraintStream; @@ -69,14 +72,22 @@ public void setConstraint(BavetConstraint constraint) { @Override public > void buildNode(NodeBuildHelper buildHelper) { assertEmptyChildStreamList(); - var constraintMatchEnabled = buildHelper.getScoreInliner().isConstraintMatchEnabled(); - var scoreImpacter = constraintMatchEnabled ? buildScoreImpacterWithConstraintMatch() : buildScoreImpacter(); + var scoreImpacter = buildScoreImpacter(buildHelper.getScoreInliner().getConstraintMatchPolicy()); var weightedScoreImpacter = buildHelper.getScoreInliner().buildWeightedScoreImpacter(constraint); var scorer = new BiScorer<>(weightedScoreImpacter, scoreImpacter, buildHelper.reserveTupleStoreIndex(parent.getTupleSource())); buildHelper.putInsertUpdateRetract(this, scorer); } + private TriFunction, A, B, UndoScoreImpacter> + buildScoreImpacter(ConstraintMatchPolicy constraintMatchPolicy) { + return switch (constraintMatchPolicy) { + case DISABLED -> buildScoreImpacter(); + case ENABLED -> buildScoreImpacterWithConstraintMatch(); + case ENABLED_WITHOUT_JUSTIFICATIONS -> buildScoreImpacterWithConstraintMatchNoJustifications(); + }; + } + private TriFunction, A, B, UndoScoreImpacter> buildScoreImpacter() { if (intMatchWeigher != null) { return (impacter, a, b) -> { @@ -143,6 +154,28 @@ public > void buildNode(NodeBuildHelper bui return impacter.impactScore(matchWeight, constraintMatchSupplier); } + private TriFunction, A, B, UndoScoreImpacter> + buildScoreImpacterWithConstraintMatchNoJustifications() { + if (intMatchWeigher != null) { + return (impacter, a, b) -> { + int matchWeight = intMatchWeigher.applyAsInt(a, b); + return impactWithConstraintMatchNoJustifications(impacter, matchWeight); + }; + } else if (longMatchWeigher != null) { + return (impacter, a, b) -> { + long matchWeight = longMatchWeigher.applyAsLong(a, b); + return impactWithConstraintMatchNoJustifications(impacter, matchWeight); + }; + } else if (bigDecimalMatchWeigher != null) { + return (impacter, a, b) -> { + BigDecimal matchWeight = bigDecimalMatchWeigher.apply(a, b); + return impactWithConstraintMatchNoJustifications(impacter, matchWeight); + }; + } else { + throw new IllegalStateException("Impossible state: neither of the supported match weighers provided."); + } + } + // ************************************************************************ // Equality for node sharing // ************************************************************************ diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/BavetScoringConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/BavetScoringConstraintStream.java index bc74469437..3285af288c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/BavetScoringConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/BavetScoringConstraintStream.java @@ -1,8 +1,13 @@ package ai.timefold.solver.core.impl.score.stream.bavet.common; +import java.math.BigDecimal; import java.util.Set; +import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraint; +import ai.timefold.solver.core.impl.score.stream.common.inliner.ConstraintMatchSupplier; +import ai.timefold.solver.core.impl.score.stream.common.inliner.UndoScoreImpacter; +import ai.timefold.solver.core.impl.score.stream.common.inliner.WeightedScoreImpacter; public interface BavetScoringConstraintStream { @@ -10,4 +15,22 @@ public interface BavetScoringConstraintStream { void collectActiveConstraintStreams(Set> constraintStreamSet); + static > UndoScoreImpacter + impactWithConstraintMatchNoJustifications(WeightedScoreImpacter impacter, int matchWeight) { + var constraintMatchSupplier = ConstraintMatchSupplier. empty(); + return impacter.impactScore(matchWeight, constraintMatchSupplier); + } + + static > UndoScoreImpacter + impactWithConstraintMatchNoJustifications(WeightedScoreImpacter impacter, long matchWeight) { + var constraintMatchSupplier = ConstraintMatchSupplier. empty(); + return impacter.impactScore(matchWeight, constraintMatchSupplier); + } + + static > UndoScoreImpacter + impactWithConstraintMatchNoJustifications(WeightedScoreImpacter impacter, BigDecimal matchWeight) { + var constraintMatchSupplier = ConstraintMatchSupplier. empty(); + return impacter.impactScore(matchWeight, constraintMatchSupplier); + } + } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetScoringQuadConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetScoringQuadConstraintStream.java index 9ea37fff27..36432cd455 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetScoringQuadConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetScoringQuadConstraintStream.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.score.stream.bavet.quad; +import static ai.timefold.solver.core.impl.score.stream.bavet.common.BavetScoringConstraintStream.impactWithConstraintMatchNoJustifications; + import java.math.BigDecimal; import ai.timefold.solver.core.api.function.PentaFunction; @@ -7,6 +9,7 @@ import ai.timefold.solver.core.api.function.ToIntQuadFunction; import ai.timefold.solver.core.api.function.ToLongQuadFunction; import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraint; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintFactory; import ai.timefold.solver.core.impl.score.stream.bavet.common.BavetScoringConstraintStream; @@ -70,14 +73,22 @@ public void setConstraint(BavetConstraint constraint) { @Override public > void buildNode(NodeBuildHelper buildHelper) { assertEmptyChildStreamList(); - var constraintMatchEnabled = buildHelper.getScoreInliner().isConstraintMatchEnabled(); - var scoreImpacter = constraintMatchEnabled ? buildScoreImpacterWithConstraintMatch() : buildScoreImpacter(); + var scoreImpacter = buildScoreImpacter(buildHelper.getScoreInliner().getConstraintMatchPolicy()); var weightedScoreImpacter = buildHelper.getScoreInliner().buildWeightedScoreImpacter(constraint); var scorer = new QuadScorer<>(weightedScoreImpacter, scoreImpacter, buildHelper.reserveTupleStoreIndex(parent.getTupleSource())); buildHelper.putInsertUpdateRetract(this, scorer); } + private PentaFunction, A, B, C, D, UndoScoreImpacter> + buildScoreImpacter(ConstraintMatchPolicy constraintMatchPolicy) { + return switch (constraintMatchPolicy) { + case DISABLED -> buildScoreImpacter(); + case ENABLED -> buildScoreImpacterWithConstraintMatch(); + case ENABLED_WITHOUT_JUSTIFICATIONS -> buildScoreImpacterWithConstraintMatchNoJustifications(); + }; + } + private PentaFunction, A, B, C, D, UndoScoreImpacter> buildScoreImpacter() { if (intMatchWeigher != null) { return (impacter, a, b, c, d) -> { @@ -144,6 +155,28 @@ public > void buildNode(NodeBuildHelper bui return impacter.impactScore(matchWeight, constraintMatchSupplier); } + private PentaFunction, A, B, C, D, UndoScoreImpacter> + buildScoreImpacterWithConstraintMatchNoJustifications() { + if (intMatchWeigher != null) { + return (impacter, a, b, c, d) -> { + int matchWeight = intMatchWeigher.applyAsInt(a, b, c, d); + return impactWithConstraintMatchNoJustifications(impacter, matchWeight); + }; + } else if (longMatchWeigher != null) { + return (impacter, a, b, c, d) -> { + long matchWeight = longMatchWeigher.applyAsLong(a, b, c, d); + return impactWithConstraintMatchNoJustifications(impacter, matchWeight); + }; + } else if (bigDecimalMatchWeigher != null) { + return (impacter, a, b, c, d) -> { + BigDecimal matchWeight = bigDecimalMatchWeigher.apply(a, b, c, d); + return impactWithConstraintMatchNoJustifications(impacter, matchWeight); + }; + } else { + throw new IllegalStateException("Impossible state: neither of the supported match weighers provided."); + } + } + // ************************************************************************ // Equality for node sharing // ************************************************************************ diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetScoringTriConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetScoringTriConstraintStream.java index d4a3d95b80..a612b983bf 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetScoringTriConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetScoringTriConstraintStream.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.score.stream.bavet.tri; +import static ai.timefold.solver.core.impl.score.stream.bavet.common.BavetScoringConstraintStream.impactWithConstraintMatchNoJustifications; + import java.math.BigDecimal; import ai.timefold.solver.core.api.function.QuadFunction; @@ -7,6 +9,7 @@ import ai.timefold.solver.core.api.function.ToLongTriFunction; import ai.timefold.solver.core.api.function.TriFunction; import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraint; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintFactory; import ai.timefold.solver.core.impl.score.stream.bavet.common.BavetScoringConstraintStream; @@ -70,14 +73,22 @@ public void setConstraint(BavetConstraint constraint) { @Override public > void buildNode(NodeBuildHelper buildHelper) { assertEmptyChildStreamList(); - var constraintMatchEnabled = buildHelper.getScoreInliner().isConstraintMatchEnabled(); - var scoreImpacter = constraintMatchEnabled ? buildScoreImpacterWithConstraintMatch() : buildScoreImpacter(); + var scoreImpacter = buildScoreImpacter(buildHelper.getScoreInliner().getConstraintMatchPolicy()); var weightedScoreImpacter = buildHelper.getScoreInliner().buildWeightedScoreImpacter(constraint); var scorer = new TriScorer<>(weightedScoreImpacter, scoreImpacter, buildHelper.reserveTupleStoreIndex(parent.getTupleSource())); buildHelper.putInsertUpdateRetract(this, scorer); } + private QuadFunction, A, B, C, UndoScoreImpacter> + buildScoreImpacter(ConstraintMatchPolicy constraintMatchPolicy) { + return switch (constraintMatchPolicy) { + case DISABLED -> buildScoreImpacter(); + case ENABLED -> buildScoreImpacterWithConstraintMatch(); + case ENABLED_WITHOUT_JUSTIFICATIONS -> buildScoreImpacterWithConstraintMatchNoJustifications(); + }; + } + private QuadFunction, A, B, C, UndoScoreImpacter> buildScoreImpacter() { if (intMatchWeigher != null) { return (impacter, a, b, c) -> { @@ -144,6 +155,28 @@ public > void buildNode(NodeBuildHelper bui return impacter.impactScore(matchWeight, constraintMatchSupplier); } + private QuadFunction, A, B, C, UndoScoreImpacter> + buildScoreImpacterWithConstraintMatchNoJustifications() { + if (intMatchWeigher != null) { + return (impacter, a, b, c) -> { + int matchWeight = intMatchWeigher.applyAsInt(a, b, c); + return impactWithConstraintMatchNoJustifications(impacter, matchWeight); + }; + } else if (longMatchWeigher != null) { + return (impacter, a, b, c) -> { + long matchWeight = longMatchWeigher.applyAsLong(a, b, c); + return impactWithConstraintMatchNoJustifications(impacter, matchWeight); + }; + } else if (bigDecimalMatchWeigher != null) { + return (impacter, a, b, c) -> { + BigDecimal matchWeight = bigDecimalMatchWeigher.apply(a, b, c); + return impactWithConstraintMatchNoJustifications(impacter, matchWeight); + }; + } else { + throw new IllegalStateException("Impossible state: neither of the supported match weighers provided."); + } + } + // ************************************************************************ // Equality for node sharing // ************************************************************************ diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetScoringUniConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetScoringUniConstraintStream.java index 187adea9e0..517e54c664 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetScoringUniConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetScoringUniConstraintStream.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.score.stream.bavet.uni; +import static ai.timefold.solver.core.impl.score.stream.bavet.common.BavetScoringConstraintStream.impactWithConstraintMatchNoJustifications; + import java.math.BigDecimal; import java.util.function.BiFunction; import java.util.function.Function; @@ -7,6 +9,7 @@ import java.util.function.ToLongFunction; import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraint; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintFactory; import ai.timefold.solver.core.impl.score.stream.bavet.common.BavetScoringConstraintStream; @@ -69,14 +72,22 @@ public void setConstraint(BavetConstraint constraint) { @Override public > void buildNode(NodeBuildHelper buildHelper) { assertEmptyChildStreamList(); - var constraintMatchEnabled = buildHelper.getScoreInliner().isConstraintMatchEnabled(); - var scoreImpacter = constraintMatchEnabled ? buildScoreImpacterWithConstraintMatch() : buildScoreImpacter(); + var scoreImpacter = buildScoreImpacter(buildHelper.getScoreInliner().getConstraintMatchPolicy()); var weightedScoreImpacter = buildHelper.getScoreInliner().buildWeightedScoreImpacter(constraint); var scorer = new UniScorer<>(weightedScoreImpacter, scoreImpacter, buildHelper.reserveTupleStoreIndex(parent.getTupleSource())); buildHelper.putInsertUpdateRetract(this, scorer); } + private BiFunction, A, UndoScoreImpacter> + buildScoreImpacter(ConstraintMatchPolicy constraintMatchPolicy) { + return switch (constraintMatchPolicy) { + case DISABLED -> buildScoreImpacter(); + case ENABLED -> buildScoreImpacterWithConstraintMatch(); + case ENABLED_WITHOUT_JUSTIFICATIONS -> buildScoreImpacterWithConstraintMatchNoJustifications(); + }; + } + private BiFunction, A, UndoScoreImpacter> buildScoreImpacter() { if (intMatchWeigher != null) { return (impacter, a) -> { @@ -143,6 +154,28 @@ public > void buildNode(NodeBuildHelper bui return impacter.impactScore(matchWeight, constraintMatchSupplier); } + private BiFunction, A, UndoScoreImpacter> + buildScoreImpacterWithConstraintMatchNoJustifications() { + if (intMatchWeigher != null) { + return (impacter, a) -> { + int matchWeight = intMatchWeigher.applyAsInt(a); + return impactWithConstraintMatchNoJustifications(impacter, matchWeight); + }; + } else if (longMatchWeigher != null) { + return (impacter, a) -> { + long matchWeight = longMatchWeigher.applyAsLong(a); + return impactWithConstraintMatchNoJustifications(impacter, matchWeight); + }; + } else if (bigDecimalMatchWeigher != null) { + return (impacter, a) -> { + BigDecimal matchWeight = bigDecimalMatchWeigher.apply(a); + return impactWithConstraintMatchNoJustifications(impacter, matchWeight); + }; + } else { + throw new IllegalStateException("Impossible state: neither of the supported match weighers provided."); + } + } + // ************************************************************************ // Equality for node sharing // ************************************************************************ diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamScoreDirectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamScoreDirectorFactory.java index 25347c650d..a7e091da9a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamScoreDirectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamScoreDirectorFactory.java @@ -4,6 +4,7 @@ import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.stream.ConstraintMetaModel; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.director.AbstractScoreDirectorFactory; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.score.director.ScoreDirectorFactory; @@ -40,6 +41,6 @@ public boolean supportsConstraintMatching() { @Override public abstract InnerScoreDirector buildDerivedScoreDirector(boolean lookUpEnabled, - boolean constraintMatchEnabledPreference); + ConstraintMatchPolicy constraintMatchPolicy); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/AbstractScoreInliner.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/AbstractScoreInliner.java index e6f327bc0b..8921d201bf 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/AbstractScoreInliner.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/AbstractScoreInliner.java @@ -1,6 +1,7 @@ package ai.timefold.solver.core.impl.score.stream.common.inliner; import java.lang.reflect.InvocationTargetException; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import java.util.TreeMap; @@ -23,13 +24,13 @@ import ai.timefold.solver.core.impl.score.buildin.SimpleBigDecimalScoreDefinition; import ai.timefold.solver.core.impl.score.buildin.SimpleLongScoreDefinition; import ai.timefold.solver.core.impl.score.buildin.SimpleScoreDefinition; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.constraint.DefaultConstraintMatchTotal; import ai.timefold.solver.core.impl.score.constraint.DefaultIndictment; import ai.timefold.solver.core.impl.score.definition.ScoreDefinition; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraint; import ai.timefold.solver.core.impl.util.CollectionUtils; import ai.timefold.solver.core.impl.util.ElementAwareList; -import ai.timefold.solver.core.impl.util.ElementAwareListEntry; /** * Keeps track of the working score and constraint matches for a single constraint session. @@ -45,38 +46,37 @@ public abstract class AbstractScoreInliner> { public static , ScoreInliner_ extends AbstractScoreInliner> ScoreInliner_ buildScoreInliner(ScoreDefinition scoreDefinition, Map constraintWeightMap, - boolean constraintMatchEnabled) { + ConstraintMatchPolicy constraintMatchPolicy) { if (scoreDefinition instanceof SimpleScoreDefinition) { - return (ScoreInliner_) new SimpleScoreInliner((Map) constraintWeightMap, constraintMatchEnabled); + return (ScoreInliner_) new SimpleScoreInliner((Map) constraintWeightMap, constraintMatchPolicy); } else if (scoreDefinition instanceof SimpleLongScoreDefinition) { - return (ScoreInliner_) new SimpleLongScoreInliner((Map) constraintWeightMap, constraintMatchEnabled); + return (ScoreInliner_) new SimpleLongScoreInliner((Map) constraintWeightMap, constraintMatchPolicy); } else if (scoreDefinition instanceof SimpleBigDecimalScoreDefinition) { - return (ScoreInliner_) new SimpleBigDecimalScoreInliner((Map) constraintWeightMap, constraintMatchEnabled); + return (ScoreInliner_) new SimpleBigDecimalScoreInliner((Map) constraintWeightMap, constraintMatchPolicy); } else if (scoreDefinition instanceof HardSoftScoreDefinition) { - return (ScoreInliner_) new HardSoftScoreInliner((Map) constraintWeightMap, constraintMatchEnabled); + return (ScoreInliner_) new HardSoftScoreInliner((Map) constraintWeightMap, constraintMatchPolicy); } else if (scoreDefinition instanceof HardSoftLongScoreDefinition) { - return (ScoreInliner_) new HardSoftLongScoreInliner((Map) constraintWeightMap, constraintMatchEnabled); + return (ScoreInliner_) new HardSoftLongScoreInliner((Map) constraintWeightMap, constraintMatchPolicy); } else if (scoreDefinition instanceof HardSoftBigDecimalScoreDefinition) { - return (ScoreInliner_) new HardSoftBigDecimalScoreInliner((Map) constraintWeightMap, constraintMatchEnabled); + return (ScoreInliner_) new HardSoftBigDecimalScoreInliner((Map) constraintWeightMap, constraintMatchPolicy); } else if (scoreDefinition instanceof HardMediumSoftScoreDefinition) { - return (ScoreInliner_) new HardMediumSoftScoreInliner((Map) constraintWeightMap, constraintMatchEnabled); + return (ScoreInliner_) new HardMediumSoftScoreInliner((Map) constraintWeightMap, constraintMatchPolicy); } else if (scoreDefinition instanceof HardMediumSoftLongScoreDefinition) { - return (ScoreInliner_) new HardMediumSoftLongScoreInliner((Map) constraintWeightMap, constraintMatchEnabled); + return (ScoreInliner_) new HardMediumSoftLongScoreInliner((Map) constraintWeightMap, constraintMatchPolicy); } else if (scoreDefinition instanceof HardMediumSoftBigDecimalScoreDefinition) { - return (ScoreInliner_) new HardMediumSoftBigDecimalScoreInliner((Map) constraintWeightMap, constraintMatchEnabled); + return (ScoreInliner_) new HardMediumSoftBigDecimalScoreInliner((Map) constraintWeightMap, constraintMatchPolicy); } else if (scoreDefinition instanceof BendableScoreDefinition bendableScoreDefinition) { - return (ScoreInliner_) new BendableScoreInliner((Map) constraintWeightMap, constraintMatchEnabled, - bendableScoreDefinition.getHardLevelsSize(), - bendableScoreDefinition.getSoftLevelsSize()); + return (ScoreInliner_) new BendableScoreInliner((Map) constraintWeightMap, constraintMatchPolicy, + bendableScoreDefinition.getHardLevelsSize(), bendableScoreDefinition.getSoftLevelsSize()); } else if (scoreDefinition instanceof BendableLongScoreDefinition bendableScoreDefinition) { - return (ScoreInliner_) new BendableLongScoreInliner((Map) constraintWeightMap, constraintMatchEnabled, + return (ScoreInliner_) new BendableLongScoreInliner((Map) constraintWeightMap, constraintMatchPolicy, bendableScoreDefinition.getHardLevelsSize(), bendableScoreDefinition.getSoftLevelsSize()); } else if (scoreDefinition instanceof BendableBigDecimalScoreDefinition bendableScoreDefinition) { - return (ScoreInliner_) new BendableBigDecimalScoreInliner((Map) constraintWeightMap, constraintMatchEnabled, + return (ScoreInliner_) new BendableBigDecimalScoreInliner((Map) constraintWeightMap, constraintMatchPolicy, bendableScoreDefinition.getHardLevelsSize(), bendableScoreDefinition.getSoftLevelsSize()); } else { - String customScoreInlinerClassName = System.getProperty(CUSTOM_SCORE_INLINER_CLASS_PROPERTY_NAME); + var customScoreInlinerClassName = System.getProperty(CUSTOM_SCORE_INLINER_CLASS_PROPERTY_NAME); if (customScoreInlinerClassName == null) { throw new UnsupportedOperationException("Unknown score definition class (" + scoreDefinition.getClass().getCanonicalName() + ").\n" + @@ -86,7 +86,7 @@ public abstract class AbstractScoreInliner> { "Note: support for custom scores will be removed in Timefold 2.0."); } try { - Class customScoreInlinerClass = Class.forName(customScoreInlinerClassName); + var customScoreInlinerClass = Class.forName(customScoreInlinerClassName); if (!AbstractScoreInliner.class.isAssignableFrom(customScoreInlinerClass)) { throw new IllegalStateException("Custom score inliner class (" + customScoreInlinerClassName + ") does not extend " + AbstractScoreInliner.class.getCanonicalName() + ".\n" + @@ -104,23 +104,24 @@ public abstract class AbstractScoreInliner> { } } - protected final boolean constraintMatchEnabled; + protected final ConstraintMatchPolicy constraintMatchPolicy; protected final Map constraintWeightMap; private final Map>> constraintMatchMap; private Map> constraintIdToConstraintMatchTotalMap = null; private Map> indictmentMap = null; - protected AbstractScoreInliner(Map constraintWeightMap, boolean constraintMatchEnabled) { - this.constraintMatchEnabled = constraintMatchEnabled; + protected AbstractScoreInliner(Map constraintWeightMap, ConstraintMatchPolicy constraintMatchPolicy) { + this.constraintMatchPolicy = constraintMatchPolicy; constraintWeightMap.forEach(this::validateConstraintWeight); this.constraintWeightMap = constraintWeightMap; - this.constraintMatchMap = - constraintMatchEnabled ? CollectionUtils.newIdentityHashMap(constraintWeightMap.size()) : null; - if (constraintMatchEnabled) { + if (constraintMatchPolicy.isEnabled()) { + this.constraintMatchMap = CollectionUtils.newIdentityHashMap(constraintWeightMap.size()); for (var constraint : constraintWeightMap.keySet()) { // Ensure that even constraints without matches have their entry. - constraintMatchMap.put(constraint, new ElementAwareList<>()); + this.constraintMatchMap.put(constraint, new ElementAwareList<>()); } + } else { + this.constraintMatchMap = Collections.emptyMap(); } } @@ -144,12 +145,12 @@ private void validateConstraintWeight(Constraint constraint, Score_ constraintWe protected final UndoScoreImpacter addConstraintMatch(Constraint constraint, Score_ score, ConstraintMatchSupplier constraintMatchSupplier, UndoScoreImpacter undoScoreImpact) { - ElementAwareList> constraintMatchList = getConstraintMatchList(constraint); + var constraintMatchList = getConstraintMatchList(constraint); /* * Creating a constraint match is a heavy operation which may yet be undone. * Defer creation of the constraint match until a later point. */ - ElementAwareListEntry> entry = + var entry = constraintMatchList.add(new ConstraintMatchCarrier<>(constraintMatchSupplier, constraint, score)); clearMaps(); return () -> { @@ -161,7 +162,7 @@ protected final UndoScoreImpacter addConstraintMatch(Constraint constraint, Scor private ElementAwareList> getConstraintMatchList(Constraint constraint) { // Optimization: computeIfAbsent() would have created a lambda on the hot path. - ElementAwareList> constraintMatchList = constraintMatchMap.get(constraint); + var constraintMatchList = constraintMatchMap.get(constraint); if (constraintMatchList == null) { throw new IllegalStateException( "Impossible state: Unknown constraint (%s)." @@ -175,12 +176,14 @@ private void clearMaps() { indictmentMap = null; } - public boolean isConstraintMatchEnabled() { - return constraintMatchEnabled; + public ConstraintMatchPolicy getConstraintMatchPolicy() { + return constraintMatchPolicy; } public final Map> getConstraintIdToConstraintMatchTotalMap() { - if (constraintIdToConstraintMatchTotalMap == null) { + if (!constraintMatchPolicy.isEnabled()) { + throw new IllegalStateException("Impossible state: Method called while constraint matching is disabled."); + } else if (constraintIdToConstraintMatchTotalMap == null) { rebuildConstraintMatchTotals(); } return constraintIdToConstraintMatchTotalMap; @@ -203,7 +206,9 @@ private void rebuildConstraintMatchTotals() { } public final Map> getIndictmentMap() { - if (indictmentMap == null) { + if (!constraintMatchPolicy.isJustificationEnabled()) { + throw new IllegalStateException("Impossible state: Method called while justifications are disabled."); + } else if (indictmentMap == null) { rebuildIndictments(); } return indictmentMap; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableBigDecimalScoreContext.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableBigDecimalScoreContext.java index 4404ad6e29..fee99387a5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableBigDecimalScoreContext.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableBigDecimalScoreContext.java @@ -33,7 +33,7 @@ public UndoScoreImpacter changeSoftScoreBy(BigDecimal matchWeight, parent.softScores[scoreLevel] = parent.softScores[scoreLevel].add(softImpact); UndoScoreImpacter undoScoreImpact = () -> parent.softScores[scoreLevel] = parent.softScores[scoreLevel].subtract(softImpact); - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, @@ -47,7 +47,7 @@ public UndoScoreImpacter changeHardScoreBy(BigDecimal matchWeight, parent.hardScores[scoreLevel] = parent.hardScores[scoreLevel].add(hardImpact); UndoScoreImpacter undoScoreImpact = () -> parent.hardScores[scoreLevel] = parent.hardScores[scoreLevel].subtract(hardImpact); - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, @@ -77,7 +77,7 @@ public UndoScoreImpacter changeScoreBy(BigDecimal matchWeight, parent.softScores[softScoreLevel] = parent.softScores[softScoreLevel].subtract(softImpacts[softScoreLevel]); } }; - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, BendableBigDecimalScore.of(hardImpacts, softImpacts), diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableBigDecimalScoreInliner.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableBigDecimalScoreInliner.java index 80ffe0c056..99ca219985 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableBigDecimalScoreInliner.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableBigDecimalScoreInliner.java @@ -6,6 +6,7 @@ import ai.timefold.solver.core.api.score.buildin.bendablebigdecimal.BendableBigDecimalScore; import ai.timefold.solver.core.api.score.stream.Constraint; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraint; final class BendableBigDecimalScoreInliner extends AbstractScoreInliner { @@ -13,9 +14,9 @@ final class BendableBigDecimalScoreInliner extends AbstractScoreInliner constraintWeightMap, boolean constraintMatchEnabled, - int hardLevelsSize, int softLevelsSize) { - super(constraintWeightMap, constraintMatchEnabled); + BendableBigDecimalScoreInliner(Map constraintWeightMap, + ConstraintMatchPolicy constraintMatchPolicy, int hardLevelsSize, int softLevelsSize) { + super(constraintWeightMap, constraintMatchPolicy); hardScores = new BigDecimal[hardLevelsSize]; Arrays.fill(hardScores, BigDecimal.ZERO); softScores = new BigDecimal[softLevelsSize]; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableLongScoreContext.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableLongScoreContext.java index a345f95108..3b10961213 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableLongScoreContext.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableLongScoreContext.java @@ -31,7 +31,7 @@ public UndoScoreImpacter changeSoftScoreBy(long matchWeight, parent.softScores[scoreLevel] += softImpact; UndoScoreImpacter undoScoreImpact = () -> parent.softScores[scoreLevel] -= softImpact; ; - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, @@ -44,7 +44,7 @@ public UndoScoreImpacter changeHardScoreBy(long matchWeight, long hardImpact = scoreLevelWeight * matchWeight; parent.hardScores[scoreLevel] += hardImpact; UndoScoreImpacter undoScoreImpact = () -> parent.hardScores[scoreLevel] -= hardImpact; - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, @@ -74,7 +74,7 @@ public UndoScoreImpacter changeScoreBy(long matchWeight, parent.softScores[softScoreLevel] -= softImpacts[softScoreLevel]; } }; - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, BendableLongScore.of(hardImpacts, softImpacts), diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableLongScoreInliner.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableLongScoreInliner.java index a6e3cf7104..f095ebbadb 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableLongScoreInliner.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableLongScoreInliner.java @@ -5,6 +5,7 @@ import ai.timefold.solver.core.api.score.buildin.bendablelong.BendableLongScore; import ai.timefold.solver.core.api.score.stream.Constraint; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraint; public final class BendableLongScoreInliner extends AbstractScoreInliner { @@ -12,9 +13,10 @@ public final class BendableLongScoreInliner extends AbstractScoreInliner constraintWeightMap, boolean constraintMatchEnabled, + BendableLongScoreInliner(Map constraintWeightMap, + ConstraintMatchPolicy constraintMatchPolicy, int hardLevelsSize, int softLevelsSize) { - super(constraintWeightMap, constraintMatchEnabled); + super(constraintWeightMap, constraintMatchPolicy); hardScores = new long[hardLevelsSize]; softScores = new long[softLevelsSize]; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableScoreContext.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableScoreContext.java index a6e9191938..8aef9b9d9d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableScoreContext.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableScoreContext.java @@ -30,7 +30,7 @@ public UndoScoreImpacter changeSoftScoreBy(int matchWeight, int softImpact = scoreLevelWeight * matchWeight; parent.softScores[scoreLevel] += softImpact; UndoScoreImpacter undoScoreImpact = () -> parent.softScores[scoreLevel] -= softImpact; - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, @@ -43,7 +43,7 @@ public UndoScoreImpacter changeHardScoreBy(int matchWeight, int hardImpact = scoreLevelWeight * matchWeight; parent.hardScores[scoreLevel] += hardImpact; UndoScoreImpacter undoScoreImpact = () -> parent.hardScores[scoreLevel] -= hardImpact; - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, @@ -72,7 +72,7 @@ public UndoScoreImpacter changeScoreBy(int matchWeight, ConstraintMatchSupplier< parent.softScores[softScoreLevel] -= softImpacts[softScoreLevel]; } }; - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, BendableScore.of(hardImpacts, softImpacts), constraintMatchSupplier); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableScoreInliner.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableScoreInliner.java index b9380e54b9..29094d8a2d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableScoreInliner.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableScoreInliner.java @@ -5,6 +5,7 @@ import ai.timefold.solver.core.api.score.buildin.bendable.BendableScore; import ai.timefold.solver.core.api.score.stream.Constraint; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraint; final class BendableScoreInliner extends AbstractScoreInliner { @@ -12,9 +13,10 @@ final class BendableScoreInliner extends AbstractScoreInliner { final int[] hardScores; final int[] softScores; - BendableScoreInliner(Map constraintWeightMap, boolean constraintMatchEnabled, int hardLevelsSize, + BendableScoreInliner(Map constraintWeightMap, ConstraintMatchPolicy constraintMatchPolicy, + int hardLevelsSize, int softLevelsSize) { - super(constraintWeightMap, constraintMatchEnabled); + super(constraintWeightMap, constraintMatchPolicy); hardScores = new int[hardLevelsSize]; softScores = new int[softLevelsSize]; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java index fe657d6b60..62ac63e0be 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ConstraintMatchSupplier.java @@ -15,7 +15,6 @@ import ai.timefold.solver.core.api.score.constraint.ConstraintMatch; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintJustification; -import ai.timefold.solver.core.api.score.stream.DefaultConstraintJustification; /** * Allows creating {@link ConstraintMatch} instances lazily if and only if they are required by the end user. @@ -33,10 +32,15 @@ public interface ConstraintMatchSupplier> extends BiFunction> { + /** + * + * @return the constraint match returned by the supplier will have its justification set to null. + * This is useful when the justifications are disabled, to save memory. + * @param + */ static > ConstraintMatchSupplier empty() { - return (constraint, impact) -> new ConstraintMatch<>(constraint.getConstraintRef(), - DefaultConstraintJustification.of(impact), - Collections.emptyList(), impact); + return (constraint, impact) -> new ConstraintMatch<>(constraint.getConstraintRef(), null, Collections.emptyList(), + impact); } static > ConstraintMatchSupplier of( diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftBigDecimalScoreContext.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftBigDecimalScoreContext.java index 5a7cfa9447..f4038aa9cd 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftBigDecimalScoreContext.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftBigDecimalScoreContext.java @@ -18,7 +18,7 @@ public UndoScoreImpacter changeSoftScoreBy(BigDecimal matchWeight, BigDecimal softImpact = constraintWeight.softScore().multiply(matchWeight); parent.softScore = parent.softScore.add(softImpact); UndoScoreImpacter undoScoreImpact = () -> parent.softScore = parent.softScore.subtract(softImpact); - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, HardMediumSoftBigDecimalScore.ofSoft(softImpact), @@ -30,7 +30,7 @@ public UndoScoreImpacter changeMediumScoreBy(BigDecimal matchWeight, BigDecimal mediumImpact = constraintWeight.mediumScore().multiply(matchWeight); parent.mediumScore = parent.mediumScore.add(mediumImpact); UndoScoreImpacter undoScoreImpact = () -> parent.mediumScore = parent.mediumScore.subtract(mediumImpact); - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, HardMediumSoftBigDecimalScore.ofMedium(mediumImpact), @@ -42,7 +42,7 @@ public UndoScoreImpacter changeHardScoreBy(BigDecimal matchWeight, BigDecimal hardImpact = constraintWeight.hardScore().multiply(matchWeight); parent.hardScore = parent.hardScore.add(hardImpact); UndoScoreImpacter undoScoreImpact = () -> parent.hardScore = parent.hardScore.subtract(hardImpact); - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, HardMediumSoftBigDecimalScore.ofHard(hardImpact), @@ -62,7 +62,7 @@ public UndoScoreImpacter changeScoreBy(BigDecimal matchWeight, parent.mediumScore = parent.mediumScore.subtract(mediumImpact); parent.softScore = parent.softScore.subtract(softImpact); }; - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftBigDecimalScoreInliner.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftBigDecimalScoreInliner.java index 63d8e5031c..0430b430ca 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftBigDecimalScoreInliner.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftBigDecimalScoreInliner.java @@ -5,6 +5,7 @@ import ai.timefold.solver.core.api.score.buildin.hardmediumsoftbigdecimal.HardMediumSoftBigDecimalScore; import ai.timefold.solver.core.api.score.stream.Constraint; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraint; final class HardMediumSoftBigDecimalScoreInliner extends AbstractScoreInliner { @@ -14,8 +15,8 @@ final class HardMediumSoftBigDecimalScoreInliner extends AbstractScoreInliner constraintWeightMap, - boolean constraintMatchEnabled) { - super(constraintWeightMap, constraintMatchEnabled); + ConstraintMatchPolicy constraintMatchPolicy) { + super(constraintWeightMap, constraintMatchPolicy); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftLongScoreContext.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftLongScoreContext.java index 8574ed74fa..cf4a3a64d6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftLongScoreContext.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftLongScoreContext.java @@ -15,7 +15,7 @@ public UndoScoreImpacter changeSoftScoreBy(long matchWeight, long softImpact = constraintWeight.softScore() * matchWeight; parent.softScore += softImpact; UndoScoreImpacter undoScoreImpact = () -> parent.softScore -= softImpact; - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, HardMediumSoftLongScore.ofSoft(softImpact), constraintMatchSupplier); @@ -26,7 +26,7 @@ public UndoScoreImpacter changeMediumScoreBy(long matchWeight, long mediumImpact = constraintWeight.mediumScore() * matchWeight; parent.mediumScore += mediumImpact; UndoScoreImpacter undoScoreImpact = () -> parent.mediumScore -= mediumImpact; - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, HardMediumSoftLongScore.ofMedium(mediumImpact), @@ -38,7 +38,7 @@ public UndoScoreImpacter changeHardScoreBy(long matchWeight, long hardImpact = constraintWeight.hardScore() * matchWeight; parent.hardScore += hardImpact; UndoScoreImpacter undoScoreImpact = () -> parent.hardScore -= hardImpact; - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, HardMediumSoftLongScore.ofHard(hardImpact), constraintMatchSupplier); @@ -57,7 +57,7 @@ public UndoScoreImpacter changeScoreBy(long matchWeight, parent.mediumScore -= mediumImpact; parent.softScore -= softImpact; }; - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, HardMediumSoftLongScore.of(hardImpact, mediumImpact, softImpact), diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftLongScoreInliner.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftLongScoreInliner.java index 634aab0551..2c0e7ed859 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftLongScoreInliner.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftLongScoreInliner.java @@ -4,6 +4,7 @@ import ai.timefold.solver.core.api.score.buildin.hardmediumsoftlong.HardMediumSoftLongScore; import ai.timefold.solver.core.api.score.stream.Constraint; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraint; final class HardMediumSoftLongScoreInliner extends AbstractScoreInliner { @@ -13,8 +14,8 @@ final class HardMediumSoftLongScoreInliner extends AbstractScoreInliner constraintWeightMap, - boolean constraintMatchEnabled) { - super(constraintWeightMap, constraintMatchEnabled); + ConstraintMatchPolicy constraintMatchPolicy) { + super(constraintWeightMap, constraintMatchPolicy); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftScoreContext.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftScoreContext.java index 2e6843cccf..653a6c7f97 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftScoreContext.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftScoreContext.java @@ -15,7 +15,7 @@ public UndoScoreImpacter changeSoftScoreBy(int matchWeight, int softImpact = constraintWeight.softScore() * matchWeight; parent.softScore += softImpact; UndoScoreImpacter undoScoreImpact = () -> parent.softScore -= softImpact; - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, HardMediumSoftScore.ofSoft(softImpact), constraintMatchSupplier); @@ -26,7 +26,7 @@ public UndoScoreImpacter changeMediumScoreBy(int matchWeight, int mediumImpact = constraintWeight.mediumScore() * matchWeight; parent.mediumScore += mediumImpact; UndoScoreImpacter undoScoreImpact = () -> parent.mediumScore -= mediumImpact; - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, HardMediumSoftScore.ofMedium(mediumImpact), constraintMatchSupplier); @@ -37,7 +37,7 @@ public UndoScoreImpacter changeHardScoreBy(int matchWeight, int hardImpact = constraintWeight.hardScore() * matchWeight; parent.hardScore += hardImpact; UndoScoreImpacter undoScoreImpact = () -> parent.hardScore -= hardImpact; - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, HardMediumSoftScore.ofHard(hardImpact), constraintMatchSupplier); @@ -56,7 +56,7 @@ public UndoScoreImpacter changeScoreBy(int matchWeight, parent.mediumScore -= mediumImpact; parent.softScore -= softImpact; }; - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, HardMediumSoftScore.of(hardImpact, mediumImpact, softImpact), diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftScoreInliner.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftScoreInliner.java index 51f7a19059..24ddfa3996 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftScoreInliner.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftScoreInliner.java @@ -4,6 +4,7 @@ import ai.timefold.solver.core.api.score.buildin.hardmediumsoft.HardMediumSoftScore; import ai.timefold.solver.core.api.score.stream.Constraint; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraint; final class HardMediumSoftScoreInliner extends AbstractScoreInliner { @@ -12,8 +13,9 @@ final class HardMediumSoftScoreInliner extends AbstractScoreInliner constraintWeightMap, boolean constraintMatchEnabled) { - super(constraintWeightMap, constraintMatchEnabled); + HardMediumSoftScoreInliner(Map constraintWeightMap, + ConstraintMatchPolicy constraintMatchPolicy) { + super(constraintWeightMap, constraintMatchPolicy); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftBigDecimalScoreContext.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftBigDecimalScoreContext.java index f619332e35..6e01aa8213 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftBigDecimalScoreContext.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftBigDecimalScoreContext.java @@ -17,7 +17,7 @@ public UndoScoreImpacter changeSoftScoreBy(BigDecimal matchWeight, BigDecimal softImpact = constraintWeight.softScore().multiply(matchWeight); parent.softScore = parent.softScore.add(softImpact); UndoScoreImpacter undoScoreImpact = () -> parent.softScore = parent.softScore.subtract(softImpact); - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, HardSoftBigDecimalScore.ofSoft(softImpact), constraintMatchSupplier); @@ -28,7 +28,7 @@ public UndoScoreImpacter changeHardScoreBy(BigDecimal matchWeight, BigDecimal hardImpact = constraintWeight.hardScore().multiply(matchWeight); parent.hardScore = parent.hardScore.add(hardImpact); UndoScoreImpacter undoScoreImpact = () -> parent.hardScore = parent.hardScore.subtract(hardImpact); - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, HardSoftBigDecimalScore.ofHard(hardImpact), constraintMatchSupplier); @@ -44,7 +44,7 @@ public UndoScoreImpacter changeScoreBy(BigDecimal matchWeight, parent.hardScore = parent.hardScore.subtract(hardImpact); parent.softScore = parent.softScore.subtract(softImpact); }; - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, HardSoftBigDecimalScore.of(hardImpact, softImpact), diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftBigDecimalScoreInliner.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftBigDecimalScoreInliner.java index 5d2885155e..8beedb78a8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftBigDecimalScoreInliner.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftBigDecimalScoreInliner.java @@ -5,6 +5,7 @@ import ai.timefold.solver.core.api.score.buildin.hardsoftbigdecimal.HardSoftBigDecimalScore; import ai.timefold.solver.core.api.score.stream.Constraint; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraint; final class HardSoftBigDecimalScoreInliner extends AbstractScoreInliner { @@ -13,8 +14,8 @@ final class HardSoftBigDecimalScoreInliner extends AbstractScoreInliner constraintWeightMap, - boolean constraintMatchEnabled) { - super(constraintWeightMap, constraintMatchEnabled); + ConstraintMatchPolicy constraintMatchPolicy) { + super(constraintWeightMap, constraintMatchPolicy); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftLongScoreContext.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftLongScoreContext.java index 9ac5b85108..9e5f831d8f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftLongScoreContext.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftLongScoreContext.java @@ -15,7 +15,7 @@ public UndoScoreImpacter changeSoftScoreBy(long matchWeight, long softImpact = constraintWeight.softScore() * matchWeight; parent.softScore += softImpact; UndoScoreImpacter undoScoreImpact = () -> parent.softScore -= softImpact; - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, HardSoftLongScore.ofSoft(softImpact), constraintMatchSupplier); @@ -26,7 +26,7 @@ public UndoScoreImpacter changeHardScoreBy(long matchWeight, long hardImpact = constraintWeight.hardScore() * matchWeight; parent.hardScore += hardImpact; UndoScoreImpacter undoScoreImpact = () -> parent.hardScore -= hardImpact; - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, HardSoftLongScore.ofHard(hardImpact), constraintMatchSupplier); @@ -42,7 +42,7 @@ public UndoScoreImpacter changeScoreBy(long matchWeight, parent.hardScore -= hardImpact; parent.softScore -= softImpact; }; - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, HardSoftLongScore.of(hardImpact, softImpact), diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftLongScoreInliner.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftLongScoreInliner.java index 05e34d7de0..d9504cf34f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftLongScoreInliner.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftLongScoreInliner.java @@ -4,6 +4,7 @@ import ai.timefold.solver.core.api.score.buildin.hardsoftlong.HardSoftLongScore; import ai.timefold.solver.core.api.score.stream.Constraint; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraint; final class HardSoftLongScoreInliner extends AbstractScoreInliner { @@ -11,8 +12,9 @@ final class HardSoftLongScoreInliner extends AbstractScoreInliner constraintWeightMap, boolean constraintMatchEnabled) { - super(constraintWeightMap, constraintMatchEnabled); + HardSoftLongScoreInliner(Map constraintWeightMap, + ConstraintMatchPolicy constraintMatchPolicy) { + super(constraintWeightMap, constraintMatchPolicy); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftScoreContext.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftScoreContext.java index e1c57b42c9..c0038ecf9c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftScoreContext.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftScoreContext.java @@ -15,7 +15,7 @@ public UndoScoreImpacter changeSoftScoreBy(int matchWeight, int softImpact = constraintWeight.softScore() * matchWeight; parent.softScore += softImpact; UndoScoreImpacter undoScoreImpact = () -> parent.softScore -= softImpact; - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, HardSoftScore.ofSoft(softImpact), constraintMatchSupplier); @@ -26,7 +26,7 @@ public UndoScoreImpacter changeHardScoreBy(int matchWeight, int hardImpact = constraintWeight.hardScore() * matchWeight; parent.hardScore += hardImpact; UndoScoreImpacter undoScoreImpact = () -> parent.hardScore -= hardImpact; - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, HardSoftScore.ofHard(hardImpact), constraintMatchSupplier); @@ -41,7 +41,7 @@ public UndoScoreImpacter changeScoreBy(int matchWeight, ConstraintMatchSupplier< parent.hardScore -= hardImpact; parent.softScore -= softImpact; }; - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, HardSoftScore.of(hardImpact, softImpact), constraintMatchSupplier); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftScoreInliner.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftScoreInliner.java index ee06e62d02..38fe31e22a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftScoreInliner.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftScoreInliner.java @@ -4,6 +4,7 @@ import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; import ai.timefold.solver.core.api.score.stream.Constraint; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraint; final class HardSoftScoreInliner extends AbstractScoreInliner { @@ -11,8 +12,8 @@ final class HardSoftScoreInliner extends AbstractScoreInliner { int hardScore; int softScore; - HardSoftScoreInliner(Map constraintWeightMap, boolean constraintMatchEnabled) { - super(constraintWeightMap, constraintMatchEnabled); + HardSoftScoreInliner(Map constraintWeightMap, ConstraintMatchPolicy constraintMatchPolicy) { + super(constraintWeightMap, constraintMatchPolicy); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ScoreContext.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ScoreContext.java index 37a2c6f3a6..afc85abc1e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ScoreContext.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/ScoreContext.java @@ -1,6 +1,7 @@ package ai.timefold.solver.core.impl.score.stream.common.inliner; import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraint; public abstract class ScoreContext, ScoreInliner_ extends AbstractScoreInliner> { @@ -8,13 +9,13 @@ public abstract class ScoreContext, ScoreInliner_ e protected final ScoreInliner_ parent; protected final AbstractConstraint constraint; protected final Score_ constraintWeight; - protected final boolean constraintMatchEnabled; + protected final ConstraintMatchPolicy constraintMatchPolicy; protected ScoreContext(ScoreInliner_ parent, AbstractConstraint constraint, Score_ constraintWeight) { this.parent = parent; this.constraint = constraint; this.constraintWeight = constraintWeight; - this.constraintMatchEnabled = parent.constraintMatchEnabled; + this.constraintMatchPolicy = parent.constraintMatchPolicy; } public AbstractConstraint getConstraint() { @@ -25,10 +26,6 @@ public Score_ getConstraintWeight() { return constraintWeight; } - public boolean isConstraintMatchEnabled() { - return constraintMatchEnabled; - } - protected UndoScoreImpacter impactWithConstraintMatch(UndoScoreImpacter undoScoreImpact, Score_ score, ConstraintMatchSupplier constraintMatchSupplier) { return parent.addConstraintMatch(constraint, score, constraintMatchSupplier, undoScoreImpact); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleBigDecimalScoreContext.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleBigDecimalScoreContext.java index 33f3a1106f..580d59fa78 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleBigDecimalScoreContext.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleBigDecimalScoreContext.java @@ -17,7 +17,7 @@ public UndoScoreImpacter changeScoreBy(BigDecimal matchWeight, BigDecimal impact = constraintWeight.score().multiply(matchWeight); parent.score = parent.score.add(impact); UndoScoreImpacter undoScoreImpact = () -> parent.score = parent.score.subtract(impact); - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, SimpleBigDecimalScore.of(impact), constraintMatchSupplier); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleBigDecimalScoreInliner.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleBigDecimalScoreInliner.java index 65ec9f9631..cca2400d89 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleBigDecimalScoreInliner.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleBigDecimalScoreInliner.java @@ -5,14 +5,16 @@ import ai.timefold.solver.core.api.score.buildin.simplebigdecimal.SimpleBigDecimalScore; import ai.timefold.solver.core.api.score.stream.Constraint; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraint; final class SimpleBigDecimalScoreInliner extends AbstractScoreInliner { BigDecimal score = BigDecimal.ZERO; - SimpleBigDecimalScoreInliner(Map constraintWeightMap, boolean constraintMatchEnabled) { - super(constraintWeightMap, constraintMatchEnabled); + SimpleBigDecimalScoreInliner(Map constraintWeightMap, + ConstraintMatchPolicy constraintMatchPolicy) { + super(constraintWeightMap, constraintMatchPolicy); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleLongScoreContext.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleLongScoreContext.java index a66a739a02..ab57995777 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleLongScoreContext.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleLongScoreContext.java @@ -14,7 +14,7 @@ public UndoScoreImpacter changeScoreBy(long matchWeight, ConstraintMatchSupplier long impact = constraintWeight.score() * matchWeight; parent.score += impact; UndoScoreImpacter undoScoreImpact = () -> parent.score -= impact; - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, SimpleLongScore.of(impact), constraintMatchSupplier); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleLongScoreInliner.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleLongScoreInliner.java index 1d2dadc5d7..72aa0e606e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleLongScoreInliner.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleLongScoreInliner.java @@ -4,14 +4,15 @@ import ai.timefold.solver.core.api.score.buildin.simplelong.SimpleLongScore; import ai.timefold.solver.core.api.score.stream.Constraint; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraint; final class SimpleLongScoreInliner extends AbstractScoreInliner { long score; - SimpleLongScoreInliner(Map constraintWeightMap, boolean constraintMatchEnabled) { - super(constraintWeightMap, constraintMatchEnabled); + SimpleLongScoreInliner(Map constraintWeightMap, ConstraintMatchPolicy constraintMatchPolicy) { + super(constraintWeightMap, constraintMatchPolicy); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleScoreContext.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleScoreContext.java index d20f7864ad..adb93ae6d6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleScoreContext.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleScoreContext.java @@ -13,7 +13,7 @@ public UndoScoreImpacter changeScoreBy(int matchWeight, ConstraintMatchSupplier< int impact = constraintWeight.score() * matchWeight; parent.score += impact; UndoScoreImpacter undoScoreImpact = () -> parent.score -= impact; - if (!constraintMatchEnabled) { + if (!constraintMatchPolicy.isEnabled()) { return undoScoreImpact; } return impactWithConstraintMatch(undoScoreImpact, SimpleScore.of(impact), constraintMatchSupplier); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleScoreInliner.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleScoreInliner.java index 57bff32d2a..0127a3c602 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleScoreInliner.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleScoreInliner.java @@ -4,14 +4,15 @@ import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.score.stream.Constraint; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraint; final class SimpleScoreInliner extends AbstractScoreInliner { int score; - SimpleScoreInliner(Map constraintWeightMap, boolean constraintMatchEnabled) { - super(constraintWeightMap, constraintMatchEnabled); + SimpleScoreInliner(Map constraintWeightMap, ConstraintMatchPolicy constraintMatchPolicy) { + super(constraintWeightMap, constraintMatchPolicy); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/Assigner.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/Assigner.java index 6d7525a233..91d32d07d3 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/Assigner.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/Assigner.java @@ -41,8 +41,8 @@ public List apply(InnerScoreDirector scoreDi Assignment Recommendation API requires at most one uninitialized element in the solution.""" .formatted(originalSolution, uninitializedCount)); } - var originalScoreAnalysis = scoreDirector.buildScoreAnalysis(fetchPolicy == ScoreAnalysisFetchPolicy.FETCH_ALL, - InnerScoreDirector.ScoreAnalysisMode.RECOMMENDATION_API); + var originalScoreAnalysis = + scoreDirector.buildScoreAnalysis(fetchPolicy, InnerScoreDirector.ScoreAnalysisMode.RECOMMENDATION_API); var clonedElement = scoreDirector.lookUpWorkingObject(originalElement); var processor = new AssignmentProcessor<>(solverFactory, propositionFunction, recommendationConstructor, fetchPolicy, clonedElement, originalScoreAnalysis); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/AssignmentProcessor.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/AssignmentProcessor.java index fffffb3021..a8ce6cbe48 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/AssignmentProcessor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/AssignmentProcessor.java @@ -153,7 +153,7 @@ private Recommendation_ execute(InnerScoreDirector scoreDirec In_ clonedElement, Function propositionFunction) { try (var ephemeralMoveDirector = scoreDirector.getMoveDirector().ephemeral()) { move.execute(ephemeralMoveDirector); - var newScoreAnalysis = scoreDirector.buildScoreAnalysis(fetchPolicy == ScoreAnalysisFetchPolicy.FETCH_ALL); + var newScoreAnalysis = scoreDirector.buildScoreAnalysis(fetchPolicy); var newScoreDifference = newScoreAnalysis.diff(originalScoreAnalysis); var result = propositionFunction.apply(clonedElement); return recommendationConstructor.apply(moveIndex, result, newScoreDifference); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolutionManager.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolutionManager.java index 1d07209042..44a5cdc445 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolutionManager.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolutionManager.java @@ -17,6 +17,7 @@ import ai.timefold.solver.core.api.solver.SolverManager; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.score.DefaultScoreExplanation; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.score.director.InnerScoreDirectorFactory; @@ -52,19 +53,19 @@ public InnerScoreDirectorFactory getScoreDirectorFactory() { + ".update() with this solutionUpdatePolicy (" + solutionUpdatePolicy + ")."); } return callScoreDirector(solution, solutionUpdatePolicy, - s -> s.getSolutionDescriptor().getScore(s.getWorkingSolution()), false, false); + s -> s.getSolutionDescriptor().getScore(s.getWorkingSolution()), ConstraintMatchPolicy.DISABLED, false); } private Result_ callScoreDirector(Solution_ solution, SolutionUpdatePolicy solutionUpdatePolicy, Function, Result_> function, - boolean enableConstraintMatch, boolean cloneSolution) { + ConstraintMatchPolicy constraintMatchPolicy, boolean cloneSolution) { var isShadowVariableUpdateEnabled = solutionUpdatePolicy.isShadowVariableUpdateEnabled(); var nonNullSolution = Objects.requireNonNull(solution); - try (var scoreDirector = getScoreDirectorFactory().buildScoreDirector(cloneSolution, enableConstraintMatch, + try (var scoreDirector = getScoreDirectorFactory().buildScoreDirector(cloneSolution, constraintMatchPolicy, !isShadowVariableUpdateEnabled)) { nonNullSolution = cloneSolution ? scoreDirector.cloneSolution(nonNullSolution) : nonNullSolution; scoreDirector.setWorkingSolution(nonNullSolution); - if (enableConstraintMatch && !scoreDirector.isConstraintMatchEnabled()) { + if (constraintMatchPolicy.isEnabled() && !scoreDirector.getConstraintMatchPolicy().isEnabled()) { throw new IllegalStateException(""" Requested constraint matching but score director doesn't support it. Maybe use Constraint Streams instead of Easy or Incremental score calculator?"""); @@ -84,7 +85,8 @@ private Result_ callScoreDirector(Solution_ solution, public @NonNull ScoreExplanation explain(@NonNull Solution_ solution, @NonNull SolutionUpdatePolicy solutionUpdatePolicy) { var currentScore = (Score_) scoreDirectorFactory.getSolutionDescriptor().getScore(solution); - var explanation = callScoreDirector(solution, solutionUpdatePolicy, DefaultScoreExplanation::new, true, false); + var explanation = callScoreDirector(solution, solutionUpdatePolicy, DefaultScoreExplanation::new, + ConstraintMatchPolicy.ENABLED, false); assertFreshScore(solution, currentScore, explanation.getScore(), solutionUpdatePolicy); return explanation; } @@ -114,7 +116,7 @@ Current score (%s) and freshly calculated score (%s) for solution (%s) do not ma Objects.requireNonNull(fetchPolicy, "fetchPolicy"); var currentScore = (Score_) scoreDirectorFactory.getSolutionDescriptor().getScore(solution); var analysis = callScoreDirector(solution, solutionUpdatePolicy, - scoreDirector -> scoreDirector.buildScoreAnalysis(fetchPolicy == ScoreAnalysisFetchPolicy.FETCH_ALL), true, + scoreDirector -> scoreDirector.buildScoreAnalysis(fetchPolicy), ConstraintMatchPolicy.match(fetchPolicy), false); assertFreshScore(solution, currentScore, analysis.score(), solutionUpdatePolicy); return analysis; @@ -126,7 +128,8 @@ Current score (%s) and freshly calculated score (%s) for solution (%s) do not ma @NonNull ScoreAnalysisFetchPolicy fetchPolicy) { var assigner = new Assigner, In_, Out_>(solverFactory, propositionFunction, DefaultRecommendedAssignment::new, fetchPolicy, solution, evaluatedEntityOrElement); - return callScoreDirector(solution, SolutionUpdatePolicy.UPDATE_ALL, assigner, true, true); + return callScoreDirector(solution, SolutionUpdatePolicy.UPDATE_ALL, assigner, ConstraintMatchPolicy.match(fetchPolicy), + true); } @Override @@ -134,7 +137,8 @@ public List> recommendFit(Solution_ sol Function propositionFunction, ScoreAnalysisFetchPolicy fetchPolicy) { var assigner = new Assigner, In_, Out_>(solverFactory, propositionFunction, DefaultRecommendedFit::new, fetchPolicy, solution, fittedEntityOrElement); - return callScoreDirector(solution, SolutionUpdatePolicy.UPDATE_ALL, assigner, true, true); + return callScoreDirector(solution, SolutionUpdatePolicy.UPDATE_ALL, assigner, ConstraintMatchPolicy.match(fetchPolicy), + true); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java index 6fab99492b..ed8fc87578 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java @@ -30,6 +30,7 @@ import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; import ai.timefold.solver.core.impl.phase.Phase; import ai.timefold.solver.core.impl.phase.PhaseFactory; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.director.InnerScoreDirectorFactory; import ai.timefold.solver.core.impl.score.director.ScoreDirectorFactoryFactory; import ai.timefold.solver.core.impl.solver.change.DefaultProblemChangeDirector; @@ -104,7 +105,8 @@ public > InnerScoreDirectorFactory(castScoreDirector)); diff --git a/core/src/test/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysisTest.java b/core/src/test/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysisTest.java index 503a674bf4..5c66f6e43d 100644 --- a/core/src/test/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysisTest.java +++ b/core/src/test/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysisTest.java @@ -1,5 +1,8 @@ package ai.timefold.solver.core.api.score.analysis; +import static ai.timefold.solver.core.api.solver.ScoreAnalysisFetchPolicy.FETCH_ALL; +import static ai.timefold.solver.core.api.solver.ScoreAnalysisFetchPolicy.FETCH_MATCH_COUNT; +import static ai.timefold.solver.core.api.solver.ScoreAnalysisFetchPolicy.FETCH_SHALLOW; import static ai.timefold.solver.core.impl.score.director.InnerScoreDirector.getConstraintAnalysis; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; @@ -12,6 +15,7 @@ import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.score.constraint.ConstraintRef; import ai.timefold.solver.core.api.score.stream.DefaultConstraintJustification; +import ai.timefold.solver.core.api.solver.ScoreAnalysisFetchPolicy; import ai.timefold.solver.core.impl.score.constraint.DefaultConstraintMatchTotal; import org.junit.jupiter.api.Test; @@ -61,9 +65,12 @@ void summarize() { addConstraintMatch(constraintMatchTotal2, SimpleScore.of(12)); var emptyConstraintMatchTotal1 = new DefaultConstraintMatchTotal<>(constraintId3, SimpleScore.of(0)); var constraintAnalysisMap = Map.of( - constraintMatchTotal.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal, true), - constraintMatchTotal2.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal2, true), - emptyConstraintMatchTotal1.getConstraintRef(), getConstraintAnalysis(emptyConstraintMatchTotal1, true)); + constraintMatchTotal.getConstraintRef(), + getConstraintAnalysis(constraintMatchTotal, ScoreAnalysisFetchPolicy.FETCH_ALL), + constraintMatchTotal2.getConstraintRef(), + getConstraintAnalysis(constraintMatchTotal2, ScoreAnalysisFetchPolicy.FETCH_ALL), + emptyConstraintMatchTotal1.getConstraintRef(), + getConstraintAnalysis(emptyConstraintMatchTotal1, ScoreAnalysisFetchPolicy.FETCH_ALL)); var scoreAnalysis = new ScoreAnalysis<>(SimpleScore.of(67), constraintAnalysisMap); // Single constraint analysis @@ -111,8 +118,10 @@ void summarizeUninitializedSolution() { var constraintMatchTotal = new DefaultConstraintMatchTotal<>(constraintId1, SimpleScore.of(0)); var constraintMatchTotal2 = new DefaultConstraintMatchTotal<>(constraintId2, SimpleScore.of(0)); var constraintAnalysisMap = Map.of( - constraintMatchTotal.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal, true), - constraintMatchTotal2.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal2, true)); + constraintMatchTotal.getConstraintRef(), + getConstraintAnalysis(constraintMatchTotal, ScoreAnalysisFetchPolicy.FETCH_ALL), + constraintMatchTotal2.getConstraintRef(), + getConstraintAnalysis(constraintMatchTotal2, ScoreAnalysisFetchPolicy.FETCH_ALL)); var scoreAnalysis = new ScoreAnalysis<>(SimpleScore.ofUninitialized(3, 0), constraintAnalysisMap); // Single constraint analysis @@ -145,14 +154,14 @@ void failFastSummarize() { var constraintMatchTotal = new DefaultConstraintMatchTotal<>(constraintId1, SimpleScore.of(1)); addConstraintMatch(constraintMatchTotal, SimpleScore.of(2), "A", "B", "C"); var constraintAnalysisMap = Map.of( - constraintMatchTotal.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal, false)); + constraintMatchTotal.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal, FETCH_SHALLOW)); var scoreAnalysis = new ScoreAnalysis<>(SimpleScore.of(3), constraintAnalysisMap); assertThatThrownBy(scoreAnalysis::summarize) .hasMessageContaining("The constraint matches must be non-null"); - assertThatThrownBy(() -> constraintAnalysisMap.values().stream().findFirst().get().matchCount()) - .hasMessageContaining("The constraint matches must be non-null"); + assertThat(constraintAnalysisMap.values().stream().findFirst().get().matchCount()) + .isEqualTo(-1); } @Test @@ -177,9 +186,107 @@ void compareWithConstraintMatchesWithoutMatchAnalysis() { addConstraintMatch(constraintMatchTotal2, SimpleScore.of(12)); var emptyConstraintMatchTotal1 = new DefaultConstraintMatchTotal<>(constraintId3, SimpleScore.of(0)); var constraintAnalysisMap1 = Map.of( - constraintMatchTotal1.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal1, false), - constraintMatchTotal2.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal2, false), - emptyConstraintMatchTotal1.getConstraintRef(), getConstraintAnalysis(emptyConstraintMatchTotal1, false)); + constraintMatchTotal1.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal1, FETCH_SHALLOW), + constraintMatchTotal2.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal2, FETCH_SHALLOW), + emptyConstraintMatchTotal1.getConstraintRef(), + getConstraintAnalysis(emptyConstraintMatchTotal1, FETCH_SHALLOW)); + var scoreAnalysis1 = new ScoreAnalysis<>(SimpleScore.of(50), constraintAnalysisMap1); + + var emptyConstraintMatchTotal2 = new DefaultConstraintMatchTotal<>(constraintId1, SimpleScore.of(0)); + var constraintMatchTotal3 = new DefaultConstraintMatchTotal<>(constraintId2, SimpleScore.of(2)); + addConstraintMatch(constraintMatchTotal3, SimpleScore.of(2), "A", "B", "C"); + addConstraintMatch(constraintMatchTotal3, SimpleScore.of(4), "B", "C"); + addConstraintMatch(constraintMatchTotal3, SimpleScore.of(6), "A", "B"); + var constraintMatchTotal4 = new DefaultConstraintMatchTotal<>(constraintId3, SimpleScore.of(3)); + addConstraintMatch(constraintMatchTotal4, SimpleScore.of(3), "B", "C", "D"); + addConstraintMatch(constraintMatchTotal4, SimpleScore.of(6), "B", "C"); + addConstraintMatch(constraintMatchTotal4, SimpleScore.of(9), "C", "D"); + addConstraintMatch(constraintMatchTotal4, SimpleScore.of(12)); + var constraintAnalysisMap2 = Map.of( + emptyConstraintMatchTotal2.getConstraintRef(), getConstraintAnalysis(emptyConstraintMatchTotal2, FETCH_SHALLOW), + constraintMatchTotal3.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal3, FETCH_SHALLOW), + constraintMatchTotal4.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal4, FETCH_SHALLOW)); + var scoreAnalysis2 = new ScoreAnalysis<>(SimpleScore.of(42), constraintAnalysisMap2); + + var comparison = scoreAnalysis1.diff(scoreAnalysis2); + assertSoftly(softly -> { + softly.assertThat(comparison.score()).isEqualTo(SimpleScore.of(8)); + softly.assertThat(comparison.constraintMap()) + .containsOnlyKeys(constraintMatchTotal1.getConstraintRef(), constraintMatchTotal2.getConstraintRef(), + constraintMatchTotal4.getConstraintRef()); + }); + // Matches for constraint1 not present. + var constraintAnalysis1 = comparison.getConstraintAnalysis(constraintName1); + assertSoftly(softly -> { + softly.assertThat(constraintAnalysis1.score()).isEqualTo(SimpleScore.of(20)); + softly.assertThat(constraintAnalysis1.matches()).isNull(); + }); + // Matches for constraint2 still not present. + var constraintAnalysis2 = comparison.getConstraintAnalysis(constraintName2); + assertSoftly(softly -> { + softly.assertThat(constraintAnalysis2.score()).isEqualTo(SimpleScore.of(18)); + softly.assertThat(constraintAnalysis2.matches()).isNull(); + }); + // Matches for constraint3 not present. + var constraintAnalysis3 = comparison.getConstraintAnalysis(constraintName3); + assertSoftly(softly -> { + softly.assertThat(constraintAnalysis3.score()).isEqualTo(SimpleScore.of(-30)); + softly.assertThat(constraintAnalysis3.matches()).isNull(); + }); + + var reverseComparison = scoreAnalysis2.diff(scoreAnalysis1); + assertSoftly(softly -> { + softly.assertThat(reverseComparison.score()).isEqualTo(SimpleScore.of(-8)); + softly.assertThat(reverseComparison.constraintMap()) + .containsOnlyKeys(constraintMatchTotal1.getConstraintRef(), constraintMatchTotal3.getConstraintRef(), + constraintMatchTotal4.getConstraintRef()); + }); + // Matches for constraint1 not present. + var reverseConstraintAnalysis1 = reverseComparison.getConstraintAnalysis(constraintName1); + assertSoftly(softly -> { + softly.assertThat(reverseConstraintAnalysis1.score()).isEqualTo(SimpleScore.of(-20)); + softly.assertThat(reverseConstraintAnalysis1.matches()).isNull(); + }); + // Matches for constraint2 still not present. + var reverseConstraintAnalysis2 = reverseComparison.getConstraintAnalysis(constraintName2); + assertSoftly(softly -> { + softly.assertThat(reverseConstraintAnalysis2.score()).isEqualTo(SimpleScore.of(-18)); + softly.assertThat(reverseConstraintAnalysis2.matches()).isNull(); + }); + // Matches for constraint3 not present in reverse. + var reverseConstraintAnalysis3 = reverseComparison.getConstraintAnalysis(constraintName3); + assertSoftly(softly -> { + softly.assertThat(reverseConstraintAnalysis3.score()).isEqualTo(SimpleScore.of(30)); + softly.assertThat(reverseConstraintAnalysis3.matches()).isNull(); + }); + } + + @Test + void compareWithConstraintMatchesWithMatchCountOnly() { + var constraintPackage = "constraintPackage"; + var constraintName1 = "constraint1"; + var constraintName2 = "constraint2"; + var constraintName3 = "constraint3"; + var constraintId1 = ConstraintRef.of(constraintPackage, constraintName1); + var constraintId2 = ConstraintRef.of(constraintPackage, constraintName2); + var constraintId3 = ConstraintRef.of(constraintPackage, constraintName3); + + var constraintMatchTotal1 = new DefaultConstraintMatchTotal<>(constraintId1, SimpleScore.of(1)); + addConstraintMatch(constraintMatchTotal1, SimpleScore.of(2), "A", "B", "C"); + addConstraintMatch(constraintMatchTotal1, SimpleScore.of(4), "A", "B"); + addConstraintMatch(constraintMatchTotal1, SimpleScore.of(6), "B", "C"); + addConstraintMatch(constraintMatchTotal1, SimpleScore.of(8)); + var constraintMatchTotal2 = new DefaultConstraintMatchTotal<>(constraintId2, SimpleScore.of(3)); + addConstraintMatch(constraintMatchTotal2, SimpleScore.of(3), "B", "C", "D"); + addConstraintMatch(constraintMatchTotal2, SimpleScore.of(6), "B", "C"); + addConstraintMatch(constraintMatchTotal2, SimpleScore.of(9), "C", "D"); + addConstraintMatch(constraintMatchTotal2, SimpleScore.of(12)); + var emptyConstraintMatchTotal1 = new DefaultConstraintMatchTotal<>(constraintId3, SimpleScore.of(0)); + var constraintAnalysisMap1 = Map.of( + constraintMatchTotal1.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal1, FETCH_MATCH_COUNT), + constraintMatchTotal2.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal2, FETCH_MATCH_COUNT), + emptyConstraintMatchTotal1.getConstraintRef(), + getConstraintAnalysis(emptyConstraintMatchTotal1, FETCH_MATCH_COUNT)); var scoreAnalysis1 = new ScoreAnalysis<>(SimpleScore.of(50), constraintAnalysisMap1); var emptyConstraintMatchTotal2 = new DefaultConstraintMatchTotal<>(constraintId1, SimpleScore.of(0)); @@ -193,9 +300,10 @@ void compareWithConstraintMatchesWithoutMatchAnalysis() { addConstraintMatch(constraintMatchTotal4, SimpleScore.of(9), "C", "D"); addConstraintMatch(constraintMatchTotal4, SimpleScore.of(12)); var constraintAnalysisMap2 = Map.of( - emptyConstraintMatchTotal2.getConstraintRef(), getConstraintAnalysis(emptyConstraintMatchTotal2, false), - constraintMatchTotal3.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal3, false), - constraintMatchTotal4.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal4, false)); + emptyConstraintMatchTotal2.getConstraintRef(), + getConstraintAnalysis(emptyConstraintMatchTotal2, FETCH_MATCH_COUNT), + constraintMatchTotal3.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal3, FETCH_MATCH_COUNT), + constraintMatchTotal4.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal4, FETCH_MATCH_COUNT)); var scoreAnalysis2 = new ScoreAnalysis<>(SimpleScore.of(42), constraintAnalysisMap2); var comparison = scoreAnalysis1.diff(scoreAnalysis2); @@ -210,18 +318,21 @@ void compareWithConstraintMatchesWithoutMatchAnalysis() { assertSoftly(softly -> { softly.assertThat(constraintAnalysis1.score()).isEqualTo(SimpleScore.of(20)); softly.assertThat(constraintAnalysis1.matches()).isNull(); + softly.assertThat(constraintAnalysis1.matchCount()).isGreaterThan(0); }); // Matches for constraint2 still not present. var constraintAnalysis2 = comparison.getConstraintAnalysis(constraintName2); assertSoftly(softly -> { softly.assertThat(constraintAnalysis2.score()).isEqualTo(SimpleScore.of(18)); softly.assertThat(constraintAnalysis2.matches()).isNull(); + softly.assertThat(constraintAnalysis2.matchCount()).isGreaterThan(0); }); // Matches for constraint3 not present. var constraintAnalysis3 = comparison.getConstraintAnalysis(constraintName3); assertSoftly(softly -> { softly.assertThat(constraintAnalysis3.score()).isEqualTo(SimpleScore.of(-30)); softly.assertThat(constraintAnalysis3.matches()).isNull(); + softly.assertThat(constraintAnalysis3.matchCount()).isLessThan(0); }); var reverseComparison = scoreAnalysis2.diff(scoreAnalysis1); @@ -236,18 +347,21 @@ void compareWithConstraintMatchesWithoutMatchAnalysis() { assertSoftly(softly -> { softly.assertThat(reverseConstraintAnalysis1.score()).isEqualTo(SimpleScore.of(-20)); softly.assertThat(reverseConstraintAnalysis1.matches()).isNull(); + softly.assertThat(reverseConstraintAnalysis1.matchCount()).isLessThan(0); }); // Matches for constraint2 still not present. var reverseConstraintAnalysis2 = reverseComparison.getConstraintAnalysis(constraintName2); assertSoftly(softly -> { softly.assertThat(reverseConstraintAnalysis2.score()).isEqualTo(SimpleScore.of(-18)); softly.assertThat(reverseConstraintAnalysis2.matches()).isNull(); + softly.assertThat(reverseConstraintAnalysis2.matchCount()).isLessThan(0); }); // Matches for constraint3 not present in reverse. var reverseConstraintAnalysis3 = reverseComparison.getConstraintAnalysis(constraintName3); assertSoftly(softly -> { softly.assertThat(reverseConstraintAnalysis3.score()).isEqualTo(SimpleScore.of(30)); softly.assertThat(reverseConstraintAnalysis3.matches()).isNull(); + softly.assertThat(reverseConstraintAnalysis3.matchCount()).isGreaterThan(0); }); } @@ -273,9 +387,9 @@ void compareWithConstraintMatchesAndMatchAnalysis() { addConstraintMatch(constraintMatchTotal2, SimpleScore.of(12)); var emptyConstraintMatchTotal1 = new DefaultConstraintMatchTotal<>(constraintId3, SimpleScore.of(0)); var constraintAnalysisMap1 = Map.of( - constraintMatchTotal1.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal1, true), - constraintMatchTotal2.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal2, true), - emptyConstraintMatchTotal1.getConstraintRef(), getConstraintAnalysis(emptyConstraintMatchTotal1, true)); + constraintMatchTotal1.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal1, FETCH_ALL), + constraintMatchTotal2.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal2, FETCH_ALL), + emptyConstraintMatchTotal1.getConstraintRef(), getConstraintAnalysis(emptyConstraintMatchTotal1, FETCH_ALL)); var scoreAnalysis1 = new ScoreAnalysis<>(SimpleScore.of(50), constraintAnalysisMap1); var emptyConstraintMatchTotal2 = new DefaultConstraintMatchTotal<>(constraintId1, SimpleScore.of(0)); @@ -289,9 +403,9 @@ void compareWithConstraintMatchesAndMatchAnalysis() { addConstraintMatch(constraintMatchTotal4, SimpleScore.of(9), "C", "D"); addConstraintMatch(constraintMatchTotal4, SimpleScore.of(12)); var constraintAnalysisMap2 = Map.of( - emptyConstraintMatchTotal2.getConstraintRef(), getConstraintAnalysis(emptyConstraintMatchTotal2, true), - constraintMatchTotal3.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal3, true), - constraintMatchTotal4.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal4, true)); + emptyConstraintMatchTotal2.getConstraintRef(), getConstraintAnalysis(emptyConstraintMatchTotal2, FETCH_ALL), + constraintMatchTotal3.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal3, FETCH_ALL), + constraintMatchTotal4.getConstraintRef(), getConstraintAnalysis(constraintMatchTotal4, FETCH_ALL)); var scoreAnalysis2 = new ScoreAnalysis<>(SimpleScore.of(42), constraintAnalysisMap2); var comparison = scoreAnalysis1.diff(scoreAnalysis2); diff --git a/core/src/test/java/ai/timefold/solver/core/api/solver/SolverFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/api/solver/SolverFactoryTest.java index f9feb8f594..b288af150f 100644 --- a/core/src/test/java/ai/timefold/solver/core/api/solver/SolverFactoryTest.java +++ b/core/src/test/java/ai/timefold/solver/core/api/solver/SolverFactoryTest.java @@ -9,22 +9,19 @@ import java.io.File; import java.io.IOException; -import java.io.InputStream; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.time.Duration; import java.util.Arrays; import java.util.List; -import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicPhaseConfig; import ai.timefold.solver.core.config.localsearch.LocalSearchPhaseConfig; import ai.timefold.solver.core.config.phase.custom.CustomPhaseConfig; import ai.timefold.solver.core.config.solver.SolverConfig; import ai.timefold.solver.core.config.solver.termination.TerminationConfig; import ai.timefold.solver.core.impl.score.DummySimpleScoreEasyScoreCalculator; -import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; -import ai.timefold.solver.core.impl.score.director.InnerScoreDirectorFactory; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.solver.DefaultSolver; import ai.timefold.solver.core.impl.solver.DefaultSolverFactory; import ai.timefold.solver.core.impl.testdata.domain.TestdataEntity; @@ -50,7 +47,7 @@ static void setup() { void createFromXmlResource() { SolverFactory solverFactory = SolverFactory.createFromXmlResource( "ai/timefold/solver/core/api/solver/testdataSolverConfig.xml"); - Solver solver = solverFactory.buildSolver(); + var solver = solverFactory.buildSolver(); assertThat(solver).isNotNull(); } @@ -59,20 +56,20 @@ void createFromXmlResource() { void createFromXmlResource_noGenericsForBackwardsCompatibility() { SolverFactory solverFactory = SolverFactory.createFromXmlResource( "ai/timefold/solver/core/api/solver/testdataSolverConfig.xml"); - Solver solver = solverFactory.buildSolver(); + var solver = solverFactory.buildSolver(); assertThat(solver).isNotNull(); } @Test void createFromNonExistingXmlResource_failsShowingResource() { - final String xmlSolverConfigResource = "ai/timefold/solver/core/api/solver/nonExistingSolverConfig.xml"; + final var xmlSolverConfigResource = "ai/timefold/solver/core/api/solver/nonExistingSolverConfig.xml"; assertThatIllegalArgumentException().isThrownBy(() -> SolverFactory.createFromXmlResource(xmlSolverConfigResource)) .withMessageContaining(xmlSolverConfigResource); } @Test void createFromNonExistingXmlFile_failsShowingPath() { - final File xmlSolverConfigFile = new File(solverTestDir, "nonExistingSolverConfig.xml"); + final var xmlSolverConfigFile = new File(solverTestDir, "nonExistingSolverConfig.xml"); assertThatIllegalArgumentException().isThrownBy(() -> SolverFactory.createFromXmlFile(xmlSolverConfigFile)) .withMessageContaining(xmlSolverConfigFile.toString()); } @@ -83,19 +80,19 @@ void createFromXmlResource_classLoader() { ClassLoader classLoader = new DivertingClassLoader(getClass().getClassLoader()); SolverFactory solverFactory = SolverFactory.createFromXmlResource( "divertThroughClassLoader/ai/timefold/solver/core/api/solver/classloaderTestdataSolverConfig.xml", classLoader); - Solver solver = solverFactory.buildSolver(); + var solver = solverFactory.buildSolver(); assertThat(solver).isNotNull(); } @Test void createFromXmlFile() throws IOException { - File file = new File(solverTestDir, "testdataSolverConfig.xml"); - try (InputStream in = getClass().getClassLoader().getResourceAsStream( + var file = new File(solverTestDir, "testdataSolverConfig.xml"); + try (var in = getClass().getClassLoader().getResourceAsStream( "ai/timefold/solver/core/api/solver/testdataSolverConfig.xml")) { Files.copy(in, file.toPath(), StandardCopyOption.REPLACE_EXISTING); } SolverFactory solverFactory = SolverFactory.createFromXmlFile(file); - Solver solver = solverFactory.buildSolver(); + var solver = solverFactory.buildSolver(); assertThat(solver).isNotNull(); } @@ -103,19 +100,19 @@ void createFromXmlFile() throws IOException { void createFromXmlFile_classLoader() throws IOException { // Mocking loadClass doesn't work well enough, because the className still differs from class.getName() ClassLoader classLoader = new DivertingClassLoader(getClass().getClassLoader()); - File file = new File(solverTestDir, "classloaderTestdataSolverConfig.xml"); - try (InputStream in = getClass().getClassLoader().getResourceAsStream( + var file = new File(solverTestDir, "classloaderTestdataSolverConfig.xml"); + try (var in = getClass().getClassLoader().getResourceAsStream( "ai/timefold/solver/core/api/solver/classloaderTestdataSolverConfig.xml")) { Files.copy(in, file.toPath(), StandardCopyOption.REPLACE_EXISTING); } SolverFactory solverFactory = SolverFactory.createFromXmlFile(file, classLoader); - Solver solver = solverFactory.buildSolver(); + var solver = solverFactory.buildSolver(); assertThat(solver).isNotNull(); } @Test void createFromInvalidXmlResource_failsShowingBothResourceAndReason() { - final String invalidXmlSolverConfigResource = "ai/timefold/solver/core/api/solver/invalidSolverConfig.xml"; + final var invalidXmlSolverConfigResource = "ai/timefold/solver/core/api/solver/invalidSolverConfig.xml"; assertThatIllegalArgumentException() .isThrownBy(() -> SolverFactory.createFromXmlResource(invalidXmlSolverConfigResource)) .withMessageContaining(invalidXmlSolverConfigResource) @@ -124,9 +121,9 @@ void createFromInvalidXmlResource_failsShowingBothResourceAndReason() { @Test void createFromInvalidXmlFile_failsShowingBothPathAndReason() throws IOException { - final String invalidXmlSolverConfigResource = "ai/timefold/solver/core/api/solver/invalidSolverConfig.xml"; - File file = new File(solverTestDir, "invalidSolverConfig.xml"); - try (InputStream in = getClass().getClassLoader().getResourceAsStream(invalidXmlSolverConfigResource)) { + final var invalidXmlSolverConfigResource = "ai/timefold/solver/core/api/solver/invalidSolverConfig.xml"; + var file = new File(solverTestDir, "invalidSolverConfig.xml"); + try (var in = getClass().getClassLoader().getResourceAsStream(invalidXmlSolverConfigResource)) { Files.copy(in, file.toPath(), StandardCopyOption.REPLACE_EXISTING); } assertThatIllegalArgumentException() @@ -137,40 +134,38 @@ void createFromInvalidXmlFile_failsShowingBothPathAndReason() throws IOException @Test void create() { - SolverConfig solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class); + var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class); SolverFactory solverFactory = SolverFactory.create(solverConfig); - Solver solver = solverFactory.buildSolver(); + var solver = solverFactory.buildSolver(); assertThat(solver).isNotNull(); } @Test void createAndOverrideSettings() { - SolverConfig solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class); + var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class); SolverFactory solverFactory = SolverFactory.create(solverConfig); SolverConfigOverride configOverride = mock(SolverConfigOverride.class); - TerminationConfig terminationConfig = new TerminationConfig(); + var terminationConfig = new TerminationConfig(); terminationConfig.withSpentLimit(Duration.ofSeconds(60)); doReturn(terminationConfig).when(configOverride).getTerminationConfig(); - Solver solver = solverFactory.buildSolver(configOverride); + var solver = solverFactory.buildSolver(configOverride); assertThat(solver).isNotNull(); verify(configOverride, atLeast(1)).getTerminationConfig(); } @Test void getScoreDirectorFactory() { - SolverConfig solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class); - DefaultSolverFactory solverFactory = - (DefaultSolverFactory) SolverFactory. create(solverConfig); - InnerScoreDirectorFactory scoreDirectorFactory = - solverFactory.getScoreDirectorFactory(); + var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class); + var solverFactory = (DefaultSolverFactory) SolverFactory. create(solverConfig); + var scoreDirectorFactory = solverFactory.getScoreDirectorFactory(); assertThat(scoreDirectorFactory).isNotNull(); - TestdataSolution solution = new TestdataSolution("s1"); + var solution = new TestdataSolution("s1"); solution.setEntityList(Arrays.asList(new TestdataEntity("e1"), new TestdataEntity("e2"), new TestdataEntity("e3"))); solution.setValueList(Arrays.asList(new TestdataValue("v1"), new TestdataValue("v2"))); - try (InnerScoreDirector scoreDirector = scoreDirectorFactory.buildScoreDirector()) { + try (var scoreDirector = scoreDirectorFactory.buildScoreDirector(false, ConstraintMatchPolicy.DISABLED)) { scoreDirector.setWorkingSolution(solution); - SimpleScore score = scoreDirector.calculateScore(); + var score = scoreDirector.calculateScore(); assertThat(score).isNotNull(); } } @@ -178,7 +173,7 @@ void getScoreDirectorFactory() { @Test void localSearchAfterUnterminatedLocalSearch() { // Create a solver config that has two local searches, the second one unreachable. - SolverConfig solverConfig = new SolverConfig() + var solverConfig = new SolverConfig() .withSolutionClass(TestdataSolution.class) .withEntityClasses(TestdataEntity.class) .withEasyScoreCalculatorClass(DummySimpleScoreEasyScoreCalculator.class) @@ -186,7 +181,7 @@ void localSearchAfterUnterminatedLocalSearch() { new LocalSearchPhaseConfig(), new LocalSearchPhaseConfig()); - DefaultSolverFactory solverFactory = + var solverFactory = (DefaultSolverFactory) SolverFactory. create(solverConfig); Assertions.assertThatThrownBy(() -> solverFactory.buildSolver()) .hasMessageContaining("unreachable phase"); @@ -195,10 +190,10 @@ void localSearchAfterUnterminatedLocalSearch() { @Test void validateInitializationPhases() { // Default configuration - SolverConfig solverConfig = PlannerTestUtils + var solverConfig = PlannerTestUtils .buildSolverConfig(TestdataSolution.class, TestdataEntity.class); SolverFactory solverFactory = SolverFactory.create(solverConfig); - DefaultSolver solver = (DefaultSolver) solverFactory.buildSolver(); + var solver = (DefaultSolver) solverFactory.buildSolver(); assertThat(solver.getPhaseList().get(0).triggersFirstInitializedSolutionEvent()).isTrue(); assertThat(solver.getPhaseList().get(1).triggersFirstInitializedSolutionEvent()).isFalse(); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/solution/ConstraintWeightOverridesTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/solution/ConstraintWeightOverridesTest.java index dd13072c0c..1a302f747f 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/solution/ConstraintWeightOverridesTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/solution/ConstraintWeightOverridesTest.java @@ -10,6 +10,7 @@ import ai.timefold.solver.core.api.score.constraint.ConstraintRef; import ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig; import ai.timefold.solver.core.config.solver.EnvironmentMode; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.director.stream.BavetConstraintStreamScoreDirectorFactory; import ai.timefold.solver.core.impl.testdata.domain.constraintweightoverrides.TestdataConstraintWeightOverridesConstraintProvider; import ai.timefold.solver.core.impl.testdata.domain.constraintweightoverrides.TestdataConstraintWeightOverridesSolution; @@ -80,7 +81,7 @@ void appliesOverridesToConstraintProvider() { new ScoreDirectorFactoryConfig() .withConstraintProviderClass(TestdataConstraintWeightOverridesConstraintProvider.class), EnvironmentMode.REPRODUCIBLE) - .buildScoreDirector(false, false)) { + .buildScoreDirector(false, ConstraintMatchPolicy.DISABLED)) { // Default weights scoreDirector.setWorkingSolution(solution); scoreDirector.triggerVariableListeners(); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/ChangeMoveTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/ChangeMoveTest.java index 8b8fefc6ec..cddb9d0751 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/ChangeMoveTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/ChangeMoveTest.java @@ -10,6 +10,7 @@ import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.score.director.ScoreDirector; import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.director.ScoreDirectorFactory; import ai.timefold.solver.core.impl.score.director.easy.EasyScoreDirectorFactory; import ai.timefold.solver.core.impl.testdata.domain.TestdataEntity; @@ -60,7 +61,8 @@ void doMove() { ScoreDirectorFactory scoreDirectorFactory = new EasyScoreDirectorFactory<>(TestdataEntityProvidingSolution.buildSolutionDescriptor(), solution -> SimpleScore.ZERO); - ScoreDirector scoreDirector = scoreDirectorFactory.buildScoreDirector(); + ScoreDirector scoreDirector = + scoreDirectorFactory.buildScoreDirector(false, ConstraintMatchPolicy.DISABLED); GenuineVariableDescriptor variableDescriptor = TestdataEntityProvidingEntity.buildVariableDescriptorForValue(); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/PillarChangeMoveTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/PillarChangeMoveTest.java index 16f2cc27c7..12acf7f61a 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/PillarChangeMoveTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/PillarChangeMoveTest.java @@ -12,6 +12,7 @@ import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.score.director.ScoreDirector; import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.director.ScoreDirectorFactory; import ai.timefold.solver.core.impl.score.director.easy.EasyScoreDirectorFactory; import ai.timefold.solver.core.impl.testdata.domain.TestdataEntity; @@ -75,7 +76,8 @@ void doMove() { ScoreDirectorFactory scoreDirectorFactory = new EasyScoreDirectorFactory<>(TestdataEntityProvidingSolution.buildSolutionDescriptor(), solution -> SimpleScore.ZERO); - ScoreDirector scoreDirector = scoreDirectorFactory.buildScoreDirector(); + ScoreDirector scoreDirector = + scoreDirectorFactory.buildScoreDirector(false, ConstraintMatchPolicy.DISABLED); GenuineVariableDescriptor variableDescriptor = TestdataEntityProvidingEntity .buildVariableDescriptorForValue(); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/PillarSwapMoveTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/PillarSwapMoveTest.java index 4d8cf0fa02..9f55af1523 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/PillarSwapMoveTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/PillarSwapMoveTest.java @@ -12,6 +12,7 @@ import ai.timefold.solver.core.api.score.director.ScoreDirector; import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.director.ScoreDirectorFactory; import ai.timefold.solver.core.impl.score.director.easy.EasyScoreDirectorFactory; import ai.timefold.solver.core.impl.testdata.domain.TestdataEntity; @@ -128,7 +129,8 @@ void doMove() { ScoreDirectorFactory scoreDirectorFactory = new EasyScoreDirectorFactory<>(TestdataEntityProvidingSolution.buildSolutionDescriptor(), solution -> SimpleScore.ZERO); - ScoreDirector scoreDirector = scoreDirectorFactory.buildScoreDirector(); + ScoreDirector scoreDirector = + scoreDirectorFactory.buildScoreDirector(false, ConstraintMatchPolicy.DISABLED); List> variableDescriptorList = TestdataEntityProvidingEntity .buildEntityDescriptor().getGenuineVariableDescriptorList(); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/SwapMoveTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/SwapMoveTest.java index 670fb67f35..da06267e49 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/SwapMoveTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/SwapMoveTest.java @@ -12,6 +12,7 @@ import ai.timefold.solver.core.api.score.director.ScoreDirector; import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.director.ScoreDirectorFactory; import ai.timefold.solver.core.impl.score.director.easy.EasyScoreDirectorFactory; import ai.timefold.solver.core.impl.testdata.domain.TestdataEntity; @@ -103,7 +104,8 @@ void doMove() { ScoreDirectorFactory scoreDirectorFactory = new EasyScoreDirectorFactory<>(TestdataEntityProvidingSolution.buildSolutionDescriptor(), solution -> SimpleScore.ZERO); - ScoreDirector scoreDirector = scoreDirectorFactory.buildScoreDirector(); + ScoreDirector scoreDirector = + scoreDirectorFactory.buildScoreDirector(false, ConstraintMatchPolicy.DISABLED); EntityDescriptor entityDescriptor = TestdataEntityProvidingEntity .buildEntityDescriptor(); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirectorSemanticsTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirectorSemanticsTest.java index 60928a4048..76fe476be5 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirectorSemanticsTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirectorSemanticsTest.java @@ -7,6 +7,7 @@ import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.testdata.domain.TestdataEntity; import ai.timefold.solver.core.impl.testdata.domain.constraintconfiguration.TestdataConstraintConfiguration; import ai.timefold.solver.core.impl.testdata.domain.constraintconfiguration.TestdataConstraintConfigurationSolution; @@ -46,7 +47,7 @@ void independentScoreDirectors() { TestdataConstraintConfigurationSolution solution1 = TestdataConstraintConfigurationSolution.generateSolution(1, 1); InnerScoreDirector scoreDirector1 = - scoreDirectorFactory.buildScoreDirector(false, false); + scoreDirectorFactory.buildScoreDirector(false, ConstraintMatchPolicy.DISABLED); scoreDirector1.setWorkingSolution(solution1); SimpleScore score1 = scoreDirector1.calculateScore(); assertThat(score1).isEqualTo(SimpleScore.of(1)); @@ -55,7 +56,7 @@ void independentScoreDirectors() { TestdataConstraintConfigurationSolution solution2 = TestdataConstraintConfigurationSolution.generateSolution(2, 2); InnerScoreDirector scoreDirector2 = - scoreDirectorFactory.buildScoreDirector(false, false); + scoreDirectorFactory.buildScoreDirector(false, ConstraintMatchPolicy.DISABLED); scoreDirector2.setWorkingSolution(solution2); SimpleScore score2 = scoreDirector2.calculateScore(); assertThat(score2).isEqualTo(SimpleScore.of(2)); @@ -90,7 +91,7 @@ void solutionBasedScoreWeights() { TestdataConstraintConfigurationSolution solution1 = TestdataConstraintConfigurationSolution.generateSolution(1, 1); InnerScoreDirector scoreDirector = - scoreDirectorFactory.buildScoreDirector(false, false); + scoreDirectorFactory.buildScoreDirector(false, ConstraintMatchPolicy.DISABLED); scoreDirector.setWorkingSolution(solution1); SimpleScore score1 = scoreDirector.calculateScore(); assertThat(score1).isEqualTo(SimpleScore.of(1)); @@ -121,7 +122,7 @@ void mutableConstraintConfiguration() { TestdataConstraintConfigurationSolution solution = TestdataConstraintConfigurationSolution.generateSolution(1, 1); InnerScoreDirector scoreDirector = - scoreDirectorFactory.buildScoreDirector(false, false); + scoreDirectorFactory.buildScoreDirector(false, ConstraintMatchPolicy.DISABLED); scoreDirector.setWorkingSolution(solution); SimpleScore score1 = scoreDirector.calculateScore(); assertThat(score1).isEqualTo(SimpleScore.of(1)); @@ -144,7 +145,7 @@ void constraintPresentEvenIfNoMatches() { // Create score director, calculate score with a given constraint configuration. var solution = TestdataConstraintConfigurationSolution.generateSolution(1, 1); - try (var scoreDirector = scoreDirectorFactory.buildScoreDirector(false, true)) { + try (var scoreDirector = scoreDirectorFactory.buildScoreDirector(false, ConstraintMatchPolicy.ENABLED)) { scoreDirector.setWorkingSolution(solution); var score1 = scoreDirector.calculateScore(); assertSoftly(softly -> { @@ -177,7 +178,7 @@ void listVariableEntityPinningSupported() { firstEntity.setValueList(List.of(solution.getValueList().get(0))); firstEntity.setPinned(true); - try (var scoreDirector = scoreDirectorFactory.buildScoreDirector(false, false)) { + try (var scoreDirector = scoreDirectorFactory.buildScoreDirector(false, ConstraintMatchPolicy.DISABLED)) { scoreDirector.setWorkingSolution(solution); var score1 = scoreDirector.calculateScore(); assertThat(score1).isEqualTo(SimpleScore.ofUninitialized(-1, -2)); @@ -205,7 +206,7 @@ void listVariableIndexPinningSupported() { secondEntity.setValueList(List.of(solution.getValueList().get(1))); secondEntity.setPlanningPinToIndex(1); - try (var scoreDirector = scoreDirectorFactory.buildScoreDirector(false, false)) { + try (var scoreDirector = scoreDirectorFactory.buildScoreDirector(false, ConstraintMatchPolicy.DISABLED)) { scoreDirector.setWorkingSolution(solution); var score1 = scoreDirector.calculateScore(); assertThat(score1).isEqualTo(SimpleScore.ofUninitialized(-1, -3)); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactoryTest.java index 761a060cc5..30bb103328 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactoryTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactoryTest.java @@ -15,6 +15,7 @@ import ai.timefold.solver.core.api.score.stream.ConstraintStreamImplType; import ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig; import ai.timefold.solver.core.config.solver.EnvironmentMode; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.director.incremental.IncrementalScoreDirector; import ai.timefold.solver.core.impl.score.director.stream.BavetConstraintStreamScoreDirectorFactory; import ai.timefold.solver.core.impl.testdata.domain.TestdataSolution; @@ -36,7 +37,8 @@ void incrementalScoreCalculatorWithCustomProperties() { ScoreDirectorFactory scoreDirectorFactory = buildTestdataScoreDirectoryFactory(config); IncrementalScoreDirector scoreDirector = - (IncrementalScoreDirector) scoreDirectorFactory.buildScoreDirector(); + (IncrementalScoreDirector) scoreDirectorFactory.buildScoreDirector(false, + ConstraintMatchPolicy.DISABLED); TestCustomPropertiesIncrementalScoreCalculator scoreCalculator = (TestCustomPropertiesIncrementalScoreCalculator) scoreDirector .getIncrementalScoreCalculator(); @@ -59,7 +61,8 @@ void buildWithAssertionScoreDirectorFactory() { ScoreDirectorFactory assertionScoreDirectorFactory = scoreDirectorFactory.getAssertionScoreDirectorFactory(); IncrementalScoreDirector assertionScoreDirector = - (IncrementalScoreDirector) assertionScoreDirectorFactory.buildScoreDirector(); + (IncrementalScoreDirector) assertionScoreDirectorFactory.buildScoreDirector(false, + ConstraintMatchPolicy.DISABLED); IncrementalScoreCalculator assertionScoreCalculator = assertionScoreDirector.getIncrementalScoreCalculator(); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorFactoryTest.java index 0955b9aef5..3055a0744a 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorFactoryTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorFactoryTest.java @@ -10,6 +10,7 @@ import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.testdata.domain.TestdataSolution; import org.junit.jupiter.api.Test; @@ -25,7 +26,7 @@ void buildScoreDirector() { EasyScoreDirectorFactory directorFactory = new EasyScoreDirectorFactory<>( solutionDescriptor, scoreCalculator); - try (var director = directorFactory.buildScoreDirector(false, false)) { + try (var director = directorFactory.buildScoreDirector(false, ConstraintMatchPolicy.DISABLED)) { TestdataSolution solution = new TestdataSolution(); solution.setValueList(Collections.emptyList()); solution.setEntityList(Collections.emptyList()); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorSemanticsTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorSemanticsTest.java index 658e42c85a..8391916772 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorSemanticsTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorSemanticsTest.java @@ -10,6 +10,7 @@ import ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.director.AbstractScoreDirectorSemanticsTest; import ai.timefold.solver.core.impl.score.director.InnerScoreDirectorFactory; import ai.timefold.solver.core.impl.score.director.ScoreDirectorFactory; @@ -70,7 +71,8 @@ void easyScoreCalculatorWithCustomProperties() { config.setEasyScoreCalculatorCustomProperties(customProperties); EasyScoreDirector scoreDirector = - (EasyScoreDirector) buildTestdataScoreDirectoryFactory(config).buildScoreDirector(); + (EasyScoreDirector) buildTestdataScoreDirectoryFactory(config) + .buildScoreDirector(false, ConstraintMatchPolicy.DISABLED); TestCustomPropertiesEasyScoreCalculator scoreCalculator = (TestCustomPropertiesEasyScoreCalculator) scoreDirector .getEasyScoreCalculator(); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorTest.java index 32faca2efa..ca24dc5d89 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorTest.java @@ -1,16 +1,12 @@ package ai.timefold.solver.core.impl.score.director.easy; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import java.util.Arrays; import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.config.score.trend.InitializingScoreTrendLevel; -import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.trend.InitializingScoreTrend; import ai.timefold.solver.core.impl.testdata.domain.TestdataValue; import ai.timefold.solver.core.impl.testdata.domain.shadow.corrupted.TestdataCorruptedShadowedEntity; @@ -20,52 +16,33 @@ class EasyScoreDirectorTest { - @Test - void constraintMatchTotalsUnsupported() { - EasyScoreDirector director = - new EasyScoreDirector<>(mockEasyScoreDirectorFactory(), false, true, true, null); - assertThat(director.isConstraintMatchEnabled()).isFalse(); - assertThatIllegalStateException() - .isThrownBy(director::getConstraintMatchTotalMap) - .withMessageContaining("not supported"); - } - - @SuppressWarnings("unchecked") - private EasyScoreDirectorFactory mockEasyScoreDirectorFactory() { - EasyScoreDirectorFactory factory = mock(EasyScoreDirectorFactory.class); - when(factory.getSolutionDescriptor()).thenReturn(mock(SolutionDescriptor.class)); - return factory; - } - @Test void shadowVariableCorruption() { - EasyScoreDirectorFactory scoreDirectorFactory = - new EasyScoreDirectorFactory<>(TestdataCorruptedShadowedSolution.buildSolutionDescriptor(), - (solution_) -> SimpleScore.of(0)); - scoreDirectorFactory.setInitializingScoreTrend( - InitializingScoreTrend.buildUniformTrend(InitializingScoreTrendLevel.ONLY_DOWN, 1)); - EasyScoreDirector scoreDirector = - scoreDirectorFactory.buildScoreDirector(false, false, true); - - TestdataCorruptedShadowedSolution solution = new TestdataCorruptedShadowedSolution("s1"); - TestdataValue v1 = new TestdataValue("v1"); - TestdataValue v2 = new TestdataValue("v2"); - solution.setValueList(Arrays.asList(v1, v2)); - TestdataCorruptedShadowedEntity e1 = new TestdataCorruptedShadowedEntity("e1"); - TestdataCorruptedShadowedEntity e2 = new TestdataCorruptedShadowedEntity("e2"); - solution.setEntityList(Arrays.asList(e1, e2)); - scoreDirector.setWorkingSolution(solution); - - scoreDirector.assertShadowVariablesAreNotStale(SimpleScore.ofUninitialized(-2, 0), "NoChange"); - scoreDirector.beforeVariableChanged(e1, "value"); - e1.setValue(v1); - scoreDirector.afterVariableChanged(e1, "value"); - scoreDirector.beforeVariableChanged(e2, "value"); - e2.setValue(v1); - scoreDirector.afterVariableChanged(e2, "value"); - scoreDirector.triggerVariableListeners(); - assertThatThrownBy( - () -> scoreDirector.assertShadowVariablesAreNotStale(SimpleScore.ofUninitialized(0, 0), "FirstChange")) - .isInstanceOf(IllegalStateException.class); + var scoreDirectorFactory = new EasyScoreDirectorFactory<>(TestdataCorruptedShadowedSolution.buildSolutionDescriptor(), + (solution_) -> SimpleScore.of(0)); + scoreDirectorFactory + .setInitializingScoreTrend(InitializingScoreTrend.buildUniformTrend(InitializingScoreTrendLevel.ONLY_DOWN, 1)); + try (var scoreDirector = scoreDirectorFactory.buildScoreDirector(false, ConstraintMatchPolicy.DISABLED)) { + var solution = new TestdataCorruptedShadowedSolution("s1"); + var v1 = new TestdataValue("v1"); + var v2 = new TestdataValue("v2"); + solution.setValueList(Arrays.asList(v1, v2)); + var e1 = new TestdataCorruptedShadowedEntity("e1"); + var e2 = new TestdataCorruptedShadowedEntity("e2"); + solution.setEntityList(Arrays.asList(e1, e2)); + scoreDirector.setWorkingSolution(solution); + + scoreDirector.assertShadowVariablesAreNotStale(SimpleScore.ofUninitialized(-2, 0), "NoChange"); + scoreDirector.beforeVariableChanged(e1, "value"); + e1.setValue(v1); + scoreDirector.afterVariableChanged(e1, "value"); + scoreDirector.beforeVariableChanged(e2, "value"); + e2.setValue(v1); + scoreDirector.afterVariableChanged(e2, "value"); + scoreDirector.triggerVariableListeners(); + assertThatThrownBy( + () -> scoreDirector.assertShadowVariablesAreNotStale(SimpleScore.ofUninitialized(0, 0), "FirstChange")) + .isInstanceOf(IllegalStateException.class); + } } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorTest.java index 515663691a..a564444482 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorTest.java @@ -17,6 +17,7 @@ import ai.timefold.solver.core.api.score.calculator.IncrementalScoreCalculator; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.score.buildin.SimpleScoreDefinition; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.testdata.domain.chained.shadow.TestdataShadowingChainedAnchor; import ai.timefold.solver.core.impl.testdata.domain.chained.shadow.TestdataShadowingChainedEntity; import ai.timefold.solver.core.impl.testdata.domain.chained.shadow.TestdataShadowingChainedSolution; @@ -55,7 +56,8 @@ void variableListener() { mock(IncrementalScoreCalculator.class); when(incrementalScoreCalculator.calculateScore()).thenReturn(SimpleScore.of(100)); try (var scoreDirector = - new IncrementalScoreDirector<>(scoreDirectorFactory, false, false, true, incrementalScoreCalculator)) { + new IncrementalScoreDirector<>(scoreDirectorFactory, false, ConstraintMatchPolicy.DISABLED, true, + incrementalScoreCalculator)) { scoreDirector.setWorkingSolution(solution); reset(incrementalScoreCalculator); @@ -78,18 +80,20 @@ void variableListener() { @Test void illegalStateExceptionThrownWhenConstraintMatchNotEnabled() { - try (var director = new IncrementalScoreDirector<>(mockIncrementalScoreDirectorFactory(), false, false, true, + try (var director = new IncrementalScoreDirector<>(mockIncrementalScoreDirectorFactory(), false, + ConstraintMatchPolicy.DISABLED, true, mockIncrementalScoreCalculator(false))) { director.setWorkingSolution(new Object()); assertThatIllegalStateException() .isThrownBy(director::getConstraintMatchTotalMap) - .withMessageContaining("constraintMatchEnabled"); + .withMessageContaining(ConstraintMatchPolicy.DISABLED.name()); } } @Test void constraintMatchTotalsNeverNull() { - try (var director = new IncrementalScoreDirector<>(mockIncrementalScoreDirectorFactory(), false, true, true, + try (var director = new IncrementalScoreDirector<>(mockIncrementalScoreDirectorFactory(), false, + ConstraintMatchPolicy.ENABLED, true, mockIncrementalScoreCalculator(true))) { director.setWorkingSolution(new Object()); assertThat(director.getConstraintMatchTotalMap()).isNotNull(); @@ -98,9 +102,10 @@ void constraintMatchTotalsNeverNull() { @Test void constraintMatchIsNotEnabledWhenScoreCalculatorNotConstraintMatchAware() { - try (var director = new IncrementalScoreDirector<>(mockIncrementalScoreDirectorFactory(), false, true, true, + try (var director = new IncrementalScoreDirector<>(mockIncrementalScoreDirectorFactory(), false, + ConstraintMatchPolicy.ENABLED, true, mockIncrementalScoreCalculator(false))) { - assertThat(director.isConstraintMatchEnabled()).isFalse(); + assertThat(director.getConstraintMatchPolicy()).isEqualTo(ConstraintMatchPolicy.DISABLED); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetAdvancedGroupByConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetAdvancedGroupByConstraintStreamTest.java index a4da289efb..1a1ba78ad3 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetAdvancedGroupByConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetAdvancedGroupByConstraintStreamTest.java @@ -21,6 +21,7 @@ import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.score.stream.ConstraintCollectors; import ai.timefold.solver.core.api.score.stream.Joiners; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.score.stream.common.AbstractAdvancedGroupByConstraintStreamTest; import ai.timefold.solver.core.impl.testdata.domain.score.lavish.TestdataLavishEntity; @@ -32,8 +33,8 @@ final class BavetAdvancedGroupByConstraintStreamTest extends AbstractAdvancedGroupByConstraintStreamTest { - public BavetAdvancedGroupByConstraintStreamTest(boolean constraintMatchEnabled) { - super(new BavetConstraintStreamImplSupport(constraintMatchEnabled)); + public BavetAdvancedGroupByConstraintStreamTest(ConstraintMatchPolicy constraintMatchPolicy) { + super(new BavetConstraintStreamImplSupport(constraintMatchPolicy)); } @TestTemplate diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintStreamImplSupport.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintStreamImplSupport.java index 5ce1de7752..796ed1119f 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintStreamImplSupport.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetConstraintStreamImplSupport.java @@ -5,30 +5,21 @@ import ai.timefold.solver.core.api.score.stream.ConstraintProvider; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.score.director.stream.BavetConstraintStreamScoreDirectorFactory; import ai.timefold.solver.core.impl.score.stream.common.ConstraintStreamImplSupport; -public final class BavetConstraintStreamImplSupport - implements ConstraintStreamImplSupport { - - private final boolean constraintMatchEnabled; - - public BavetConstraintStreamImplSupport(boolean constraintMatchEnabled) { - this.constraintMatchEnabled = constraintMatchEnabled; - } - - @Override - public boolean isConstreamMatchEnabled() { - return constraintMatchEnabled; - } +public record BavetConstraintStreamImplSupport(ConstraintMatchPolicy constraintMatchPolicy) + implements + ConstraintStreamImplSupport { @Override public , Solution_> InnerScoreDirector buildScoreDirector( SolutionDescriptor solutionDescriptorSupplier, ConstraintProvider constraintProvider) { return (InnerScoreDirector) new BavetConstraintStreamScoreDirectorFactory<>( solutionDescriptorSupplier, constraintProvider, EnvironmentMode.REPRODUCIBLE) - .buildScoreDirector(false, constraintMatchEnabled); + .buildScoreDirector(false, constraintMatchPolicy); } @Override diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetRegressionTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetRegressionTest.java index 2e1909b3a2..019547c137 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetRegressionTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/BavetRegressionTest.java @@ -6,6 +6,7 @@ import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.score.stream.Constraint; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraintStreamTest; import ai.timefold.solver.core.impl.testdata.domain.TestdataEntity; @@ -16,8 +17,8 @@ final class BavetRegressionTest extends AbstractConstraintStreamTest { - protected BavetRegressionTest(boolean constraintMatchEnabled) { - super(new BavetConstraintStreamImplSupport(constraintMatchEnabled)); + protected BavetRegressionTest(ConstraintMatchPolicy constraintMatchPolicy) { + super(new BavetConstraintStreamImplSupport(constraintMatchPolicy)); } /** diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetBiConstraintStreamNodeSharingTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetBiConstraintStreamNodeSharingTest.java index efe9c8b5d2..e3cf6e9e26 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetBiConstraintStreamNodeSharingTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetBiConstraintStreamNodeSharingTest.java @@ -1,12 +1,13 @@ package ai.timefold.solver.core.impl.score.stream.bavet.bi; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintStreamImplSupport; import ai.timefold.solver.core.impl.score.stream.common.bi.AbstractBiConstraintStreamNodeSharingTest; final class BavetBiConstraintStreamNodeSharingTest extends AbstractBiConstraintStreamNodeSharingTest { - public BavetBiConstraintStreamNodeSharingTest(boolean constraintMatchEnabled) { - super(new BavetConstraintStreamImplSupport(constraintMatchEnabled)); + public BavetBiConstraintStreamNodeSharingTest(ConstraintMatchPolicy constraintMatchPolicy) { + super(new BavetConstraintStreamImplSupport(constraintMatchPolicy)); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetBiConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetBiConstraintStreamTest.java index f6ff8b29cd..b7212a7f59 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetBiConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetBiConstraintStreamTest.java @@ -1,12 +1,13 @@ package ai.timefold.solver.core.impl.score.stream.bavet.bi; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintStreamImplSupport; import ai.timefold.solver.core.impl.score.stream.common.bi.AbstractBiConstraintStreamTest; final class BavetBiConstraintStreamTest extends AbstractBiConstraintStreamTest { - public BavetBiConstraintStreamTest(boolean constraintMatchEnabled) { - super(new BavetConstraintStreamImplSupport(constraintMatchEnabled)); + public BavetBiConstraintStreamTest(ConstraintMatchPolicy constraintMatchPolicy) { + super(new BavetConstraintStreamImplSupport(constraintMatchPolicy)); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetQuadConstraintStreamNodeSharingTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetQuadConstraintStreamNodeSharingTest.java index f0407ae5d4..d64e821d6e 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetQuadConstraintStreamNodeSharingTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetQuadConstraintStreamNodeSharingTest.java @@ -1,12 +1,13 @@ package ai.timefold.solver.core.impl.score.stream.bavet.quad; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintStreamImplSupport; import ai.timefold.solver.core.impl.score.stream.common.quad.AbstractQuadConstraintStreamNodeSharingTest; final class BavetQuadConstraintStreamNodeSharingTest extends AbstractQuadConstraintStreamNodeSharingTest { - public BavetQuadConstraintStreamNodeSharingTest(boolean constraintMatchEnabled) { - super(new BavetConstraintStreamImplSupport(constraintMatchEnabled)); + public BavetQuadConstraintStreamNodeSharingTest(ConstraintMatchPolicy constraintMatchPolicy) { + super(new BavetConstraintStreamImplSupport(constraintMatchPolicy)); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetQuadConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetQuadConstraintStreamTest.java index d574ac1a60..26c8f5fa4e 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetQuadConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetQuadConstraintStreamTest.java @@ -1,12 +1,13 @@ package ai.timefold.solver.core.impl.score.stream.bavet.quad; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintStreamImplSupport; import ai.timefold.solver.core.impl.score.stream.common.quad.AbstractQuadConstraintStreamTest; final class BavetQuadConstraintStreamTest extends AbstractQuadConstraintStreamTest { - public BavetQuadConstraintStreamTest(boolean constraintMatchEnabled) { - super(new BavetConstraintStreamImplSupport(constraintMatchEnabled)); + public BavetQuadConstraintStreamTest(ConstraintMatchPolicy constraintMatchPolicy) { + super(new BavetConstraintStreamImplSupport(constraintMatchPolicy)); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetTriConstraintStreamNodeSharingTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetTriConstraintStreamNodeSharingTest.java index 249ba768d9..8bb1092d5d 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetTriConstraintStreamNodeSharingTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetTriConstraintStreamNodeSharingTest.java @@ -1,12 +1,13 @@ package ai.timefold.solver.core.impl.score.stream.bavet.tri; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintStreamImplSupport; import ai.timefold.solver.core.impl.score.stream.common.tri.AbstractTriConstraintStreamNodeSharingTest; final class BavetTriConstraintStreamNodeSharingTest extends AbstractTriConstraintStreamNodeSharingTest { - public BavetTriConstraintStreamNodeSharingTest(boolean constraintMatchEnabled) { - super(new BavetConstraintStreamImplSupport(constraintMatchEnabled)); + public BavetTriConstraintStreamNodeSharingTest(ConstraintMatchPolicy constraintMatchPolicy) { + super(new BavetConstraintStreamImplSupport(constraintMatchPolicy)); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetTriConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetTriConstraintStreamTest.java index 4aa98efec6..b9ca37dc6f 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetTriConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetTriConstraintStreamTest.java @@ -1,12 +1,13 @@ package ai.timefold.solver.core.impl.score.stream.bavet.tri; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintStreamImplSupport; import ai.timefold.solver.core.impl.score.stream.common.tri.AbstractTriConstraintStreamTest; final class BavetTriConstraintStreamTest extends AbstractTriConstraintStreamTest { - public BavetTriConstraintStreamTest(boolean constraintMatchEnabled) { - super(new BavetConstraintStreamImplSupport(constraintMatchEnabled)); + public BavetTriConstraintStreamTest(ConstraintMatchPolicy constraintMatchPolicy) { + super(new BavetConstraintStreamImplSupport(constraintMatchPolicy)); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetUniConstraintStreamNodeSharingTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetUniConstraintStreamNodeSharingTest.java index 12689d4a6b..8df0569eae 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetUniConstraintStreamNodeSharingTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetUniConstraintStreamNodeSharingTest.java @@ -1,12 +1,13 @@ package ai.timefold.solver.core.impl.score.stream.bavet.uni; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintStreamImplSupport; import ai.timefold.solver.core.impl.score.stream.common.uni.AbstractUniConstraintStreamNodeSharingTest; final class BavetUniConstraintStreamNodeSharingTest extends AbstractUniConstraintStreamNodeSharingTest { - public BavetUniConstraintStreamNodeSharingTest(boolean constraintMatchEnabled) { - super(new BavetConstraintStreamImplSupport(constraintMatchEnabled)); + public BavetUniConstraintStreamNodeSharingTest(ConstraintMatchPolicy constraintMatchPolicy) { + super(new BavetConstraintStreamImplSupport(constraintMatchPolicy)); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetUniConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetUniConstraintStreamTest.java index 7361547ea4..9ba249b297 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetUniConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetUniConstraintStreamTest.java @@ -1,12 +1,13 @@ package ai.timefold.solver.core.impl.score.stream.bavet.uni; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintStreamImplSupport; import ai.timefold.solver.core.impl.score.stream.common.uni.AbstractUniConstraintStreamTest; final class BavetUniConstraintStreamTest extends AbstractUniConstraintStreamTest { - public BavetUniConstraintStreamTest(boolean constraintMatchEnabled) { - super(new BavetConstraintStreamImplSupport(constraintMatchEnabled)); + public BavetUniConstraintStreamTest(ConstraintMatchPolicy constraintMatchPolicy) { + super(new BavetConstraintStreamImplSupport(constraintMatchPolicy)); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamTest.java index d5becf4dac..ee410705ba 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamTest.java @@ -5,7 +5,6 @@ import java.util.Arrays; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Function; @@ -64,19 +63,19 @@ protected , Solution_> InnerScoreDirector void assertScore(InnerScoreDirector scoreDirector, AssertableMatch... assertableMatches) { scoreDirector.triggerVariableListeners(); - SimpleScore score = scoreDirector.calculateScore(); - int scoreTotal = Arrays.stream(assertableMatches) + var score = scoreDirector.calculateScore(); + var scoreTotal = Arrays.stream(assertableMatches) .mapToInt(assertableMatch -> assertableMatch.score) .sum(); - if (implSupport.isConstreamMatchEnabled()) { - for (AssertableMatch assertableMatch : assertableMatches) { - String constraintPackage = assertableMatch.constraintPackage == null + if (implSupport.constraintMatchPolicy().isJustificationEnabled()) { + for (var assertableMatch : assertableMatches) { + var constraintPackage = assertableMatch.constraintPackage == null ? scoreDirector.getSolutionDescriptor().getSolutionClass().getPackage().getName() : assertableMatch.constraintPackage; - Map> constraintMatchTotals = + var constraintMatchTotals = scoreDirector.getConstraintMatchTotalMap(); - String constraintId = ConstraintRef.composeConstraintId(constraintPackage, assertableMatch.constraintName); - ConstraintMatchTotal constraintMatchTotal = constraintMatchTotals.get(constraintId); + var constraintId = ConstraintRef.composeConstraintId(constraintPackage, assertableMatch.constraintName); + var constraintMatchTotal = constraintMatchTotals.get(constraintId); if (constraintMatchTotal == null) { throw new IllegalStateException("Requested constraint matches for unknown constraint (" + constraintId + ")."); @@ -87,10 +86,9 @@ protected void assertScore(InnerScoreDirector> constraintMatchTotalMap = - scoreDirector.getConstraintMatchTotalMap(); - for (ConstraintMatchTotal constraintMatchTotal : constraintMatchTotalMap.values()) { - for (ConstraintMatch constraintMatch : constraintMatchTotal.getConstraintMatchSet()) { + var constraintMatchTotalMap = scoreDirector.getConstraintMatchTotalMap(); + for (var constraintMatchTotal : constraintMatchTotalMap.values()) { + for (var constraintMatch : constraintMatchTotal.getConstraintMatchSet()) { if (Arrays.stream(assertableMatches) .filter(assertableMatch -> assertableMatch.constraintName .equals(constraintMatch.getConstraintRef().constraintName())) @@ -100,6 +98,13 @@ protected void assertScore(InnerScoreDirector constraintMatch) { if (!constraintName.equals(constraintMatch.getConstraintRef().constraintName())) { return false; } - ConstraintJustification justification = constraintMatch.getJustification(); + var justification = constraintMatch.getJustification(); if (justification instanceof DefaultConstraintJustification constraintJustification) { - List actualJustificationList = constraintJustification.getFacts(); + var actualJustificationList = constraintJustification.getFacts(); if (actualJustificationList.size() != justificationList.size()) { return false; } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamImplSupport.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamImplSupport.java index d7667112c5..b30e31534d 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamImplSupport.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamImplSupport.java @@ -4,11 +4,12 @@ import ai.timefold.solver.core.api.score.stream.ConstraintFactory; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; public interface ConstraintStreamImplSupport { - boolean isConstreamMatchEnabled(); + ConstraintMatchPolicy constraintMatchPolicy(); , Solution_> InnerScoreDirector buildScoreDirector( SolutionDescriptor solutionDescriptorSupplier, ConstraintProvider constraintProvider); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamTestExtension.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamTestExtension.java index 98c6ff931c..74d0c0e01e 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamTestExtension.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/ConstraintStreamTestExtension.java @@ -5,6 +5,7 @@ import ai.timefold.solver.core.api.score.stream.ConstraintStream; import ai.timefold.solver.core.api.score.stream.ConstraintStreamImplType; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.extension.ExtensionContext; @@ -17,7 +18,7 @@ /** * This extension helps implement parameterized {@link ConstraintStream} tests. It provides invocation contexts * representing the cartesian product of {true, false} тип {BAVET} for a test matrix with - * {@code constraintMatchEnabled} and {@link ConstraintStreamImplType} axes. + * {@link ConstraintMatchPolicy} and {@link ConstraintStreamImplType} axes. *

    * Each invocation context includes two additional extensions being {@link ParameterResolver parameter resolvers} that * populate the test class constructor with the test data. Since each CS test class has dozens of test methods @@ -33,21 +34,23 @@ public boolean supportsTestTemplate(ExtensionContext context) { @Override public Stream provideTestTemplateInvocationContexts(ExtensionContext context) { - return Stream.of(true, false) + return Stream + .of(ConstraintMatchPolicy.ENABLED, ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS, + ConstraintMatchPolicy.DISABLED) .map(ConstraintStreamTestExtension::invocationContext); } - private static TestTemplateInvocationContext invocationContext(Boolean constraintMatchEnabled) { + private static TestTemplateInvocationContext invocationContext(ConstraintMatchPolicy constraintMatchPolicy) { return new TestTemplateInvocationContext() { @Override public String getDisplayName(int invocationIndex) { - return "constraintMatchEnabled=" + constraintMatchEnabled; + return "constraintMatchPolicy=" + constraintMatchPolicy; } @Override public List getAdditionalExtensions() { - return List.of(parameterResolver(boolean.class, constraintMatchEnabled)); + return List.of(parameterResolver(ConstraintMatchPolicy.class, constraintMatchPolicy)); } }; } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java index 8e40e2faa8..dbdcc69953 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java @@ -2399,7 +2399,7 @@ public void penalizeUnweightedBigDecimal() { private , Solution_, Entity_> void assertDefaultJustifications( InnerScoreDirector scoreDirector, List entityList) { - if (!implSupport.isConstreamMatchEnabled()) + if (!implSupport.constraintMatchPolicy().isJustificationEnabled()) return; assertThat(scoreDirector.getIndictmentMap()) @@ -2703,7 +2703,7 @@ public void penalizeUnweightedCustomJustifications() { private , Solution_, Entity_> void assertCustomJustifications( InnerScoreDirector scoreDirector, List entityList) { - if (!implSupport.isConstreamMatchEnabled()) + if (!implSupport.constraintMatchPolicy().isJustificationEnabled()) return; assertThat(scoreDirector.getIndictmentMap()) diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/AbstractScoreInlinerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/AbstractScoreInlinerTest.java index 8519f0d61b..7458b0fe7f 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/AbstractScoreInlinerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/AbstractScoreInlinerTest.java @@ -6,13 +6,14 @@ import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraint; import ai.timefold.solver.core.impl.testdata.TestConstraint; import ai.timefold.solver.core.impl.testdata.TestConstraintFactory; public abstract class AbstractScoreInlinerTest> { - protected final boolean constraintMatchEnabled = true; + protected final ConstraintMatchPolicy constraintMatchPolicy = ConstraintMatchPolicy.ENABLED; private final TestConstraintFactory constraintFactory = new TestConstraintFactory<>(buildSolutionDescriptor()); @@ -25,11 +26,11 @@ protected TestConstraint buildConstraint(Score_ constraintWei protected WeightedScoreImpacter buildScoreImpacter(Score_ constraintWeight) { AbstractConstraint constraint = buildConstraint(constraintWeight); Map constraintWeightMap = Collections.singletonMap(constraint, constraintWeight); - AbstractScoreInliner scoreInliner = buildScoreInliner(constraintWeightMap, constraintMatchEnabled); + AbstractScoreInliner scoreInliner = buildScoreInliner(constraintWeightMap, constraintMatchPolicy); return scoreInliner.buildWeightedScoreImpacter(constraint); } abstract protected AbstractScoreInliner buildScoreInliner(Map constraintWeightMap, - boolean constraintMatchEnabled); + ConstraintMatchPolicy constraintMatchPolicy); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableBigDecimalScoreInlinerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableBigDecimalScoreInlinerTest.java index 4cfc38ada1..77993d0bb8 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableBigDecimalScoreInlinerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableBigDecimalScoreInlinerTest.java @@ -9,6 +9,7 @@ import ai.timefold.solver.core.api.score.buildin.bendablebigdecimal.BendableBigDecimalScore; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.testdata.domain.score.TestdataBendableBigDecimalScoreSolution; import org.junit.jupiter.api.Test; @@ -18,7 +19,7 @@ class BendableBigDecimalScoreInlinerTest @Test void defaultScore() { - var scoreInliner = buildScoreInliner(Collections.emptyMap(), constraintMatchEnabled); + var scoreInliner = buildScoreInliner(Collections.emptyMap(), constraintMatchPolicy); assertThat(scoreInliner.extractScore(0)).isEqualTo(buildScore(0, 0, 0)); } @@ -117,8 +118,9 @@ protected SolutionDescriptor buildSolut @Override protected AbstractScoreInliner - buildScoreInliner(Map constraintWeightMap, boolean constraintMatchEnabled) { - return new BendableBigDecimalScoreInliner(constraintWeightMap, constraintMatchEnabled, 1, 2); + buildScoreInliner(Map constraintWeightMap, + ConstraintMatchPolicy constraintMatchPolicy) { + return new BendableBigDecimalScoreInliner(constraintWeightMap, constraintMatchPolicy, 1, 2); } private BendableBigDecimalScore buildScore(long hard, long soft1, long soft2) { diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableLongScoreInlinerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableLongScoreInlinerTest.java index 6b2fdcde96..58df1a2416 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableLongScoreInlinerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableLongScoreInlinerTest.java @@ -8,6 +8,7 @@ import ai.timefold.solver.core.api.score.buildin.bendablelong.BendableLongScore; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.testdata.domain.score.TestdataBendableLongScoreSolution; import org.junit.jupiter.api.Test; @@ -16,7 +17,7 @@ class BendableLongScoreInlinerTest extends AbstractScoreInlinerTest buildSolutionDes @Override protected AbstractScoreInliner buildScoreInliner(Map constraintWeightMap, - boolean constraintMatchEnabled) { - return new BendableLongScoreInliner(constraintWeightMap, constraintMatchEnabled, 1, 2); + ConstraintMatchPolicy constraintMatchPolicy) { + return new BendableLongScoreInliner(constraintWeightMap, constraintMatchPolicy, 1, 2); } private BendableLongScore buildScore(long hard, long soft1, long soft2) { diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableScoreInlinerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableScoreInlinerTest.java index ecf45b6ea0..1abb444ff0 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableScoreInlinerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/BendableScoreInlinerTest.java @@ -8,6 +8,7 @@ import ai.timefold.solver.core.api.score.buildin.bendable.BendableScore; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.testdata.domain.score.TestdataBendableScoreSolution; import org.junit.jupiter.api.Test; @@ -16,7 +17,7 @@ class BendableScoreInlinerTest extends AbstractScoreInlinerTest buildSolutionDescrip @Override protected AbstractScoreInliner buildScoreInliner(Map constraintWeightMap, - boolean constraintMatchEnabled) { - return new BendableScoreInliner(constraintWeightMap, constraintMatchEnabled, 1, 2); + ConstraintMatchPolicy constraintMatchPolicy) { + return new BendableScoreInliner(constraintWeightMap, constraintMatchPolicy, 1, 2); } private BendableScore buildScore(int hard, int soft1, int soft2) { diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftBigDecimalScoreInlinerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftBigDecimalScoreInlinerTest.java index e0ff272604..f16d9bee36 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftBigDecimalScoreInlinerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftBigDecimalScoreInlinerTest.java @@ -9,6 +9,7 @@ import ai.timefold.solver.core.api.score.buildin.hardmediumsoftbigdecimal.HardMediumSoftBigDecimalScore; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.testdata.domain.score.TestdataHardMediumSoftBigDecimalScoreSolution; import org.junit.jupiter.api.Test; @@ -18,7 +19,7 @@ class HardMediumSoftBigDecimalScoreInlinerTest @Test void defaultScore() { - var scoreInliner = buildScoreInliner(Collections.emptyMap(), constraintMatchEnabled); + var scoreInliner = buildScoreInliner(Collections.emptyMap(), constraintMatchPolicy); assertThat(scoreInliner.extractScore(0)).isEqualTo(HardMediumSoftBigDecimalScore.ZERO); } @@ -125,7 +126,7 @@ protected SolutionDescriptor buil @Override protected AbstractScoreInliner buildScoreInliner( - Map constraintWeightMap, boolean constraintMatchEnabled) { - return new HardMediumSoftBigDecimalScoreInliner(constraintWeightMap, constraintMatchEnabled); + Map constraintWeightMap, ConstraintMatchPolicy constraintMatchPolicy) { + return new HardMediumSoftBigDecimalScoreInliner(constraintWeightMap, constraintMatchPolicy); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftLongScoreInlinerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftLongScoreInlinerTest.java index a00e7ba0d7..efdb882647 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftLongScoreInlinerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftLongScoreInlinerTest.java @@ -8,6 +8,7 @@ import ai.timefold.solver.core.api.score.buildin.hardmediumsoftlong.HardMediumSoftLongScore; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.testdata.domain.score.TestdataHardMediumSoftLongScoreSolution; import org.junit.jupiter.api.Test; @@ -17,7 +18,7 @@ class HardMediumSoftLongScoreInlinerTest @Test void defaultScore() { - var scoreInliner = buildScoreInliner(Collections.emptyMap(), constraintMatchEnabled); + var scoreInliner = buildScoreInliner(Collections.emptyMap(), constraintMatchPolicy); assertThat(scoreInliner.extractScore(0)).isEqualTo(HardMediumSoftLongScore.ZERO); } @@ -120,7 +121,8 @@ protected SolutionDescriptor buildSolut @Override protected AbstractScoreInliner - buildScoreInliner(Map constraintWeightMap, boolean constraintMatchEnabled) { - return new HardMediumSoftLongScoreInliner(constraintWeightMap, constraintMatchEnabled); + buildScoreInliner(Map constraintWeightMap, + ConstraintMatchPolicy constraintMatchPolicy) { + return new HardMediumSoftLongScoreInliner(constraintWeightMap, constraintMatchPolicy); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftScoreInlinerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftScoreInlinerTest.java index 24360a3bbb..fdaa61956b 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftScoreInlinerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardMediumSoftScoreInlinerTest.java @@ -8,6 +8,7 @@ import ai.timefold.solver.core.api.score.buildin.hardmediumsoft.HardMediumSoftScore; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.testdata.domain.score.TestdataHardMediumSoftScoreSolution; import org.junit.jupiter.api.Test; @@ -17,7 +18,7 @@ class HardMediumSoftScoreInlinerTest @Test void defaultScore() { - var scoreInliner = buildScoreInliner(Collections.emptyMap(), constraintMatchEnabled); + var scoreInliner = buildScoreInliner(Collections.emptyMap(), constraintMatchPolicy); assertThat(scoreInliner.extractScore(0)).isEqualTo(HardMediumSoftScore.ZERO); } @@ -120,7 +121,8 @@ protected SolutionDescriptor buildSolutionD @Override protected AbstractScoreInliner - buildScoreInliner(Map constraintWeightMap, boolean constraintMatchEnabled) { - return new HardMediumSoftScoreInliner(constraintWeightMap, constraintMatchEnabled); + buildScoreInliner(Map constraintWeightMap, + ConstraintMatchPolicy constraintMatchPolicy) { + return new HardMediumSoftScoreInliner(constraintWeightMap, constraintMatchPolicy); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftBigDecimalScoreInlinerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftBigDecimalScoreInlinerTest.java index c8854c458d..6ed389b936 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftBigDecimalScoreInlinerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftBigDecimalScoreInlinerTest.java @@ -9,6 +9,7 @@ import ai.timefold.solver.core.api.score.buildin.hardsoftbigdecimal.HardSoftBigDecimalScore; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.testdata.domain.score.TestdataHardSoftBigDecimalScoreSolution; import org.junit.jupiter.api.Test; @@ -18,7 +19,7 @@ class HardSoftBigDecimalScoreInlinerTest @Test void defaultScore() { - var scoreInliner = buildScoreInliner(Collections.emptyMap(), constraintMatchEnabled); + var scoreInliner = buildScoreInliner(Collections.emptyMap(), constraintMatchPolicy); assertThat(scoreInliner.extractScore(0)).isEqualTo(HardSoftBigDecimalScore.ZERO); } @@ -98,7 +99,8 @@ protected SolutionDescriptor buildSolut @Override protected AbstractScoreInliner - buildScoreInliner(Map constraintWeightMap, boolean constraintMatchEnabled) { - return new HardSoftBigDecimalScoreInliner(constraintWeightMap, constraintMatchEnabled); + buildScoreInliner(Map constraintWeightMap, + ConstraintMatchPolicy constraintMatchPolicy) { + return new HardSoftBigDecimalScoreInliner(constraintWeightMap, constraintMatchPolicy); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftLongScoreInlinerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftLongScoreInlinerTest.java index 540158a9bd..7518c9d5f9 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftLongScoreInlinerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftLongScoreInlinerTest.java @@ -8,6 +8,7 @@ import ai.timefold.solver.core.api.score.buildin.hardsoftlong.HardSoftLongScore; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.testdata.domain.score.TestdataHardSoftLongScoreSolution; import org.junit.jupiter.api.Test; @@ -16,7 +17,7 @@ class HardSoftLongScoreInlinerTest extends AbstractScoreInlinerTest buildSolutionDes @Override protected AbstractScoreInliner buildScoreInliner(Map constraintWeightMap, - boolean constraintMatchEnabled) { - return new HardSoftLongScoreInliner(constraintWeightMap, constraintMatchEnabled); + ConstraintMatchPolicy constraintMatchPolicy) { + return new HardSoftLongScoreInliner(constraintWeightMap, constraintMatchPolicy); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftScoreInlinerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftScoreInlinerTest.java index c3677701e7..749cfcdfb9 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftScoreInlinerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/HardSoftScoreInlinerTest.java @@ -8,6 +8,7 @@ import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.testdata.domain.score.TestdataHardSoftScoreSolution; import org.junit.jupiter.api.Test; @@ -16,7 +17,7 @@ class HardSoftScoreInlinerTest extends AbstractScoreInlinerTest buildSolutionDescrip @Override protected AbstractScoreInliner buildScoreInliner(Map constraintWeightMap, - boolean constraintMatchEnabled) { - return new HardSoftScoreInliner(constraintWeightMap, constraintMatchEnabled); + ConstraintMatchPolicy constraintMatchPolicy) { + return new HardSoftScoreInliner(constraintWeightMap, constraintMatchPolicy); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleBigDecimalScoreInlinerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleBigDecimalScoreInlinerTest.java index 7961e02691..cc08effc9d 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleBigDecimalScoreInlinerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleBigDecimalScoreInlinerTest.java @@ -9,6 +9,7 @@ import ai.timefold.solver.core.api.score.buildin.simplebigdecimal.SimpleBigDecimalScore; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.testdata.domain.score.TestdataSimpleBigDecimalScoreSolution; import org.junit.jupiter.api.Test; @@ -18,7 +19,7 @@ class SimpleBigDecimalScoreInlinerTest @Test void defaultScore() { - var scoreInliner = buildScoreInliner(Collections.emptyMap(), constraintMatchEnabled); + var scoreInliner = buildScoreInliner(Collections.emptyMap(), constraintMatchPolicy); assertThat(scoreInliner.extractScore(0)).isEqualTo(SimpleBigDecimalScore.ZERO); } @@ -52,7 +53,8 @@ protected SolutionDescriptor buildSolutio @Override protected AbstractScoreInliner - buildScoreInliner(Map constraintWeightMap, boolean constraintMatchEnabled) { - return new SimpleBigDecimalScoreInliner(constraintWeightMap, constraintMatchEnabled); + buildScoreInliner(Map constraintWeightMap, + ConstraintMatchPolicy constraintMatchPolicy) { + return new SimpleBigDecimalScoreInliner(constraintWeightMap, constraintMatchPolicy); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleLongScoreInlinerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleLongScoreInlinerTest.java index 1eb218c450..8b0035c4c6 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleLongScoreInlinerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleLongScoreInlinerTest.java @@ -8,6 +8,7 @@ import ai.timefold.solver.core.api.score.buildin.simplelong.SimpleLongScore; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.testdata.domain.score.TestdataSimpleLongScoreSolution; import org.junit.jupiter.api.Test; @@ -16,7 +17,7 @@ class SimpleLongScoreInlinerTest extends AbstractScoreInlinerTest buildSolutionDescr @Override protected AbstractScoreInliner buildScoreInliner(Map constraintWeightMap, - boolean constraintMatchEnabled) { - return new SimpleLongScoreInliner(constraintWeightMap, constraintMatchEnabled); + ConstraintMatchPolicy constraintMatchPolicy) { + return new SimpleLongScoreInliner(constraintWeightMap, constraintMatchPolicy); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleScoreInlinerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleScoreInlinerTest.java index 5f9868bbb0..2d9c2cf0c1 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleScoreInlinerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/inliner/SimpleScoreInlinerTest.java @@ -8,6 +8,7 @@ import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.testdata.domain.TestdataSolution; import org.junit.jupiter.api.Test; @@ -16,7 +17,7 @@ class SimpleScoreInlinerTest extends AbstractScoreInlinerTest buildSolutionDescriptor() { @Override protected AbstractScoreInliner buildScoreInliner(Map constraintWeightMap, - boolean constraintMatchEnabled) { - return new SimpleScoreInliner(constraintWeightMap, constraintMatchEnabled); + ConstraintMatchPolicy constraintMatchPolicy) { + return new SimpleScoreInliner(constraintWeightMap, constraintMatchPolicy); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamTest.java index fb6053871d..fe0660381d 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamTest.java @@ -2098,7 +2098,7 @@ public void penalizeUnweightedBigDecimal() { private , Solution_, Entity_, Value_> void assertDefaultJustifications( InnerScoreDirector scoreDirector, List entityList, List valueList) { - if (!implSupport.isConstreamMatchEnabled()) + if (!implSupport.constraintMatchPolicy().isJustificationEnabled()) return; assertThat(scoreDirector.getIndictmentMap()) @@ -2437,7 +2437,7 @@ public void penalizeUnweightedCustomJustifications() { private , Solution_, Entity_, Value_> void assertCustomJustifications( InnerScoreDirector scoreDirector, List entityList, List valueList) { - if (!implSupport.isConstreamMatchEnabled()) + if (!implSupport.constraintMatchPolicy().isJustificationEnabled()) return; assertThat(scoreDirector.getIndictmentMap()) diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java index f55e635310..4eb50e8552 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java @@ -2385,7 +2385,7 @@ public void penalizeUnweightedBigDecimal() { private , Solution_, Entity_, Value_> void assertDefaultJustifications( InnerScoreDirector scoreDirector, List entityList, List valueList) { - if (!implSupport.isConstreamMatchEnabled()) + if (!implSupport.constraintMatchPolicy().isJustificationEnabled()) return; assertThat(scoreDirector.getIndictmentMap()) @@ -2709,7 +2709,7 @@ public void penalizeUnweightedCustomJustifications() { private , Solution_, Entity_, Value_> void assertCustomJustifications( InnerScoreDirector scoreDirector, List entityList, List valueList) { - if (!implSupport.isConstreamMatchEnabled()) + if (!implSupport.constraintMatchPolicy().isJustificationEnabled()) return; assertThat(scoreDirector.getIndictmentMap()) diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java index a000c0dc1c..0f9dfa0c74 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java @@ -2974,7 +2974,7 @@ public void penalizeUnweightedBigDecimal() { private , Solution_, Entity_> void assertDefaultJustifications( InnerScoreDirector scoreDirector, List entityList) { - if (!implSupport.isConstreamMatchEnabled()) + if (!implSupport.constraintMatchPolicy().isJustificationEnabled()) return; assertThat(scoreDirector.getIndictmentMap()) @@ -3275,7 +3275,7 @@ public void penalizeUnweightedCustomJustifications() { private , Solution_, Entity_> void assertCustomJustifications( InnerScoreDirector scoreDirector, List entityList) { - if (!implSupport.isConstreamMatchEnabled()) + if (!implSupport.constraintMatchPolicy().isJustificationEnabled()) return; assertThat(scoreDirector.getIndictmentMap()) diff --git a/core/src/test/java/ai/timefold/solver/core/impl/testdata/util/PlannerTestUtils.java b/core/src/test/java/ai/timefold/solver/core/impl/testdata/util/PlannerTestUtils.java index e025669859..3fc872b3fb 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/testdata/util/PlannerTestUtils.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/testdata/util/PlannerTestUtils.java @@ -31,6 +31,7 @@ import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; import ai.timefold.solver.core.impl.score.DummySimpleScoreEasyScoreCalculator; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.score.director.easy.EasyScoreDirectorFactory; import ai.timefold.solver.core.impl.score.trend.InitializingScoreTrend; @@ -117,7 +118,7 @@ public static TestdataSolution generateTestdataSolution(String code, int entityA scoreDirectorFactory.setInitializingScoreTrend( InitializingScoreTrend.buildUniformTrend(InitializingScoreTrendLevel.ONLY_DOWN, 1)); return mock(InnerScoreDirector.class, - AdditionalAnswers.delegatesTo(scoreDirectorFactory.buildScoreDirector(false, false))); + AdditionalAnswers.delegatesTo(scoreDirectorFactory.buildScoreDirector(false, ConstraintMatchPolicy.DISABLED))); } public static > InnerScoreDirector diff --git a/docs/src/modules/ROOT/pages/constraints-and-score/understanding-the-score.adoc b/docs/src/modules/ROOT/pages/constraints-and-score/understanding-the-score.adoc index 4b1399a3d7..80987205ac 100644 --- a/docs/src/modules/ROOT/pages/constraints-and-score/understanding-the-score.adoc +++ b/docs/src/modules/ROOT/pages/constraints-and-score/understanding-the-score.adoc @@ -108,6 +108,15 @@ Using score analysis, you can find out: * Which constraints are broken, and how many times. * Which planning entities and problem facts are responsible for breaking which constraints. +[NOTE] +==== +For performance reasons and especially with large datasets that you'll later need to serialize, +you may choose to use `ScoreAnalysis` without justifications, +while still maintaining the count of constraint matches. +In that case, use `ScoreAnalysisFetchPolicy.FETCH_MATCH_COUNT` instead of +the default `ScoreAnalysisFetchPolicy.FETCH_ALL` when calling `SolutionManager.analyze(...)`. +==== + It is also possible to print the score summary: [tabs] @@ -213,7 +222,6 @@ Each match is accompanied by the score difference it caused, and a justification Typically, the scoring engine creates justification objects automatically by using the results of xref:constraints-and-score/score-calculation.adoc#constraintStreamsCustomizingJustificationsAndIndictments[Constraint Streams' `justifyWith(...)` call]. - [#scoreAnalysisDiff] === Identifying changes between two solutions @@ -304,6 +312,12 @@ However, deserialization is easy to implement yourself by extending `AbstractSco and registering it with Jackson's `ObjectMapper`. ==== +With large datasets, +you may choose to use `ScoreAnalysis` without justifications, +while still maintaining the count of constraint matches. +In that case, use `ScoreAnalysisFetchPolicy.FETCH_MATCH_COUNT` instead of +the default `ScoreAnalysisFetchPolicy.FETCH_ALL` when calling `SolutionManager.analyze(...)`. + [#indictmentHeatMap] == Heat map: Visualize the hot planning entities diff --git a/docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-to-latest-version.adoc b/docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-to-latest-version.adoc index d4e3f75449..230a5b1deb 100644 --- a/docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-to-latest-version.adoc +++ b/docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-to-latest-version.adoc @@ -101,6 +101,27 @@ Methods in the `Move` interface that deal with undo moves have been deprecated and will be removed in a future major version of Timefold Solver. ==== +''' + +.icon:info-circle[role=yellow] `ConstraintAnalysis.matchCount()` no longer throws an exception +[%collapsible%open] +==== +Previously in xref:constraints-and-score/understanding-the-score.adoc[score analysis], +when there were no matches for a constraint, +`ConstraintAnalysis.matchCount()` would throw an exception instead of returning a number. +The behavior has been changed to depend on selected `ScoreAnalysisFetchPolicy`: + +- With `FETCH_ALL`, constraint match analysis will be performed, constraint matches will be available, +and the method will return their precise count. +- With `FETCH_MATCH_COUNT`, constraint match analysis will still be performed +and the method will return the precise count of constraint matches. +The constraint matches themselves will not be available. +This is useful for situations +where the score analysis with a full list of matches can be expected to be too large to transmit over the wire. +- With `FETCH_SHALLOW`, constraint match analysis will not run, constraint matches will not be available +and the method will return `-1`. + +==== === Upgrade from 1.14.0 to 1.15.0 diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/analysis/AbstractScoreAnalysisJacksonDeserializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/analysis/AbstractScoreAnalysisJacksonDeserializer.java index d9ea6d5f1c..3f3f4cb25d 100644 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/analysis/AbstractScoreAnalysisJacksonDeserializer.java +++ b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/analysis/AbstractScoreAnalysisJacksonDeserializer.java @@ -39,13 +39,20 @@ public final ScoreAnalysis deserialize(JsonParser p, DeserializationCont var constraintScore = parseScore(constraintNode.get("score").asText()); var matchScoreList = new ArrayList>(); var matchesNode = constraintNode.get("matches"); + var matchCountNode = constraintNode.get("matchCount"); if (matchesNode == null) { constraintAnalysisList.put(constraintRef, - new ConstraintAnalysis<>(constraintRef, constraintWeight, constraintScore, null)); + new ConstraintAnalysis<>(constraintRef, constraintWeight, constraintScore, null, + matchCountNode == null ? -1 : Integer.parseInt(matchCountNode.asText()))); } else { for (var matchNode : constraintNode.get("matches")) { var matchScore = parseScore(matchNode.get("score").asText()); var justificationNode = matchNode.get("justification"); + if (justificationNode == null) { + // Not allowed; if matches are present, they must have justifications. + throw new IllegalStateException("The match justification of constraint (%s)'s match is missing." + .formatted(constraintRef)); + } var justificationString = justificationNode.toString(); if (getConstraintJustificationClass(constraintRef) == null) { // String-based fallback. var parsedJustification = parseConstraintJustification(constraintRef, justificationString, matchScore); diff --git a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/analysis/ScoreAnalysisJacksonSerializer.java b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/analysis/ScoreAnalysisJacksonSerializer.java index 1264a39291..4d8022be50 100644 --- a/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/analysis/ScoreAnalysisJacksonSerializer.java +++ b/persistence/jackson/src/main/java/ai/timefold/solver/jackson/api/score/analysis/ScoreAnalysisJacksonSerializer.java @@ -41,6 +41,9 @@ public void serialize(ScoreAnalysis value, JsonGenerator gen, Serializer }); constraintAnalysisMap.put("matches", matchAnalysis); } + if (constraintAnalysis.matchCount() != -1) { + constraintAnalysisMap.put("matchCount", constraintAnalysis.matchCount()); + } result.add(constraintAnalysisMap); }); gen.writeObjectField("constraints", result); diff --git a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/TimefoldJacksonModuleTest.java b/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/TimefoldJacksonModuleTest.java index 3fd3d24346..adbb3e6497 100644 --- a/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/TimefoldJacksonModuleTest.java +++ b/persistence/jackson/src/test/java/ai/timefold/solver/jackson/api/TimefoldJacksonModuleTest.java @@ -77,7 +77,7 @@ void scoreAnalysisWithoutMatches() throws JsonProcessingException { var constraintAnalysis1 = new ConstraintAnalysis<>(constraintRef1, HardSoftScore.ofSoft(1), HardSoftScore.ofSoft(2), null); var constraintAnalysis2 = - new ConstraintAnalysis<>(constraintRef2, HardSoftScore.ofHard(1), HardSoftScore.ofHard(1), null); + new ConstraintAnalysis<>(constraintRef2, HardSoftScore.ofHard(1), HardSoftScore.ofHard(1), null, 2); var originalScoreAnalysis = new ScoreAnalysis<>(HardSoftScore.of(1, 2), Map.of(constraintRef1, constraintAnalysis1, constraintRef2, constraintAnalysis2)); @@ -92,7 +92,8 @@ void scoreAnalysisWithoutMatches() throws JsonProcessingException { "package" : "packageA", "name" : "constraint2", "weight" : "1hard/0soft", - "score" : "1hard/0soft" + "score" : "1hard/0soft", + "matchCount" : 2 }, { "package" : "packageB", "name" : "constraint1", @@ -154,24 +155,27 @@ private static String getSerializedScoreAnalysis() { "weight" : "1hard/0soft", "score" : "2hard/0soft", "matches" : [ { - "score" : "1hard/0soft", - "justification" : [ "A", "B" ] - }, { - "score" : "1hard/0soft", - "justification" : [ "B", "C", "D" ] - } ] + "score" : "1hard/0soft", + "justification" : [ "A", "B" ] + }, { + "score" : "1hard/0soft", + "justification" : [ "B", "C", "D" ] + } + ], + "matchCount" : 2 }, { "package" : "package2", "name" : "constraint2", "weight" : "0hard/1soft", "score" : "0hard/4soft", "matches" : [ { - "score" : "0hard/1soft", - "justification" : [ "D" ] - }, { - "score" : "0hard/3soft", - "justification" : [ "A", "C" ] - } ] + "score" : "0hard/1soft", + "justification" : [ "D" ] + }, { + "score" : "0hard/3soft", + "justification" : [ "A", "C" ] + } ], + "matchCount" : 2 } ] }"""; } diff --git a/test/src/main/java/ai/timefold/solver/test/impl/score/stream/DefaultMultiConstraintVerification.java b/test/src/main/java/ai/timefold/solver/test/impl/score/stream/DefaultMultiConstraintVerification.java index bf07bee877..88ea141678 100644 --- a/test/src/main/java/ai/timefold/solver/test/impl/score/stream/DefaultMultiConstraintVerification.java +++ b/test/src/main/java/ai/timefold/solver/test/impl/score/stream/DefaultMultiConstraintVerification.java @@ -4,6 +4,7 @@ import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraintStreamScoreDirectorFactory; import ai.timefold.solver.test.api.score.stream.MultiConstraintVerification; @@ -27,7 +28,7 @@ public DefaultMultiConstraintAssertion given(Object... facts) { @Override public DefaultMultiConstraintAssertion givenSolution(Solution_ solution) { - try (var scoreDirector = scoreDirectorFactory.buildDerivedScoreDirector(true, true)) { + try (var scoreDirector = scoreDirectorFactory.buildDerivedScoreDirector(true, ConstraintMatchPolicy.ENABLED)) { scoreDirector.setWorkingSolution(Objects.requireNonNull(solution)); return new DefaultMultiConstraintAssertion<>(constraintProvider, scoreDirector.calculateScore(), scoreDirector.getConstraintMatchTotalMap(), scoreDirector.getIndictmentMap()); diff --git a/test/src/main/java/ai/timefold/solver/test/impl/score/stream/DefaultSingleConstraintVerification.java b/test/src/main/java/ai/timefold/solver/test/impl/score/stream/DefaultSingleConstraintVerification.java index 7f252634b3..d23b89c23c 100644 --- a/test/src/main/java/ai/timefold/solver/test/impl/score/stream/DefaultSingleConstraintVerification.java +++ b/test/src/main/java/ai/timefold/solver/test/impl/score/stream/DefaultSingleConstraintVerification.java @@ -3,6 +3,7 @@ import java.util.Objects; import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.impl.score.constraint.ConstraintMatchPolicy; import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraintStreamScoreDirectorFactory; import ai.timefold.solver.test.api.score.stream.SingleConstraintVerification; @@ -22,7 +23,7 @@ public DefaultSingleConstraintAssertion given(Object... facts @Override public DefaultSingleConstraintAssertion givenSolution(Solution_ solution) { - try (var scoreDirector = scoreDirectorFactory.buildDerivedScoreDirector(true, true)) { + try (var scoreDirector = scoreDirectorFactory.buildDerivedScoreDirector(true, ConstraintMatchPolicy.ENABLED)) { scoreDirector.setWorkingSolution(Objects.requireNonNull(solution)); return new DefaultSingleConstraintAssertion<>(scoreDirectorFactory, scoreDirector.calculateScore(), scoreDirector.getConstraintMatchTotalMap(), scoreDirector.getIndictmentMap());