From 8351135c1b36ac93883d5ac2f04ba42a10074cca Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Fri, 17 Jul 2026 19:11:50 -0400 Subject: [PATCH 01/30] feat: add plumbing for indictment support --- .../core/impl/bavet/bi/PrecomputeBiNode.java | 4 +- .../impl/bavet/common/AbstractConcatNode.java | 2 + .../bavet/common/AbstractFlattenNode.java | 1 + .../impl/bavet/common/AbstractGroupNode.java | 2 + .../bavet/common/AbstractIfExistsNode.java | 17 +++- .../common/AbstractIndexedIfExistsNode.java | 10 +- .../impl/bavet/common/AbstractJoinNode.java | 2 + .../impl/bavet/common/AbstractMapNode.java | 1 + .../common/AbstractUnindexedIfExistsNode.java | 9 +- .../core/impl/bavet/common/ExistsCounter.java | 15 ++- .../core/impl/bavet/common/tuple/Tuple.java | 21 +++++ .../bavet/common/tuple/UniversalTuple.java | 21 +++++ .../tuple/indictment/IndictmentSource.java | 93 +++++++++++++++++++ .../impl/bavet/quad/PrecomputeQuadNode.java | 4 +- .../impl/bavet/tri/PrecomputeTriNode.java | 4 +- .../bavet/uni/AbstractForEachUniNode.java | 8 +- .../bavet/uni/ForEachFilteredUniNode.java | 4 +- .../bavet/uni/ForEachUnfilteredUniNode.java | 4 +- .../impl/bavet/uni/PrecomputeUniNode.java | 4 +- .../uni/AbstractForEachEnumeratingStream.java | 2 +- .../score/constraint/ConstraintMatch.java | 11 ++- .../constraint/ConstraintMatchPolicy.java | 19 ++-- .../constraint/ConstraintMatchTotal.java | 21 ++++- .../score/director/InnerScoreDirector.java | 2 +- .../bavet/BavetConstraintSessionFactory.java | 10 +- .../bi/BavetPrecomputeBiConstraintStream.java | 3 +- .../bi/BavetScoringBiConstraintStream.java | 5 +- .../bavet/bi/BiBigDecimalImpactHandler.java | 2 +- .../stream/bavet/bi/BiLongImpactHandler.java | 2 +- .../common/BavetPrecomputeBuildHelper.java | 3 +- .../common/ConstraintNodeBuildHelper.java | 6 ++ .../BavetPrecomputeQuadConstraintStream.java | 3 +- .../BavetScoringQuadConstraintStream.java | 5 +- .../quad/QuadBigDecimalImpactHandler.java | 3 +- .../bavet/quad/QuadLongImpactHandler.java | 3 +- .../BavetPrecomputeTriConstraintStream.java | 3 +- .../tri/BavetScoringTriConstraintStream.java | 5 +- .../bavet/tri/TriBigDecimalImpactHandler.java | 3 +- .../bavet/tri/TriLongImpactHandler.java | 3 +- .../uni/BavetForEachUniConstraintStream.java | 5 +- .../BavetPrecomputeUniConstraintStream.java | 3 +- .../uni/BavetScoringUniConstraintStream.java | 5 +- .../bavet/uni/UniBigDecimalImpactHandler.java | 2 +- .../bavet/uni/UniLongImpactHandler.java | 2 +- .../inliner/ConstraintMatchSupplier.java | 52 +++++++---- .../bavet/uni/ForEachUniNodeActivityTest.java | 12 +-- .../score/constraint/ConstraintMatchTest.java | 3 + .../common/ConstraintStreamTestExtension.java | 2 +- 48 files changed, 345 insertions(+), 81 deletions(-) create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/PrecomputeBiNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/PrecomputeBiNode.java index ee4f3575c31..6afb77873a3 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/PrecomputeBiNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/PrecomputeBiNode.java @@ -23,6 +23,8 @@ public PrecomputeBiNode(Supplier>> prec @Override protected BiTuple remapTuple(BiTuple tuple) { - return BiTuple.of(tuple.getA(), tuple.getB(), outputStoreSize); + var out = BiTuple.of(tuple.getA(), tuple.getB(), outputStoreSize); + out.setIndictmentSource(tuple.getIndictmentSource()); + return out; } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractConcatNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractConcatNode.java index 31946fe0b50..a2c2028ee43 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractConcatNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractConcatNode.java @@ -66,6 +66,7 @@ protected boolean canProduceTuples() { @Override public final void insertLeft(LeftTuple_ tuple) { var outTuple = getOutTupleFromLeft(tuple); + outTuple.setIndictmentSource(tuple.getIndictmentSource()); tuple.setStore(leftSourceTupleCloneStoreIndex, outTuple); propagationQueue.insert(outTuple); } @@ -109,6 +110,7 @@ public final void retractLeft(LeftTuple_ tuple) { @Override public final void insertRight(RightTuple_ tuple) { var outTuple = getOutTupleFromRight(tuple); + outTuple.setIndictmentSource(tuple.getIndictmentSource()); tuple.setStore(rightSourceTupleCloneStoreIndex, outTuple); propagationQueue.insert(outTuple); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractFlattenNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractFlattenNode.java index b20fe59e1c2..16fdff5bc58 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractFlattenNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractFlattenNode.java @@ -73,6 +73,7 @@ private void addTuple(InTuple_ originalTuple, FlattenedItem_ item, var reuse = bag.reuseOrAdvance(); if (reuse == null) { var created = createTuple(originalTuple, bag.value); + created.setIndictmentSource(originalTuple.getIndictmentSource()); bag.append(created); propagationQueue.insert(created); } else { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractGroupNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractGroupNode.java index ad9fc4ac5c4..b438715e613 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractGroupNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractGroupNode.java @@ -11,6 +11,7 @@ import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState; +import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; public abstract class AbstractGroupNode extends AbstractSingleInputNode { @@ -111,6 +112,7 @@ private void createTuple(InTuple_ tuple, GroupKey_ userSuppliedKey) { } tuple.setStore(groupStoreIndex, group); var outTuple = group.getTuple(); + outTuple.setIndictmentSource(IndictmentSource.aggregating(tuple, outTuple)); switch (outTuple.getState()) { case CREATING, UPDATING -> { // Already in the correct state. diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java index 072e417bef1..f0105a62061 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java @@ -5,6 +5,7 @@ import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -124,7 +125,7 @@ protected void killCounterLeft(ExistsCounter counter) { } } - protected void incrementCounterRight(ExistsCounter counter) { + protected void incrementCounterRightWithoutIndictment(ExistsCounter counter) { if (counter.countRight == 0) { if (shouldExist) { doInsertCounter(counter); @@ -135,6 +136,18 @@ protected void incrementCounterRight(ExistsCounter counter) { counter.countRight++; } + protected void incrementCounterRightUpdatingIndictment(ExistsCounter counter, UniTuple rightTuple) { + if (counter.countRight == 0) { + if (shouldExist) { + doInsertCounter(counter); + } else { + doRetractCounter(counter); + } + } // Else do not even propagate an update + counter.outTuple.setIndictmentSource(IndictmentSource.sourceWithSupport(counter.outTuple, rightTuple)); + counter.countRight++; + } + protected void decrementCounterRight(ExistsCounter counter) { counter.countRight--; if (counter.countRight == 0) { @@ -297,7 +310,7 @@ protected void updateCounterRight(ExistsCounter counter, UniTuple(counter, rightTuple); linkLeft(tracker); linkRight(tracker); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java index c4fd0c0f1b5..46aed3f764d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java @@ -14,6 +14,7 @@ import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; import ai.timefold.solver.core.impl.util.ListEntry; import org.jspecify.annotations.Nullable; @@ -207,7 +208,14 @@ public final void insertRight(UniTuple rightTuple) { private void updateCounterLeft(UniTuple rightTuple, Object compositeKey) { if (!isFiltering) { - forEachLeftCounter(rightTuple, compositeKey, this::incrementCounterRight); + // To prevent creating a dynamic lambda on the hot path, + // only call the 2-args version when indictments are enabled + if (rightTuple.getIndictmentSource() == IndictmentSource.DISABLED) { + forEachLeftCounter(rightTuple, compositeKey, this::incrementCounterRightWithoutIndictment); + } else { + forEachLeftCounter(rightTuple, compositeKey, + counter -> incrementCounterRightUpdatingIndictment(counter, rightTuple)); + } } else { // Defer the cross-match (the opposite-side read) to this node's own layer turn instead of computing it now, // at whatever layer the parent that produced rightTuple happens to be in. diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractJoinNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractJoinNode.java index 8d0a8b33eb2..2e85b88729f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractJoinNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractJoinNode.java @@ -10,6 +10,7 @@ import ai.timefold.solver.core.impl.bavet.common.tuple.TupleList; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; import org.jspecify.annotations.Nullable; @@ -101,6 +102,7 @@ protected final void insertOutTupleIfActiveFiltered(LeftTuple_ leftTuple, UniTup private void insertOutTuple(LeftTuple_ leftTuple, UniTuple rightTuple) { var outTuple = createOutTuple(leftTuple, rightTuple); + outTuple.setIndictmentSource(IndictmentSource.joining(leftTuple, rightTuple)); TupleList outTupleListLeft = leftTuple.getStore(inputStoreIndexLeftOutTupleList); outTupleListLeft.add(outTuple); outTuple.setStore(outputStoreIndexLeftOutTupleList, outTupleListLeft); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractMapNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractMapNode.java index 6fbc4ed2a12..394354f5af6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractMapNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractMapNode.java @@ -30,6 +30,7 @@ public final void insert(InTuple_ tuple) { + ") was already added in the tupleStore."); } var outTuple = map(tuple); + outTuple.setIndictmentSource(tuple.getIndictmentSource()); tuple.setStore(inputStoreIndex, outTuple); propagationQueue.insert(outTuple); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java index a471d2d0194..eb5ea426ecc 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java @@ -6,6 +6,7 @@ import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; import ai.timefold.solver.core.impl.util.ElementAwareLinkedList; /** @@ -101,7 +102,13 @@ public final void insertRight(UniTuple rightTuple) { } rightTuple.setStore(inputStoreIndexRightEntry, rightTupleList.add(rightTuple)); if (!isFiltering) { - counterList.forEach(this::incrementCounterRight); + // To prevent creating a dynamic lambda on the hot path, + // only call the 2-args version when indictments are enabled + if (rightTuple.getIndictmentSource() == IndictmentSource.DISABLED) { + counterList.forEach(this::incrementCounterRightWithoutIndictment); + } else { + counterList.forEach(counter -> incrementCounterRightUpdatingIndictment(counter, rightTuple)); + } } else { // Defer the cross-match (the opposite-side read) to this node's own layer turn instead of computing it now, // at whatever layer the parent that produced rightTuple happens to be in. diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounter.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounter.java index 0b69b862fb4..6f493d7e34c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounter.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounter.java @@ -1,22 +1,33 @@ package ai.timefold.solver.core.impl.bavet.common; +import java.util.ArrayList; + import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState; +import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; public final class ExistsCounter extends AbstractPropagationMetadataCarrier { final Tuple_ leftTuple; + final Tuple_ outTuple; TupleState state = TupleState.DEAD; // It's the node's job to mark a new instance as CREATING. int countRight = 0; ExistsCounter(Tuple_ leftTuple) { this.leftTuple = leftTuple; + if (leftTuple.getIndictmentSource() == IndictmentSource.DISABLED) { + outTuple = leftTuple; + } else { + outTuple = Tuple.copyOf(leftTuple); + outTuple.setIndictmentSource(new IndictmentSource.IndictmentSourceWithSupport(leftTuple.getIndictmentSource(), + new ArrayList<>())); + } } @Override public Tuple_ getTuple() { - return leftTuple; + return outTuple; } @Override @@ -31,7 +42,7 @@ public void setState(TupleState state) { @Override public String toString() { - return "Counter(" + leftTuple + ")"; + return "Counter(%s)".formatted(leftTuple); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java index 4c41fcdbc28..9c7f685029b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java @@ -3,6 +3,7 @@ import java.util.function.Function; import ai.timefold.solver.core.api.score.stream.uni.UniConstraintStream; +import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -24,6 +25,26 @@ public sealed interface Tuple permits BiTuple, QuadTuple, TriTuple, UniTuple { + static Tuple_ copyOf(Tuple_ originalTuple) { + var universalTuple = (UniversalTuple) originalTuple; + var copiedTuple = new UniversalTuple<>(universalTuple.storeSize(), universalTuple.cardinality()); + + copiedTuple.setA(universalTuple.getA()); + copiedTuple.setB(universalTuple.getB()); + copiedTuple.setC(universalTuple.getC()); + copiedTuple.setD(universalTuple.getD()); + copiedTuple.setState(universalTuple.getState()); + copiedTuple.setIndictmentSource(universalTuple.getIndictmentSource()); + for (var i = 0; i < universalTuple.storeSize(); i++) { + copiedTuple.setStore(i, universalTuple.getStore(i)); + } + return (Tuple_) copiedTuple; + } + + IndictmentSource getIndictmentSource(); + + void setIndictmentSource(IndictmentSource indictmentSource); + TupleState getState(); void setState(TupleState state); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java index 5efa7381d24..63cadf46844 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.bavet.common.tuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; + import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -20,6 +22,7 @@ final class UniversalTuple private @Nullable B b; private @Nullable C c; private @Nullable D d; + private IndictmentSource indictmentSource = IndictmentSource.DISABLED; private TupleState state = TupleState.DEAD; // It's the node's job to mark a new tuple as CREATING. UniversalTuple(int storeSize, int cardinality) { @@ -27,6 +30,14 @@ final class UniversalTuple this.store = storeSize > 0 ? new Object[storeSize] : EMPTY_STORE; } + int cardinality() { + return cardinality; + } + + int storeSize() { + return store.length; + } + @Override public void setA(@Nullable A a) { this.a = a; @@ -95,6 +106,16 @@ public void setStore(int index, @Nullable Object value) { return value; } + @Override + public IndictmentSource getIndictmentSource() { + return indictmentSource; + } + + @Override + public void setIndictmentSource(IndictmentSource indictmentSource) { + this.indictmentSource = indictmentSource; + } + @Override public String toString() { return switch (cardinality) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java new file mode 100644 index 00000000000..65cc64d2adc --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java @@ -0,0 +1,93 @@ +package ai.timefold.solver.core.impl.bavet.common.tuple.indictment; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; + +public sealed interface IndictmentSource { + IndictmentSource DISABLED = new DisabledIndictmentSource(); + + void visitSources(Consumer sourceConsumer); + + static IndictmentSource of(Object source) { + return new RootIndictmentSource(source); + } + + static IndictmentSource joining(Tuple left, Tuple right) { + if (left.getIndictmentSource() == DISABLED) { + return DISABLED; + } + return new JoinedIndictmentSource(left.getIndictmentSource(), right.getIndictmentSource()); + } + + static IndictmentSource aggregating(Tuple elementTuple, Tuple groupTuple) { + if (elementTuple.getIndictmentSource() == DISABLED) { + return DISABLED; + } + if (groupTuple.getIndictmentSource() instanceof AggregateIndictmentSource aggregateIndictmentSource) { + aggregateIndictmentSource.sourceList.add(elementTuple.getIndictmentSource()); + return aggregateIndictmentSource; + } else { + var collection = new ArrayList(); + collection.add(elementTuple.getIndictmentSource()); + return new AggregateIndictmentSource(collection); + } + } + + static IndictmentSource sourceWithSupport(Tuple carry, Tuple support) { + if (carry.getIndictmentSource() == DISABLED) { + return DISABLED; + } + if (carry.getIndictmentSource() instanceof IndictmentSourceWithSupport indictmentSourceWithSupport) { + indictmentSourceWithSupport.support.add(support.getIndictmentSource()); + return indictmentSourceWithSupport; + } else { + throw new IllegalStateException("Carry tuple (%s) does not have a %s %s; its source is (%s) instead." + .formatted(carry, IndictmentSourceWithSupport.class.getSimpleName(), IndictmentSource.class.getSimpleName(), + support.getIndictmentSource())); + } + } + + record DisabledIndictmentSource() implements IndictmentSource { + @Override + public void visitSources(Consumer sourceConsumer) { + throw new UnsupportedOperationException("Impossible state: indictments are disabled."); + } + } + + record RootIndictmentSource(Object source) implements IndictmentSource { + @Override + public void visitSources(Consumer sourceConsumer) { + sourceConsumer.accept(source); + } + } + + record JoinedIndictmentSource(IndictmentSource left, IndictmentSource right) implements IndictmentSource { + @Override + public void visitSources(Consumer sourceConsumer) { + left.visitSources(sourceConsumer); + right.visitSources(sourceConsumer); + } + } + + record AggregateIndictmentSource(List sourceList) implements IndictmentSource { + @Override + public void visitSources(Consumer sourceConsumer) { + for (var source : sourceList) { + source.visitSources(sourceConsumer); + } + } + } + + record IndictmentSourceWithSupport(IndictmentSource source, List support) implements IndictmentSource { + @Override + public void visitSources(Consumer sourceConsumer) { + source.visitSources(sourceConsumer); + for (var support : support) { + support.visitSources(sourceConsumer); + } + } + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/PrecomputeQuadNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/PrecomputeQuadNode.java index 8c65119bd99..371c8448162 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/PrecomputeQuadNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/PrecomputeQuadNode.java @@ -23,6 +23,8 @@ public PrecomputeQuadNode(Supplier remapTuple(QuadTuple tuple) { - return QuadTuple.of(tuple.getA(), tuple.getB(), tuple.getC(), tuple.getD(), outputStoreSize); + var out = QuadTuple.of(tuple.getA(), tuple.getB(), tuple.getC(), tuple.getD(), outputStoreSize); + out.setIndictmentSource(tuple.getIndictmentSource()); + return out; } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/PrecomputeTriNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/PrecomputeTriNode.java index 936563f13c5..06d776b782a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/PrecomputeTriNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/PrecomputeTriNode.java @@ -23,6 +23,8 @@ public PrecomputeTriNode(Supplier>> @Override protected TriTuple remapTuple(TriTuple tuple) { - return TriTuple.of(tuple.getA(), tuple.getB(), tuple.getC(), outputStoreSize); + var out = TriTuple.of(tuple.getA(), tuple.getB(), tuple.getC(), outputStoreSize); + out.setIndictmentSource(tuple.getIndictmentSource()); + return out; } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractForEachUniNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractForEachUniNode.java index a2843c24ea9..cfb61d0607f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractForEachUniNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractForEachUniNode.java @@ -11,6 +11,7 @@ import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -31,12 +32,14 @@ public abstract sealed class AbstractForEachUniNode private final Class forEachClass; private final int outputStoreSize; private final StaticPropagationQueue> propagationQueue; + private final boolean indictmentsEnabled; protected final Map> tupleMap = HashMap.newHashMap(1000); protected AbstractForEachUniNode(Class forEachClass, TupleLifecycle> nextNodesTupleLifecycle, - int outputStoreSize) { + boolean indictmentsEnabled, int outputStoreSize) { this.forEachClass = forEachClass; this.outputStoreSize = outputStoreSize; + this.indictmentsEnabled = indictmentsEnabled; this.propagationQueue = new StaticPropagationQueue<>(nextNodesTupleLifecycle); } @@ -58,6 +61,9 @@ public Class[] getSourceClasses() { @Override public void insert(@Nullable A a) { var tuple = UniTuple.of(a, outputStoreSize); + if (indictmentsEnabled) { + tuple.setIndictmentSource(IndictmentSource.of(a)); + } var old = tupleMap.put(a, tuple); if (old != null) { throw new IllegalStateException(""" diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/ForEachFilteredUniNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/ForEachFilteredUniNode.java index 015042f2d97..18533d8acfc 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/ForEachFilteredUniNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/ForEachFilteredUniNode.java @@ -24,8 +24,8 @@ public final class ForEachFilteredUniNode private int tupleCountWithoutFiltering = 0; public ForEachFilteredUniNode(Class forEachClass, Predicate filter, - TupleLifecycle> nextNodesTupleLifecycle, int outputStoreSize) { - super(forEachClass, nextNodesTupleLifecycle, outputStoreSize); + TupleLifecycle> nextNodesTupleLifecycle, boolean indictmentsEnabled, int outputStoreSize) { + super(forEachClass, nextNodesTupleLifecycle, indictmentsEnabled, outputStoreSize); this.nextNodesTupleLifecycle = Objects.requireNonNull(nextNodesTupleLifecycle); this.filter = Objects.requireNonNull(filter); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/ForEachUnfilteredUniNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/ForEachUnfilteredUniNode.java index fb40cb58be0..46198e99ca8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/ForEachUnfilteredUniNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/ForEachUnfilteredUniNode.java @@ -15,8 +15,8 @@ public final class ForEachUnfilteredUniNode private boolean isActive; public ForEachUnfilteredUniNode(Class forEachClass, TupleLifecycle> nextNodesTupleLifecycle, - int outputStoreSize) { - super(forEachClass, nextNodesTupleLifecycle, outputStoreSize); + boolean indictmentsEnabled, int outputStoreSize) { + super(forEachClass, nextNodesTupleLifecycle, indictmentsEnabled, outputStoreSize); this.nextNodesTupleLifecycle = nextNodesTupleLifecycle; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/PrecomputeUniNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/PrecomputeUniNode.java index 92dfee56180..70f64d5ad73 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/PrecomputeUniNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/PrecomputeUniNode.java @@ -23,7 +23,9 @@ public PrecomputeUniNode(Supplier>> preco @Override protected UniTuple remapTuple(UniTuple tuple) { - return UniTuple.of(tuple.getA(), outputStoreSize); + var out = UniTuple.of(tuple.getA(), outputStoreSize); + out.setIndictmentSource(tuple.getIndictmentSource()); + return out; } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/uni/AbstractForEachEnumeratingStream.java b/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/uni/AbstractForEachEnumeratingStream.java index ebb8cf59945..cf301f81438 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/uni/AbstractForEachEnumeratingStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/uni/AbstractForEachEnumeratingStream.java @@ -41,7 +41,7 @@ public final void collectActiveEnumeratingStreams(Set buildHelper) { TupleLifecycle> tupleLifecycle = buildHelper.getAggregatedTupleLifecycle(childStreamList); var outputStoreSize = buildHelper.extractTupleStoreSize(this); - var node = new ForEachUnfilteredUniNode<>(forEachClass, tupleLifecycle, outputStoreSize); + var node = new ForEachUnfilteredUniNode<>(forEachClass, tupleLifecycle, false, outputStoreSize); if (shouldIncludeNull && node.supports(LifecycleOperation.INSERT)) { node.insert(null); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java index 25a9746b816..dfac8c52ead 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java @@ -2,6 +2,8 @@ import static java.util.Objects.requireNonNull; +import java.util.List; + import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.stream.ConstraintJustification; import ai.timefold.solver.core.api.score.stream.ConstraintRef; @@ -28,6 +30,7 @@ public final class ConstraintMatch> implements Comp private final ConstraintRef constraintRef; private final @Nullable ConstraintJustification justification; + private final List indictedObjects; private final Score_ score; /** @@ -35,9 +38,11 @@ public final class ConstraintMatch> implements Comp * @param justification only null if justifications are disabled * @param score penalty or reward associated with the constraint match */ - public ConstraintMatch(ConstraintRef constraintRef, @Nullable ConstraintJustification justification, Score_ score) { + public ConstraintMatch(ConstraintRef constraintRef, @Nullable ConstraintJustification justification, + List indictedObjects, Score_ score) { this.constraintRef = requireNonNull(constraintRef); this.justification = justification; + this.indictedObjects = indictedObjects; this.score = requireNonNull(score); } @@ -62,6 +67,10 @@ public ConstraintRef getConstraintRef() { return (Justification_) justification; } + public List getIndictedObjects() { + return indictedObjects; + } + public Score_ getScore() { return score; } 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 index f239ad113da..bd963663f6d 100644 --- 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 @@ -13,33 +13,37 @@ @NullMarked public enum ConstraintMatchPolicy { - DISABLED(false, false), - ENABLED_WITHOUT_JUSTIFICATIONS(true, false), - ENABLED(true, true); + DISABLED(false, false, false), + ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS(true, false, false), + ENABLED_WITHOUT_JUSTIFICATIONS(true, false, true), + ENABLED_WITHOUT_INDICTMENTS(true, true, false), + ENABLED(true, 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}). + * we can configure the solver to not produce justifications ({@link #ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS}). * * @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_MATCH_COUNT, FETCH_SHALLOW -> ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS; case FETCH_ALL -> ENABLED; }; } private final boolean enabled; private final boolean justificationEnabled; + private final boolean indictmentsEnabled; - ConstraintMatchPolicy(boolean enabled, boolean justificationEnabled) { + ConstraintMatchPolicy(boolean enabled, boolean justificationEnabled, boolean indictmentsEnabled) { this.enabled = enabled; this.justificationEnabled = justificationEnabled; + this.indictmentsEnabled = indictmentsEnabled; } public boolean isEnabled() { @@ -50,4 +54,7 @@ public boolean isJustificationEnabled() { return justificationEnabled; } + public boolean isIndictmentsEnabled() { + return indictmentsEnabled; + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTotal.java b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTotal.java index 6902f3014eb..31502a8336c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTotal.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTotal.java @@ -2,6 +2,7 @@ import static java.util.Objects.requireNonNull; +import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -68,18 +69,32 @@ public Score_ getScore() { * @return never null */ public ConstraintMatch addConstraintMatch(List justifications, Score_ score) { - return addConstraintMatch(DefaultConstraintJustification.of(score, justifications), score); + return addConstraintMatch(DefaultConstraintJustification.of(score, justifications), justifications, score); } /** * Creates a {@link ConstraintMatch} and adds it to the collection returned by {@link #getConstraintMatchSet()}. - * It will be justified with the provided {@link ConstraintJustification}. + * It will be justified with the provided {@link ConstraintJustification} and will indict the given objects. + * + * @param score never null + * @return never null + */ + public ConstraintMatch addConstraintMatch(ConstraintJustification justification, List indictedObjects, + Score_ score) { + var constraintMatch = new ConstraintMatch<>(constraintRef, justification, indictedObjects, score); + addConstraintMatch(constraintMatch); + return constraintMatch; + } + + /** + * Creates a {@link ConstraintMatch} and adds it to the collection returned by {@link #getConstraintMatchSet()}. + * It will be justified with the provided {@link ConstraintJustification} and will indict no objects. * * @param score never null * @return never null */ public ConstraintMatch addConstraintMatch(ConstraintJustification justification, Score_ score) { - var constraintMatch = new ConstraintMatch(constraintRef, justification, score); + var constraintMatch = new ConstraintMatch<>(constraintRef, justification, Collections.emptyList(), score); addConstraintMatch(constraintMatch); return constraintMatch; } 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 361c396e6a8..93465a02502 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 @@ -87,7 +87,7 @@ public interface InnerScoreDirector> /** * @return {@link ConstraintMatchPolicy#ENABLED} if {@link #getConstraintMatchTotalMap()} can be called. - * {@link ConstraintMatchPolicy#ENABLED_WITHOUT_JUSTIFICATIONS} if only the former can be called. + * {@link ConstraintMatchPolicy#ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS} if only the former can be called. * {@link ConstraintMatchPolicy#DISABLED} if neither can be called. */ ConstraintMatchPolicy getConstraintMatchPolicy(); 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 7629a033f5e..d30aa1805ef 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 @@ -117,13 +117,17 @@ public BavetConstraintSession buildSession(Solution_ workingSolution, } return new BavetConstraintSession<>(scoreInliner, buildNodeNetwork(workingSolution, consistencyTracker, constraintStreamSet, scoreInliner, constraintProfiler, - scoreDirectorDerived)); + constraintMatchPolicy.isIndictmentsEnabled(), scoreDirectorDerived)); } private ConstraintStreamsBavetNodeNetwork buildNodeNetwork(Solution_ workingSolution, ConsistencyTracker consistencyTracker, Set> constraintStreamSet, - AbstractScoreInliner scoreInliner, InnerConstraintProfiler profiler, boolean scoreDirectorDerived) { - var buildHelper = new ConstraintNodeBuildHelper<>(consistencyTracker, constraintStreamSet, scoreInliner, profiler); + AbstractScoreInliner scoreInliner, InnerConstraintProfiler profiler, + boolean indictmentsEnabled, + boolean scoreDirectorDerived) { + var buildHelper = + new ConstraintNodeBuildHelper<>(consistencyTracker, constraintStreamSet, scoreInliner, indictmentsEnabled, + profiler); var declaredClassToNodeMap = new LinkedHashMap, List>>(); var nodeList = buildHelper.buildNodeList(constraintStreamSet, buildHelper, BavetAbstractConstraintStream::buildNode, node -> { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetPrecomputeBiConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetPrecomputeBiConstraintStream.java index 50c0be11cc3..6b67b38ac2b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetPrecomputeBiConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetPrecomputeBiConstraintStream.java @@ -40,7 +40,8 @@ public void setAftBridge(BavetAftBridgeBiConstraintStream aftSt @Override public > void buildNode(ConstraintNodeBuildHelper buildHelper) { Supplier>> precomputeBuildHelperSupplier = - () -> new BavetPrecomputeBuildHelper<>(recordingPrecomputedConstraintStream, entityClassSet); + () -> new BavetPrecomputeBuildHelper<>(recordingPrecomputedConstraintStream, + buildHelper.isIndictmentEnabled(), entityClassSet); var outputStoreSize = buildHelper.extractTupleStoreSize(aftStream); buildHelper.addNode(new PrecomputeBiNode<>(precomputeBuildHelperSupplier, 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 3bf4653eb17..4360cfb9428 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 @@ -53,8 +53,9 @@ public > void buildNode(ConstraintNodeBuildHelper buildScoreImpacter(ConstraintMatchPolicy constraintMatchPolicy) { return switch (constraintMatchPolicy) { case DISABLED -> scoreImpact::impactNaked; - case ENABLED -> scoreImpact::impactFull; - case ENABLED_WITHOUT_JUSTIFICATIONS -> scoreImpact::impactWithoutJustification; + case ENABLED, ENABLED_WITHOUT_INDICTMENTS -> scoreImpact::impactFull; + case ENABLED_WITHOUT_JUSTIFICATIONS, ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS -> + scoreImpact::impactWithoutJustification; }; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BiBigDecimalImpactHandler.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BiBigDecimalImpactHandler.java index a24737b796e..0a86db1d218 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BiBigDecimalImpactHandler.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BiBigDecimalImpactHandler.java @@ -31,7 +31,7 @@ public ScoreImpact impactFull(WeightedScoreImpacter impacter, BiTuple impactFull(WeightedScoreImpacter impacter, BiTuple { public BavetPrecomputeBuildHelper( BavetAbstractConstraintStream recordingPrecomputeConstraintStream, + boolean indictmentsEnabled, Set> entityClassSet) { if (recordingPrecomputeConstraintStream.getRetrievalSemantics() != RetrievalSemantics.PRECOMPUTE) { throw new IllegalStateException( @@ -63,7 +64,7 @@ public BavetPrecomputeBuildHelper( var buildHelper = new ConstraintNodeBuildHelper<>(new ConsistencyTracker<>(), streamSet, AbstractScoreInliner.buildScoreInliner(new SimpleScoreDefinition(), Collections.emptyMap(), ConstraintMatchPolicy.DISABLED), - null); + indictmentsEnabled, null); var declaredClassToNodeMap = new LinkedHashMap, List>>(); var nodeList = buildHelper.buildNodeList(streamSet, buildHelper, diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/ConstraintNodeBuildHelper.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/ConstraintNodeBuildHelper.java index 62df337672d..6af3871aaa4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/ConstraintNodeBuildHelper.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/ConstraintNodeBuildHelper.java @@ -41,6 +41,7 @@ public final class ConstraintNodeBuildHelper scoreInliner; private final ConsistencyTracker consistencyTracker; private final @Nullable InnerConstraintProfiler constraintProfiler; + private final boolean indictmentsEnabled; private final Map, Map>> entityDescriptorToForEachCriteriaToPredicateMap; private final Map, List>> streamToProfileIdSets; private final Map, Scorer> streamToScorers = new HashMap<>(); @@ -49,11 +50,13 @@ public final class ConstraintNodeBuildHelper consistencyTracker, Set> activeStreamSet, AbstractScoreInliner scoreInliner, + boolean indictmentsEnabled, @Nullable InnerConstraintProfiler profiler) { super(activeStreamSet); this.consistencyTracker = consistencyTracker; this.scoreInliner = scoreInliner; this.constraintProfiler = profiler; + this.indictmentsEnabled = indictmentsEnabled; this.entityDescriptorToForEachCriteriaToPredicateMap = new HashMap<>(); this.streamToProfileIdSets = HashMap.newHashMap(Math.max(16, activeStreamSet.size() / 2)); } @@ -153,4 +156,7 @@ public ConstraintStreamsBavetNodeNetwork buildPrecomputeNodeNetwork(List> void buildNode(ConstraintNodeBuildHelper buildHelper) { Supplier>> precomputeBuildHelperSupplier = - () -> new BavetPrecomputeBuildHelper<>(recordingPrecomputedConstraintStream, entityClassSet); + () -> new BavetPrecomputeBuildHelper<>(recordingPrecomputedConstraintStream, + buildHelper.isIndictmentEnabled(), entityClassSet); var outputStoreSize = buildHelper.extractTupleStoreSize(aftStream); buildHelper.addNode(new PrecomputeQuadNode<>(precomputeBuildHelperSupplier, 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 ebaecf8b6da..8f81dd5acab 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 @@ -54,8 +54,9 @@ public > void buildNode(ConstraintNodeBuildHelper buildScoreImpacter(ConstraintMatchPolicy constraintMatchPolicy) { return switch (constraintMatchPolicy) { case DISABLED -> scoreImpact::impactNaked; - case ENABLED -> scoreImpact::impactFull; - case ENABLED_WITHOUT_JUSTIFICATIONS -> scoreImpact::impactWithoutJustification; + case ENABLED, ENABLED_WITHOUT_INDICTMENTS -> scoreImpact::impactFull; + case ENABLED_WITHOUT_JUSTIFICATIONS, ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS -> + scoreImpact::impactWithoutJustification; }; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/QuadBigDecimalImpactHandler.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/QuadBigDecimalImpactHandler.java index acbe999c17c..2e48e38937a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/QuadBigDecimalImpactHandler.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/QuadBigDecimalImpactHandler.java @@ -35,8 +35,7 @@ public ScoreImpact impactFull(WeightedScoreImpacter impacter, QuadTuple var d = tuple.getD(); var constraint = impacter.getContext().getConstraint(); return impacter.impactScore(matchWeigher.apply(a, b, c, d), - ConstraintMatchSupplier.of(constraint.getJustificationMapping(), a, b, - c, d)); + ConstraintMatchSupplier.of(constraint.getJustificationMapping(), tuple)); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/QuadLongImpactHandler.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/QuadLongImpactHandler.java index c58f81da4c0..b3866e237ff 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/QuadLongImpactHandler.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/QuadLongImpactHandler.java @@ -33,8 +33,7 @@ public ScoreImpact impactFull(WeightedScoreImpacter impacter, QuadTuple var d = tuple.getD(); var constraint = impacter.getContext().getConstraint(); return impacter.impactScore(matchWeigher.applyAsLong(a, b, c, d), - ConstraintMatchSupplier.of(constraint.getJustificationMapping(), a, b, - c, d)); + ConstraintMatchSupplier.of(constraint.getJustificationMapping(), tuple)); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetPrecomputeTriConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetPrecomputeTriConstraintStream.java index c8ed992bf7e..66ab83c3845 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetPrecomputeTriConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetPrecomputeTriConstraintStream.java @@ -39,7 +39,8 @@ public void setAftBridge(BavetAftBridgeTriConstraintStream a @Override public > void buildNode(ConstraintNodeBuildHelper buildHelper) { Supplier>> precomputeBuildHelperSupplier = - () -> new BavetPrecomputeBuildHelper<>(recordingPrecomputedConstraintStream, entityClassSet); + () -> new BavetPrecomputeBuildHelper<>(recordingPrecomputedConstraintStream, + buildHelper.isIndictmentEnabled(), entityClassSet); var outputStoreSize = buildHelper.extractTupleStoreSize(aftStream); buildHelper.addNode(new PrecomputeTriNode<>(precomputeBuildHelperSupplier, 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 41e55d2a562..6412a07540f 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 @@ -54,8 +54,9 @@ public > void buildNode(ConstraintNodeBuildHelper buildScoreImpacter(ConstraintMatchPolicy constraintMatchPolicy) { return switch (constraintMatchPolicy) { case DISABLED -> scoreImpact::impactNaked; - case ENABLED -> scoreImpact::impactFull; - case ENABLED_WITHOUT_JUSTIFICATIONS -> scoreImpact::impactWithoutJustification; + case ENABLED, ENABLED_WITHOUT_INDICTMENTS -> scoreImpact::impactFull; + case ENABLED_WITHOUT_JUSTIFICATIONS, ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS -> + scoreImpact::impactWithoutJustification; }; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/TriBigDecimalImpactHandler.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/TriBigDecimalImpactHandler.java index b2f3f62902d..9cb71402639 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/TriBigDecimalImpactHandler.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/TriBigDecimalImpactHandler.java @@ -27,8 +27,7 @@ public ScoreImpact impactFull(WeightedScoreImpacter impacter, TriTuple< var c = tuple.getC(); var constraint = impacter.getContext().getConstraint(); return impacter.impactScore(matchWeigher.apply(a, b, c), - ConstraintMatchSupplier.of(constraint.getJustificationMapping(), a, b, - c)); + ConstraintMatchSupplier.of(constraint.getJustificationMapping(), tuple)); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/TriLongImpactHandler.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/TriLongImpactHandler.java index 5f8b21f8ffb..6fd63bf641e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/TriLongImpactHandler.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/TriLongImpactHandler.java @@ -31,8 +31,7 @@ public ScoreImpact impactFull(WeightedScoreImpacter impacter, TriTuple< var c = tuple.getC(); var constraint = impacter.getContext().getConstraint(); return impacter.impactScore(matchWeigher.applyAsLong(a, b, c), - ConstraintMatchSupplier.of(constraint.getJustificationMapping(), a, b, - c)); + ConstraintMatchSupplier.of(constraint.getJustificationMapping(), tuple)); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetForEachUniConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetForEachUniConstraintStream.java index f0458c90a6b..f0505a1eeb6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetForEachUniConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetForEachUniConstraintStream.java @@ -54,9 +54,10 @@ public > void buildNode(ConstraintNodeBuildHelper> getAggregatedTupleLifecycle(childStreamList); int outputStoreSize = buildHelper.extractTupleStoreSize(this); var filter = filterFunction != null ? filterFunction.apply(buildHelper) : null; - var node = filter == null ? new ForEachUnfilteredUniNode<>(forEachClass, tupleLifecycle, outputStoreSize) + var node = filter == null ? new ForEachUnfilteredUniNode<>(forEachClass, tupleLifecycle, + buildHelper.isIndictmentEnabled(), outputStoreSize) : new ForEachFilteredUniNode<>(forEachClass, filter, tupleLifecycle, - outputStoreSize); + buildHelper.isIndictmentEnabled(), outputStoreSize); buildHelper.addNode(node, this, null); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetPrecomputeUniConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetPrecomputeUniConstraintStream.java index ad7ca716fc7..6eee732d05c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetPrecomputeUniConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetPrecomputeUniConstraintStream.java @@ -39,7 +39,8 @@ public void setAftBridge(BavetAftBridgeUniConstraintStream aftStre @Override public > void buildNode(ConstraintNodeBuildHelper buildHelper) { Supplier>> precomputeBuildHelperSupplier = - () -> new BavetPrecomputeBuildHelper<>(recordingPrecomputedConstraintStream, entityClassSet); + () -> new BavetPrecomputeBuildHelper<>(recordingPrecomputedConstraintStream, + buildHelper.isIndictmentEnabled(), entityClassSet); var outputStoreSize = buildHelper.extractTupleStoreSize(aftStream); buildHelper.addNode(new PrecomputeUniNode<>(precomputeBuildHelperSupplier, 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 2ae32839cb5..091dd1611a4 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 @@ -53,8 +53,9 @@ public > void buildNode(ConstraintNodeBuildHelper buildScoreImpacter(ConstraintMatchPolicy constraintMatchPolicy) { return switch (constraintMatchPolicy) { case DISABLED -> scoreImpact::impactNaked; - case ENABLED -> scoreImpact::impactFull; - case ENABLED_WITHOUT_JUSTIFICATIONS -> scoreImpact::impactWithoutJustification; + case ENABLED, ENABLED_WITHOUT_INDICTMENTS -> scoreImpact::impactFull; + case ENABLED_WITHOUT_JUSTIFICATIONS, ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS -> + scoreImpact::impactWithoutJustification; }; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/UniBigDecimalImpactHandler.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/UniBigDecimalImpactHandler.java index 0da5ce44888..9bd7b27c833 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/UniBigDecimalImpactHandler.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/UniBigDecimalImpactHandler.java @@ -28,7 +28,7 @@ public ScoreImpact impactFull(WeightedScoreImpacter impacter, UniTuple< var a = tuple.getA(); var constraint = impacter.getContext().getConstraint(); return impacter.impactScore(matchWeigher.apply(a), - ConstraintMatchSupplier.of(constraint.getJustificationMapping(), a)); + ConstraintMatchSupplier.of(constraint.getJustificationMapping(), tuple)); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/UniLongImpactHandler.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/UniLongImpactHandler.java index 27d8c2a6312..5484b5e4bda 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/UniLongImpactHandler.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/UniLongImpactHandler.java @@ -22,7 +22,7 @@ public ScoreImpact impactFull(WeightedScoreImpacter impacter, UniTuple< var a = tuple.getA(); var constraint = impacter.getContext().getConstraint(); return impacter.impactScore(matchWeigher.applyAsLong(a), - ConstraintMatchSupplier.of(constraint.getJustificationMapping(), a)); + ConstraintMatchSupplier.of(constraint.getJustificationMapping(), tuple)); } @Override 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 09c4e7115d1..e0c1c287ce0 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 @@ -1,6 +1,9 @@ package ai.timefold.solver.core.impl.score.stream.common.inliner; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Objects; import java.util.function.BiFunction; import java.util.stream.Collectors; @@ -11,6 +14,12 @@ import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintJustification; +import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.QuadTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.TriTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; import ai.timefold.solver.core.impl.score.constraint.ConstraintMatch; import org.jspecify.annotations.NullMarked; @@ -31,6 +40,15 @@ public interface ConstraintMatchSupplier> extends BiFunction> { + static List collectIndictments(Tuple tuple) { + if (tuple.getIndictmentSource() == IndictmentSource.DISABLED) { + return Collections.emptyList(); + } + var out = new ArrayList<>(); + tuple.getIndictmentSource().visitSources(out::add); + return out; + } + /** * * @return the constraint match returned by the supplier will have its justification set to null. @@ -39,18 +57,18 @@ public interface ConstraintMatchSupplier> */ static > ConstraintMatchSupplier empty() { return (constraint, impact) -> new ConstraintMatch<>(constraint.getConstraintRef(), null, - impact); + Collections.emptyList(), impact); } static > ConstraintMatchSupplier of( BiFunction, ConstraintJustification> justificationMapping, - A a) { + UniTuple tuple) { return (constraint, impact) -> { try { - var justification = justificationMapping.apply(a, impact); - return new ConstraintMatch<>(constraint.getConstraintRef(), justification, impact); + var justification = justificationMapping.apply(tuple.getA(), impact); + return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(tuple), impact); } catch (Exception e) { - throw createJustificationException(constraint, e, a); + throw createJustificationException(constraint, e, tuple.getA()); } }; } @@ -68,39 +86,39 @@ private static String factsToString(Object... facts) { static > ConstraintMatchSupplier of( TriFunction justificationMapping, - A a, B b) { + BiTuple tuple) { return (constraint, impact) -> { try { - var justification = justificationMapping.apply(a, b, impact); - return new ConstraintMatch<>(constraint.getConstraintRef(), justification, impact); + var justification = justificationMapping.apply(tuple.getA(), tuple.getB(), impact); + return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(tuple), impact); } catch (Exception e) { - throw createJustificationException(constraint, e, a, b); + throw createJustificationException(constraint, e, tuple.getA(), tuple.getB()); } }; } static > ConstraintMatchSupplier of( QuadFunction justificationMapping, - A a, B b, C c) { + TriTuple tuple) { return (constraint, impact) -> { try { - var justification = justificationMapping.apply(a, b, c, impact); - return new ConstraintMatch<>(constraint.getConstraintRef(), justification, impact); + var justification = justificationMapping.apply(tuple.getA(), tuple.getB(), tuple.getC(), impact); + return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(tuple), impact); } catch (Exception e) { - throw createJustificationException(constraint, e, a, b, c); + throw createJustificationException(constraint, e, tuple.getA(), tuple.getB(), tuple.getC()); } }; } static > ConstraintMatchSupplier of( PentaFunction justificationMapping, - A a, B b, C c, D d) { + QuadTuple tuple) { return (constraint, impact) -> { try { - var justification = justificationMapping.apply(a, b, c, d, impact); - return new ConstraintMatch<>(constraint.getConstraintRef(), justification, impact); + var justification = justificationMapping.apply(tuple.getA(), tuple.getB(), tuple.getC(), tuple.getD(), impact); + return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(tuple), impact); } catch (Exception e) { - throw createJustificationException(constraint, e, a, b, c, d); + throw createJustificationException(constraint, e, tuple.getA(), tuple.getB(), tuple.getC(), tuple.getD()); } }; } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/bavet/uni/ForEachUniNodeActivityTest.java b/core/src/test/java/ai/timefold/solver/core/impl/bavet/uni/ForEachUniNodeActivityTest.java index 5017b89c41b..40b09c9ade7 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/bavet/uni/ForEachUniNodeActivityTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/bavet/uni/ForEachUniNodeActivityTest.java @@ -25,7 +25,7 @@ private static TupleLifecycle> mockDownstream(boolean active) { @Test void unfilteredInactiveWhenNoFacts() { var downstream = mockDownstream(true); - var node = new ForEachUnfilteredUniNode<>(String.class, downstream, 1); + var node = new ForEachUnfilteredUniNode<>(String.class, downstream, false, 1); node.afterAllFactsInserted(true); assertThat(node.isActive()).isFalse(); } @@ -33,7 +33,7 @@ void unfilteredInactiveWhenNoFacts() { @Test void unfilteredActiveWhenFactsExist() { var downstream = mockDownstream(true); - var node = new ForEachUnfilteredUniNode<>(String.class, downstream, 1); + var node = new ForEachUnfilteredUniNode<>(String.class, downstream, false, 1); node.insert("a"); node.afterAllFactsInserted(true); assertThat(node.isActive()).isTrue(); @@ -42,7 +42,7 @@ void unfilteredActiveWhenFactsExist() { @Test void unfilteredInactiveWhenDownstreamInactive() { var downstream = mockDownstream(false); - var node = new ForEachUnfilteredUniNode<>(String.class, downstream, 1); + var node = new ForEachUnfilteredUniNode<>(String.class, downstream, false, 1); node.insert("a"); node.afterAllFactsInserted(true); assertThat(node.isActive()).isFalse(); @@ -51,7 +51,7 @@ void unfilteredInactiveWhenDownstreamInactive() { @Test void filteredInactiveWhenNoFacts() { var downstream = mockDownstream(true); - var node = new ForEachFilteredUniNode<>(String.class, s -> true, downstream, 1); + var node = new ForEachFilteredUniNode<>(String.class, s -> true, downstream, false, 1); node.afterAllFactsInserted(true); assertThat(node.isActive()).isFalse(); } @@ -59,7 +59,7 @@ void filteredInactiveWhenNoFacts() { @Test void filteredActiveWhenFactInsertedEvenIfFilteredOut() { var downstream = mockDownstream(true); - var node = new ForEachFilteredUniNode<>(String.class, s -> false, downstream, 1); + var node = new ForEachFilteredUniNode<>(String.class, s -> false, downstream, false, 1); node.insert("a"); node.afterAllFactsInserted(true); assertThat(node.isActive()).isTrue(); @@ -68,7 +68,7 @@ void filteredActiveWhenFactInsertedEvenIfFilteredOut() { @Test void filteredInactiveWhenDownstreamInactive() { var downstream = mockDownstream(false); - var node = new ForEachFilteredUniNode<>(String.class, s -> true, downstream, 1); + var node = new ForEachFilteredUniNode<>(String.class, s -> true, downstream, false, 1); node.insert("a"); node.afterAllFactsInserted(true); assertThat(node.isActive()).isFalse(); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTest.java index 5102f4cd01b..f7e31759389 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTest.java @@ -3,6 +3,8 @@ import static ai.timefold.solver.core.api.score.SimpleScore.ONE; import static ai.timefold.solver.core.api.score.SimpleScore.ZERO; +import java.util.List; + import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.SimpleScore; import ai.timefold.solver.core.api.score.stream.ConstraintRef; @@ -26,6 +28,7 @@ private > ConstraintMatch buildConstraintMa Object... facts) { return new ConstraintMatch<>(ConstraintRef.of(constraintName), DefaultConstraintJustification.of(score, facts), + List.of(facts), score); } 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 c16f5df7d54..d2edef784a4 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 @@ -33,7 +33,7 @@ public boolean supportsTestTemplate(ExtensionContext context) { @Override public Stream provideTestTemplateInvocationContexts(ExtensionContext context) { return Stream - .of(ConstraintMatchPolicy.ENABLED, ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS, + .of(ConstraintMatchPolicy.ENABLED, ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS, ConstraintMatchPolicy.DISABLED) .map(ConstraintStreamTestExtension::invocationContext); } From 8a548632745b414c345f530c9b7c693b27fdbe1f Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Mon, 20 Jul 2026 13:41:14 -0400 Subject: [PATCH 02/30] chore: remove creating a new tuple for `ifExists` indictments --- .../impl/bavet/common/AbstractIfExistsNode.java | 3 +-- .../core/impl/bavet/common/ExistsCounter.java | 13 +------------ 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java index f0105a62061..23e628d3cf2 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java @@ -5,7 +5,6 @@ import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; -import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -144,7 +143,7 @@ protected void incrementCounterRightUpdatingIndictment(ExistsCounter doRetractCounter(counter); } } // Else do not even propagate an update - counter.outTuple.setIndictmentSource(IndictmentSource.sourceWithSupport(counter.outTuple, rightTuple)); + // TODO: Add right tuple to support counter.countRight++; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounter.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounter.java index 6f493d7e34c..db646736653 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounter.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounter.java @@ -1,33 +1,22 @@ package ai.timefold.solver.core.impl.bavet.common; -import java.util.ArrayList; - import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState; -import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; public final class ExistsCounter extends AbstractPropagationMetadataCarrier { final Tuple_ leftTuple; - final Tuple_ outTuple; TupleState state = TupleState.DEAD; // It's the node's job to mark a new instance as CREATING. int countRight = 0; ExistsCounter(Tuple_ leftTuple) { this.leftTuple = leftTuple; - if (leftTuple.getIndictmentSource() == IndictmentSource.DISABLED) { - outTuple = leftTuple; - } else { - outTuple = Tuple.copyOf(leftTuple); - outTuple.setIndictmentSource(new IndictmentSource.IndictmentSourceWithSupport(leftTuple.getIndictmentSource(), - new ArrayList<>())); - } } @Override public Tuple_ getTuple() { - return outTuple; + return leftTuple; } @Override From c0f24ddb0a62064551ee773569aa996f1e047f8c Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Mon, 20 Jul 2026 16:51:19 -0400 Subject: [PATCH 03/30] chore: use a Map to store extra indicted objects from ifExists --- .../bavet/common/AbstractIfExistsNode.java | 27 +++++++++++---- .../common/AbstractIndexedIfExistsNode.java | 24 +++++++++++-- .../bavet/common/AbstractNodeBuildHelper.java | 22 ++++++++++++ .../common/AbstractUnindexedIfExistsNode.java | 8 ++++- .../core/impl/bavet/common/tuple/Tuple.java | 19 ++--------- .../bavet/common/tuple/UniversalTuple.java | 14 ++++++++ .../score/constraint/ConstraintMatch.java | 3 +- .../common/ConstraintNodeBuildHelper.java | 8 +++++ .../score/stream/bavet/common/Scorer.java | 5 ++- .../stream/common/AbstractConstraint.java | 9 +++++ .../common/inliner/AbstractScoreInliner.java | 3 +- .../inliner/ConstraintMatchSupplier.java | 21 ++++++++---- .../stream/common/inliner/ScoreContext.java | 4 +++ .../ForEachFilteredUniNodeActivityTest.java | 2 +- .../common/AbstractConstraintStreamTest.java | 34 +++++++++++++++---- .../common/ConstraintStreamTestExtension.java | 15 ++++++-- .../uni/AbstractUniConstraintStreamTest.java | 11 ++++-- 17 files changed, 182 insertions(+), 47 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java index 23e628d3cf2..010810636ca 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java @@ -1,10 +1,13 @@ package ai.timefold.solver.core.impl.bavet.common; +import java.util.Objects; + import ai.timefold.solver.core.impl.bavet.common.tuple.InTupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -143,12 +146,15 @@ protected void incrementCounterRightUpdatingIndictment(ExistsCounter doRetractCounter(counter); } } // Else do not even propagate an update - // TODO: Add right tuple to support + counter.getTuple().getIndictmentSupportForNodeId(getId()) + .add(Objects.requireNonNull(rightTuple.getA())); counter.countRight++; } protected void decrementCounterRight(ExistsCounter counter) { counter.countRight--; + counter.getTuple().getIndictmentSupportForNodeId(getId()) + .remove(Objects.requireNonNull(rightTuple.getA())); if (counter.countRight == 0) { if (shouldExist) { doRetractCounter(counter); @@ -206,11 +212,20 @@ private void removeRight(FilteringTracker tracker) { */ protected void clearRightTrackerList(UniTuple rightTuple) { FilteringTracker tracker = rightTuple.removeStore(inputStoreIndexRightTrackerList); - while (tracker != null) { - var next = tracker.rightNext; - decrementCounterRight(tracker.counter); - removeLeft(tracker); - tracker = next; + if (rightTuple.getIndictmentSource() != IndictmentSource.DISABLED) { + while (tracker != null) { + var next = tracker.rightNext; + decrementCounterRightUpdatingIndictment(tracker.counter, rightTuple); + removeFromLeft(tracker); + tracker = next; + } + } else { + while (tracker != null) { + var next = tracker.rightNext; + decrementCounterRightWithoutIndictment(tracker.counter); + removeFromLeft(tracker); + tracker = next; + } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java index 46aed3f764d..b5b5312d4e2 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java @@ -255,7 +255,13 @@ public final void updateRight(UniTuple rightTuple) { indexerRight.remove(oldCompositeKey, entry); } if (!isFiltering) { - forEachLeftCounter(rightTuple, oldCompositeKey, this::decrementCounterRight); + // To prevent creating a dynamic lambda on the hot path, + // only call the 2-args version when indictments are enabled + if (rightTuple.getIndictmentSource() == IndictmentSource.DISABLED) { + forEachLeftCounter(rightTuple, oldCompositeKey, this::decrementCounterRightWithoutIndictment); + } else { + forEachLeftCounter(rightTuple, oldCompositeKey, counter -> decrementCounterRightUpdatingIndictment(counter, rightTuple)); + } } else { clearRightTrackerList(rightTuple); } @@ -279,14 +285,26 @@ public final void retractRight(UniTuple rightTuple) { bucket.removeRight(compositeKey, entry); fusedEqualIndex.removeBucketIfEmpty(compositeKey, bucket); if (!isFiltering) { - bucket.forEachLeft(compositeKey, this::decrementCounterRight); + // To prevent creating a dynamic lambda on the hot path, + // only call the 2-args version when indictments are enabled + if (rightTuple.getIndictmentSource() == IndictmentSource.DISABLED) { + bucket.forEachLeft(compositeKey, this::decrementCounterRightWithoutIndictment); + } else { + bucket.forEachLeft(compositeKey, counter -> decrementCounterRightUpdatingIndictment(counter, rightTuple)); + } } else { clearRightTrackerList(rightTuple); } } else { indexerRight.remove(compositeKey, entry); if (!isFiltering) { - indexerLeft.forEach(compositeKey, this::decrementCounterRight); + // To prevent creating a dynamic lambda on the hot path, + // only call the 2-args version when indictments are enabled + if (rightTuple.getIndictmentSource() == IndictmentSource.DISABLED) { + indexerLeft.forEach(compositeKey, this::decrementCounterRightWithoutIndictment); + } else { + indexerLeft.forEach(compositeKey, counter -> decrementCounterRightUpdatingIndictment(counter, rightTuple)); + } } else { clearRightTrackerList(rightTuple); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractNodeBuildHelper.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractNodeBuildHelper.java index 2272599f66e..c3130c240b3 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractNodeBuildHelper.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractNodeBuildHelper.java @@ -24,6 +24,7 @@ public abstract class AbstractNodeBuildHelper { private final Set activeStreamSet; private final Map nodeCreatorMap; + private final Map streamToCreatedNodeMap; private final Map> tupleLifecycleMap; private final Map storeIndexMap; @@ -34,6 +35,7 @@ protected AbstractNodeBuildHelper(Set activeStreamSet) { this.activeStreamSet = activeStreamSet; var activeStreamSetSize = activeStreamSet.size(); this.nodeCreatorMap = HashMap.newHashMap(Math.max(16, activeStreamSetSize)); + this.streamToCreatedNodeMap = HashMap.newHashMap(Math.max(16, activeStreamSetSize)); this.tupleLifecycleMap = HashMap.newHashMap(Math.max(16, activeStreamSetSize)); this.storeIndexMap = HashMap.newHashMap(Math.max(16, activeStreamSetSize / 2)); this.reversedNodeList = new ArrayList<>(activeStreamSetSize); @@ -51,6 +53,7 @@ public void addNode(AbstractNode node, Stream_ creator, @Nullable Stream_ parent reversedNodeList.add(node); node.addLocationSet(creator.getLocationSet()); nodeCreatorMap.put(node, creator); + streamToCreatedNodeMap.put(creator, node); if (!(node instanceof AbstractRootNode)) { if (parent == null) { throw new IllegalStateException("Impossible state: The node (%s) has no parent.".formatted(node)); @@ -63,6 +66,7 @@ public void addNode(AbstractNode node, Stream_ creator, Stream_ leftParent, Stre reversedNodeList.add(node); node.addLocationSet(creator.getLocationSet()); nodeCreatorMap.put(node, creator); + streamToCreatedNodeMap.put(creator, node); putInsertUpdateRetract(leftParent, TupleLifecycle.ofLeft((LeftTupleLifecycle) node)); putInsertUpdateRetract(rightParent, TupleLifecycle.ofRight((RightTupleLifecycle) node)); } @@ -143,6 +147,24 @@ public Stream_ getNodeCreatingStream(AbstractNode node) { return nodeCreatorMap.get(node); } + public List getParentNodeList(Stream_ stream) { + var out = new ArrayList(); + while (stream != null) { + var node = streamToCreatedNodeMap.get(stream); + if (node != null) { + out.add(node); + } + if (stream instanceof BavetStreamBinaryOperation binaryOperation) { + out.addAll(getParentNodeList((Stream_) binaryOperation.getLeftParent())); + out.addAll(getParentNodeList((Stream_) binaryOperation.getRightParent())); + return out; + } else { + stream = stream.getParent(); + } + } + return out; + } + public AbstractNode findParentNode(Stream_ childNodeCreator) { if (childNodeCreator == null) { // We've recursed to the bottom without finding a parent node. throw new IllegalStateException( diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java index eb5ea426ecc..c50579c3c78 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java @@ -143,7 +143,13 @@ public final void retractRight(UniTuple rightTuple) { } rightEntry.remove(); if (!isFiltering) { - counterList.forEach(this::decrementCounterRight); + // To prevent creating a dynamic lambda on the hot path, + // only call the 2-args version when indictments are enabled + if (rightTuple.getIndictmentSource() == IndictmentSource.DISABLED) { + counterList.forEach(this::decrementCounterRightWithoutIndictment); + } else { + counterList.forEach(counter -> decrementCounterRightUpdatingIndictment(counter, rightTuple)); + } } else { clearRightTrackerList(rightTuple); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java index 9c7f685029b..5012bfcc21a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java @@ -1,5 +1,6 @@ package ai.timefold.solver.core.impl.bavet.common.tuple; +import java.util.Set; import java.util.function.Function; import ai.timefold.solver.core.api.score.stream.uni.UniConstraintStream; @@ -25,26 +26,12 @@ public sealed interface Tuple permits BiTuple, QuadTuple, TriTuple, UniTuple { - static Tuple_ copyOf(Tuple_ originalTuple) { - var universalTuple = (UniversalTuple) originalTuple; - var copiedTuple = new UniversalTuple<>(universalTuple.storeSize(), universalTuple.cardinality()); - - copiedTuple.setA(universalTuple.getA()); - copiedTuple.setB(universalTuple.getB()); - copiedTuple.setC(universalTuple.getC()); - copiedTuple.setD(universalTuple.getD()); - copiedTuple.setState(universalTuple.getState()); - copiedTuple.setIndictmentSource(universalTuple.getIndictmentSource()); - for (var i = 0; i < universalTuple.storeSize(); i++) { - copiedTuple.setStore(i, universalTuple.getStore(i)); - } - return (Tuple_) copiedTuple; - } - IndictmentSource getIndictmentSource(); void setIndictmentSource(IndictmentSource indictmentSource); + Set getIndictmentSupportForNodeId(long nodeId); + TupleState getState(); void setState(TupleState state); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java index 63cadf46844..fbeb4b1e426 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java @@ -1,5 +1,10 @@ package ai.timefold.solver.core.impl.bavet.common.tuple; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; import org.jspecify.annotations.NullMarked; @@ -23,6 +28,7 @@ final class UniversalTuple private @Nullable C c; private @Nullable D d; private IndictmentSource indictmentSource = IndictmentSource.DISABLED; + private @Nullable Map> nodeIdToIndictmentSupport; private TupleState state = TupleState.DEAD; // It's the node's job to mark a new tuple as CREATING. UniversalTuple(int storeSize, int cardinality) { @@ -116,6 +122,14 @@ public void setIndictmentSource(IndictmentSource indictmentSource) { this.indictmentSource = indictmentSource; } + @Override + public Set getIndictmentSupportForNodeId(long nodeId) { + if (nodeIdToIndictmentSupport == null) { + nodeIdToIndictmentSupport = new LinkedHashMap<>(); + } + return nodeIdToIndictmentSupport.computeIfAbsent(nodeId, ignored -> new LinkedHashSet<>()); + } + @Override public String toString() { return switch (cardinality) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java index dfac8c52ead..2c8f6a2e6b5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java @@ -98,7 +98,8 @@ public int compareTo(ConstraintMatch other) { @Override public String toString() { - return "%s/%s=%s".formatted(getConstraintRef().id(), justification, score); + return "%s/%s=%s (indicting %s)".formatted(getConstraintRef().id(), justification, score, + indictedObjects); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/ConstraintNodeBuildHelper.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/ConstraintNodeBuildHelper.java index 6af3871aaa4..df85b7d5c6c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/ConstraintNodeBuildHelper.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/ConstraintNodeBuildHelper.java @@ -135,6 +135,14 @@ public Scorer getScorer(BavetScoringConstraintStream stream) { public ConstraintStreamsBavetNodeNetwork buildNodeNetwork(List nodeList, Map, List>> declaredClassToNodeMap, Map, Scorer> constraintToScorerMap, boolean scoreDirectorDerived) { + for (var scorerEntry : constraintToScorerMap.entrySet()) { + var scorerParentNodes = getParentNodeList( + (BavetAbstractConstraintStream) scorerEntry.getKey().getScoringConstraintStream()); + var scorerNodeIds = scorerParentNodes.stream() + .mapToLong(AbstractNode::getId) + .toArray(); + scorerEntry.getValue().setNodeIds(scorerNodeIds); + } return ConstraintStreamsBavetNodeNetwork.of(nodeList, declaredClassToNodeMap, (Map) constraintToScorerMap, node -> { if (constraintProfiler == null) { return node.getPropagator(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/Scorer.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/Scorer.java index df121f529b0..45a934dd16c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/Scorer.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/common/Scorer.java @@ -78,6 +78,10 @@ public void retract(Tuple_ tuple) { } } + public void setNodeIds(long[] scorerNodeIds) { + weightedScoreImpacter.getContext().setInvolvedNodeIds(scorerNodeIds); + } + public ConstraintRef getConstraintRef() { var context = weightedScoreImpacter.getContext(); return context.getConstraint().getConstraintRef(); @@ -94,5 +98,4 @@ public String toString() { .formatted(getClass().getSimpleName(), context.getConstraint().getConstraintRef(), context.getConstraintWeight()); } - } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraint.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraint.java index 2da51dbbf06..093c962a70f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraint.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraint.java @@ -25,6 +25,7 @@ public abstract class AbstractConstraint> Score_ extractConstraintWeight(Solution_ solution) { return adjustConstraintWeight((Score_) determineConstraintWeight(solution)); 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 ed7863f6930..70f5b66b6f7 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 @@ -104,7 +104,8 @@ private void validateConstraintWeight(Constraint constraint, Score_ constraintWe public abstract WeightedScoreImpacter buildWeightedScoreImpacter(AbstractConstraint constraint); protected final ScoreImpact addConstraintMatch(Constraint constraint, - ConstraintMatchSupplier constraintMatchSupplier, ScoreImpact scoreImpact) { + ConstraintMatchSupplier constraintMatchSupplier, + ScoreImpact scoreImpact) { var constraintMatchList = getConstraintMatchList(constraint); /* * Creating a constraint match is a heavy operation which may yet be undone. 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 e0c1c287ce0..8ffa4b40ede 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 @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; import java.util.function.BiFunction; @@ -21,6 +22,7 @@ import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; import ai.timefold.solver.core.impl.score.constraint.ConstraintMatch; +import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraint; import org.jspecify.annotations.NullMarked; @@ -40,13 +42,18 @@ public interface ConstraintMatchSupplier> extends BiFunction> { - static List collectIndictments(Tuple tuple) { + @SuppressWarnings("unchecked") + static List collectIndictments(Constraint constraint, Tuple tuple) { if (tuple.getIndictmentSource() == IndictmentSource.DISABLED) { return Collections.emptyList(); } - var out = new ArrayList<>(); + var out = new LinkedHashSet<>(); tuple.getIndictmentSource().visitSources(out::add); - return out; + var abstractConstraint = (AbstractConstraint) constraint; + for (var involvedNodeId : Objects.requireNonNull(abstractConstraint.getInvolvedNodeIds())) { + out.addAll(tuple.getIndictmentSupportForNodeId(involvedNodeId)); + } + return new ArrayList<>(out); } /** @@ -66,7 +73,7 @@ static > ConstraintMatchSupplier of( return (constraint, impact) -> { try { var justification = justificationMapping.apply(tuple.getA(), impact); - return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(tuple), impact); + return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(constraint, tuple), impact); } catch (Exception e) { throw createJustificationException(constraint, e, tuple.getA()); } @@ -90,7 +97,7 @@ static > ConstraintMatchSupplier of( return (constraint, impact) -> { try { var justification = justificationMapping.apply(tuple.getA(), tuple.getB(), impact); - return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(tuple), impact); + return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(constraint, tuple), impact); } catch (Exception e) { throw createJustificationException(constraint, e, tuple.getA(), tuple.getB()); } @@ -103,7 +110,7 @@ static > ConstraintMatchSupplier o return (constraint, impact) -> { try { var justification = justificationMapping.apply(tuple.getA(), tuple.getB(), tuple.getC(), impact); - return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(tuple), impact); + return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(constraint, tuple), impact); } catch (Exception e) { throw createJustificationException(constraint, e, tuple.getA(), tuple.getB(), tuple.getC()); } @@ -116,7 +123,7 @@ static > ConstraintMatchSupplier { try { var justification = justificationMapping.apply(tuple.getA(), tuple.getB(), tuple.getC(), tuple.getD(), impact); - return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(tuple), impact); + return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(constraint, tuple), impact); } catch (Exception e) { throw createJustificationException(constraint, e, tuple.getA(), tuple.getB(), tuple.getC(), tuple.getD()); } 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 ebdd5f0b040..f38bdb980fa 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 @@ -29,6 +29,10 @@ public final Score_ getConstraintWeight() { return constraintWeight; } + public void setInvolvedNodeIds(long[] involvedNodeIds) { + constraint.setInvolvedNodeIds(involvedNodeIds); + } + protected final ScoreImpact possiblyAddConstraintMatch(ScoreImpact scoreImpact, @Nullable ConstraintMatchSupplier constraintMatchSupplier) { if (!inliner.constraintMatchPolicy.isEnabled()) { diff --git a/core/src/test/java/ai/timefold/solver/core/impl/bavet/uni/ForEachFilteredUniNodeActivityTest.java b/core/src/test/java/ai/timefold/solver/core/impl/bavet/uni/ForEachFilteredUniNodeActivityTest.java index 59b936e7a4d..8826d97609a 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/bavet/uni/ForEachFilteredUniNodeActivityTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/bavet/uni/ForEachFilteredUniNodeActivityTest.java @@ -26,7 +26,7 @@ class ForEachFilteredUniNodeActivityTest { private final Set passing = new HashSet<>(); private ForEachFilteredUniNode node(TupleLifecycle> downstream) { - return new ForEachFilteredUniNode<>(String.class, passing::contains, downstream, 1); + return new ForEachFilteredUniNode<>(String.class, passing::contains, downstream, false, 1); } @SuppressWarnings("unchecked") 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 530b4205c8d..655be6a2a03 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 @@ -69,6 +69,7 @@ protected void assertScore(InnerScoreDirector assertableMatch.score) .sum(); if (implSupport.constraintMatchPolicy().isJustificationEnabled()) { + var indictmentsEnabled = implSupport.constraintMatchPolicy().isIndictmentsEnabled(); for (var assertableMatch : assertableMatches) { var constraintMatchTotals = scoreDirector.getConstraintMatchTotalMap(); @@ -78,7 +79,8 @@ protected void assertScore(InnerScoreDirector + assertableMatch.isEqualTo(obj, indictmentsEnabled))) { fail("The assertableMatch (" + assertableMatch + ") is lacking," + " it's not in the constraintMatchSet (" + constraintMatchTotal.getConstraintMatchSet() + ")."); @@ -89,7 +91,7 @@ protected void assertScore(InnerScoreDirector assertableMatch.constraintRef.equals(constraintMatch.getConstraintRef())) - .noneMatch(assertableMatch -> assertableMatch.isEqualTo(constraintMatch))) { + .noneMatch(assertableMatch -> assertableMatch.isEqualTo(constraintMatch, indictmentsEnabled))) { fail("The constraintMatch (" + constraintMatch + ") is in excess," + " it's not in the assertableMatches (" + Arrays.toString(assertableMatches) + ")."); } @@ -127,14 +129,21 @@ protected static class AssertableMatch { private final int score; private final ConstraintRef constraintRef; private final List justificationList; + private List indictmentList; public AssertableMatch(int score, ConstraintRef constraintRef, Object... justifications) { this.justificationList = Arrays.asList(justifications); this.constraintRef = constraintRef; this.score = score; + this.indictmentList = justificationList; } - public boolean isEqualTo(ConstraintMatch constraintMatch) { + public AssertableMatch withIndictedObjects(Object... indictedObjects) { + this.indictmentList = Arrays.asList(indictedObjects); + return this; + } + + public boolean isEqualTo(ConstraintMatch constraintMatch, boolean indictmentsEnabled) { if (score != ((SimpleScore) constraintMatch.getScore()).score()) { return false; } @@ -148,19 +157,32 @@ public boolean isEqualTo(ConstraintMatch constraintMatch) { return false; } // Can't simply compare the lists, since the elements may be in different orders. The order is not relevant. - return justificationList.containsAll(actualJustificationList); + if (!justificationList.containsAll(actualJustificationList)) { + return false; + } } else { // Support for custom justification mapping. if (justificationList.size() != 1) { Assertions.fail("Expected number of justifications (" + justificationList.size() + ") does not match actual (1; " + justification + ")."); } - return justification == justificationList.getFirst(); + if (justification != justificationList.getFirst()) { + return false; + } + } + if (!indictmentsEnabled) { + return true; + } + var indictedObjects = constraintMatch.getIndictedObjects(); + if (indictedObjects.size() != indictmentList.size()) { + return false; } + return indictmentList.containsAll(indictedObjects); } @Override public String toString() { - return constraintRef + " " + justificationList + "=" + score; + return "%s %s=%d (indicting %s)".formatted(constraintRef, justificationList, score, + indictmentList); } } 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 d2edef784a4..531de75f5d4 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 @@ -32,9 +32,20 @@ public boolean supportsTestTemplate(ExtensionContext context) { @Override public Stream provideTestTemplateInvocationContexts(ExtensionContext context) { - return Stream - .of(ConstraintMatchPolicy.ENABLED, ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS, + if (ConstraintStreamPrecomputeTest.class.isAssignableFrom(context.getTestClass().get())) { + // Precompute test use + return Stream + .of(ConstraintMatchPolicy.ENABLED_WITHOUT_INDICTMENTS, + ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS, ConstraintMatchPolicy.DISABLED) + .map(ConstraintStreamTestExtension::invocationContext); + } + return Stream + .of(ConstraintMatchPolicy.ENABLED, + ConstraintMatchPolicy.ENABLED_WITHOUT_INDICTMENTS, + ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS, + ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS, + ConstraintMatchPolicy.DISABLED) .map(ConstraintStreamTestExtension::invocationContext); } 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 883b8856886..04e971e2eda 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 @@ -462,8 +462,15 @@ public void ifExists_0Joiner0Filter() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstValueGroup()), - assertMatch(valueGroup)); + assertMatch(solution.getFirstValueGroup()) + .withIndictedObjects( + solution.getFirstValueGroup(), + solution.getFirstEntityGroup(), + entityGroup), + assertMatch(valueGroup).withIndictedObjects( + valueGroup, + solution.getFirstEntityGroup(), + entityGroup)); // Incremental scoreDirector.beforeProblemFactRemoved(entityGroup); From dcc45cfaf813fcf9e2a0a180cd221eded202cc0e Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 21 Jul 2026 11:51:36 -0400 Subject: [PATCH 04/30] chore: make UniConstraintStream tests aware of indictments --- .../common/AbstractIndexedIfExistsNode.java | 3 +- .../inliner/ConstraintMatchSupplier.java | 12 +- .../common/AbstractConstraintStreamTest.java | 4 +- .../common/ConstraintStreamTestExtension.java | 12 +- .../uni/AbstractUniConstraintStreamTest.java | 618 +++++++++++------- 5 files changed, 399 insertions(+), 250 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java index b5b5312d4e2..c954d09898c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java @@ -260,7 +260,8 @@ public final void updateRight(UniTuple rightTuple) { if (rightTuple.getIndictmentSource() == IndictmentSource.DISABLED) { forEachLeftCounter(rightTuple, oldCompositeKey, this::decrementCounterRightWithoutIndictment); } else { - forEachLeftCounter(rightTuple, oldCompositeKey, counter -> decrementCounterRightUpdatingIndictment(counter, rightTuple)); + forEachLeftCounter(rightTuple, oldCompositeKey, + counter -> decrementCounterRightUpdatingIndictment(counter, rightTuple)); } } else { clearRightTrackerList(rightTuple); 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 8ffa4b40ede..6780cd24475 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 @@ -73,7 +73,8 @@ static > ConstraintMatchSupplier of( return (constraint, impact) -> { try { var justification = justificationMapping.apply(tuple.getA(), impact); - return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(constraint, tuple), impact); + return new ConstraintMatch<>(constraint.getConstraintRef(), justification, + collectIndictments(constraint, tuple), impact); } catch (Exception e) { throw createJustificationException(constraint, e, tuple.getA()); } @@ -97,7 +98,8 @@ static > ConstraintMatchSupplier of( return (constraint, impact) -> { try { var justification = justificationMapping.apply(tuple.getA(), tuple.getB(), impact); - return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(constraint, tuple), impact); + return new ConstraintMatch<>(constraint.getConstraintRef(), justification, + collectIndictments(constraint, tuple), impact); } catch (Exception e) { throw createJustificationException(constraint, e, tuple.getA(), tuple.getB()); } @@ -110,7 +112,8 @@ static > ConstraintMatchSupplier o return (constraint, impact) -> { try { var justification = justificationMapping.apply(tuple.getA(), tuple.getB(), tuple.getC(), impact); - return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(constraint, tuple), impact); + return new ConstraintMatch<>(constraint.getConstraintRef(), justification, + collectIndictments(constraint, tuple), impact); } catch (Exception e) { throw createJustificationException(constraint, e, tuple.getA(), tuple.getB(), tuple.getC()); } @@ -123,7 +126,8 @@ static > ConstraintMatchSupplier { try { var justification = justificationMapping.apply(tuple.getA(), tuple.getB(), tuple.getC(), tuple.getD(), impact); - return new ConstraintMatch<>(constraint.getConstraintRef(), justification, collectIndictments(constraint, tuple), impact); + return new ConstraintMatch<>(constraint.getConstraintRef(), justification, + collectIndictments(constraint, tuple), impact); } catch (Exception e) { throw createJustificationException(constraint, e, tuple.getA(), tuple.getB(), tuple.getC(), tuple.getD()); } 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 655be6a2a03..6487bc17450 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 @@ -79,8 +79,8 @@ protected void assertScore(InnerScoreDirector - assertableMatch.isEqualTo(obj, indictmentsEnabled))) { + if (constraintMatchTotal.getConstraintMatchSet().stream() + .noneMatch(obj -> assertableMatch.isEqualTo(obj, indictmentsEnabled))) { fail("The assertableMatch (" + assertableMatch + ") is lacking," + " it's not in the constraintMatchSet (" + constraintMatchTotal.getConstraintMatchSet() + ")."); 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 531de75f5d4..3953260c7f1 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 @@ -36,16 +36,16 @@ public Stream provideTestTemplateInvocationContex // Precompute test use return Stream .of(ConstraintMatchPolicy.ENABLED_WITHOUT_INDICTMENTS, - ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS, - ConstraintMatchPolicy.DISABLED) + ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS, + ConstraintMatchPolicy.DISABLED) .map(ConstraintStreamTestExtension::invocationContext); } return Stream .of(ConstraintMatchPolicy.ENABLED, - ConstraintMatchPolicy.ENABLED_WITHOUT_INDICTMENTS, - ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS, - ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS, - ConstraintMatchPolicy.DISABLED) + ConstraintMatchPolicy.ENABLED_WITHOUT_INDICTMENTS, + ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS, + ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS, + ConstraintMatchPolicy.DISABLED) .map(ConstraintStreamTestExtension::invocationContext); } 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 04e971e2eda..2cc786a0f12 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 @@ -255,22 +255,22 @@ public void join_1Equal() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity1), - assertMatch(entity1, entity3), - assertMatch(entity2, entity2), - assertMatch(entity3, entity1), - assertMatch(entity3, entity3)); + assertMatch(entity1, entity1).withIndictedObjects(entity1), + assertMatch(entity1, entity3).withIndictedObjects(entity1, entity3), + assertMatch(entity2, entity2).withIndictedObjects(entity2), + assertMatch(entity3, entity1).withIndictedObjects(entity3, entity1), + assertMatch(entity3, entity3).withIndictedObjects(entity3)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); entity3.setValue(value2); scoreDirector.afterVariableChanged(entity3, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity1), - assertMatch(entity2, entity2), - assertMatch(entity2, entity3), - assertMatch(entity3, entity2), - assertMatch(entity3, entity3)); + assertMatch(entity1, entity1).withIndictedObjects(entity1), + assertMatch(entity2, entity2).withIndictedObjects(entity2), + assertMatch(entity2, entity3).withIndictedObjects(entity2, entity3), + assertMatch(entity3, entity2).withIndictedObjects(entity3, entity2), + assertMatch(entity3, entity3).withIndictedObjects(entity3)); // Incremental for which the first change matches a join that doesn't survive the second change scoreDirector.beforeVariableChanged(entity1, "value"); @@ -280,11 +280,11 @@ public void join_1Equal() { entity3.setValue(value1); scoreDirector.afterVariableChanged(entity3, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity1), - assertMatch(entity2, entity2), - assertMatch(entity1, entity2), - assertMatch(entity2, entity1), - assertMatch(entity3, entity3)); + assertMatch(entity1, entity1).withIndictedObjects(entity1), + assertMatch(entity2, entity2).withIndictedObjects(entity2), + assertMatch(entity1, entity2).withIndictedObjects(entity1, entity2), + assertMatch(entity2, entity1).withIndictedObjects(entity2, entity1), + assertMatch(entity3, entity3).withIndictedObjects(entity3)); } /** @@ -365,24 +365,26 @@ public void join_2Equal() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), solution.getFirstEntity()), - assertMatch(entity1, entity1), - assertMatch(entity1, entity2), - assertMatch(entity2, entity1), - assertMatch(entity2, entity2), - assertMatch(entity3, entity3)); + assertMatch(solution.getFirstEntity(), solution.getFirstEntity()) + .withIndictedObjects(solution.getFirstEntity()), + assertMatch(entity1, entity1).withIndictedObjects(entity1), + assertMatch(entity1, entity2).withIndictedObjects(entity1, entity2), + assertMatch(entity2, entity1).withIndictedObjects(entity2, entity1), + assertMatch(entity2, entity2).withIndictedObjects(entity2), + assertMatch(entity3, entity3).withIndictedObjects(entity3)); // Incremental scoreDirector.beforeProblemPropertyChanged(entity1); entity1.setIntegerProperty(8); scoreDirector.afterProblemPropertyChanged(entity1); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), solution.getFirstEntity()), - assertMatch(entity1, entity1), - assertMatch(entity1, entity3), - assertMatch(entity2, entity2), - assertMatch(entity3, entity1), - assertMatch(entity3, entity3)); + assertMatch(solution.getFirstEntity(), solution.getFirstEntity()) + .withIndictedObjects(solution.getFirstEntity()), + assertMatch(entity1, entity1).withIndictedObjects(entity1), + assertMatch(entity1, entity3).withIndictedObjects(entity1, entity3), + assertMatch(entity2, entity2).withIndictedObjects(entity2), + assertMatch(entity3, entity1).withIndictedObjects(entity3, entity1), + assertMatch(entity3, entity3).withIndictedObjects(entity3)); } @Override @@ -412,24 +414,24 @@ public void joinAfterGroupBy() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(1L, extra1), - assertMatch(1L, extra2)); + assertMatch(1L, extra1).withIndictedObjects(entity1, entity2, extra1), + assertMatch(1L, extra2).withIndictedObjects(entity1, entity2, extra2)); // Incremental scoreDirector.beforeVariableChanged(entity2, "value"); entity2.setValue(value2); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(2L, extra1), - assertMatch(2L, extra2)); + assertMatch(2L, extra1).withIndictedObjects(entity1, entity2, extra1), + assertMatch(2L, extra2).withIndictedObjects(entity1, entity2, extra2)); // Incremental scoreDirector.beforeEntityRemoved(entity2); solution.getEntityList().remove(entity2); scoreDirector.afterEntityRemoved(entity2); assertScore(scoreDirector, - assertMatch(1L, extra1), - assertMatch(1L, extra2)); + assertMatch(1L, extra1).withIndictedObjects(entity1, entity2, extra1), + assertMatch(1L, extra2).withIndictedObjects(entity1, entity2, extra2)); } @Override @@ -477,8 +479,14 @@ public void ifExists_0Joiner0Filter() { solution.getEntityGroupList().remove(entityGroup); scoreDirector.afterProblemFactRemoved(entityGroup); assertScore(scoreDirector, - assertMatch(solution.getFirstValueGroup()), - assertMatch(valueGroup)); + assertMatch(solution.getFirstValueGroup()).withIndictedObjects( + solution.getFirstValueGroup(), + solution.getFirstEntityGroup(), + entityGroup), + assertMatch(valueGroup).withIndictedObjects( + valueGroup, + solution.getFirstEntityGroup(), + entityGroup)); } @Override @@ -503,17 +511,19 @@ public void ifExists_0Join1Filter() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity()), - assertMatch(entity1), - assertMatch(entity2)); + assertMatch(solution.getFirstEntity()).withIndictedObjects(solution.getFirstEntity(), + solution.getFirstEntityGroup()), + assertMatch(entity1).withIndictedObjects(entity1, entityGroup), + assertMatch(entity2).withIndictedObjects(entity2, solution.getFirstEntityGroup())); // Incremental scoreDirector.beforeProblemFactRemoved(entityGroup); solution.getEntityGroupList().remove(entityGroup); scoreDirector.afterProblemFactRemoved(entityGroup); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity()), - assertMatch(entity2)); + assertMatch(solution.getFirstEntity()).withIndictedObjects(solution.getFirstEntity(), + solution.getFirstEntityGroup()), + assertMatch(entity2).withIndictedObjects(entity2, solution.getFirstEntityGroup())); } @Override @@ -537,17 +547,19 @@ public void ifExists_1Join0Filter() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity()), - assertMatch(entity1), - assertMatch(entity2)); + assertMatch(solution.getFirstEntity()).withIndictedObjects(solution.getFirstEntity(), + solution.getFirstEntityGroup()), + assertMatch(entity1).withIndictedObjects(entity1, entityGroup), + assertMatch(entity2).withIndictedObjects(entity2, solution.getFirstEntityGroup())); // Incremental scoreDirector.beforeProblemFactRemoved(entityGroup); solution.getEntityGroupList().remove(entityGroup); scoreDirector.afterProblemFactRemoved(entityGroup); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity()), - assertMatch(entity2)); + assertMatch(solution.getFirstEntity()).withIndictedObjects(solution.getFirstEntity(), + solution.getFirstEntityGroup()), + assertMatch(entity2).withIndictedObjects(entity2, solution.getFirstEntityGroup())); } @Override @@ -574,15 +586,15 @@ public void ifExists_1Join1Filter() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1), - assertMatch(entity2)); + assertMatch(entity1).withIndictedObjects(entity1, entityGroup), + assertMatch(entity2).withIndictedObjects(entity2, solution.getFirstEntityGroup())); // Incremental scoreDirector.beforeProblemFactRemoved(entityGroup); solution.getEntityGroupList().remove(entityGroup); scoreDirector.afterProblemFactRemoved(entityGroup); assertScore(scoreDirector, - assertMatch(entity2)); + assertMatch(entity2).withIndictedObjects(entity2, solution.getFirstEntityGroup())); } @TestTemplate @@ -605,16 +617,16 @@ public void ifExistsOther_1Join0Filter() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity()), - assertMatch(entity2)); + assertMatch(solution.getFirstEntity()).withIndictedObjects(solution.getFirstEntity(), entity2), + assertMatch(entity2).withIndictedObjects(entity2)); // Incremental scoreDirector.beforeProblemPropertyChanged(entity2); entity2.setEntityGroup(entityGroup); scoreDirector.afterProblemPropertyChanged(entity2); assertScore(scoreDirector, - assertMatch(entity1), - assertMatch(entity2)); + assertMatch(entity1).withIndictedObjects(entity1, entity2), + assertMatch(entity2).withIndictedObjects(entity2)); } @Override @@ -641,16 +653,16 @@ public void ifExistsDoesNotIncludeUnassigned() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity()), - assertMatch(entity2)); + assertMatch(solution.getFirstEntity()).withIndictedObjects(solution.getFirstEntity(), entity2), + assertMatch(entity2).withIndictedObjects(entity2)); // Incremental scoreDirector.beforeProblemPropertyChanged(entity2); entity2.setEntityGroup(entityGroup); scoreDirector.afterProblemPropertyChanged(entity2); assertScore(scoreDirector, - assertMatch(entity1), - assertMatch(entity2)); + assertMatch(entity1).withIndictedObjects(entity1, entity2), + assertMatch(entity2).withIndictedObjects(entity2)); } @Override @@ -877,21 +889,21 @@ public void ifExistsAfterGroupBy() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(1L)); + assertMatch(1L).withIndictedObjects(entity1, entity2)); // Incremental scoreDirector.beforeVariableChanged(entity2, "value"); entity2.setValue(value2); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(2L)); + assertMatch(2L).withIndictedObjects(entity1, entity2)); // Incremental scoreDirector.beforeEntityRemoved(entity2); solution.getEntityList().remove(entity2); scoreDirector.afterEntityRemoved(entity2); assertScore(scoreDirector, - assertMatch(1L)); + assertMatch(1L).withIndictedObjects(entity1, entity2)); } @TestTemplate @@ -1043,7 +1055,7 @@ public void forEach_listVarNotAllowsUnassignedValues_noInverseVar() { // v2 is not assigned, so it should not be matched scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(v1)); + assertMatch(v1).withIndictedObjects(v1, e1)); // Incremental scoreDirector.beforeListVariableChanged(e2, "valueList", 0, 1); @@ -1052,8 +1064,8 @@ public void forEach_listVarNotAllowsUnassignedValues_noInverseVar() { scoreDirector.afterListVariableElementAssigned(e2, "valueList", v2); scoreDirector.afterListVariableChanged(e2, "valueList", 0, 1); assertScore(scoreDirector, - assertMatch(v1), - assertMatch(v2)); + assertMatch(v1).withIndictedObjects(v1, e1), + assertMatch(v2).withIndictedObjects(v2, e2)); scoreDirector.beforeListVariableChanged(e1, "valueList", 0, 0); scoreDirector.beforeListVariableElementUnassigned(e1, "valueList", v1); @@ -1061,7 +1073,7 @@ public void forEach_listVarNotAllowsUnassignedValues_noInverseVar() { scoreDirector.afterListVariableElementUnassigned(e1, "valueList", v1); scoreDirector.afterListVariableChanged(e1, "valueList", 0, 0); assertScore(scoreDirector, - assertMatch(v2)); + assertMatch(v2).withIndictedObjects(v2, e2)); } @TestTemplate @@ -1414,7 +1426,7 @@ public void groupBy_1Mapping0Collect_filtered() { .asConstraint(TEST_CONSTRAINT_ID)); scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, assertMatchWithScore(-1, entityGroup1)); + assertScore(scoreDirector, assertMatchWithScore(-1, entityGroup1).withIndictedObjects(entity1, entity2)); } @TestTemplate @@ -1439,8 +1451,16 @@ public void groupBy_1Mapping1Collect_filtered() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, entityGroup1, 2L), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), 8L)); + assertMatchWithScore(-1, entityGroup1, 2L).withIndictedObjects(entity1, entity2), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), 8L).withIndictedObjects( + solution.getFirstEntity(), + solution.getEntityList().get(1), + solution.getEntityList().get(2), + solution.getEntityList().get(3), + solution.getEntityList().get(4), + solution.getEntityList().get(5), + solution.getEntityList().get(6), + entity3)); } @TestTemplate @@ -1466,8 +1486,8 @@ public void groupBy_joinedAndFiltered() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, entityGroup1, entity1), - assertMatchWithScore(-1, entityGroup1, entity2)); + assertMatchWithScore(-1, entityGroup1, entity1).withIndictedObjects(entity1, entity2), + assertMatchWithScore(-1, entityGroup1, entity2).withIndictedObjects(entity1, entity2)); } @Override @@ -1493,8 +1513,16 @@ public void groupBy_1Mapping0Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, solution.getFirstEntityGroup()), - assertMatchWithScore(-1, entityGroup1)); + assertMatchWithScore(-1, solution.getFirstEntityGroup()).withIndictedObjects( + solution.getFirstEntity(), + solution.getEntityList().get(1), + solution.getEntityList().get(2), + solution.getEntityList().get(3), + solution.getEntityList().get(4), + solution.getEntityList().get(5), + solution.getEntityList().get(6), + entity3), + assertMatchWithScore(-1, entityGroup1).withIndictedObjects(entity1, entity2)); // Incremental Stream.of(entity1, entity2).forEach(entity -> { @@ -1502,7 +1530,15 @@ public void groupBy_1Mapping0Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); }); - assertScore(scoreDirector, assertMatchWithScore(-1, solution.getFirstEntityGroup())); + assertScore(scoreDirector, assertMatchWithScore(-1, solution.getFirstEntityGroup()).withIndictedObjects( + solution.getFirstEntity(), + solution.getEntityList().get(1), + solution.getEntityList().get(2), + solution.getEntityList().get(3), + solution.getEntityList().get(4), + solution.getEntityList().get(5), + solution.getEntityList().get(6), + entity3)); } @Override @@ -1518,10 +1554,13 @@ public void groupBy_1Mapping1Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, solution.getFirstEntityGroup(), 2L), - assertMatchWithScore(-1, solution.getEntityGroupList().get(1), 1L)); + assertMatchWithScore(-1, solution.getFirstEntityGroup(), 2L).withIndictedObjects(solution.getFirstEntity(), + solution.getEntityList().get(2)), + assertMatchWithScore(-1, solution.getEntityGroupList().get(1), 1L) + .withIndictedObjects(solution.getEntityList().get(1))); // Incremental + var originalFirstEntity = solution.getFirstEntity(); Stream.of(solution.getEntityList().get(0), solution.getEntityList().get(1)) .forEach(entity -> { scoreDirector.beforeEntityRemoved(entity); @@ -1529,7 +1568,8 @@ public void groupBy_1Mapping1Collector() { scoreDirector.afterEntityRemoved(entity); }); assertScore(scoreDirector, - assertMatchWithScore(-1, solution.getFirstEntityGroup(), 1L)); + assertMatchWithScore(-1, solution.getFirstEntityGroup(), 1L) + .withIndictedObjects(originalFirstEntity, solution.getEntityList().get(0))); } @Override @@ -1551,16 +1591,20 @@ public void groupBy_1Mapping2Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, solution.getFirstEntityGroup(), 2L, asSet(entity1, entity3)), - assertMatchWithScore(-1, solution.getEntityGroupList().get(1), 1L, Collections.singleton(entity2))); + assertMatchWithScore(-1, solution.getFirstEntityGroup(), 2L, asSet(entity1, entity3)) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(2)), + assertMatchWithScore(-1, solution.getEntityGroupList().get(1), 1L, Collections.singleton(entity2)) + .withIndictedObjects(solution.getEntityList().get(1))); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, solution.getFirstEntityGroup(), 1L, Collections.singleton(entity3)), - assertMatchWithScore(-1, solution.getEntityGroupList().get(1), 1L, Collections.singleton(entity2))); + assertMatchWithScore(-1, solution.getFirstEntityGroup(), 1L, Collections.singleton(entity3)) + .withIndictedObjects(entity1, entity3), + assertMatchWithScore(-1, solution.getEntityGroupList().get(1), 1L, Collections.singleton(entity2)) + .withIndictedObjects(entity2)); } @Override @@ -1583,16 +1627,20 @@ public void groupBy_1Mapping3Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, solution.getFirstEntityGroup(), 2L, 2, asSet(entity1, entity3)), - assertMatchWithScore(-1, solution.getEntityGroupList().get(1), 1L, 1, Collections.singleton(entity2))); + assertMatchWithScore(-1, solution.getFirstEntityGroup(), 2L, 2, asSet(entity1, entity3)) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(2)), + assertMatchWithScore(-1, solution.getEntityGroupList().get(1), 1L, 1, Collections.singleton(entity2)) + .withIndictedObjects(solution.getEntityList().get(1))); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, solution.getFirstEntityGroup(), 1L, 1, Collections.singleton(entity3)), - assertMatchWithScore(-1, solution.getEntityGroupList().get(1), 1L, 1, Collections.singleton(entity2))); + assertMatchWithScore(-1, solution.getFirstEntityGroup(), 1L, 1, Collections.singleton(entity3)) + .withIndictedObjects(entity1, entity3), + assertMatchWithScore(-1, solution.getEntityGroupList().get(1), 1L, 1, Collections.singleton(entity2)) + .withIndictedObjects(entity2)); } @Override @@ -1617,13 +1665,29 @@ public void groupBy_0Mapping1Collector() { // From scratch scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, assertMatchWithScore(-10, 10L)); + assertScore(scoreDirector, assertMatchWithScore(-10, 10L).withIndictedObjects( + solution.getFirstEntity(), + solution.getEntityList().get(1), + solution.getEntityList().get(2), + solution.getEntityList().get(3), + solution.getEntityList().get(4), + solution.getEntityList().get(5), + solution.getEntityList().get(6), + entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity3); solution.getEntityList().remove(entity3); scoreDirector.afterEntityRemoved(entity3); - assertScore(scoreDirector, assertMatchWithScore(-9, 9L)); + assertScore(scoreDirector, assertMatchWithScore(-9, 9L).withIndictedObjects( + solution.getFirstEntity(), + solution.getEntityList().get(1), + solution.getEntityList().get(2), + solution.getEntityList().get(3), + solution.getEntityList().get(4), + solution.getEntityList().get(5), + solution.getEntityList().get(6), + entity1, entity2, entity3)); } @Override @@ -1641,13 +1705,15 @@ public void groupBy_0Mapping2Collector() { // From scratch scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, assertMatchWithScore(-1, 3L, 3)); + assertScore(scoreDirector, assertMatchWithScore(-1, 3L, 3).withIndictedObjects(solution.getFirstEntity(), + solution.getEntityList().get(1), solution.getEntityList().get(2))); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); - assertScore(scoreDirector, assertMatchWithScore(-1, 2L, 2)); + assertScore(scoreDirector, assertMatchWithScore(-1, 2L, 2).withIndictedObjects(entity1, solution.getEntityList().get(0), + solution.getEntityList().get(1))); } @Override @@ -1672,14 +1738,16 @@ public void groupBy_0Mapping3Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, 3L, 0, 2)); + assertMatchWithScore(-1, 3L, 0, 2).withIndictedObjects(solution.getFirstEntity(), + solution.getEntityList().get(1), solution.getEntityList().get(2))); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, 2L, 1, 2)); + assertMatchWithScore(-1, 2L, 1, 2).withIndictedObjects(entity1, solution.getEntityList().get(0), + solution.getEntityList().get(1))); } @Override @@ -1705,14 +1773,17 @@ public void groupBy_0Mapping4Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, 3L, 0, 2, asSet(entity1, entity2, entity3))); + assertMatchWithScore(-1, 3L, 0, 2, asSet(entity1, entity2, entity3)).withIndictedObjects( + solution.getFirstEntity(), solution.getEntityList().get(1), solution.getEntityList().get(2))); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, 2L, 1, 2, asSet(entity2, entity3))); + assertMatchWithScore(-1, 2L, 1, 2, asSet(entity2, entity3)).withIndictedObjects(entity1, + solution.getEntityList().get(0), + solution.getEntityList().get(1))); } @TestTemplate @@ -1740,17 +1811,33 @@ public void groupBy_1Mapping1Collector_groupingOnPrimitives() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-8, 1, 8L), - assertMatchWithScore(-1, 2, 1L), - assertMatchWithScore(-1, 3, 1L)); + assertMatchWithScore(-8, 1, 8L).withIndictedObjects( + solution.getFirstEntity(), + solution.getEntityList().get(1), + solution.getEntityList().get(2), + solution.getEntityList().get(3), + solution.getEntityList().get(4), + solution.getEntityList().get(5), + solution.getEntityList().get(6), + entity1), + assertMatchWithScore(-1, 2, 1L).withIndictedObjects(entity2), + assertMatchWithScore(-1, 3, 1L).withIndictedObjects(entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity3); solution.getEntityList().remove(entity3); scoreDirector.afterEntityRemoved(entity3); assertScore(scoreDirector, - assertMatchWithScore(-8, 1, 8L), - assertMatchWithScore(-1, 2, 1L)); + assertMatchWithScore(-8, 1, 8L).withIndictedObjects( + solution.getFirstEntity(), + solution.getEntityList().get(1), + solution.getEntityList().get(2), + solution.getEntityList().get(3), + solution.getEntityList().get(4), + solution.getEntityList().get(5), + solution.getEntityList().get(6), + entity1), + assertMatchWithScore(-1, 2, 1L).withIndictedObjects(entity2)); } @Override @@ -1776,37 +1863,52 @@ public void groupBy_2Mapping0Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, entityGroup1, solution.getFirstValue()), - assertMatchWithScore(-1, entityGroup1, secondValue), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(0)), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(1)), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(2)), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(3)), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(4))); + assertMatchWithScore(-1, entityGroup1, solution.getFirstValue()).withIndictedObjects(entity1, entity2), + assertMatchWithScore(-1, entityGroup1, secondValue).withIndictedObjects(entity3), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(0)) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(5)), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(1)) + .withIndictedObjects(solution.getEntityList().get(1), solution.getEntityList().get(6)), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(2)) + .withIndictedObjects(solution.getEntityList().get(2)), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(3)) + .withIndictedObjects(solution.getEntityList().get(3)), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(4)) + .withIndictedObjects(solution.getEntityList().get(4))); // Incremental scoreDirector.beforeEntityRemoved(entity3); solution.getEntityList().remove(entity3); scoreDirector.afterEntityRemoved(entity3); assertScore(scoreDirector, - assertMatchWithScore(-1, entityGroup1, solution.getFirstValue()), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(0)), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(1)), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(2)), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(3)), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(4))); + assertMatchWithScore(-1, entityGroup1, solution.getFirstValue()).withIndictedObjects(entity1, entity2), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(0)) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(5)), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(1)) + .withIndictedObjects(solution.getEntityList().get(1), solution.getEntityList().get(6)), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(2)) + .withIndictedObjects(solution.getEntityList().get(2)), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(3)) + .withIndictedObjects(solution.getEntityList().get(3)), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(4)) + .withIndictedObjects(solution.getEntityList().get(4))); // Ensure that the first match is still there when entity2, as it still has entity1 scoreDirector.beforeEntityRemoved(entity2); solution.getEntityList().remove(entity2); scoreDirector.afterEntityRemoved(entity2); assertScore(scoreDirector, - assertMatchWithScore(-1, entityGroup1, solution.getFirstValue()), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(0)), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(1)), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(2)), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(3)), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(4))); + assertMatchWithScore(-1, entityGroup1, solution.getFirstValue()).withIndictedObjects(entity1, entity2), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(0)) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(5)), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(1)) + .withIndictedObjects(solution.getEntityList().get(1), solution.getEntityList().get(6)), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(2)) + .withIndictedObjects(solution.getEntityList().get(2)), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(3)) + .withIndictedObjects(solution.getEntityList().get(3)), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(4)) + .withIndictedObjects(solution.getEntityList().get(4))); } @Override @@ -1833,17 +1935,32 @@ public void groupBy_2Mapping1Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-7, solution.getFirstEntityGroup(), solution.getFirstValue(), 7L), - assertMatchWithScore(-2, entityGroup1, value1, 2L), - assertMatchWithScore(-1, entityGroup1, solution.getFirstValue(), 1L)); + assertMatchWithScore(-7, solution.getFirstEntityGroup(), solution.getFirstValue(), 7L).withIndictedObjects( + solution.getFirstEntity(), + solution.getEntityList().get(1), + solution.getEntityList().get(2), + solution.getEntityList().get(3), + solution.getEntityList().get(4), + solution.getEntityList().get(5), + solution.getEntityList().get(6)), + assertMatchWithScore(-2, entityGroup1, value1, 2L).withIndictedObjects(entity1, entity3), + assertMatchWithScore(-1, entityGroup1, solution.getFirstValue(), 1L).withIndictedObjects(entity2)); // Incremental scoreDirector.beforeProblemPropertyChanged(entity2); entity2.setEntityGroup(solution.getFirstEntityGroup()); scoreDirector.afterProblemPropertyChanged(entity2); assertScore(scoreDirector, - assertMatchWithScore(-8, solution.getFirstEntityGroup(), solution.getFirstValue(), 8L), - assertMatchWithScore(-2, entityGroup1, value1, 2L)); + assertMatchWithScore(-8, solution.getFirstEntityGroup(), solution.getFirstValue(), 8L).withIndictedObjects( + solution.getFirstEntity(), + solution.getEntityList().get(1), + solution.getEntityList().get(2), + solution.getEntityList().get(3), + solution.getEntityList().get(4), + solution.getEntityList().get(5), + solution.getEntityList().get(6), + entity2), + assertMatchWithScore(-2, entityGroup1, value1, 2L).withIndictedObjects(entity1, entity3)); } @Override @@ -1870,17 +1987,32 @@ public void groupBy_2Mapping2Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-7, solution.getFirstEntityGroup(), solution.getFirstValue(), 7L, 7L), - assertMatchWithScore(-2, entityGroup1, value1, 2L, 2L), - assertMatchWithScore(-1, entityGroup1, solution.getFirstValue(), 1L, 1L)); + assertMatchWithScore(-7, solution.getFirstEntityGroup(), solution.getFirstValue(), 7L, 7L).withIndictedObjects( + solution.getFirstEntity(), + solution.getEntityList().get(1), + solution.getEntityList().get(2), + solution.getEntityList().get(3), + solution.getEntityList().get(4), + solution.getEntityList().get(5), + solution.getEntityList().get(6)), + assertMatchWithScore(-2, entityGroup1, value1, 2L, 2L).withIndictedObjects(entity1, entity3), + assertMatchWithScore(-1, entityGroup1, solution.getFirstValue(), 1L, 1L).withIndictedObjects(entity2)); // Incremental scoreDirector.beforeProblemPropertyChanged(entity2); entity2.setEntityGroup(solution.getFirstEntityGroup()); scoreDirector.afterProblemPropertyChanged(entity2); assertScore(scoreDirector, - assertMatchWithScore(-8, solution.getFirstEntityGroup(), solution.getFirstValue(), 8L, 8L), - assertMatchWithScore(-2, entityGroup1, value1, 2L, 2L)); + assertMatchWithScore(-8, solution.getFirstEntityGroup(), solution.getFirstValue(), 8L, 8L).withIndictedObjects( + solution.getFirstEntity(), + solution.getEntityList().get(1), + solution.getEntityList().get(2), + solution.getEntityList().get(3), + solution.getEntityList().get(4), + solution.getEntityList().get(5), + solution.getEntityList().get(6), + entity2), + assertMatchWithScore(-2, entityGroup1, value1, 2L, 2L).withIndictedObjects(entity1, entity3)); } @Override @@ -1909,21 +2041,21 @@ public void groupBy_3Mapping0Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group1, value1, entity1.getCode()), - assertMatchWithScore(-1, group2, value2, entity2.getCode()), - assertMatchWithScore(-1, group1, value3, entity3.getCode()), - assertMatchWithScore(-1, group2, value1, entity4.getCode()), - assertMatchWithScore(-1, group1, value2, entity5.getCode())); + assertMatchWithScore(-1, group1, value1, entity1.getCode()).withIndictedObjects(entity1), + assertMatchWithScore(-1, group2, value2, entity2.getCode()).withIndictedObjects(entity2), + assertMatchWithScore(-1, group1, value3, entity3.getCode()).withIndictedObjects(entity3), + assertMatchWithScore(-1, group2, value1, entity4.getCode()).withIndictedObjects(entity4), + assertMatchWithScore(-1, group1, value2, entity5.getCode()).withIndictedObjects(entity5)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, value2, entity2.getCode()), - assertMatchWithScore(-1, group1, value3, entity3.getCode()), - assertMatchWithScore(-1, group2, value1, entity4.getCode()), - assertMatchWithScore(-1, group1, value2, entity5.getCode())); + assertMatchWithScore(-1, group2, value2, entity2.getCode()).withIndictedObjects(entity2), + assertMatchWithScore(-1, group1, value3, entity3.getCode()).withIndictedObjects(entity3), + assertMatchWithScore(-1, group2, value1, entity4.getCode()).withIndictedObjects(entity4), + assertMatchWithScore(-1, group1, value2, entity5.getCode()).withIndictedObjects(entity5)); } @Override @@ -1952,21 +2084,30 @@ public void groupBy_3Mapping1Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group1, value1, entity1.getCode(), Collections.singleton(entity1)), - assertMatchWithScore(-1, group2, value2, entity2.getCode(), Collections.singleton(entity2)), - assertMatchWithScore(-1, group1, value3, entity3.getCode(), Collections.singleton(entity3)), - assertMatchWithScore(-1, group2, value1, entity4.getCode(), Collections.singleton(entity4)), - assertMatchWithScore(-1, group1, value2, entity5.getCode(), Collections.singleton(entity5))); + assertMatchWithScore(-1, group1, value1, entity1.getCode(), Collections.singleton(entity1)) + .withIndictedObjects(entity1), + assertMatchWithScore(-1, group2, value2, entity2.getCode(), Collections.singleton(entity2)) + .withIndictedObjects(entity2), + assertMatchWithScore(-1, group1, value3, entity3.getCode(), Collections.singleton(entity3)) + .withIndictedObjects(entity3), + assertMatchWithScore(-1, group2, value1, entity4.getCode(), Collections.singleton(entity4)) + .withIndictedObjects(entity4), + assertMatchWithScore(-1, group1, value2, entity5.getCode(), Collections.singleton(entity5)) + .withIndictedObjects(entity5)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, value2, entity2.getCode(), Collections.singleton(entity2)), - assertMatchWithScore(-1, group1, value3, entity3.getCode(), Collections.singleton(entity3)), - assertMatchWithScore(-1, group2, value1, entity4.getCode(), Collections.singleton(entity4)), - assertMatchWithScore(-1, group1, value2, entity5.getCode(), Collections.singleton(entity5))); + assertMatchWithScore(-1, group2, value2, entity2.getCode(), Collections.singleton(entity2)) + .withIndictedObjects(entity2), + assertMatchWithScore(-1, group1, value3, entity3.getCode(), Collections.singleton(entity3)) + .withIndictedObjects(entity3), + assertMatchWithScore(-1, group2, value1, entity4.getCode(), Collections.singleton(entity4)) + .withIndictedObjects(entity4), + assertMatchWithScore(-1, group1, value2, entity5.getCode(), Collections.singleton(entity5)) + .withIndictedObjects(entity5)); } @Override @@ -1995,21 +2136,21 @@ public void groupBy_4Mapping0Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, entity1, group1, value1, entity1.getCode()), - assertMatchWithScore(-1, entity2, group2, value2, entity2.getCode()), - assertMatchWithScore(-1, entity3, group1, value3, entity3.getCode()), - assertMatchWithScore(-1, entity4, group2, value1, entity4.getCode()), - assertMatchWithScore(-1, entity5, group1, value2, entity5.getCode())); + assertMatchWithScore(-1, entity1, group1, value1, entity1.getCode()).withIndictedObjects(entity1), + assertMatchWithScore(-1, entity2, group2, value2, entity2.getCode()).withIndictedObjects(entity2), + assertMatchWithScore(-1, entity3, group1, value3, entity3.getCode()).withIndictedObjects(entity3), + assertMatchWithScore(-1, entity4, group2, value1, entity4.getCode()).withIndictedObjects(entity4), + assertMatchWithScore(-1, entity5, group1, value2, entity5.getCode()).withIndictedObjects(entity5)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, entity2, group2, value2, entity2.getCode()), - assertMatchWithScore(-1, entity3, group1, value3, entity3.getCode()), - assertMatchWithScore(-1, entity4, group2, value1, entity4.getCode()), - assertMatchWithScore(-1, entity5, group1, value2, entity5.getCode())); + assertMatchWithScore(-1, entity2, group2, value2, entity2.getCode()).withIndictedObjects(entity2), + assertMatchWithScore(-1, entity3, group1, value3, entity3.getCode()).withIndictedObjects(entity3), + assertMatchWithScore(-1, entity4, group2, value1, entity4.getCode()).withIndictedObjects(entity4), + assertMatchWithScore(-1, entity5, group1, value2, entity5.getCode()).withIndictedObjects(entity5)); } @Override @@ -2047,8 +2188,8 @@ public void mapToUniWithDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group), - assertMatch(group)); + assertMatch(group).withIndictedObjects(solution.getFirstEntity()), + assertMatch(group).withIndictedObjects(solution.getEntityList().get(1))); var entity = solution.getFirstEntity(); @@ -2057,7 +2198,7 @@ public void mapToUniWithDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group)); + assertMatch(group).withIndictedObjects(solution.getEntityList().get(0))); } @Override @@ -2076,8 +2217,8 @@ public void mapToUniWithoutDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1), - assertMatch(group2)); + assertMatch(group1).withIndictedObjects(solution.getFirstEntity()), + assertMatch(group2).withIndictedObjects(solution.getEntityList().get(1))); var entity = solution.getFirstEntity(); @@ -2086,7 +2227,7 @@ public void mapToUniWithoutDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group2)); + assertMatch(group2).withIndictedObjects(solution.getEntityList().get(0))); } @Override @@ -2105,7 +2246,7 @@ public void mapToUniAndDistinctWithDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group)); + assertMatch(group).withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(1))); var entity = solution.getFirstEntity(); @@ -2114,7 +2255,7 @@ public void mapToUniAndDistinctWithDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group)); + assertMatch(group).withIndictedObjects(entity, solution.getEntityList().get(0))); } @Override @@ -2134,8 +2275,8 @@ public void mapToUniAndDistinctWithoutDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1), - assertMatch(group2)); + assertMatch(group1).withIndictedObjects(solution.getFirstEntity()), + assertMatch(group2).withIndictedObjects(solution.getEntityList().get(1))); var entity = solution.getFirstEntity(); @@ -2144,7 +2285,7 @@ public void mapToUniAndDistinctWithoutDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group2)); + assertMatch(group2).withIndictedObjects(solution.getEntityList().get(0))); } @Override @@ -2166,8 +2307,8 @@ public void mapToBi() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1, value1), - assertMatch(group2, value2)); + assertMatch(group1, value1).withIndictedObjects(solution.getFirstEntity()), + assertMatch(group2, value2).withIndictedObjects(solution.getEntityList().get(1))); var entity = solution.getFirstEntity(); @@ -2176,7 +2317,7 @@ public void mapToBi() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group2, value2)); + assertMatch(group2, value2).withIndictedObjects(solution.getEntityList().get(0))); } @Override @@ -2201,8 +2342,8 @@ public void mapToTri() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1, value1, code1), - assertMatch(group2, value2, code2)); + assertMatch(group1, value1, code1).withIndictedObjects(solution.getFirstEntity()), + assertMatch(group2, value2, code2).withIndictedObjects(solution.getEntityList().get(1))); var entity = solution.getFirstEntity(); @@ -2211,7 +2352,7 @@ public void mapToTri() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group2, value2, code2)); + assertMatch(group2, value2, code2).withIndictedObjects(solution.getEntityList().get(0))); } @Override @@ -2239,8 +2380,8 @@ public void mapToQuad() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1, value1, code1, property1), - assertMatch(group2, value2, code2, property2)); + assertMatch(group1, value1, code1, property1).withIndictedObjects(solution.getFirstEntity()), + assertMatch(group2, value2, code2, property2).withIndictedObjects(solution.getEntityList().get(1))); var entity = solution.getFirstEntity(); @@ -2249,7 +2390,7 @@ public void mapToQuad() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group2, value2, code2, property2)); + assertMatch(group2, value2, code2, property2).withIndictedObjects(solution.getEntityList().get(0))); } @Override @@ -2268,8 +2409,8 @@ public void expandToBi() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), group1), - assertMatch(solution.getEntityList().get(1), group2)); + assertMatch(solution.getFirstEntity(), group1).withIndictedObjects(solution.getFirstEntity()), + assertMatch(solution.getEntityList().get(1), group2).withIndictedObjects(solution.getEntityList().get(1))); var entity = solution.getFirstEntity(); @@ -2278,7 +2419,7 @@ public void expandToBi() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), group2)); + assertMatch(solution.getFirstEntity(), group2).withIndictedObjects(solution.getEntityList().get(0))); } @Override @@ -2299,8 +2440,9 @@ public void expandToTri() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), group1, value1), - assertMatch(solution.getEntityList().get(1), group2, value2)); + assertMatch(solution.getFirstEntity(), group1, value1).withIndictedObjects(solution.getFirstEntity()), + assertMatch(solution.getEntityList().get(1), group2, value2) + .withIndictedObjects(solution.getEntityList().get(1))); var entity = solution.getFirstEntity(); @@ -2309,7 +2451,7 @@ public void expandToTri() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), group2, value2)); + assertMatch(solution.getFirstEntity(), group2, value2).withIndictedObjects(solution.getEntityList().get(0))); } @Override @@ -2333,8 +2475,9 @@ public void expandToQuad() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), group1, value1, code1), - assertMatch(solution.getEntityList().get(1), group2, value2, code2)); + assertMatch(solution.getFirstEntity(), group1, value1, code1).withIndictedObjects(solution.getFirstEntity()), + assertMatch(solution.getEntityList().get(1), group2, value2, code2) + .withIndictedObjects(solution.getEntityList().get(1))); var entity = solution.getFirstEntity(); @@ -2343,7 +2486,8 @@ public void expandToQuad() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), group2, value2, code2)); + assertMatch(solution.getFirstEntity(), group2, value2, code2) + .withIndictedObjects(solution.getEntityList().get(0))); } @Override @@ -2369,11 +2513,11 @@ public void flattenLastWithDuplicates() { // group1 is used instead of group1Dup because it equals to it scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1), - assertMatch(group1), - assertMatch(group2), - assertMatch(group1), - assertMatch(group2)); + assertMatch(group1).withIndictedObjects(solution.getFirstEntity()), + assertMatch(group1).withIndictedObjects(solution.getFirstEntity()), + assertMatch(group2).withIndictedObjects(solution.getFirstEntity()), + assertMatch(group1).withIndictedObjects(solution.getEntityList().get(1)), + assertMatch(group2).withIndictedObjects(solution.getEntityList().get(1))); var entity = solution.getFirstEntity(); @@ -2383,17 +2527,17 @@ public void flattenLastWithDuplicates() { scoreDirector.afterVariableChanged(entity, "value"); assertScore(scoreDirector, - assertMatch(group1), - assertMatch(group2), - assertMatch(group1), - assertMatch(group2)); + assertMatch(group1).withIndictedObjects(solution.getFirstEntity()), + assertMatch(group2).withIndictedObjects(solution.getFirstEntity()), + assertMatch(group1).withIndictedObjects(solution.getEntityList().get(1)), + assertMatch(group2).withIndictedObjects(solution.getEntityList().get(1))); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group1), - assertMatch(group2)); + assertMatch(group1).withIndictedObjects(solution.getEntityList().get(0)), + assertMatch(group2).withIndictedObjects(solution.getEntityList().get(0))); } @Override @@ -2412,8 +2556,8 @@ public void flattenLastWithoutDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1), - assertMatch(group2)); + assertMatch(group1).withIndictedObjects(solution.getFirstEntity()), + assertMatch(group2).withIndictedObjects(solution.getEntityList().get(1))); var entity = solution.getFirstEntity(); @@ -2422,7 +2566,7 @@ public void flattenLastWithoutDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group2)); + assertMatch(group2).withIndictedObjects(solution.getEntityList().get(0))); } @Override @@ -2444,18 +2588,18 @@ public void flatten() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, group1), - assertMatch(entity1, group2), - assertMatch(entity2, group1), - assertMatch(entity2, group2)); + assertMatch(entity1, group1).withIndictedObjects(entity1), + assertMatch(entity1, group2).withIndictedObjects(entity1), + assertMatch(entity2, group1).withIndictedObjects(entity2), + assertMatch(entity2, group2).withIndictedObjects(entity2)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatch(entity2, group1), - assertMatch(entity2, group2)); + assertMatch(entity2, group1).withIndictedObjects(entity2), + assertMatch(entity2, group2).withIndictedObjects(entity2)); } @Override @@ -2475,8 +2619,8 @@ public void flattenLastAndDistinctWithDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1), - assertMatch(group2)); + assertMatch(group1).withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(1)), + assertMatch(group2).withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(1))); var entity = solution.getFirstEntity(); @@ -2485,8 +2629,8 @@ public void flattenLastAndDistinctWithDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group1), - assertMatch(group2)); + assertMatch(group1).withIndictedObjects(entity, solution.getEntityList().get(0)), + assertMatch(group2).withIndictedObjects(entity, solution.getEntityList().get(0))); } @Override @@ -2506,8 +2650,8 @@ public void flattenLastAndDistinctWithoutDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1), - assertMatch(group2)); + assertMatch(group1).withIndictedObjects(solution.getFirstEntity()), + assertMatch(group2).withIndictedObjects(solution.getEntityList().get(1))); var entity = solution.getFirstEntity(); @@ -2516,7 +2660,7 @@ public void flattenLastAndDistinctWithoutDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group2)); + assertMatch(group2).withIndictedObjects(solution.getEntityList().get(0))); } @Override @@ -2725,8 +2869,8 @@ public void concatBiWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, null), - assertMatch(entity2, entity3)); + assertMatch(entity1, null).withIndictedObjects(entity1), + assertMatch(entity2, entity3).withIndictedObjects(entity2, entity3)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2737,8 +2881,8 @@ public void concatBiWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, null), - assertMatch(entity3, entity2)); + assertMatch(entity1, null).withIndictedObjects(entity1), + assertMatch(entity3, entity2).withIndictedObjects(entity3, entity2)); } @Override @@ -2772,8 +2916,8 @@ public void concatAndDistinctBiWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, null), - assertMatch(entity2, entity3)); + assertMatch(entity1, null).withIndictedObjects(entity1), + assertMatch(entity2, entity3).withIndictedObjects(entity2, entity3)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2784,8 +2928,8 @@ public void concatAndDistinctBiWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, null), - assertMatch(entity3, entity2)); + assertMatch(entity1, null).withIndictedObjects(entity1), + assertMatch(entity3, entity2).withIndictedObjects(entity3, entity2)); } @Override @@ -2820,8 +2964,8 @@ public void concatTriWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, null, null), - assertMatch(entity2, entity3, entity1)); + assertMatch(entity1, null, null).withIndictedObjects(entity1), + assertMatch(entity2, entity3, entity1).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2832,8 +2976,8 @@ public void concatTriWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, null, null), - assertMatch(entity3, entity2, entity1)); + assertMatch(entity1, null, null).withIndictedObjects(entity1), + assertMatch(entity3, entity2, entity1).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -2869,8 +3013,8 @@ public void concatAndDistinctTriWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, null, null), - assertMatch(entity2, entity3, entity1)); + assertMatch(entity1, null, null).withIndictedObjects(entity1), + assertMatch(entity2, entity3, entity1).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2881,8 +3025,8 @@ public void concatAndDistinctTriWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, null, null), - assertMatch(entity3, entity2, entity1)); + assertMatch(entity1, null, null).withIndictedObjects(entity1), + assertMatch(entity3, entity2, entity1).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -2919,8 +3063,8 @@ public void concatQuadWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, null, null, null), - assertMatch(entity2, entity3, entity1, entity2)); + assertMatch(entity1, null, null, null).withIndictedObjects(entity1), + assertMatch(entity2, entity3, entity1, entity2).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2931,8 +3075,8 @@ public void concatQuadWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, null, null, null), - assertMatch(entity3, entity2, entity1, entity3)); + assertMatch(entity1, null, null, null).withIndictedObjects(entity1), + assertMatch(entity3, entity2, entity1, entity3).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -2970,8 +3114,8 @@ public void concatAndDistinctQuadWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, null, null, null), - assertMatch(entity2, entity3, entity1, entity2)); + assertMatch(entity1, null, null, null).withIndictedObjects(entity1), + assertMatch(entity2, entity3, entity1, entity2).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2982,8 +3126,8 @@ public void concatAndDistinctQuadWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, null, null, null), - assertMatch(entity3, entity2, entity1, entity3)); + assertMatch(entity1, null, null, null).withIndictedObjects(entity1), + assertMatch(entity3, entity2, entity1, entity3).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -3016,16 +3160,16 @@ public void concatAfterGroupBy() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, value1, 1L), - assertMatchWithScore(-1, value2, 1L)); + assertMatchWithScore(-1, value1, 1L).withIndictedObjects(entity1), + assertMatchWithScore(-1, value2, 1L).withIndictedObjects(entity2)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); entity3.setValue(value2); scoreDirector.afterVariableChanged(entity3, "value"); assertScore(scoreDirector, - assertMatchWithScore(-1, value1, 1L), - assertMatchWithScore(-2, value2, 2L)); + assertMatchWithScore(-1, value1, 1L).withIndictedObjects(entity1), + assertMatchWithScore(-2, value2, 2L).withIndictedObjects(entity2, entity3)); // Incremental for which the first change matches a join that doesn't survive the second change scoreDirector.beforeVariableChanged(entity1, "value"); @@ -3035,8 +3179,8 @@ public void concatAfterGroupBy() { entity3.setValue(value1); scoreDirector.afterVariableChanged(entity3, "value"); assertScore(scoreDirector, - assertMatchWithScore(-1, value1, 1L), - assertMatchWithScore(-1, value2, 1L)); + assertMatchWithScore(-1, value1, 1L).withIndictedObjects(entity3), + assertMatchWithScore(-1, value2, 1L).withIndictedObjects(entity2, entity3)); } @Override From 271cdf1a0faf9e557b9de7ad39a7096acad25e9b Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 21 Jul 2026 12:10:41 -0400 Subject: [PATCH 05/30] fix: remove old indicted objects from original group in GroupNode --- .../impl/bavet/common/AbstractGroupNode.java | 4 ++ .../tuple/indictment/IndictmentSource.java | 14 ++++ .../uni/AbstractUniConstraintStreamTest.java | 68 +++++++++---------- 3 files changed, 52 insertions(+), 34 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractGroupNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractGroupNode.java index b438715e613..2c27066c23f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractGroupNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractGroupNode.java @@ -192,6 +192,8 @@ public final void update(InTuple_ tuple) { if (sameKey) { updateGroup(tuple, oldGroup); } else { + var oldOutTuple = oldGroup.getTuple(); + oldOutTuple.setIndictmentSource(IndictmentSource.removeFromAggregate(tuple, oldOutTuple)); if (hasCollector) { groupRetract(tuple); } @@ -276,6 +278,8 @@ public final void retract(InTuple_ tuple) { // No fail fast if null because we don't track which tuples made it through the filter predicate(s) return; } + var oldOutTuple = group.getTuple(); + oldOutTuple.setIndictmentSource(IndictmentSource.removeFromAggregate(tuple, oldOutTuple)); if (hasCollector) { groupRetract(tuple); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java index 65cc64d2adc..8340f1c23d9 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java @@ -36,6 +36,20 @@ static IndictmentSource aggregating(Tuple elementTuple, Tuple groupTuple) { } } + static IndictmentSource removeFromAggregate(Tuple elementTuple, Tuple groupTuple) { + if (elementTuple.getIndictmentSource() == DISABLED) { + return DISABLED; + } + if (groupTuple.getIndictmentSource() instanceof AggregateIndictmentSource aggregateIndictmentSource) { + aggregateIndictmentSource.sourceList.remove(elementTuple.getIndictmentSource()); + return aggregateIndictmentSource; + } else { + var collection = new ArrayList(); + collection.add(elementTuple.getIndictmentSource()); + return new AggregateIndictmentSource(collection); + } + } + static IndictmentSource sourceWithSupport(Tuple carry, Tuple support) { if (carry.getIndictmentSource() == DISABLED) { return DISABLED; 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 2cc786a0f12..8ae79d3d862 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 @@ -163,7 +163,7 @@ public void filter_entity() { @TestTemplate public void filter_consecutive() { var solution = TestdataLavishSolution.generateSolution(4, 4); - var entity1 = solution.getEntityList().get(0); + var entity1 = solution.getEntityList().getFirst(); var entity2 = solution.getEntityList().get(1); var entity3 = solution.getEntityList().get(2); var entity4 = solution.getEntityList().get(3); @@ -430,8 +430,8 @@ public void joinAfterGroupBy() { solution.getEntityList().remove(entity2); scoreDirector.afterEntityRemoved(entity2); assertScore(scoreDirector, - assertMatch(1L, extra1).withIndictedObjects(entity1, entity2, extra1), - assertMatch(1L, extra2).withIndictedObjects(entity1, entity2, extra2)); + assertMatch(1L, extra1).withIndictedObjects(entity1, extra1), + assertMatch(1L, extra2).withIndictedObjects(entity1, extra2)); } @Override @@ -903,7 +903,7 @@ public void ifExistsAfterGroupBy() { solution.getEntityList().remove(entity2); scoreDirector.afterEntityRemoved(entity2); assertScore(scoreDirector, - assertMatch(1L).withIndictedObjects(entity1, entity2)); + assertMatch(1L).withIndictedObjects(entity1)); } @TestTemplate @@ -1561,7 +1561,7 @@ public void groupBy_1Mapping1Collector() { // Incremental var originalFirstEntity = solution.getFirstEntity(); - Stream.of(solution.getEntityList().get(0), solution.getEntityList().get(1)) + Stream.of(solution.getEntityList().getFirst(), solution.getEntityList().get(1)) .forEach(entity -> { scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); @@ -1569,7 +1569,7 @@ public void groupBy_1Mapping1Collector() { }); assertScore(scoreDirector, assertMatchWithScore(-1, solution.getFirstEntityGroup(), 1L) - .withIndictedObjects(originalFirstEntity, solution.getEntityList().get(0))); + .withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -1602,7 +1602,7 @@ public void groupBy_1Mapping2Collector() { scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, assertMatchWithScore(-1, solution.getFirstEntityGroup(), 1L, Collections.singleton(entity3)) - .withIndictedObjects(entity1, entity3), + .withIndictedObjects(entity3), assertMatchWithScore(-1, solution.getEntityGroupList().get(1), 1L, Collections.singleton(entity2)) .withIndictedObjects(entity2)); } @@ -1638,7 +1638,7 @@ public void groupBy_1Mapping3Collector() { scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, assertMatchWithScore(-1, solution.getFirstEntityGroup(), 1L, 1, Collections.singleton(entity3)) - .withIndictedObjects(entity1, entity3), + .withIndictedObjects(entity3), assertMatchWithScore(-1, solution.getEntityGroupList().get(1), 1L, 1, Collections.singleton(entity2)) .withIndictedObjects(entity2)); } @@ -1687,7 +1687,7 @@ public void groupBy_0Mapping1Collector() { solution.getEntityList().get(4), solution.getEntityList().get(5), solution.getEntityList().get(6), - entity1, entity2, entity3)); + entity1, entity2)); } @Override @@ -1712,7 +1712,7 @@ public void groupBy_0Mapping2Collector() { scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); - assertScore(scoreDirector, assertMatchWithScore(-1, 2L, 2).withIndictedObjects(entity1, solution.getEntityList().get(0), + assertScore(scoreDirector, assertMatchWithScore(-1, 2L, 2).withIndictedObjects(solution.getEntityList().getFirst(), solution.getEntityList().get(1))); } @@ -1746,7 +1746,7 @@ public void groupBy_0Mapping3Collector() { solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, 2L, 1, 2).withIndictedObjects(entity1, solution.getEntityList().get(0), + assertMatchWithScore(-1, 2L, 1, 2).withIndictedObjects(solution.getEntityList().getFirst(), solution.getEntityList().get(1))); } @@ -1781,8 +1781,8 @@ public void groupBy_0Mapping4Collector() { solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, 2L, 1, 2, asSet(entity2, entity3)).withIndictedObjects(entity1, - solution.getEntityList().get(0), + assertMatchWithScore(-1, 2L, 1, 2, asSet(entity2, entity3)).withIndictedObjects( + solution.getEntityList().getFirst(), solution.getEntityList().get(1))); } @@ -1865,7 +1865,7 @@ public void groupBy_2Mapping0Collector() { assertScore(scoreDirector, assertMatchWithScore(-1, entityGroup1, solution.getFirstValue()).withIndictedObjects(entity1, entity2), assertMatchWithScore(-1, entityGroup1, secondValue).withIndictedObjects(entity3), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(0)) + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().getFirst()) .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(5)), assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(1)) .withIndictedObjects(solution.getEntityList().get(1), solution.getEntityList().get(6)), @@ -1882,7 +1882,7 @@ public void groupBy_2Mapping0Collector() { scoreDirector.afterEntityRemoved(entity3); assertScore(scoreDirector, assertMatchWithScore(-1, entityGroup1, solution.getFirstValue()).withIndictedObjects(entity1, entity2), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(0)) + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().getFirst()) .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(5)), assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(1)) .withIndictedObjects(solution.getEntityList().get(1), solution.getEntityList().get(6)), @@ -1898,8 +1898,8 @@ public void groupBy_2Mapping0Collector() { solution.getEntityList().remove(entity2); scoreDirector.afterEntityRemoved(entity2); assertScore(scoreDirector, - assertMatchWithScore(-1, entityGroup1, solution.getFirstValue()).withIndictedObjects(entity1, entity2), - assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(0)) + assertMatchWithScore(-1, entityGroup1, solution.getFirstValue()).withIndictedObjects(entity1), + assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().getFirst()) .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(5)), assertMatchWithScore(-1, solution.getFirstEntityGroup(), solution.getValueList().get(1)) .withIndictedObjects(solution.getEntityList().get(1), solution.getEntityList().get(6)), @@ -2198,7 +2198,7 @@ public void mapToUniWithDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group).withIndictedObjects(solution.getEntityList().get(0))); + assertMatch(group).withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -2227,7 +2227,7 @@ public void mapToUniWithoutDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group2).withIndictedObjects(solution.getEntityList().get(0))); + assertMatch(group2).withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -2255,7 +2255,7 @@ public void mapToUniAndDistinctWithDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group).withIndictedObjects(entity, solution.getEntityList().get(0))); + assertMatch(group).withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -2285,7 +2285,7 @@ public void mapToUniAndDistinctWithoutDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group2).withIndictedObjects(solution.getEntityList().get(0))); + assertMatch(group2).withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -2317,7 +2317,7 @@ public void mapToBi() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group2, value2).withIndictedObjects(solution.getEntityList().get(0))); + assertMatch(group2, value2).withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -2352,7 +2352,7 @@ public void mapToTri() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group2, value2, code2).withIndictedObjects(solution.getEntityList().get(0))); + assertMatch(group2, value2, code2).withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -2390,7 +2390,7 @@ public void mapToQuad() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group2, value2, code2, property2).withIndictedObjects(solution.getEntityList().get(0))); + assertMatch(group2, value2, code2, property2).withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -2419,7 +2419,7 @@ public void expandToBi() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), group2).withIndictedObjects(solution.getEntityList().get(0))); + assertMatch(solution.getFirstEntity(), group2).withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -2451,7 +2451,7 @@ public void expandToTri() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), group2, value2).withIndictedObjects(solution.getEntityList().get(0))); + assertMatch(solution.getFirstEntity(), group2, value2).withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -2487,7 +2487,7 @@ public void expandToQuad() { scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, assertMatch(solution.getFirstEntity(), group2, value2, code2) - .withIndictedObjects(solution.getEntityList().get(0))); + .withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -2536,8 +2536,8 @@ public void flattenLastWithDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group1).withIndictedObjects(solution.getEntityList().get(0)), - assertMatch(group2).withIndictedObjects(solution.getEntityList().get(0))); + assertMatch(group1).withIndictedObjects(solution.getEntityList().getFirst()), + assertMatch(group2).withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -2566,7 +2566,7 @@ public void flattenLastWithoutDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group2).withIndictedObjects(solution.getEntityList().get(0))); + assertMatch(group2).withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -2629,8 +2629,8 @@ public void flattenLastAndDistinctWithDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group1).withIndictedObjects(entity, solution.getEntityList().get(0)), - assertMatch(group2).withIndictedObjects(entity, solution.getEntityList().get(0))); + assertMatch(group1).withIndictedObjects(solution.getEntityList().getFirst()), + assertMatch(group2).withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -2660,7 +2660,7 @@ public void flattenLastAndDistinctWithoutDuplicates() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(group2).withIndictedObjects(solution.getEntityList().get(0))); + assertMatch(group2).withIndictedObjects(solution.getEntityList().getFirst())); } @Override @@ -3180,7 +3180,7 @@ public void concatAfterGroupBy() { scoreDirector.afterVariableChanged(entity3, "value"); assertScore(scoreDirector, assertMatchWithScore(-1, value1, 1L).withIndictedObjects(entity3), - assertMatchWithScore(-1, value2, 1L).withIndictedObjects(entity2, entity3)); + assertMatchWithScore(-1, value2, 1L).withIndictedObjects(entity2)); } @Override From 2cf2be925b355f8e448135abc0c214cea7144f01 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 21 Jul 2026 13:38:52 -0400 Subject: [PATCH 06/30] chore: move indictment code for IfExists to IndictmentSource --- .../bavet/common/AbstractIfExistsNode.java | 8 ++------ .../core/impl/bavet/common/tuple/Tuple.java | 2 +- .../bavet/common/tuple/UniversalTuple.java | 4 ++-- .../tuple/indictment/IndictmentSource.java | 18 +++++++++--------- .../inliner/ConstraintMatchSupplier.java | 5 +++-- .../uni/AbstractUniConstraintStreamTest.java | 6 ++---- 6 files changed, 19 insertions(+), 24 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java index 010810636ca..1e5e8a0cf6f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java @@ -1,7 +1,5 @@ package ai.timefold.solver.core.impl.bavet.common; -import java.util.Objects; - import ai.timefold.solver.core.impl.bavet.common.tuple.InTupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; @@ -146,15 +144,13 @@ protected void incrementCounterRightUpdatingIndictment(ExistsCounter doRetractCounter(counter); } } // Else do not even propagate an update - counter.getTuple().getIndictmentSupportForNodeId(getId()) - .add(Objects.requireNonNull(rightTuple.getA())); + IndictmentSource.addSupport(getId(), counter.leftTuple, rightTuple); counter.countRight++; } protected void decrementCounterRight(ExistsCounter counter) { counter.countRight--; - counter.getTuple().getIndictmentSupportForNodeId(getId()) - .remove(Objects.requireNonNull(rightTuple.getA())); + IndictmentSource.removeSupport(getId(), counter.leftTuple, rightTuple); if (counter.countRight == 0) { if (shouldExist) { doRetractCounter(counter); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java index 5012bfcc21a..f46d3d4ecc1 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java @@ -30,7 +30,7 @@ public sealed interface Tuple void setIndictmentSource(IndictmentSource indictmentSource); - Set getIndictmentSupportForNodeId(long nodeId); + Set getIndictmentSupportForNodeId(long nodeId); TupleState getState(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java index fbeb4b1e426..b6fdc71f9f6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java @@ -28,7 +28,7 @@ final class UniversalTuple private @Nullable C c; private @Nullable D d; private IndictmentSource indictmentSource = IndictmentSource.DISABLED; - private @Nullable Map> nodeIdToIndictmentSupport; + private @Nullable Map> nodeIdToIndictmentSupport; private TupleState state = TupleState.DEAD; // It's the node's job to mark a new tuple as CREATING. UniversalTuple(int storeSize, int cardinality) { @@ -123,7 +123,7 @@ public void setIndictmentSource(IndictmentSource indictmentSource) { } @Override - public Set getIndictmentSupportForNodeId(long nodeId) { + public Set getIndictmentSupportForNodeId(long nodeId) { if (nodeIdToIndictmentSupport == null) { nodeIdToIndictmentSupport = new LinkedHashMap<>(); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java index 8340f1c23d9..b44a5146f02 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java @@ -50,18 +50,18 @@ static IndictmentSource removeFromAggregate(Tuple elementTuple, Tuple groupTuple } } - static IndictmentSource sourceWithSupport(Tuple carry, Tuple support) { + static void addSupport(long nodeId, Tuple carry, Tuple support) { if (carry.getIndictmentSource() == DISABLED) { - return DISABLED; + return; } - if (carry.getIndictmentSource() instanceof IndictmentSourceWithSupport indictmentSourceWithSupport) { - indictmentSourceWithSupport.support.add(support.getIndictmentSource()); - return indictmentSourceWithSupport; - } else { - throw new IllegalStateException("Carry tuple (%s) does not have a %s %s; its source is (%s) instead." - .formatted(carry, IndictmentSourceWithSupport.class.getSimpleName(), IndictmentSource.class.getSimpleName(), - support.getIndictmentSource())); + carry.getIndictmentSupportForNodeId(nodeId).add(support.getIndictmentSource()); + } + + static void removeSupport(long nodeId, Tuple carry, Tuple support) { + if (carry.getIndictmentSource() == DISABLED) { + return; } + carry.getIndictmentSupportForNodeId(nodeId).remove(support.getIndictmentSource()); } record DisabledIndictmentSource() implements IndictmentSource { 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 6780cd24475..d5ee7f3518e 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 @@ -42,7 +42,6 @@ public interface ConstraintMatchSupplier> extends BiFunction> { - @SuppressWarnings("unchecked") static List collectIndictments(Constraint constraint, Tuple tuple) { if (tuple.getIndictmentSource() == IndictmentSource.DISABLED) { return Collections.emptyList(); @@ -51,7 +50,9 @@ static List collectIndictments(Constraint constraint, Tuple tuple) { tuple.getIndictmentSource().visitSources(out::add); var abstractConstraint = (AbstractConstraint) constraint; for (var involvedNodeId : Objects.requireNonNull(abstractConstraint.getInvolvedNodeIds())) { - out.addAll(tuple.getIndictmentSupportForNodeId(involvedNodeId)); + for (var indictmentSource : tuple.getIndictmentSupportForNodeId(involvedNodeId)) { + indictmentSource.visitSources(out::add); + } } return new ArrayList<>(out); } 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 8ae79d3d862..e3745e9746b 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 @@ -481,12 +481,10 @@ public void ifExists_0Joiner0Filter() { assertScore(scoreDirector, assertMatch(solution.getFirstValueGroup()).withIndictedObjects( solution.getFirstValueGroup(), - solution.getFirstEntityGroup(), - entityGroup), + solution.getFirstEntityGroup()), assertMatch(valueGroup).withIndictedObjects( valueGroup, - solution.getFirstEntityGroup(), - entityGroup)); + solution.getFirstEntityGroup())); } @Override From 46d39600114ab875b8010ee01c851135348749be Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 21 Jul 2026 14:47:31 -0400 Subject: [PATCH 07/30] chore: track support in indictment, not tuple This allows support infomation to be automatically passed when a new tuple is created (ex: in map/expand). --- .../core/impl/bavet/common/tuple/Tuple.java | 3 - .../bavet/common/tuple/UniversalTuple.java | 14 --- .../tuple/indictment/IndictmentSource.java | 118 ++++++++++++++---- .../inliner/ConstraintMatchSupplier.java | 8 +- .../uni/AbstractUniConstraintStreamTest.java | 3 +- 5 files changed, 98 insertions(+), 48 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java index f46d3d4ecc1..7cdfa23e285 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/Tuple.java @@ -1,6 +1,5 @@ package ai.timefold.solver.core.impl.bavet.common.tuple; -import java.util.Set; import java.util.function.Function; import ai.timefold.solver.core.api.score.stream.uni.UniConstraintStream; @@ -30,8 +29,6 @@ public sealed interface Tuple void setIndictmentSource(IndictmentSource indictmentSource); - Set getIndictmentSupportForNodeId(long nodeId); - TupleState getState(); void setState(TupleState state); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java index b6fdc71f9f6..63cadf46844 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java @@ -1,10 +1,5 @@ package ai.timefold.solver.core.impl.bavet.common.tuple; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; - import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; import org.jspecify.annotations.NullMarked; @@ -28,7 +23,6 @@ final class UniversalTuple private @Nullable C c; private @Nullable D d; private IndictmentSource indictmentSource = IndictmentSource.DISABLED; - private @Nullable Map> nodeIdToIndictmentSupport; private TupleState state = TupleState.DEAD; // It's the node's job to mark a new tuple as CREATING. UniversalTuple(int storeSize, int cardinality) { @@ -122,14 +116,6 @@ public void setIndictmentSource(IndictmentSource indictmentSource) { this.indictmentSource = indictmentSource; } - @Override - public Set getIndictmentSupportForNodeId(long nodeId) { - if (nodeIdToIndictmentSupport == null) { - nodeIdToIndictmentSupport = new LinkedHashMap<>(); - } - return nodeIdToIndictmentSupport.computeIfAbsent(nodeId, ignored -> new LinkedHashSet<>()); - } - @Override public String toString() { return switch (cardinality) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java index b44a5146f02..04f57047105 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java @@ -1,7 +1,13 @@ package ai.timefold.solver.core.impl.bavet.common.tuple.indictment; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.function.Consumer; import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; @@ -9,17 +15,40 @@ public sealed interface IndictmentSource { IndictmentSource DISABLED = new DisabledIndictmentSource(); - void visitSources(Consumer sourceConsumer); + void visitSources(Set visited, long[] involvedNodeIds, Consumer sourceConsumer); + + Map> support(); + + default void visitSources(long[] involvedNodeIds, Consumer sourceConsumer) { + visitSources(new HashSet<>(), involvedNodeIds, sourceConsumer); + } + + default Set getSupportForNodeId(long nodeId) { + return support().computeIfAbsent(nodeId, ignored -> new LinkedHashSet<>()); + } + + static boolean checkIfAlreadyVisitedAndVisitSupport(IndictmentSource self, Set visited, + long[] involvedNodeIds, Consumer sourceConsumer) { + if (!visited.add(self)) { + return true; + } + for (var nodeId : involvedNodeIds) { + for (var indictmentSource : self.support().getOrDefault(nodeId, Collections.emptySet())) { + indictmentSource.visitSources(visited, involvedNodeIds, sourceConsumer); + } + } + return false; + } static IndictmentSource of(Object source) { - return new RootIndictmentSource(source); + return new RootIndictmentSource(source, new LinkedHashMap<>()); } static IndictmentSource joining(Tuple left, Tuple right) { if (left.getIndictmentSource() == DISABLED) { return DISABLED; } - return new JoinedIndictmentSource(left.getIndictmentSource(), right.getIndictmentSource()); + return new JoinedIndictmentSource(left.getIndictmentSource(), right.getIndictmentSource(), new LinkedHashMap<>()); } static IndictmentSource aggregating(Tuple elementTuple, Tuple groupTuple) { @@ -32,7 +61,7 @@ static IndictmentSource aggregating(Tuple elementTuple, Tuple groupTuple) { } else { var collection = new ArrayList(); collection.add(elementTuple.getIndictmentSource()); - return new AggregateIndictmentSource(collection); + return new AggregateIndictmentSource(collection, new LinkedHashMap<>()); } } @@ -46,7 +75,7 @@ static IndictmentSource removeFromAggregate(Tuple elementTuple, Tuple groupTuple } else { var collection = new ArrayList(); collection.add(elementTuple.getIndictmentSource()); - return new AggregateIndictmentSource(collection); + return new AggregateIndictmentSource(collection, new LinkedHashMap<>()); } } @@ -54,54 +83,95 @@ static void addSupport(long nodeId, Tuple carry, Tuple support) { if (carry.getIndictmentSource() == DISABLED) { return; } - carry.getIndictmentSupportForNodeId(nodeId).add(support.getIndictmentSource()); + carry.getIndictmentSource().getSupportForNodeId(nodeId).add(support.getIndictmentSource()); } static void removeSupport(long nodeId, Tuple carry, Tuple support) { if (carry.getIndictmentSource() == DISABLED) { return; } - carry.getIndictmentSupportForNodeId(nodeId).remove(support.getIndictmentSource()); + carry.getIndictmentSource().getSupportForNodeId(nodeId).remove(support.getIndictmentSource()); } record DisabledIndictmentSource() implements IndictmentSource { @Override - public void visitSources(Consumer sourceConsumer) { + public void visitSources(Set visited, long[] involvedNodeIds, Consumer sourceConsumer) { + throw new UnsupportedOperationException("Impossible state: indictments are disabled."); + } + + @Override + public Map> support() { throw new UnsupportedOperationException("Impossible state: indictments are disabled."); } } - record RootIndictmentSource(Object source) implements IndictmentSource { + record RootIndictmentSource(Object source, Map> support) implements IndictmentSource { @Override - public void visitSources(Consumer sourceConsumer) { + public void visitSources(Set visited, long[] involvedNodeIds, Consumer sourceConsumer) { + if (checkIfAlreadyVisitedAndVisitSupport(this, visited, involvedNodeIds, sourceConsumer)) { + return; + } sourceConsumer.accept(source); } - } - record JoinedIndictmentSource(IndictmentSource left, IndictmentSource right) implements IndictmentSource { @Override - public void visitSources(Consumer sourceConsumer) { - left.visitSources(sourceConsumer); - right.visitSources(sourceConsumer); + public boolean equals(Object o) { + return this == o; + } + + @Override + public int hashCode() { + return System.identityHashCode(this); } } - record AggregateIndictmentSource(List sourceList) implements IndictmentSource { + record JoinedIndictmentSource(IndictmentSource left, IndictmentSource right, + Map> support) implements IndictmentSource { @Override - public void visitSources(Consumer sourceConsumer) { - for (var source : sourceList) { - source.visitSources(sourceConsumer); + public void visitSources(Set visited, long[] involvedNodeIds, Consumer sourceConsumer) { + if (checkIfAlreadyVisitedAndVisitSupport(this, visited, involvedNodeIds, sourceConsumer)) { + return; } + left.visitSources(visited, involvedNodeIds, sourceConsumer); + right.visitSources(visited, involvedNodeIds, sourceConsumer); + } + + @Override + public boolean equals(Object o) { + return this == o; + } + + @Override + public int hashCode() { + return System.identityHashCode(this); } } - record IndictmentSourceWithSupport(IndictmentSource source, List support) implements IndictmentSource { + record AggregateIndictmentSource(List sourceList, + Map> support) implements IndictmentSource { @Override - public void visitSources(Consumer sourceConsumer) { - source.visitSources(sourceConsumer); - for (var support : support) { - support.visitSources(sourceConsumer); + public void visitSources(Set visited, long[] involvedNodeIds, Consumer sourceConsumer) { + if (checkIfAlreadyVisitedAndVisitSupport(this, visited, involvedNodeIds, sourceConsumer)) { + return; + } + for (var source : sourceList) { + source.visitSources(involvedNodeIds, sourceConsumer); } } + + @Override + public Set getSupportForNodeId(long nodeId) { + return support.computeIfAbsent(nodeId, ignored -> new LinkedHashSet<>()); + } + + @Override + public boolean equals(Object o) { + return this == o; + } + + @Override + public int hashCode() { + return System.identityHashCode(this); + } } } 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 d5ee7f3518e..93d41787cae 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 @@ -47,13 +47,9 @@ static List collectIndictments(Constraint constraint, Tuple tuple) { return Collections.emptyList(); } var out = new LinkedHashSet<>(); - tuple.getIndictmentSource().visitSources(out::add); var abstractConstraint = (AbstractConstraint) constraint; - for (var involvedNodeId : Objects.requireNonNull(abstractConstraint.getInvolvedNodeIds())) { - for (var indictmentSource : tuple.getIndictmentSupportForNodeId(involvedNodeId)) { - indictmentSource.visitSources(out::add); - } - } + var involvedNodeIds = Objects.requireNonNull(abstractConstraint.getInvolvedNodeIds()); + tuple.getIndictmentSource().visitSources(involvedNodeIds, out::add); return new ArrayList<>(out); } 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 e3745e9746b..80715bab290 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 @@ -2449,7 +2449,8 @@ public void expandToTri() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), group2, value2).withIndictedObjects(solution.getEntityList().getFirst())); + assertMatch(solution.getFirstEntity(), group2, value2) + .withIndictedObjects(solution.getEntityList().getFirst())); } @Override From 244462c9a1285dc679037613e0d658817a7e51be Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 21 Jul 2026 15:13:02 -0400 Subject: [PATCH 08/30] chore: make getIndictedObjects() return null when indictments disabled, add notes about stale ifExists indictments --- .../core/impl/bavet/common/AbstractIfExistsNode.java | 4 ++++ .../core/impl/score/constraint/ConstraintMatch.java | 6 +++--- .../impl/score/constraint/ConstraintMatchTotal.java | 3 +-- .../stream/common/inliner/ConstraintMatchSupplier.java | 5 +++-- .../common/uni/AbstractUniConstraintStreamTest.java | 10 ++++++++-- 5 files changed, 19 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java index 1e5e8a0cf6f..49358c5c80e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java @@ -133,6 +133,8 @@ protected void incrementCounterRightWithoutIndictment(ExistsCounter doRetractCounter(counter); } } // Else do not even propagate an update + // NOTE: By not propagating here, the left tuple's indicted objects can be stale + // if an element is removed. counter.countRight++; } @@ -144,6 +146,8 @@ protected void incrementCounterRightUpdatingIndictment(ExistsCounter doRetractCounter(counter); } } // Else do not even propagate an update + // NOTE: By not propagating here, the left tuple's indicted objects can be stale + // if an element is removed. IndictmentSource.addSupport(getId(), counter.leftTuple, rightTuple); counter.countRight++; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java index 2c8f6a2e6b5..f977608580e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java @@ -30,7 +30,7 @@ public final class ConstraintMatch> implements Comp private final ConstraintRef constraintRef; private final @Nullable ConstraintJustification justification; - private final List indictedObjects; + private final @Nullable List indictedObjects; private final Score_ score; /** @@ -39,7 +39,7 @@ public final class ConstraintMatch> implements Comp * @param score penalty or reward associated with the constraint match */ public ConstraintMatch(ConstraintRef constraintRef, @Nullable ConstraintJustification justification, - List indictedObjects, Score_ score) { + @Nullable List indictedObjects, Score_ score) { this.constraintRef = requireNonNull(constraintRef); this.justification = justification; this.indictedObjects = indictedObjects; @@ -67,7 +67,7 @@ public ConstraintRef getConstraintRef() { return (Justification_) justification; } - public List getIndictedObjects() { + public @Nullable List getIndictedObjects() { return indictedObjects; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTotal.java b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTotal.java index 31502a8336c..4ed2b3e0014 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTotal.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTotal.java @@ -2,7 +2,6 @@ import static java.util.Objects.requireNonNull; -import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -94,7 +93,7 @@ public ConstraintMatch addConstraintMatch(ConstraintJustification justif * @return never null */ public ConstraintMatch addConstraintMatch(ConstraintJustification justification, Score_ score) { - var constraintMatch = new ConstraintMatch<>(constraintRef, justification, Collections.emptyList(), score); + var constraintMatch = new ConstraintMatch<>(constraintRef, justification, null, score); addConstraintMatch(constraintMatch); return constraintMatch; } 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 93d41787cae..623e37f127d 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 @@ -25,6 +25,7 @@ import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraint; import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; /** * Allows creating {@link ConstraintMatch} instances lazily if and only if they are required by the end user. @@ -42,9 +43,9 @@ public interface ConstraintMatchSupplier> extends BiFunction> { - static List collectIndictments(Constraint constraint, Tuple tuple) { + static @Nullable List collectIndictments(Constraint constraint, Tuple tuple) { if (tuple.getIndictmentSource() == IndictmentSource.DISABLED) { - return Collections.emptyList(); + return null; } var out = new LinkedHashSet<>(); var abstractConstraint = (AbstractConstraint) constraint; 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 80715bab290..42e57172f94 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 @@ -478,13 +478,19 @@ public void ifExists_0Joiner0Filter() { scoreDirector.beforeProblemFactRemoved(entityGroup); solution.getEntityGroupList().remove(entityGroup); scoreDirector.afterProblemFactRemoved(entityGroup); + + // Note: entityGroup is still indicted, despite being removed, + // since only the right tuple of the ifExists changed, so no + // change was propagated. assertScore(scoreDirector, assertMatch(solution.getFirstValueGroup()).withIndictedObjects( solution.getFirstValueGroup(), - solution.getFirstEntityGroup()), + solution.getFirstEntityGroup(), + entityGroup), assertMatch(valueGroup).withIndictedObjects( valueGroup, - solution.getFirstEntityGroup())); + solution.getFirstEntityGroup(), + entityGroup)); } @Override From 38642183cc0a651982b1cdac8d1c56386934f9e2 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 21 Jul 2026 16:48:06 -0400 Subject: [PATCH 09/30] test: add tests for BiConstraintStream indictments --- .../bavet/common/AbstractIfExistsNode.java | 8 +- .../bi/AbstractBiConstraintStreamTest.java | 613 ++++++++++-------- 2 files changed, 348 insertions(+), 273 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java index 49358c5c80e..317b9004a3f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java @@ -133,8 +133,8 @@ protected void incrementCounterRightWithoutIndictment(ExistsCounter doRetractCounter(counter); } } // Else do not even propagate an update - // NOTE: By not propagating here, the left tuple's indicted objects can be stale - // if an element is removed. + // NOTE: By not propagating here, the left tuple's indicted objects can be stale + // if an element is removed. counter.countRight++; } @@ -146,8 +146,8 @@ protected void incrementCounterRightUpdatingIndictment(ExistsCounter doRetractCounter(counter); } } // Else do not even propagate an update - // NOTE: By not propagating here, the left tuple's indicted objects can be stale - // if an element is removed. + // NOTE: By not propagating here, the left tuple's indicted objects can be stale + // if an element is removed. IndictmentSource.addSupport(getId(), counter.leftTuple, rightTuple); counter.countRight++; } 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 0fd2d1f078f..7b5cf3873ed 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 @@ -166,10 +166,10 @@ public void join_filterOnAssignedValue_unassignOne() { scoreDirector.afterListVariableElementAssigned(entity, "valueList", value1); assertScore(scoreDirector, - assertMatch(entity, value1, value1), - assertMatch(entity, value1, value2), - assertMatch(entity, value2, value1), - assertMatch(entity, value2, value2)); + assertMatch(entity, value1, value1).withIndictedObjects(entity, value1), + assertMatch(entity, value1, value2).withIndictedObjects(entity, value1, value2), + assertMatch(entity, value2, value1).withIndictedObjects(entity, value1, value2), + assertMatch(entity, value2, value2).withIndictedObjects(entity, value2)); // Unassign and check result. var variableDescriptor = scoreDirector.getSolutionDescriptor() @@ -181,7 +181,7 @@ public void join_filterOnAssignedValue_unassignOne() { scoreDirector.afterListVariableElementUnassigned(variableDescriptor, value1); assertScore(scoreDirector, - assertMatch(entity, value2, value2)); + assertMatch(entity, value2, value2).withIndictedObjects(entity, value2)); // Reassign and check result. scoreDirector.beforeListVariableElementAssigned(variableDescriptor, value1); @@ -191,10 +191,10 @@ public void join_filterOnAssignedValue_unassignOne() { scoreDirector.afterListVariableElementAssigned(variableDescriptor, value1); assertScore(scoreDirector, - assertMatch(entity, value1, value1), - assertMatch(entity, value1, value2), - assertMatch(entity, value2, value1), - assertMatch(entity, value2, value2)); + assertMatch(entity, value1, value1).withIndictedObjects(entity, value1), + assertMatch(entity, value1, value2).withIndictedObjects(entity, value1, value2), + assertMatch(entity, value2, value1).withIndictedObjects(entity, value1, value2), + assertMatch(entity, value2, value2).withIndictedObjects(entity, value2)); } } @@ -231,7 +231,7 @@ public void join_filterOnAssignedValue_unassignOneReassignOther() { scoreDirector.afterListVariableElementAssigned(entity, "valueList", value1); assertScore(scoreDirector, - assertMatch(entity, value1, value1)); + assertMatch(entity, value1, value1).withIndictedObjects(entity, value1)); // Unassign+assign and check result. var variableDescriptor = scoreDirector.getSolutionDescriptor() @@ -245,7 +245,7 @@ public void join_filterOnAssignedValue_unassignOneReassignOther() { scoreDirector.afterListVariableElementAssigned(variableDescriptor, value2); assertScore(scoreDirector, - assertMatch(entity, value2, value2)); + assertMatch(entity, value2, value2).withIndictedObjects(entity, value2)); // Reassign and check result. scoreDirector.beforeListVariableElementAssigned(variableDescriptor, value1); @@ -257,7 +257,7 @@ public void join_filterOnAssignedValue_unassignOneReassignOther() { scoreDirector.afterListVariableElementAssigned(variableDescriptor, value1); assertScore(scoreDirector, - assertMatch(entity, value1, value1)); + assertMatch(entity, value1, value1).withIndictedObjects(entity, value1)); } } @@ -710,24 +710,24 @@ public void joinAfterGroupBy() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(1L, 1L, extra1), - assertMatch(1L, 1L, extra2)); + assertMatch(1L, 1L, extra1).withIndictedObjects(entity1, entity2, extra1), + assertMatch(1L, 1L, extra2).withIndictedObjects(entity1, entity2, extra2)); // Incremental scoreDirector.beforeVariableChanged(entity2, "value"); entity2.setValue(value2); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(2L, 2L, extra1), - assertMatch(2L, 2L, extra2)); + assertMatch(2L, 2L, extra1).withIndictedObjects(entity1, entity2, extra1), + assertMatch(2L, 2L, extra2).withIndictedObjects(entity1, entity2, extra2)); // Incremental scoreDirector.beforeEntityRemoved(entity2); solution.getEntityList().remove(entity2); scoreDirector.afterEntityRemoved(entity2); assertScore(scoreDirector, - assertMatch(1L, 1L, extra1), - assertMatch(1L, 1L, extra2)); + assertMatch(1L, 1L, extra1).withIndictedObjects(entity1, extra1), + assertMatch(1L, 1L, extra2).withIndictedObjects(entity1, extra2)); } @Override @@ -776,8 +776,8 @@ public void ifExists_filterOnAssignedValue_unassignOne() { scoreDirector.afterListVariableElementAssigned(entity, "valueList", value1); assertScore(scoreDirector, - assertMatch(entity, value1), - assertMatch(entity, value2)); + assertMatch(entity, value1).withIndictedObjects(entity, value1), + assertMatch(entity, value2).withIndictedObjects(entity, value2)); // Unassign and check result. var variableDescriptor = scoreDirector.getSolutionDescriptor() @@ -789,7 +789,7 @@ public void ifExists_filterOnAssignedValue_unassignOne() { scoreDirector.afterListVariableElementUnassigned(variableDescriptor, value1); assertScore(scoreDirector, - assertMatch(entity, value2)); + assertMatch(entity, value2).withIndictedObjects(entity, value2)); // Reassign and check result. scoreDirector.beforeListVariableElementAssigned(variableDescriptor, value1); @@ -799,8 +799,8 @@ public void ifExists_filterOnAssignedValue_unassignOne() { scoreDirector.afterListVariableElementAssigned(variableDescriptor, value1); assertScore(scoreDirector, - assertMatch(entity, value1), - assertMatch(entity, value2)); + assertMatch(entity, value1).withIndictedObjects(entity, value1), + assertMatch(entity, value2).withIndictedObjects(entity, value1, value2)); } } @@ -1221,21 +1221,21 @@ public void ifExistsAfterGroupBy() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(1L, 1L)); + assertMatch(1L, 1L).withIndictedObjects(entity1, entity2)); // Incremental scoreDirector.beforeVariableChanged(entity2, "value"); entity2.setValue(value2); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(2L, 2L)); + assertMatch(2L, 2L).withIndictedObjects(entity1, entity2)); // Incremental scoreDirector.beforeEntityRemoved(entity2); solution.getEntityList().remove(entity2); scoreDirector.afterEntityRemoved(entity2); assertScore(scoreDirector, - assertMatch(1L, 1L)); + assertMatch(1L, 1L).withIndictedObjects(entity1)); } @Override @@ -1258,11 +1258,16 @@ public void groupBy_1Mapping0Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); - // From scratch + // From scratch: groupBy accumulates indicted objects from all tuples in the group scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, solution.getFirstEntityGroup()), - assertMatchWithScore(-1, entityGroup1)); + assertMatchWithScore(-1, solution.getFirstEntityGroup()) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(1), + solution.getEntityList().get(2), + solution.getEntityList().get(3), solution.getEntityList().get(4), + solution.getEntityList().get(5), + solution.getEntityList().get(6), entity3), + assertMatchWithScore(-1, entityGroup1).withIndictedObjects(entity1, entity2)); // Incremental Stream.of(entity1, entity2).forEach(entity -> { @@ -1270,7 +1275,11 @@ public void groupBy_1Mapping0Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); }); - assertScore(scoreDirector, assertMatchWithScore(-1, solution.getFirstEntityGroup())); + assertScore(scoreDirector, assertMatchWithScore(-1, solution.getFirstEntityGroup()) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(1), + solution.getEntityList().get(2), + solution.getEntityList().get(3), solution.getEntityList().get(4), solution.getEntityList().get(5), + solution.getEntityList().get(6), entity3)); } @Override @@ -1285,11 +1294,20 @@ public void groupBy_1Mapping1Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); - // From scratch + // From scratch: groupBy accumulates indicted objects from all tuples in the group scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, solution.getFirstEntity().toString(), 6L), - assertMatchWithScore(-1, solution.getEntityList().get(1).toString(), 5L)); + assertMatchWithScore(-1, solution.getFirstEntity().toString(), 6L) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(1), + solution.getEntityList().get(2), + solution.getEntityList().get(3), solution.getEntityList().get(4), + solution.getEntityList().get(5), + solution.getEntityList().get(6)), + assertMatchWithScore(-1, solution.getEntityList().get(1).toString(), 5L) + .withIndictedObjects(solution.getEntityList().get(1), solution.getEntityList().get(2), + solution.getEntityList().get(3), solution.getEntityList().get(4), + solution.getEntityList().get(5), + solution.getEntityList().get(6))); // Incremental; we have a new first entity, and less entities in total. var entity = solution.getFirstEntity(); @@ -1297,7 +1315,10 @@ public void groupBy_1Mapping1Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, solution.getFirstEntity().toString(), 5L)); + assertMatchWithScore(-1, solution.getFirstEntity().toString(), 5L) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(1), + solution.getEntityList().get(2), solution.getEntityList().get(3), + solution.getEntityList().get(4), solution.getEntityList().get(5))); } @Override @@ -1316,11 +1337,13 @@ public void groupBy_1Mapping2Collector() { var entity1 = solution.getFirstEntity(); var entity2 = solution.getEntityList().get(1); - // From scratch + // From scratch: groupBy accumulates indicted objects from all tuples in the group scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, entity1.toString(), 2L, singleton(entity1)), - assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2))); + assertMatchWithScore(-1, entity1.toString(), 2L, singleton(entity1)) + .withIndictedObjects(entity1, entity2, solution.getEntityList().get(2)), + assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2)) + .withIndictedObjects(entity2, solution.getEntityList().get(2))); // Incremental var entity = solution.getFirstEntity(); @@ -1328,7 +1351,8 @@ public void groupBy_1Mapping2Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2))); + assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2)) + .withIndictedObjects(entity2, solution.getEntityList().get(1))); } @Override @@ -1350,13 +1374,15 @@ public void groupBy_1Mapping3Collector() { var entity2 = solution.getEntityList().get(1); entity2.setLongProperty(Long.MIN_VALUE); - // From scratch + // From scratch: groupBy accumulates indicted objects from all tuples in the group scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, assertMatchWithScore(-1, entity1.toString(), Long.MAX_VALUE, Long.MAX_VALUE, - singleton(entity1)), + singleton(entity1)) + .withIndictedObjects(entity1, entity2, solution.getEntityList().get(2)), assertMatchWithScore(-1, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, - singleton(entity2))); + singleton(entity2)) + .withIndictedObjects(entity2, solution.getEntityList().get(2))); // Incremental var entity = solution.getFirstEntity(); @@ -1365,7 +1391,8 @@ public void groupBy_1Mapping3Collector() { scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, assertMatchWithScore(-1, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, - singleton(entity2))); + singleton(entity2)) + .withIndictedObjects(entity2, solution.getEntityList().get(1))); } @Override @@ -1379,16 +1406,19 @@ public void groupBy_0Mapping1Collector() { .penalize(SimpleScore.ONE, count -> count) .asConstraint(TEST_CONSTRAINT_ID)); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); + // From scratch scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, assertMatchWithScore(-3, 3L)); + assertScore(scoreDirector, assertMatchWithScore(-3, 3L).withIndictedObjects(entity1, entity2, entity3)); // Incremental - var entity = solution.getFirstEntity(); - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); - assertScore(scoreDirector, assertMatchWithScore(-1, 1L)); + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); + assertScore(scoreDirector, assertMatchWithScore(-1, 1L).withIndictedObjects(entity2, entity3)); } @Override @@ -1403,16 +1433,18 @@ public void groupBy_0Mapping2Collector() { .asConstraint(TEST_CONSTRAINT_ID)); var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, assertMatchWithScore(-1, 3L, 2L)); + assertScore(scoreDirector, assertMatchWithScore(-1, 3L, 2L).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); - assertScore(scoreDirector, assertMatchWithScore(-1, 1L, 1L)); + assertScore(scoreDirector, assertMatchWithScore(-1, 1L, 1L).withIndictedObjects(entity2, entity3)); } @Override @@ -1437,14 +1469,14 @@ public void groupBy_0Mapping3Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, 3L, 0L, 1L)); + assertMatchWithScore(-1, 3L, 0L, 1L).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, 1L, 1L, 1L)); + assertMatchWithScore(-1, 1L, 1L, 1L).withIndictedObjects(entity2, entity3)); } @Override @@ -1470,14 +1502,14 @@ public void groupBy_0Mapping4Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, 3L, 0L, 1L, asSet(entity1, entity2))); + assertMatchWithScore(-1, 3L, 0L, 1L, asSet(entity1, entity2)).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, 1L, 1L, 1L, asSet(entity2))); + assertMatchWithScore(-1, 1L, 1L, 1L, asSet(entity2)).withIndictedObjects(entity2, entity3)); } @Override @@ -1493,21 +1525,23 @@ public void groupBy_2Mapping0Collector() { var group1 = solution.getEntityGroupList().get(0); var group2 = solution.getEntityGroupList().get(1); var group3 = solution.getEntityGroupList().get(2); + var entity1 = solution.getEntityList().get(0); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group1, group2), - assertMatchWithScore(-1, group1, group3), - assertMatchWithScore(-1, group2, group3)); + assertMatchWithScore(-1, group1, group2).withIndictedObjects(entity1, entity2), + assertMatchWithScore(-1, group1, group3).withIndictedObjects(entity1, entity3), + assertMatchWithScore(-1, group2, group3).withIndictedObjects(entity2, entity3)); // Incremental - var entity = solution.getFirstEntity(); - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, group3)); + assertMatchWithScore(-1, group2, group3).withIndictedObjects(entity2, entity3)); } @Override @@ -1523,24 +1557,27 @@ public void groupBy_2Mapping1Collector() { var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); + var entity1 = solution.getEntityList().get(0); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); + var entity4 = solution.getEntityList().get(3); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group1, group1, 1L), - assertMatchWithScore(-3, group1, group2, 3L), - assertMatchWithScore(-1, group2, group1, 1L), - assertMatchWithScore(-1, group2, group2, 1L)); + assertMatchWithScore(-1, group1, group1, 1L).withIndictedObjects(entity1, entity3), + assertMatchWithScore(-3, group1, group2, 3L).withIndictedObjects(entity1, entity2, entity4, entity3), + assertMatchWithScore(-1, group2, group1, 1L).withIndictedObjects(entity2, entity3), + assertMatchWithScore(-1, group2, group2, 1L).withIndictedObjects(entity2, entity4)); - // Incremental - var entity = solution.getFirstEntity(); - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); + // Incremental: groupBy indicted objects may be stale after incremental changes (acceptable per rules) + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, group2, 1L), - assertMatchWithScore(-1, group1, group2, 1L), - assertMatchWithScore(-1, group2, group1, 1L)); + assertMatchWithScore(-1, group2, group1, 1L).withIndictedObjects(entity2, entity3), + assertMatchWithScore(-1, group2, group2, 1L).withIndictedObjects(entity2, entity4), + assertMatchWithScore(-1, group1, group2, 1L).withIndictedObjects(entity4, entity3)); } @Override @@ -1557,24 +1594,27 @@ public void groupBy_2Mapping2Collector() { var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); + var entity1 = solution.getEntityList().get(0); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); + var entity4 = solution.getEntityList().get(3); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-2, group1, group1, 1L, 1L), - assertMatchWithScore(-6, group1, group2, 3L, 3L), - assertMatchWithScore(-2, group2, group1, 1L, 1L), - assertMatchWithScore(-2, group2, group2, 1L, 1L)); + assertMatchWithScore(-2, group1, group1, 1L, 1L).withIndictedObjects(entity1, entity3), + assertMatchWithScore(-6, group1, group2, 3L, 3L).withIndictedObjects(entity1, entity2, entity4, entity3), + assertMatchWithScore(-2, group2, group1, 1L, 1L).withIndictedObjects(entity2, entity3), + assertMatchWithScore(-2, group2, group2, 1L, 1L).withIndictedObjects(entity2, entity4)); - // Incremental - var entity = solution.getFirstEntity(); - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); + // Incremental: groupBy indicted objects may be stale after incremental changes (acceptable per rules) + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-2, group2, group2, 1L, 1L), - assertMatchWithScore(-2, group1, group2, 1L, 1L), - assertMatchWithScore(-2, group2, group1, 1L, 1L)); + assertMatchWithScore(-2, group2, group1, 1L, 1L).withIndictedObjects(entity2, entity3), + assertMatchWithScore(-2, group2, group2, 1L, 1L).withIndictedObjects(entity2, entity4), + assertMatchWithScore(-2, group1, group2, 1L, 1L).withIndictedObjects(entity4, entity3)); } @Override @@ -1592,21 +1632,23 @@ public void groupBy_3Mapping0Collector() { var group3 = solution.getEntityGroupList().get(2); var value1 = solution.getValueList().get(0); var value2 = solution.getValueList().get(1); + var entity1 = solution.getEntityList().get(0); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group1, group2, value1), - assertMatchWithScore(-1, group1, group3, value1), - assertMatchWithScore(-1, group2, group3, value2)); + assertMatchWithScore(-1, group1, group2, value1).withIndictedObjects(entity1, entity2), + assertMatchWithScore(-1, group1, group3, value1).withIndictedObjects(entity1, entity3), + assertMatchWithScore(-1, group2, group3, value2).withIndictedObjects(entity2, entity3)); // Incremental - var entity = solution.getFirstEntity(); - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, group3, value2)); + assertMatchWithScore(-1, group2, group3, value2).withIndictedObjects(entity2, entity3)); } @Override @@ -1625,21 +1667,23 @@ public void groupBy_3Mapping1Collector() { var group3 = solution.getEntityGroupList().get(2); var value1 = solution.getValueList().get(0); var value2 = solution.getValueList().get(1); + var entity1 = solution.getEntityList().get(0); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group1, group2, value1, 1L), - assertMatchWithScore(-1, group1, group3, value1, 1L), - assertMatchWithScore(-1, group2, group3, value2, 1L)); + assertMatchWithScore(-1, group1, group2, value1, 1L).withIndictedObjects(entity1, entity2), + assertMatchWithScore(-1, group1, group3, value1, 1L).withIndictedObjects(entity1, entity3), + assertMatchWithScore(-1, group2, group3, value2, 1L).withIndictedObjects(entity2, entity3)); // Incremental - var entity = solution.getFirstEntity(); - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, group3, value2, 1L)); + assertMatchWithScore(-1, group2, group3, value2, 1L).withIndictedObjects(entity2, entity3)); } @Override @@ -1658,21 +1702,23 @@ public void groupBy_4Mapping0Collector() { var group3 = solution.getEntityGroupList().get(2); var value1 = solution.getValueList().get(0); var value2 = solution.getValueList().get(1); + var entity1 = solution.getEntityList().get(0); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group1, group2, value1, value2), - assertMatchWithScore(-1, group1, group3, value1, value1), - assertMatchWithScore(-1, group2, group3, value2, value1)); + assertMatchWithScore(-1, group1, group2, value1, value2).withIndictedObjects(entity1, entity2), + assertMatchWithScore(-1, group1, group3, value1, value1).withIndictedObjects(entity1, entity3), + assertMatchWithScore(-1, group2, group3, value2, value1).withIndictedObjects(entity2, entity3)); // Incremental - var entity = solution.getFirstEntity(); - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, group3, value2, value1)); + assertMatchWithScore(-1, group2, group3, value2, value1).withIndictedObjects(entity2, entity3)); } @Override @@ -1709,22 +1755,23 @@ public void mapToUniWithDuplicates() { var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(asSet(group1, group2)), - assertMatch(asSet(group1, group2)), - assertMatch(asSet(group1))); - - var entity = solution.getFirstEntity(); + assertMatch(asSet(group1, group2)).withIndictedObjects(entity1, entity2), + assertMatch(asSet(group1, group2)).withIndictedObjects(entity2, entity3), + assertMatch(asSet(group1)).withIndictedObjects(entity1, entity3)); // Incremental - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatch(asSet(group1, group2))); + assertMatch(asSet(group1, group2)).withIndictedObjects(entity2, entity3)); } @Override @@ -1740,22 +1787,23 @@ public void mapToUniWithoutDuplicates() { var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var group3 = solution.getEntityGroupList().get(2); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(asSet(group1, group2)), - assertMatch(asSet(group1, group3)), - assertMatch(asSet(group2, group3))); - - var entity = solution.getFirstEntity(); + assertMatch(asSet(group1, group2)).withIndictedObjects(entity1, entity2), + assertMatch(asSet(group1, group3)).withIndictedObjects(entity1, entity3), + assertMatch(asSet(group2, group3)).withIndictedObjects(entity2, entity3)); // Incremental - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatch(asSet(group2, group3))); + assertMatch(asSet(group2, group3)).withIndictedObjects(entity2, entity3)); } @Override @@ -1771,21 +1819,22 @@ public void mapToUniAndDistinctWithDuplicates() { var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); - // From scratch + // From scratch: distinct accumulates indicted objects from all tuples with same key scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(asSet(group1, group2)), - assertMatch(asSet(group1))); - - var entity = solution.getFirstEntity(); + assertMatch(asSet(group1, group2)).withIndictedObjects(entity1, entity2, entity3), + assertMatch(asSet(group1)).withIndictedObjects(entity1, entity3)); // Incremental - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatch(asSet(group1, group2))); + assertMatch(asSet(group1, group2)).withIndictedObjects(entity2, entity3)); } @Override @@ -1802,22 +1851,23 @@ public void mapToUniAndDistinctWithoutDuplicates() { var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var group3 = solution.getEntityGroupList().get(2); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(asSet(group1, group2)), - assertMatch(asSet(group1, group3)), - assertMatch(asSet(group2, group3))); - - var entity = solution.getFirstEntity(); + assertMatch(asSet(group1, group2)).withIndictedObjects(entity1, entity2), + assertMatch(asSet(group1, group3)).withIndictedObjects(entity1, entity3), + assertMatch(asSet(group2, group3)).withIndictedObjects(entity2, entity3)); // Incremental - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatch(asSet(group2, group3))); + assertMatch(asSet(group2, group3)).withIndictedObjects(entity2, entity3)); } @Override @@ -1837,18 +1887,21 @@ public void mapToBi() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1, group2), - assertMatch(group2, group1), - assertMatch(group1, group1)); + assertMatch(group1, group2).withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(1)), + assertMatch(group2, group1).withIndictedObjects(solution.getEntityList().get(1), + solution.getEntityList().get(2)), + assertMatch(group1, group1).withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(2))); - var entity = solution.getFirstEntity(); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // Incremental - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatch(group2, group1)); + assertMatch(group2, group1).withIndictedObjects(entity2, entity3)); } @Override @@ -1872,18 +1925,23 @@ public void mapToTri() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1, group2, sum01), - assertMatch(group2, group1, sum12), - assertMatch(group1, group1, sum02)); + assertMatch(group1, group2, sum01).withIndictedObjects(solution.getFirstEntity(), + solution.getEntityList().get(1)), + assertMatch(group2, group1, sum12).withIndictedObjects(solution.getEntityList().get(1), + solution.getEntityList().get(2)), + assertMatch(group1, group1, sum02).withIndictedObjects(solution.getFirstEntity(), + solution.getEntityList().get(2))); - var entity = solution.getFirstEntity(); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // Incremental - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatch(group2, group1, sum12)); + assertMatch(group2, group1, sum12).withIndictedObjects(entity2, entity3)); } @Override @@ -1907,18 +1965,23 @@ public void mapToQuad() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1, group2, value1, value2), - assertMatch(group2, group1, value2, value1), - assertMatch(group1, group1, value1, value1)); + assertMatch(group1, group2, value1, value2).withIndictedObjects(solution.getFirstEntity(), + solution.getEntityList().get(1)), + assertMatch(group2, group1, value2, value1).withIndictedObjects(solution.getEntityList().get(1), + solution.getEntityList().get(2)), + assertMatch(group1, group1, value1, value1).withIndictedObjects(solution.getFirstEntity(), + solution.getEntityList().get(2))); - var entity = solution.getFirstEntity(); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // Incremental - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatch(group2, group1, value2, value1)); + assertMatch(group2, group1, value2, value1).withIndictedObjects(entity2, entity3)); } @Override @@ -1938,9 +2001,12 @@ public void expandToTri() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), solution.getEntityList().get(1), sum01), - assertMatch(solution.getEntityList().get(1), solution.getEntityList().get(2), sum12), - assertMatch(solution.getFirstEntity(), solution.getEntityList().get(2), sum02)); + assertMatch(solution.getFirstEntity(), solution.getEntityList().get(1), sum01) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(1)), + assertMatch(solution.getEntityList().get(1), solution.getEntityList().get(2), sum12) + .withIndictedObjects(solution.getEntityList().get(1), solution.getEntityList().get(2)), + assertMatch(solution.getFirstEntity(), solution.getEntityList().get(2), sum02) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(2))); var entity = solution.getFirstEntity(); @@ -1949,7 +2015,8 @@ public void expandToTri() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), solution.getEntityList().get(1), sum12)); + assertMatch(solution.getFirstEntity(), solution.getEntityList().get(1), sum12) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(1))); } @Override @@ -1975,9 +2042,12 @@ public void expandToQuad() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), solution.getEntityList().get(1), sum01, concat01), - assertMatch(solution.getEntityList().get(1), solution.getEntityList().get(2), sum12, concat12), - assertMatch(solution.getFirstEntity(), solution.getEntityList().get(2), sum02, concat02)); + assertMatch(solution.getFirstEntity(), solution.getEntityList().get(1), sum01, concat01) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(1)), + assertMatch(solution.getEntityList().get(1), solution.getEntityList().get(2), sum12, concat12) + .withIndictedObjects(solution.getEntityList().get(1), solution.getEntityList().get(2)), + assertMatch(solution.getFirstEntity(), solution.getEntityList().get(2), sum02, concat02) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(2))); var entity = solution.getFirstEntity(); @@ -1986,7 +2056,8 @@ public void expandToQuad() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), solution.getEntityList().get(1), sum12, concat12)); + assertMatch(solution.getFirstEntity(), solution.getEntityList().get(1), sum12, concat12) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(1))); } @Override @@ -2007,9 +2078,9 @@ public void flatten() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, group1), - assertMatch(entity1, entity2, group1), - assertMatch(entity1, entity2, group2)); + assertMatch(entity1, entity2, group1).withIndictedObjects(entity1, entity2), + assertMatch(entity1, entity2, group1).withIndictedObjects(entity1, entity2), + assertMatch(entity1, entity2, group2).withIndictedObjects(entity1, entity2)); // Incremental scoreDirector.beforeEntityRemoved(entity1); @@ -2033,27 +2104,28 @@ public void flattenLastWithDuplicates() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); - // From scratch + // From scratch: flattenLast produces multiple tuples per pair, indicted objects are from original pair scoreDirector.setWorkingSolution(solution); + var entity3 = solution.getEntityList().get(2); assertScore(scoreDirector, - assertMatch(entity1, group1), - assertMatch(entity1, group1), - assertMatch(entity1, group2), - assertMatch(entity2, group2), - assertMatch(entity2, group1), - assertMatch(entity2, group2), - assertMatch(entity1, group1), - assertMatch(entity1, group1), - assertMatch(entity1, group2)); + assertMatch(entity1, group1).withIndictedObjects(entity1, entity2), + assertMatch(entity1, group1).withIndictedObjects(entity1, entity2), + assertMatch(entity1, group2).withIndictedObjects(entity1, entity2), + assertMatch(entity1, group1).withIndictedObjects(entity1, entity3), + assertMatch(entity1, group1).withIndictedObjects(entity1, entity3), + assertMatch(entity1, group2).withIndictedObjects(entity1, entity3), + assertMatch(entity2, group1).withIndictedObjects(entity2, entity3), + assertMatch(entity2, group1).withIndictedObjects(entity2, entity3), + assertMatch(entity2, group2).withIndictedObjects(entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatch(entity2, group2), - assertMatch(entity2, group1), - assertMatch(entity2, group2)); + assertMatch(entity2, group1).withIndictedObjects(entity2, entity3), + assertMatch(entity2, group1).withIndictedObjects(entity2, entity3), + assertMatch(entity2, group2).withIndictedObjects(entity2, entity3)); } @Override @@ -2071,19 +2143,20 @@ public void flattenLastWithoutDuplicates() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); - // From scratch + // From scratch: flattenLast produces one tuple per pair, indicted objects are from original pair scoreDirector.setWorkingSolution(solution); + var entity3 = solution.getEntityList().get(2); assertScore(scoreDirector, - assertMatch(entity2, group1), - assertMatch(entity1, group2), - assertMatch(entity1, group1)); + assertMatch(entity1, group2).withIndictedObjects(entity1, entity2), + assertMatch(entity1, group1).withIndictedObjects(entity1, entity3), + assertMatch(entity2, group1).withIndictedObjects(entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatch(entity2, group1)); + assertMatch(entity2, group1).withIndictedObjects(entity2, entity3)); } @Override @@ -2102,21 +2175,22 @@ public void flattenLastAndDistinctWithDuplicates() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); - // From scratch + // From scratch: flattenLast + distinct produces unique tuples, indicted from original pairs scoreDirector.setWorkingSolution(solution); + var entity3 = solution.getEntityList().get(2); assertScore(scoreDirector, - assertMatch(entity1, group1), - assertMatch(entity1, group2), - assertMatch(entity2, group2), - assertMatch(entity2, group1)); + assertMatch(entity1, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, group1).withIndictedObjects(entity2, entity3), + assertMatch(entity2, group2).withIndictedObjects(entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatch(entity2, group1), - assertMatch(entity2, group2)); + assertMatch(entity2, group1).withIndictedObjects(entity2, entity3), + assertMatch(entity2, group2).withIndictedObjects(entity2, entity3)); } @Override @@ -2135,19 +2209,20 @@ public void flattenLastAndDistinctWithoutDuplicates() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); - // From scratch + // From scratch: flattenLast + distinct produces unique tuples, indicted from original pairs scoreDirector.setWorkingSolution(solution); + var entity3 = solution.getEntityList().get(2); assertScore(scoreDirector, - assertMatch(entity2, group1), - assertMatch(entity1, group2), - assertMatch(entity1, group1)); + assertMatch(entity1, group2).withIndictedObjects(entity1, entity2), + assertMatch(entity1, group1).withIndictedObjects(entity1, entity3), + assertMatch(entity2, group1).withIndictedObjects(entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatch(entity2, group1)); + assertMatch(entity2, group1).withIndictedObjects(entity2, entity3)); } @Override @@ -2180,8 +2255,8 @@ public void concatUniWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2), - assertMatch(entity2, null)); + assertMatch(entity1, entity2).withIndictedObjects(entity1, entity2), + assertMatch(entity2, null).withIndictedObjects(entity2)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2192,8 +2267,8 @@ public void concatUniWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3), - assertMatch(entity3, null)); + assertMatch(entity1, entity3).withIndictedObjects(entity1, entity3), + assertMatch(entity3, null).withIndictedObjects(entity3)); } @Override @@ -2227,8 +2302,8 @@ public void concatAndDistinctUniWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2), - assertMatch(entity2, null)); + assertMatch(entity1, entity2).withIndictedObjects(entity1, entity2), + assertMatch(entity2, null).withIndictedObjects(entity2)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2239,8 +2314,8 @@ public void concatAndDistinctUniWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3), - assertMatch(entity3, null)); + assertMatch(entity1, entity3).withIndictedObjects(entity1, entity3), + assertMatch(entity3, null).withIndictedObjects(entity3)); } @Override @@ -2275,8 +2350,8 @@ public void concatBiWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2), - assertMatch(entity2, entity3)); + assertMatch(entity1, entity2).withIndictedObjects(entity1, entity2), + assertMatch(entity2, entity3).withIndictedObjects(entity2, entity3)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2287,8 +2362,8 @@ public void concatBiWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3), - assertMatch(entity3, entity2)); + assertMatch(entity1, entity3).withIndictedObjects(entity1, entity3), + assertMatch(entity3, entity2).withIndictedObjects(entity3, entity2)); } @Override @@ -2323,8 +2398,8 @@ public void concatBiWithValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2), - assertMatch(entity1, entity2)); + assertMatch(entity1, entity2).withIndictedObjects(entity1, entity2), + assertMatch(entity1, entity2).withIndictedObjects(entity1, entity2)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2335,8 +2410,8 @@ public void concatBiWithValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3), - assertMatch(entity1, entity3)); + assertMatch(entity1, entity3).withIndictedObjects(entity1, entity3), + assertMatch(entity1, entity3).withIndictedObjects(entity1, entity3)); } @Override @@ -2372,8 +2447,8 @@ public void concatAndDistinctBiWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2), - assertMatch(entity2, entity3)); + assertMatch(entity1, entity2).withIndictedObjects(entity1, entity2), + assertMatch(entity2, entity3).withIndictedObjects(entity2, entity3)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2384,8 +2459,8 @@ public void concatAndDistinctBiWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3), - assertMatch(entity3, entity2)); + assertMatch(entity1, entity3).withIndictedObjects(entity1, entity3), + assertMatch(entity3, entity2).withIndictedObjects(entity3, entity2)); } @Override @@ -2421,7 +2496,7 @@ public void concatAndDistinctBiWithValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2)); + assertMatch(entity1, entity2).withIndictedObjects(entity1, entity2)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2432,7 +2507,7 @@ public void concatAndDistinctBiWithValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3)); + assertMatch(entity1, entity3).withIndictedObjects(entity1, entity3)); } @Override @@ -2469,8 +2544,8 @@ public void concatTriWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, null), - assertMatch(entity2, entity3, entity1)); + assertMatch(entity1, entity2, null).withIndictedObjects(entity1, entity2), + assertMatch(entity2, entity3, entity1).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2481,8 +2556,8 @@ public void concatTriWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, null), - assertMatch(entity3, entity2, entity1)); + assertMatch(entity1, entity3, null).withIndictedObjects(entity1, entity3), + assertMatch(entity3, entity2, entity1).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -2520,8 +2595,8 @@ public void concatAndDistinctTriWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, null), - assertMatch(entity2, entity3, entity1)); + assertMatch(entity1, entity2, null).withIndictedObjects(entity1, entity2), + assertMatch(entity2, entity3, entity1).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2532,8 +2607,8 @@ public void concatAndDistinctTriWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, null), - assertMatch(entity3, entity2, entity1)); + assertMatch(entity1, entity3, null).withIndictedObjects(entity1, entity3), + assertMatch(entity3, entity2, entity1).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -2572,8 +2647,8 @@ public void concatQuadWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, null, null), - assertMatch(entity2, entity3, entity1, entity2)); + assertMatch(entity1, entity2, null, null).withIndictedObjects(entity1, entity2), + assertMatch(entity2, entity3, entity1, entity2).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2584,8 +2659,8 @@ public void concatQuadWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, null, null), - assertMatch(entity3, entity2, entity1, entity3)); + assertMatch(entity1, entity3, null, null).withIndictedObjects(entity1, entity3), + assertMatch(entity3, entity2, entity1, entity3).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -2625,8 +2700,8 @@ public void concatAndDistinctQuadWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, null, null), - assertMatch(entity2, entity3, entity1, entity2)); + assertMatch(entity1, entity2, null, null).withIndictedObjects(entity1, entity2), + assertMatch(entity2, entity3, entity1, entity2).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2637,8 +2712,8 @@ public void concatAndDistinctQuadWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, null, null), - assertMatch(entity3, entity2, entity1, entity3)); + assertMatch(entity1, entity3, null, null).withIndictedObjects(entity1, entity3), + assertMatch(entity3, entity2, entity1, entity3).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -2677,15 +2752,15 @@ public void concatAfterGroupBy() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, value1, value2, 1L), - assertMatchWithScore(-1, value2, value3, 1L)); + assertMatchWithScore(-1, value1, value2, 1L).withIndictedObjects(entity1, entity2), + assertMatchWithScore(-1, value2, value3, 1L).withIndictedObjects(entity2, entity3)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); entity3.setValue(value2); scoreDirector.afterVariableChanged(entity3, "value"); assertScore(scoreDirector, - assertMatchWithScore(-2, value1, value2, 2L)); + assertMatchWithScore(-2, value1, value2, 2L).withIndictedObjects(entity1, entity2, entity3)); // Incremental for which the first change matches a join that doesn't survive the second change scoreDirector.beforeVariableChanged(entity1, "value"); @@ -2695,8 +2770,8 @@ public void concatAfterGroupBy() { entity3.setValue(value1); scoreDirector.afterVariableChanged(entity3, "value"); assertScore(scoreDirector, - assertMatchWithScore(-1, value1, value2, 1L), - assertMatchWithScore(-1, value2, value3, 1L)); + assertMatchWithScore(-1, value1, value2, 1L).withIndictedObjects(entity3, entity2), + assertMatchWithScore(-1, value2, value3, 1L).withIndictedObjects(entity2, entity1)); } @Override @@ -2729,18 +2804,18 @@ public void complement() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, 0), - assertMatch(entity2, Integer.MAX_VALUE), - assertMatch(entity3, Integer.MAX_VALUE)); + assertMatch(entity1, 0).withIndictedObjects(entity1), + assertMatch(entity2, Integer.MAX_VALUE).withIndictedObjects(entity2), + assertMatch(entity3, Integer.MAX_VALUE).withIndictedObjects(entity3)); // Incremental; all entities are still present, but the indexes are different. scoreDirector.beforeVariableChanged(entity2, "value"); entity2.setValue(value1); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, 0), - assertMatch(entity2, 0), - assertMatch(entity3, Integer.MAX_VALUE)); + assertMatch(entity1, 0).withIndictedObjects(entity1), + assertMatch(entity2, 0).withIndictedObjects(entity2), + assertMatch(entity3, Integer.MAX_VALUE).withIndictedObjects(entity3)); } @Override @@ -3222,15 +3297,15 @@ public void joinerEqualsAndSameness() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, // Each entity's BigDecimal property is joined to itself. - assertMatch(entity1, entity1), - assertMatch(entity2, entity2), - assertMatch(entity3, entity3), + assertMatch(entity1, entity1).withIndictedObjects(entity1), + assertMatch(entity2, entity2).withIndictedObjects(entity2), + assertMatch(entity3, entity3).withIndictedObjects(entity3), // Each entity's BigDecimal property is joined to each other entity's. - assertMatch(entity1, entity2), - assertMatch(entity1, entity3), - assertMatch(entity2, entity1), - assertMatch(entity2, entity3), - assertMatch(entity3, entity1), - assertMatch(entity3, entity2)); + assertMatch(entity1, entity2).withIndictedObjects(entity1, entity2), + assertMatch(entity1, entity3).withIndictedObjects(entity1, entity3), + assertMatch(entity2, entity1).withIndictedObjects(entity2, entity1), + assertMatch(entity2, entity3).withIndictedObjects(entity2, entity3), + assertMatch(entity3, entity1).withIndictedObjects(entity3, entity1), + assertMatch(entity3, entity2).withIndictedObjects(entity3, entity2)); } } From fedde0d4f00ad04fe7c8b01c7bfcb1f0ca094d06 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 21 Jul 2026 17:37:25 -0400 Subject: [PATCH 10/30] test: make TriConstraintStream tests indictment aware --- .../tri/AbstractTriConstraintStreamTest.java | 379 ++++++++++-------- 1 file changed, 209 insertions(+), 170 deletions(-) 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 90724a2057f..d1d79aef37c 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 @@ -129,7 +129,7 @@ public void filter_consecutive() { // From scratch scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, assertMatch(entity4, entity5, entity4)); + assertScore(scoreDirector, assertMatch(entity4, entity5, entity4).withIndictedObjects(entity4, entity5)); // Remove entity scoreDirector.beforeEntityRemoved(entity4); @@ -328,24 +328,24 @@ public void joinAfterGroupBy() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(1L, 1L, 1L, extra1), - assertMatch(1L, 1L, 1L, extra2)); + assertMatch(1L, 1L, 1L, extra1).withIndictedObjects(entity1, entity2, extra1), + assertMatch(1L, 1L, 1L, extra2).withIndictedObjects(entity1, entity2, extra2)); // Incremental scoreDirector.beforeVariableChanged(entity2, "value"); entity2.setValue(value2); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(2L, 2L, 2L, extra1), - assertMatch(2L, 2L, 2L, extra2)); + assertMatch(2L, 2L, 2L, extra1).withIndictedObjects(entity1, entity2, extra1), + assertMatch(2L, 2L, 2L, extra2).withIndictedObjects(entity1, entity2, extra2)); // Incremental scoreDirector.beforeEntityRemoved(entity2); solution.getEntityList().remove(entity2); scoreDirector.afterEntityRemoved(entity2); assertScore(scoreDirector, - assertMatch(1L, 1L, 1L, extra1), - assertMatch(1L, 1L, 1L, extra2)); + assertMatch(1L, 1L, 1L, extra1).withIndictedObjects(entity1, extra1), + assertMatch(1L, 1L, 1L, extra2).withIndictedObjects(entity1, extra2)); } @Override @@ -753,21 +753,21 @@ public void ifExistsAfterGroupBy() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(1L, 1L, 1L)); + assertMatch(1L, 1L, 1L).withIndictedObjects(entity1, entity2)); // Incremental scoreDirector.beforeVariableChanged(entity2, "value"); entity2.setValue(value2); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(2L, 2L, 2L)); + assertMatch(2L, 2L, 2L).withIndictedObjects(entity1, entity2)); // Incremental scoreDirector.beforeEntityRemoved(entity2); solution.getEntityList().remove(entity2); scoreDirector.afterEntityRemoved(entity2); assertScore(scoreDirector, - assertMatch(1L, 1L, 1L)); + assertMatch(1L, 1L, 1L).withIndictedObjects(entity1)); } @Override @@ -783,18 +783,26 @@ public void groupBy_0Mapping1Collector() { .penalize(SimpleScore.ONE, count -> count) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); + var entityGroup1 = solution.getFirstEntityGroup(); + var entityGroup2 = solution.getEntityGroupList().get(1); + var value1 = solution.getFirstValue(); + var value2 = solution.getValueList().get(1); + // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-3, 3L)); + assertMatchWithScore(-3, 3L).withIndictedObjects(entity, entity2, entity3, entityGroup1, entityGroup2, value1, + value2)); // Incremental - var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-2, 2L)); + assertMatchWithScore(-2, 2L).withIndictedObjects(entity2, entity3, entityGroup1, entityGroup2, value1, value2)); } @Override @@ -810,16 +818,18 @@ public void groupBy_0Mapping2Collector() { .asConstraint(TEST_CONSTRAINT_ID)); var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, assertMatchWithScore(-1, 3L, 2L)); + assertScore(scoreDirector, assertMatchWithScore(-1, 3L, 2L).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); - assertScore(scoreDirector, assertMatchWithScore(-1, 1L, 1L)); + assertScore(scoreDirector, assertMatchWithScore(-1, 1L, 1L).withIndictedObjects(entity2, entity3)); } @Override @@ -847,14 +857,14 @@ public void groupBy_0Mapping3Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, 3L, 0L, 1L)); + assertMatchWithScore(-1, 3L, 0L, 1L).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, 1L, 1L, 1L)); + assertMatchWithScore(-1, 1L, 1L, 1L).withIndictedObjects(entity2, entity3)); } @Override @@ -883,14 +893,14 @@ public void groupBy_0Mapping4Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, 3L, 0L, 1L, asSet(entity1, entity2))); + assertMatchWithScore(-1, 3L, 0L, 1L, asSet(entity1, entity2)).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, 1L, 1L, 1L, asSet(entity2))); + assertMatchWithScore(-1, 1L, 1L, 1L, asSet(entity2)).withIndictedObjects(entity2, entity3)); } @Override @@ -906,13 +916,18 @@ public void groupBy_1Mapping0Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); + var entityGroup1 = solution.getFirstEntityGroup(); + var entityGroup2 = solution.getEntityGroupList().get(1); var value1 = solution.getFirstValue(); var value2 = solution.getValueList().get(1); scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, value2), - assertMatchWithScore(-1, value1)); + assertMatchWithScore(-1, value2).withIndictedObjects(entity2, entityGroup2, value2), + assertMatchWithScore(-1, value1).withIndictedObjects(entity1, entity3, entityGroup1, value1)); } @Override @@ -928,23 +943,27 @@ public void groupBy_1Mapping1Collector() { .penalize(SimpleScore.ONE, (group, count) -> count) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); + var entityGroup1 = solution.getFirstEntityGroup(); + var entityGroup2 = solution.getEntityGroupList().get(1); var value1 = solution.getFirstValue(); var value2 = solution.getValueList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, value2, 1L), - assertMatchWithScore(-2, value1, 2L)); + assertMatchWithScore(-1, value2, 1L).withIndictedObjects(entity2, entityGroup2, value2), + assertMatchWithScore(-2, value1, 2L).withIndictedObjects(entity, entity3, entityGroup1, value1)); // Incremental - var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, value2, 1L), - assertMatchWithScore(-1, value1, 1L)); + assertMatchWithScore(-1, value2, 1L).withIndictedObjects(entity2, entityGroup2, value2), + assertMatchWithScore(-1, value1, 1L).withIndictedObjects(entity3, entityGroup1, value1)); } @Override @@ -963,12 +982,14 @@ public void groupBy_1Mapping2Collector() { var entity1 = solution.getFirstEntity(); var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, entity1.toString(), 2L, singleton(entity1)), - assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2))); + assertMatchWithScore(-1, entity1.toString(), 2L, singleton(entity1)).withIndictedObjects(entity1, entity2, + entity3), + assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2)).withIndictedObjects(entity2, entity3)); // Incremental var entity = solution.getFirstEntity(); @@ -976,7 +997,7 @@ public void groupBy_1Mapping2Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2))); + assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2)).withIndictedObjects(entity2, entity3)); } @Override @@ -1000,14 +1021,15 @@ public void groupBy_1Mapping3Collector() { entity1.setLongProperty(Long.MAX_VALUE); var entity2 = solution.getEntityList().get(1); entity2.setLongProperty(Long.MIN_VALUE); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, assertMatchWithScore(-1, entity1.toString(), Long.MAX_VALUE, Long.MAX_VALUE, - singleton(entity1)), + singleton(entity1)).withIndictedObjects(entity1, entity2, entity3), assertMatchWithScore(-1, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, - singleton(entity2))); + singleton(entity2)).withIndictedObjects(entity2, entity3)); // Incremental var entity = solution.getFirstEntity(); @@ -1016,7 +1038,7 @@ public void groupBy_1Mapping3Collector() { scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, assertMatchWithScore(-1, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, - singleton(entity2))); + singleton(entity2)).withIndictedObjects(entity2, entity3)); } @Override @@ -1031,6 +1053,9 @@ public void groupBy_2Mapping0Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var value1 = solution.getFirstValue(); @@ -1039,17 +1064,16 @@ public void groupBy_2Mapping0Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, value2), - assertMatchWithScore(-1, group1, value1)); + assertMatchWithScore(-1, group2, value2).withIndictedObjects(entity2, group2, value2), + assertMatchWithScore(-1, group1, value1).withIndictedObjects(entity, entity3, group1, value1)); // Incremental - var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, value2), - assertMatchWithScore(-1, group1, value1)); + assertMatchWithScore(-1, group2, value2).withIndictedObjects(entity2, group2, value2), + assertMatchWithScore(-1, group1, value1).withIndictedObjects(entity3, group1, value1)); } @Override @@ -1065,6 +1089,9 @@ public void groupBy_2Mapping1Collector() { .penalize(SimpleScore.ONE, (group, value, count) -> count) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var value1 = solution.getFirstValue(); @@ -1073,17 +1100,16 @@ public void groupBy_2Mapping1Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, value2, 1L), - assertMatchWithScore(-2, group1, value1, 2L)); + assertMatchWithScore(-1, group2, value2, 1L).withIndictedObjects(entity2, group2, value2), + assertMatchWithScore(-2, group1, value1, 2L).withIndictedObjects(entity, entity3, group1, value1)); // Incremental - var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, value2, 1L), - assertMatchWithScore(-1, group1, value1, 1L)); + assertMatchWithScore(-1, group2, value2, 1L).withIndictedObjects(entity2, group2, value2), + assertMatchWithScore(-1, group1, value1, 1L).withIndictedObjects(entity3, group1, value1)); } @Override @@ -1100,6 +1126,9 @@ public void groupBy_2Mapping2Collector() { (group, value, count, sameCount) -> count + sameCount) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var value1 = solution.getFirstValue(); @@ -1108,17 +1137,16 @@ public void groupBy_2Mapping2Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-2, group2, value2, 1L, 1L), - assertMatchWithScore(-4, group1, value1, 2L, 2L)); + assertMatchWithScore(-2, group2, value2, 1L, 1L).withIndictedObjects(entity2, group2, value2), + assertMatchWithScore(-4, group1, value1, 2L, 2L).withIndictedObjects(entity, entity3, group1, value1)); // Incremental - var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-2, group2, value2, 1L, 1L), - assertMatchWithScore(-2, group1, value1, 1L, 1L)); + assertMatchWithScore(-2, group2, value2, 1L, 1L).withIndictedObjects(entity2, group2, value2), + assertMatchWithScore(-2, group1, value1, 1L, 1L).withIndictedObjects(entity3, group1, value1)); } @Override @@ -1133,6 +1161,9 @@ public void groupBy_3Mapping0Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getEntityGroupList().get(0); var group2 = solution.getEntityGroupList().get(1); var group3 = solution.getEntityGroupList().get(2); @@ -1140,12 +1171,11 @@ public void groupBy_3Mapping0Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group1, group2, group3), - assertMatchWithScore(-1, group1, group3, group2), - assertMatchWithScore(-1, group2, group3, group1)); + assertMatchWithScore(-1, group1, group2, group3).withIndictedObjects(entity, entity2, entity3), + assertMatchWithScore(-1, group1, group3, group2).withIndictedObjects(entity, entity2, entity3), + assertMatchWithScore(-1, group2, group3, group1).withIndictedObjects(entity, entity2, entity3)); // Incremental - var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -1165,6 +1195,9 @@ public void groupBy_3Mapping1Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getEntityGroupList().get(0); var group2 = solution.getEntityGroupList().get(1); var group3 = solution.getEntityGroupList().get(2); @@ -1172,12 +1205,11 @@ public void groupBy_3Mapping1Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group1, group2, group3, 1L), - assertMatchWithScore(-1, group1, group3, group2, 1L), - assertMatchWithScore(-1, group2, group3, group1, 1L)); + assertMatchWithScore(-1, group1, group2, group3, 1L).withIndictedObjects(entity, entity2, entity3), + assertMatchWithScore(-1, group1, group3, group2, 1L).withIndictedObjects(entity, entity2, entity3), + assertMatchWithScore(-1, group2, group3, group1, 1L).withIndictedObjects(entity, entity2, entity3)); // Incremental - var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -1197,6 +1229,9 @@ public void groupBy_4Mapping0Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getEntityGroupList().get(0); var group2 = solution.getEntityGroupList().get(1); var group3 = solution.getEntityGroupList().get(2); @@ -1206,12 +1241,11 @@ public void groupBy_4Mapping0Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group1, group2, group3, value1), - assertMatchWithScore(-1, group1, group3, group2, value2), - assertMatchWithScore(-1, group2, group3, group1, value1)); + assertMatchWithScore(-1, group1, group2, group3, value1).withIndictedObjects(entity, entity2, entity3), + assertMatchWithScore(-1, group1, group3, group2, value2).withIndictedObjects(entity, entity2, entity3), + assertMatchWithScore(-1, group2, group3, group1, value1).withIndictedObjects(entity, entity2, entity3)); // Incremental - var entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); @@ -1252,17 +1286,18 @@ public void mapToUniWithDuplicates() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(asSet(group1, group2)), - assertMatch(asSet(group1, group2)), - assertMatch(asSet(group1, group2))); - - var entity = solution.getFirstEntity(); + assertMatch(asSet(group1, group2)).withIndictedObjects(entity, entity2, entity3), + assertMatch(asSet(group1, group2)).withIndictedObjects(entity, entity2, entity3), + assertMatch(asSet(group1, group2)).withIndictedObjects(entity, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1282,6 +1317,9 @@ public void mapToUniWithoutDuplicates() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var group3 = solution.getEntityGroupList().get(2); @@ -1289,11 +1327,9 @@ public void mapToUniWithoutDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(asSet(group1, group2)), - assertMatch(asSet(group1, group3)), - assertMatch(asSet(group2, group3))); - - var entity = solution.getFirstEntity(); + assertMatch(asSet(group1, group2)).withIndictedObjects(entity, entity2, entity3), + assertMatch(asSet(group1, group3)).withIndictedObjects(entity, entity2, entity3), + assertMatch(asSet(group2, group3)).withIndictedObjects(entity, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1314,15 +1350,16 @@ public void mapToUniAndDistinctWithDuplicates() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(asSet(group1, group2))); - - var entity = solution.getFirstEntity(); + assertMatch(asSet(group1, group2)).withIndictedObjects(entity, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1343,6 +1380,9 @@ public void mapToUniAndDistinctWithoutDuplicates() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var group3 = solution.getEntityGroupList().get(2); @@ -1350,11 +1390,9 @@ public void mapToUniAndDistinctWithoutDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(asSet(group1, group2)), - assertMatch(asSet(group1, group3)), - assertMatch(asSet(group2, group3))); - - var entity = solution.getFirstEntity(); + assertMatch(asSet(group1, group2)).withIndictedObjects(entity, entity2, entity3), + assertMatch(asSet(group1, group3)).withIndictedObjects(entity, entity2, entity3), + assertMatch(asSet(group2, group3)).withIndictedObjects(entity, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1375,6 +1413,9 @@ public void mapToBi() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var group3 = solution.getEntityGroupList().get(2); @@ -1382,11 +1423,9 @@ public void mapToBi() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1, group2), - assertMatch(group1, group3), - assertMatch(group2, group3)); - - var entity = solution.getFirstEntity(); + assertMatch(group1, group2).withIndictedObjects(entity, entity2, entity3), + assertMatch(group1, group3).withIndictedObjects(entity, entity2, entity3), + assertMatch(group2, group3).withIndictedObjects(entity, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1408,6 +1447,9 @@ public void mapToTri() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var group3 = solution.getEntityGroupList().get(2); @@ -1415,11 +1457,9 @@ public void mapToTri() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1, group2, group3), - assertMatch(group1, group3, group2), - assertMatch(group2, group3, group1)); - - var entity = solution.getFirstEntity(); + assertMatch(group1, group2, group3).withIndictedObjects(entity, entity2, entity3), + assertMatch(group1, group3, group2).withIndictedObjects(entity, entity2, entity3), + assertMatch(group2, group3, group1).withIndictedObjects(entity, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1442,6 +1482,9 @@ public void mapToQuad() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var group3 = solution.getEntityGroupList().get(2); @@ -1452,11 +1495,9 @@ public void mapToQuad() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(group1, group2, group3, sum), - assertMatch(group1, group3, group2, sum), - assertMatch(group2, group3, group1, sum)); - - var entity = solution.getFirstEntity(); + assertMatch(group1, group2, group3, sum).withIndictedObjects(entity, entity2, entity3), + assertMatch(group1, group3, group2, sum).withIndictedObjects(entity, entity2, entity3), + assertMatch(group2, group3, group1, sum).withIndictedObjects(entity, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -1486,16 +1527,14 @@ public void expandToQuad() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, sum), - assertMatch(entity1, entity3, entity2, sum), - assertMatch(entity2, entity3, entity1, sum)); - - var entity = solution.getFirstEntity(); + assertMatch(entity1, entity2, entity3, sum).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, entity2, sum).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, entity1, sum).withIndictedObjects(entity1, entity2, entity3)); // Incremental - scoreDirector.beforeEntityRemoved(entity); - solution.getEntityList().remove(entity); - scoreDirector.afterEntityRemoved(entity); + scoreDirector.beforeEntityRemoved(entity1); + solution.getEntityList().remove(entity1); + scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector); } @@ -1519,15 +1558,15 @@ public void flatten() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, group1), - assertMatch(entity1, entity2, entity3, group2), - assertMatch(entity1, entity2, entity3, group1), - assertMatch(entity1, entity3, entity2, group1), - assertMatch(entity1, entity3, entity2, group1), - assertMatch(entity1, entity3, entity2, group2), - assertMatch(entity2, entity3, entity1, group2), - assertMatch(entity2, entity3, entity1, group1), - assertMatch(entity2, entity3, entity1, group1)); + assertMatch(entity1, entity2, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity2, entity3, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity2, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, entity2, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, entity2, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, entity2, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, entity1, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, entity1, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, entity1, group1).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); @@ -1556,15 +1595,15 @@ public void flattenLastWithDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, group1), - assertMatch(entity1, entity2, group1), - assertMatch(entity1, entity2, group2), - assertMatch(entity1, entity3, group1), - assertMatch(entity1, entity3, group2), - assertMatch(entity1, entity3, group2), - assertMatch(entity2, entity3, group1), - assertMatch(entity2, entity3, group1), - assertMatch(entity2, entity3, group2)); + assertMatch(entity1, entity2, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity2, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity2, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, group2).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); @@ -1593,12 +1632,12 @@ public void flattenLastWithoutDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, group1), - assertMatch(entity1, entity2, group2), - assertMatch(entity1, entity3, group1), - assertMatch(entity1, entity3, group2), - assertMatch(entity2, entity3, group1), - assertMatch(entity2, entity3, group2)); + assertMatch(entity1, entity2, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity2, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, group2).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); @@ -1628,12 +1667,12 @@ public void flattenLastAndDistinctWithDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, group1), - assertMatch(entity1, entity2, group2), - assertMatch(entity1, entity3, group1), - assertMatch(entity1, entity3, group2), - assertMatch(entity2, entity3, group1), - assertMatch(entity2, entity3, group2)); + assertMatch(entity1, entity2, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity2, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, group2).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); @@ -1663,12 +1702,12 @@ public void flattenLastAndDistinctWithoutDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, group1), - assertMatch(entity1, entity2, group2), - assertMatch(entity1, entity3, group1), - assertMatch(entity1, entity3, group2), - assertMatch(entity2, entity3, group1), - assertMatch(entity2, entity3, group2)); + assertMatch(entity1, entity2, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity2, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, group2).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); @@ -1709,8 +1748,8 @@ public void concatUniWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3), - assertMatch(entity2, null, null)); + assertMatch(entity1, entity2, entity3).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, null, null).withIndictedObjects(entity2)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1721,8 +1760,8 @@ public void concatUniWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2), - assertMatch(entity3, null, null)); + assertMatch(entity1, entity3, entity2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity3, null, null).withIndictedObjects(entity3)); } @Override @@ -1758,8 +1797,8 @@ public void concatAndDistinctUniWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3), - assertMatch(entity2, null, null)); + assertMatch(entity1, entity2, entity3).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, null, null).withIndictedObjects(entity2)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1770,8 +1809,8 @@ public void concatAndDistinctUniWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2), - assertMatch(entity3, null, null)); + assertMatch(entity1, entity3, entity2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity3, null, null).withIndictedObjects(entity3)); } @Override @@ -1808,8 +1847,8 @@ public void concatBiWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3), - assertMatch(entity2, entity3, null)); + assertMatch(entity1, entity2, entity3).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, null).withIndictedObjects(entity2, entity3)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1820,8 +1859,8 @@ public void concatBiWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2), - assertMatch(entity3, entity2, null)); + assertMatch(entity1, entity3, entity2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity3, entity2, null).withIndictedObjects(entity3, entity2)); } @Override @@ -1859,8 +1898,8 @@ public void concatAndDistinctBiWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3), - assertMatch(entity2, entity3, null)); + assertMatch(entity1, entity2, entity3).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, null).withIndictedObjects(entity2, entity3)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1871,8 +1910,8 @@ public void concatAndDistinctBiWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2), - assertMatch(entity3, entity2, null)); + assertMatch(entity1, entity3, entity2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity3, entity2, null).withIndictedObjects(entity3, entity2)); } @Override @@ -2121,8 +2160,8 @@ public void concatQuadWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, null), - assertMatch(entity2, entity3, entity1, entity2)); + assertMatch(entity1, entity2, entity3, null).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, entity1, entity2).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2133,8 +2172,8 @@ public void concatQuadWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2, null), - assertMatch(entity3, entity2, entity1, entity3)); + assertMatch(entity1, entity3, entity2, null).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity3, entity2, entity1, entity3).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -2176,8 +2215,8 @@ public void concatAndDistinctQuadWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, null), - assertMatch(entity2, entity3, entity1, entity2)); + assertMatch(entity1, entity2, entity3, null).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, entity1, entity2).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2188,8 +2227,8 @@ public void concatAndDistinctQuadWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2, null), - assertMatch(entity3, entity2, entity1, entity3)); + assertMatch(entity1, entity3, entity2, null).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity3, entity2, entity1, entity3).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -2233,8 +2272,8 @@ public void concatAfterGroupBy() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, value1, value2, value3, 1L), - assertMatchWithScore(-1, value3, value2, value1, 1L)); + assertMatchWithScore(-1, value1, value2, value3, 1L).withIndictedObjects(entity1, entity2, entity3), + assertMatchWithScore(-1, value3, value2, value1, 1L).withIndictedObjects(entity3, entity2, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -2250,8 +2289,8 @@ public void concatAfterGroupBy() { entity3.setValue(value1); scoreDirector.afterVariableChanged(entity3, "value"); assertScore(scoreDirector, - assertMatchWithScore(-1, value1, value2, value3, 1L), - assertMatchWithScore(-1, value3, value2, value1, 1L)); + assertMatchWithScore(-1, value1, value2, value3, 1L).withIndictedObjects(entity1, entity2, entity3), + assertMatchWithScore(-1, value3, value2, value1, 1L).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -2285,18 +2324,18 @@ public void complement() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, 0, 1), - assertMatch(entity2, Integer.MAX_VALUE, -1), - assertMatch(entity3, Integer.MAX_VALUE, -1)); + assertMatch(entity1, 0, 1).withIndictedObjects(entity1), + assertMatch(entity2, Integer.MAX_VALUE, -1).withIndictedObjects(entity2), + assertMatch(entity3, Integer.MAX_VALUE, -1).withIndictedObjects(entity3)); // Incremental; all entities are still present, but the indexes are different. scoreDirector.beforeVariableChanged(entity2, "value"); entity2.setValue(value1); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, 0, 1), - assertMatch(entity2, 0, 1), - assertMatch(entity3, Integer.MAX_VALUE, -1)); + assertMatch(entity1, 0, 1).withIndictedObjects(entity1), + assertMatch(entity2, 0, 1).withIndictedObjects(entity2), + assertMatch(entity3, Integer.MAX_VALUE, -1).withIndictedObjects(entity3)); } @Override From 46bb0f471c7ae1f8cf4b62c3d0eb2a76afbfc314 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 21 Jul 2026 18:37:51 -0400 Subject: [PATCH 11/30] test: Make BavetQuadConstraintStreamTest aware of indictments --- .../AbstractQuadConstraintStreamTest.java | 346 +++++++++++------- 1 file changed, 208 insertions(+), 138 deletions(-) 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 7673d675b98..eb8eb31b9c7 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 @@ -129,7 +129,7 @@ public void filter_consecutive() { // From scratch scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, assertMatch(entity4, entity5, entity4, entity4)); + assertScore(scoreDirector, assertMatch(entity4, entity5, entity4, entity4).withIndictedObjects(entity4, entity5)); // Remove entity scoreDirector.beforeEntityRemoved(entity4); @@ -175,7 +175,8 @@ public void ifExists_0Joiner0Filter() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, assertMatch(solution.getFirstEntity(), solution.getFirstEntityGroup(), solution.getFirstValue(), - solution.getFirstEntity())); + solution.getFirstEntity()).withIndictedObjects(solution.getFirstEntity(), + solution.getFirstEntityGroup(), solution.getFirstValue())); // Incremental scoreDirector.beforeProblemFactRemoved(valueGroup); @@ -343,7 +344,8 @@ public void ifNotExists_0Joiner0Filter() { scoreDirector.afterProblemFactRemoved(valueGroup); assertScore(scoreDirector, assertMatch(solution.getFirstEntity(), solution.getFirstEntityGroup(), solution.getFirstValue(), - solution.getFirstEntity())); + solution.getFirstEntity()).withIndictedObjects(solution.getFirstEntity(), + solution.getFirstEntityGroup(), solution.getFirstValue())); } @Override @@ -489,21 +491,21 @@ public void ifExistsAfterGroupBy() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(1L, 1L, 1L, 1L)); + assertMatch(1L, 1L, 1L, 1L).withIndictedObjects(entity1, entity2)); // Incremental scoreDirector.beforeVariableChanged(entity2, "value"); entity2.setValue(value2); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(2L, 2L, 2L, 2L)); + assertMatch(2L, 2L, 2L, 2L).withIndictedObjects(entity1, entity2)); // Incremental scoreDirector.beforeEntityRemoved(entity2); solution.getEntityList().remove(entity2); scoreDirector.afterEntityRemoved(entity2); assertScore(scoreDirector, - assertMatch(1L, 1L, 1L, 1L)); + assertMatch(1L, 1L, 1L, 1L).withIndictedObjects(entity1)); } @Override @@ -526,10 +528,17 @@ public void groupBy_0Mapping1Collector() { .penalize(SimpleScore.ONE, count -> count) .asConstraint(TEST_CONSTRAINT_ID)); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); + // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-5, 5L)); // E1 G1 V1 E1, E1 G1 V1 E3, E2 G2 V2 E2, E3 G1 V1 E1, E3 G1 V1 E3 + assertMatchWithScore(-5, 5L).withIndictedObjects( // E1 G1 V1 E1, E1 G1 V1 E3, E2 G2 V2 E2, E3 G1 V1 E1, E3 G1 V1 E3 + entity1, solution.getFirstEntityGroup(), solution.getFirstValue(), + entity2, solution.getEntityGroupList().get(1), solution.getValueList().get(1), + entity3)); // Incremental var entity = solution.getFirstEntity(); @@ -537,7 +546,9 @@ public void groupBy_0Mapping1Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-2, 2L)); // E2 G2 V2 E2, E3 G1 V1 E3 + assertMatchWithScore(-2, 2L).withIndictedObjects( // E2 G2 V2 E2, E3 G1 V1 E3 + entity2, solution.getEntityGroupList().get(1), solution.getValueList().get(1), + entity3, solution.getFirstEntityGroup(), solution.getFirstValue())); } @Override @@ -554,16 +565,18 @@ public void groupBy_0Mapping2Collector() { .asConstraint(TEST_CONSTRAINT_ID)); var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, assertMatchWithScore(-1, 3L, 2L)); + assertScore(scoreDirector, assertMatchWithScore(-1, 3L, 2L).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); - assertScore(scoreDirector, assertMatchWithScore(-1, 1L, 1L)); + assertScore(scoreDirector, assertMatchWithScore(-1, 1L, 1L).withIndictedObjects(entity2, entity3)); } @Override @@ -592,14 +605,14 @@ public void groupBy_0Mapping3Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, 3L, 0L, 1L)); + assertMatchWithScore(-1, 3L, 0L, 1L).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, 1L, 1L, 1L)); + assertMatchWithScore(-1, 1L, 1L, 1L).withIndictedObjects(entity2, entity3)); } @Override @@ -629,14 +642,14 @@ public void groupBy_0Mapping4Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, 3L, 0L, 1L, asSet(entity1, entity2))); + assertMatchWithScore(-1, 3L, 0L, 1L, asSet(entity1, entity2)).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, 1L, 1L, 1L, asSet(entity2))); + assertMatchWithScore(-1, 1L, 1L, 1L, asSet(entity2)).withIndictedObjects(entity2, entity3)); } @Override @@ -659,8 +672,10 @@ public void groupBy_1Mapping0Collector() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, value2), - assertMatchWithScore(-1, value1)); + assertMatchWithScore(-1, value2).withIndictedObjects(solution.getEntityList().get(1), + solution.getEntityGroupList().get(1), solution.getValueList().get(1)), + assertMatchWithScore(-1, value1).withIndictedObjects(solution.getFirstEntity(), solution.getFirstEntityGroup(), + solution.getFirstValue(), solution.getEntityList().get(2))); } @Override @@ -678,14 +693,19 @@ public void groupBy_1Mapping1Collector() { .penalize(SimpleScore.ONE, (group, count) -> count) .asConstraint(TEST_CONSTRAINT_ID)); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var value1 = solution.getFirstValue(); var value2 = solution.getValueList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, value2, 1L), - assertMatchWithScore(-4, value1, 4L)); + assertMatchWithScore(-1, value2, 1L).withIndictedObjects(entity2, + solution.getEntityGroupList().get(1), solution.getValueList().get(1)), + assertMatchWithScore(-4, value1, 4L).withIndictedObjects(entity1, solution.getFirstEntityGroup(), value1, + entity3)); // Incremental var entity = solution.getFirstEntity(); @@ -693,8 +713,9 @@ public void groupBy_1Mapping1Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, value2, 1L), - assertMatchWithScore(-1, value1, 1L)); + assertMatchWithScore(-1, value2, 1L).withIndictedObjects(entity2, + solution.getEntityGroupList().get(1), solution.getValueList().get(1)), + assertMatchWithScore(-1, value1, 1L).withIndictedObjects(entity3, solution.getFirstEntityGroup(), value1)); } @Override @@ -714,12 +735,14 @@ public void groupBy_1Mapping2Collector() { var entity1 = solution.getFirstEntity(); var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, entity1.toString(), 2L, singleton(entity1)), - assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2))); + assertMatchWithScore(-1, entity1.toString(), 2L, singleton(entity1)).withIndictedObjects(entity1, entity2, + entity3), + assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2)).withIndictedObjects(entity2, entity3)); // Incremental var entity = solution.getFirstEntity(); @@ -727,7 +750,7 @@ public void groupBy_1Mapping2Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2))); + assertMatchWithScore(-1, entity2.toString(), 1L, singleton(entity2)).withIndictedObjects(entity2, entity3)); } @Override @@ -752,14 +775,15 @@ public void groupBy_1Mapping3Collector() { entity1.setLongProperty(Long.MAX_VALUE); var entity2 = solution.getEntityList().get(1); entity2.setLongProperty(Long.MIN_VALUE); + var entity3 = solution.getEntityList().get(2); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, assertMatchWithScore(-1, entity1.toString(), Long.MAX_VALUE, Long.MAX_VALUE, - singleton(entity1)), + singleton(entity1)).withIndictedObjects(entity1, entity2, entity3), assertMatchWithScore(-1, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, - singleton(entity2))); + singleton(entity2)).withIndictedObjects(entity2, entity3)); // Incremental var entity = solution.getFirstEntity(); @@ -768,7 +792,7 @@ public void groupBy_1Mapping3Collector() { scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, assertMatchWithScore(-1, entity2.toString(), Long.MIN_VALUE, Long.MIN_VALUE, - singleton(entity2))); + singleton(entity2)).withIndictedObjects(entity2, entity3)); } @Override @@ -785,6 +809,9 @@ public void groupBy_2Mapping0Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var value1 = solution.getFirstValue(); @@ -793,8 +820,9 @@ public void groupBy_2Mapping0Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, value2), - assertMatchWithScore(-1, group1, value1)); + assertMatchWithScore(-1, group2, value2).withIndictedObjects(entity2, + solution.getEntityGroupList().get(1), solution.getValueList().get(1)), + assertMatchWithScore(-1, group1, value1).withIndictedObjects(entity1, group1, value1, entity3)); // Incremental var entity = solution.getFirstEntity(); @@ -802,8 +830,9 @@ public void groupBy_2Mapping0Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, value2), - assertMatchWithScore(-1, group1, value1)); + assertMatchWithScore(-1, group2, value2).withIndictedObjects(entity2, + solution.getEntityGroupList().get(1), solution.getValueList().get(1)), + assertMatchWithScore(-1, group1, value1).withIndictedObjects(entity3, group1, value1)); } @Override @@ -822,6 +851,9 @@ public void groupBy_2Mapping1Collector() { .penalize(SimpleScore.ONE, (group, value, count) -> count) .asConstraint(TEST_CONSTRAINT_ID)); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var value1 = solution.getFirstValue(); @@ -830,8 +862,9 @@ public void groupBy_2Mapping1Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, value2, 1L), - assertMatchWithScore(-4, group1, value1, 4L)); + assertMatchWithScore(-1, group2, value2, 1L).withIndictedObjects(entity2, + solution.getEntityGroupList().get(1), solution.getValueList().get(1)), + assertMatchWithScore(-4, group1, value1, 4L).withIndictedObjects(entity1, group1, value1, entity3)); // Incremental var entity = solution.getFirstEntity(); @@ -839,8 +872,9 @@ public void groupBy_2Mapping1Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, value2, 1L), - assertMatchWithScore(-1, group1, value1, 1L)); + assertMatchWithScore(-1, group2, value2, 1L).withIndictedObjects(entity2, + solution.getEntityGroupList().get(1), solution.getValueList().get(1)), + assertMatchWithScore(-1, group1, value1, 1L).withIndictedObjects(entity3, group1, value1)); } @Override @@ -859,6 +893,9 @@ public void groupBy_2Mapping2Collector() { .penalize(SimpleScore.ONE, (group, value, count, sameCount) -> count + sameCount) .asConstraint(TEST_CONSTRAINT_ID)); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var value1 = solution.getFirstValue(); @@ -867,8 +904,8 @@ public void groupBy_2Mapping2Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-2, group2, value2, 1L, 1L), - assertMatchWithScore(-8, group1, value1, 4L, 4L)); + assertMatchWithScore(-2, group2, value2, 1L, 1L).withIndictedObjects(entity2, group2, value2), + assertMatchWithScore(-8, group1, value1, 4L, 4L).withIndictedObjects(entity1, group1, value1, entity3)); // Incremental var entity = solution.getFirstEntity(); @@ -876,8 +913,8 @@ public void groupBy_2Mapping2Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-2, group2, value2, 1L, 1L), - assertMatchWithScore(-2, group1, value1, 1L, 1L)); + assertMatchWithScore(-2, group2, value2, 1L, 1L).withIndictedObjects(entity2, group2, value2), + assertMatchWithScore(-2, group1, value1, 1L, 1L).withIndictedObjects(entity3, group1, value1)); } @Override @@ -897,6 +934,9 @@ public void groupBy_3Mapping0Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var value1 = solution.getFirstValue(); @@ -905,8 +945,9 @@ public void groupBy_3Mapping0Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, group2, value2), - assertMatchWithScore(-1, group1, group1, value1)); + assertMatchWithScore(-1, group2, group2, value2).withIndictedObjects(entity2, + solution.getEntityGroupList().get(1), solution.getValueList().get(1)), + assertMatchWithScore(-1, group1, group1, value1).withIndictedObjects(entity1, group1, value1, entity3)); // Incremental var entity = solution.getFirstEntity(); @@ -914,8 +955,9 @@ public void groupBy_3Mapping0Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, value2, value2), - assertMatchWithScore(-1, group1, value1, value1)); + assertMatchWithScore(-1, group2, value2, value2).withIndictedObjects(entity2, + solution.getEntityGroupList().get(1), solution.getValueList().get(1)), + assertMatchWithScore(-1, group1, value1, value1).withIndictedObjects(entity3, group1, value1)); } @Override @@ -935,14 +977,19 @@ public void groupBy_3Mapping1Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, group2, group2, 1L), - assertMatchWithScore(-1, group1, group1, group1, 4L)); + assertMatchWithScore(-1, group2, group2, group2, 1L).withIndictedObjects(entity2, + solution.getEntityGroupList().get(1), solution.getValueList().get(1)), + assertMatchWithScore(-1, group1, group1, group1, 4L).withIndictedObjects(entity1, group1, + solution.getFirstValue(), entity3)); // Incremental var entity = solution.getFirstEntity(); @@ -950,8 +997,10 @@ public void groupBy_3Mapping1Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, group2, group2, 1L), - assertMatchWithScore(-1, group1, group1, group1, 1L)); + assertMatchWithScore(-1, group2, group2, group2, 1L).withIndictedObjects(entity2, + solution.getEntityGroupList().get(1), solution.getValueList().get(1)), + assertMatchWithScore(-1, group1, group1, group1, 1L).withIndictedObjects(entity3, group1, + solution.getFirstValue())); } @Override @@ -972,6 +1021,9 @@ public void groupBy_4Mapping0Collector() { .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_ID)); + var entity1 = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); var value1 = solution.getFirstValue(); @@ -980,8 +1032,9 @@ public void groupBy_4Mapping0Collector() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, group2, group2, value2), - assertMatchWithScore(-1, group1, group1, group1, value1)); + assertMatchWithScore(-1, group2, group2, group2, value2).withIndictedObjects(entity2, + solution.getEntityGroupList().get(1), solution.getValueList().get(1)), + assertMatchWithScore(-1, group1, group1, group1, value1).withIndictedObjects(entity1, group1, value1, entity3)); // Incremental var entity = solution.getFirstEntity(); @@ -989,8 +1042,9 @@ public void groupBy_4Mapping0Collector() { solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatchWithScore(-1, group2, group2, group2, value2), - assertMatchWithScore(-1, group1, group1, group1, value1)); + assertMatchWithScore(-1, group2, group2, group2, value2).withIndictedObjects(entity2, + solution.getEntityGroupList().get(1), solution.getValueList().get(1)), + assertMatchWithScore(-1, group1, group1, group1, value1).withIndictedObjects(entity3, group1, value1)); } @Override @@ -1017,7 +1071,7 @@ public void distinct() { // On a distinct stream, this is a no-op. scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, assertMatch(entity1, entity2, group1, group2), - assertMatch(entity1, entity3, group1, group1), + assertMatch(entity1, entity3, group1, group1).withIndictedObjects(entity1, entity3, group1), assertMatch(entity2, entity3, group2, group1)); } @@ -1036,22 +1090,24 @@ public void mapToUniWithDuplicates() { .asConstraint(TEST_CONSTRAINT_ID)); var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(asSet(group1, group2)), - assertMatch(asSet(group1, group2)), - assertMatch(asSet(group1))); + assertMatch(asSet(group1, group2)).withIndictedObjects(entity, entity2, group1, group2), + assertMatch(asSet(group1, group2)).withIndictedObjects(entity2, entity3, group2, group1), + assertMatch(asSet(group1)).withIndictedObjects(entity, entity3, group1)); // Incremental scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(asSet(group1, group2))); + assertMatch(asSet(group1, group2)).withIndictedObjects(entity2, entity3, group2, group1)); } @Override @@ -1074,7 +1130,8 @@ public void mapToUniWithoutDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(asSet(group1, group2))); + assertMatch(asSet(group1, group2)).withIndictedObjects(solution.getFirstEntity(), + solution.getEntityList().get(1), group1, group2)); var entity = solution.getFirstEntity(); @@ -1101,21 +1158,23 @@ public void mapToUniAndDistinctWithDuplicates() { .asConstraint(TEST_CONSTRAINT_ID)); var entity = solution.getFirstEntity(); + var entity2 = solution.getEntityList().get(1); + var entity3 = solution.getEntityList().get(2); var group1 = solution.getFirstEntityGroup(); var group2 = solution.getEntityGroupList().get(1); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(asSet(group1, group2)), - assertMatch(asSet(group1))); + assertMatch(asSet(group1, group2)).withIndictedObjects(entity, entity2, group1, group2, entity3), + assertMatch(asSet(group1)).withIndictedObjects(entity, entity3, group1)); // Incremental scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, - assertMatch(asSet(group1, group2))); + assertMatch(asSet(group1, group2)).withIndictedObjects(entity2, entity3, group2, group1)); } @Override @@ -1139,7 +1198,8 @@ public void mapToUniAndDistinctWithoutDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(asSet(group1, group2))); + assertMatch(asSet(group1, group2)).withIndictedObjects(solution.getFirstEntity(), + solution.getEntityList().get(1), group1, group2)); var entity = solution.getFirstEntity(); @@ -1168,7 +1228,9 @@ public void mapToBi() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), solution.getEntityList().get(1))); + assertMatch(solution.getFirstEntity(), solution.getEntityList().get(1)).withIndictedObjects( + solution.getFirstEntity(), solution.getEntityList().get(1), solution.getFirstEntityGroup(), + solution.getEntityGroupList().get(1))); var entity = solution.getFirstEntity(); @@ -1199,7 +1261,9 @@ public void mapToTri() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, assertMatch(solution.getFirstEntity(), solution.getEntityList().get(1), - solution.getFirstEntity().getLongProperty() + solution.getEntityList().get(1).getLongProperty())); + solution.getFirstEntity().getLongProperty() + solution.getEntityList().get(1).getLongProperty()) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(1), + solution.getFirstEntityGroup(), solution.getEntityGroupList().get(1))); var entity = solution.getFirstEntity(); @@ -1232,7 +1296,9 @@ public void mapToQuad() { assertScore(scoreDirector, assertMatchWithScore(-1, solution.getFirstEntity().getCode(), solution.getEntityList().get(1).getCode(), solution.getFirstEntityGroup().getCode(), - solution.getEntityGroupList().get(1).getCode())); + solution.getEntityGroupList().get(1).getCode()) + .withIndictedObjects(solution.getFirstEntity(), solution.getEntityList().get(1), + solution.getFirstEntityGroup(), solution.getEntityGroupList().get(1))); var entity = solution.getFirstEntity(); @@ -1264,15 +1330,15 @@ public void flattenLastWithDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, group1), - assertMatch(entity1, entity2, entity3, group1), - assertMatch(entity1, entity2, entity3, group2), - assertMatch(entity1, entity3, entity2, group1), - assertMatch(entity1, entity3, entity2, group1), - assertMatch(entity1, entity3, entity2, group2), - assertMatch(entity2, entity3, entity1, group1), - assertMatch(entity2, entity3, entity1, group1), - assertMatch(entity2, entity3, entity1, group2)); + assertMatch(entity1, entity2, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity2, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity2, entity3, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, entity2, group1).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity1, entity3, entity2, group1).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity1, entity3, entity2, group2).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity2, entity3, entity1, group1).withIndictedObjects(entity2, entity3, entity1), + assertMatch(entity2, entity3, entity1, group1).withIndictedObjects(entity2, entity3, entity1), + assertMatch(entity2, entity3, entity1, group2).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeEntityRemoved(entity1); @@ -1302,12 +1368,12 @@ public void flattenLastWithoutDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, group1), - assertMatch(entity1, entity2, entity3, group2), - assertMatch(entity1, entity3, entity2, group1), - assertMatch(entity1, entity3, entity2, group2), - assertMatch(entity2, entity3, entity1, group1), - assertMatch(entity2, entity3, entity1, group2)); + assertMatch(entity1, entity2, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity2, entity3, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, entity2, group1).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity1, entity3, entity2, group2).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity2, entity3, entity1, group1).withIndictedObjects(entity2, entity3, entity1), + assertMatch(entity2, entity3, entity1, group2).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeEntityRemoved(entity1); @@ -1338,12 +1404,12 @@ public void flattenLastAndDistinctWithDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, group1), - assertMatch(entity1, entity2, entity3, group2), - assertMatch(entity1, entity3, entity2, group1), - assertMatch(entity1, entity3, entity2, group2), - assertMatch(entity2, entity3, entity1, group1), - assertMatch(entity2, entity3, entity1, group2)); + assertMatch(entity1, entity2, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity2, entity3, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, entity2, group1).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity1, entity3, entity2, group2).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity2, entity3, entity1, group1).withIndictedObjects(entity2, entity3, entity1), + assertMatch(entity2, entity3, entity1, group2).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeEntityRemoved(entity1); @@ -1374,12 +1440,12 @@ public void flattenLastAndDistinctWithoutDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, group1), - assertMatch(entity1, entity2, entity3, group2), - assertMatch(entity1, entity3, entity2, group1), - assertMatch(entity1, entity3, entity2, group2), - assertMatch(entity2, entity3, entity1, group1), - assertMatch(entity2, entity3, entity1, group2)); + assertMatch(entity1, entity2, entity3, group1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity2, entity3, group2).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity3, entity2, group1).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity1, entity3, entity2, group2).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity2, entity3, entity1, group1).withIndictedObjects(entity2, entity3, entity1), + assertMatch(entity2, entity3, entity1, group2).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeEntityRemoved(entity1); @@ -1422,8 +1488,8 @@ public void concatUniWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, entity1), - assertMatch(entity2, null, null, null)); + assertMatch(entity1, entity2, entity3, entity1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, null, null, null).withIndictedObjects(entity2)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1434,8 +1500,8 @@ public void concatUniWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2, entity1), - assertMatch(entity3, null, null, null)); + assertMatch(entity1, entity3, entity2, entity1).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity3, null, null, null).withIndictedObjects(entity3)); } @Override @@ -1473,8 +1539,8 @@ public void concatAndDistinctUniWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, entity1), - assertMatch(entity2, null, null, null)); + assertMatch(entity1, entity2, entity3, entity1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, null, null, null).withIndictedObjects(entity2)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1485,8 +1551,8 @@ public void concatAndDistinctUniWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2, entity1), - assertMatch(entity3, null, null, null)); + assertMatch(entity1, entity3, entity2, entity1).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity3, null, null, null).withIndictedObjects(entity3)); } @Override @@ -1525,8 +1591,8 @@ public void concatBiWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, entity1), - assertMatch(entity2, entity3, null, null)); + assertMatch(entity1, entity2, entity3, entity1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, null, null).withIndictedObjects(entity2, entity3)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1537,8 +1603,8 @@ public void concatBiWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2, entity1), - assertMatch(entity3, entity2, null, null)); + assertMatch(entity1, entity3, entity2, entity1).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity3, entity2, null, null).withIndictedObjects(entity3, entity2)); } @Override @@ -1578,8 +1644,8 @@ public void concatAndDistinctBiWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, entity1), - assertMatch(entity2, entity3, null, null)); + assertMatch(entity1, entity2, entity3, entity1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, null, null).withIndictedObjects(entity2, entity3)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1590,8 +1656,8 @@ public void concatAndDistinctBiWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2, entity1), - assertMatch(entity3, entity2, null, null)); + assertMatch(entity1, entity3, entity2, entity1).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity3, entity2, null, null).withIndictedObjects(entity3, entity2)); } @Override @@ -1632,8 +1698,8 @@ public void concatTriWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, entity1), - assertMatch(entity2, entity3, entity1, null)); + assertMatch(entity1, entity2, entity3, entity1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, entity1, null).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1644,8 +1710,8 @@ public void concatTriWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2, entity1), - assertMatch(entity3, entity2, entity1, null)); + assertMatch(entity1, entity3, entity2, entity1).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity3, entity2, entity1, null).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -1687,8 +1753,8 @@ public void concatAndDistinctTriWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, entity1), - assertMatch(entity2, entity3, entity1, null)); + assertMatch(entity1, entity2, entity3, entity1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, entity1, null).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1699,8 +1765,8 @@ public void concatAndDistinctTriWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2, entity1), - assertMatch(entity3, entity2, entity1, null)); + assertMatch(entity1, entity3, entity2, entity1).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity3, entity2, entity1, null).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -1743,8 +1809,8 @@ public void concatQuadWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, entity1), - assertMatch(entity2, entity3, entity1, entity2)); + assertMatch(entity1, entity2, entity3, entity1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, entity1, entity2).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1755,8 +1821,8 @@ public void concatQuadWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2, entity1), - assertMatch(entity3, entity2, entity1, entity3)); + assertMatch(entity1, entity3, entity2, entity1).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity3, entity2, entity1, entity3).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -1799,8 +1865,8 @@ public void concatQuadWithValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, entity1), - assertMatch(entity1, entity2, entity3, entity1)); + assertMatch(entity1, entity2, entity3, entity1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity1, entity2, entity3, entity1).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1811,8 +1877,8 @@ public void concatQuadWithValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2, entity1), - assertMatch(entity1, entity3, entity2, entity1)); + assertMatch(entity1, entity3, entity2, entity1).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity1, entity3, entity2, entity1).withIndictedObjects(entity1, entity3, entity2)); } @Override @@ -1856,8 +1922,8 @@ public void concatAndDistinctQuadWithoutValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, entity1), - assertMatch(entity2, entity3, entity1, entity2)); + assertMatch(entity1, entity2, entity3, entity1).withIndictedObjects(entity1, entity2, entity3), + assertMatch(entity2, entity3, entity1, entity2).withIndictedObjects(entity2, entity3, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1868,8 +1934,8 @@ public void concatAndDistinctQuadWithoutValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2, entity1), - assertMatch(entity3, entity2, entity1, entity3)); + assertMatch(entity1, entity3, entity2, entity1).withIndictedObjects(entity1, entity3, entity2), + assertMatch(entity3, entity2, entity1, entity3).withIndictedObjects(entity3, entity2, entity1)); } @Override @@ -1913,7 +1979,7 @@ public void concatAndDistinctQuadWithValueDuplicates() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, entity3, entity1)); + assertMatch(entity1, entity2, entity3, entity1).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1924,7 +1990,7 @@ public void concatAndDistinctQuadWithValueDuplicates() { entity2.setValue(value3); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity3, entity2, entity1)); + assertMatch(entity1, entity3, entity2, entity1).withIndictedObjects(entity1, entity3, entity2)); } @Override @@ -1971,8 +2037,10 @@ public void concatAfterGroupBy() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, value1, value2, value3.getCode() + value1.getCode(), 1L), - assertMatchWithScore(-1, value3, value2, value1.getCode() + value3.getCode(), 1L)); + assertMatchWithScore(-1, value1, value2, value3.getCode() + value1.getCode(), 1L).withIndictedObjects(entity1, + entity2, entity3), + assertMatchWithScore(-1, value3, value2, value1.getCode() + value3.getCode(), 1L).withIndictedObjects(entity3, + entity2, entity1)); // Incremental scoreDirector.beforeVariableChanged(entity3, "value"); @@ -1988,8 +2056,10 @@ public void concatAfterGroupBy() { entity3.setValue(value1); scoreDirector.afterVariableChanged(entity3, "value"); assertScore(scoreDirector, - assertMatchWithScore(-1, value1, value2, value3.getCode() + value1.getCode(), 1L), - assertMatchWithScore(-1, value3, value2, value1.getCode() + value3.getCode(), 1L)); + assertMatchWithScore(-1, value1, value2, value3.getCode() + value1.getCode(), 1L).withIndictedObjects(entity1, + entity2, entity3), + assertMatchWithScore(-1, value3, value2, value1.getCode() + value3.getCode(), 1L).withIndictedObjects(entity3, + entity2, entity1)); } @Override @@ -2024,18 +2094,18 @@ public void complement() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, 0, 1, 2), - assertMatch(entity2, Integer.MAX_VALUE, -1, 0), - assertMatch(entity3, Integer.MAX_VALUE, -1, 0)); + assertMatch(entity1, 0, 1, 2).withIndictedObjects(entity1), + assertMatch(entity2, Integer.MAX_VALUE, -1, 0).withIndictedObjects(entity2), + assertMatch(entity3, Integer.MAX_VALUE, -1, 0).withIndictedObjects(entity3)); // Incremental; all entities are still present, but the indexes are different. scoreDirector.beforeVariableChanged(entity2, "value"); entity2.setValue(value1); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, 0, 1, 2), - assertMatch(entity2, 0, 1, 2), - assertMatch(entity3, Integer.MAX_VALUE, -1, 0)); + assertMatch(entity1, 0, 1, 2).withIndictedObjects(entity1), + assertMatch(entity2, 0, 1, 2).withIndictedObjects(entity2), + assertMatch(entity3, Integer.MAX_VALUE, -1, 0).withIndictedObjects(entity3)); } @Override From b68f7afe6d85a659b991fcda7a93a13adb48421d Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 21 Jul 2026 23:41:36 -0400 Subject: [PATCH 12/30] test: make AdvanceGroupByTest aware of indictments --- ...ctAdvancedGroupByConstraintStreamTest.java | 152 ++++++++++++------ 1 file changed, 107 insertions(+), 45 deletions(-) diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractAdvancedGroupByConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractAdvancedGroupByConstraintStreamTest.java index 834c2a78217..cd30a4646b2 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractAdvancedGroupByConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractAdvancedGroupByConstraintStreamTest.java @@ -54,7 +54,7 @@ void collectedDowngradedAndFiltered() { // From scratch scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, assertMatch(new Pair<>("M", 1L))); + assertScore(scoreDirector, assertMatch(new Pair<>("M", 1L)).withIndictedObjects(entity)); // Incremental scoreDirector.beforeEntityRemoved(entity); @@ -85,7 +85,7 @@ void collectedAndFiltered() { // From scratch scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, assertMatchWithScore(-10, 10L)); + assertScore(scoreDirector, assertMatchWithScore(-10, 10L).withIndictedObjects(solution.getEntityList().toArray())); // Incremental Stream.of(entity1, entity2).forEach(entity -> { @@ -109,14 +109,14 @@ void collectedFilteredRecollected() { // From scratch scoreDirector.setWorkingSolution(solution); - assertScore(scoreDirector, assertMatchWithScore(-2, 2L)); + assertScore(scoreDirector, assertMatchWithScore(-2, 2L).withIndictedObjects(solution.getEntityList().toArray())); // Incremental TestdataLavishEntity entity = solution.getFirstEntity(); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); - assertScore(scoreDirector, assertMatchWithScore(-1, 1L)); + assertScore(scoreDirector, assertMatchWithScore(-1, 1L).withIndictedObjects(solution.getEntityList().toArray())); } @TestTemplate @@ -136,14 +136,15 @@ void uniGroupByRecollected() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-2, asSet(entity1.getEntityGroup(), entity2.getEntityGroup()))); + assertMatchWithScore(-2, asSet(entity1.getEntityGroup(), entity2.getEntityGroup())).withIndictedObjects(entity1, + entity2)); // Incremental scoreDirector.beforeEntityRemoved(entity1); solution.getEntityList().remove(entity1); scoreDirector.afterEntityRemoved(entity1); assertScore(scoreDirector, - assertMatchWithScore(-1, asSet(entity2.getEntityGroup()))); + assertMatchWithScore(-1, asSet(entity2.getEntityGroup())).withIndictedObjects(entity2)); } @TestTemplate @@ -163,7 +164,8 @@ void biGroupByRecollectedToList() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, assertMatchWithScore(-1, - Arrays.asList(solution.getFirstEntityGroup(), solution.getEntityGroupList().get(1)))); + Arrays.asList(solution.getFirstEntityGroup(), solution.getEntityGroupList().get(1))) + .withIndictedObjects(solution.getEntityList().toArray())); // Incremental TestdataLavishEntity entity = solution.getFirstEntity(); @@ -172,7 +174,8 @@ void biGroupByRecollectedToList() { scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, assertMatchWithScore(-1, - Arrays.asList(solution.getFirstEntityGroup(), solution.getEntityGroupList().get(1)))); + Arrays.asList(solution.getFirstEntityGroup(), solution.getEntityGroupList().get(1))) + .withIndictedObjects(solution.getEntityList().toArray())); } @TestTemplate @@ -192,7 +195,8 @@ void biGroupByRecollectedToMap() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, assertMatchWithScore(-1, - asMap(solution.getFirstEntityGroup(), 3L, solution.getEntityGroupList().get(1), 1L))); + asMap(solution.getFirstEntityGroup(), 3L, solution.getEntityGroupList().get(1), 1L)) + .withIndictedObjects(solution.getEntityList().toArray())); // Incremental TestdataLavishEntity entity = solution.getFirstEntity(); @@ -201,7 +205,8 @@ void biGroupByRecollectedToMap() { scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, assertMatchWithScore(-1, - asMap(solution.getFirstEntityGroup(), 1L, solution.getEntityGroupList().get(1), 1L))); + asMap(solution.getFirstEntityGroup(), 1L, solution.getEntityGroupList().get(1), 1L)) + .withIndictedObjects(solution.getEntityList().toArray())); } @TestTemplate @@ -224,16 +229,21 @@ void triGroupByRecollected() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, assertMatchWithScore(-1, - asMap(solution.getFirstEntityGroup(), 3L, solution.getEntityGroupList().get(1), 3L))); + asMap(solution.getFirstEntityGroup(), 3L, solution.getEntityGroupList().get(1), 3L)) + .withIndictedObjects(solution.getEntityList().toArray())); // Incremental TestdataLavishEntity entity = solution.getFirstEntity(); + TestdataLavishEntity group1Entity1 = solution.getEntityList().get(1); + TestdataLavishEntity group1Entity3 = solution.getEntityList().get(3); + TestdataLavishEntity group1Entity5 = solution.getEntityList().get(5); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, assertMatchWithScore(-1, - asMap(solution.getEntityGroupList().get(1), 3L))); + asMap(solution.getEntityGroupList().get(1), 3L)) + .withIndictedObjects(group1Entity1, group1Entity3, group1Entity5)); } @TestTemplate @@ -260,16 +270,22 @@ void quadGroupByRecollected() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, assertMatchWithScore(-1, - asMap(solution.getFirstEntityGroup(), 12L, solution.getEntityGroupList().get(1), 12L))); + asMap(solution.getFirstEntityGroup(), 12L, solution.getEntityGroupList().get(1), 12L)) + .withIndictedObjects(solution.getEntityList().toArray())); // Incremental TestdataLavishEntity entity = solution.getFirstEntity(); + TestdataLavishEntity group1Entity1 = solution.getEntityList().get(1); + TestdataLavishEntity group1Entity3 = solution.getEntityList().get(3); + TestdataLavishEntity group1Entity5 = solution.getEntityList().get(5); + TestdataLavishEntity group1Entity7 = solution.getEntityList().get(7); scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); assertScore(scoreDirector, assertMatchWithScore(-1, - asMap(solution.getEntityGroupList().get(1), 12L))); + asMap(solution.getEntityGroupList().get(1), 12L)) + .withIndictedObjects(group1Entity1, group1Entity3, group1Entity5, group1Entity7)); } @TestTemplate @@ -286,18 +302,21 @@ void biGroupByRegrouped() { TestdataLavishEntity entity = solution.getFirstEntity(); TestdataLavishEntity entity2 = solution.getEntityList().get(1); + TestdataLavishEntity entityInGroup0_2 = solution.getEntityList().get(2); + TestdataLavishEntity entityInGroup1_3 = solution.getEntityList().get(3); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, entity.getEntityGroup(), 1L), - assertMatchWithScore(-1, entity2.getEntityGroup(), 1L)); + assertMatchWithScore(-1, entity.getEntityGroup(), 1L).withIndictedObjects(entity, entityInGroup0_2), + assertMatchWithScore(-1, entity2.getEntityGroup(), 1L).withIndictedObjects(entity2, entityInGroup1_3)); // Incremental scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); - assertScore(scoreDirector, assertMatchWithScore(-1, entity2.getEntityGroup(), 1L)); + assertScore(scoreDirector, + assertMatchWithScore(-1, entity2.getEntityGroup(), 1L).withIndictedObjects(entity2, entityInGroup1_3)); } @TestTemplate @@ -318,18 +337,25 @@ void triGroupByRegrouped() { TestdataLavishEntity entity = solution.getFirstEntity(); TestdataLavishEntity entity2 = solution.getEntityList().get(1); + TestdataLavishEntity entityInGroup0_2 = solution.getEntityList().get(2); + TestdataLavishEntity entityInGroup0_4 = solution.getEntityList().get(4); + TestdataLavishEntity entityInGroup1_3 = solution.getEntityList().get(3); + TestdataLavishEntity entityInGroup1_5 = solution.getEntityList().get(5); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, entity.getEntityGroup(), 1L), - assertMatchWithScore(-1, entity2.getEntityGroup(), 1L)); + assertMatchWithScore(-1, entity.getEntityGroup(), 1L).withIndictedObjects(entity, entityInGroup0_2, + entityInGroup0_4), + assertMatchWithScore(-1, entity2.getEntityGroup(), 1L).withIndictedObjects(entity2, entityInGroup1_3, + entityInGroup1_5)); // Incremental scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); - assertScore(scoreDirector, assertMatchWithScore(-1, entity2.getEntityGroup(), 1L)); + assertScore(scoreDirector, assertMatchWithScore(-1, entity2.getEntityGroup(), 1L).withIndictedObjects(entity2, + entityInGroup1_3, entityInGroup1_5)); } @TestTemplate @@ -354,18 +380,27 @@ void quadGroupByRegrouped() { TestdataLavishEntity entity = solution.getFirstEntity(); TestdataLavishEntity entity2 = solution.getEntityList().get(1); + TestdataLavishEntity entityInGroup0_2 = solution.getEntityList().get(2); + TestdataLavishEntity entityInGroup0_4 = solution.getEntityList().get(4); + TestdataLavishEntity entityInGroup0_6 = solution.getEntityList().get(6); + TestdataLavishEntity entityInGroup1_3 = solution.getEntityList().get(3); + TestdataLavishEntity entityInGroup1_5 = solution.getEntityList().get(5); + TestdataLavishEntity entityInGroup1_7 = solution.getEntityList().get(7); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, entity.getEntityGroup(), 1L), - assertMatchWithScore(-1, entity2.getEntityGroup(), 1L)); + assertMatchWithScore(-1, entity.getEntityGroup(), 1L).withIndictedObjects(entity, entityInGroup0_2, + entityInGroup0_4, entityInGroup0_6), + assertMatchWithScore(-1, entity2.getEntityGroup(), 1L).withIndictedObjects(entity2, entityInGroup1_3, + entityInGroup1_5, entityInGroup1_7)); // Incremental scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); - assertScore(scoreDirector, assertMatchWithScore(-1, entity2.getEntityGroup(), 1L)); + assertScore(scoreDirector, assertMatchWithScore(-1, entity2.getEntityGroup(), 1L).withIndictedObjects(entity2, + entityInGroup1_3, entityInGroup1_5, entityInGroup1_7)); } @TestTemplate @@ -383,18 +418,23 @@ void biGroupByRegroupedDouble() { TestdataLavishEntity entity = solution.getFirstEntity(); TestdataLavishEntity entity2 = solution.getEntityList().get(1); + TestdataLavishEntity entityInGroup0_2 = solution.getEntityList().get(2); + TestdataLavishEntity entityInGroup1_3 = solution.getEntityList().get(3); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, new Object[] { entity.getEntityGroup().toString(), 1L }), - assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L })); + assertMatchWithScore(-1, new Object[] { entity.getEntityGroup().toString(), 1L }).withIndictedObjects(entity, + entityInGroup0_2), + assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L }).withIndictedObjects(entity2, + entityInGroup1_3)); // Incremental scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); - assertScore(scoreDirector, assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L })); + assertScore(scoreDirector, assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L }) + .withIndictedObjects(entity2, entityInGroup1_3)); } @TestTemplate @@ -417,18 +457,25 @@ void triGroupByRegroupedDouble() { TestdataLavishEntity entity = solution.getFirstEntity(); TestdataLavishEntity entity2 = solution.getEntityList().get(1); + TestdataLavishEntity entityInGroup0_2 = solution.getEntityList().get(2); + TestdataLavishEntity entityInGroup0_4 = solution.getEntityList().get(4); + TestdataLavishEntity entityInGroup1_3 = solution.getEntityList().get(3); + TestdataLavishEntity entityInGroup1_5 = solution.getEntityList().get(5); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, new Object[] { entity.getEntityGroup().toString(), 1L }), - assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L })); + assertMatchWithScore(-1, new Object[] { entity.getEntityGroup().toString(), 1L }).withIndictedObjects(entity, + entityInGroup0_2, entityInGroup0_4), + assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L }).withIndictedObjects(entity2, + entityInGroup1_3, entityInGroup1_5)); // Incremental scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); - assertScore(scoreDirector, assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L })); + assertScore(scoreDirector, assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L }) + .withIndictedObjects(entity2, entityInGroup1_3, entityInGroup1_5)); } @TestTemplate @@ -454,18 +501,27 @@ void quadGroupByRegroupedDouble() { TestdataLavishEntity entity = solution.getFirstEntity(); TestdataLavishEntity entity2 = solution.getEntityList().get(1); + TestdataLavishEntity entityInGroup0_2 = solution.getEntityList().get(2); + TestdataLavishEntity entityInGroup0_4 = solution.getEntityList().get(4); + TestdataLavishEntity entityInGroup0_6 = solution.getEntityList().get(6); + TestdataLavishEntity entityInGroup1_3 = solution.getEntityList().get(3); + TestdataLavishEntity entityInGroup1_5 = solution.getEntityList().get(5); + TestdataLavishEntity entityInGroup1_7 = solution.getEntityList().get(7); // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, new Object[] { entity.getEntityGroup().toString(), 1L }), - assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L })); + assertMatchWithScore(-1, new Object[] { entity.getEntityGroup().toString(), 1L }).withIndictedObjects(entity, + entityInGroup0_2, entityInGroup0_4, entityInGroup0_6), + assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L }).withIndictedObjects(entity2, + entityInGroup1_3, entityInGroup1_5, entityInGroup1_7)); // Incremental scoreDirector.beforeEntityRemoved(entity); solution.getEntityList().remove(entity); scoreDirector.afterEntityRemoved(entity); - assertScore(scoreDirector, assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L })); + assertScore(scoreDirector, assertMatchWithScore(-1, new Object[] { entity2.getEntityGroup().toString(), 1L }) + .withIndictedObjects(entity2, entityInGroup1_3, entityInGroup1_5, entityInGroup1_7)); } @TestTemplate @@ -491,15 +547,17 @@ void existsAfterGroupBy() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-2, solution.getFirstEntityGroup(), 2L), - assertMatchWithScore(-2, entityGroup1, 2L)); + assertMatchWithScore(-2, solution.getFirstEntityGroup(), 2L).withIndictedObjects(solution.getFirstEntity(), + entity3), + assertMatchWithScore(-2, entityGroup1, 2L).withIndictedObjects(entity1, entity2)); // Incremental scoreDirector.beforeProblemFactRemoved(entityGroup1); solution.getEntityGroupList().remove(entityGroup1); scoreDirector.afterProblemFactRemoved(entityGroup1); assertScore(scoreDirector, - assertMatchWithScore(-2, solution.getFirstEntityGroup(), 2L)); + assertMatchWithScore(-2, solution.getFirstEntityGroup(), 2L).withIndictedObjects(solution.getFirstEntity(), + entity3)); } @TestTemplate @@ -526,15 +584,17 @@ void groupByAfterExists() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-2, solution.getFirstEntityGroup(), 2L), - assertMatchWithScore(-2, entityGroup1, 2L)); + assertMatchWithScore(-2, solution.getFirstEntityGroup(), 2L).withIndictedObjects(solution.getFirstEntityGroup(), + solution.getFirstEntity(), entity3), + assertMatchWithScore(-2, entityGroup1, 2L).withIndictedObjects(entityGroup1, entity1, entity2)); // Incremental scoreDirector.beforeProblemFactRemoved(entityGroup1); solution.getEntityGroupList().remove(entityGroup1); scoreDirector.afterProblemFactRemoved(entityGroup1); assertScore(scoreDirector, - assertMatchWithScore(-2, solution.getFirstEntityGroup(), 2L)); + assertMatchWithScore(-2, solution.getFirstEntityGroup(), 2L).withIndictedObjects(solution.getFirstEntityGroup(), + solution.getFirstEntity(), entity3)); } @TestTemplate @@ -561,15 +621,17 @@ void groupByAfterExistsBi() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-3, solution.getFirstEntityGroup(), 3L), - assertMatchWithScore(-3, entityGroup1, 3L)); + assertMatchWithScore(-3, solution.getFirstEntityGroup(), 3L).withIndictedObjects(solution.getFirstEntity(), + entity1, entity2, entity3), + assertMatchWithScore(-3, entityGroup1, 3L).withIndictedObjects(entity1, entity2, entity3)); // Incremental scoreDirector.beforeProblemFactRemoved(entityGroup1); solution.getEntityGroupList().remove(entityGroup1); scoreDirector.afterProblemFactRemoved(entityGroup1); assertScore(scoreDirector, - assertMatchWithScore(-3, solution.getFirstEntityGroup(), 3L)); + assertMatchWithScore(-3, solution.getFirstEntityGroup(), 3L).withIndictedObjects(solution.getFirstEntity(), + entity1, entity2, entity3)); } @TestTemplate @@ -639,10 +701,10 @@ void reusedStreamsInJoin() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-1, entity1, entity1), - assertMatchWithScore(-1, entity1, entity3), - assertMatchWithScore(-1, entity3, entity1), - assertMatchWithScore(-1, entity3, entity3)); + assertMatchWithScore(-1, entity1, entity1).withIndictedObjects(entity1), + assertMatchWithScore(-1, entity1, entity3).withIndictedObjects(entity1, entity3), + assertMatchWithScore(-1, entity3, entity1).withIndictedObjects(entity3, entity1), + assertMatchWithScore(-1, entity3, entity3).withIndictedObjects(entity3)); } } From 0ef72f761e91a15d33f569754a4eee8aecb4ab98 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 21 Jul 2026 23:49:55 -0400 Subject: [PATCH 13/30] test: make BavetRegressionTest aware of indictments --- .../stream/bavet/BavetRegressionTest.java | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) 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 d9c6f6bc9e8..5348bf69fb2 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 @@ -337,8 +337,8 @@ void joinWithNullKeyFromRight() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity1), - assertMatch(entity2, entity2)); + assertMatch(entity1, entity1).withIndictedObjects(entity1), + assertMatch(entity2, entity2).withIndictedObjects(entity2)); // Switch entity1 and entity2 values; now entity2 has null and entity1 does not. scoreDirector.beforeVariableChanged(entity1, "value"); @@ -348,8 +348,8 @@ void joinWithNullKeyFromRight() { entity2.setValue(null); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity1), - assertMatch(entity2, entity2)); + assertMatch(entity1, entity1).withIndictedObjects(entity1), + assertMatch(entity2, entity2).withIndictedObjects(entity2)); // Put both to null. scoreDirector.beforeVariableChanged(entity1, "value"); @@ -359,10 +359,10 @@ void joinWithNullKeyFromRight() { entity2.setValue(null); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity1), - assertMatch(entity1, entity2), - assertMatch(entity2, entity1), - assertMatch(entity2, entity2)); + assertMatch(entity1, entity1).withIndictedObjects(entity1), + assertMatch(entity1, entity2).withIndictedObjects(entity1, entity2), + assertMatch(entity2, entity1).withIndictedObjects(entity1, entity2), + assertMatch(entity2, entity2).withIndictedObjects(entity2)); } @TestTemplate @@ -1518,7 +1518,7 @@ void filteringJoinNullConflict() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity2, entity2)); // Only entity1 is left, because forEach/join ignore nulls. + assertMatch(entity2, entity2).withIndictedObjects(entity2)); // Only entity1 is left, because forEach/join ignore nulls. // Switch entity1 and entity2 values; now entity2 has null and entity1 does not. scoreDirector.beforeVariableChanged(entity1, "value"); @@ -1528,7 +1528,7 @@ void filteringJoinNullConflict() { entity2.setValue(null); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity1)); + assertMatch(entity1, entity1).withIndictedObjects(entity1)); // Switch entity1 and entity2 values again to test the same from the other side. scoreDirector.beforeVariableChanged(entity1, "value"); @@ -1538,7 +1538,7 @@ void filteringJoinNullConflict() { entity2.setValue(value); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity2, entity2)); + assertMatch(entity2, entity2).withIndictedObjects(entity2)); } /** @@ -1680,7 +1680,7 @@ void filteringJoinNullConflictDifferentNodes() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity2, entity2)); + assertMatch(entity2, entity2).withIndictedObjects(entity2)); // Switch entity1 and entity2 values; now entity2 has null and entity1 does not. scoreDirector.beforeVariableChanged(entity1, "value"); @@ -1690,7 +1690,7 @@ void filteringJoinNullConflictDifferentNodes() { entity2.setValue(null); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity1, entity1)); + assertMatch(entity1, entity1).withIndictedObjects(entity1)); // Switch entity1 and entity2 values again to test the same from the other side. scoreDirector.beforeVariableChanged(entity1, "value"); @@ -1700,7 +1700,7 @@ void filteringJoinNullConflictDifferentNodes() { entity2.setValue(value); scoreDirector.afterVariableChanged(entity2, "value"); assertScore(scoreDirector, - assertMatch(entity2, entity2)); + assertMatch(entity2, entity2).withIndictedObjects(entity2)); } /** From 9a07e312554c71d83d4d8e00ff0cf5f850ed46f4 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Wed, 22 Jul 2026 12:24:29 -0400 Subject: [PATCH 14/30] fix: Propagate updates to indictments in ifExists even if left side doesn't update, make PrecomputeUni tests aware of indictments --- .../bavet/common/AbstractIfExistsNode.java | 19 ++++ .../tuple/indictment/IndictmentSource.java | 9 +- ...ctAdvancedGroupByConstraintStreamTest.java | 22 ++-- .../common/ConstraintStreamTestExtension.java | 8 -- ...ractUniConstraintStreamPrecomputeTest.java | 107 ++++++++++++++---- .../uni/AbstractUniConstraintStreamTest.java | 6 +- 6 files changed, 128 insertions(+), 43 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java index 317b9004a3f..2a1750d8b10 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java @@ -145,6 +145,11 @@ protected void incrementCounterRightUpdatingIndictment(ExistsCounter } else { doRetractCounter(counter); } + } else { + // count != 0, so only propagate if we are in an `ifExists` + if (shouldExist) { + doUpdateCounter(counter); + } } // Else do not even propagate an update // NOTE: By not propagating here, the left tuple's indicted objects can be stale // if an element is removed. @@ -161,6 +166,11 @@ protected void decrementCounterRight(ExistsCounter counter) { } else { doInsertCounter(counter); } + } else { + // count != 0, so only propagate if we are in an `ifExists` + if (shouldExist) { + doUpdateCounter(counter); + } } // Else do not even propagate an update } @@ -352,6 +362,15 @@ private void doRetractCounter(ExistsCounter counter) { } } + private void doUpdateCounter(ExistsCounter counter) { + switch (counter.state) { + case DYING, OK, UPDATING, CREATING -> propagationQueue.update(counter); + case DEAD, ABORTING -> propagationQueue.insert(counter); + default -> throw new IllegalStateException("Impossible state: the counter (%s) has an impossible insert state (%s)." + .formatted(counter, counter.state)); + } + } + @Override protected boolean canProduceTuples() { // The left input must produce tuples no matter what, diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java index 04f57047105..feabf42bfdc 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java @@ -86,6 +86,13 @@ static void addSupport(long nodeId, Tuple carry, Tuple support) { carry.getIndictmentSource().getSupportForNodeId(nodeId).add(support.getIndictmentSource()); } + static void clearSupport(long nodeId, Tuple carry) { + if (carry.getIndictmentSource() == DISABLED) { + return; + } + carry.getIndictmentSource().getSupportForNodeId(nodeId).clear(); + } + static void removeSupport(long nodeId, Tuple carry, Tuple support) { if (carry.getIndictmentSource() == DISABLED) { return; @@ -155,7 +162,7 @@ public void visitSources(Set visited, long[] involvedNodeIds, return; } for (var source : sourceList) { - source.visitSources(involvedNodeIds, sourceConsumer); + source.visitSources(visited, involvedNodeIds, sourceConsumer); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractAdvancedGroupByConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractAdvancedGroupByConstraintStreamTest.java index cd30a4646b2..d7efab5062a 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractAdvancedGroupByConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/AbstractAdvancedGroupByConstraintStreamTest.java @@ -547,17 +547,20 @@ void existsAfterGroupBy() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-2, solution.getFirstEntityGroup(), 2L).withIndictedObjects(solution.getFirstEntity(), - entity3), - assertMatchWithScore(-2, entityGroup1, 2L).withIndictedObjects(entity1, entity2)); + assertMatchWithScore(-2, solution.getFirstEntityGroup(), 2L) + .withIndictedObjects(solution.getFirstEntityGroup(), solution.getFirstEntity(), + entity3), + assertMatchWithScore(-2, entityGroup1, 2L) + .withIndictedObjects(entityGroup1, entity1, entity2)); // Incremental scoreDirector.beforeProblemFactRemoved(entityGroup1); solution.getEntityGroupList().remove(entityGroup1); scoreDirector.afterProblemFactRemoved(entityGroup1); assertScore(scoreDirector, - assertMatchWithScore(-2, solution.getFirstEntityGroup(), 2L).withIndictedObjects(solution.getFirstEntity(), - entity3)); + assertMatchWithScore(-2, solution.getFirstEntityGroup(), 2L) + .withIndictedObjects(solution.getFirstEntityGroup(), solution.getFirstEntity(), + entity3)); } @TestTemplate @@ -621,16 +624,19 @@ void groupByAfterExistsBi() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatchWithScore(-3, solution.getFirstEntityGroup(), 3L).withIndictedObjects(solution.getFirstEntity(), + assertMatchWithScore(-3, solution.getFirstEntityGroup(), 3L).withIndictedObjects( + solution.getFirstEntityGroup(), solution.getFirstEntity(), entity1, entity2, entity3), - assertMatchWithScore(-3, entityGroup1, 3L).withIndictedObjects(entity1, entity2, entity3)); + assertMatchWithScore(-3, entityGroup1, 3L).withIndictedObjects( + entityGroup1, entity1, entity2, entity3)); // Incremental scoreDirector.beforeProblemFactRemoved(entityGroup1); solution.getEntityGroupList().remove(entityGroup1); scoreDirector.afterProblemFactRemoved(entityGroup1); assertScore(scoreDirector, - assertMatchWithScore(-3, solution.getFirstEntityGroup(), 3L).withIndictedObjects(solution.getFirstEntity(), + assertMatchWithScore(-3, solution.getFirstEntityGroup(), 3L).withIndictedObjects( + solution.getFirstEntityGroup(), solution.getFirstEntity(), entity1, entity2, entity3)); } 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 3953260c7f1..a2eeaca2027 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 @@ -32,14 +32,6 @@ public boolean supportsTestTemplate(ExtensionContext context) { @Override public Stream provideTestTemplateInvocationContexts(ExtensionContext context) { - if (ConstraintStreamPrecomputeTest.class.isAssignableFrom(context.getTestClass().get())) { - // Precompute test use - return Stream - .of(ConstraintMatchPolicy.ENABLED_WITHOUT_INDICTMENTS, - ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS, - ConstraintMatchPolicy.DISABLED) - .map(ConstraintStreamTestExtension::invocationContext); - } return Stream .of(ConstraintMatchPolicy.ENABLED, ConstraintMatchPolicy.ENABLED_WITHOUT_INDICTMENTS, diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamPrecomputeTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamPrecomputeTest.java index 65b3f749e48..cc9fb27620f 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamPrecomputeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamPrecomputeTest.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.score.stream.common.uni; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.function.Function; @@ -13,7 +15,6 @@ import ai.timefold.solver.core.impl.score.stream.common.ConstraintStreamImplSupport; import ai.timefold.solver.core.impl.score.stream.common.ConstraintStreamPrecomputeTest; import ai.timefold.solver.core.impl.score.stream.common.ConstraintStreamTestExtension; -import ai.timefold.solver.core.impl.util.Pair; import ai.timefold.solver.core.testdomain.score.lavish.TestdataLavishEntity; import ai.timefold.solver.core.testdomain.score.lavish.TestdataLavishEntityGroup; import ai.timefold.solver.core.testdomain.score.lavish.TestdataLavishSolution; @@ -132,9 +133,27 @@ public void filter_0_changed() { assertMatch(entity3)); } - private void assertPrecompute(TestdataLavishSolution solution, - List expectedValues, - Function> entityStreamSupplier) { + record ExpectedUni(A a, Object... indicted) { + ExpectedUni addIndicted(Object indictedObject) { + for (var object : indicted) { + if (object == indictedObject) { + return this; + } + } + var indictments = Arrays.copyOf(indicted, indicted.length + 1); + indictments[indicted.length] = indictedObject; + return new ExpectedUni<>(a, indictments); + } + } + + ExpectedUni expect(A a, Object... indicted) { + return new ExpectedUni<>(a, indicted); + } + + private void assertPrecompute(TestdataLavishSolution solution, + List> expectedTuples, + Function> entityStreamSupplier) { + expectedTuples = new ArrayList<>(expectedTuples); var scoreDirector = buildScoreDirector(factory -> factory.precompute(entityStreamSupplier) .ifExists(TestdataLavishEntity.class) @@ -149,10 +168,16 @@ private void assertPrecompute(TestdataLavishSolution solution, scoreDirector.beforeVariableChanged(entity, "value"); entity.setValue(solution.getFirstValue()); scoreDirector.afterVariableChanged(entity, "value"); + var listIterator = expectedTuples.listIterator(); + while (listIterator.hasNext()) { + var expectedTuple = listIterator.next(); + listIterator.set(expectedTuple.addIndicted(entity)); + } } - assertScore(scoreDirector, expectedValues.stream() - .map(AbstractConstraintStreamTest::assertMatch) + assertScore(scoreDirector, expectedTuples.stream() + .map(expected -> AbstractConstraintStreamTest.assertMatch(expected.a) + .withIndictedObjects(expected.indicted)) .toArray(AssertableMatch[]::new)); } @@ -161,14 +186,17 @@ private void assertPrecompute(TestdataLavishSolution solution, public void ifExists() { var solution = TestdataLavishSolution.generateEmptySolution(); var entityWithoutGroup = new TestdataLavishEntity(); + entityWithoutGroup.setCode("A"); var entityWithGroup = new TestdataLavishEntity(); + entityWithGroup.setCode("B"); var entityGroup = new TestdataLavishEntityGroup(); + entityGroup.setCode("C"); entityWithGroup.setEntityGroup(entityGroup); solution.getEntityList().addAll(List.of(entityWithoutGroup, entityWithGroup)); solution.getEntityGroupList().add(entityGroup); solution.getValueList().add(new TestdataLavishValue()); - assertPrecompute(solution, List.of(entityWithGroup), + assertPrecompute(solution, List.of(expect(entityWithGroup, entityWithGroup, entityGroup)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .ifExists(TestdataLavishEntityGroup.class, Joiners.equal( TestdataLavishEntity::getEntityGroup, Function.identity()))); @@ -186,7 +214,7 @@ public void ifNotExists() { solution.getEntityGroupList().add(entityGroup); solution.getValueList().add(new TestdataLavishValue()); - assertPrecompute(solution, List.of(entityWithoutGroup), + assertPrecompute(solution, List.of(expect(entityWithoutGroup, entityWithoutGroup)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .ifNotExists(TestdataLavishEntityGroup.class, Joiners.equal( TestdataLavishEntity::getEntityGroup, Function.identity()))); @@ -204,7 +232,7 @@ public void groupBy() { solution.getEntityGroupList().add(entityGroup); solution.getValueList().add(new TestdataLavishValue()); - assertPrecompute(solution, List.of(entityGroup), + assertPrecompute(solution, List.of(expect(entityGroup, entityWithGroup)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .filter(entity -> entity.getEntityGroup() != null) .groupBy(TestdataLavishEntity::getEntityGroup)); @@ -215,21 +243,42 @@ public void groupBy() { public void flatten() { var solution = TestdataLavishSolution.generateEmptySolution(); var entityWithoutGroup = new TestdataLavishEntity(); + entityWithoutGroup.setCode("A"); var entityWithGroup = new TestdataLavishEntity(); + entityWithGroup.setCode("B"); var entityGroup = new TestdataLavishEntityGroup(); + entityGroup.setCode("C"); entityWithGroup.setEntityGroup(entityGroup); solution.getEntityList().addAll(List.of(entityWithoutGroup, entityWithGroup)); solution.getEntityGroupList().add(entityGroup); solution.getValueList().add(new TestdataLavishValue()); - assertPrecomputeBi(solution, List.of(new Pair<>(entityWithoutGroup, entityWithoutGroup), - new Pair<>(entityWithGroup, entityWithGroup)), + assertPrecomputeBi(solution, List.of(expectBi(entityWithoutGroup, entityWithoutGroup, entityWithoutGroup), + expectBi(entityWithGroup, entityWithGroup, entityWithGroup)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .flatten(List::of)); } - private void assertPrecomputeBi(TestdataLavishSolution solution, List> expectedValues, + record ExpectedBi(A a, B b, Object... indicted) { + ExpectedBi addIndicted(Object indictedObject) { + for (var object : indicted) { + if (object == indictedObject) { + return this; + } + } + var indictments = Arrays.copyOf(indicted, indicted.length + 1); + indictments[indicted.length] = indictedObject; + return new ExpectedBi<>(a, b, indictments); + } + } + + ExpectedBi expectBi(A a, B b, Object... indicted) { + return new ExpectedBi<>(a, b, indicted); + } + + private void assertPrecomputeBi(TestdataLavishSolution solution, List> expectedValues, Function> entityStreamSupplier) { + expectedValues = new ArrayList<>(expectedValues); var scoreDirector = buildScoreDirector(factory -> factory.precompute(entityStreamSupplier) .ifExists(TestdataLavishEntity.class) @@ -244,11 +293,16 @@ private void assertPrecomputeBi(TestdataLavishSolution solution, List new Object[] { pair.key(), pair.value() }) - .map(AbstractConstraintStreamTest::assertMatch) + .map(expectedBi -> assertMatch(expectedBi.a(), expectedBi.b()) + .withIndictedObjects(expectedBi.indicted)) .toArray(AssertableMatch[]::new)); } @@ -268,8 +322,8 @@ record ValueHolder(int value) { solution.getValueList().add(new TestdataLavishValue()); assertPrecomputeBi(solution, List.of( - new Pair<>(entity1, new ValueHolder(1)), - new Pair<>(entity2, new ValueHolder(2))), + expectBi(entity1, new ValueHolder(1), entity1), + expectBi(entity2, new ValueHolder(2), entity2)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .flatten(entity -> List.of(new ValueHolder(entity.getIntegerProperty())))); } @@ -286,7 +340,8 @@ public void flattenLast() { solution.getEntityGroupList().add(entityGroup); solution.getValueList().add(new TestdataLavishValue()); - assertPrecompute(solution, List.of(entityWithoutGroup, entityWithGroup), + assertPrecompute(solution, + List.of(expect(entityWithoutGroup, entityWithoutGroup), expect(entityWithGroup, entityWithGroup)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .groupBy(ConstraintCollectors.toList()) .flattenLast(entityList -> entityList)); @@ -307,7 +362,8 @@ record ValueHolder(int value) { solution.getEntityList().addAll(List.of(entity1, entity2)); solution.getValueList().add(new TestdataLavishValue()); - assertPrecompute(solution, List.of(new ValueHolder(1), new ValueHolder(2)), + assertPrecompute(solution, List.of(expect(new ValueHolder(1), entity1), + expect(new ValueHolder(2), entity2)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .groupBy(ConstraintCollectors.toList()) .flattenLast(entityList -> entityList @@ -330,7 +386,8 @@ public void map() { solution.getEntityGroupList().add(entityGroup); solution.getValueList().add(new TestdataLavishValue()); - assertPrecompute(solution, List.of(entityGroup, entityGroup), + assertPrecompute(solution, List.of(expect(entityGroup, entityWithGroup1), + expect(entityGroup, entityWithGroup2)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .filter(entity -> entity.getEntityGroup() != null) .map(TestdataLavishEntity::getEntityGroup)); @@ -341,14 +398,18 @@ public void map() { public void concat() { var solution = TestdataLavishSolution.generateEmptySolution(); var entityWithoutGroup = new TestdataLavishEntity(); + entityWithoutGroup.setCode("EntityWithoutGroup"); var entityWithGroup = new TestdataLavishEntity(); + entityWithGroup.setCode("EntityWithGroup"); var entityGroup = new TestdataLavishEntityGroup(); + entityGroup.setCode("EntityGroup"); entityWithGroup.setEntityGroup(entityGroup); solution.getEntityList().addAll(List.of(entityWithoutGroup, entityWithGroup)); solution.getEntityGroupList().add(entityGroup); solution.getValueList().add(new TestdataLavishValue()); - assertPrecompute(solution, List.of(entityWithoutGroup, entityWithGroup), + assertPrecompute(solution, List.of(expect(entityWithoutGroup, entityWithoutGroup), + expect(entityWithGroup, entityWithGroup)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .filter(entity -> entity.getEntityGroup() == null) .concat(pf.forEachUnfiltered(TestdataLavishEntity.class) @@ -369,7 +430,7 @@ public void distinct() { solution.getEntityGroupList().add(entityGroup); solution.getValueList().add(new TestdataLavishValue()); - assertPrecompute(solution, List.of(entityGroup), + assertPrecompute(solution, List.of(expect(entityGroup, entityWithGroup1, entityWithGroup2)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .filter(entity -> entity.getEntityGroup() != null) .map(TestdataLavishEntity::getEntityGroup) @@ -390,7 +451,9 @@ public void complement() { solution.getEntityGroupList().add(entityGroup); solution.getValueList().add(new TestdataLavishValue()); - assertPrecompute(solution, List.of(entityWithGroup1, entityWithGroup2, entityWithoutGroup), + assertPrecompute(solution, List.of(expect(entityWithGroup1, entityWithGroup1), + expect(entityWithGroup2, entityWithGroup2), + expect(entityWithoutGroup, entityWithoutGroup)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .filter(entity -> entity.getEntityGroup() != null) .complement(TestdataLavishEntity.class)); 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 42e57172f94..64645c474bf 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 @@ -485,12 +485,10 @@ public void ifExists_0Joiner0Filter() { assertScore(scoreDirector, assertMatch(solution.getFirstValueGroup()).withIndictedObjects( solution.getFirstValueGroup(), - solution.getFirstEntityGroup(), - entityGroup), + solution.getFirstEntityGroup()), assertMatch(valueGroup).withIndictedObjects( valueGroup, - solution.getFirstEntityGroup(), - entityGroup)); + solution.getFirstEntityGroup())); } @Override From e4f096fbefc29ce55a9c0684a1f4bb5f7a76a814 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Wed, 22 Jul 2026 15:28:04 -0400 Subject: [PATCH 15/30] fix: update left indicted set even if its key not changed for indexed if exists, aggregate support for precompute, make BiPrecomputeTest Indictment aware --- .../core/impl/bavet/bi/PrecomputeBiNode.java | 4 +- .../common/AbstractIndexedIfExistsNode.java | 6 ++ .../core/impl/bavet/common/TupleRecorder.java | 17 +++- .../tuple/indictment/IndictmentSource.java | 39 +++++++--- .../impl/bavet/quad/PrecomputeQuadNode.java | 4 +- .../impl/bavet/tri/PrecomputeTriNode.java | 4 +- .../impl/bavet/uni/PrecomputeUniNode.java | 4 +- ...tractBiConstraintStreamPrecomputeTest.java | 78 +++++++++++++------ .../bi/AbstractBiConstraintStreamTest.java | 15 ++-- .../tri/AbstractTriConstraintStreamTest.java | 15 ++-- 10 files changed, 132 insertions(+), 54 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/PrecomputeBiNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/PrecomputeBiNode.java index 6afb77873a3..ee4f3575c31 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/PrecomputeBiNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/PrecomputeBiNode.java @@ -23,8 +23,6 @@ public PrecomputeBiNode(Supplier>> prec @Override protected BiTuple remapTuple(BiTuple tuple) { - var out = BiTuple.of(tuple.getA(), tuple.getB(), outputStoreSize); - out.setIndictmentSource(tuple.getIndictmentSource()); - return out; + return BiTuple.of(tuple.getA(), tuple.getB(), outputStoreSize); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java index c954d09898c..b08226505a1 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java @@ -132,6 +132,12 @@ public final void updateLeft(LeftTuple_ leftTuple) { // The indexers contain counters in the DEAD state, to track the rightCount. if (!isFiltering) { updateUnchangedCounterLeft(counter); + if (leftTuple.getIndictmentSource() != IndictmentSource.DISABLED) { + IndictmentSource.clearSupport(getId(), leftTuple); + forEachRightFromLeft(leftTuple, newCompositeKey, rightTuple -> { + IndictmentSource.addSupport(getId(), leftTuple, rightTuple); + }); + } } else { // Eager own-side cleanup (reads only pairs already tracked, so it's eager-safe), // then defer the re-walk of the opposite side to this node's own layer turn. diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/TupleRecorder.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/TupleRecorder.java index c2fab90d528..2ae88ccdb76 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/TupleRecorder.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/TupleRecorder.java @@ -1,10 +1,13 @@ package ai.timefold.solver.core.impl.bavet.common; +import java.util.ArrayList; import java.util.IdentityHashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.function.UnaryOperator; import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource; import org.jspecify.annotations.NullMarked; @@ -13,6 +16,18 @@ public record TupleRecorder(List recordedTupleList UnaryOperator mapper, IdentityHashMap inputTupleToOutputTuple) { public void recordTuple(Tuple_ tuple) { - recordedTupleList.add(inputTupleToOutputTuple.computeIfAbsent(tuple, mapper)); + var outTuple = inputTupleToOutputTuple.computeIfAbsent(tuple, mapper); + if (tuple.getIndictmentSource() != IndictmentSource.DISABLED) { + if (outTuple.getIndictmentSource() == IndictmentSource.DISABLED) { + outTuple.setIndictmentSource( + new IndictmentSource.AggregateIndictmentSource(new ArrayList<>(), new LinkedHashMap<>())); + } + // Precompute uses an independent node network, so we need to aggregate its supports to not interfere with the + // outer node network support + var aggregateIndictmentSource = (IndictmentSource.AggregateIndictmentSource) outTuple.getIndictmentSource(); + tuple.getIndictmentSource() + .visitAllSources(source -> aggregateIndictmentSource.sourceList().add(IndictmentSource.of(source))); + } + recordedTupleList.add(outTuple); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java index feabf42bfdc..69f565f2421 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java @@ -12,13 +12,21 @@ import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked public sealed interface IndictmentSource { IndictmentSource DISABLED = new DisabledIndictmentSource(); - void visitSources(Set visited, long[] involvedNodeIds, Consumer sourceConsumer); + void visitSources(Set visited, long @Nullable [] involvedNodeIds, Consumer sourceConsumer); Map> support(); + default void visitAllSources(Consumer sourceConsumer) { + visitSources(new HashSet<>(), null, sourceConsumer); + } + default void visitSources(long[] involvedNodeIds, Consumer sourceConsumer) { visitSources(new HashSet<>(), involvedNodeIds, sourceConsumer); } @@ -28,13 +36,22 @@ default Set getSupportForNodeId(long nodeId) { } static boolean checkIfAlreadyVisitedAndVisitSupport(IndictmentSource self, Set visited, - long[] involvedNodeIds, Consumer sourceConsumer) { + long @Nullable [] involvedNodeIds, Consumer sourceConsumer) { if (!visited.add(self)) { return true; } - for (var nodeId : involvedNodeIds) { - for (var indictmentSource : self.support().getOrDefault(nodeId, Collections.emptySet())) { - indictmentSource.visitSources(visited, involvedNodeIds, sourceConsumer); + + if (involvedNodeIds == null) { + for (var indictmentSourceSet : self.support().values()) { + for (var indictmentSource : indictmentSourceSet) { + indictmentSource.visitSources(visited, null, sourceConsumer); + } + } + } else { + for (var nodeId : involvedNodeIds) { + for (var indictmentSource : self.support().getOrDefault(nodeId, Collections.emptySet())) { + indictmentSource.visitSources(visited, involvedNodeIds, sourceConsumer); + } } } return false; @@ -102,7 +119,8 @@ static void removeSupport(long nodeId, Tuple carry, Tuple support) { record DisabledIndictmentSource() implements IndictmentSource { @Override - public void visitSources(Set visited, long[] involvedNodeIds, Consumer sourceConsumer) { + public void visitSources(Set visited, long @Nullable [] involvedNodeIds, + Consumer sourceConsumer) { throw new UnsupportedOperationException("Impossible state: indictments are disabled."); } @@ -114,7 +132,8 @@ public Map> support() { record RootIndictmentSource(Object source, Map> support) implements IndictmentSource { @Override - public void visitSources(Set visited, long[] involvedNodeIds, Consumer sourceConsumer) { + public void visitSources(Set visited, long @Nullable [] involvedNodeIds, + Consumer sourceConsumer) { if (checkIfAlreadyVisitedAndVisitSupport(this, visited, involvedNodeIds, sourceConsumer)) { return; } @@ -135,7 +154,8 @@ public int hashCode() { record JoinedIndictmentSource(IndictmentSource left, IndictmentSource right, Map> support) implements IndictmentSource { @Override - public void visitSources(Set visited, long[] involvedNodeIds, Consumer sourceConsumer) { + public void visitSources(Set visited, long @Nullable [] involvedNodeIds, + Consumer sourceConsumer) { if (checkIfAlreadyVisitedAndVisitSupport(this, visited, involvedNodeIds, sourceConsumer)) { return; } @@ -157,7 +177,8 @@ public int hashCode() { record AggregateIndictmentSource(List sourceList, Map> support) implements IndictmentSource { @Override - public void visitSources(Set visited, long[] involvedNodeIds, Consumer sourceConsumer) { + public void visitSources(Set visited, long @Nullable [] involvedNodeIds, + Consumer sourceConsumer) { if (checkIfAlreadyVisitedAndVisitSupport(this, visited, involvedNodeIds, sourceConsumer)) { return; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/PrecomputeQuadNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/PrecomputeQuadNode.java index 371c8448162..8c65119bd99 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/PrecomputeQuadNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/PrecomputeQuadNode.java @@ -23,8 +23,6 @@ public PrecomputeQuadNode(Supplier remapTuple(QuadTuple tuple) { - var out = QuadTuple.of(tuple.getA(), tuple.getB(), tuple.getC(), tuple.getD(), outputStoreSize); - out.setIndictmentSource(tuple.getIndictmentSource()); - return out; + return QuadTuple.of(tuple.getA(), tuple.getB(), tuple.getC(), tuple.getD(), outputStoreSize); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/PrecomputeTriNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/PrecomputeTriNode.java index 06d776b782a..936563f13c5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/PrecomputeTriNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/PrecomputeTriNode.java @@ -23,8 +23,6 @@ public PrecomputeTriNode(Supplier>> @Override protected TriTuple remapTuple(TriTuple tuple) { - var out = TriTuple.of(tuple.getA(), tuple.getB(), tuple.getC(), outputStoreSize); - out.setIndictmentSource(tuple.getIndictmentSource()); - return out; + return TriTuple.of(tuple.getA(), tuple.getB(), tuple.getC(), outputStoreSize); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/PrecomputeUniNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/PrecomputeUniNode.java index 70f64d5ad73..92dfee56180 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/PrecomputeUniNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/PrecomputeUniNode.java @@ -23,9 +23,7 @@ public PrecomputeUniNode(Supplier>> preco @Override protected UniTuple remapTuple(UniTuple tuple) { - var out = UniTuple.of(tuple.getA(), outputStoreSize); - out.setIndictmentSource(tuple.getIndictmentSource()); - return out; + return UniTuple.of(tuple.getA(), outputStoreSize); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamPrecomputeTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamPrecomputeTest.java index 032a4493f05..48d9ae75ea3 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamPrecomputeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamPrecomputeTest.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.score.stream.common.bi; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.function.Function; @@ -11,7 +13,6 @@ import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraintStreamTest; import ai.timefold.solver.core.impl.score.stream.common.ConstraintStreamImplSupport; import ai.timefold.solver.core.impl.score.stream.common.ConstraintStreamPrecomputeTest; -import ai.timefold.solver.core.impl.util.Pair; import ai.timefold.solver.core.testdomain.score.lavish.TestdataLavishEntity; import ai.timefold.solver.core.testdomain.score.lavish.TestdataLavishEntityGroup; import ai.timefold.solver.core.testdomain.score.lavish.TestdataLavishSolution; @@ -354,9 +355,27 @@ public void filter_1_changed_forEachUnfilteredUniquePair() { assertMatch(entity2, entity3)); } + record Expected(A a, B b, Object... indicted) { + Expected addIndicted(Object indictedObject) { + for (var object : indicted) { + if (object == indictedObject) { + return this; + } + } + var newIndictments = Arrays.copyOf(indicted, indicted.length + 1); + newIndictments[indicted.length] = indictedObject; + return new Expected<>(a, b, newIndictments); + } + } + + Expected expect(A a, B b, Object... indicted) { + return new Expected<>(a, b, indicted); + } + private void assertPrecompute(TestdataLavishSolution solution, - List> expectedValues, + List> expectedValues, Function> entityStreamSupplier) { + expectedValues = new ArrayList<>(expectedValues); var scoreDirector = buildScoreDirector(factory -> factory.precompute(entityStreamSupplier) .ifExists(TestdataLavishEntity.class) @@ -371,11 +390,16 @@ private void assertPrecompute(TestdataLavishSolution solution, scoreDirector.beforeVariableChanged(entity, "value"); entity.setValue(solution.getFirstValue()); scoreDirector.afterVariableChanged(entity, "value"); + var listIterator = expectedValues.listIterator(); + while (listIterator.hasNext()) { + var expected = listIterator.next(); + listIterator.set(expected.addIndicted(entity)); + } } assertScore(scoreDirector, expectedValues.stream() - .map(pair -> new Object[] { pair.key(), pair.value() }) - .map(AbstractConstraintStreamTest::assertMatch) + .map(expected -> assertMatch(expected.a, expected.b) + .withIndictedObjects(expected.indicted)) .toArray(AssertableMatch[]::new)); } @@ -384,15 +408,19 @@ private void assertPrecompute(TestdataLavishSolution solution, public void ifExists() { var solution = TestdataLavishSolution.generateEmptySolution(); var entityWithoutGroup = new TestdataLavishEntity(); + entityWithoutGroup.setCode("A"); var entityWithGroup = new TestdataLavishEntity(); + entityWithGroup.setCode("B"); var entityGroup = new TestdataLavishEntityGroup(); + entityGroup.setCode("C"); entityWithGroup.setEntityGroup(entityGroup); solution.getEntityList().addAll(List.of(entityWithoutGroup, entityWithGroup)); solution.getEntityGroupList().add(entityGroup); var value = new TestdataLavishValue(); + value.setCode("D"); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Pair<>(entityWithGroup, value)), + assertPrecompute(solution, List.of(expect(entityWithGroup, value, entityWithGroup, value, entityGroup)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .ifExists(TestdataLavishEntityGroup.class, Joiners.equal( @@ -413,7 +441,7 @@ public void ifNotExists() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Pair<>(entityWithoutGroup, value)), + assertPrecompute(solution, List.of(expect(entityWithoutGroup, value, entityWithoutGroup, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .ifNotExists(TestdataLavishEntityGroup.class, Joiners.equal( @@ -434,7 +462,7 @@ public void groupBy() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Pair<>(entityGroup, 1L)), + assertPrecompute(solution, List.of(expect(entityGroup, 1L)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .filter(entity -> entity.getEntityGroup() != null) .groupBy(TestdataLavishEntity::getEntityGroup, ConstraintCollectors.count())); @@ -453,8 +481,8 @@ public void flatten() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Pair<>(entityWithoutGroup, entityWithoutGroup), - new Pair<>(entityWithGroup, entityWithoutGroup)), + assertPrecompute(solution, List.of(expect(entityWithoutGroup, entityWithoutGroup), + expect(entityWithGroup, entityWithoutGroup)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .flatten(List::of)); } @@ -475,8 +503,8 @@ record ValueHolder(int value) { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Pair<>(entity1, new ValueHolder(entity1.getIntegerProperty())), - new Pair<>(entity2, new ValueHolder(entity2.getIntegerProperty()))), + assertPrecompute(solution, List.of(expect(entity1, new ValueHolder(entity1.getIntegerProperty())), + expect(entity2, new ValueHolder(entity2.getIntegerProperty()))), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .flatten(entity -> List.of(new ValueHolder(entity.getIntegerProperty())))); } @@ -494,8 +522,8 @@ public void flattenLast() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Pair<>(entityWithoutGroup, value), - new Pair<>(entityWithGroup, value)), + assertPrecompute(solution, List.of(expect(entityWithoutGroup, value, entityWithoutGroup, value), + expect(entityWithGroup, value, entityWithGroup, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .groupBy(ConstraintCollectors.toList()) .flattenLast(entityList -> entityList) @@ -518,8 +546,8 @@ record ValueHolder(int value) { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Pair<>(new ValueHolder(1), value), - new Pair<>(new ValueHolder(2), value)), + assertPrecompute(solution, List.of(expect(new ValueHolder(1), value, entity1, value), + expect(new ValueHolder(2), value, entity2, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .groupBy(ConstraintCollectors.toList()) .flattenLast(entityList -> entityList @@ -544,8 +572,8 @@ public void map() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Pair<>(entityGroup, value), - new Pair<>(entityGroup, value)), + assertPrecompute(solution, List.of(expect(entityGroup, value, entityWithGroup1, value), + expect(entityGroup, value, entityWithGroup2, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .filter((entity, joinedValue) -> entity.getEntityGroup() != null) @@ -558,15 +586,21 @@ public void map() { public void concat() { var solution = TestdataLavishSolution.generateEmptySolution(); var entityWithoutGroup = new TestdataLavishEntity(); + entityWithoutGroup.setCode("A"); var entityWithGroup = new TestdataLavishEntity(); + entityWithGroup.setCode("B"); var entityGroup = new TestdataLavishEntityGroup(); + entityGroup.setCode("C"); entityWithGroup.setEntityGroup(entityGroup); solution.getEntityList().addAll(List.of(entityWithoutGroup, entityWithGroup)); solution.getEntityGroupList().add(entityGroup); var value = new TestdataLavishValue(); + value.setCode("D"); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Pair<>(entityWithoutGroup, value), new Pair<>(entityWithGroup, value)), + assertPrecompute(solution, List.of( + expect(entityWithoutGroup, value, entityWithoutGroup, value), + expect(entityWithGroup, value, entityWithGroup, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .filter((entity, joinedValue) -> entity.getEntityGroup() == null) @@ -590,7 +624,7 @@ public void distinct() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Pair<>(entityGroup, value)), + assertPrecompute(solution, List.of(expect(entityGroup, value, entityWithGroup1, entityWithGroup2, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .filter((entity, joinedValue) -> entity.getEntityGroup() != null) @@ -615,9 +649,9 @@ public void complement() { solution.getValueList().add(value); assertPrecompute(solution, List.of( - new Pair<>(entityWithGroup1, value), - new Pair<>(entityWithGroup2, value), - new Pair<>(entityWithoutGroup, null)), + expect(entityWithGroup1, value, entityWithGroup1, value), + expect(entityWithGroup2, value, entityWithGroup2, value), + expect(entityWithoutGroup, null, entityWithoutGroup)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .filter((entity, joinedValue) -> entity.getEntityGroup() != null) 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 7b5cf3873ed..87fcbeed658 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 @@ -949,17 +949,22 @@ public void ifExists_1Join0Filter() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), entity1), - assertMatch(solution.getFirstEntity(), entity2), - assertMatch(entity1, entity2)); + assertMatch(solution.getFirstEntity(), entity1) + .withIndictedObjects(solution.getFirstEntityGroup(), solution.getFirstEntity(), entity1), + assertMatch(solution.getFirstEntity(), entity2) + .withIndictedObjects(solution.getFirstEntityGroup(), solution.getFirstEntity(), entity2), + assertMatch(entity1, entity2) + .withIndictedObjects(entityGroup, entity1, entity2)); // Incremental scoreDirector.beforeProblemFactRemoved(entityGroup); solution.getEntityGroupList().remove(entityGroup); scoreDirector.afterProblemFactRemoved(entityGroup); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), entity1), - assertMatch(solution.getFirstEntity(), entity2)); + assertMatch(solution.getFirstEntity(), entity1) + .withIndictedObjects(solution.getFirstEntityGroup(), solution.getFirstEntity(), entity1), + assertMatch(solution.getFirstEntity(), entity2) + .withIndictedObjects(solution.getFirstEntityGroup(), solution.getFirstEntity(), entity2)); } @Override 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 d1d79aef37c..d027d54c48d 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 @@ -449,17 +449,22 @@ public void ifExists_1Join0Filter() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity2, entity1, solution.getFirstEntity()), - assertMatch(entity1, entity2, solution.getFirstEntity()), - assertMatch(solution.getFirstEntity(), entity1, entity2)); + assertMatch(entity2, entity1, solution.getFirstEntity()) + .withIndictedObjects(solution.getFirstEntityGroup(), entity2, entity1, solution.getFirstEntity()), + assertMatch(entity1, entity2, solution.getFirstEntity()) + .withIndictedObjects(entityGroup, entity1, entity2, solution.getFirstEntity()), + assertMatch(solution.getFirstEntity(), entity1, entity2) + .withIndictedObjects(solution.getFirstEntityGroup(), solution.getFirstEntity(), entity1, entity2)); // Incremental scoreDirector.beforeProblemFactRemoved(entityGroup); solution.getEntityGroupList().remove(entityGroup); scoreDirector.afterProblemFactRemoved(entityGroup); assertScore(scoreDirector, - assertMatch(entity2, entity1, solution.getFirstEntity()), - assertMatch(entity1, entity2, solution.getFirstEntity())); + assertMatch(entity2, entity1, solution.getFirstEntity()) + .withIndictedObjects(solution.getFirstEntityGroup(), entity2, entity1, solution.getFirstEntity()), + assertMatch(entity1, entity2, solution.getFirstEntity()) + .withIndictedObjects(solution.getFirstEntityGroup(), entity1, entity2, solution.getFirstEntity())); } @Override From 7a85df112ff1299f48cd5ba40a8779e3d4ea8e9b Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Wed, 22 Jul 2026 16:18:34 -0400 Subject: [PATCH 16/30] test: make TriPrecompute test indictment aware --- ...ractTriConstraintStreamPrecomputeTest.java | 74 +++++++++++++------ 1 file changed, 53 insertions(+), 21 deletions(-) diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamPrecomputeTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamPrecomputeTest.java index d80cfab093a..56b83bddd81 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamPrecomputeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamPrecomputeTest.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.score.stream.common.tri; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.function.Function; @@ -162,9 +164,27 @@ public void filter_2_changed() { (entity, value, entityGroup) -> new Triple<>(value, entityGroup, entity)); } + record ExpectedTri(A a, B b, C c, Object... indicted) { + ExpectedTri withIndictedObject(Object indictedObject) { + for (var obj : indicted) { + if (obj == indictedObject) { + return this; + } + } + var newIndicted = Arrays.copyOf(indicted, indicted.length + 1); + newIndicted[indicted.length] = indictedObject; + return new ExpectedTri<>(a, b, c, newIndicted); + } + } + + ExpectedTri expected(A a, B b, C c, Object... indicted) { + return new ExpectedTri<>(a, b, c, indicted); + } + private void assertPrecompute(TestdataLavishSolution solution, - List> expectedValues, + List> expectedValues, Function> entityStreamSupplier) { + expectedValues = new ArrayList<>(expectedValues); var scoreDirector = buildScoreDirector(factory -> factory.precompute(entityStreamSupplier) .ifExists(TestdataLavishEntity.class) @@ -179,11 +199,16 @@ private void assertPrecompute(TestdataLavishSolution solution, scoreDirector.beforeVariableChanged(entity, "value"); entity.setValue(solution.getFirstValue()); scoreDirector.afterVariableChanged(entity, "value"); + var listIterator = expectedValues.listIterator(); + while (listIterator.hasNext()) { + var expected = listIterator.next(); + listIterator.set(expected.withIndictedObject(entity)); + } } assertScore(scoreDirector, expectedValues.stream() - .map(triple -> new Object[] { triple.a(), triple.b(), triple.c() }) - .map(AbstractConstraintStreamTest::assertMatch) + .map(expected -> assertMatch(expected.a, expected.b, expected.c) + .withIndictedObjects(expected.indicted)) .toArray(AssertableMatch[]::new)); } @@ -200,7 +225,7 @@ public void ifExists() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Triple<>(entityWithGroup, value, value)), + assertPrecompute(solution, List.of(expected(entityWithGroup, value, value, entityWithGroup, value, entityGroup)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .join(TestdataLavishValue.class) @@ -222,7 +247,7 @@ public void ifNotExists() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Triple<>(entityWithoutGroup, value, value)), + assertPrecompute(solution, List.of(expected(entityWithoutGroup, value, value, entityWithoutGroup, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .join(TestdataLavishValue.class) @@ -244,7 +269,7 @@ public void groupBy() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Triple<>(entityGroup, 1L, 1L)), + assertPrecompute(solution, List.of(expected(entityGroup, 1L, 1L)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .filter(entity -> entity.getEntityGroup() != null) .groupBy(TestdataLavishEntity::getEntityGroup, @@ -265,8 +290,9 @@ public void flatten() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Triple<>(entityWithoutGroup, entityWithoutGroup, value), - new Triple<>(entityWithGroup, entityWithoutGroup, value)), + assertPrecompute(solution, List.of( + expected(entityWithoutGroup, entityWithoutGroup, value, entityWithoutGroup, value), + expected(entityWithGroup, entityWithoutGroup, value, entityWithGroup, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .flatten(List::of) .join(TestdataLavishValue.class)); @@ -288,8 +314,9 @@ record ValueHolder(int value) { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Triple<>(entity1, new ValueHolder(entity1.getIntegerProperty()), value), - new Triple<>(entity2, new ValueHolder(entity2.getIntegerProperty()), value)), + assertPrecompute(solution, List.of( + expected(entity1, new ValueHolder(entity1.getIntegerProperty()), value, entity1, value), + expected(entity2, new ValueHolder(entity2.getIntegerProperty()), value, entity2, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .flatten(entity -> List.of(new ValueHolder(entity.getIntegerProperty()))) .join(TestdataLavishValue.class)); @@ -308,8 +335,9 @@ public void flattenLast() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Triple<>(entityWithoutGroup, value, value), - new Triple<>(entityWithGroup, value, value)), + assertPrecompute(solution, List.of( + expected(entityWithoutGroup, value, value, entityWithoutGroup, value), + expected(entityWithGroup, value, value, entityWithGroup, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .groupBy(ConstraintCollectors.toList()) .flattenLast(entityList -> entityList) @@ -333,8 +361,9 @@ record ValueHolder(int value) { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Triple<>(new ValueHolder(1), value, value), - new Triple<>(new ValueHolder(2), value, value)), + assertPrecompute(solution, List.of( + expected(new ValueHolder(1), value, value, entity1, value), + expected(new ValueHolder(2), value, value, entity2, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .groupBy(ConstraintCollectors.toList()) .flattenLast(entityList -> entityList @@ -360,8 +389,9 @@ public void map() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Triple<>(entityGroup, value, value), - new Triple<>(entityGroup, value, value)), + assertPrecompute(solution, List.of( + expected(entityGroup, value, value, entityWithGroup1, value), + expected(entityGroup, value, value, entityWithGroup2, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .join(TestdataLavishValue.class) @@ -385,7 +415,8 @@ public void concat() { solution.getValueList().add(value); assertPrecompute(solution, - List.of(new Triple<>(entityWithoutGroup, value, value), new Triple<>(entityWithGroup, value, value)), + List.of(expected(entityWithoutGroup, value, value, entityWithoutGroup, value), + expected(entityWithGroup, value, value, entityWithGroup, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .join(TestdataLavishValue.class) @@ -411,7 +442,8 @@ public void distinct() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Triple<>(entityGroup, value, value)), + assertPrecompute(solution, List.of( + expected(entityGroup, value, value, entityWithGroup1, entityWithGroup2, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .join(TestdataLavishValue.class) @@ -438,9 +470,9 @@ public void complement() { solution.getValueList().add(value); assertPrecompute(solution, List.of( - new Triple<>(entityWithGroup1, value, value), - new Triple<>(entityWithGroup2, value, value), - new Triple<>(entityWithoutGroup, null, null)), + expected(entityWithGroup1, value, value, entityWithGroup1, value), + expected(entityWithGroup2, value, value, entityWithGroup2, value), + expected(entityWithoutGroup, null, null, entityWithoutGroup)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .join(TestdataLavishValue.class) From ddcaefbb40ba7b74ab4666449868186e195dc1b0 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Wed, 22 Jul 2026 17:08:14 -0400 Subject: [PATCH 17/30] test: make QuadPrecompute tests indictment aware --- ...actQuadConstraintStreamPrecomputeTest.java | 64 +++++++++++++------ 1 file changed, 46 insertions(+), 18 deletions(-) diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamPrecomputeTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamPrecomputeTest.java index 06d6b9df156..d12b05d80fe 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamPrecomputeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamPrecomputeTest.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.score.stream.common.quad; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.function.Function; @@ -186,9 +188,27 @@ public void filter_3_changed() { (entity, value, entityGroup, valueGroup) -> new Quadruple<>(value, entityGroup, valueGroup, entity)); } + record ExpectedQuad(A a, B b, C c, D d, Object... indicted) { + ExpectedQuad withIndictedObject(Object indictedObject) { + for (var obj : indicted) { + if (obj == indictedObject) { + return this; + } + } + var newIndicted = Arrays.copyOf(indicted, indicted.length + 1); + newIndicted[indicted.length] = indictedObject; + return new ExpectedQuad<>(a, b, c, d, newIndicted); + } + } + + ExpectedQuad expected(A a, B b, C c, D d, Object... indicted) { + return new ExpectedQuad<>(a, b, c, d, indicted); + } + private void assertPrecompute(TestdataLavishSolution solution, - List> expectedValues, + List> expectedValues, Function> entityStreamSupplier) { + expectedValues = new ArrayList<>(expectedValues); var scoreDirector = buildScoreDirector(factory -> factory.precompute(entityStreamSupplier) .ifExists(TestdataLavishEntity.class) @@ -203,11 +223,16 @@ private void assertPrecompute(TestdataLavishSolution solution, scoreDirector.beforeVariableChanged(entity, "value"); entity.setValue(solution.getFirstValue()); scoreDirector.afterVariableChanged(entity, "value"); + var listIterator = expectedValues.listIterator(); + while (listIterator.hasNext()) { + var expected = listIterator.next(); + listIterator.set(expected.withIndictedObject(entity)); + } } assertScore(scoreDirector, expectedValues.stream() - .map(quad -> new Object[] { quad.a(), quad.b(), quad.c(), quad.d() }) - .map(AbstractConstraintStreamTest::assertMatch) + .map(expected -> assertMatch(expected.a, expected.b, expected.c, expected.d) + .withIndictedObjects(expected.indicted)) .toArray(AssertableMatch[]::new)); } @@ -224,7 +249,7 @@ public void ifExists() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Quadruple<>(entityWithGroup, value, value, value)), + assertPrecompute(solution, List.of(expected(entityWithGroup, value, value, value, entityWithGroup, value, entityGroup)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .join(TestdataLavishValue.class) @@ -247,7 +272,7 @@ public void ifNotExists() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Quadruple<>(entityWithoutGroup, value, value, value)), + assertPrecompute(solution, List.of(expected(entityWithoutGroup, value, value, value, entityWithoutGroup, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .join(TestdataLavishValue.class) @@ -270,7 +295,7 @@ public void groupBy() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Quadruple<>(entityGroup, 1L, 1L, 1L)), + assertPrecompute(solution, List.of(expected(entityGroup, 1L, 1L, 1L)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .filter(entity -> entity.getEntityGroup() != null) .groupBy(TestdataLavishEntity::getEntityGroup, @@ -292,8 +317,9 @@ public void flattenLast() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Quadruple<>(entityWithoutGroup, value, value, value), - new Quadruple<>(entityWithGroup, value, value, value)), + assertPrecompute(solution, List.of( + expected(entityWithoutGroup, value, value, value, entityWithoutGroup, value), + expected(entityWithGroup, value, value, value, entityWithGroup, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .groupBy(ConstraintCollectors.toList()) .flattenLast(entityList -> entityList) @@ -318,8 +344,9 @@ record ValueHolder(int value) { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Quadruple<>(new ValueHolder(1), value, value, value), - new Quadruple<>(new ValueHolder(2), value, value, value)), + assertPrecompute(solution, List.of( + expected(new ValueHolder(1), value, value, value, entity1, value), + expected(new ValueHolder(2), value, value, value, entity2, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .groupBy(ConstraintCollectors.toList()) .flattenLast(entityList -> entityList @@ -346,8 +373,8 @@ public void map() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Quadruple<>(entityGroup, value, value, value), - new Quadruple<>(entityGroup, value, value, value)), + assertPrecompute(solution, List.of(expected(entityGroup, value, value, value, entityWithGroup1, value), + expected(entityGroup, value, value, value, entityWithGroup2, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .join(TestdataLavishValue.class) @@ -373,8 +400,8 @@ public void concat() { solution.getValueList().add(value); assertPrecompute(solution, - List.of(new Quadruple<>(entityWithoutGroup, value, value, value), - new Quadruple<>(entityWithGroup, value, value, value)), + List.of(expected(entityWithoutGroup, value, value, value, entityWithoutGroup, value), + expected(entityWithGroup, value, value, value, entityWithGroup, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .join(TestdataLavishValue.class) @@ -403,7 +430,8 @@ public void distinct() { var value = new TestdataLavishValue(); solution.getValueList().add(value); - assertPrecompute(solution, List.of(new Quadruple<>(entityGroup, value, value, value)), + assertPrecompute(solution, + List.of(expected(entityGroup, value, value, value, entityWithGroup1, entityWithGroup2, value)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .join(TestdataLavishValue.class) @@ -432,9 +460,9 @@ public void complement() { solution.getValueList().add(value); assertPrecompute(solution, List.of( - new Quadruple<>(entityWithGroup1, value, value, value), - new Quadruple<>(entityWithGroup2, value, value, value), - new Quadruple<>(entityWithoutGroup, null, null, null)), + expected(entityWithGroup1, value, value, value, entityWithGroup1, value), + expected(entityWithGroup2, value, value, value, entityWithGroup2, value), + expected(entityWithoutGroup, null, null, null, entityWithoutGroup)), pf -> pf.forEachUnfiltered(TestdataLavishEntity.class) .join(TestdataLavishValue.class) .join(TestdataLavishValue.class) From 0381238a08c9de4bec2c0014c1035f0cd6f12475 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 28 Jul 2026 16:41:34 -0400 Subject: [PATCH 18/30] chore: merge fixes --- .../bavet/common/AbstractIfExistsNode.java | 43 ++++++++++++------- .../common/AbstractIndexedIfExistsNode.java | 16 +++---- .../common/AbstractUnindexedIfExistsNode.java | 4 +- .../stream/bavet/BavetRegressionTest.java | 36 ++++++++-------- .../bi/AbstractBiConstraintStreamTest.java | 12 +++--- .../AbstractQuadConstraintStreamTest.java | 12 ++++-- .../tri/AbstractTriConstraintStreamTest.java | 6 ++- .../uni/AbstractUniConstraintStreamTest.java | 8 ++-- 8 files changed, 76 insertions(+), 61 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java index 2a1750d8b10..0d19275a41d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java @@ -125,7 +125,7 @@ protected void killCounterLeft(ExistsCounter counter) { } } - protected void incrementCounterRightWithoutIndictment(ExistsCounter counter) { + protected void incrementCounterRight(ExistsCounter counter) { if (counter.countRight == 0) { if (shouldExist) { doInsertCounter(counter); @@ -133,27 +133,20 @@ protected void incrementCounterRightWithoutIndictment(ExistsCounter doRetractCounter(counter); } } // Else do not even propagate an update - // NOTE: By not propagating here, the left tuple's indicted objects can be stale - // if an element is removed. counter.countRight++; } protected void incrementCounterRightUpdatingIndictment(ExistsCounter counter, UniTuple rightTuple) { + IndictmentSource.addSupport(getId(), counter.getTuple(), rightTuple); if (counter.countRight == 0) { if (shouldExist) { doInsertCounter(counter); } else { doRetractCounter(counter); } - } else { - // count != 0, so only propagate if we are in an `ifExists` - if (shouldExist) { - doUpdateCounter(counter); - } - } // Else do not even propagate an update - // NOTE: By not propagating here, the left tuple's indicted objects can be stale - // if an element is removed. - IndictmentSource.addSupport(getId(), counter.leftTuple, rightTuple); + } else if (shouldExist) { + doUpdateCounter(counter); + } counter.countRight++; } @@ -174,6 +167,20 @@ protected void decrementCounterRight(ExistsCounter counter) { } // Else do not even propagate an update } + protected void decrementCounterRightUpdatingIndictment(ExistsCounter counter, UniTuple rightTuple) { + IndictmentSource.removeSupport(getId(), counter.getTuple(), rightTuple); + counter.countRight--; + if (counter.countRight == 0) { + if (shouldExist) { + doRetractCounter(counter); + } else { + doInsertCounter(counter); + } + } else if (shouldExist) { + doUpdateCounter(counter); + } + } + /** * Clears the left tracker list rooted at leftTuple's inputStoreIndexLeftTrackerList slot, * cross-removing each tracker from its right tuple's hidden list. @@ -222,17 +229,17 @@ private void removeRight(FilteringTracker tracker) { */ protected void clearRightTrackerList(UniTuple rightTuple) { FilteringTracker tracker = rightTuple.removeStore(inputStoreIndexRightTrackerList); - if (rightTuple.getIndictmentSource() != IndictmentSource.DISABLED) { + if (rightTuple.getIndictmentSource() == IndictmentSource.DISABLED) { while (tracker != null) { var next = tracker.rightNext; - decrementCounterRightUpdatingIndictment(tracker.counter, rightTuple); + decrementCounterRight(tracker.counter); removeFromLeft(tracker); tracker = next; } } else { while (tracker != null) { var next = tracker.rightNext; - decrementCounterRightWithoutIndictment(tracker.counter); + decrementCounterRightUpdatingIndictment(tracker.counter, rightTuple); removeFromLeft(tracker); tracker = next; } @@ -270,9 +277,12 @@ protected void updateCounterLeft(ExistsCounter counter, UniTuple(counter, rightTuple); linkLeft(tracker); linkRight(tracker); + } else { + IndictmentSource.removeSupport(getId(), counter.getTuple(), rightTuple); } } @@ -334,7 +344,8 @@ protected void updateCounterRight(ExistsCounter counter, UniTuple(counter, rightTuple); linkLeft(tracker); linkRight(tracker); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java index b08226505a1..18a26583714 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java @@ -217,7 +217,7 @@ private void updateCounterLeft(UniTuple rightTuple, Object compositeKey) // To prevent creating a dynamic lambda on the hot path, // only call the 2-args version when indictments are enabled if (rightTuple.getIndictmentSource() == IndictmentSource.DISABLED) { - forEachLeftCounter(rightTuple, compositeKey, this::incrementCounterRightWithoutIndictment); + forEachLeftCounter(rightTuple, compositeKey, this::incrementCounterRight); } else { forEachLeftCounter(rightTuple, compositeKey, counter -> incrementCounterRightUpdatingIndictment(counter, rightTuple)); @@ -261,14 +261,7 @@ public final void updateRight(UniTuple rightTuple) { indexerRight.remove(oldCompositeKey, entry); } if (!isFiltering) { - // To prevent creating a dynamic lambda on the hot path, - // only call the 2-args version when indictments are enabled - if (rightTuple.getIndictmentSource() == IndictmentSource.DISABLED) { - forEachLeftCounter(rightTuple, oldCompositeKey, this::decrementCounterRightWithoutIndictment); - } else { - forEachLeftCounter(rightTuple, oldCompositeKey, - counter -> decrementCounterRightUpdatingIndictment(counter, rightTuple)); - } + forEachLeftCounter(rightTuple, oldCompositeKey, this::decrementCounterRight); } else { clearRightTrackerList(rightTuple); } @@ -295,9 +288,10 @@ public final void retractRight(UniTuple rightTuple) { // To prevent creating a dynamic lambda on the hot path, // only call the 2-args version when indictments are enabled if (rightTuple.getIndictmentSource() == IndictmentSource.DISABLED) { - bucket.forEachLeft(compositeKey, this::decrementCounterRightWithoutIndictment); + bucket.forEachLeft(compositeKey, this::decrementCounterRight); } else { - bucket.forEachLeft(compositeKey, counter -> decrementCounterRightUpdatingIndictment(counter, rightTuple)); + bucket.forEachLeft(compositeKey, + leftTuple -> decrementCounterRightUpdatingIndictment(leftTuple, rightTuple)); } } else { clearRightTrackerList(rightTuple); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java index c50579c3c78..38480798f63 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedIfExistsNode.java @@ -105,7 +105,7 @@ public final void insertRight(UniTuple rightTuple) { // To prevent creating a dynamic lambda on the hot path, // only call the 2-args version when indictments are enabled if (rightTuple.getIndictmentSource() == IndictmentSource.DISABLED) { - counterList.forEach(this::incrementCounterRightWithoutIndictment); + counterList.forEach(this::incrementCounterRight); } else { counterList.forEach(counter -> incrementCounterRightUpdatingIndictment(counter, rightTuple)); } @@ -146,7 +146,7 @@ public final void retractRight(UniTuple rightTuple) { // To prevent creating a dynamic lambda on the hot path, // only call the 2-args version when indictments are enabled if (rightTuple.getIndictmentSource() == IndictmentSource.DISABLED) { - counterList.forEach(this::decrementCounterRightWithoutIndictment); + counterList.forEach(this::decrementCounterRight); } else { counterList.forEach(counter -> decrementCounterRightUpdatingIndictment(counter, rightTuple)); } 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 5348bf69fb2..7e79282e028 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 @@ -397,7 +397,7 @@ public void filteringJoinNullConflictRight() { scoreDirector.afterListVariableElementAssigned(entity, "valueList", value1); assertScore(scoreDirector, - assertMatch(value1, value1)); + assertMatch(value1, value1).withIndictedObjects(value1)); // Unassign+assign and check result. var variableDescriptor = scoreDirector.getSolutionDescriptor() @@ -411,7 +411,7 @@ public void filteringJoinNullConflictRight() { scoreDirector.afterListVariableElementAssigned(variableDescriptor, value2); assertScore(scoreDirector, - assertMatch(value2, value2)); + assertMatch(value2, value2).withIndictedObjects(value2)); // Reassign and check result. scoreDirector.beforeListVariableElementAssigned(variableDescriptor, value1); @@ -423,7 +423,7 @@ public void filteringJoinNullConflictRight() { scoreDirector.afterListVariableElementAssigned(variableDescriptor, value1); assertScore(scoreDirector, - assertMatch(value1, value1)); + assertMatch(value1, value1).withIndictedObjects(value1)); } } @@ -458,7 +458,7 @@ public void filteringJoinNullConflictRightUnindexed() { scoreDirector.afterListVariableElementAssigned(entity, "valueList", value1); assertScore(scoreDirector, - assertMatch(value1, value1)); + assertMatch(value1, value1).withIndictedObjects(value1)); // Unassign+assign and check result. var variableDescriptor = scoreDirector.getSolutionDescriptor() @@ -472,7 +472,7 @@ public void filteringJoinNullConflictRightUnindexed() { scoreDirector.afterListVariableElementAssigned(variableDescriptor, value2); assertScore(scoreDirector, - assertMatch(value2, value2)); + assertMatch(value2, value2).withIndictedObjects(value2)); // Reassign and check result. scoreDirector.beforeListVariableElementAssigned(variableDescriptor, value1); @@ -484,7 +484,7 @@ public void filteringJoinNullConflictRightUnindexed() { scoreDirector.afterListVariableElementAssigned(variableDescriptor, value1); assertScore(scoreDirector, - assertMatch(value1, value1)); + assertMatch(value1, value1).withIndictedObjects(value1)); } } @@ -521,7 +521,7 @@ public void filteringJoinNullConflictRightViaIfExists() { scoreDirector.afterListVariableElementAssigned(entity, "valueList", value1); assertScore(scoreDirector, - assertMatch(value1, value1)); + assertMatch(value1, value1).withIndictedObjects(value1)); // Unassign+assign and check result. var variableDescriptor = scoreDirector.getSolutionDescriptor() @@ -535,7 +535,7 @@ public void filteringJoinNullConflictRightViaIfExists() { scoreDirector.afterListVariableElementAssigned(variableDescriptor, value2); assertScore(scoreDirector, - assertMatch(value2, value2)); + assertMatch(value2, value2).withIndictedObjects(value2)); // Reassign and check result. scoreDirector.beforeListVariableElementAssigned(variableDescriptor, value1); @@ -547,7 +547,7 @@ public void filteringJoinNullConflictRightViaIfExists() { scoreDirector.afterListVariableElementAssigned(variableDescriptor, value1); assertScore(scoreDirector, - assertMatch(value1, value1)); + assertMatch(value1, value1).withIndictedObjects(value1)); } } @@ -586,10 +586,10 @@ public void filteringJoinNullConflictRightUnassignOne() { scoreDirector.afterListVariableElementAssigned(entity, "valueList", value1); assertScore(scoreDirector, - assertMatch(value1, value1), - assertMatch(value1, value2), - assertMatch(value2, value1), - assertMatch(value2, value2)); + assertMatch(value1, value1).withIndictedObjects(value1), + assertMatch(value1, value2).withIndictedObjects(value1, value2), + assertMatch(value2, value1).withIndictedObjects(value1, value2), + assertMatch(value2, value2).withIndictedObjects(value2)); // Unassign and check result. var variableDescriptor = scoreDirector.getSolutionDescriptor() @@ -601,7 +601,7 @@ public void filteringJoinNullConflictRightUnassignOne() { scoreDirector.afterListVariableElementUnassigned(variableDescriptor, value1); assertScore(scoreDirector, - assertMatch(value2, value2)); + assertMatch(value2, value2).withIndictedObjects(value2)); // Reassign and check result. scoreDirector.beforeListVariableElementAssigned(variableDescriptor, value1); @@ -611,10 +611,10 @@ public void filteringJoinNullConflictRightUnassignOne() { scoreDirector.afterListVariableElementAssigned(variableDescriptor, value1); assertScore(scoreDirector, - assertMatch(value1, value1), - assertMatch(value1, value2), - assertMatch(value2, value1), - assertMatch(value2, value2)); + assertMatch(value1, value1).withIndictedObjects(value1), + assertMatch(value1, value2).withIndictedObjects(value1, value2), + assertMatch(value2, value1).withIndictedObjects(value1, value2), + assertMatch(value2, value2).withIndictedObjects(value2)); } } 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 87fcbeed658..1642ba043f6 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 @@ -776,8 +776,8 @@ public void ifExists_filterOnAssignedValue_unassignOne() { scoreDirector.afterListVariableElementAssigned(entity, "valueList", value1); assertScore(scoreDirector, - assertMatch(entity, value1).withIndictedObjects(entity, value1), - assertMatch(entity, value2).withIndictedObjects(entity, value2)); + assertMatch(entity, value1).withIndictedObjects(entity, value1, value2), + assertMatch(entity, value2).withIndictedObjects(entity, value1, value2)); // Unassign and check result. var variableDescriptor = scoreDirector.getSolutionDescriptor() @@ -799,7 +799,7 @@ public void ifExists_filterOnAssignedValue_unassignOne() { scoreDirector.afterListVariableElementAssigned(variableDescriptor, value1); assertScore(scoreDirector, - assertMatch(entity, value1).withIndictedObjects(entity, value1), + assertMatch(entity, value1).withIndictedObjects(entity, value1, value2), assertMatch(entity, value2).withIndictedObjects(entity, value1, value2)); } @@ -917,7 +917,8 @@ public void ifExists_0Join1Filter() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), entity2)); + assertMatch(solution.getFirstEntity(), entity2) + .withIndictedObjects(solution.getFirstEntity(), entity2, solution.getFirstEntityGroup())); // Incremental var toRemove = solution.getFirstEntityGroup(); @@ -991,7 +992,8 @@ public void ifExists_1Join1Filter() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2)); + assertMatch(entity1, entity2) + .withIndictedObjects(entity1, entity2, entityGroup)); // Incremental scoreDirector.beforeProblemFactRemoved(entityGroup); 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 eb8eb31b9c7..48b5bee5d68 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 @@ -210,9 +210,15 @@ public void ifExists_0Join1Filter() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity2, entity1, entityGroup, solution.getFirstValue()), - assertMatch(entity2, solution.getFirstEntity(), solution.getFirstEntityGroup(), solution.getFirstValue()), - assertMatch(entity1, solution.getFirstEntity(), solution.getFirstEntityGroup(), solution.getFirstValue())); + assertMatch(entity2, entity1, entityGroup, solution.getFirstValue()) + .withIndictedObjects(entity2, entity1, entityGroup, solution.getFirstValue(), + solution.getFirstValue().getValueGroup()), + assertMatch(entity2, solution.getFirstEntity(), solution.getFirstEntityGroup(), solution.getFirstValue()) + .withIndictedObjects(entity2, solution.getFirstEntity(), solution.getFirstEntityGroup(), + solution.getFirstValue(), solution.getFirstValue().getValueGroup()), + assertMatch(entity1, solution.getFirstEntity(), solution.getFirstEntityGroup(), solution.getFirstValue()) + .withIndictedObjects(entity1, solution.getFirstEntity(), solution.getFirstEntityGroup(), + solution.getFirstValue(), solution.getFirstValue().getValueGroup())); // Incremental var toRemove = solution.getFirstValueGroup(); 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 d027d54c48d..6e79563b8c7 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 @@ -414,7 +414,8 @@ public void ifExists_0Join1Filter() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(entity1, entity2, solution.getFirstEntity())); + assertMatch(entity1, entity2, solution.getFirstEntity()) + .withIndictedObjects(entity1, entity2, solution.getFirstEntity(), solution.getFirstEntityGroup())); // Incremental var toRemove = solution.getFirstEntityGroup(); @@ -494,7 +495,8 @@ public void ifExists_1Join1Filter() { // From scratch scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, - assertMatch(solution.getFirstEntity(), entity2, entity1)); + assertMatch(solution.getFirstEntity(), entity2, entity1) + .withIndictedObjects(solution.getFirstEntity(), entity2, entity1, entityGroup)); // Incremental scoreDirector.beforeProblemFactRemoved(entityGroup); 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 64645c474bf..da4c87235a5 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 @@ -620,7 +620,7 @@ public void ifExistsOther_1Join0Filter() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, assertMatch(solution.getFirstEntity()).withIndictedObjects(solution.getFirstEntity(), entity2), - assertMatch(entity2).withIndictedObjects(entity2)); + assertMatch(entity2).withIndictedObjects(solution.getFirstEntity(), entity2)); // Incremental scoreDirector.beforeProblemPropertyChanged(entity2); @@ -628,7 +628,7 @@ public void ifExistsOther_1Join0Filter() { scoreDirector.afterProblemPropertyChanged(entity2); assertScore(scoreDirector, assertMatch(entity1).withIndictedObjects(entity1, entity2), - assertMatch(entity2).withIndictedObjects(entity2)); + assertMatch(entity2).withIndictedObjects(entity1, entity2)); } @Override @@ -656,7 +656,7 @@ public void ifExistsDoesNotIncludeUnassigned() { scoreDirector.setWorkingSolution(solution); assertScore(scoreDirector, assertMatch(solution.getFirstEntity()).withIndictedObjects(solution.getFirstEntity(), entity2), - assertMatch(entity2).withIndictedObjects(entity2)); + assertMatch(entity2).withIndictedObjects(solution.getFirstEntity(), entity2)); // Incremental scoreDirector.beforeProblemPropertyChanged(entity2); @@ -664,7 +664,7 @@ public void ifExistsDoesNotIncludeUnassigned() { scoreDirector.afterProblemPropertyChanged(entity2); assertScore(scoreDirector, assertMatch(entity1).withIndictedObjects(entity1, entity2), - assertMatch(entity2).withIndictedObjects(entity2)); + assertMatch(entity2).withIndictedObjects(entity1, entity2)); } @Override From 235c351b2da3e94c6d452379c24b7c18e01b4d1d Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Wed, 29 Jul 2026 22:37:49 -0400 Subject: [PATCH 19/30] chore: review comments --- .../core/impl/bavet/common/TupleRecorder.java | 8 +------- .../impl/bavet/common/tuple/UniversalTuple.java | 8 -------- .../common/tuple/indictment/IndictmentSource.java | 9 +++++++++ .../impl/score/constraint/ConstraintMatch.java | 8 ++++---- .../score/constraint/ConstraintMatchTotal.java | 7 +++++-- .../common/inliner/ConstraintMatchSupplier.java | 9 ++++----- .../impl/score/constraint/ConstraintMatchTest.java | 3 ++- .../common/AbstractConstraintStreamTest.java | 14 ++++++++------ 8 files changed, 33 insertions(+), 33 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/TupleRecorder.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/TupleRecorder.java index 2ae88ccdb76..52f3106fa41 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/TupleRecorder.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/TupleRecorder.java @@ -1,8 +1,6 @@ package ai.timefold.solver.core.impl.bavet.common; -import java.util.ArrayList; import java.util.IdentityHashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.function.UnaryOperator; @@ -18,13 +16,9 @@ public record TupleRecorder(List recordedTupleList public void recordTuple(Tuple_ tuple) { var outTuple = inputTupleToOutputTuple.computeIfAbsent(tuple, mapper); if (tuple.getIndictmentSource() != IndictmentSource.DISABLED) { - if (outTuple.getIndictmentSource() == IndictmentSource.DISABLED) { - outTuple.setIndictmentSource( - new IndictmentSource.AggregateIndictmentSource(new ArrayList<>(), new LinkedHashMap<>())); - } // Precompute uses an independent node network, so we need to aggregate its supports to not interfere with the // outer node network support - var aggregateIndictmentSource = (IndictmentSource.AggregateIndictmentSource) outTuple.getIndictmentSource(); + var aggregateIndictmentSource = IndictmentSource.getPrecomputeAggregation(outTuple); tuple.getIndictmentSource() .visitAllSources(source -> aggregateIndictmentSource.sourceList().add(IndictmentSource.of(source))); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java index 63cadf46844..24337b2d42b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/UniversalTuple.java @@ -30,14 +30,6 @@ final class UniversalTuple this.store = storeSize > 0 ? new Object[storeSize] : EMPTY_STORE; } - int cardinality() { - return cardinality; - } - - int storeSize() { - return store.length; - } - @Override public void setA(@Nullable A a) { this.a = a; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java index 69f565f2421..50d92d10c64 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java @@ -68,6 +68,15 @@ static IndictmentSource joining(Tuple left, Tuple right) { return new JoinedIndictmentSource(left.getIndictmentSource(), right.getIndictmentSource(), new LinkedHashMap<>()); } + static AggregateIndictmentSource getPrecomputeAggregation(Tuple outTuple) { + if (outTuple.getIndictmentSource() != DISABLED) { + return (AggregateIndictmentSource) outTuple.getIndictmentSource(); + } + var out = new AggregateIndictmentSource(new ArrayList<>(), new LinkedHashMap<>()); + outTuple.setIndictmentSource(out); + return out; + } + static IndictmentSource aggregating(Tuple elementTuple, Tuple groupTuple) { if (elementTuple.getIndictmentSource() == DISABLED) { return DISABLED; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java index f977608580e..e49d15a2830 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatch.java @@ -2,7 +2,7 @@ import static java.util.Objects.requireNonNull; -import java.util.List; +import java.util.SequencedSet; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.stream.ConstraintJustification; @@ -30,7 +30,7 @@ public final class ConstraintMatch> implements Comp private final ConstraintRef constraintRef; private final @Nullable ConstraintJustification justification; - private final @Nullable List indictedObjects; + private final @Nullable SequencedSet indictedObjects; private final Score_ score; /** @@ -39,7 +39,7 @@ public final class ConstraintMatch> implements Comp * @param score penalty or reward associated with the constraint match */ public ConstraintMatch(ConstraintRef constraintRef, @Nullable ConstraintJustification justification, - @Nullable List indictedObjects, Score_ score) { + @Nullable SequencedSet indictedObjects, Score_ score) { this.constraintRef = requireNonNull(constraintRef); this.justification = justification; this.indictedObjects = indictedObjects; @@ -67,7 +67,7 @@ public ConstraintRef getConstraintRef() { return (Justification_) justification; } - public @Nullable List getIndictedObjects() { + public @Nullable SequencedSet getIndictedObjects() { return indictedObjects; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTotal.java b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTotal.java index 4ed2b3e0014..293083ae65a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTotal.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTotal.java @@ -4,6 +4,7 @@ import java.util.LinkedHashSet; import java.util.List; +import java.util.SequencedSet; import java.util.Set; import ai.timefold.solver.core.api.score.Score; @@ -68,7 +69,8 @@ public Score_ getScore() { * @return never null */ public ConstraintMatch addConstraintMatch(List justifications, Score_ score) { - return addConstraintMatch(DefaultConstraintJustification.of(score, justifications), justifications, score); + return addConstraintMatch(DefaultConstraintJustification.of(score, justifications), + new LinkedHashSet<>(justifications), score); } /** @@ -78,7 +80,8 @@ public ConstraintMatch addConstraintMatch(List justifications, S * @param score never null * @return never null */ - public ConstraintMatch addConstraintMatch(ConstraintJustification justification, List indictedObjects, + public ConstraintMatch addConstraintMatch(ConstraintJustification justification, + SequencedSet indictedObjects, Score_ score) { var constraintMatch = new ConstraintMatch<>(constraintRef, justification, indictedObjects, score); addConstraintMatch(constraintMatch); 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 623e37f127d..d475e8925ff 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 @@ -1,11 +1,10 @@ package ai.timefold.solver.core.impl.score.stream.common.inliner; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; -import java.util.List; import java.util.Objects; +import java.util.SequencedSet; import java.util.function.BiFunction; import java.util.stream.Collectors; @@ -43,7 +42,7 @@ public interface ConstraintMatchSupplier> extends BiFunction> { - static @Nullable List collectIndictments(Constraint constraint, Tuple tuple) { + static @Nullable SequencedSet collectIndictments(Constraint constraint, Tuple tuple) { if (tuple.getIndictmentSource() == IndictmentSource.DISABLED) { return null; } @@ -51,7 +50,7 @@ public interface ConstraintMatchSupplier> var abstractConstraint = (AbstractConstraint) constraint; var involvedNodeIds = Objects.requireNonNull(abstractConstraint.getInvolvedNodeIds()); tuple.getIndictmentSource().visitSources(involvedNodeIds, out::add); - return new ArrayList<>(out); + return out; } /** @@ -62,7 +61,7 @@ public interface ConstraintMatchSupplier> */ static > ConstraintMatchSupplier empty() { return (constraint, impact) -> new ConstraintMatch<>(constraint.getConstraintRef(), null, - Collections.emptyList(), impact); + Collections.emptySortedSet(), impact); } static > ConstraintMatchSupplier of( diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTest.java index f7e31759389..2309f580251 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/constraint/ConstraintMatchTest.java @@ -3,6 +3,7 @@ import static ai.timefold.solver.core.api.score.SimpleScore.ONE; import static ai.timefold.solver.core.api.score.SimpleScore.ZERO; +import java.util.LinkedHashSet; import java.util.List; import ai.timefold.solver.core.api.score.Score; @@ -28,7 +29,7 @@ private > ConstraintMatch buildConstraintMa Object... facts) { return new ConstraintMatch<>(ConstraintRef.of(constraintName), DefaultConstraintJustification.of(score, facts), - List.of(facts), + new LinkedHashSet<>(List.of(facts)), score); } 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 6487bc17450..67805db46ca 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 @@ -4,8 +4,10 @@ import static org.assertj.core.api.Assertions.fail; import java.util.Arrays; +import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; +import java.util.SequencedSet; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -129,17 +131,17 @@ protected static class AssertableMatch { private final int score; private final ConstraintRef constraintRef; private final List justificationList; - private List indictmentList; + private SequencedSet indictmentSet; public AssertableMatch(int score, ConstraintRef constraintRef, Object... justifications) { this.justificationList = Arrays.asList(justifications); this.constraintRef = constraintRef; this.score = score; - this.indictmentList = justificationList; + this.indictmentSet = new LinkedHashSet<>(justificationList); } public AssertableMatch withIndictedObjects(Object... indictedObjects) { - this.indictmentList = Arrays.asList(indictedObjects); + this.indictmentSet = new LinkedHashSet<>(Arrays.asList(indictedObjects)); return this; } @@ -173,16 +175,16 @@ public boolean isEqualTo(ConstraintMatch constraintMatch, boolean indictments return true; } var indictedObjects = constraintMatch.getIndictedObjects(); - if (indictedObjects.size() != indictmentList.size()) { + if (indictedObjects.size() != indictmentSet.size()) { return false; } - return indictmentList.containsAll(indictedObjects); + return indictmentSet.containsAll(indictedObjects); } @Override public String toString() { return "%s %s=%d (indicting %s)".formatted(constraintRef, justificationList, score, - indictmentList); + indictmentSet); } } From 2759a79e0ee6ccb73893073bce6684100e262d37 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Mon, 10 Aug 2026 18:13:04 -0400 Subject: [PATCH 20/30] chore: merge fixes --- .../bavet/common/AbstractIfExistsNode.java | 7 +- .../common/AbstractIndexedIfExistsNode.java | 15 +- .../stream/bavet/BavetRegressionTest.java | 161 ++++++++++++------ 3 files changed, 126 insertions(+), 57 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java index 0d19275a41d..c6c6fc50b1f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java @@ -152,7 +152,6 @@ protected void incrementCounterRightUpdatingIndictment(ExistsCounter protected void decrementCounterRight(ExistsCounter counter) { counter.countRight--; - IndictmentSource.removeSupport(getId(), counter.leftTuple, rightTuple); if (counter.countRight == 0) { if (shouldExist) { doRetractCounter(counter); @@ -233,14 +232,14 @@ protected void clearRightTrackerList(UniTuple rightTuple) { while (tracker != null) { var next = tracker.rightNext; decrementCounterRight(tracker.counter); - removeFromLeft(tracker); + removeLeft(tracker); tracker = next; } } else { while (tracker != null) { var next = tracker.rightNext; decrementCounterRightUpdatingIndictment(tracker.counter, rightTuple); - removeFromLeft(tracker); + removeLeft(tracker); tracker = next; } } @@ -349,6 +348,8 @@ protected void updateCounterRight(ExistsCounter counter, UniTuple(counter, rightTuple); linkLeft(tracker); linkRight(tracker); + } else { + IndictmentSource.removeSupport(getId(), counter.getTuple(), rightTuple); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java index 18a26583714..5e40cdfc9bb 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java @@ -98,6 +98,11 @@ public final void insertLeft(LeftTuple_ leftTuple) { if (!isFiltering) { counter.countRight = rightSize(leftTuple, compositeKey); initCounterLeft(counter); + if (leftTuple.getIndictmentSource() != IndictmentSource.DISABLED) { + forEachRightFromLeft(leftTuple, compositeKey, rightTuple -> { + IndictmentSource.addSupport(getId(), leftTuple, rightTuple); + }); + } } else { // Defer the cross-match (the opposite-side read) to this node's own layer turn instead of computing it now, // at whatever layer the parent that produced leftTuple happens to be in. @@ -144,6 +149,7 @@ public final void updateLeft(LeftTuple_ leftTuple) { // See AbstractCrossMatchNode's pendingLeft/pendingRight javadoc. clearLeftTrackerList(leftTuple); counter.countRight = 0; + IndictmentSource.clearSupport(getId(), leftTuple); crossMatchLeft(leftTuple); } } else { @@ -151,6 +157,7 @@ public final void updateLeft(LeftTuple_ leftTuple) { var sameBucket = reuseBucketEligible && fusedEqualIndex.isSameBucket(oldCompositeKey, newCompositeKey); updateIndexerLeft(oldCompositeKey, counterEntry, leftTuple, sameBucket); // Also clears the tracker list. counter.countRight = 0; + IndictmentSource.clearSupport(getId(), leftTuple); leftTuple.setStore(inputStoreIndexLefCompositeKey, newCompositeKey); leftTuple.setStore(inputStoreIndexLeftCounterEntry, putLeftCounter(leftTuple, newCompositeKey, counter, sameBucket)); @@ -159,6 +166,11 @@ public final void updateLeft(LeftTuple_ leftTuple) { } else { counter.countRight = rightSize(leftTuple, newCompositeKey); updateCounterLeft(counter); + if (leftTuple.getIndictmentSource() != IndictmentSource.DISABLED) { + forEachRightFromLeft(leftTuple, newCompositeKey, rightTuple -> { + IndictmentSource.addSupport(getId(), leftTuple, rightTuple); + }); + } } } } @@ -302,7 +314,7 @@ public final void retractRight(UniTuple rightTuple) { // To prevent creating a dynamic lambda on the hot path, // only call the 2-args version when indictments are enabled if (rightTuple.getIndictmentSource() == IndictmentSource.DISABLED) { - indexerLeft.forEach(compositeKey, this::decrementCounterRightWithoutIndictment); + indexerLeft.forEach(compositeKey, this::decrementCounterRight); } else { indexerLeft.forEach(compositeKey, counter -> decrementCounterRightUpdatingIndictment(counter, rightTuple)); } @@ -387,6 +399,7 @@ protected void reconcilePendingLeft(LeftTuple_ leftTuple) { ListEntry> counterEntry = leftTuple.getStore(inputStoreIndexLeftCounterEntry); var counter = counterEntry.element(); clearLeftTrackerList(leftTuple); + IndictmentSource.clearSupport(getId(), leftTuple); counter.countRight = 0; forEachRightFromLeft(leftTuple, compositeKey, rightTuple -> updateCounterLeft(counter, rightTuple)); updateCounterLeft(counter); 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 7e79282e028..8ead3e00ad7 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 @@ -1224,22 +1224,38 @@ public void filteringIfExistsNullConflictDeepLeftUnassignOne() { scoreDirector.afterListVariableElementAssigned(entity, "valueList", value1); assertScore(scoreDirector, - assertMatch(value1, value1, value1, value1), - assertMatch(value1, value1, value1, value2), - assertMatch(value1, value1, value2, value1), - assertMatch(value1, value1, value2, value2), - assertMatch(value1, value2, value1, value1), - assertMatch(value1, value2, value1, value2), - assertMatch(value1, value2, value2, value1), - assertMatch(value1, value2, value2, value2), - assertMatch(value2, value1, value1, value1), - assertMatch(value2, value1, value1, value2), - assertMatch(value2, value1, value2, value1), - assertMatch(value2, value1, value2, value2), - assertMatch(value2, value2, value1, value1), - assertMatch(value2, value2, value1, value2), - assertMatch(value2, value2, value2, value1), - assertMatch(value2, value2, value2, value2)); + assertMatch(value1, value1, value1, value1) + .withIndictedObjects(value1, value2), + assertMatch(value1, value1, value1, value2) + .withIndictedObjects(value1, value2), + assertMatch(value1, value1, value2, value1) + .withIndictedObjects(value1, value2), + assertMatch(value1, value1, value2, value2) + .withIndictedObjects(value1, value2), + assertMatch(value1, value2, value1, value1) + .withIndictedObjects(value1, value2), + assertMatch(value1, value2, value1, value2) + .withIndictedObjects(value1, value2), + assertMatch(value1, value2, value2, value1) + .withIndictedObjects(value1, value2), + assertMatch(value1, value2, value2, value2) + .withIndictedObjects(value1, value2), + assertMatch(value2, value1, value1, value1) + .withIndictedObjects(value1, value2), + assertMatch(value2, value1, value1, value2) + .withIndictedObjects(value1, value2), + assertMatch(value2, value1, value2, value1) + .withIndictedObjects(value1, value2), + assertMatch(value2, value1, value2, value2) + .withIndictedObjects(value1, value2), + assertMatch(value2, value2, value1, value1) + .withIndictedObjects(value1, value2), + assertMatch(value2, value2, value1, value2) + .withIndictedObjects(value1, value2), + assertMatch(value2, value2, value2, value1) + .withIndictedObjects(value1, value2), + assertMatch(value2, value2, value2, value2) + .withIndictedObjects(value1, value2)); // Unassign and check result. var variableDescriptor = scoreDirector.getSolutionDescriptor() @@ -1302,8 +1318,10 @@ public void filteringIfExistsNullConflictDeepRightUnassignOne() { scoreDirector.afterListVariableElementAssigned(entity, "valueList", value1); assertScore(scoreDirector, - assertMatch(value1), - assertMatch(value2)); + assertMatch(value1) + .withIndictedObjects(value1, value2), + assertMatch(value2) + .withIndictedObjects(value1, value2)); // Unassign and check result. var variableDescriptor = scoreDirector.getSolutionDescriptor() @@ -1374,22 +1392,38 @@ public void filteringIfExistsNullConflictThroughQuadJoinReorder() { scoreDirector.afterListVariableElementAssigned(entity, "valueList", value1); assertScore(scoreDirector, - assertMatch(value1, value1, value1, value1), - assertMatch(value1, value1, value1, value2), - assertMatch(value1, value1, value2, value1), - assertMatch(value1, value1, value2, value2), - assertMatch(value1, value2, value1, value1), - assertMatch(value1, value2, value1, value2), - assertMatch(value1, value2, value2, value1), - assertMatch(value1, value2, value2, value2), - assertMatch(value2, value1, value1, value1), - assertMatch(value2, value1, value1, value2), - assertMatch(value2, value1, value2, value1), - assertMatch(value2, value1, value2, value2), - assertMatch(value2, value2, value1, value1), - assertMatch(value2, value2, value1, value2), - assertMatch(value2, value2, value2, value1), - assertMatch(value2, value2, value2, value2)); + assertMatch(value1, value1, value1, value1) + .withIndictedObjects(value1, value2), + assertMatch(value1, value1, value1, value2) + .withIndictedObjects(value1, value2), + assertMatch(value1, value1, value2, value1) + .withIndictedObjects(value1, value2), + assertMatch(value1, value1, value2, value2) + .withIndictedObjects(value1, value2), + assertMatch(value1, value2, value1, value1) + .withIndictedObjects(value1, value2), + assertMatch(value1, value2, value1, value2) + .withIndictedObjects(value1, value2), + assertMatch(value1, value2, value2, value1) + .withIndictedObjects(value1, value2), + assertMatch(value1, value2, value2, value2) + .withIndictedObjects(value1, value2), + assertMatch(value2, value1, value1, value1) + .withIndictedObjects(value1, value2), + assertMatch(value2, value1, value1, value2) + .withIndictedObjects(value1, value2), + assertMatch(value2, value1, value2, value1) + .withIndictedObjects(value1, value2), + assertMatch(value2, value1, value2, value2) + .withIndictedObjects(value1, value2), + assertMatch(value2, value2, value1, value1) + .withIndictedObjects(value1, value2), + assertMatch(value2, value2, value1, value2) + .withIndictedObjects(value1, value2), + assertMatch(value2, value2, value2, value1) + .withIndictedObjects(value1, value2), + assertMatch(value2, value2, value2, value2) + .withIndictedObjects(value1, value2)); // Swap value1 and value2's positions in place -- pure UPDATE, no assign/unassign. scoreDirector.beforeListVariableChanged(entity, "valueList", 0, 2); @@ -1397,22 +1431,38 @@ public void filteringIfExistsNullConflictThroughQuadJoinReorder() { scoreDirector.afterListVariableChanged(entity, "valueList", 0, 2); assertScore(scoreDirector, - assertMatch(value1, value1, value1, value1), - assertMatch(value1, value1, value1, value2), - assertMatch(value1, value1, value2, value1), - assertMatch(value1, value1, value2, value2), - assertMatch(value1, value2, value1, value1), - assertMatch(value1, value2, value1, value2), - assertMatch(value1, value2, value2, value1), - assertMatch(value1, value2, value2, value2), - assertMatch(value2, value1, value1, value1), - assertMatch(value2, value1, value1, value2), - assertMatch(value2, value1, value2, value1), - assertMatch(value2, value1, value2, value2), - assertMatch(value2, value2, value1, value1), - assertMatch(value2, value2, value1, value2), - assertMatch(value2, value2, value2, value1), - assertMatch(value2, value2, value2, value2)); + assertMatch(value1, value1, value1, value1) + .withIndictedObjects(value1, value2), + assertMatch(value1, value1, value1, value2) + .withIndictedObjects(value1, value2), + assertMatch(value1, value1, value2, value1) + .withIndictedObjects(value1, value2), + assertMatch(value1, value1, value2, value2) + .withIndictedObjects(value1, value2), + assertMatch(value1, value2, value1, value1) + .withIndictedObjects(value1, value2), + assertMatch(value1, value2, value1, value2) + .withIndictedObjects(value1, value2), + assertMatch(value1, value2, value2, value1) + .withIndictedObjects(value1, value2), + assertMatch(value1, value2, value2, value2) + .withIndictedObjects(value1, value2), + assertMatch(value2, value1, value1, value1) + .withIndictedObjects(value1, value2), + assertMatch(value2, value1, value1, value2) + .withIndictedObjects(value1, value2), + assertMatch(value2, value1, value2, value1) + .withIndictedObjects(value1, value2), + assertMatch(value2, value1, value2, value2) + .withIndictedObjects(value1, value2), + assertMatch(value2, value2, value1, value1) + .withIndictedObjects(value1, value2), + assertMatch(value2, value2, value1, value2) + .withIndictedObjects(value1, value2), + assertMatch(value2, value2, value2, value1) + .withIndictedObjects(value1, value2), + assertMatch(value2, value2, value2, value2) + .withIndictedObjects(value1, value2)); } } @@ -2392,7 +2442,8 @@ public void filteringJoinNullConflictThroughLargeGroupUnassignOne() { } assertScore(scoreDirector, - values.stream().map(value -> assertMatch(entity, value)).toArray(AssertableMatch[]::new)); + values.stream().map(value -> assertMatch(entity, value) + .withIndictedObjects(values.toArray())).toArray(AssertableMatch[]::new)); // Unassign one of the ten contributors; the group (keyed by the shared entity) still has nine // and survives. @@ -2408,7 +2459,10 @@ public void filteringJoinNullConflictThroughLargeGroupUnassignOne() { assertScore(scoreDirector, values.stream() .filter(value -> value != valueToUnassign) - .map(value -> assertMatch(entity, value)) + .map(value -> assertMatch(entity, value) + .withIndictedObjects(values.stream() + .filter(otherValue -> otherValue != valueToUnassign) + .toArray())) .toArray(AssertableMatch[]::new)); } } @@ -2541,7 +2595,8 @@ public void filteringJoinNullConflictThroughLargeGroupUnassignAll() { } assertScore(scoreDirector, - values.stream().map(value -> assertMatch(entity, value)).toArray(AssertableMatch[]::new)); + values.stream().map(value -> assertMatch(entity, value) + .withIndictedObjects(values.toArray())).toArray(AssertableMatch[]::new)); // Unassign every contributor at once; the group must fully retract, with nothing left downstream. var variableDescriptor = scoreDirector.getSolutionDescriptor() From 5333b0801fb3088893b0b6b6d71679b3370acd71 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 22 Sep 2026 14:13:37 -0400 Subject: [PATCH 21/30] chore: add preview feature to enable solving with indictments enabled --- .../timefold/solver/core/config/solver/PreviewFeature.java | 6 +++++- .../solver/core/impl/solver/DefaultSolverFactory.java | 4 +++- core/src/main/resources/solver.xsd | 1 + tools/benchmark/src/main/resources/benchmark.xsd | 2 ++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/config/solver/PreviewFeature.java b/core/src/main/java/ai/timefold/solver/core/config/solver/PreviewFeature.java index 1629f6970eb..1fa4587e711 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/solver/PreviewFeature.java +++ b/core/src/main/java/ai/timefold/solver/core/config/solver/PreviewFeature.java @@ -26,6 +26,10 @@ public enum PreviewFeature { * It is intended to simplify the creation of custom moves, eventually replacing move selectors. * The component is under development, and many key features are yet to be delivered. */ - NEIGHBORHOODS + NEIGHBORHOODS, + /** + * Not really a preview feature; allows solving with indictments enabled for benchmark purposes. + */ + SOLVE_WITH_INDICTMENTS } 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 8865d865811..e7085cbcce8 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 @@ -142,6 +142,9 @@ public Solver buildSolver(SolverConfigOverride configOverride) { } var isStepAssertOrMore = globalEnvironmentMode.isStepAssertOrMore(); var constraintMatchEnabled = solverScope.isAnyMetricConstraintMatchBased() || isStepAssertOrMore; + var previewFeaturesEnabled = solverConfig.getEnablePreviewFeatureSet(); + var indictmentsEnabled = + previewFeaturesEnabled != null && previewFeaturesEnabled.contains(PreviewFeature.SOLVE_WITH_INDICTMENTS); if (constraintMatchEnabled && !isStepAssertOrMore) { LOGGER.info( "Enabling constraint matching as required by the enabled metrics ({}). This will impact solver performance.", @@ -154,7 +157,6 @@ public Solver buildSolver(SolverConfigOverride configOverride) { solverScope.setScoreDirector(scoreDirector); solverScope.setProblemChangeDirector(new DefaultProblemChangeDirector<>(scoreDirector)); var moveThreadCount = resolveMoveThreadCount(true); - var previewFeaturesEnabled = solverConfig.getEnablePreviewFeatureSet(); var scoreDirectorFactoryConfig = solverConfig.getScoreDirectorFactoryConfig(); if (scoreDirectorFactoryConfig != null) { diff --git a/core/src/main/resources/solver.xsd b/core/src/main/resources/solver.xsd index 09b3d77f61e..ad570ee40cd 100644 --- a/core/src/main/resources/solver.xsd +++ b/core/src/main/resources/solver.xsd @@ -732,6 +732,7 @@ + diff --git a/tools/benchmark/src/main/resources/benchmark.xsd b/tools/benchmark/src/main/resources/benchmark.xsd index e2520f65b1b..545ccf52b3a 100644 --- a/tools/benchmark/src/main/resources/benchmark.xsd +++ b/tools/benchmark/src/main/resources/benchmark.xsd @@ -1614,6 +1614,8 @@ + + From ec292711d8e20bc1668cf66c4f4d786b3c013456 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Wed, 16 Sep 2026 14:46:21 -0400 Subject: [PATCH 22/30] chore: make regression tests aware of indictments --- .../stream/bavet/BavetRegressionTest.java | 42 +++++++++++++------ 1 file changed, 29 insertions(+), 13 deletions(-) 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 8ead3e00ad7..884a9dfdf09 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 @@ -240,7 +240,9 @@ private void assertIfExistsPredicateFlipWhenDeferring(boolean exists) { scoreDirector.setWorkingSolution(solution); // Both entities hold value1, so every left tuple has a match. if (exists) { - assertScore(scoreDirector, assertMatch(entityA), assertMatch(entityB)); + assertScore(scoreDirector, + assertMatch(entityA).withIndictedObjects(entityA, entityB), + assertMatch(entityB).withIndictedObjects(entityA, entityB)); } else { assertScore(scoreDirector); } @@ -250,7 +252,9 @@ private void assertIfExistsPredicateFlipWhenDeferring(boolean exists) { entityA.setValue(value2); scoreDirector.afterVariableChanged(entityA, TestdataLavishEntity.VALUE_FIELD); if (exists) { - assertScore(scoreDirector, assertMatch(entityA), assertMatch(entityB)); + assertScore(scoreDirector, + assertMatch(entityA).withIndictedObjects(entityA, entityB), + assertMatch(entityB)); } else { assertScore(scoreDirector); } @@ -263,7 +267,9 @@ private void assertIfExistsPredicateFlipWhenDeferring(boolean exists) { if (exists) { assertScore(scoreDirector); } else { - assertScore(scoreDirector, assertMatch(entityA), assertMatch(entityB)); + assertScore(scoreDirector, + assertMatch(entityA), + assertMatch(entityB)); } assertScoreMatchesFromScratch(scoreDirector, solution, constraintProvider); } @@ -2115,8 +2121,10 @@ public void filteringIfExistsNullConflictThroughMapUnassignOne() { scoreDirector.afterListVariableElementAssigned(entity, "valueList", value1); assertScore(scoreDirector, - assertMatch(value1), - assertMatch(value2)); + assertMatch(value1) + .withIndictedObjects(value1, value2), + assertMatch(value2) + .withIndictedObjects(value1, value2)); // Unassign and check result. var variableDescriptor = scoreDirector.getSolutionDescriptor() @@ -2248,8 +2256,10 @@ public void filteringIfExistsNullConflictThroughFlattenUnassignOne() { scoreDirector.afterListVariableElementAssigned(entity, "valueList", value1); assertScore(scoreDirector, - assertMatch(value1, value1, value1), - assertMatch(value2, value2, value2)); + assertMatch(value1, value1, value1) + .withIndictedObjects(value1, value2), + assertMatch(value2, value2, value2) + .withIndictedObjects(value1, value2)); // Unassign and check result. var variableDescriptor = scoreDirector.getSolutionDescriptor() @@ -2378,8 +2388,10 @@ public void filteringIfExistsNullConflictThroughGroupByUnassignOne() { scoreDirector.afterListVariableElementAssigned(entity, "valueList", value1); assertScore(scoreDirector, - assertMatch(value1), - assertMatch(value2)); + assertMatch(value1) + .withIndictedObjects(value1, value2), + assertMatch(value2) + .withIndictedObjects(value1, value2)); // Unassign and check result. var variableDescriptor = scoreDirector.getSolutionDescriptor() @@ -2528,8 +2540,10 @@ public void filteringIfExistsNullConflictThroughLargeGroupUnassignOne() { assignAllTo(scoreDirector, entity2, values2); assertScore(scoreDirector, - assertMatch(entity1), - assertMatch(entity2)); + assertMatch(entity1) + .withIndictedObjects(entity1.getValueList().toArray()), + assertMatch(entity2) + .withIndictedObjects(entity2.getValueList().toArray())); // Unassign one of entity1's five contributors; its group still has four and survives, // and entity2's group must be left entirely alone. @@ -2543,8 +2557,10 @@ public void filteringIfExistsNullConflictThroughLargeGroupUnassignOne() { scoreDirector.afterListVariableElementUnassigned(variableDescriptor, valueToUnassign); assertScore(scoreDirector, - assertMatch(entity1), - assertMatch(entity2)); + assertMatch(entity1) + .withIndictedObjects(entity1.getValueList().toArray()), + assertMatch(entity2) + .withIndictedObjects(entity2.getValueList().toArray())); } } From d5c7f93a09b86a5af8061870626b21c1dc7c33eb Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Thu, 17 Sep 2026 16:51:10 -0400 Subject: [PATCH 23/30] chore: add IndictmentAnalysis API to ScoreAnalysis --- .../core/api/score/analysis/Indictable.java | 35 +++++++++++++++++++ .../score/analysis/IndictmentAnalysis.java | 12 +++++++ .../api/score/analysis/ScoreAnalysis.java | 10 ++++++ .../api/solver/ScoreAnalysisFetchPolicy.java | 5 ++- 4 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 core/src/main/java/ai/timefold/solver/core/api/score/analysis/Indictable.java create mode 100644 core/src/main/java/ai/timefold/solver/core/api/score/analysis/IndictmentAnalysis.java diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/Indictable.java b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/Indictable.java new file mode 100644 index 00000000000..31a8850d910 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/Indictable.java @@ -0,0 +1,35 @@ +package ai.timefold.solver.core.api.score.analysis; + +import org.jspecify.annotations.NullMarked; + +/** + * An optional interface a class can implement to customize how it appears + * when indicted in a {@link ScoreAnalysis}. + *

+ * When not implemented, the class' canonical name is used for {@link IndictmentAnalysis#type()} + * and {@link IndictmentAnalysis#indictee()} is the same instance. + *

+ * The primary use case for implementing {@link Indictable} is if you have a separate + * solver and rest model, and don't want to expose your solver types in your REST API. + *

+ * Note: {@link ScoreAnalysis} in general and indictments in particular + * are exclusive to Timefold Solver Enterprise Edition. + */ +@NullMarked +public interface Indictable { + /** + * An identifier that uniquely identifies the type returned by + * {@link #getIndictedObject()}. + * @return a string that can be used to identify the class of {@link #getIndictedObject()} + */ + String getIndictedTypeId(); + + /** + * Optional method. It is used to calculate the {@link IndictmentAnalysis#indictee()} + * when this object is indicted. If not implemented, it will return this object. + * @return the object that {@link IndictmentAnalysis#indictee()} should return + */ + default Object getIndictedObject() { + return this; + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/IndictmentAnalysis.java b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/IndictmentAnalysis.java new file mode 100644 index 00000000000..7b8d6b8a08d --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/IndictmentAnalysis.java @@ -0,0 +1,12 @@ +package ai.timefold.solver.core.api.score.analysis; + +import java.util.List; + +import ai.timefold.solver.core.api.score.Score; + +public interface IndictmentAnalysis> { + String type(); + Object indictee(); + Score_ score(); + List> matches(); +} diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysis.java b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysis.java index 179b9eb885e..38fc2dc95a1 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysis.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysis.java @@ -1,6 +1,7 @@ package ai.timefold.solver.core.api.score.analysis; import java.util.Collection; +import java.util.List; import java.util.SequencedMap; import ai.timefold.solver.core.api.score.Score; @@ -154,6 +155,15 @@ public interface ScoreAnalysis> { */ Collection> constraintAnalyses(); + /** + * Returns a list of {@link IndictmentAnalysis} instances that make up this {@link ScoreAnalysis}, + * sorted by the absolute value of their total score contribution. + * + * @return a list of {@link IndictmentAnalysis} linking planning entities and problem facts to their impact + * in this {@link ScoreAnalysis} + */ + List> indictmentAnalyses(); + /** * Returns a diagnostic text that explains the solution through the {@link ConstraintAnalysis} API to identify which * constraints cause that score quality. 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 73ee7b0def5..e98bd5bca92 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 @@ -12,13 +12,15 @@ public enum ScoreAnalysisFetchPolicy { /** * {@link ScoreAnalysis} is fully initialized. - * All included {@link ConstraintAnalysis} objects include full {@link ConstraintAnalysis#matches() match analysis}. + * All included {@link ConstraintAnalysis} objects include full {@link ConstraintAnalysis#matches() match analysis}, + * and {@link ScoreAnalysis#indictmentAnalyses()} will be populated. */ FETCH_ALL, /** * {@link ConstraintAnalysis} included in {@link ScoreAnalysis} * provides neither {@link ConstraintAnalysis#matches() match analysis} * nor {@link ConstraintAnalysis#matchCount() match count}. + * Additionally, {@link ScoreAnalysis#indictmentAnalyses()} will be empty. * This is useful for performance reasons when the match analysis is not needed. */ FETCH_SHALLOW, @@ -26,6 +28,7 @@ public enum ScoreAnalysisFetchPolicy { * {@link ConstraintAnalysis} included in {@link ScoreAnalysis} * does not provide {@link ConstraintAnalysis#matches() match analysis}, * but does provide {@link ConstraintAnalysis#matchCount() match count}. + * Additionally, {@link ScoreAnalysis#indictmentAnalyses()} will be empty. * This is useful when there are too many matches to send over the wire * or meaningfully present to users. */ From 4a13b29c3c9957d56bfa2869b736e0f1bc579f0b Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Thu, 17 Sep 2026 17:24:24 -0400 Subject: [PATCH 24/30] chore: remove ConstraintMatchPolicy changes --- .../solver/core/api/score/analysis/Indictable.java | 2 ++ .../core/api/score/analysis/IndictmentAnalysis.java | 3 +++ .../solver/core/api/score/analysis/ScoreAnalysis.java | 2 +- .../impl/score/constraint/ConstraintMatchPolicy.java | 9 +++------ .../core/impl/score/director/InnerScoreDirector.java | 2 +- .../stream/bavet/bi/BavetScoringBiConstraintStream.java | 4 ++-- .../bavet/quad/BavetScoringQuadConstraintStream.java | 4 ++-- .../bavet/tri/BavetScoringTriConstraintStream.java | 4 ++-- .../bavet/uni/BavetScoringUniConstraintStream.java | 4 ++-- .../stream/common/ConstraintStreamTestExtension.java | 2 -- 10 files changed, 18 insertions(+), 18 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/Indictable.java b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/Indictable.java index 31a8850d910..e313a4761cd 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/Indictable.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/Indictable.java @@ -20,6 +20,7 @@ public interface Indictable { /** * An identifier that uniquely identifies the type returned by * {@link #getIndictedObject()}. + * * @return a string that can be used to identify the class of {@link #getIndictedObject()} */ String getIndictedTypeId(); @@ -27,6 +28,7 @@ public interface Indictable { /** * Optional method. It is used to calculate the {@link IndictmentAnalysis#indictee()} * when this object is indicted. If not implemented, it will return this object. + * * @return the object that {@link IndictmentAnalysis#indictee()} should return */ default Object getIndictedObject() { diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/IndictmentAnalysis.java b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/IndictmentAnalysis.java index 7b8d6b8a08d..3df7c2c8c2a 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/IndictmentAnalysis.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/IndictmentAnalysis.java @@ -6,7 +6,10 @@ public interface IndictmentAnalysis> { String type(); + Object indictee(); + Score_ score(); + List> matches(); } diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysis.java b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysis.java index 38fc2dc95a1..b4d27517c83 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysis.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysis.java @@ -160,7 +160,7 @@ public interface ScoreAnalysis> { * sorted by the absolute value of their total score contribution. * * @return a list of {@link IndictmentAnalysis} linking planning entities and problem facts to their impact - * in this {@link ScoreAnalysis} + * in this {@link ScoreAnalysis} */ List> indictmentAnalyses(); 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 index bd963663f6d..5a0bb256d95 100644 --- 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 @@ -12,11 +12,8 @@ */ @NullMarked public enum ConstraintMatchPolicy { - DISABLED(false, false, false), - ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS(true, false, false), - ENABLED_WITHOUT_JUSTIFICATIONS(true, false, true), - ENABLED_WITHOUT_INDICTMENTS(true, true, false), + ENABLED_WITHOUT_JUSTIFICATIONS(true, false, false), ENABLED(true, true, true); /** @@ -24,14 +21,14 @@ public enum ConstraintMatchPolicy { * 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_AND_INDICTMENTS}). + * 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_AND_INDICTMENTS; + case FETCH_MATCH_COUNT, FETCH_SHALLOW -> ENABLED_WITHOUT_JUSTIFICATIONS; case FETCH_ALL -> ENABLED; }; } 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 93465a02502..361c396e6a8 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 @@ -87,7 +87,7 @@ public interface InnerScoreDirector> /** * @return {@link ConstraintMatchPolicy#ENABLED} if {@link #getConstraintMatchTotalMap()} can be called. - * {@link ConstraintMatchPolicy#ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS} if only the former can be called. + * {@link ConstraintMatchPolicy#ENABLED_WITHOUT_JUSTIFICATIONS} if only the former can be called. * {@link ConstraintMatchPolicy#DISABLED} if neither can be called. */ ConstraintMatchPolicy getConstraintMatchPolicy(); 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 4360cfb9428..9993b8376cb 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 @@ -53,8 +53,8 @@ public > void buildNode(ConstraintNodeBuildHelper buildScoreImpacter(ConstraintMatchPolicy constraintMatchPolicy) { return switch (constraintMatchPolicy) { case DISABLED -> scoreImpact::impactNaked; - case ENABLED, ENABLED_WITHOUT_INDICTMENTS -> scoreImpact::impactFull; - case ENABLED_WITHOUT_JUSTIFICATIONS, ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS -> + case ENABLED -> scoreImpact::impactFull; + case ENABLED_WITHOUT_JUSTIFICATIONS -> scoreImpact::impactWithoutJustification; }; } 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 8f81dd5acab..bdbce97abd0 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 @@ -54,8 +54,8 @@ public > void buildNode(ConstraintNodeBuildHelper buildScoreImpacter(ConstraintMatchPolicy constraintMatchPolicy) { return switch (constraintMatchPolicy) { case DISABLED -> scoreImpact::impactNaked; - case ENABLED, ENABLED_WITHOUT_INDICTMENTS -> scoreImpact::impactFull; - case ENABLED_WITHOUT_JUSTIFICATIONS, ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS -> + case ENABLED -> scoreImpact::impactFull; + case ENABLED_WITHOUT_JUSTIFICATIONS -> scoreImpact::impactWithoutJustification; }; } 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 6412a07540f..b72f8e0c4d9 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 @@ -54,8 +54,8 @@ public > void buildNode(ConstraintNodeBuildHelper buildScoreImpacter(ConstraintMatchPolicy constraintMatchPolicy) { return switch (constraintMatchPolicy) { case DISABLED -> scoreImpact::impactNaked; - case ENABLED, ENABLED_WITHOUT_INDICTMENTS -> scoreImpact::impactFull; - case ENABLED_WITHOUT_JUSTIFICATIONS, ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS -> + case ENABLED -> scoreImpact::impactFull; + case ENABLED_WITHOUT_JUSTIFICATIONS -> scoreImpact::impactWithoutJustification; }; } 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 091dd1611a4..76316074dc7 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 @@ -53,8 +53,8 @@ public > void buildNode(ConstraintNodeBuildHelper buildScoreImpacter(ConstraintMatchPolicy constraintMatchPolicy) { return switch (constraintMatchPolicy) { case DISABLED -> scoreImpact::impactNaked; - case ENABLED, ENABLED_WITHOUT_INDICTMENTS -> scoreImpact::impactFull; - case ENABLED_WITHOUT_JUSTIFICATIONS, ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS -> + case ENABLED -> scoreImpact::impactFull; + case ENABLED_WITHOUT_JUSTIFICATIONS -> scoreImpact::impactWithoutJustification; }; } 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 a2eeaca2027..f07bcd36110 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 @@ -34,9 +34,7 @@ public boolean supportsTestTemplate(ExtensionContext context) { public Stream provideTestTemplateInvocationContexts(ExtensionContext context) { return Stream .of(ConstraintMatchPolicy.ENABLED, - ConstraintMatchPolicy.ENABLED_WITHOUT_INDICTMENTS, ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS, - ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS_AND_INDICTMENTS, ConstraintMatchPolicy.DISABLED) .map(ConstraintStreamTestExtension::invocationContext); } From 835d50d5d94ec7f07f3128b59a2e9f59e8b30721 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Thu, 17 Sep 2026 18:37:53 -0400 Subject: [PATCH 25/30] chore: update revapi --- core/src/build/revapi-differences.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/src/build/revapi-differences.json b/core/src/build/revapi-differences.json index 14e5e564871..a673695e5fe 100644 --- a/core/src/build/revapi-differences.json +++ b/core/src/build/revapi-differences.json @@ -158,6 +158,12 @@ "code": "java.method.addedToInterface", "new": "method Result_ ai.timefold.solver.core.api.solver.phase.PhaseCommandContext::executeTemporarilyAndCalculateScore(ai.timefold.solver.core.preview.api.move.Move, java.util.function.Function, java.util.function.Function)", "justification": "PhaseCommandContext is only ever supplied by the solver to PhaseCommand.changeWorkingSolution(context); it is not implemented by user code." + }, + { + "ignore": true, + "code": "java.method.addedToInterface", + "new": "method java.util.List> ai.timefold.solver.core.api.score.analysis.ScoreAnalysis>::indictmentAnalyses()", + "justification": "Add support for Indictments in ScoreAnalysis; it is not implemented by user code." } ] } From 01695d04ca863783cf019103b5d060fda2324e65 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 22 Sep 2026 14:16:26 -0400 Subject: [PATCH 26/30] chore: review comments --- .../score/analysis/IndictmentAnalysis.java | 28 ++++++ .../core/config/solver/PreviewFeature.java | 6 +- .../bavet/common/AbstractIfExistsNode.java | 5 - .../tuple/indictment/IndictmentSource.java | 94 +++++++++++++++++++ .../impl/solver/DefaultSolverFactory.java | 2 - core/src/main/resources/solver.xsd | 1 - .../src/main/resources/benchmark.xsd | 2 - 7 files changed, 123 insertions(+), 15 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/IndictmentAnalysis.java b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/IndictmentAnalysis.java index 3df7c2c8c2a..41bd3bd55c7 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/IndictmentAnalysis.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/IndictmentAnalysis.java @@ -4,12 +4,40 @@ import ai.timefold.solver.core.api.score.Score; +/** + * Note: {@link ScoreAnalysis} is exclusive to Timefold Solver Enterprise Edition. + * + * @param + * @see ScoreAnalysis Description of score analysis and the purpose of this class. + */ public interface IndictmentAnalysis> { + /** + * An identifier for the type of object indicted. + * + * @return a string that can be used to identify the type of object indicted + */ String type(); + /** + * The object that was indicted by constraints. + * + * @return the indicted object + */ Object indictee(); + /** + * The sum of score impacts of each constraint match the {@link #indictee()} + * is involved with. + * + * @return the total score impact of the indicted object + */ Score_ score(); + /** + * A list of {@link MatchAnalysis} for each constraint match the + * {@link #indictee()} is involved with, sorted by absolute score impact + * + * @return a list of matches the indictee is involved with, sorted by absolute score impact + */ List> matches(); } diff --git a/core/src/main/java/ai/timefold/solver/core/config/solver/PreviewFeature.java b/core/src/main/java/ai/timefold/solver/core/config/solver/PreviewFeature.java index 1fa4587e711..1629f6970eb 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/solver/PreviewFeature.java +++ b/core/src/main/java/ai/timefold/solver/core/config/solver/PreviewFeature.java @@ -26,10 +26,6 @@ public enum PreviewFeature { * It is intended to simplify the creation of custom moves, eventually replacing move selectors. * The component is under development, and many key features are yet to be delivered. */ - NEIGHBORHOODS, - /** - * Not really a preview feature; allows solving with indictments enabled for benchmark purposes. - */ - SOLVE_WITH_INDICTMENTS + NEIGHBORHOODS } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java index c6c6fc50b1f..10cdac61522 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java @@ -158,11 +158,6 @@ protected void decrementCounterRight(ExistsCounter counter) { } else { doInsertCounter(counter); } - } else { - // count != 0, so only propagate if we are in an `ifExists` - if (shouldExist) { - doUpdateCounter(counter); - } } // Else do not even propagate an update } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java index 50d92d10c64..6131d3a83dc 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java @@ -15,22 +15,64 @@ import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; +/** + * Used to track the original indicted object as tuples pass through + * the various constraint streams. Form a tree, based on the number of + * "inputs" a node has. For instance, `join` nodes have two inputs, since + * two tuples affect the out tuple the `join` node produces. This tree can + * then be navigated to find the indicted objects for each constraint match. + */ @NullMarked public sealed interface IndictmentSource { + /** + * The indictment source to be used when indictments are disabled. + * Can be used to check if indictments are enabled to influence node behavior. + */ IndictmentSource DISABLED = new DisabledIndictmentSource(); + /** + * Does a tree traversal of the indicted objects referenced by this source. + * + * @param visited The sources already visited + * @param involvedNodeIds The nodes involved in the triggered constraint + * @param sourceConsumer The indicted object consumer, may receive the same indicted object multiple times + */ void visitSources(Set visited, long @Nullable [] involvedNodeIds, Consumer sourceConsumer); + /** + * Get the support of an indictment source. The support is a map from + * node id to supporting indictment sources. This is used when a node reuses + * tuples and adds additional indicted objects, such as `ifExists` nodes. + * + * @return a map from node id to additional indictment sources + */ Map> support(); + /** + * Do a tree transversal visiting all supporting nodes with an empty visited set + * + * @see #visitSources(Set, long[], Consumer) + */ default void visitAllSources(Consumer sourceConsumer) { visitSources(new HashSet<>(), null, sourceConsumer); } + /** + * Do a tree transversal visiting only the supporting nodes matching involvedNodeIds with an empty visited set + * + * @see #visitSources(Set, long[], Consumer) + */ default void visitSources(long[] involvedNodeIds, Consumer sourceConsumer) { visitSources(new HashSet<>(), involvedNodeIds, sourceConsumer); } + /** + * Get the support set for a given node id. + * + * @param nodeId The node id to get the support set of + * @return A set to be used as the support of a given node id + * @see #support() + */ default Set getSupportForNodeId(long nodeId) { return support().computeIfAbsent(nodeId, ignored -> new LinkedHashSet<>()); } @@ -57,10 +99,21 @@ static boolean checkIfAlreadyVisitedAndVisitSupport(IndictmentSource self, Set()); } + /** + * Create an indictment source from the sources of the two tuples. + * + * @apiNote will return {@link #DISABLED} if indictments are disabled + */ static IndictmentSource joining(Tuple left, Tuple right) { if (left.getIndictmentSource() == DISABLED) { return DISABLED; @@ -68,6 +121,9 @@ static IndictmentSource joining(Tuple left, Tuple right) { return new JoinedIndictmentSource(left.getIndictmentSource(), right.getIndictmentSource(), new LinkedHashMap<>()); } + /** + * Create an (or return the existing) aggregate indictment source and attach it to the given tuple + */ static AggregateIndictmentSource getPrecomputeAggregation(Tuple outTuple) { if (outTuple.getIndictmentSource() != DISABLED) { return (AggregateIndictmentSource) outTuple.getIndictmentSource(); @@ -77,6 +133,15 @@ static AggregateIndictmentSource getPrecomputeAggregation(Tuple outTuple) { return out; } + /** + * Adds the given element indictment source to the group indictment sources, and return + * the group's aggregate indictment source (creating it if does not exist yet). + * + * @param elementTuple the tuple being aggregated + * @param groupTuple the aggregation tuple + * @return the indictment source of the aggregation + * @apiNote will return {@link #DISABLED} if indictments are disabled + */ static IndictmentSource aggregating(Tuple elementTuple, Tuple groupTuple) { if (elementTuple.getIndictmentSource() == DISABLED) { return DISABLED; @@ -91,6 +156,15 @@ static IndictmentSource aggregating(Tuple elementTuple, Tuple groupTuple) { } } + /** + * Removes the given element indictment source from the group indictment sources, and return + * the group's aggregate indictment source (creating it if does not exist yet). + * + * @param elementTuple the tuple being aggregated + * @param groupTuple the aggregation tuple + * @return the indictment source of the aggregation + * @apiNote will return {@link #DISABLED} if indictments are disabled + */ static IndictmentSource removeFromAggregate(Tuple elementTuple, Tuple groupTuple) { if (elementTuple.getIndictmentSource() == DISABLED) { return DISABLED; @@ -105,6 +179,13 @@ static IndictmentSource removeFromAggregate(Tuple elementTuple, Tuple groupTuple } } + /** + * Adds the indictment source of the supporting tuple to the carrying tuple's support + * + * @param nodeId The node id that created this support + * @param carry the tuple that is propagated + * @param support the tuple that contributed to the carry being propagated but is not propagated itself + */ static void addSupport(long nodeId, Tuple carry, Tuple support) { if (carry.getIndictmentSource() == DISABLED) { return; @@ -112,6 +193,12 @@ static void addSupport(long nodeId, Tuple carry, Tuple support) { carry.getIndictmentSource().getSupportForNodeId(nodeId).add(support.getIndictmentSource()); } + /** + * Clears the carrying tuple's support + * + * @param nodeId The node id that created this support + * @param carry the tuple that is propagated + */ static void clearSupport(long nodeId, Tuple carry) { if (carry.getIndictmentSource() == DISABLED) { return; @@ -119,6 +206,13 @@ static void clearSupport(long nodeId, Tuple carry) { carry.getIndictmentSource().getSupportForNodeId(nodeId).clear(); } + /** + * Remove the indictment source of the supporting tuple from the carrying tuple's support + * + * @param nodeId The node id that created this support + * @param carry the tuple that is propagated + * @param support the tuple that used to contribute to the carry being propagated but is not propagated itself + */ static void removeSupport(long nodeId, Tuple carry, Tuple support) { if (carry.getIndictmentSource() == DISABLED) { return; 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 e7085cbcce8..1258b58598d 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 @@ -143,8 +143,6 @@ public Solver buildSolver(SolverConfigOverride configOverride) { var isStepAssertOrMore = globalEnvironmentMode.isStepAssertOrMore(); var constraintMatchEnabled = solverScope.isAnyMetricConstraintMatchBased() || isStepAssertOrMore; var previewFeaturesEnabled = solverConfig.getEnablePreviewFeatureSet(); - var indictmentsEnabled = - previewFeaturesEnabled != null && previewFeaturesEnabled.contains(PreviewFeature.SOLVE_WITH_INDICTMENTS); if (constraintMatchEnabled && !isStepAssertOrMore) { LOGGER.info( "Enabling constraint matching as required by the enabled metrics ({}). This will impact solver performance.", diff --git a/core/src/main/resources/solver.xsd b/core/src/main/resources/solver.xsd index ad570ee40cd..09b3d77f61e 100644 --- a/core/src/main/resources/solver.xsd +++ b/core/src/main/resources/solver.xsd @@ -732,7 +732,6 @@ - diff --git a/tools/benchmark/src/main/resources/benchmark.xsd b/tools/benchmark/src/main/resources/benchmark.xsd index 545ccf52b3a..e2520f65b1b 100644 --- a/tools/benchmark/src/main/resources/benchmark.xsd +++ b/tools/benchmark/src/main/resources/benchmark.xsd @@ -1614,8 +1614,6 @@ - - From 69ed896532a077ecfa874e65720947d4eebef59c Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 22 Sep 2026 21:02:25 -0400 Subject: [PATCH 27/30] chore: move to new API --- core/src/build/revapi-differences.json | 2 +- .../core/api/score/analysis/Indictable.java | 2 +- .../api/score/analysis/IndictableTypeRef.java | 19 +++++++++++++++++++ .../score/analysis/IndictmentAnalysis.java | 7 ------- .../api/score/analysis/ScoreAnalysis.java | 12 +++++++----- .../api/solver/ScoreAnalysisFetchPolicy.java | 6 +++--- 6 files changed, 31 insertions(+), 17 deletions(-) create mode 100644 core/src/main/java/ai/timefold/solver/core/api/score/analysis/IndictableTypeRef.java diff --git a/core/src/build/revapi-differences.json b/core/src/build/revapi-differences.json index a673695e5fe..d73aa69fada 100644 --- a/core/src/build/revapi-differences.json +++ b/core/src/build/revapi-differences.json @@ -162,7 +162,7 @@ { "ignore": true, "code": "java.method.addedToInterface", - "new": "method java.util.List> ai.timefold.solver.core.api.score.analysis.ScoreAnalysis>::indictmentAnalyses()", + "new": "method java.util.SequencedMap>> ai.timefold.solver.core.api.score.analysis.ScoreAnalysis>::indictmentMap()", "justification": "Add support for Indictments in ScoreAnalysis; it is not implemented by user code." } ] diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/Indictable.java b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/Indictable.java index e313a4761cd..90ae6eff216 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/Indictable.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/Indictable.java @@ -23,7 +23,7 @@ public interface Indictable { * * @return a string that can be used to identify the class of {@link #getIndictedObject()} */ - String getIndictedTypeId(); + IndictableTypeRef getIndictableTypeRef(); /** * Optional method. It is used to calculate the {@link IndictmentAnalysis#indictee()} diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/IndictableTypeRef.java b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/IndictableTypeRef.java new file mode 100644 index 00000000000..3802c657f9b --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/IndictableTypeRef.java @@ -0,0 +1,19 @@ +package ai.timefold.solver.core.api.score.analysis; + +/** + * Represents a unique identifier of an {@link Indictable} class. + *

+ * If you need an instance created, use {@link IndictableTypeRef#of(Class)} or {@link IndictableTypeRef#of(String)} + * and not the record's constructors. + * + * @param id The indictable class id. It must be unique per class. + */ +public record IndictableTypeRef(String id) { + public static IndictableTypeRef of(String id) { + return new IndictableTypeRef(id); + } + + public static IndictableTypeRef of(Class clazz) { + return new IndictableTypeRef(clazz.getCanonicalName()); + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/IndictmentAnalysis.java b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/IndictmentAnalysis.java index 41bd3bd55c7..ee47fc02abd 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/IndictmentAnalysis.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/IndictmentAnalysis.java @@ -11,13 +11,6 @@ * @see ScoreAnalysis Description of score analysis and the purpose of this class. */ public interface IndictmentAnalysis> { - /** - * An identifier for the type of object indicted. - * - * @return a string that can be used to identify the type of object indicted - */ - String type(); - /** * The object that was indicted by constraints. * diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysis.java b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysis.java index b4d27517c83..4a1cae33dcc 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysis.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysis.java @@ -156,13 +156,15 @@ public interface ScoreAnalysis> { Collection> constraintAnalyses(); /** - * Returns a list of {@link IndictmentAnalysis} instances that make up this {@link ScoreAnalysis}, - * sorted by the absolute value of their total score contribution. + * Returns a map from indicted classes to a list of {@link IndictmentAnalysis} that contributed to at least one constraint + * match in the {@link ScoreAnalysis}. By default, the key is the canonical name of the class, but can be controlled + * by implementing {@link Indictable}. The lists are sorted by the absolute value of their total score contribution, in + * descending order. * - * @return a list of {@link IndictmentAnalysis} linking planning entities and problem facts to their impact - * in this {@link ScoreAnalysis} + * @return a map from indicted class to a list of {@link IndictmentAnalysis} linking planning entities and problem facts + * to their impact in this {@link ScoreAnalysis} */ - List> indictmentAnalyses(); + SequencedMap>> indictmentMap(); /** * Returns a diagnostic text that explains the solution through the {@link ConstraintAnalysis} API to identify which 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 e98bd5bca92..948988909db 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 @@ -13,14 +13,14 @@ public enum ScoreAnalysisFetchPolicy { /** * {@link ScoreAnalysis} is fully initialized. * All included {@link ConstraintAnalysis} objects include full {@link ConstraintAnalysis#matches() match analysis}, - * and {@link ScoreAnalysis#indictmentAnalyses()} will be populated. + * and {@link ScoreAnalysis#indictmentMap()} will be populated. */ FETCH_ALL, /** * {@link ConstraintAnalysis} included in {@link ScoreAnalysis} * provides neither {@link ConstraintAnalysis#matches() match analysis} * nor {@link ConstraintAnalysis#matchCount() match count}. - * Additionally, {@link ScoreAnalysis#indictmentAnalyses()} will be empty. + * Additionally, {@link ScoreAnalysis#indictmentMap()} will be empty. * This is useful for performance reasons when the match analysis is not needed. */ FETCH_SHALLOW, @@ -28,7 +28,7 @@ public enum ScoreAnalysisFetchPolicy { * {@link ConstraintAnalysis} included in {@link ScoreAnalysis} * does not provide {@link ConstraintAnalysis#matches() match analysis}, * but does provide {@link ConstraintAnalysis#matchCount() match count}. - * Additionally, {@link ScoreAnalysis#indictmentAnalyses()} will be empty. + * Additionally, {@link ScoreAnalysis#indictmentMap()} will be empty. * This is useful when there are too many matches to send over the wire * or meaningfully present to users. */ From 03ca38ff26ff8d469b6d8a8e13690610751e40d7 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Wed, 23 Sep 2026 18:48:24 -0400 Subject: [PATCH 28/30] chore: review comments --- core/src/build/revapi-differences.json | 2 +- .../core/api/score/analysis/Indictable.java | 20 ++++++- .../api/score/analysis/IndictableTypeRef.java | 19 ------ .../score/analysis/IndictmentAnalysis.java | 1 + .../api/score/analysis/ScoreAnalysis.java | 2 +- .../api/solver/ScoreAnalysisFetchPolicy.java | 9 ++- .../core/api/solver/SolutionManager.java | 10 ++-- .../bavet/common/AbstractIfExistsNode.java | 12 ++-- .../common/AbstractIndexedIfExistsNode.java | 14 ++--- .../tuple/indictment/IndictmentSource.java | 58 +++++++++---------- .../constraint/ConstraintMatchPolicy.java | 2 + .../bi/BavetScoringBiConstraintStream.java | 2 +- .../BavetScoringQuadConstraintStream.java | 2 +- .../tri/BavetScoringTriConstraintStream.java | 2 +- .../uni/BavetScoringUniConstraintStream.java | 2 +- .../impl/solver/DefaultSolverFactory.java | 2 +- .../common/ConstraintStreamTestExtension.java | 1 + 17 files changed, 84 insertions(+), 76 deletions(-) delete mode 100644 core/src/main/java/ai/timefold/solver/core/api/score/analysis/IndictableTypeRef.java diff --git a/core/src/build/revapi-differences.json b/core/src/build/revapi-differences.json index d73aa69fada..e0005b4cfdb 100644 --- a/core/src/build/revapi-differences.json +++ b/core/src/build/revapi-differences.json @@ -162,7 +162,7 @@ { "ignore": true, "code": "java.method.addedToInterface", - "new": "method java.util.SequencedMap>> ai.timefold.solver.core.api.score.analysis.ScoreAnalysis>::indictmentMap()", + "new": "method java.util.SequencedMap>> ai.timefold.solver.core.api.score.analysis.ScoreAnalysis>::indictmentMap()", "justification": "Add support for Indictments in ScoreAnalysis; it is not implemented by user code." } ] diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/Indictable.java b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/Indictable.java index 90ae6eff216..ef61e8dfc84 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/Indictable.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/Indictable.java @@ -17,13 +17,31 @@ */ @NullMarked public interface Indictable { + + /** + * Returns the indictment class id used by {@link ScoreAnalysis#indictmentMap()} + * when a class does not implement {@link Indictable}. + * + * @param clazz The type of class + * @return the key into {@link ScoreAnalysis#indictmentMap()} to get indicted objects of that type + */ + static String getIndictmentClassId(Class clazz) { + if (Indictable.class.isAssignableFrom(clazz)) { + throw new IllegalArgumentException(""" + The class (%s) implements %s so its indictment map key cannot be determined statically. + Maybe check (%s) implementation of getIndictmentClassId()?""".formatted(clazz.getCanonicalName(), + Indictable.class.getSimpleName(), clazz.getCanonicalName())); + } + return clazz.getCanonicalName(); + } + /** * An identifier that uniquely identifies the type returned by * {@link #getIndictedObject()}. * * @return a string that can be used to identify the class of {@link #getIndictedObject()} */ - IndictableTypeRef getIndictableTypeRef(); + String getIndictmentClassId(); /** * Optional method. It is used to calculate the {@link IndictmentAnalysis#indictee()} diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/IndictableTypeRef.java b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/IndictableTypeRef.java deleted file mode 100644 index 3802c657f9b..00000000000 --- a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/IndictableTypeRef.java +++ /dev/null @@ -1,19 +0,0 @@ -package ai.timefold.solver.core.api.score.analysis; - -/** - * Represents a unique identifier of an {@link Indictable} class. - *

- * If you need an instance created, use {@link IndictableTypeRef#of(Class)} or {@link IndictableTypeRef#of(String)} - * and not the record's constructors. - * - * @param id The indictable class id. It must be unique per class. - */ -public record IndictableTypeRef(String id) { - public static IndictableTypeRef of(String id) { - return new IndictableTypeRef(id); - } - - public static IndictableTypeRef of(Class clazz) { - return new IndictableTypeRef(clazz.getCanonicalName()); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/IndictmentAnalysis.java b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/IndictmentAnalysis.java index ee47fc02abd..b02b17b66a8 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/IndictmentAnalysis.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/IndictmentAnalysis.java @@ -13,6 +13,7 @@ public interface IndictmentAnalysis> { /** * The object that was indicted by constraints. + * Its type corresponds to the containing entry's key in {@link ScoreAnalysis#indictmentMap()}. * * @return the indicted object */ diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysis.java b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysis.java index 4a1cae33dcc..cf052f24afb 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysis.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/ScoreAnalysis.java @@ -164,7 +164,7 @@ public interface ScoreAnalysis> { * @return a map from indicted class to a list of {@link IndictmentAnalysis} linking planning entities and problem facts * to their impact in this {@link ScoreAnalysis} */ - SequencedMap>> indictmentMap(); + SequencedMap>> indictmentMap(); /** * Returns a diagnostic text that explains the solution through the {@link ConstraintAnalysis} API to identify which 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 948988909db..f1d9dd3af88 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 @@ -32,6 +32,11 @@ public enum ScoreAnalysisFetchPolicy { * This is useful when there are too many matches to send over the wire * or meaningfully present to users. */ - FETCH_MATCH_COUNT - + FETCH_MATCH_COUNT, + /** + * {@link ScoreAnalysis} is initialized except for indictments. + * All included {@link ConstraintAnalysis} objects include full {@link ConstraintAnalysis#matches() match analysis}, + * {@link ScoreAnalysis#indictmentMap()} will be empty. + */ + FETCH_JUSTIFICATIONS } 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 0c4fc2b71fc..0bc6af09741 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 @@ -1,6 +1,6 @@ package ai.timefold.solver.core.api.solver; -import static ai.timefold.solver.core.api.solver.ScoreAnalysisFetchPolicy.FETCH_ALL; +import static ai.timefold.solver.core.api.solver.ScoreAnalysisFetchPolicy.FETCH_JUSTIFICATIONS; import static ai.timefold.solver.core.api.solver.SolutionUpdatePolicy.UPDATE_ALL; import java.util.List; @@ -118,10 +118,10 @@ static void updateShadowVariables(Solution_ solution) { /** * As defined by {@link #analyze(Object, ScoreAnalysisFetchPolicy, SolutionUpdatePolicy)}, - * using {@link SolutionUpdatePolicy#UPDATE_ALL} and {@link ScoreAnalysisFetchPolicy#FETCH_ALL}. + * using {@link SolutionUpdatePolicy#UPDATE_ALL} and {@link ScoreAnalysisFetchPolicy#FETCH_JUSTIFICATIONS}. */ default ScoreAnalysis analyze(Solution_ solution) { - return analyze(solution, FETCH_ALL, UPDATE_ALL); + return analyze(solution, FETCH_JUSTIFICATIONS, UPDATE_ALL); } /** @@ -179,12 +179,12 @@ ScoreAnalysis analyze(Solution_ solution, ScoreAnalysisFetchPolicy fetch /** * As defined by {@link #recommendAssignment(Object, Object, Function, ScoreAnalysisFetchPolicy)}, - * with {@link ScoreAnalysisFetchPolicy#FETCH_ALL}. + * with {@link ScoreAnalysisFetchPolicy#FETCH_JUSTIFICATIONS}. */ default List> recommendAssignment( Solution_ solution, EntityOrElement_ evaluatedEntityOrElement, Function propositionFunction) { - return recommendAssignment(solution, evaluatedEntityOrElement, propositionFunction, FETCH_ALL); + return recommendAssignment(solution, evaluatedEntityOrElement, propositionFunction, FETCH_JUSTIFICATIONS); } /** diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java index 10cdac61522..592ada3787e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIfExistsNode.java @@ -137,7 +137,7 @@ protected void incrementCounterRight(ExistsCounter counter) { } protected void incrementCounterRightUpdatingIndictment(ExistsCounter counter, UniTuple rightTuple) { - IndictmentSource.addSupport(getId(), counter.getTuple(), rightTuple); + IndictmentSource.addCorroborator(getId(), counter.getTuple(), rightTuple); if (counter.countRight == 0) { if (shouldExist) { doInsertCounter(counter); @@ -162,7 +162,7 @@ protected void decrementCounterRight(ExistsCounter counter) { } protected void decrementCounterRightUpdatingIndictment(ExistsCounter counter, UniTuple rightTuple) { - IndictmentSource.removeSupport(getId(), counter.getTuple(), rightTuple); + IndictmentSource.removeCorroborator(getId(), counter.getTuple(), rightTuple); counter.countRight--; if (counter.countRight == 0) { if (shouldExist) { @@ -271,12 +271,12 @@ protected void updateCounterLeft(ExistsCounter counter, UniTuple(counter, rightTuple); linkLeft(tracker); linkRight(tracker); } else { - IndictmentSource.removeSupport(getId(), counter.getTuple(), rightTuple); + IndictmentSource.removeCorroborator(getId(), counter.getTuple(), rightTuple); } } @@ -339,12 +339,12 @@ protected void updateCounterRight(ExistsCounter counter, UniTuple(counter, rightTuple); linkLeft(tracker); linkRight(tracker); } else { - IndictmentSource.removeSupport(getId(), counter.getTuple(), rightTuple); + IndictmentSource.removeCorroborator(getId(), counter.getTuple(), rightTuple); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java index 5e40cdfc9bb..8124c5d5706 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java @@ -100,7 +100,7 @@ public final void insertLeft(LeftTuple_ leftTuple) { initCounterLeft(counter); if (leftTuple.getIndictmentSource() != IndictmentSource.DISABLED) { forEachRightFromLeft(leftTuple, compositeKey, rightTuple -> { - IndictmentSource.addSupport(getId(), leftTuple, rightTuple); + IndictmentSource.addCorroborator(getId(), leftTuple, rightTuple); }); } } else { @@ -138,9 +138,9 @@ public final void updateLeft(LeftTuple_ leftTuple) { if (!isFiltering) { updateUnchangedCounterLeft(counter); if (leftTuple.getIndictmentSource() != IndictmentSource.DISABLED) { - IndictmentSource.clearSupport(getId(), leftTuple); + IndictmentSource.clearCorroborators(getId(), leftTuple); forEachRightFromLeft(leftTuple, newCompositeKey, rightTuple -> { - IndictmentSource.addSupport(getId(), leftTuple, rightTuple); + IndictmentSource.addCorroborator(getId(), leftTuple, rightTuple); }); } } else { @@ -149,7 +149,7 @@ public final void updateLeft(LeftTuple_ leftTuple) { // See AbstractCrossMatchNode's pendingLeft/pendingRight javadoc. clearLeftTrackerList(leftTuple); counter.countRight = 0; - IndictmentSource.clearSupport(getId(), leftTuple); + IndictmentSource.clearCorroborators(getId(), leftTuple); crossMatchLeft(leftTuple); } } else { @@ -157,7 +157,7 @@ public final void updateLeft(LeftTuple_ leftTuple) { var sameBucket = reuseBucketEligible && fusedEqualIndex.isSameBucket(oldCompositeKey, newCompositeKey); updateIndexerLeft(oldCompositeKey, counterEntry, leftTuple, sameBucket); // Also clears the tracker list. counter.countRight = 0; - IndictmentSource.clearSupport(getId(), leftTuple); + IndictmentSource.clearCorroborators(getId(), leftTuple); leftTuple.setStore(inputStoreIndexLefCompositeKey, newCompositeKey); leftTuple.setStore(inputStoreIndexLeftCounterEntry, putLeftCounter(leftTuple, newCompositeKey, counter, sameBucket)); @@ -168,7 +168,7 @@ public final void updateLeft(LeftTuple_ leftTuple) { updateCounterLeft(counter); if (leftTuple.getIndictmentSource() != IndictmentSource.DISABLED) { forEachRightFromLeft(leftTuple, newCompositeKey, rightTuple -> { - IndictmentSource.addSupport(getId(), leftTuple, rightTuple); + IndictmentSource.addCorroborator(getId(), leftTuple, rightTuple); }); } } @@ -399,7 +399,7 @@ protected void reconcilePendingLeft(LeftTuple_ leftTuple) { ListEntry> counterEntry = leftTuple.getStore(inputStoreIndexLeftCounterEntry); var counter = counterEntry.element(); clearLeftTrackerList(leftTuple); - IndictmentSource.clearSupport(getId(), leftTuple); + IndictmentSource.clearCorroborators(getId(), leftTuple); counter.countRight = 0; forEachRightFromLeft(leftTuple, compositeKey, rightTuple -> updateCounterLeft(counter, rightTuple)); updateCounterLeft(counter); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java index 6131d3a83dc..61970486ddf 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java @@ -40,13 +40,13 @@ public sealed interface IndictmentSource { void visitSources(Set visited, long @Nullable [] involvedNodeIds, Consumer sourceConsumer); /** - * Get the support of an indictment source. The support is a map from - * node id to supporting indictment sources. This is used when a node reuses + * Get the corroborators of an indictment source. The corroborators is a map from + * node id to corroborating indictment sources. This is used when a node reuses * tuples and adds additional indicted objects, such as `ifExists` nodes. * * @return a map from node id to additional indictment sources */ - Map> support(); + Map> corroborators(); /** * Do a tree transversal visiting all supporting nodes with an empty visited set @@ -67,14 +67,14 @@ default void visitSources(long[] involvedNodeIds, Consumer sourceConsume } /** - * Get the support set for a given node id. + * Get the corroborators set for a given node id. * * @param nodeId The node id to get the support set of * @return A set to be used as the support of a given node id - * @see #support() + * @see #corroborators() */ - default Set getSupportForNodeId(long nodeId) { - return support().computeIfAbsent(nodeId, ignored -> new LinkedHashSet<>()); + default Set getCorroboratorsForNodeId(long nodeId) { + return corroborators().computeIfAbsent(nodeId, ignored -> new LinkedHashSet<>()); } static boolean checkIfAlreadyVisitedAndVisitSupport(IndictmentSource self, Set visited, @@ -84,14 +84,14 @@ static boolean checkIfAlreadyVisitedAndVisitSupport(IndictmentSource self, Set visited, long @Nullable [] involv } @Override - public Map> support() { + public Map> corroborators() { throw new UnsupportedOperationException("Impossible state: indictments are disabled."); } } - record RootIndictmentSource(Object source, Map> support) implements IndictmentSource { + record RootIndictmentSource(Object source, Map> corroborators) implements IndictmentSource { @Override public void visitSources(Set visited, long @Nullable [] involvedNodeIds, Consumer sourceConsumer) { @@ -255,7 +255,7 @@ public int hashCode() { } record JoinedIndictmentSource(IndictmentSource left, IndictmentSource right, - Map> support) implements IndictmentSource { + Map> corroborators) implements IndictmentSource { @Override public void visitSources(Set visited, long @Nullable [] involvedNodeIds, Consumer sourceConsumer) { @@ -278,7 +278,7 @@ public int hashCode() { } record AggregateIndictmentSource(List sourceList, - Map> support) implements IndictmentSource { + Map> corroborators) implements IndictmentSource { @Override public void visitSources(Set visited, long @Nullable [] involvedNodeIds, Consumer sourceConsumer) { @@ -291,8 +291,8 @@ public void visitSources(Set visited, long @Nullable [] involv } @Override - public Set getSupportForNodeId(long nodeId) { - return support.computeIfAbsent(nodeId, ignored -> new LinkedHashSet<>()); + public Set getCorroboratorsForNodeId(long nodeId) { + return corroborators.computeIfAbsent(nodeId, ignored -> new LinkedHashSet<>()); } @Override 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 index 5a0bb256d95..0752ebea111 100644 --- 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 @@ -14,6 +14,7 @@ public enum ConstraintMatchPolicy { DISABLED(false, false, false), ENABLED_WITHOUT_JUSTIFICATIONS(true, false, false), + ENABLED_WITHOUT_INDICTMENTS(true, true, false), ENABLED(true, true, true); /** @@ -29,6 +30,7 @@ public enum ConstraintMatchPolicy { public static ConstraintMatchPolicy match(ScoreAnalysisFetchPolicy scoreAnalysisFetchPolicy) { return switch (scoreAnalysisFetchPolicy) { case FETCH_MATCH_COUNT, FETCH_SHALLOW -> ENABLED_WITHOUT_JUSTIFICATIONS; + case FETCH_JUSTIFICATIONS -> ENABLED_WITHOUT_INDICTMENTS; case FETCH_ALL -> ENABLED; }; } 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 9993b8376cb..159313e213a 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 @@ -53,7 +53,7 @@ public > void buildNode(ConstraintNodeBuildHelper buildScoreImpacter(ConstraintMatchPolicy constraintMatchPolicy) { return switch (constraintMatchPolicy) { case DISABLED -> scoreImpact::impactNaked; - case ENABLED -> scoreImpact::impactFull; + case ENABLED, ENABLED_WITHOUT_INDICTMENTS -> scoreImpact::impactFull; case ENABLED_WITHOUT_JUSTIFICATIONS -> scoreImpact::impactWithoutJustification; }; 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 bdbce97abd0..074969e9654 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 @@ -54,7 +54,7 @@ public > void buildNode(ConstraintNodeBuildHelper buildScoreImpacter(ConstraintMatchPolicy constraintMatchPolicy) { return switch (constraintMatchPolicy) { case DISABLED -> scoreImpact::impactNaked; - case ENABLED -> scoreImpact::impactFull; + case ENABLED, ENABLED_WITHOUT_INDICTMENTS -> scoreImpact::impactFull; case ENABLED_WITHOUT_JUSTIFICATIONS -> scoreImpact::impactWithoutJustification; }; 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 b72f8e0c4d9..af767b207d6 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 @@ -54,7 +54,7 @@ public > void buildNode(ConstraintNodeBuildHelper buildScoreImpacter(ConstraintMatchPolicy constraintMatchPolicy) { return switch (constraintMatchPolicy) { case DISABLED -> scoreImpact::impactNaked; - case ENABLED -> scoreImpact::impactFull; + case ENABLED, ENABLED_WITHOUT_INDICTMENTS -> scoreImpact::impactFull; case ENABLED_WITHOUT_JUSTIFICATIONS -> scoreImpact::impactWithoutJustification; }; 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 76316074dc7..d19b9e9f3db 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 @@ -53,7 +53,7 @@ public > void buildNode(ConstraintNodeBuildHelper buildScoreImpacter(ConstraintMatchPolicy constraintMatchPolicy) { return switch (constraintMatchPolicy) { case DISABLED -> scoreImpact::impactNaked; - case ENABLED -> scoreImpact::impactFull; + case ENABLED, ENABLED_WITHOUT_INDICTMENTS -> scoreImpact::impactFull; case ENABLED_WITHOUT_JUSTIFICATIONS -> scoreImpact::impactWithoutJustification; }; 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 1258b58598d..8865d865811 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 @@ -142,7 +142,6 @@ public Solver buildSolver(SolverConfigOverride configOverride) { } var isStepAssertOrMore = globalEnvironmentMode.isStepAssertOrMore(); var constraintMatchEnabled = solverScope.isAnyMetricConstraintMatchBased() || isStepAssertOrMore; - var previewFeaturesEnabled = solverConfig.getEnablePreviewFeatureSet(); if (constraintMatchEnabled && !isStepAssertOrMore) { LOGGER.info( "Enabling constraint matching as required by the enabled metrics ({}). This will impact solver performance.", @@ -155,6 +154,7 @@ public Solver buildSolver(SolverConfigOverride configOverride) { solverScope.setScoreDirector(scoreDirector); solverScope.setProblemChangeDirector(new DefaultProblemChangeDirector<>(scoreDirector)); var moveThreadCount = resolveMoveThreadCount(true); + var previewFeaturesEnabled = solverConfig.getEnablePreviewFeatureSet(); var scoreDirectorFactoryConfig = solverConfig.getScoreDirectorFactoryConfig(); if (scoreDirectorFactoryConfig != null) { 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 f07bcd36110..d898a4a26c8 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 @@ -35,6 +35,7 @@ public Stream provideTestTemplateInvocationContex return Stream .of(ConstraintMatchPolicy.ENABLED, ConstraintMatchPolicy.ENABLED_WITHOUT_JUSTIFICATIONS, + ConstraintMatchPolicy.ENABLED_WITHOUT_INDICTMENTS, ConstraintMatchPolicy.DISABLED) .map(ConstraintStreamTestExtension::invocationContext); } From c542057db7c2d0c2635d60353509a86149d0ba5b Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Thu, 24 Sep 2026 10:32:15 -0400 Subject: [PATCH 29/30] docs: fix Javadocs --- .../ai/timefold/solver/core/api/score/analysis/Indictable.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/Indictable.java b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/Indictable.java index ef61e8dfc84..8e798a97628 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/score/analysis/Indictable.java +++ b/core/src/main/java/ai/timefold/solver/core/api/score/analysis/Indictable.java @@ -6,7 +6,7 @@ * An optional interface a class can implement to customize how it appears * when indicted in a {@link ScoreAnalysis}. *

- * When not implemented, the class' canonical name is used for {@link IndictmentAnalysis#type()} + * When not implemented, the class' canonical name is used as the key in {@link ScoreAnalysis#indictmentMap()}} * and {@link IndictmentAnalysis#indictee()} is the same instance. *

* The primary use case for implementing {@link Indictable} is if you have a separate From 5263dcb896369194326a03a0a9f101f3bda38395 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Thu, 24 Sep 2026 12:02:43 -0400 Subject: [PATCH 30/30] docs: add indictments to docs --- .../understanding-the-score.adoc | 97 ++++++++++++++++++- 1 file changed, 95 insertions(+), 2 deletions(-) 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 8dac6b805ad..8ddd2364960 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 @@ -88,7 +88,7 @@ Using score analysis, you can find out: [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(...)`. +In that case, use `ScoreAnalysisFetchPolicy.FETCH_MATCH_COUNT` instead of the default `ScoreAnalysisFetchPolicy.FETCH_JUSTIFICATIONS` when calling `SolutionManager.analyze(...)`. ==== NOTE: The score analysis of structurally flawed solutions has a structural flaw analysis, @@ -165,6 +165,98 @@ constraintAnalysis.matches().forEach(matchAnalysis -> { Each match is accompanied by the score difference it caused, and a justification object (see above). Typically, the scoring engine creates justification objects automatically by using the results of xref:constraints-and-score/score-calculation.adoc#constraintStreamsCustomizingJustifications[Constraint Streams' `justifyWith(...)` call]. +[#scoreAnalysisIndictments] +=== Finding how entities and problem facts contributed to the score + +When you fetch a `ScoreAnalysis` instance with `ScoreAnalysisFetchPolicy.FETCH_ALL`, you can find out how each problem fact and entity contributed to the score: + +[tabs] +==== +Java:: ++ +[source,java,options="nowrap"] +---- +ScoreAnalysis scoreAnalysis = solutionManager.analyze(solution, ScoreAnalysisFetchPolicy.FETCH_ALL); +scoreAnalysis.indictmentMap().forEach((classId, indictments) -> { + for (IndictmentAnalysis analysis : indictments) { + Object indictedObject = analysis.indictee(); + HardSoftScore constraintMatchScoreSum = analysis.score(); + for (MatchAnalysis match : analysis.matches()) { + ... + } + } +}); +---- +==== + +IMPORTANT: When a `ScoreAnalysisFetchPolicy` is not passed to `analyze`, indictments are not fetched, and `indictmentMap()` will return an empty map. + +[NOTE] +==== +The key in the indictment map is by default the canonical name of the class. +You can customize it by making your domain objects implement `Indictable`: + +[tabs] +===== +Java:: ++ +[source,java,options="nowrap"] +---- +public class Shift implements Indictable { + ... + @Override + public String getIndictmentClassId() { + return "Shift"; + } +} +---- +===== + +You can also optionally override `getIndictedObject()` to change the `indictee` of an `IndictmentAnalysis`: + +[tabs] +===== +Java:: ++ +[source,java,options="nowrap"] +---- +public class SolverShift implements Indictable { + ... + @Override + public Object getIndictedObject() { + return new ShiftDTO(start, end, ...); + } +} +---- +===== +==== + +A problem fact or entity is considered to contributed to a constraint match when: + +- It is in the tuple initially produced by a `forEach` or `join` for the match. +It does not matter if the tuple was modified by another operation like `expand` or `map`. For example, for the constraint +[tabs] +==== +Java:: ++ +[source,java,options="nowrap"] +---- +Constraint missingRequiredSkill(ConstriantFactory constraintFactory) { + record EmployeeSkillPair(Employee employee, Skill skill) {} + return constraintFactory.forEach(Shift.class) + .map(shift -> new EmployeeSkillPair(shift.getEmployee(), shift.getRequiredSkill())) + .filter(pair -> !pair.employee().getSkillSet().contains(pair.skill())) + .penalize(HardSoftScore.ONE_HARD) + .asConstraint("Missing required skill"); +} +---- +==== +the `Shift` will be the indicted object, and not `EmployeeSkillPair`. + +- It satisfies an `ifExists` the match's tuple passed through. + +- It contributed to an aggregation performed by `groupBy`. + [#scoreAnalysisDiff] === Identifying changes between two solutions @@ -217,6 +309,7 @@ To succeed at this, `ScoreAnalysis` is JSON-friendly and can be easily sent over * or if you directly included the `timefold-solver-jackson` module in your project. If you implemented `ConstraintJustication` to provide custom justification objects, you are responsible for making them JSON-friendly yourself. +Similarly, if you fetched indictments in your `ScoreAnalysis` instance, you are responsible for making the indicted objects JSON-friendly. [NOTE] ==== @@ -227,7 +320,7 @@ 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(...)`. +In that case, use `ScoreAnalysisFetchPolicy.FETCH_MATCH_COUNT` instead of the default `ScoreAnalysisFetchPolicy.FETCH_JUSTIFICATIONS` when calling `SolutionManager.analyze(...)`. [#solutionDiff]