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());