diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/IndexedIfExistsBiNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/IndexedIfExistsBiNode.java index 50b0239e2b..bb83700435 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/IndexedIfExistsBiNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/IndexedIfExistsBiNode.java @@ -5,6 +5,7 @@ import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; public final class IndexedIfExistsBiNode extends AbstractIndexedIfExistsNode, C> { @@ -12,23 +13,10 @@ public final class IndexedIfExistsBiNode extends AbstractIndexedIfExist private final TriPredicate filtering; public IndexedIfExistsBiNode(boolean shouldExist, IndexerFactory indexerFactory, - int inputStoreIndexLeftKeys, int inputStoreIndexLeftCounterEntry, - int inputStoreIndexRightKeys, int inputStoreIndexRightEntry, - TupleLifecycle> nextNodesTupleLifecycle) { - this(shouldExist, indexerFactory, - inputStoreIndexLeftKeys, inputStoreIndexLeftCounterEntry, -1, - inputStoreIndexRightKeys, inputStoreIndexRightEntry, -1, - nextNodesTupleLifecycle, null); - } - - public IndexedIfExistsBiNode(boolean shouldExist, IndexerFactory indexerFactory, - int inputStoreIndexLeftKeys, int inputStoreIndexLeftCounterEntry, int inputStoreIndexLeftTrackerList, - int inputStoreIndexRightKeys, int inputStoreIndexRightEntry, int inputStoreIndexRightTrackerList, + TupleStorePositionTracker leftTupleStorePositionTracker, TupleStorePositionTracker rightTupleStorePositionTracker, TupleLifecycle> nextNodesTupleLifecycle, TriPredicate filtering) { - super(shouldExist, indexerFactory.buildBiLeftKeysExtractor(), indexerFactory, - inputStoreIndexLeftKeys, inputStoreIndexLeftCounterEntry, inputStoreIndexLeftTrackerList, - inputStoreIndexRightKeys, inputStoreIndexRightEntry, inputStoreIndexRightTrackerList, - nextNodesTupleLifecycle, filtering != null); + super(shouldExist, indexerFactory.buildBiLeftKeysExtractor(), indexerFactory, leftTupleStorePositionTracker, + rightTupleStorePositionTracker, nextNodesTupleLifecycle, filtering != null); this.filtering = filtering; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/IndexedJoinBiNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/IndexedJoinBiNode.java index 86eddba25f..09d077fd11 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/IndexedJoinBiNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/IndexedJoinBiNode.java @@ -5,31 +5,26 @@ import ai.timefold.solver.core.impl.bavet.common.AbstractIndexedJoinNode; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.OutputStoreSizeTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; public final class IndexedJoinBiNode extends AbstractIndexedJoinNode, B, BiTuple> { private final BiPredicate filtering; - private final int outputStoreSize; - - public IndexedJoinBiNode(IndexerFactory indexerFactory, - int inputStoreIndexA, int inputStoreIndexEntryA, int inputStoreIndexOutTupleListA, - int inputStoreIndexB, int inputStoreIndexEntryB, int inputStoreIndexOutTupleListB, - TupleLifecycle> nextNodesTupleLifecycle, BiPredicate filtering, - int outputStoreSize, int outputStoreIndexOutEntryA, int outputStoreIndexOutEntryB) { - super(indexerFactory.buildUniLeftKeysExtractor(), indexerFactory, - inputStoreIndexA, inputStoreIndexEntryA, inputStoreIndexOutTupleListA, - inputStoreIndexB, inputStoreIndexEntryB, inputStoreIndexOutTupleListB, - nextNodesTupleLifecycle, filtering != null, - outputStoreIndexOutEntryA, outputStoreIndexOutEntryB); + + public IndexedJoinBiNode(IndexerFactory indexerFactory, TupleStorePositionTracker leftTupleStorePositionTracker, + TupleStorePositionTracker rightTupleStorePositionTracker, OutputStoreSizeTracker outputStoreSizeTracker, + TupleLifecycle> nextNodesTupleLifecycle, BiPredicate filtering) { + super(indexerFactory.buildUniLeftKeysExtractor(), indexerFactory, leftTupleStorePositionTracker, + rightTupleStorePositionTracker, outputStoreSizeTracker, nextNodesTupleLifecycle, filtering != null); this.filtering = filtering; - this.outputStoreSize = outputStoreSize; } @Override protected BiTuple createOutTuple(UniTuple leftTuple, UniTuple rightTuple) { - return new BiTuple<>(leftTuple.factA, rightTuple.factA, outputStoreSize); + return new BiTuple<>(leftTuple.factA, rightTuple.factA, outputStoreSizeTracker.computeOutputStoreSize()); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/UnindexedIfExistsBiNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/UnindexedIfExistsBiNode.java index 7316766738..a54170598a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/UnindexedIfExistsBiNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/UnindexedIfExistsBiNode.java @@ -4,6 +4,7 @@ import ai.timefold.solver.core.impl.bavet.common.AbstractUnindexedIfExistsNode; import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; public final class UnindexedIfExistsBiNode extends AbstractUnindexedIfExistsNode, C> { @@ -11,22 +12,11 @@ public final class UnindexedIfExistsBiNode extends AbstractUnindexedIfE private final TriPredicate filtering; public UnindexedIfExistsBiNode(boolean shouldExist, - int inputStoreIndexLeftCounterEntry, int inputStoreIndexRightEntry, - TupleLifecycle> nextNodesTupleLifecycle) { - this(shouldExist, - inputStoreIndexLeftCounterEntry, -1, inputStoreIndexRightEntry, -1, - nextNodesTupleLifecycle, null); - } - - public UnindexedIfExistsBiNode(boolean shouldExist, - int inputStoreIndexLeftCounterEntry, int inputStoreIndexLeftTrackerList, int inputStoreIndexRightEntry, - int inputStoreIndexRightTrackerList, + TupleStorePositionTracker leftTupleStorePositionTracker, TupleStorePositionTracker rightTupleStorePositionTracker, TupleLifecycle> nextNodesTupleLifecycle, TriPredicate filtering) { - super(shouldExist, - inputStoreIndexLeftCounterEntry, inputStoreIndexLeftTrackerList, inputStoreIndexRightEntry, - inputStoreIndexRightTrackerList, - nextNodesTupleLifecycle, filtering != null); + super(shouldExist, leftTupleStorePositionTracker, rightTupleStorePositionTracker, nextNodesTupleLifecycle, + filtering != null); this.filtering = filtering; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/UnindexedJoinBiNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/UnindexedJoinBiNode.java index ebd0cd9c72..74e3e55588 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/UnindexedJoinBiNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/UnindexedJoinBiNode.java @@ -4,32 +4,27 @@ import ai.timefold.solver.core.impl.bavet.common.AbstractUnindexedJoinNode; import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.OutputStoreSizeTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; public final class UnindexedJoinBiNode extends AbstractUnindexedJoinNode, B, BiTuple> { private final BiPredicate filtering; - private final int outputStoreSize; - - public UnindexedJoinBiNode( - int inputStoreIndexLeftEntry, int inputStoreIndexLeftOutTupleList, - int inputStoreIndexRightEntry, int inputStoreIndexRightOutTupleList, - TupleLifecycle> nextNodesTupleLifecycle, BiPredicate filtering, - int outputStoreSize, - int outputStoreIndexLeftOutEntry, int outputStoreIndexRightOutEntry) { - super(inputStoreIndexLeftEntry, inputStoreIndexLeftOutTupleList, - inputStoreIndexRightEntry, inputStoreIndexRightOutTupleList, - nextNodesTupleLifecycle, filtering != null, - outputStoreIndexLeftOutEntry, outputStoreIndexRightOutEntry); + + public UnindexedJoinBiNode(TupleStorePositionTracker leftTupleStorePositionTracker, + TupleStorePositionTracker rightTupleStorePositionTracker, OutputStoreSizeTracker outputStoreSizeTracker, + TupleLifecycle> nextNodesTupleLifecycle, BiPredicate filtering) { + super(leftTupleStorePositionTracker, rightTupleStorePositionTracker, outputStoreSizeTracker, nextNodesTupleLifecycle, + filtering != null); this.filtering = filtering; - this.outputStoreSize = outputStoreSize; } @Override protected BiTuple createOutTuple(UniTuple leftTuple, UniTuple rightTuple) { - return new BiTuple<>(leftTuple.factA, rightTuple.factA, outputStoreSize); + return new BiTuple<>(leftTuple.factA, rightTuple.factA, outputStoreSizeTracker.computeOutputStoreSize()); } @Override 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 26aecf2ab1..346dc8038a 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,11 +1,11 @@ package ai.timefold.solver.core.impl.bavet.common; +import ai.timefold.solver.core.impl.bavet.common.index.IndexedSet; import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; 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.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; -import ai.timefold.solver.core.impl.util.ElementAwareList; -import ai.timefold.solver.core.impl.util.ElementAwareListEntry; /** * This class has two direct children: {@link AbstractIndexedIfExistsNode} and {@link AbstractUnindexedIfExistsNode}. @@ -20,20 +20,14 @@ public abstract class AbstractIfExistsNode> { protected final boolean shouldExist; - - protected final int inputStoreIndexLeftTrackerList; // -1 if !isFiltering - protected final int inputStoreIndexRightTrackerList; // -1 if !isFiltering - + protected final int inputStoreIndexRightHandleSet; // -1 if !isFiltering protected final boolean isFiltering; private final DynamicPropagationQueue> propagationQueue; - protected AbstractIfExistsNode(boolean shouldExist, - int inputStoreIndexLeftTrackerList, int inputStoreIndexRightTrackerList, - TupleLifecycle nextNodesTupleLifecycle, - boolean isFiltering) { + protected AbstractIfExistsNode(boolean shouldExist, TupleStorePositionTracker rightTupleStorePositionTracker, + TupleLifecycle nextNodesTupleLifecycle, boolean isFiltering) { this.shouldExist = shouldExist; - this.inputStoreIndexLeftTrackerList = inputStoreIndexLeftTrackerList; - this.inputStoreIndexRightTrackerList = inputStoreIndexRightTrackerList; + this.inputStoreIndexRightHandleSet = isFiltering ? rightTupleStorePositionTracker.reserveNextAvailablePosition() : -1; this.isFiltering = isFiltering; this.propagationQueue = new DynamicPropagationQueue<>(nextNodesTupleLifecycle); } @@ -66,8 +60,9 @@ protected void updateCounterLeft(ExistsCounter counter) { } case OK, DYING -> propagationQueue.update(counter); case DEAD, ABORTING -> propagationQueue.insert(counter); - default -> throw new IllegalStateException("Impossible state: the counter (" + counter - + ") has an impossible insert state (" + state + ")."); + default -> + throw new IllegalStateException("Impossible state: the counter (%s) has an impossible insert state (%s)." + .formatted(counter, state)); } } else { // Retract or remain dead @@ -80,8 +75,9 @@ protected void updateCounterLeft(ExistsCounter counter) { propagationQueue.retract(counter, TupleState.ABORTING); case OK, UPDATING -> // Kill the original propagation. propagationQueue.retract(counter, TupleState.DYING); - default -> throw new IllegalStateException("Impossible state: The counter (" + counter - + ") has an impossible retract state (" + state + ")."); + default -> + throw new IllegalStateException("Impossible state: The counter (%s) has an impossible retract state (%s)." + .formatted(counter, state)); } } @@ -115,27 +111,24 @@ protected void decrementCounterRight(ExistsCounter counter) { } // Else do not even propagate an update } - protected ElementAwareList> updateRightTrackerList(UniTuple rightTuple) { - ElementAwareList> rightTrackerList = rightTuple.getStore(inputStoreIndexRightTrackerList); - for (FilteringTracker tuple : rightTrackerList) { - decrementCounterRight(tuple.counter); - tuple.remove(); - } - return rightTrackerList; + IndexedSet> updateRightHandleSet(UniTuple rightTuple) { + IndexedSet> rightHandleSet = rightTuple.getStore(inputStoreIndexRightHandleSet); + rightHandleSet.clear(handle -> { + handle.removeByRight(); + decrementCounterRight(handle.counter); + }); + return rightHandleSet; } - protected void updateCounterFromLeft(LeftTuple_ leftTuple, UniTuple rightTuple, ExistsCounter counter, - ElementAwareList> leftTrackerList) { - if (testFiltering(leftTuple, rightTuple)) { + void updateCounterFromLeft(ExistsCounter counter, UniTuple rightTuple) { + if (testFiltering(counter.leftTuple, rightTuple)) { counter.countRight++; - ElementAwareList> rightTrackerList = - rightTuple.getStore(inputStoreIndexRightTrackerList); - new FilteringTracker<>(counter, leftTrackerList, rightTrackerList); + new ExistsCounterHandle<>(counter, rightTuple.getStore(inputStoreIndexRightHandleSet)); } } - protected void updateCounterFromRight(UniTuple rightTuple, ExistsCounter counter, - ElementAwareList> rightTrackerList) { + void updateCounterFromRight(ExistsCounter counter, UniTuple rightTuple, + IndexedSet> rightHandleSet) { var leftTuple = counter.leftTuple; if (!leftTuple.state.isActive()) { // Assume the following scenario: @@ -154,11 +147,9 @@ protected void updateCounterFromRight(UniTuple rightTuple, ExistsCounter // However, no such issue could have been reproduced; when in doubt, leave it out. return; } - if (testFiltering(counter.leftTuple, rightTuple)) { + if (testFiltering(leftTuple, rightTuple)) { incrementCounterRight(counter); - ElementAwareList> leftTrackerList = - counter.leftTuple.getStore(inputStoreIndexLeftTrackerList); - new FilteringTracker<>(counter, leftTrackerList, rightTrackerList); + new ExistsCounterHandle<>(counter, rightHandleSet); } } @@ -166,8 +157,8 @@ private void doInsertCounter(ExistsCounter counter) { switch (counter.state) { case DYING -> propagationQueue.update(counter); case DEAD, ABORTING -> propagationQueue.insert(counter); - default -> throw new IllegalStateException("Impossible state: the counter (" + counter - + ") has an impossible insert state (" + counter.state + ")."); + default -> throw new IllegalStateException("Impossible state: the counter (%s) has an impossible insert state (%s)." + .formatted(counter, counter.state)); } } @@ -177,8 +168,9 @@ private void doRetractCounter(ExistsCounter counter) { propagationQueue.retract(counter, TupleState.ABORTING); case OK, UPDATING -> // Kill the original propagation. propagationQueue.retract(counter, TupleState.DYING); - default -> throw new IllegalStateException("Impossible state: The counter (" + counter - + ") has an impossible retract state (" + counter.state + ")."); + default -> + throw new IllegalStateException("Impossible state: The counter (%s) has an impossible retract state (%s)." + .formatted(counter, counter.state)); } } @@ -187,23 +179,4 @@ public Propagator getPropagator() { return propagationQueue; } - protected static final class FilteringTracker { - final ExistsCounter counter; - private final ElementAwareListEntry> leftTrackerEntry; - private final ElementAwareListEntry> rightTrackerEntry; - - FilteringTracker(ExistsCounter counter, ElementAwareList> leftTrackerList, - ElementAwareList> rightTrackerList) { - this.counter = counter; - leftTrackerEntry = leftTrackerList.add(this); - rightTrackerEntry = rightTrackerList.add(this); - } - - public void remove() { - leftTrackerEntry.remove(); - rightTrackerEntry.remove(); - } - - } - } 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 1f12232e6a..4baaf2ff40 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 @@ -1,5 +1,6 @@ package ai.timefold.solver.core.impl.bavet.common; +import ai.timefold.solver.core.impl.bavet.common.index.IndexedSet; import ai.timefold.solver.core.impl.bavet.common.index.Indexer; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory.KeysExtractor; @@ -8,9 +9,8 @@ import ai.timefold.solver.core.impl.bavet.common.tuple.LeftTupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.RightTupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; -import ai.timefold.solver.core.impl.util.ElementAwareList; -import ai.timefold.solver.core.impl.util.ElementAwareListEntry; /** * There is a strong likelihood that any change to this class, which is not related to indexing, @@ -26,56 +26,49 @@ public abstract class AbstractIndexedIfExistsNode keysExtractorLeft; private final UniKeysExtractor keysExtractorRight; private final int inputStoreIndexLeftKeys; - private final int inputStoreIndexLeftCounterEntry; + private final int inputStoreIndexLeftCounter; private final int inputStoreIndexRightKeys; - private final int inputStoreIndexRightEntry; private final Indexer> indexerLeft; private final Indexer> indexerRight; protected AbstractIndexedIfExistsNode(boolean shouldExist, KeysExtractor keysExtractorLeft, IndexerFactory indexerFactory, - int inputStoreIndexLeftKeys, int inputStoreIndexLeftCounterEntry, int inputStoreIndexLeftTrackerList, - int inputStoreIndexRightKeys, int inputStoreIndexRightEntry, int inputStoreIndexRightTrackerList, + TupleStorePositionTracker leftTupleStorePositionTracker, TupleStorePositionTracker rightTupleStorePositionTracker, TupleLifecycle nextNodesTupleLifecycle, boolean isFiltering) { - super(shouldExist, - inputStoreIndexLeftTrackerList, inputStoreIndexRightTrackerList, - nextNodesTupleLifecycle, isFiltering); + super(shouldExist, rightTupleStorePositionTracker, nextNodesTupleLifecycle, isFiltering); this.keysExtractorLeft = keysExtractorLeft; this.keysExtractorRight = indexerFactory.buildRightKeysExtractor(); - this.inputStoreIndexLeftKeys = inputStoreIndexLeftKeys; - this.inputStoreIndexLeftCounterEntry = inputStoreIndexLeftCounterEntry; - this.inputStoreIndexRightKeys = inputStoreIndexRightKeys; - this.inputStoreIndexRightEntry = inputStoreIndexRightEntry; - this.indexerLeft = indexerFactory.buildIndexer(true); - this.indexerRight = indexerFactory.buildIndexer(false); + this.inputStoreIndexLeftKeys = leftTupleStorePositionTracker.reserveNextAvailablePosition(); + this.inputStoreIndexLeftCounter = leftTupleStorePositionTracker.reserveNextAvailablePosition(); + this.inputStoreIndexRightKeys = rightTupleStorePositionTracker.reserveNextAvailablePosition(); + this.indexerLeft = indexerFactory.buildIndexer(true, ExistsCounterPositionTracker.instance()); + this.indexerRight = indexerFactory.buildIndexer(false, + new TuplePositionTracker<>(rightTupleStorePositionTracker.reserveNextAvailablePosition())); } @Override public final void insertLeft(LeftTuple_ leftTuple) { if (leftTuple.getStore(inputStoreIndexLeftKeys) != null) { - throw new IllegalStateException("Impossible state: the input for the tuple (" + leftTuple - + ") was already added in the tupleStore."); + throw new IllegalStateException( + "Impossible state: the input for the tuple (%s) was already added in the tupleStore." + .formatted(leftTuple)); } var indexKeys = keysExtractorLeft.apply(leftTuple); leftTuple.setStore(inputStoreIndexLeftKeys, indexKeys); var counter = new ExistsCounter<>(leftTuple); - var counterEntry = indexerLeft.put(indexKeys, counter); - updateCounterRight(leftTuple, indexKeys, counter, counterEntry); + indexerLeft.put(indexKeys, counter); + updateCounterRight(leftTuple, indexKeys, counter); initCounterLeft(counter); } - private void updateCounterRight(LeftTuple_ leftTuple, Object indexKeys, ExistsCounter counter, - ElementAwareListEntry> counterEntry) { - leftTuple.setStore(inputStoreIndexLeftCounterEntry, counterEntry); + private void updateCounterRight(LeftTuple_ leftTuple, Object indexKeys, ExistsCounter counter) { + leftTuple.setStore(inputStoreIndexLeftCounter, counter); if (!isFiltering) { counter.countRight = indexerRight.size(indexKeys); } else { - var leftTrackerList = new ElementAwareList>(); - indexerRight.forEach(indexKeys, - rightTuple -> updateCounterFromLeft(leftTuple, rightTuple, counter, leftTrackerList)); - leftTuple.setStore(inputStoreIndexLeftTrackerList, leftTrackerList); + indexerRight.forEach(indexKeys, rightTuple -> updateCounterFromLeft(counter, rightTuple)); } } @@ -88,8 +81,7 @@ public final void updateLeft(LeftTuple_ leftTuple) { return; } var newIndexKeys = keysExtractorLeft.apply(leftTuple); - ElementAwareListEntry> counterEntry = leftTuple.getStore(inputStoreIndexLeftCounterEntry); - var counter = counterEntry.getElement(); + ExistsCounter counter = leftTuple.getStore(inputStoreIndexLeftCounter); if (oldIndexKeys.equals(newIndexKeys)) { // No need for re-indexing because the index keys didn't change @@ -98,19 +90,16 @@ public final void updateLeft(LeftTuple_ leftTuple) { updateUnchangedCounterLeft(counter); } else { // Call filtering for the leftTuple and rightTuple combinations again - ElementAwareList> leftTrackerList = - leftTuple.getStore(inputStoreIndexLeftTrackerList); - leftTrackerList.forEach(FilteringTracker::remove); - counter.countRight = 0; - indexerRight.forEach(oldIndexKeys, - rightTuple -> updateCounterFromLeft(leftTuple, rightTuple, counter, leftTrackerList)); + counter.clearIncludingCount(); + indexerRight.forEach(oldIndexKeys, rightTuple -> updateCounterFromLeft(counter, rightTuple)); updateCounterLeft(counter); } } else { - updateIndexerLeft(oldIndexKeys, counterEntry, leftTuple); + updateIndexerLeft(oldIndexKeys, counter); counter.countRight = 0; leftTuple.setStore(inputStoreIndexLeftKeys, newIndexKeys); - updateCounterRight(leftTuple, newIndexKeys, counter, indexerLeft.put(newIndexKeys, counter)); + indexerLeft.put(newIndexKeys, counter); + updateCounterRight(leftTuple, newIndexKeys, counter); updateCounterLeft(counter); } } @@ -122,32 +111,29 @@ public final void retractLeft(LeftTuple_ leftTuple) { // No fail fast if null because we don't track which tuples made it through the filter predicate(s) return; } - ElementAwareListEntry> counterEntry = leftTuple.getStore(inputStoreIndexLeftCounterEntry); - var counter = counterEntry.getElement(); - updateIndexerLeft(indexKeys, counterEntry, leftTuple); + ExistsCounter counter = leftTuple.getStore(inputStoreIndexLeftCounter); + updateIndexerLeft(indexKeys, counter); killCounterLeft(counter); } - private void updateIndexerLeft(Object indexKeys, ElementAwareListEntry> counterEntry, - LeftTuple_ leftTuple) { - indexerLeft.remove(indexKeys, counterEntry); + private void updateIndexerLeft(Object indexKeys, ExistsCounter counter) { + indexerLeft.remove(indexKeys, counter); if (isFiltering) { - ElementAwareList> leftTrackerList = leftTuple.getStore(inputStoreIndexLeftTrackerList); - leftTrackerList.forEach(FilteringTracker::remove); + counter.clearWithoutCount(); } } @Override public final void insertRight(UniTuple rightTuple) { if (rightTuple.getStore(inputStoreIndexRightKeys) != null) { - throw new IllegalStateException("Impossible state: the input for the tuple (" + rightTuple - + ") was already added in the tupleStore."); + throw new IllegalStateException( + "Impossible state: the input for the tuple (%s) was already added in the tupleStore." + .formatted(rightTuple)); } var indexKeys = keysExtractorRight.apply(rightTuple); rightTuple.setStore(inputStoreIndexRightKeys, indexKeys); - var rightEntry = indexerRight.put(indexKeys, rightTuple); - rightTuple.setStore(inputStoreIndexRightEntry, rightEntry); + indexerRight.put(indexKeys, rightTuple); updateCounterLeft(rightTuple, indexKeys); } @@ -155,9 +141,9 @@ private void updateCounterLeft(UniTuple rightTuple, Object indexKeys) { if (!isFiltering) { indexerLeft.forEach(indexKeys, this::incrementCounterRight); } else { - var rightTrackerList = new ElementAwareList>(); - indexerLeft.forEach(indexKeys, counter -> updateCounterFromRight(rightTuple, counter, rightTrackerList)); - rightTuple.setStore(inputStoreIndexRightTrackerList, rightTrackerList); + var rightHandleSet = new IndexedSet>(ExistsCounterHandlePositionTracker.right()); + indexerLeft.forEach(indexKeys, counter -> updateCounterFromRight(counter, rightTuple, rightHandleSet)); + rightTuple.setStore(inputStoreIndexRightHandleSet, rightHandleSet); } } @@ -173,21 +159,19 @@ public final void updateRight(UniTuple rightTuple) { if (oldIndexKeys.equals(newIndexKeys)) { // No need for re-indexing because the index keys didn't change if (isFiltering) { - var rightTrackerList = updateRightTrackerList(rightTuple); + var rightHandleSet = updateRightHandleSet(rightTuple); indexerLeft.forEach(oldIndexKeys, - counter -> updateCounterFromRight(rightTuple, counter, rightTrackerList)); + counter -> updateCounterFromRight(counter, rightTuple, rightHandleSet)); } } else { - ElementAwareListEntry> rightEntry = rightTuple.getStore(inputStoreIndexRightEntry); - indexerRight.remove(oldIndexKeys, rightEntry); + indexerRight.remove(oldIndexKeys, rightTuple); if (!isFiltering) { indexerLeft.forEach(oldIndexKeys, this::decrementCounterRight); } else { - updateRightTrackerList(rightTuple); + updateRightHandleSet(rightTuple); } rightTuple.setStore(inputStoreIndexRightKeys, newIndexKeys); - rightEntry = indexerRight.put(newIndexKeys, rightTuple); - rightTuple.setStore(inputStoreIndexRightEntry, rightEntry); + indexerRight.put(newIndexKeys, rightTuple); updateCounterLeft(rightTuple, newIndexKeys); } } @@ -199,12 +183,11 @@ public final void retractRight(UniTuple rightTuple) { // No fail fast if null because we don't track which tuples made it through the filter predicate(s) return; } - ElementAwareListEntry> rightEntry = rightTuple.removeStore(inputStoreIndexRightEntry); - indexerRight.remove(indexKeys, rightEntry); + indexerRight.remove(indexKeys, rightTuple); if (!isFiltering) { indexerLeft.forEach(indexKeys, this::decrementCounterRight); } else { - updateRightTrackerList(rightTuple); + updateRightHandleSet(rightTuple); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedJoinNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedJoinNode.java index 2e050483cf..a5a1e31845 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedJoinNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedJoinNode.java @@ -1,16 +1,17 @@ package ai.timefold.solver.core.impl.bavet.common; +import ai.timefold.solver.core.impl.bavet.common.index.IndexedSet; import ai.timefold.solver.core.impl.bavet.common.index.Indexer; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory.KeysExtractor; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory.UniKeysExtractor; import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.LeftTupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.OutputStoreSizeTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.RightTupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; -import ai.timefold.solver.core.impl.util.ElementAwareList; -import ai.timefold.solver.core.impl.util.ElementAwareListEntry; /** * There is a strong likelihood that any change to this class, which is not related to indexing, @@ -26,9 +27,10 @@ public abstract class AbstractIndexedJoinNode keysExtractorLeft; private final UniKeysExtractor keysExtractorRight; private final int inputStoreIndexLeftKeys; - private final int inputStoreIndexLeftEntry; private final int inputStoreIndexRightKeys; - private final int inputStoreIndexRightEntry; + private final int outputStoreIndexLeftPosition; + private final int outputStoreIndexRightPosition; + /** * Calls for example {@link AbstractScorer#insert(AbstractTuple)} and/or ... */ @@ -36,31 +38,33 @@ public abstract class AbstractIndexedJoinNode> indexerRight; protected AbstractIndexedJoinNode(KeysExtractor keysExtractorLeft, IndexerFactory indexerFactory, - int inputStoreIndexLeftKeys, int inputStoreIndexLeftEntry, int inputStoreIndexLeftOutTupleList, - int inputStoreIndexRightKeys, int inputStoreIndexRightEntry, int inputStoreIndexRightOutTupleList, - TupleLifecycle nextNodesTupleLifecycle, boolean isFiltering, int outputStoreIndexLeftOutEntry, - int outputStoreIndexRightOutEntry) { - super(inputStoreIndexLeftOutTupleList, inputStoreIndexRightOutTupleList, nextNodesTupleLifecycle, isFiltering, - outputStoreIndexLeftOutEntry, outputStoreIndexRightOutEntry); + TupleStorePositionTracker leftTupleStorePositionTracker, TupleStorePositionTracker rightTupleStorePositionTracker, + OutputStoreSizeTracker outputStoreSizeTracker, TupleLifecycle nextNodesTupleLifecycle, + boolean isFiltering) { + super(leftTupleStorePositionTracker, rightTupleStorePositionTracker, outputStoreSizeTracker, nextNodesTupleLifecycle, + isFiltering); this.keysExtractorLeft = keysExtractorLeft; this.keysExtractorRight = indexerFactory.buildRightKeysExtractor(); - this.inputStoreIndexLeftKeys = inputStoreIndexLeftKeys; - this.inputStoreIndexLeftEntry = inputStoreIndexLeftEntry; - this.inputStoreIndexRightKeys = inputStoreIndexRightKeys; - this.inputStoreIndexRightEntry = inputStoreIndexRightEntry; - this.indexerLeft = indexerFactory.buildIndexer(true); - this.indexerRight = indexerFactory.buildIndexer(false); + this.inputStoreIndexLeftKeys = leftTupleStorePositionTracker.reserveNextAvailablePosition(); + this.inputStoreIndexRightKeys = rightTupleStorePositionTracker.reserveNextAvailablePosition(); + this.outputStoreIndexLeftPosition = outputStoreSizeTracker.reserveNextAvailablePosition(); + this.outputStoreIndexRightPosition = outputStoreSizeTracker.reserveNextAvailablePosition(); + this.indexerLeft = indexerFactory.buildIndexer(true, + new TuplePositionTracker<>(leftTupleStorePositionTracker.reserveNextAvailablePosition())); + this.indexerRight = indexerFactory.buildIndexer(false, + new TuplePositionTracker<>(rightTupleStorePositionTracker.reserveNextAvailablePosition())); } @Override public final void insertLeft(LeftTuple_ leftTuple) { if (leftTuple.getStore(inputStoreIndexLeftKeys) != null) { - throw new IllegalStateException("Impossible state: the input for the tuple (" + leftTuple - + ") was already added in the tupleStore."); + throw new IllegalStateException( + "Impossible state: the input for the tuple (%s) was already added in the tupleStore." + .formatted(leftTuple)); } var indexKeys = keysExtractorLeft.apply(leftTuple); - var outTupleListLeft = new ElementAwareList(); - leftTuple.setStore(inputStoreIndexLeftOutTupleList, outTupleListLeft); + var outTupleSetLeft = new IndexedSet<>(new TuplePositionTracker<>(outputStoreIndexLeftPosition)); + leftTuple.setStore(inputStoreIndexLeftOutTupleSet, outTupleSetLeft); indexAndPropagateLeft(leftTuple, indexKeys); } @@ -78,20 +82,17 @@ public final void updateLeft(LeftTuple_ leftTuple) { // Prefer an update over retract-insert if possible innerUpdateLeft(leftTuple, consumer -> indexerRight.forEach(oldIndexKeys, consumer)); } else { - ElementAwareListEntry leftEntry = leftTuple.getStore(inputStoreIndexLeftEntry); - ElementAwareList outTupleListLeft = leftTuple.getStore(inputStoreIndexLeftOutTupleList); - indexerLeft.remove(oldIndexKeys, leftEntry); - outTupleListLeft.forEach(this::retractOutTuple); - // outTupleListLeft is now empty - // No need for leftTuple.setStore(inputStoreIndexLeftOutTupleList, outTupleListLeft); + indexerLeft.remove(oldIndexKeys, leftTuple); + IndexedSet outTupleSetLeft = leftTuple.getStore(inputStoreIndexLeftOutTupleSet); + outTupleSetLeft.clear(this::retractOutTupleByLeft); + // outTupleSetLeft is now empty, no need for leftTuple.setStore(...); indexAndPropagateLeft(leftTuple, newIndexKeys); } } private void indexAndPropagateLeft(LeftTuple_ leftTuple, Object indexKeys) { leftTuple.setStore(inputStoreIndexLeftKeys, indexKeys); - var leftEntry = indexerLeft.put(indexKeys, leftTuple); - leftTuple.setStore(inputStoreIndexLeftEntry, leftEntry); + indexerLeft.put(indexKeys, leftTuple); indexerRight.forEach(indexKeys, rightTuple -> insertOutTupleFiltered(leftTuple, rightTuple)); } @@ -102,21 +103,21 @@ public final void retractLeft(LeftTuple_ leftTuple) { // No fail fast if null because we don't track which tuples made it through the filter predicate(s) return; } - ElementAwareListEntry leftEntry = leftTuple.removeStore(inputStoreIndexLeftEntry); - ElementAwareList outTupleListLeft = leftTuple.removeStore(inputStoreIndexLeftOutTupleList); - indexerLeft.remove(indexKeys, leftEntry); - outTupleListLeft.forEach(this::retractOutTuple); + IndexedSet outTupleSetLeft = leftTuple.removeStore(inputStoreIndexLeftOutTupleSet); + indexerLeft.remove(indexKeys, leftTuple); + outTupleSetLeft.clear(this::retractOutTupleByLeft); } @Override public final void insertRight(UniTuple rightTuple) { if (rightTuple.getStore(inputStoreIndexRightKeys) != null) { - throw new IllegalStateException("Impossible state: the input for the tuple (" + rightTuple - + ") was already added in the tupleStore."); + throw new IllegalStateException( + "Impossible state: the input for the tuple (%s) was already added in the tupleStore." + .formatted(rightTuple)); } var indexKeys = keysExtractorRight.apply(rightTuple); - var outTupleListRight = new ElementAwareList(); - rightTuple.setStore(inputStoreIndexRightOutTupleList, outTupleListRight); + var outTupleSetRight = new IndexedSet<>(new TuplePositionTracker<>(outputStoreIndexRightPosition)); + rightTuple.setStore(inputStoreIndexRightOutTupleSet, outTupleSetRight); indexAndPropagateRight(rightTuple, indexKeys); } @@ -134,20 +135,17 @@ public final void updateRight(UniTuple rightTuple) { // Prefer an update over retract-insert if possible innerUpdateRight(rightTuple, consumer -> indexerLeft.forEach(oldIndexKeys, consumer)); } else { - ElementAwareListEntry> rightEntry = rightTuple.getStore(inputStoreIndexRightEntry); - ElementAwareList outTupleListRight = rightTuple.getStore(inputStoreIndexRightOutTupleList); - indexerRight.remove(oldIndexKeys, rightEntry); - outTupleListRight.forEach(this::retractOutTuple); - // outTupleListRight is now empty - // No need for rightTuple.setStore(inputStoreIndexRightOutTupleList, outTupleListRight); + IndexedSet outTupleSetRight = rightTuple.getStore(inputStoreIndexRightOutTupleSet); + indexerRight.remove(oldIndexKeys, rightTuple); + outTupleSetRight.clear(this::retractOutTupleByRight); + // outTupleSetRight is now empty, no need for rightTuple.setStore(...); indexAndPropagateRight(rightTuple, newIndexKeys); } } private void indexAndPropagateRight(UniTuple rightTuple, Object indexKeys) { rightTuple.setStore(inputStoreIndexRightKeys, indexKeys); - var rightEntry = indexerRight.put(indexKeys, rightTuple); - rightTuple.setStore(inputStoreIndexRightEntry, rightEntry); + indexerRight.put(indexKeys, rightTuple); indexerLeft.forEach(indexKeys, leftTuple -> insertOutTupleFiltered(leftTuple, rightTuple)); } @@ -158,10 +156,9 @@ public final void retractRight(UniTuple rightTuple) { // No fail fast if null because we don't track which tuples made it through the filter predicate(s) return; } - ElementAwareListEntry> rightEntry = rightTuple.removeStore(inputStoreIndexRightEntry); - ElementAwareList outTupleListRight = rightTuple.removeStore(inputStoreIndexRightOutTupleList); - indexerRight.remove(indexKeys, rightEntry); - outTupleListRight.forEach(this::retractOutTuple); + IndexedSet outTupleSetRight = rightTuple.removeStore(inputStoreIndexRightOutTupleSet); + indexerRight.remove(indexKeys, rightTuple); + outTupleSetRight.clear(this::retractOutTupleByRight); } } 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 5303ce47e0..e4ab8822a0 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 @@ -2,12 +2,13 @@ import java.util.function.Consumer; +import ai.timefold.solver.core.impl.bavet.common.index.IndexedSet; import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.OutputStoreSizeTracker; 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.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; -import ai.timefold.solver.core.impl.util.ElementAwareList; -import ai.timefold.solver.core.impl.util.ElementAwareListEntry; /** * This class has two direct children: {@link AbstractIndexedJoinNode} and {@link AbstractUnindexedJoinNode}. @@ -21,21 +22,23 @@ public abstract class AbstractJoinNode extends AbstractTwoInputNode> { - protected final int inputStoreIndexLeftOutTupleList; - protected final int inputStoreIndexRightOutTupleList; + protected final int inputStoreIndexLeftOutTupleSet; + protected final int inputStoreIndexRightOutTupleSet; private final boolean isFiltering; - private final int outputStoreIndexLeftOutEntry; - private final int outputStoreIndexRightOutEntry; + private final int outputStoreIndexLeftOutSet; + private final int outputStoreIndexRightOutSet; + protected final OutputStoreSizeTracker outputStoreSizeTracker; private final StaticPropagationQueue propagationQueue; - protected AbstractJoinNode(int inputStoreIndexLeftOutTupleList, int inputStoreIndexRightOutTupleList, - TupleLifecycle nextNodesTupleLifecycle, boolean isFiltering, - int outputStoreIndexLeftOutEntry, int outputStoreIndexRightOutEntry) { - this.inputStoreIndexLeftOutTupleList = inputStoreIndexLeftOutTupleList; - this.inputStoreIndexRightOutTupleList = inputStoreIndexRightOutTupleList; + protected AbstractJoinNode(TupleStorePositionTracker leftTupleStorePositionTracker, + TupleStorePositionTracker rightTupleStorePositionTracker, OutputStoreSizeTracker outputStoreSizeTracker, + TupleLifecycle nextNodesTupleLifecycle, boolean isFiltering) { + this.inputStoreIndexLeftOutTupleSet = leftTupleStorePositionTracker.reserveNextAvailablePosition(); + this.inputStoreIndexRightOutTupleSet = rightTupleStorePositionTracker.reserveNextAvailablePosition(); this.isFiltering = isFiltering; - this.outputStoreIndexLeftOutEntry = outputStoreIndexLeftOutEntry; - this.outputStoreIndexRightOutEntry = outputStoreIndexRightOutEntry; + this.outputStoreIndexLeftOutSet = outputStoreSizeTracker.reserveNextAvailablePosition(); + this.outputStoreIndexRightOutSet = outputStoreSizeTracker.reserveNextAvailablePosition(); + this.outputStoreSizeTracker = outputStoreSizeTracker; this.propagationQueue = new StaticPropagationQueue<>(nextNodesTupleLifecycle); } @@ -49,12 +52,12 @@ protected AbstractJoinNode(int inputStoreIndexLeftOutTupleList, int inputStoreIn protected final void insertOutTuple(LeftTuple_ leftTuple, UniTuple rightTuple) { var outTuple = createOutTuple(leftTuple, rightTuple); - ElementAwareList outTupleListLeft = leftTuple.getStore(inputStoreIndexLeftOutTupleList); - var outEntryLeft = outTupleListLeft.add(outTuple); - outTuple.setStore(outputStoreIndexLeftOutEntry, outEntryLeft); - ElementAwareList outTupleListRight = rightTuple.getStore(inputStoreIndexRightOutTupleList); - var outEntryRight = outTupleListRight.add(outTuple); - outTuple.setStore(outputStoreIndexRightOutEntry, outEntryRight); + IndexedSet outTupleSetLeft = leftTuple.getStore(inputStoreIndexLeftOutTupleSet); + outTupleSetLeft.add(outTuple); + outTuple.setStore(outputStoreIndexLeftOutSet, outTupleSetLeft); + IndexedSet outTupleSetRight = rightTuple.getStore(inputStoreIndexRightOutTupleSet); + outTupleSetRight.add(outTuple); + outTuple.setStore(outputStoreIndexRightOutSet, outTupleSetRight); propagationQueue.insert(outTuple); } @@ -82,20 +85,71 @@ protected final void insertOutTupleFiltered(LeftTuple_ leftTuple, UniTuple>> rightTupleConsumer) { // Prefer an update over retract-insert if possible - ElementAwareList outTupleListLeft = leftTuple.getStore(inputStoreIndexLeftOutTupleList); + IndexedSet outTupleSetLeft = leftTuple.getStore(inputStoreIndexLeftOutTupleSet); // Propagate the update for downstream filters, matchWeighers, ... if (!isFiltering) { - for (var outTuple : outTupleListLeft) { + outTupleSetLeft.forEach(outTuple -> updateOutTupleLeft(outTuple, leftTuple)); + } else { + if (!leftTuple.state.isActive()) { + // Assume the following scenario: + // - The join is of two entities of the same type, both filtering out unassigned. + // - One entity became unassigned, so the outTuple is getting retracted. + // - The other entity is still assigned and is being updated. + // + // This means the filter would be called with (unassignedEntity, assignedEntity), + // which breaks the expectation that the filter is only called on two assigned entities + // and requires adding null checks to the filter for something that should intuitively be impossible. + // We avoid this situation as it is clear that the outTuple must be retracted anyway, + // and therefore any further updates to it are pointless. + // + // It is possible that the same problem would exist coming from the other side as well, + // and therefore the right tuple would have to be checked for active state as well. + // However, no such issue could have been reproduced; when in doubt, leave it out. + return; + } + rightTupleConsumer.accept(rightTuple -> findAndProcessOutTuple(leftTuple, rightTuple, outTupleSetLeft, + rightTuple.getStore(inputStoreIndexRightOutTupleSet), outputStoreIndexRightOutSet)); + } + } + + private void findAndProcessOutTuple(LeftTuple_ leftTuple, UniTuple rightTuple, IndexedSet sourceSet, + IndexedSet referenceSet, int outputStoreIndex) { + var outTuple = findOutTuple(sourceSet, referenceSet, outputStoreIndex); + if (testFiltering(leftTuple, rightTuple)) { + if (outTuple == null) { + insertOutTuple(leftTuple, rightTuple); + } else { updateOutTupleLeft(outTuple, leftTuple); } } else { - rightTupleConsumer.accept(rightTuple -> { - ElementAwareList rightOutList = rightTuple.getStore(inputStoreIndexRightOutTupleList); - processOutTupleUpdate(leftTuple, rightTuple, rightOutList, outTupleListLeft, outputStoreIndexRightOutEntry); - }); + if (outTuple != null) { + retractOutTuple(outTuple); + } } } + private void retractOutTuple(OutTuple_ outTuple) { + removeFromLeftOutSet(outTuple); + removeFromRightOutSet(outTuple); + propagateRetract(outTuple); + } + + private void removeFromLeftOutSet(OutTuple_ outTuple) { + IndexedSet outSetLeft = outTuple.removeStore(outputStoreIndexLeftOutSet); + outSetLeft.remove(outTuple); + } + + private void removeFromRightOutSet(OutTuple_ outTuple) { + IndexedSet outSetRight = outTuple.removeStore(outputStoreIndexRightOutSet); + outSetRight.remove(outTuple); + } + + private static OutTuple_ findOutTuple(IndexedSet sourceSet, + IndexedSet referenceSet, int outputStoreIndex) { + // Hack: outTuple has no left/right input tuple reference, use the left/right outSet reference instead. + return sourceSet.findFirst(tuple -> referenceSet == tuple.getStore(outputStoreIndex)); + } + private void updateOutTupleLeft(OutTuple_ outTuple, LeftTuple_ leftTuple) { setOutTupleLeftFacts(outTuple, leftTuple); doUpdateOutTuple(outTuple); @@ -114,76 +168,45 @@ private void doUpdateOutTuple(OutTuple_ outTuple) { protected final void innerUpdateRight(UniTuple rightTuple, Consumer> leftTupleConsumer) { // Prefer an update over retract-insert if possible - ElementAwareList outTupleListRight = rightTuple.getStore(inputStoreIndexRightOutTupleList); + IndexedSet outTupleSetRight = rightTuple.getStore(inputStoreIndexRightOutTupleSet); if (!isFiltering) { // Propagate the update for downstream filters, matchWeighers, ... - for (var outTuple : outTupleListRight) { + outTupleSetRight.forEach(outTuple -> { setOutTupleRightFact(outTuple, rightTuple); doUpdateOutTuple(outTuple); - } + }); } else { leftTupleConsumer.accept(leftTuple -> { - ElementAwareList leftOutList = leftTuple.getStore(inputStoreIndexLeftOutTupleList); - processOutTupleUpdate(leftTuple, rightTuple, leftOutList, outTupleListRight, outputStoreIndexLeftOutEntry); + if (!leftTuple.state.isActive()) { + // Assume the following scenario: + // - The join is of two entities of the same type, both filtering out unassigned. + // - One entity became unassigned, so the outTuple is getting retracted. + // - The other entity is still assigned and is being updated. + // + // This means the filter would be called with (unassignedEntity, assignedEntity), + // which breaks the expectation that the filter is only called on two assigned entities + // and requires adding null checks to the filter for something that should intuitively be impossible. + // We avoid this situation as it is clear that the outTuple must be retracted anyway, + // and therefore any further updates to it are pointless. + // + // It is possible that the same problem would exist coming from the other side as well, + // and therefore the right tuple would have to be checked for active state as well. + // However, no such issue could have been reproduced; when in doubt, leave it out. + return; + } + findAndProcessOutTuple(leftTuple, rightTuple, outTupleSetRight, + leftTuple.getStore(inputStoreIndexLeftOutTupleSet), outputStoreIndexLeftOutSet); }); } } - private void processOutTupleUpdate(LeftTuple_ leftTuple, UniTuple rightTuple, ElementAwareList outList, - ElementAwareList outTupleList, int outputStoreIndexOutEntry) { - if (!leftTuple.state.isActive()) { - // Assume the following scenario: - // - The join is of two entities of the same type, both filtering out unassigned. - // - One entity became unassigned, so the outTuple is getting retracted. - // - The other entity is still assigned and is being updated. - // - // This means the filter would be called with (unassignedEntity, assignedEntity), - // which breaks the expectation that the filter is only called on two assigned entities - // and requires adding null checks to the filter for something that should intuitively be impossible. - // We avoid this situation as it is clear that the outTuple must be retracted anyway, - // and therefore any further updates to it are pointless. - // - // It is possible that the same problem would exist coming from the other side as well, - // and therefore the right tuple would have to be checked for active state as well. - // However, no such issue could have been reproduced; when in doubt, leave it out. - return; - } - var outTuple = findOutTuple(outTupleList, outList, outputStoreIndexOutEntry); - if (testFiltering(leftTuple, rightTuple)) { - if (outTuple == null) { - insertOutTuple(leftTuple, rightTuple); - } else { - updateOutTupleLeft(outTuple, leftTuple); - } - } else { - if (outTuple != null) { - retractOutTuple(outTuple); - } - } - } - - private static Tuple_ findOutTuple(ElementAwareList outTupleList, - ElementAwareList outList, int outputStoreIndexOutEntry) { - // Hack: the outTuple has no left/right input tuple reference, use the left/right outList reference instead. - var item = outTupleList.first(); - while (item != null) { - // Creating list iterators here caused major GC pressure; therefore, we iterate over the entries directly. - var outTuple = item.getElement(); - ElementAwareListEntry outEntry = outTuple.getStore(outputStoreIndexOutEntry); - var outEntryList = outEntry.getList(); - if (outList == outEntryList) { - return outTuple; - } - item = item.next(); - } - return null; + protected final void retractOutTupleByLeft(OutTuple_ outTuple) { + outTuple.removeStore(outputStoreIndexLeftOutSet); // The tuple will be removed from the set by the caller. + removeFromRightOutSet(outTuple); + propagateRetract(outTuple); } - protected final void retractOutTuple(OutTuple_ outTuple) { - ElementAwareListEntry outEntryLeft = outTuple.removeStore(outputStoreIndexLeftOutEntry); - outEntryLeft.remove(); - ElementAwareListEntry outEntryRight = outTuple.removeStore(outputStoreIndexRightOutEntry); - outEntryRight.remove(); + private void propagateRetract(OutTuple_ outTuple) { var state = outTuple.state; if (!state.isActive()) { // Impossible because they shouldn't linger in the indexes. throw new IllegalStateException("Impossible state: The tuple (%s) in node (%s) is in an unexpected state (%s)." @@ -192,6 +215,12 @@ protected final void retractOutTuple(OutTuple_ outTuple) { propagationQueue.retract(outTuple, state == TupleState.CREATING ? TupleState.ABORTING : TupleState.DYING); } + protected final void retractOutTupleByRight(OutTuple_ outTuple) { + removeFromLeftOutSet(outTuple); + outTuple.removeStore(outputStoreIndexRightOutSet); // The tuple will be removed from the set by the caller. + propagateRetract(outTuple); + } + @Override public Propagator getPropagator() { return propagationQueue; 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 9709dd62a1..a640c481ce 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 @@ -1,12 +1,12 @@ package ai.timefold.solver.core.impl.bavet.common; +import ai.timefold.solver.core.impl.bavet.common.index.IndexedSet; import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.LeftTupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.RightTupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; -import ai.timefold.solver.core.impl.util.ElementAwareList; -import ai.timefold.solver.core.impl.util.ElementAwareListEntry; /** * There is a strong likelihood that any change made to this class @@ -19,134 +19,115 @@ public abstract class AbstractUnindexedIfExistsNode implements LeftTupleLifecycle, RightTupleLifecycle> { - private final int inputStoreIndexLeftCounterEntry; + private final int inputStoreIndexLeftCounter; + private final int inputStoreIndexRightTuple; - private final int inputStoreIndexRightEntry; + private final IndexedSet> leftCounterSet; + private final IndexedSet> rightTupleSet; - // Acts as a leftTupleList too - private final ElementAwareList> leftCounterList = new ElementAwareList<>(); - private final ElementAwareList> rightTupleList = new ElementAwareList<>(); - - protected AbstractUnindexedIfExistsNode(boolean shouldExist, - int inputStoreIndexLeftCounterEntry, int inputStoreIndexLeftTrackerList, int inputStoreIndexRightEntry, - int inputStoreIndexRightTrackerList, - TupleLifecycle nextNodesTupleLifecycle, boolean isFiltering) { - super(shouldExist, - inputStoreIndexLeftTrackerList, inputStoreIndexRightTrackerList, - nextNodesTupleLifecycle, isFiltering); - this.inputStoreIndexLeftCounterEntry = inputStoreIndexLeftCounterEntry; - this.inputStoreIndexRightEntry = inputStoreIndexRightEntry; + protected AbstractUnindexedIfExistsNode(boolean shouldExist, TupleStorePositionTracker leftTupleStorePositionTracker, + TupleStorePositionTracker rightTupleStorePositionTracker, TupleLifecycle nextNodesTupleLifecycle, + boolean isFiltering) { + super(shouldExist, rightTupleStorePositionTracker, nextNodesTupleLifecycle, isFiltering); + this.inputStoreIndexLeftCounter = leftTupleStorePositionTracker.reserveNextAvailablePosition(); + this.inputStoreIndexRightTuple = rightTupleStorePositionTracker.reserveNextAvailablePosition(); + this.leftCounterSet = new IndexedSet<>(ExistsCounterPositionTracker.instance()); + this.rightTupleSet = new IndexedSet<>(new TuplePositionTracker<>(inputStoreIndexRightTuple)); } @Override public final void insertLeft(LeftTuple_ leftTuple) { - if (leftTuple.getStore(inputStoreIndexLeftCounterEntry) != null) { - throw new IllegalStateException("Impossible state: the input for the tuple (" + leftTuple - + ") was already added in the tupleStore."); + if (leftTuple.getStore(inputStoreIndexLeftCounter) != null) { + throw new IllegalStateException( + "Impossible state: the input for the tuple (%s) was already added in the tupleStore." + .formatted(leftTuple)); } var counter = new ExistsCounter<>(leftTuple); - var counterEntry = leftCounterList.add(counter); - leftTuple.setStore(inputStoreIndexLeftCounterEntry, counterEntry); + leftCounterSet.add(counter); + leftTuple.setStore(inputStoreIndexLeftCounter, counter); if (!isFiltering) { - counter.countRight = rightTupleList.size(); + counter.countRight = rightTupleSet.size(); } else { - var leftTrackerList = new ElementAwareList>(); - for (var tuple : rightTupleList) { - updateCounterFromLeft(leftTuple, tuple, counter, leftTrackerList); - } - leftTuple.setStore(inputStoreIndexLeftTrackerList, leftTrackerList); + rightTupleSet.forEach(tuple -> updateCounterFromLeft(counter, tuple)); } initCounterLeft(counter); } @Override public final void updateLeft(LeftTuple_ leftTuple) { - ElementAwareListEntry> counterEntry = leftTuple.getStore(inputStoreIndexLeftCounterEntry); - if (counterEntry == null) { + ExistsCounter counter = leftTuple.getStore(inputStoreIndexLeftCounter); + if (counter == null) { // No fail fast if null because we don't track which tuples made it through the filter predicate(s) insertLeft(leftTuple); return; } - var counter = counterEntry.getElement(); // The indexers contain counters in the DEAD state, to track the rightCount. if (!isFiltering) { updateUnchangedCounterLeft(counter); } else { // Call filtering for the leftTuple and rightTuple combinations again - ElementAwareList> leftTrackerList = leftTuple.getStore(inputStoreIndexLeftTrackerList); - leftTrackerList.forEach(FilteringTracker::remove); - counter.countRight = 0; - for (var tuple : rightTupleList) { - updateCounterFromLeft(leftTuple, tuple, counter, leftTrackerList); - } + counter.clearIncludingCount(); + rightTupleSet.forEach(tuple -> updateCounterFromLeft(counter, tuple)); updateCounterLeft(counter); } } @Override public final void retractLeft(LeftTuple_ leftTuple) { - ElementAwareListEntry> counterEntry = leftTuple.removeStore(inputStoreIndexLeftCounterEntry); - if (counterEntry == null) { + ExistsCounter counter = leftTuple.removeStore(inputStoreIndexLeftCounter); + if (counter == null) { // No fail fast if null because we don't track which tuples made it through the filter predicate(s) return; } - var counter = counterEntry.getElement(); - counterEntry.remove(); + leftCounterSet.remove(counter); if (isFiltering) { - ElementAwareList> leftTrackerList = leftTuple.getStore(inputStoreIndexLeftTrackerList); - leftTrackerList.forEach(FilteringTracker::remove); + counter.clearWithoutCount(); } killCounterLeft(counter); } @Override public final void insertRight(UniTuple rightTuple) { - if (rightTuple.getStore(inputStoreIndexRightEntry) != null) { - throw new IllegalStateException("Impossible state: the input for the tuple (" + rightTuple - + ") was already added in the tupleStore."); + if (rightTuple.getStore(inputStoreIndexRightTuple) != null) { + throw new IllegalStateException( + "Impossible state: the input for the tuple (%s) was already added in the tupleStore." + .formatted(rightTuple)); } - var rightEntry = rightTupleList.add(rightTuple); - rightTuple.setStore(inputStoreIndexRightEntry, rightEntry); + rightTupleSet.add(rightTuple); if (!isFiltering) { - leftCounterList.forEach(this::incrementCounterRight); + leftCounterSet.forEach(this::incrementCounterRight); } else { - var rightTrackerList = new ElementAwareList>(); - for (var tuple : leftCounterList) { - updateCounterFromRight(rightTuple, tuple, rightTrackerList); - } - rightTuple.setStore(inputStoreIndexRightTrackerList, rightTrackerList); + var rightHandleSet = new IndexedSet>(ExistsCounterHandlePositionTracker.right()); + leftCounterSet.forEach(counter -> updateCounterFromRight(counter, rightTuple, rightHandleSet)); + rightTuple.setStore(inputStoreIndexRightHandleSet, rightHandleSet); } } @Override public final void updateRight(UniTuple rightTuple) { - ElementAwareListEntry> rightEntry = rightTuple.getStore(inputStoreIndexRightEntry); - if (rightEntry == null) { + if (rightTuple.getStore(inputStoreIndexRightTuple) == null) { // No fail fast if null because we don't track which tuples made it through the filter predicate(s) insertRight(rightTuple); return; } if (isFiltering) { - var rightTrackerList = updateRightTrackerList(rightTuple); - for (var tuple : leftCounterList) { - updateCounterFromRight(rightTuple, tuple, rightTrackerList); - } + var rightHandleSet = updateRightHandleSet(rightTuple); + leftCounterSet.forEach(counter -> updateCounterFromRight(counter, rightTuple, rightHandleSet)); } } @Override public final void retractRight(UniTuple rightTuple) { - ElementAwareListEntry> rightEntry = rightTuple.removeStore(inputStoreIndexRightEntry); - if (rightEntry == null) { + if (rightTuple.getStore(inputStoreIndexRightTuple) == null) { // No fail fast if null because we don't track which tuples made it through the filter predicate(s) return; } - rightEntry.remove(); + rightTupleSet.remove(rightTuple); if (!isFiltering) { - leftCounterList.forEach(this::decrementCounterRight); + leftCounterSet.forEach(this::decrementCounterRight); } else { - updateRightTrackerList(rightTuple); + updateRightHandleSet(rightTuple); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedJoinNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedJoinNode.java index 971b569288..30863698fc 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedJoinNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractUnindexedJoinNode.java @@ -1,12 +1,13 @@ package ai.timefold.solver.core.impl.bavet.common; +import ai.timefold.solver.core.impl.bavet.common.index.IndexedSet; import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.LeftTupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.OutputStoreSizeTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.RightTupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; -import ai.timefold.solver.core.impl.util.ElementAwareList; -import ai.timefold.solver.core.impl.util.ElementAwareListEntry; /** * There is a strong likelihood that any change made to this class @@ -19,95 +20,92 @@ public abstract class AbstractUnindexedJoinNode implements LeftTupleLifecycle, RightTupleLifecycle> { - private final int inputStoreIndexLeftEntry; - private final int inputStoreIndexRightEntry; - private final ElementAwareList leftTupleList = new ElementAwareList<>(); - private final ElementAwareList> rightTupleList = new ElementAwareList<>(); + private final int inputStoreIndexLeftPosition; + private final int inputStoreIndexRightPosition; + private final int outputStoreIndexLeftPosition; + private final int outputStoreIndexRightPosition; + private final IndexedSet leftTupleSet; + private final IndexedSet> rightTupleSet; - protected AbstractUnindexedJoinNode(int inputStoreIndexLeftEntry, int inputStoreIndexLeftOutTupleList, - int inputStoreIndexRightEntry, int inputStoreIndexRightOutTupleList, - TupleLifecycle nextNodesTupleLifecycle, boolean isFiltering, int outputStoreIndexLeftOutEntry, - int outputStoreIndexRightOutEntry) { - super(inputStoreIndexLeftOutTupleList, inputStoreIndexRightOutTupleList, nextNodesTupleLifecycle, isFiltering, - outputStoreIndexLeftOutEntry, outputStoreIndexRightOutEntry); - this.inputStoreIndexLeftEntry = inputStoreIndexLeftEntry; - this.inputStoreIndexRightEntry = inputStoreIndexRightEntry; + protected AbstractUnindexedJoinNode(TupleStorePositionTracker leftTupleStorePositionTracker, + TupleStorePositionTracker rightTupleStorePositionTracker, OutputStoreSizeTracker outputStoreSizeTracker, + TupleLifecycle nextNodesTupleLifecycle, boolean isFiltering) { + super(leftTupleStorePositionTracker, rightTupleStorePositionTracker, outputStoreSizeTracker, nextNodesTupleLifecycle, + isFiltering); + this.inputStoreIndexLeftPosition = leftTupleStorePositionTracker.reserveNextAvailablePosition(); + this.inputStoreIndexRightPosition = rightTupleStorePositionTracker.reserveNextAvailablePosition(); + this.outputStoreIndexLeftPosition = outputStoreSizeTracker.reserveNextAvailablePosition(); + this.outputStoreIndexRightPosition = outputStoreSizeTracker.reserveNextAvailablePosition(); + this.leftTupleSet = new IndexedSet<>(new TuplePositionTracker<>(inputStoreIndexLeftPosition)); + this.rightTupleSet = new IndexedSet<>(new TuplePositionTracker<>(inputStoreIndexRightPosition)); } @Override public final void insertLeft(LeftTuple_ leftTuple) { - if (leftTuple.getStore(inputStoreIndexLeftEntry) != null) { - throw new IllegalStateException("Impossible state: the input for the tuple (" + leftTuple - + ") was already added in the tupleStore."); - } - var leftEntry = leftTupleList.add(leftTuple); - leftTuple.setStore(inputStoreIndexLeftEntry, leftEntry); - var outTupleListLeft = new ElementAwareList(); - leftTuple.setStore(inputStoreIndexLeftOutTupleList, outTupleListLeft); - for (var tuple : rightTupleList) { - insertOutTupleFiltered(leftTuple, tuple); + if (leftTuple.getStore(inputStoreIndexLeftPosition) != null) { + throw new IllegalStateException( + "Impossible state: the input for the tuple (%s) was already added in the tupleStore." + .formatted(leftTuple)); } + leftTupleSet.add(leftTuple); + var outTupleSetLeft = new IndexedSet<>(new TuplePositionTracker<>(outputStoreIndexLeftPosition)); + leftTuple.setStore(inputStoreIndexLeftOutTupleSet, outTupleSetLeft); + rightTupleSet.forEach(tuple -> insertOutTupleFiltered(leftTuple, tuple)); } @Override public final void updateLeft(LeftTuple_ leftTuple) { - ElementAwareListEntry leftEntry = leftTuple.getStore(inputStoreIndexLeftEntry); - if (leftEntry == null) { + if (leftTuple.getStore(inputStoreIndexLeftPosition) == null) { // No fail fast if null because we don't track which tuples made it through the filter predicate(s) insertLeft(leftTuple); return; } - innerUpdateLeft(leftTuple, rightTupleList::forEach); + innerUpdateLeft(leftTuple, rightTupleSet::forEach); } @Override public final void retractLeft(LeftTuple_ leftTuple) { - ElementAwareListEntry leftEntry = leftTuple.removeStore(inputStoreIndexLeftEntry); - if (leftEntry == null) { + if (leftTuple.getStore(inputStoreIndexLeftPosition) == null) { // No fail fast if null because we don't track which tuples made it through the filter predicate(s) return; } - ElementAwareList outTupleListLeft = leftTuple.removeStore(inputStoreIndexLeftOutTupleList); - leftEntry.remove(); - outTupleListLeft.forEach(this::retractOutTuple); + IndexedSet outTupleSetLeft = leftTuple.removeStore(inputStoreIndexLeftOutTupleSet); + leftTupleSet.remove(leftTuple); + outTupleSetLeft.clear(this::retractOutTupleByLeft); } @Override public final void insertRight(UniTuple rightTuple) { - if (rightTuple.getStore(inputStoreIndexRightEntry) != null) { - throw new IllegalStateException("Impossible state: the input for the tuple (" + rightTuple - + ") was already added in the tupleStore."); - } - var rightEntry = rightTupleList.add(rightTuple); - rightTuple.setStore(inputStoreIndexRightEntry, rightEntry); - var outTupleListRight = new ElementAwareList(); - rightTuple.setStore(inputStoreIndexRightOutTupleList, outTupleListRight); - for (var tuple : leftTupleList) { - insertOutTupleFiltered(tuple, rightTuple); + if (rightTuple.getStore(inputStoreIndexRightPosition) != null) { + throw new IllegalStateException( + "Impossible state: the input for the tuple (%s) was already added in the tupleStore." + .formatted(rightTuple)); } + rightTupleSet.add(rightTuple); + var outTupleSetRight = new IndexedSet(new TuplePositionTracker<>(outputStoreIndexRightPosition)); + rightTuple.setStore(inputStoreIndexRightOutTupleSet, outTupleSetRight); + leftTupleSet.forEach(tuple -> insertOutTupleFiltered(tuple, rightTuple)); } @Override public final void updateRight(UniTuple rightTuple) { - ElementAwareListEntry> rightEntry = rightTuple.getStore(inputStoreIndexRightEntry); - if (rightEntry == null) { + if (rightTuple.getStore(inputStoreIndexRightPosition) == null) { // No fail fast if null because we don't track which tuples made it through the filter predicate(s) insertRight(rightTuple); return; } - innerUpdateRight(rightTuple, leftTupleList::forEach); + innerUpdateRight(rightTuple, leftTupleSet::forEach); } @Override public final void retractRight(UniTuple rightTuple) { - ElementAwareListEntry> rightEntry = rightTuple.removeStore(inputStoreIndexRightEntry); - if (rightEntry == null) { + if (rightTuple.getStore(inputStoreIndexRightPosition) == null) { // No fail fast if null because we don't track which tuples made it through the filter predicate(s) return; } - ElementAwareList outTupleListRight = rightTuple.removeStore(inputStoreIndexRightOutTupleList); - rightEntry.remove(); - outTupleListRight.forEach(this::retractOutTuple); + IndexedSet outTupleSetRight = rightTuple.removeStore(inputStoreIndexRightOutTupleSet); + rightTupleSet.remove(rightTuple); + outTupleSetRight.clear(this::retractOutTupleByRight); } } 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 a47bdd2910..cd2ae4390e 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,19 +1,34 @@ package ai.timefold.solver.core.impl.bavet.common; +import ai.timefold.solver.core.impl.bavet.common.index.IndexedSet; import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState; +import org.jspecify.annotations.NullMarked; + +@NullMarked public final class ExistsCounter extends AbstractPropagationMetadataCarrier { final Tuple_ leftTuple; + final IndexedSet> leftHandleSet = new IndexedSet<>(ExistsCounterHandlePositionTracker.left()); TupleState state = TupleState.DEAD; // It's the node's job to mark a new instance as CREATING. int countRight = 0; + int indexedSetPositon = -1; ExistsCounter(Tuple_ leftTuple) { this.leftTuple = leftTuple; } + public void clearWithoutCount() { + leftHandleSet.clear(ExistsCounterHandle::removeByLeft); + } + + public void clearIncludingCount() { + clearWithoutCount(); + countRight = 0; + } + @Override public Tuple_ getTuple() { return leftTuple; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterHandle.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterHandle.java new file mode 100644 index 0000000000..b1219c32da --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterHandle.java @@ -0,0 +1,39 @@ +package ai.timefold.solver.core.impl.bavet.common; + +import ai.timefold.solver.core.impl.bavet.common.index.IndexedSet; +import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; + +import org.jspecify.annotations.NullMarked; + +/** + * Used for filtering in {@link AbstractIfExistsNode}. + * There is no place where both left and right sets for each counter would be kept together, + * therefore we create this handle to avoid expensive iteration. + * (The alternative would be to look up things on the left when we have the right, and vice versa.) + * + * @param + */ +@NullMarked +final class ExistsCounterHandle { + + final ExistsCounter counter; + final IndexedSet> rightHandleSet; + int leftPosition = -1; + int rightPosition = -1; + + ExistsCounterHandle(ExistsCounter counter, IndexedSet> rightHandleSet) { + this.counter = counter; + counter.leftHandleSet.add(this); + this.rightHandleSet = rightHandleSet; + rightHandleSet.add(this); + } + + public void removeByLeft() { + rightHandleSet.remove(this); // The counter will be removed from the left handle set by the caller. + } + + public void removeByRight() { + counter.leftHandleSet.remove(this); // The counter will be removed from the right handle set by the caller. + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterHandlePositionTracker.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterHandlePositionTracker.java new file mode 100644 index 0000000000..b19efed2a5 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterHandlePositionTracker.java @@ -0,0 +1,61 @@ +package ai.timefold.solver.core.impl.bavet.common; + +import java.util.function.ToIntFunction; + +import ai.timefold.solver.core.impl.bavet.common.index.ElementPositionTracker; +import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; + +import org.jspecify.annotations.NullMarked; + +@SuppressWarnings({ "rawtypes", "unchecked" }) +@NullMarked +record ExistsCounterHandlePositionTracker( + ToIntFunction> positionGetter, + PositionSetter positionSetter) + implements + ElementPositionTracker> { + + private static final ExistsCounterHandlePositionTracker LEFT = new ExistsCounterHandlePositionTracker( + (ToIntFunction) tracker -> tracker.leftPosition, + (tracker, position) -> { + var oldValue = tracker.leftPosition; + tracker.leftPosition = position; + return oldValue; + }); + private static final ExistsCounterHandlePositionTracker RIGHT = new ExistsCounterHandlePositionTracker( + (ToIntFunction) tracker -> tracker.rightPosition, + (tracker, position) -> { + var oldValue = tracker.rightPosition; + tracker.rightPosition = position; + return oldValue; + }); + + public static ExistsCounterHandlePositionTracker left() { + return LEFT; + } + + public static ExistsCounterHandlePositionTracker right() { + return RIGHT; + } + + @Override + public void setPosition(ExistsCounterHandle element, int position) { + positionSetter.apply(element, position); + } + + @Override + public int clearPosition(ExistsCounterHandle element) { + var oldPosition = positionGetter.applyAsInt(element); + positionSetter.apply(element, -1); + return oldPosition; + } + + @FunctionalInterface + @NullMarked + interface PositionSetter { + + int apply(ExistsCounterHandle tracker, int position); + + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterPositionTracker.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterPositionTracker.java new file mode 100644 index 0000000000..d6c3645300 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/ExistsCounterPositionTracker.java @@ -0,0 +1,32 @@ +package ai.timefold.solver.core.impl.bavet.common; + +import ai.timefold.solver.core.impl.bavet.common.index.ElementPositionTracker; +import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; + +import org.jspecify.annotations.NullMarked; + +@SuppressWarnings({ "unchecked", "rawtypes" }) +@NullMarked +record ExistsCounterPositionTracker() + implements + ElementPositionTracker> { + + private static final ExistsCounterPositionTracker INSTANCE = new ExistsCounterPositionTracker(); + + public static ExistsCounterPositionTracker instance() { + return INSTANCE; + } + + @Override + public void setPosition(ExistsCounter element, int position) { + element.indexedSetPositon = position; + } + + @Override + public int clearPosition(ExistsCounter element) { + var oldPosition = element.indexedSetPositon; + element.indexedSetPositon = -1; + return oldPosition; + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/TuplePositionTracker.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/TuplePositionTracker.java new file mode 100644 index 0000000000..355768a5c4 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/TuplePositionTracker.java @@ -0,0 +1,30 @@ +package ai.timefold.solver.core.impl.bavet.common; + +import ai.timefold.solver.core.impl.bavet.common.index.ElementPositionTracker; +import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; +import ai.timefold.solver.core.impl.util.MutableInt; + +import org.jspecify.annotations.NullMarked; + +@NullMarked +public record TuplePositionTracker(int inputStorePosition) + implements + ElementPositionTracker { + + @Override + public void setPosition(Tuple_ element, int position) { // Avoids autoboxing on updates. + MutableInt oldPosition = element.getStore(inputStorePosition); + if (oldPosition == null) { + element.setStore(inputStorePosition, new MutableInt(position)); + } else { + oldPosition.setValue(position); + } + } + + @Override + public int clearPosition(Tuple_ element) { + MutableInt oldPosition = element.removeStore(inputStorePosition); + return oldPosition == null ? -1 : oldPosition.intValue(); + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ComparisonIndexer.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ComparisonIndexer.java index 09c5102468..a783a10d31 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ComparisonIndexer.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ComparisonIndexer.java @@ -9,7 +9,6 @@ import java.util.function.Supplier; import ai.timefold.solver.core.impl.bavet.common.joiner.JoinerType; -import ai.timefold.solver.core.impl.util.ElementAwareListEntry; final class ComparisonIndexer> implements Indexer { @@ -26,8 +25,8 @@ final class ComparisonIndexer> * * @param comparisonJoinerType the type of comparison to use */ - public ComparisonIndexer(JoinerType comparisonJoinerType) { - this(comparisonJoinerType, new SingleKeyRetriever<>(), NoneIndexer::new); + public ComparisonIndexer(JoinerType comparisonJoinerType, ElementPositionTracker elementPositionTracker) { + this(comparisonJoinerType, new SingleKeyRetriever<>(), () -> new NoneIndexer<>(elementPositionTracker)); } /** @@ -61,7 +60,7 @@ private ComparisonIndexer(JoinerType comparisonJoinerType, KeyRetriever ke } @Override - public ElementAwareListEntry put(Object indexKeys, T tuple) { + public void put(Object indexKeys, T element) { Key_ indexKey = keyRetriever.apply(indexKeys); // Avoids computeIfAbsent in order to not create lambdas on the hot path. var downstreamIndexer = comparisonMap.get(indexKey); @@ -69,25 +68,25 @@ public ElementAwareListEntry put(Object indexKeys, T tuple) { downstreamIndexer = downstreamIndexerSupplier.get(); comparisonMap.put(indexKey, downstreamIndexer); } - return downstreamIndexer.put(indexKeys, tuple); + downstreamIndexer.put(indexKeys, element); } @Override - public void remove(Object indexKeys, ElementAwareListEntry entry) { + public void remove(Object indexKeys, T element) { Key_ indexKey = keyRetriever.apply(indexKeys); - var downstreamIndexer = getDownstreamIndexer(indexKeys, indexKey, entry); - downstreamIndexer.remove(indexKeys, entry); + var downstreamIndexer = getDownstreamIndexer(indexKeys, indexKey, element); + downstreamIndexer.remove(indexKeys, element); if (downstreamIndexer.isEmpty()) { comparisonMap.remove(indexKey); } } - private Indexer getDownstreamIndexer(Object indexKeys, Key_ indexerKey, ElementAwareListEntry entry) { + private Indexer getDownstreamIndexer(Object indexKeys, Key_ indexerKey, T entry) { var downstreamIndexer = comparisonMap.get(indexerKey); if (downstreamIndexer == null) { throw new IllegalStateException( "Impossible state: the tuple (%s) with indexKeys (%s) doesn't exist in the indexer %s." - .formatted(entry.getElement(), indexKeys, this)); + .formatted(entry, indexKeys, this)); } return downstreamIndexer; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ElementPositionTracker.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ElementPositionTracker.java new file mode 100644 index 0000000000..c3460ef140 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ElementPositionTracker.java @@ -0,0 +1,30 @@ +package ai.timefold.solver.core.impl.bavet.common.index; + +import org.jspecify.annotations.NullMarked; + +/** + * Allows to read and modify the position of an element in an {@link IndexedSet}. + * Typically points to a field in the element itself. + * + * @param + */ +@NullMarked +public interface ElementPositionTracker { + + /** + * Sets the position of the given element. + * + * @param element never null + * @param position >= 0 + */ + void setPosition(T element, int position); + + /** + * Clears the position of the given element. + * + * @param element never null + * @return the previous position of the element, or -1 if it was not tracked before + */ + int clearPosition(T element); + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsIndexer.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsIndexer.java index f81d90c82a..365b4faf95 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsIndexer.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsIndexer.java @@ -6,8 +6,6 @@ import java.util.function.Consumer; import java.util.function.Supplier; -import ai.timefold.solver.core.impl.util.ElementAwareListEntry; - final class EqualsIndexer implements Indexer { private final KeyRetriever keyRetriever; @@ -18,9 +16,9 @@ final class EqualsIndexer implements Indexer { * Construct an {@link EqualsIndexer} which immediately ends in a {@link NoneIndexer}. * This means {@code indexKeys} must be a single key. */ - public EqualsIndexer() { + public EqualsIndexer(ElementPositionTracker elementPositionTracker) { this.keyRetriever = new SingleKeyRetriever<>(); - this.downstreamIndexerSupplier = NoneIndexer::new; + this.downstreamIndexerSupplier = () -> new NoneIndexer<>(elementPositionTracker); } /** @@ -36,7 +34,7 @@ public EqualsIndexer(int keyIndex, Supplier> downstreamIndexerSupplie } @Override - public ElementAwareListEntry put(Object indexKeys, T tuple) { + public void put(Object indexKeys, T element) { Key_ indexKey = keyRetriever.apply(indexKeys); // Avoids computeIfAbsent in order to not create lambdas on the hot path. Indexer downstreamIndexer = downstreamIndexerMap.get(indexKey); @@ -44,25 +42,25 @@ public ElementAwareListEntry put(Object indexKeys, T tuple) { downstreamIndexer = downstreamIndexerSupplier.get(); downstreamIndexerMap.put(indexKey, downstreamIndexer); } - return downstreamIndexer.put(indexKeys, tuple); + downstreamIndexer.put(indexKeys, element); } @Override - public void remove(Object indexKeys, ElementAwareListEntry entry) { + public void remove(Object indexKeys, T element) { Key_ indexKey = keyRetriever.apply(indexKeys); - Indexer downstreamIndexer = getDownstreamIndexer(indexKeys, indexKey, entry); - downstreamIndexer.remove(indexKeys, entry); + Indexer downstreamIndexer = getDownstreamIndexer(indexKeys, indexKey, element); + downstreamIndexer.remove(indexKeys, element); if (downstreamIndexer.isEmpty()) { downstreamIndexerMap.remove(indexKey); } } - private Indexer getDownstreamIndexer(Object indexKeys, Key_ indexerKey, ElementAwareListEntry entry) { + private Indexer getDownstreamIndexer(Object indexKeys, Key_ indexerKey, T entry) { Indexer downstreamIndexer = downstreamIndexerMap.get(indexerKey); if (downstreamIndexer == null) { throw new IllegalStateException( "Impossible state: the tuple (%s) with indexKey (%s) doesn't exist in the indexer %s." - .formatted(entry.getElement(), indexKeys, this)); + .formatted(entry, indexKeys, this)); } return downstreamIndexer; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSet.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSet.java new file mode 100644 index 0000000000..ddcf96f379 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSet.java @@ -0,0 +1,332 @@ +package ai.timefold.solver.core.impl.bavet.common.index; + +import ai.timefold.solver.core.impl.util.ElementAwareList; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * {@link ArrayList}-backed set which allows to {@link #remove(Object)} an element + * without knowing its position and without an expensive lookup. + * It also allows for direct random access like a list. + * The order of iteration isn't guaranteed to be the insertion order, + * and it changes over time as elements are added and removed. + *

+ * It uses an {@link ElementPositionTracker} to track the insertion position of each element. + * When an element is removed, it's replaced by null at its insertion position, creating a gap; + * therefore, the insertion position of later elements isn't changed. + * The set is compacted back during iteration or when {@link #asList()} is called, + * by replacing these gaps with elements from the back of the set. + * This operation also doesn't change the insertion position of any elements + * except for those that are moved from the back to fill a gap. + *

+ * Together with the fact that removals are relatively rare, + * this keeps the overhead low while giving us all benefits of {@link ArrayList}, + * such as memory efficiency, random access, and fast iteration. + * Random access isn't required for Constraint Streams, but Neighborhoods make heavy use of it; + * if we used the {@link ElementAwareList} implementation instead, + * we'd have to copy the elements to an array every time we need to access them randomly during move generation. + *

+ * For performance reasons, this class doesn't check if an element was already added; + * duplicates must be avoided by the caller and will cause undefined behavior. + *

+ * This class isn't thread-safe. + * It's in fact very thread-unsafe. + * + * @param + */ +@NullMarked +public final class IndexedSet { + + // Compaction during forEach() only makes performance sense for larger sets with a significant amount of gaps. + static final int MINIMUM_ELEMENT_COUNT_FOR_COMPACTION = 20; + static final double GAP_RATIO_FOR_COMPACTION = 0.1; + + private final ElementPositionTracker elementPositionTracker; + private @Nullable List<@Nullable T> elementList; // Lazily initialized, so that empty indexes use no memory. + private int lastElementPosition = -1; + private int gapCount = 0; + + public IndexedSet(ElementPositionTracker elementPositionTracker) { + this.elementPositionTracker = Objects.requireNonNull(elementPositionTracker); + } + + /** + * Appends the specified element to the end of this collection. + * If the element is already present, + * an undefined, unexpected, and incorrect behavior should be expected. + *

+ * Presence of the element can be checked using the associated {@link ElementPositionTracker}. + * For performance reasons, this method avoids that check. + * + * @param element element to be appended to this collection + */ + public void add(T element) { + if (elementList == null) { + elementList = new ArrayList<>(); + } + elementList.add(element); + elementPositionTracker.setPosition(element, ++lastElementPosition); + } + + /** + * Removes the first occurrence of the specified element from this collection, if present. + * Will use identity comparison to check for presence; + * two different instances which {@link Object#equals(Object) equal} are considered different elements. + * + * @param element element to be removed from this collection + * @throws IllegalStateException if the element wasn't found in this collection + */ + public void remove(T element) { + var insertionPosition = elementPositionTracker.clearPosition(element); + if (insertionPosition < 0) { + throw new IllegalStateException("Impossible state: the element (%s) was not found in the IndexedSet." + .formatted(element)); + } + if (insertionPosition == lastElementPosition) { + // The element was the last one added; we can simply remove it. + elementList.remove(lastElementPosition--); + } else { + // We replace the element with null, creating a gap. + elementList.set(insertionPosition, null); + gapCount++; + } + clearIfPossible(); + } + + private boolean clearIfPossible() { + if (gapCount > 0 && lastElementPosition + 1 == gapCount) { // All positions are gaps. Clear the list entirely. + forceClear(); + return true; + } + return false; + } + + private void forceClear() { + if (elementList != null) { + elementList.clear(); + } + gapCount = 0; + lastElementPosition = -1; + } + + public boolean isEmpty() { + return size() == 0; + } + + public int size() { + return lastElementPosition - gapCount + 1; + } + + /** + * Performs the given action for each element of the collection + * until all elements have been processed. + * The order of iteration may change as elements are added and removed. + * + * @param elementConsumer the action to be performed for each element; + * mustn't modify the collection + */ + public void forEach(Consumer elementConsumer) { + if (isEmpty()) { + return; + } + if (shouldCompact()) { + forEachCompacting(elementConsumer); + } else { + forEachNonCompacting(elementConsumer); + } + } + + private boolean shouldCompact() { + if (gapCount == 0) { + return false; + } + var elementCount = lastElementPosition + 1; + if (elementCount < MINIMUM_ELEMENT_COUNT_FOR_COMPACTION) { + return false; + } + var gapPercentage = gapCount / (double) elementCount; + return gapPercentage > GAP_RATIO_FOR_COMPACTION; + } + + private void forEachNonCompacting(Consumer elementConsumer) { + forEachNonCompacting(elementConsumer, 0); + } + + private void forEachNonCompacting(Consumer elementConsumer, int startingIndex) { + for (var i = startingIndex; i <= lastElementPosition; i++) { + var element = elementList.get(i); + if (element != null) { + elementConsumer.accept(element); + } + } + } + + private void forEachCompacting(Consumer elementConsumer) { + if (clearIfPossible()) { + return; + } + for (var i = 0; i <= lastElementPosition; i++) { + var element = elementList.get(i); + if (element == null) { + element = fillGap(i); + if (element == null) { // There was nothing to fill the gap with; we are done. + return; + } + } + elementConsumer.accept(element); + if (gapCount == 0) { + // No more gaps to fill; we can continue without compacting. + // This is an optimized loop which no longer checks for gaps. + forEachNonCompacting(elementConsumer, i + 1); + return; + } + } + } + + /** + * Fills the gap at position i by moving the last element into it. + * + * @param gapPosition the position of the gap to fill + * @return the element that now occupies position i, or null if no element further in the list can fill the gap + */ + private @Nullable T fillGap(int gapPosition) { + var lastRemovedElement = removeLastNonGap(gapPosition); + if (lastRemovedElement == null) { + return null; + } + elementList.set(gapPosition, lastRemovedElement); + elementPositionTracker.setPosition(lastRemovedElement, gapPosition); + gapCount--; + return lastRemovedElement; + } + + private @Nullable T removeLastNonGap(int gapPosition) { + while (lastElementPosition >= gapPosition) { + var lastRemovedElement = elementList.remove(lastElementPosition--); + if (lastRemovedElement != null) { + return lastRemovedElement; + } + gapCount--; + } + return null; + } + + /** + * As defined by {@link #forEach(Consumer)}, + * but stops when the predicate returns true for an element. + * + * @param elementPredicate the predicate to be tested for each element; mustn't modify the collection + * @return the first element for which the predicate returned true, or null if none + */ + public @Nullable T findFirst(Predicate elementPredicate) { + if (isEmpty()) { + return null; + } + return shouldCompact() ? findFirstCompacting(elementPredicate) : findFirstNonCompacting(elementPredicate); + } + + private @Nullable T findFirstNonCompacting(Predicate elementPredicate) { + return findFirstNonCompacting(elementPredicate, 0); + } + + private @Nullable T findFirstNonCompacting(Predicate elementPredicate, int startingIndex) { + for (var i = startingIndex; i <= lastElementPosition; i++) { + var element = elementList.get(i); + if (element != null && elementPredicate.test(element)) { + return element; + } + } + return null; + } + + private @Nullable T findFirstCompacting(Predicate elementPredicate) { + if (clearIfPossible()) { + return null; + } + for (var i = 0; i <= lastElementPosition; i++) { + var element = elementList.get(i); + if (element == null) { + element = fillGap(i); + if (element == null) { // There was nothing to fill the gap with; we are done. + return null; + } + } + if (elementPredicate.test(element)) { + return element; + } + if (gapCount == 0) { + // No more gaps to fill; we can continue without compacting. + // This is an optimized loop which no longer checks for gaps. + return findFirstNonCompacting(elementPredicate, i + 1); + } + } + return null; + } + + /** + * Empties the collection. + * For each element being removed, the given consumer is called. + * This is a more efficient implementation than removing elements individually. + * + * @param elementConsumer the consumer to be called for each element being removed; mustn't modify the collection + */ + public void clear(Consumer elementConsumer) { + var nonGapCount = size(); + if (nonGapCount == 0) { + forceClear(); + return; + } + for (var i = 0; i <= lastElementPosition; i++) { + var element = elementList.get(i); + if (element == null) { + continue; + } + elementConsumer.accept(element); + elementPositionTracker.clearPosition(element); + // We can stop early once all non-gap elements have been processed. + if (--nonGapCount == 0) { + break; + } + } + forceClear(); + } + + /** + * Returns a standard {@link List} view of this collection. + * Users mustn't modify the returned list, as that'd also change the underlying data structure. + * + * @return a standard list view of this element-aware list + */ + public List asList() { + if (elementList == null) { + return Collections.emptyList(); + } + if (gapCount > 0) { // The list mustn't return any nulls. + forceCompaction(); + } + return elementList.isEmpty() ? Collections.emptyList() : elementList; + } + + private void forceCompaction() { + if (clearIfPossible()) { + return; + } + for (var i = 0; i <= lastElementPosition; i++) { + var element = elementList.get(i); + if (element == null) { + fillGap(i); + if (gapCount == 0) { // If there are no more gaps, we can stop. + return; + } + } + } + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/Indexer.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/Indexer.java index add7191cf1..b1375e5d78 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/Indexer.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/Indexer.java @@ -3,7 +3,6 @@ import java.util.function.Consumer; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState; -import ai.timefold.solver.core.impl.util.ElementAwareListEntry; /** * An indexer for entity or fact {@code X}, @@ -22,9 +21,9 @@ */ public sealed interface Indexer permits ComparisonIndexer, EqualsIndexer, NoneIndexer { - ElementAwareListEntry put(Object indexKeys, T tuple); + void put(Object indexKeys, T element); - void remove(Object indexKeys, ElementAwareListEntry entry); + void remove(Object indexKeys, T element); int size(Object indexKeys); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexerFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexerFactory.java index ce4eec540e..09f53fb401 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexerFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/IndexerFactory.java @@ -473,34 +473,32 @@ public UniKeysExtractor buildRightKeysExtractor() { return buildUniKeysExtractor(joiner::getRightMapping); } - public Indexer buildIndexer(boolean isLeftBridge) { - /* - * Note that if creating indexer for a right bridge node, the joiner type has to be flipped. - * ( becomes .) - */ + public Indexer buildIndexer(boolean isLeftBridge, ElementPositionTracker elementPositionTracker) { + // Note that if creating indexer for a right bridge node, the joiner type has to be flipped. + // ( becomes .) if (!hasJoiners()) { // NoneJoiner results in NoneIndexer. - return new NoneIndexer<>(); + return new NoneIndexer<>(elementPositionTracker); } else if (joiner.getJoinerCount() == 1) { // Single joiner maps directly to EqualsIndexer or ComparisonIndexer. var joinerType = joiner.getJoinerType(0); if (joinerType == JoinerType.EQUAL) { - return new EqualsIndexer<>(); + return new EqualsIndexer<>(elementPositionTracker); } else { - return new ComparisonIndexer<>(isLeftBridge ? joinerType : joinerType.flip()); + return new ComparisonIndexer<>(isLeftBridge ? joinerType : joinerType.flip(), elementPositionTracker); } } // The following code builds the children first, so it needs to iterate over the joiners in reverse order. var descendingJoinerTypeMap = joinerTypeMap.descendingMap(); - Supplier> noneIndexerSupplier = NoneIndexer::new; + Supplier> noneIndexerSupplier = () -> new NoneIndexer<>(elementPositionTracker); Supplier> downstreamIndexerSupplier = noneIndexerSupplier; var indexPropertyId = descendingJoinerTypeMap.size() - 1; for (var entry : descendingJoinerTypeMap.entrySet()) { var joinerType = entry.getValue(); if (downstreamIndexerSupplier == noneIndexerSupplier && indexPropertyId == 0) { if (joinerType == JoinerType.EQUAL) { - downstreamIndexerSupplier = EqualsIndexer::new; + downstreamIndexerSupplier = () -> new EqualsIndexer<>(elementPositionTracker); } else { var actualJoinerType = isLeftBridge ? joinerType : joinerType.flip(); - downstreamIndexerSupplier = () -> new ComparisonIndexer<>(actualJoinerType); + downstreamIndexerSupplier = () -> new ComparisonIndexer<>(actualJoinerType, elementPositionTracker); } } else { var actualDownstreamIndexerSupplier = downstreamIndexerSupplier; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/NoneIndexer.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/NoneIndexer.java index 88d060df76..277c2095ed 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/NoneIndexer.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/NoneIndexer.java @@ -2,41 +2,42 @@ import java.util.function.Consumer; -import ai.timefold.solver.core.impl.util.ElementAwareList; -import ai.timefold.solver.core.impl.util.ElementAwareListEntry; - public final class NoneIndexer implements Indexer { - private final ElementAwareList tupleList = new ElementAwareList<>(); + private final IndexedSet store; + + public NoneIndexer(ElementPositionTracker elementPositionTracker) { + this.store = new IndexedSet<>(elementPositionTracker); + } @Override - public ElementAwareListEntry put(Object indexKeys, T tuple) { - return tupleList.add(tuple); + public void put(Object indexKeys, T element) { + store.add(element); } @Override - public void remove(Object indexKeys, ElementAwareListEntry entry) { - entry.remove(); + public void remove(Object indexKeys, T element) { + store.remove(element); } @Override public int size(Object indexKeys) { - return tupleList.size(); + return store.size(); } @Override public void forEach(Object indexKeys, Consumer tupleConsumer) { - tupleList.forEach(tupleConsumer); + store.forEach(tupleConsumer); } @Override public boolean isEmpty() { - return tupleList.size() == 0; + return store.isEmpty(); } @Override public String toString() { - return "size = " + tupleList.size(); + return "size = " + store.size(); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/OutputStoreSizeTracker.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/OutputStoreSizeTracker.java new file mode 100644 index 0000000000..b9e9a3e66d --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/OutputStoreSizeTracker.java @@ -0,0 +1,46 @@ +package ai.timefold.solver.core.impl.bavet.common.tuple; + +/** + * Tracks the size of the output store by allowing reservations of positions. + * Once the final size is computed, no further reservations can be made + * and output tuples will be created using this size of their {@link AbstractTuple tuple store}. + */ +public final class OutputStoreSizeTracker implements TupleStorePositionTracker { + + private int effectiveOutputStoreSize; + private int finalOutputStoreSize = -1; + + public OutputStoreSizeTracker(int initialSize) { + if (initialSize < 0) { + throw new IllegalArgumentException( + "Impossible state: The initialSize (%d) must be non-negative.".formatted(initialSize)); + } + this.effectiveOutputStoreSize = initialSize; + } + + /** + * @return the next available position in the output store, reserved exclusively for use by the caller + * @throws IllegalStateException if {@link #computeOutputStoreSize()} has already been called. + */ + @Override + public int reserveNextAvailablePosition() { + if (finalOutputStoreSize >= 0) { + throw new IllegalStateException("Impossible state: The finalOutputStoreSize (%s) has already been computed." + .formatted(finalOutputStoreSize)); + } + return effectiveOutputStoreSize++; + } + + /** + * Finalizes the output store size and prevents further reservations. + * + * @return the final output store size + */ + public int computeOutputStoreSize() { + if (finalOutputStoreSize < 0) { + finalOutputStoreSize = effectiveOutputStoreSize; + } + return finalOutputStoreSize; + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/TupleStorePositionTracker.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/TupleStorePositionTracker.java new file mode 100644 index 0000000000..87a34cf448 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/TupleStorePositionTracker.java @@ -0,0 +1,7 @@ +package ai.timefold.solver.core.impl.bavet.common.tuple; + +public interface TupleStorePositionTracker { + + int reserveNextAvailablePosition(); + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/IndexedIfExistsQuadNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/IndexedIfExistsQuadNode.java index 3503e7fd0d..7f282d844d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/IndexedIfExistsQuadNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/IndexedIfExistsQuadNode.java @@ -5,6 +5,7 @@ import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.tuple.QuadTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; public final class IndexedIfExistsQuadNode extends AbstractIndexedIfExistsNode, E> { @@ -12,23 +13,10 @@ public final class IndexedIfExistsQuadNode extends AbstractIndexe private final PentaPredicate filtering; public IndexedIfExistsQuadNode(boolean shouldExist, IndexerFactory indexerFactory, - int inputStoreIndexLeftKeys, int inputStoreIndexLeftCounterEntry, - int inputStoreIndexRightKeys, int inputStoreIndexRightEntry, - TupleLifecycle> nextNodesTupleLifecycle) { - this(shouldExist, indexerFactory, - inputStoreIndexLeftKeys, inputStoreIndexLeftCounterEntry, -1, - inputStoreIndexRightKeys, inputStoreIndexRightEntry, -1, - nextNodesTupleLifecycle, null); - } - - public IndexedIfExistsQuadNode(boolean shouldExist, IndexerFactory indexerFactory, - int inputStoreIndexLeftKeys, int inputStoreIndexLeftCounterEntry, int inputStoreIndexLeftTrackerList, - int inputStoreIndexRightKeys, int inputStoreIndexRightEntry, int inputStoreIndexRightTrackerList, + TupleStorePositionTracker leftTupleStorePositionTracker, TupleStorePositionTracker rightTupleStorePositionTracker, TupleLifecycle> nextNodesTupleLifecycle, PentaPredicate filtering) { - super(shouldExist, indexerFactory.buildQuadLeftKeysExtractor(), indexerFactory, - inputStoreIndexLeftKeys, inputStoreIndexLeftCounterEntry, inputStoreIndexLeftTrackerList, - inputStoreIndexRightKeys, inputStoreIndexRightEntry, inputStoreIndexRightTrackerList, - nextNodesTupleLifecycle, filtering != null); + super(shouldExist, indexerFactory.buildQuadLeftKeysExtractor(), indexerFactory, leftTupleStorePositionTracker, + rightTupleStorePositionTracker, nextNodesTupleLifecycle, filtering != null); this.filtering = filtering; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/IndexedJoinQuadNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/IndexedJoinQuadNode.java index adc92c74d5..2e583a0657 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/IndexedJoinQuadNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/IndexedJoinQuadNode.java @@ -3,35 +3,30 @@ import ai.timefold.solver.core.api.function.QuadPredicate; import ai.timefold.solver.core.impl.bavet.common.AbstractIndexedJoinNode; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; +import ai.timefold.solver.core.impl.bavet.common.tuple.OutputStoreSizeTracker; 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.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; public final class IndexedJoinQuadNode extends AbstractIndexedJoinNode, D, QuadTuple> { private final QuadPredicate filtering; - private final int outputStoreSize; - - public IndexedJoinQuadNode(IndexerFactory indexerFactory, - int inputStoreIndexABC, int inputStoreIndexEntryABC, int inputStoreIndexOutTupleListABC, - int inputStoreIndexD, int inputStoreIndexEntryD, int inputStoreIndexOutTupleListD, - TupleLifecycle> nextNodesTupleLifecycle, QuadPredicate filtering, - int outputStoreSize, int outputStoreIndexOutEntryABC, int outputStoreIndexOutEntryD) { - super(indexerFactory.buildTriLeftKeysExtractor(), indexerFactory, - inputStoreIndexABC, inputStoreIndexEntryABC, inputStoreIndexOutTupleListABC, - inputStoreIndexD, inputStoreIndexEntryD, inputStoreIndexOutTupleListD, - nextNodesTupleLifecycle, filtering != null, - outputStoreIndexOutEntryABC, outputStoreIndexOutEntryD); + + public IndexedJoinQuadNode(IndexerFactory indexerFactory, TupleStorePositionTracker leftTupleStorePositionTracker, + TupleStorePositionTracker rightTupleStorePositionTracker, OutputStoreSizeTracker outputStoreSizeTracker, + TupleLifecycle> nextNodesTupleLifecycle, QuadPredicate filtering) { + super(indexerFactory.buildTriLeftKeysExtractor(), indexerFactory, leftTupleStorePositionTracker, + rightTupleStorePositionTracker, outputStoreSizeTracker, nextNodesTupleLifecycle, filtering != null); this.filtering = filtering; - this.outputStoreSize = outputStoreSize; } @Override protected QuadTuple createOutTuple(TriTuple leftTuple, UniTuple rightTuple) { return new QuadTuple<>(leftTuple.factA, leftTuple.factB, leftTuple.factC, rightTuple.factA, - outputStoreSize); + outputStoreSizeTracker.computeOutputStoreSize()); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/UnindexedIfExistsQuadNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/UnindexedIfExistsQuadNode.java index 88de613585..4de6ab2ec3 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/UnindexedIfExistsQuadNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/UnindexedIfExistsQuadNode.java @@ -4,6 +4,7 @@ import ai.timefold.solver.core.impl.bavet.common.AbstractUnindexedIfExistsNode; import ai.timefold.solver.core.impl.bavet.common.tuple.QuadTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; public final class UnindexedIfExistsQuadNode extends AbstractUnindexedIfExistsNode, E> { @@ -11,21 +12,11 @@ public final class UnindexedIfExistsQuadNode extends AbstractUnin private final PentaPredicate filtering; public UnindexedIfExistsQuadNode(boolean shouldExist, - int inputStoreIndexLeftCounterEntry, int inputStoreIndexRightEntry, - TupleLifecycle> nextNodesTupleLifecycle) { - this(shouldExist, - inputStoreIndexLeftCounterEntry, -1, inputStoreIndexRightEntry, -1, - nextNodesTupleLifecycle, null); - } - - public UnindexedIfExistsQuadNode(boolean shouldExist, - int inputStoreIndexLeftCounterEntry, int inputStoreIndexLeftTrackerList, int inputStoreIndexRightEntry, - int inputStoreIndexRightTrackerList, + TupleStorePositionTracker leftTupleStorePositionTracker, TupleStorePositionTracker rightTupleStorePositionTracker, TupleLifecycle> nextNodesTupleLifecycle, PentaPredicate filtering) { super(shouldExist, - inputStoreIndexLeftCounterEntry, inputStoreIndexLeftTrackerList, inputStoreIndexRightEntry, - inputStoreIndexRightTrackerList, + leftTupleStorePositionTracker, rightTupleStorePositionTracker, nextNodesTupleLifecycle, filtering != null); this.filtering = filtering; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/UnindexedJoinQuadNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/UnindexedJoinQuadNode.java index 96111c2bc3..8aeb285ced 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/UnindexedJoinQuadNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/UnindexedJoinQuadNode.java @@ -2,35 +2,30 @@ import ai.timefold.solver.core.api.function.QuadPredicate; import ai.timefold.solver.core.impl.bavet.common.AbstractUnindexedJoinNode; +import ai.timefold.solver.core.impl.bavet.common.tuple.OutputStoreSizeTracker; 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.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; public final class UnindexedJoinQuadNode extends AbstractUnindexedJoinNode, D, QuadTuple> { private final QuadPredicate filtering; - private final int outputStoreSize; - - public UnindexedJoinQuadNode( - int inputStoreIndexLeftEntry, int inputStoreIndexLeftOutTupleList, - int inputStoreIndexRightEntry, int inputStoreIndexRightOutTupleList, - TupleLifecycle> nextNodesTupleLifecycle, QuadPredicate filtering, - int outputStoreSize, - int outputStoreIndexLeftOutEntry, int outputStoreIndexRightOutEntry) { - super(inputStoreIndexLeftEntry, inputStoreIndexLeftOutTupleList, - inputStoreIndexRightEntry, inputStoreIndexRightOutTupleList, - nextNodesTupleLifecycle, filtering != null, - outputStoreIndexLeftOutEntry, outputStoreIndexRightOutEntry); + + public UnindexedJoinQuadNode(TupleStorePositionTracker leftTupleStorePositionTracker, + TupleStorePositionTracker rightTupleStorePositionTracker, OutputStoreSizeTracker outputStoreSizeTracker, + TupleLifecycle> nextNodesTupleLifecycle, QuadPredicate filtering) { + super(leftTupleStorePositionTracker, rightTupleStorePositionTracker, outputStoreSizeTracker, nextNodesTupleLifecycle, + filtering != null); this.filtering = filtering; - this.outputStoreSize = outputStoreSize; } @Override protected QuadTuple createOutTuple(TriTuple leftTuple, UniTuple rightTuple) { return new QuadTuple<>(leftTuple.factA, leftTuple.factB, leftTuple.factC, rightTuple.factA, - outputStoreSize); + outputStoreSizeTracker.computeOutputStoreSize()); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/IndexedIfExistsTriNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/IndexedIfExistsTriNode.java index e061769494..65e942d9a5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/IndexedIfExistsTriNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/IndexedIfExistsTriNode.java @@ -5,6 +5,7 @@ import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.tuple.TriTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; public final class IndexedIfExistsTriNode extends AbstractIndexedIfExistsNode, D> { @@ -12,23 +13,10 @@ public final class IndexedIfExistsTriNode extends AbstractIndexedIfE private final QuadPredicate filtering; public IndexedIfExistsTriNode(boolean shouldExist, IndexerFactory indexerFactory, - int inputStoreIndexLeftKeys, int inputStoreIndexLeftCounterEntry, - int inputStoreIndexRightKeys, int inputStoreIndexRightEntry, - TupleLifecycle> nextNodesTupleLifecycle) { - this(shouldExist, indexerFactory, - inputStoreIndexLeftKeys, inputStoreIndexLeftCounterEntry, -1, - inputStoreIndexRightKeys, inputStoreIndexRightEntry, -1, - nextNodesTupleLifecycle, null); - } - - public IndexedIfExistsTriNode(boolean shouldExist, IndexerFactory indexerFactory, - int inputStoreIndexLeftKeys, int inputStoreIndexLeftCounterEntry, int inputStoreIndexLeftTrackerList, - int inputStoreIndexRightKeys, int inputStoreIndexRightEntry, int inputStoreIndexRightTrackerList, + TupleStorePositionTracker leftTupleStorePositionTracker, TupleStorePositionTracker rightTupleStorePositionTracker, TupleLifecycle> nextNodesTupleLifecycle, QuadPredicate filtering) { - super(shouldExist, indexerFactory.buildTriLeftKeysExtractor(), indexerFactory, - inputStoreIndexLeftKeys, inputStoreIndexLeftCounterEntry, inputStoreIndexLeftTrackerList, - inputStoreIndexRightKeys, inputStoreIndexRightEntry, inputStoreIndexRightTrackerList, - nextNodesTupleLifecycle, filtering != null); + super(shouldExist, indexerFactory.buildTriLeftKeysExtractor(), indexerFactory, leftTupleStorePositionTracker, + rightTupleStorePositionTracker, nextNodesTupleLifecycle, filtering != null); this.filtering = filtering; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/IndexedJoinTriNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/IndexedJoinTriNode.java index 3751f3bd4f..d14b25f8a0 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/IndexedJoinTriNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/IndexedJoinTriNode.java @@ -4,33 +4,29 @@ import ai.timefold.solver.core.impl.bavet.common.AbstractIndexedJoinNode; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.OutputStoreSizeTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.TriTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; public final class IndexedJoinTriNode extends AbstractIndexedJoinNode, C, TriTuple> { private final TriPredicate filtering; - private final int outputStoreSize; - - public IndexedJoinTriNode(IndexerFactory indexerFactory, - int inputStoreIndexAB, int inputStoreIndexEntryAB, int inputStoreIndexOutTupleListAB, - int inputStoreIndexC, int inputStoreIndexEntryC, int inputStoreIndexOutTupleListC, - TupleLifecycle> nextNodesTupleLifecycle, TriPredicate filtering, - int outputStoreSize, int outputStoreIndexOutEntryAB, int outputStoreIndexOutEntryC) { - super(indexerFactory.buildBiLeftKeysExtractor(), indexerFactory, - inputStoreIndexAB, inputStoreIndexEntryAB, inputStoreIndexOutTupleListAB, - inputStoreIndexC, inputStoreIndexEntryC, inputStoreIndexOutTupleListC, - nextNodesTupleLifecycle, filtering != null, - outputStoreIndexOutEntryAB, outputStoreIndexOutEntryC); + + public IndexedJoinTriNode(IndexerFactory indexerFactory, TupleStorePositionTracker leftTupleStorePositionTracker, + TupleStorePositionTracker rightTupleStorePositionTracker, OutputStoreSizeTracker outputStoreSizeTracker, + TupleLifecycle> nextNodesTupleLifecycle, TriPredicate filtering) { + super(indexerFactory.buildBiLeftKeysExtractor(), indexerFactory, leftTupleStorePositionTracker, + rightTupleStorePositionTracker, outputStoreSizeTracker, nextNodesTupleLifecycle, filtering != null); this.filtering = filtering; - this.outputStoreSize = outputStoreSize; } @Override protected TriTuple createOutTuple(BiTuple leftTuple, UniTuple rightTuple) { - return new TriTuple<>(leftTuple.factA, leftTuple.factB, rightTuple.factA, outputStoreSize); + return new TriTuple<>(leftTuple.factA, leftTuple.factB, rightTuple.factA, + outputStoreSizeTracker.computeOutputStoreSize()); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/UnindexedIfExistsTriNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/UnindexedIfExistsTriNode.java index b83d7623f1..99d2607339 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/UnindexedIfExistsTriNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/UnindexedIfExistsTriNode.java @@ -4,6 +4,7 @@ import ai.timefold.solver.core.impl.bavet.common.AbstractUnindexedIfExistsNode; import ai.timefold.solver.core.impl.bavet.common.tuple.TriTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; public final class UnindexedIfExistsTriNode extends AbstractUnindexedIfExistsNode, D> { @@ -11,22 +12,11 @@ public final class UnindexedIfExistsTriNode extends AbstractUnindexe private final QuadPredicate filtering; public UnindexedIfExistsTriNode(boolean shouldExist, - int inputStoreIndexLeftCounterEntry, int inputStoreIndexRightEntry, - TupleLifecycle> nextNodesTupleLifecycle) { - this(shouldExist, - inputStoreIndexLeftCounterEntry, -1, inputStoreIndexRightEntry, -1, - nextNodesTupleLifecycle, null); - } - - public UnindexedIfExistsTriNode(boolean shouldExist, - int inputStoreIndexLeftCounterEntry, int inputStoreIndexLeftTrackerList, int inputStoreIndexRightEntry, - int inputStoreIndexRightTrackerList, + TupleStorePositionTracker leftTupleStorePositionTracker, TupleStorePositionTracker rightTupleStorePositionTracker, TupleLifecycle> nextNodesTupleLifecycle, QuadPredicate filtering) { - super(shouldExist, - inputStoreIndexLeftCounterEntry, inputStoreIndexLeftTrackerList, inputStoreIndexRightEntry, - inputStoreIndexRightTrackerList, - nextNodesTupleLifecycle, filtering != null); + super(shouldExist, leftTupleStorePositionTracker, rightTupleStorePositionTracker, nextNodesTupleLifecycle, + filtering != null); this.filtering = filtering; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/UnindexedJoinTriNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/UnindexedJoinTriNode.java index 0755e70dc5..dfcbc68b95 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/UnindexedJoinTriNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/UnindexedJoinTriNode.java @@ -3,33 +3,29 @@ import ai.timefold.solver.core.api.function.TriPredicate; import ai.timefold.solver.core.impl.bavet.common.AbstractUnindexedJoinNode; import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.OutputStoreSizeTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.TriTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; public final class UnindexedJoinTriNode extends AbstractUnindexedJoinNode, C, TriTuple> { private final TriPredicate filtering; - private final int outputStoreSize; - - public UnindexedJoinTriNode( - int inputStoreIndexLeftEntry, int inputStoreIndexLeftOutTupleList, - int inputStoreIndexRightEntry, int inputStoreIndexRightOutTupleList, - TupleLifecycle> nextNodesTupleLifecycle, TriPredicate filtering, - int outputStoreSize, - int outputStoreIndexLeftOutEntry, int outputStoreIndexRightOutEntry) { - super(inputStoreIndexLeftEntry, inputStoreIndexLeftOutTupleList, - inputStoreIndexRightEntry, inputStoreIndexRightOutTupleList, - nextNodesTupleLifecycle, filtering != null, - outputStoreIndexLeftOutEntry, outputStoreIndexRightOutEntry); + + public UnindexedJoinTriNode(TupleStorePositionTracker leftTupleStorePositionTracker, + TupleStorePositionTracker rightTupleStorePositionTracker, OutputStoreSizeTracker outputStoreSizeTracker, + TupleLifecycle> nextNodesTupleLifecycle, TriPredicate filtering) { + super(leftTupleStorePositionTracker, rightTupleStorePositionTracker, outputStoreSizeTracker, nextNodesTupleLifecycle, + filtering != null); this.filtering = filtering; - this.outputStoreSize = outputStoreSize; } @Override protected TriTuple createOutTuple(BiTuple leftTuple, UniTuple rightTuple) { - return new TriTuple<>(leftTuple.factA, leftTuple.factB, rightTuple.factA, outputStoreSize); + return new TriTuple<>(leftTuple.factA, leftTuple.factB, rightTuple.factA, + outputStoreSizeTracker.computeOutputStoreSize()); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/IndexedIfExistsUniNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/IndexedIfExistsUniNode.java index 0f7860615f..2b589deff3 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/IndexedIfExistsUniNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/IndexedIfExistsUniNode.java @@ -5,6 +5,7 @@ import ai.timefold.solver.core.impl.bavet.common.AbstractIndexedIfExistsNode; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; public final class IndexedIfExistsUniNode extends AbstractIndexedIfExistsNode, B> { @@ -12,23 +13,10 @@ public final class IndexedIfExistsUniNode extends AbstractIndexedIfExistsN private final BiPredicate filtering; public IndexedIfExistsUniNode(boolean shouldExist, IndexerFactory indexerFactory, - int inputStoreIndexLeftKeys, int inputStoreIndexLeftCounterEntry, - int inputStoreIndexRightKeys, int inputStoreIndexRightEntry, - TupleLifecycle> nextNodesTupleLifecycle) { - this(shouldExist, indexerFactory, - inputStoreIndexLeftKeys, inputStoreIndexLeftCounterEntry, -1, - inputStoreIndexRightKeys, inputStoreIndexRightEntry, -1, - nextNodesTupleLifecycle, null); - } - - public IndexedIfExistsUniNode(boolean shouldExist, IndexerFactory indexerFactory, - int inputStoreIndexLeftKeys, int inputStoreIndexLeftCounterEntry, int inputStoreIndexLeftTrackerList, - int inputStoreIndexRightKeys, int inputStoreIndexRightEntry, int inputStoreIndexRightTrackerList, + TupleStorePositionTracker leftTupleStorePositionTracker, TupleStorePositionTracker rightTupleStorePositionTracker, TupleLifecycle> nextNodesTupleLifecycle, BiPredicate filtering) { - super(shouldExist, indexerFactory.buildUniLeftKeysExtractor(), indexerFactory, - inputStoreIndexLeftKeys, inputStoreIndexLeftCounterEntry, inputStoreIndexLeftTrackerList, - inputStoreIndexRightKeys, inputStoreIndexRightEntry, inputStoreIndexRightTrackerList, - nextNodesTupleLifecycle, filtering != null); + super(shouldExist, indexerFactory.buildUniLeftKeysExtractor(), indexerFactory, leftTupleStorePositionTracker, + rightTupleStorePositionTracker, nextNodesTupleLifecycle, filtering != null); this.filtering = filtering; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/UnindexedIfExistsUniNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/UnindexedIfExistsUniNode.java index 3d8709b282..04721aba36 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/UnindexedIfExistsUniNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/UnindexedIfExistsUniNode.java @@ -4,6 +4,7 @@ import ai.timefold.solver.core.impl.bavet.common.AbstractUnindexedIfExistsNode; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; public final class UnindexedIfExistsUniNode extends AbstractUnindexedIfExistsNode, B> { @@ -11,22 +12,11 @@ public final class UnindexedIfExistsUniNode extends AbstractUnindexedIfExi private final BiPredicate filtering; public UnindexedIfExistsUniNode(boolean shouldExist, - int inputStoreIndexLeftCounterEntry, int inputStoreIndexRightEntry, - TupleLifecycle> nextNodesTupleLifecycle) { - this(shouldExist, - inputStoreIndexLeftCounterEntry, -1, inputStoreIndexRightEntry, -1, - nextNodesTupleLifecycle, null); - } - - public UnindexedIfExistsUniNode(boolean shouldExist, - int inputStoreIndexLeftCounterEntry, int inputStoreIndexLeftTrackerList, int inputStoreIndexRightEntry, - int inputStoreIndexRightTrackerList, + TupleStorePositionTracker leftTupleStorePositionTracker, TupleStorePositionTracker rightTupleStorePositionTracker, TupleLifecycle> nextNodesTupleLifecycle, BiPredicate filtering) { - super(shouldExist, - inputStoreIndexLeftCounterEntry, inputStoreIndexLeftTrackerList, inputStoreIndexRightEntry, - inputStoreIndexRightTrackerList, - nextNodesTupleLifecycle, filtering != null); + super(shouldExist, leftTupleStorePositionTracker, rightTupleStorePositionTracker, nextNodesTupleLifecycle, + filtering != null); this.filtering = filtering; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/bi/JoinBiEnumeratingStream.java b/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/bi/JoinBiEnumeratingStream.java index e70b1afdc2..4a527ef433 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/bi/JoinBiEnumeratingStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/bi/JoinBiEnumeratingStream.java @@ -7,7 +7,9 @@ import ai.timefold.solver.core.impl.bavet.bi.UnindexedJoinBiNode; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.OutputStoreSizeTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.neighborhood.maybeapi.stream.enumerating.function.BiEnumeratingFilter; import ai.timefold.solver.core.impl.neighborhood.stream.enumerating.EnumeratingStreamFactory; import ai.timefold.solver.core.impl.neighborhood.stream.enumerating.common.AbstractEnumeratingStream; @@ -45,26 +47,18 @@ public void collectActiveEnumeratingStreams(Set buildHelper) { var solutionView = buildHelper.getSessionContext().solutionView(); var filteringDataJoiner = this.filtering == null ? null : this.filtering.toBiPredicate(solutionView); - var outputStoreSize = buildHelper.extractTupleStoreSize(this); TupleLifecycle> downstream = buildHelper.getAggregatedTupleLifecycle(childStreamList); var indexerFactory = new IndexerFactory<>(joiner.toBiJoiner()); + TupleStorePositionTracker leftTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()); + TupleStorePositionTracker rightTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()); + OutputStoreSizeTracker outputStoreSizeTracker = new OutputStoreSizeTracker(buildHelper.extractTupleStoreSize(this)); var node = indexerFactory.hasJoiners() - ? new IndexedJoinBiNode<>(indexerFactory, - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - downstream, filteringDataJoiner, - outputStoreSize + 2, outputStoreSize, outputStoreSize + 1) - : new UnindexedJoinBiNode<>( - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - downstream, filteringDataJoiner, - outputStoreSize + 2, outputStoreSize, outputStoreSize + 1); + ? new IndexedJoinBiNode<>(indexerFactory, leftTupleStorePositionTracker, rightTupleStorePositionTracker, + outputStoreSizeTracker, downstream, filteringDataJoiner) + : new UnindexedJoinBiNode<>(leftTupleStorePositionTracker, rightTupleStorePositionTracker, + outputStoreSizeTracker, downstream, filteringDataJoiner); buildHelper.addNode(node, this, leftParent, rightParent); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/uni/IfExistsUniEnumeratingStream.java b/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/uni/IfExistsUniEnumeratingStream.java index 0664f4523c..b0ab4f1b07 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/uni/IfExistsUniEnumeratingStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/enumerating/uni/IfExistsUniEnumeratingStream.java @@ -6,6 +6,7 @@ import ai.timefold.solver.core.impl.bavet.common.AbstractIfExistsNode; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; import ai.timefold.solver.core.impl.bavet.uni.IndexedIfExistsUniNode; import ai.timefold.solver.core.impl.bavet.uni.UnindexedIfExistsUniNode; @@ -63,35 +64,18 @@ private AbstractIfExistsNode, B> getNode(IndexerFactory indexerFa DataNodeBuildHelper buildHelper, TupleLifecycle> downstream) { var sessionContext = buildHelper.getSessionContext(); var isFiltering = filtering != null; + TupleStorePositionTracker leftTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()); + TupleStorePositionTracker rightTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()); if (indexerFactory.hasJoiners()) { - if (isFiltering) { - return new IndexedIfExistsUniNode<>(shouldExist, indexerFactory, - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), - downstream, filtering.toBiPredicate(sessionContext.solutionView())); - } else { - return new IndexedIfExistsUniNode<>(shouldExist, indexerFactory, - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), - downstream); - } - } else if (isFiltering) { - return new UnindexedIfExistsUniNode<>(shouldExist, - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), - downstream, filtering.toBiPredicate(sessionContext.solutionView())); + return new IndexedIfExistsUniNode<>(shouldExist, indexerFactory, leftTupleStorePositionTracker, + rightTupleStorePositionTracker, downstream, + isFiltering ? filtering.toBiPredicate(sessionContext.solutionView()) : null); } else { - return new UnindexedIfExistsUniNode<>(shouldExist, - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), downstream); + return new UnindexedIfExistsUniNode<>(shouldExist, leftTupleStorePositionTracker, rightTupleStorePositionTracker, + downstream, + isFiltering ? filtering.toBiPredicate(sessionContext.solutionView()) : null); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetIfExistsBiConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetIfExistsBiConstraintStream.java index 1291db1aa7..7e0e3bcbd2 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetIfExistsBiConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetIfExistsBiConstraintStream.java @@ -11,6 +11,7 @@ import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.tri.joiner.DefaultTriJoiner; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintFactory; import ai.timefold.solver.core.impl.score.stream.bavet.common.BavetIfExistsConstraintStream; @@ -66,30 +67,15 @@ public BavetAbstractConstraintStream getTupleSource() { public > void buildNode(ConstraintNodeBuildHelper buildHelper) { TupleLifecycle> downstream = buildHelper.getAggregatedTupleLifecycle(childStreamList); IndexerFactory indexerFactory = new IndexerFactory<>(joiner); + TupleStorePositionTracker leftTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(parentAB.getTupleSource()); + TupleStorePositionTracker rightTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(parentBridgeC.getTupleSource()); var node = indexerFactory.hasJoiners() - ? (filtering == null ? new IndexedIfExistsBiNode<>(shouldExist, indexerFactory, - buildHelper.reserveTupleStoreIndex(parentAB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentAB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeC.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeC.getTupleSource()), - downstream) - : new IndexedIfExistsBiNode<>(shouldExist, indexerFactory, - buildHelper.reserveTupleStoreIndex(parentAB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentAB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentAB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeC.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeC.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeC.getTupleSource()), - downstream, filtering)) - : (filtering == null ? new UnindexedIfExistsBiNode<>(shouldExist, - buildHelper.reserveTupleStoreIndex(parentAB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeC.getTupleSource()), downstream) - : new UnindexedIfExistsBiNode<>(shouldExist, - buildHelper.reserveTupleStoreIndex(parentAB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentAB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeC.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeC.getTupleSource()), - downstream, filtering)); + ? new IndexedIfExistsBiNode<>(shouldExist, indexerFactory, leftTupleStorePositionTracker, + rightTupleStorePositionTracker, downstream, filtering) + : new UnindexedIfExistsBiNode<>(shouldExist, leftTupleStorePositionTracker, rightTupleStorePositionTracker, + downstream, filtering); buildHelper.addNode(node, this, this, parentBridgeC); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetJoinBiConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetJoinBiConstraintStream.java index 8df0b6a407..c04dfd736e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetJoinBiConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/bi/BavetJoinBiConstraintStream.java @@ -11,7 +11,9 @@ import ai.timefold.solver.core.impl.bavet.common.BavetAbstractConstraintStream; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.OutputStoreSizeTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintFactory; import ai.timefold.solver.core.impl.score.stream.bavet.common.BavetJoinConstraintStream; import ai.timefold.solver.core.impl.score.stream.bavet.common.ConstraintNodeBuildHelper; @@ -54,26 +56,18 @@ public void collectActiveConstraintStreams(Set> void buildNode(ConstraintNodeBuildHelper buildHelper) { - int outputStoreSize = buildHelper.extractTupleStoreSize(this); TupleLifecycle> downstream = buildHelper.getAggregatedTupleLifecycle(childStreamList); IndexerFactory indexerFactory = new IndexerFactory<>(joiner); + TupleStorePositionTracker leftTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()); + TupleStorePositionTracker rightTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()); + OutputStoreSizeTracker outputStoreSizeTracker = new OutputStoreSizeTracker(buildHelper.extractTupleStoreSize(this)); var node = indexerFactory.hasJoiners() - ? new IndexedJoinBiNode<>(indexerFactory, - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - downstream, filtering, outputStoreSize + 2, - outputStoreSize, outputStoreSize + 1) - : new UnindexedJoinBiNode<>( - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - downstream, filtering, outputStoreSize + 2, - outputStoreSize, outputStoreSize + 1); + ? new IndexedJoinBiNode<>(indexerFactory, leftTupleStorePositionTracker, rightTupleStorePositionTracker, + outputStoreSizeTracker, downstream, filtering) + : new UnindexedJoinBiNode<>(leftTupleStorePositionTracker, rightTupleStorePositionTracker, + outputStoreSizeTracker, downstream, filtering); buildHelper.addNode(node, this, leftParent, rightParent); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetIfExistsQuadConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetIfExistsQuadConstraintStream.java index 0225a7f1d4..e06ce7500d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetIfExistsQuadConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetIfExistsQuadConstraintStream.java @@ -9,6 +9,7 @@ import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.tuple.QuadTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.penta.joiner.DefaultPentaJoiner; import ai.timefold.solver.core.impl.bavet.quad.IndexedIfExistsQuadNode; import ai.timefold.solver.core.impl.bavet.quad.UnindexedIfExistsQuadNode; @@ -66,30 +67,15 @@ public BavetAbstractConstraintStream getTupleSource() { public > void buildNode(ConstraintNodeBuildHelper buildHelper) { TupleLifecycle> downstream = buildHelper.getAggregatedTupleLifecycle(childStreamList); IndexerFactory indexerFactory = new IndexerFactory<>(joiner); + TupleStorePositionTracker leftTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(parentABCD.getTupleSource()); + TupleStorePositionTracker rightTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(parentBridgeE.getTupleSource()); var node = indexerFactory.hasJoiners() - ? (filtering == null ? new IndexedIfExistsQuadNode<>(shouldExist, indexerFactory, - buildHelper.reserveTupleStoreIndex(parentABCD.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentABCD.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeE.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeE.getTupleSource()), - downstream) - : new IndexedIfExistsQuadNode<>(shouldExist, indexerFactory, - buildHelper.reserveTupleStoreIndex(parentABCD.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentABCD.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentABCD.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeE.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeE.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeE.getTupleSource()), - downstream, filtering)) - : (filtering == null ? new UnindexedIfExistsQuadNode<>(shouldExist, - buildHelper.reserveTupleStoreIndex(parentABCD.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeE.getTupleSource()), downstream) - : new UnindexedIfExistsQuadNode<>(shouldExist, - buildHelper.reserveTupleStoreIndex(parentABCD.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentABCD.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeE.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeE.getTupleSource()), - downstream, filtering)); + ? new IndexedIfExistsQuadNode<>(shouldExist, indexerFactory, leftTupleStorePositionTracker, + rightTupleStorePositionTracker, downstream, filtering) + : new UnindexedIfExistsQuadNode<>(shouldExist, leftTupleStorePositionTracker, rightTupleStorePositionTracker, + downstream, filtering); buildHelper.addNode(node, this, this, parentBridgeE); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetJoinQuadConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetJoinQuadConstraintStream.java index 69e39b4e54..cf0f9d873c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetJoinQuadConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/quad/BavetJoinQuadConstraintStream.java @@ -7,8 +7,10 @@ import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.impl.bavet.common.BavetAbstractConstraintStream; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; +import ai.timefold.solver.core.impl.bavet.common.tuple.OutputStoreSizeTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.QuadTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.quad.IndexedJoinQuadNode; import ai.timefold.solver.core.impl.bavet.quad.UnindexedJoinQuadNode; import ai.timefold.solver.core.impl.bavet.quad.joiner.DefaultQuadJoiner; @@ -57,26 +59,18 @@ public void collectActiveConstraintStreams(Set> void buildNode(ConstraintNodeBuildHelper buildHelper) { - int outputStoreSize = buildHelper.extractTupleStoreSize(this); TupleLifecycle> downstream = buildHelper.getAggregatedTupleLifecycle(childStreamList); IndexerFactory indexerFactory = new IndexerFactory<>(joiner); + TupleStorePositionTracker leftTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()); + TupleStorePositionTracker rightTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()); + OutputStoreSizeTracker outputStoreSizeTracker = new OutputStoreSizeTracker(buildHelper.extractTupleStoreSize(this)); var node = indexerFactory.hasJoiners() - ? new IndexedJoinQuadNode<>(indexerFactory, - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - downstream, filtering, outputStoreSize + 2, - outputStoreSize, outputStoreSize + 1) - : new UnindexedJoinQuadNode<>( - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - downstream, filtering, outputStoreSize + 2, - outputStoreSize, outputStoreSize + 1); + ? new IndexedJoinQuadNode<>(indexerFactory, leftTupleStorePositionTracker, rightTupleStorePositionTracker, + outputStoreSizeTracker, downstream, filtering) + : new UnindexedJoinQuadNode<>(leftTupleStorePositionTracker, rightTupleStorePositionTracker, + outputStoreSizeTracker, downstream, filtering); buildHelper.addNode(node, this, leftParent, rightParent); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetIfExistsTriConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetIfExistsTriConstraintStream.java index 83ff4355f4..4fb8128b4f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetIfExistsTriConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetIfExistsTriConstraintStream.java @@ -9,6 +9,7 @@ import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.tuple.TriTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.quad.joiner.DefaultQuadJoiner; import ai.timefold.solver.core.impl.bavet.tri.IndexedIfExistsTriNode; import ai.timefold.solver.core.impl.bavet.tri.UnindexedIfExistsTriNode; @@ -66,30 +67,15 @@ public BavetAbstractConstraintStream getTupleSource() { public > void buildNode(ConstraintNodeBuildHelper buildHelper) { TupleLifecycle> downstream = buildHelper.getAggregatedTupleLifecycle(childStreamList); IndexerFactory indexerFactory = new IndexerFactory<>(joiner); + TupleStorePositionTracker leftTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(parentABC.getTupleSource()); + TupleStorePositionTracker rightTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(parentBridgeD.getTupleSource()); var node = indexerFactory.hasJoiners() - ? (filtering == null ? new IndexedIfExistsTriNode<>(shouldExist, indexerFactory, - buildHelper.reserveTupleStoreIndex(parentABC.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentABC.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeD.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeD.getTupleSource()), - downstream) - : new IndexedIfExistsTriNode<>(shouldExist, indexerFactory, - buildHelper.reserveTupleStoreIndex(parentABC.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentABC.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentABC.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeD.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeD.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeD.getTupleSource()), - downstream, filtering)) - : (filtering == null ? new UnindexedIfExistsTriNode<>(shouldExist, - buildHelper.reserveTupleStoreIndex(parentABC.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeD.getTupleSource()), downstream) - : new UnindexedIfExistsTriNode<>(shouldExist, - buildHelper.reserveTupleStoreIndex(parentABC.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentABC.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeD.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeD.getTupleSource()), - downstream, filtering)); + ? new IndexedIfExistsTriNode<>(shouldExist, indexerFactory, leftTupleStorePositionTracker, + rightTupleStorePositionTracker, downstream, filtering) + : new UnindexedIfExistsTriNode<>(shouldExist, leftTupleStorePositionTracker, rightTupleStorePositionTracker, + downstream, filtering); buildHelper.addNode(node, this, this, parentBridgeD); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetJoinTriConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetJoinTriConstraintStream.java index 8303c34a00..090467c6f8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetJoinTriConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/tri/BavetJoinTriConstraintStream.java @@ -7,8 +7,10 @@ import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.impl.bavet.common.BavetAbstractConstraintStream; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; +import ai.timefold.solver.core.impl.bavet.common.tuple.OutputStoreSizeTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.TriTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.tri.IndexedJoinTriNode; import ai.timefold.solver.core.impl.bavet.tri.UnindexedJoinTriNode; import ai.timefold.solver.core.impl.bavet.tri.joiner.DefaultTriJoiner; @@ -57,26 +59,18 @@ public void collectActiveConstraintStreams(Set> void buildNode(ConstraintNodeBuildHelper buildHelper) { - int outputStoreSize = buildHelper.extractTupleStoreSize(this); TupleLifecycle> downstream = buildHelper.getAggregatedTupleLifecycle(childStreamList); IndexerFactory indexerFactory = new IndexerFactory<>(joiner); + TupleStorePositionTracker leftTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()); + TupleStorePositionTracker rightTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()); + OutputStoreSizeTracker outputStoreSizeTracker = new OutputStoreSizeTracker(buildHelper.extractTupleStoreSize(this)); var node = indexerFactory.hasJoiners() - ? new IndexedJoinTriNode<>(indexerFactory, - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - downstream, filtering, outputStoreSize + 2, - outputStoreSize, outputStoreSize + 1) - : new UnindexedJoinTriNode<>( - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(leftParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - buildHelper.reserveTupleStoreIndex(rightParent.getTupleSource()), - downstream, filtering, outputStoreSize + 2, - outputStoreSize, outputStoreSize + 1); + ? new IndexedJoinTriNode<>(indexerFactory, leftTupleStorePositionTracker, rightTupleStorePositionTracker, + outputStoreSizeTracker, downstream, filtering) + : new UnindexedJoinTriNode<>(leftTupleStorePositionTracker, rightTupleStorePositionTracker, + outputStoreSizeTracker, downstream, filtering); buildHelper.addNode(node, this, leftParent, rightParent); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetIfExistsUniConstraintStream.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetIfExistsUniConstraintStream.java index f74a234858..91fb548b36 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetIfExistsUniConstraintStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/bavet/uni/BavetIfExistsUniConstraintStream.java @@ -9,6 +9,7 @@ import ai.timefold.solver.core.impl.bavet.common.BavetAbstractConstraintStream; import ai.timefold.solver.core.impl.bavet.common.index.IndexerFactory; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleStorePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; import ai.timefold.solver.core.impl.bavet.uni.IndexedIfExistsUniNode; import ai.timefold.solver.core.impl.bavet.uni.UnindexedIfExistsUniNode; @@ -65,30 +66,15 @@ public BavetAbstractConstraintStream getTupleSource() { public > void buildNode(ConstraintNodeBuildHelper buildHelper) { TupleLifecycle> downstream = buildHelper.getAggregatedTupleLifecycle(childStreamList); IndexerFactory indexerFactory = new IndexerFactory<>(joiner); + TupleStorePositionTracker leftTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()); + TupleStorePositionTracker rightTupleStorePositionTracker = + () -> buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()); var node = indexerFactory.hasJoiners() - ? (filtering == null ? new IndexedIfExistsUniNode<>(shouldExist, indexerFactory, - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), - downstream) - : new IndexedIfExistsUniNode<>(shouldExist, indexerFactory, - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), - downstream, filtering)) - : (filtering == null ? new UnindexedIfExistsUniNode<>(shouldExist, - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), downstream) - : new UnindexedIfExistsUniNode<>(shouldExist, - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentA.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), - buildHelper.reserveTupleStoreIndex(parentBridgeB.getTupleSource()), - downstream, filtering)); + ? new IndexedIfExistsUniNode<>(shouldExist, indexerFactory, leftTupleStorePositionTracker, + rightTupleStorePositionTracker, downstream, filtering) + : new UnindexedIfExistsUniNode<>(shouldExist, leftTupleStorePositionTracker, rightTupleStorePositionTracker, + downstream, filtering); buildHelper.addNode(node, this, this, parentBridgeB); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsAndComparisonIndexerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsAndComparisonIndexerTest.java index 63330f8193..789e6eb605 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsAndComparisonIndexerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsAndComparisonIndexerTest.java @@ -5,6 +5,7 @@ import ai.timefold.solver.core.api.score.stream.Joiners; import ai.timefold.solver.core.impl.bavet.bi.joiner.DefaultBiJoiner; +import ai.timefold.solver.core.impl.bavet.common.TuplePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; import org.junit.jupiter.api.Test; @@ -14,16 +15,17 @@ class EqualsAndComparisonIndexerTest extends AbstractIndexerTest { private final DefaultBiJoiner joiner = (DefaultBiJoiner) Joiners.equal(Person::gender) .and(Joiners.lessThanOrEqual(Person::age)); + private final ElementPositionTracker> positionFunction = new TuplePositionTracker<>(0); @Test void iEmpty() { - var indexer = new IndexerFactory<>(joiner).buildIndexer(true); + var indexer = new IndexerFactory<>(joiner).buildIndexer(true, positionFunction); assertThat(getTuples(indexer, "F", 40)).isEmpty(); } @Test void put() { - var indexer = new IndexerFactory<>(joiner).buildIndexer(true); + var indexer = new IndexerFactory<>(joiner).buildIndexer(true, positionFunction); var annTuple = newTuple("Ann-F-40"); assertThat(indexer.size(IndexKeys.ofMany("F", 40))).isEqualTo(0); indexer.put(IndexKeys.ofMany("F", 40), annTuple); @@ -32,18 +34,18 @@ void put() { @Test void removeTwice() { - var indexer = new IndexerFactory<>(joiner).buildIndexer(true); + var indexer = new IndexerFactory<>(joiner).buildIndexer(true, positionFunction); var annTuple = newTuple("Ann-F-40"); - var annEntry = indexer.put(IndexKeys.ofMany("F", 40), annTuple); + indexer.put(IndexKeys.ofMany("F", 40), annTuple); - indexer.remove(IndexKeys.ofMany("F", 40), annEntry); - assertThatThrownBy(() -> indexer.remove(IndexKeys.ofMany("F", 40), annEntry)) + indexer.remove(IndexKeys.ofMany("F", 40), annTuple); + assertThatThrownBy(() -> indexer.remove(IndexKeys.ofMany("F", 40), annTuple)) .isInstanceOf(IllegalStateException.class); } @Test void visit() { - var indexer = new IndexerFactory<>(joiner).buildIndexer(true); + var indexer = new IndexerFactory<>(joiner).buildIndexer(true, positionFunction); var annTuple = newTuple("Ann-F-40"); indexer.put(IndexKeys.ofMany("F", 40), annTuple); @@ -61,7 +63,7 @@ void visit() { } private static UniTuple newTuple(String factA) { - return new UniTuple<>(factA, 0); + return new UniTuple<>(factA, 1); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsIndexerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsIndexerTest.java index 54f9bc0dd0..ef40f86d11 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsIndexerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/EqualsIndexerTest.java @@ -5,6 +5,7 @@ import ai.timefold.solver.core.api.score.stream.Joiners; import ai.timefold.solver.core.impl.bavet.bi.joiner.DefaultBiJoiner; +import ai.timefold.solver.core.impl.bavet.common.TuplePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; import ai.timefold.solver.core.impl.util.Pair; @@ -15,16 +16,17 @@ class EqualsIndexerTest extends AbstractIndexerTest { private final DefaultBiJoiner joiner = (DefaultBiJoiner) Joiners.equal(Person::gender) .and(Joiners.equal(Person::age)); + private final ElementPositionTracker> positionFunction = new TuplePositionTracker<>(0); @Test void isEmpty() { - var indexer = new IndexerFactory<>(joiner).buildIndexer(true); + var indexer = new IndexerFactory<>(joiner).buildIndexer(true, positionFunction); assertThat(getTuples(indexer, "F", 40)).isEmpty(); } @Test void put() { - var indexer = new IndexerFactory<>(joiner).buildIndexer(true); + var indexer = new IndexerFactory<>(joiner).buildIndexer(true, positionFunction); var annTuple = newTuple("Ann-F-40"); assertThat(indexer.size(IndexKeys.ofMany("F", 40))).isEqualTo(0); indexer.put(IndexKeys.ofMany("F", 40), annTuple); @@ -33,18 +35,18 @@ void put() { @Test void removeTwice() { - var indexer = new IndexerFactory<>(joiner).buildIndexer(true); + var indexer = new IndexerFactory<>(joiner).buildIndexer(true, positionFunction); var annTuple = newTuple("Ann-F-40"); - var annEntry = indexer.put(IndexKeys.ofMany("F", 40), annTuple); + indexer.put(IndexKeys.ofMany("F", 40), annTuple); - indexer.remove(IndexKeys.ofMany("F", 40), annEntry); - assertThatThrownBy(() -> indexer.remove(IndexKeys.ofMany("F", 40), annEntry)) + indexer.remove(IndexKeys.ofMany("F", 40), annTuple); + assertThatThrownBy(() -> indexer.remove(IndexKeys.ofMany("F", 40), annTuple)) .isInstanceOf(IllegalStateException.class); } @Test void visit() { - var indexer = new IndexerFactory<>(joiner).buildIndexer(true); + var indexer = new IndexerFactory<>(joiner).buildIndexer(true, positionFunction); var annTuple = newTuple("Ann-F-40"); indexer.put(IndexKeys.of(new Pair<>("F", 40)), annTuple); @@ -61,7 +63,7 @@ void visit() { } private static UniTuple newTuple(String factA) { - return new UniTuple<>(factA, 0); + return new UniTuple<>(factA, 1); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSetTest.java b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSetTest.java new file mode 100644 index 0000000000..4ad69b5568 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/IndexedSetTest.java @@ -0,0 +1,400 @@ +package ai.timefold.solver.core.impl.bavet.common.index; + +import org.jspecify.annotations.NullMarked; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@NullMarked +class IndexedSetTest { + + @Test + void addSingleElement() { + var tracker = new TestElementPositionTracker(); + var set = new IndexedSet<>(tracker); + + set.add("A"); + + assertThat(set.size()).isEqualTo(1); + assertThat(set.isEmpty()).isFalse(); + assertThat(tracker.hasPosition("A")).isTrue(); + } + + @Test + void addMultipleElements() { + var tracker = new TestElementPositionTracker(); + var set = new IndexedSet<>(tracker); + + set.add("A"); + set.add("B"); + set.add("C"); + + assertThat(set.size()).isEqualTo(3); + assertThat(set.isEmpty()).isFalse(); + assertThat(tracker.hasPosition("A")).isTrue(); + assertThat(tracker.hasPosition("B")).isTrue(); + assertThat(tracker.hasPosition("C")).isTrue(); + } + + @Test + void removeLastElement() { + var tracker = new TestElementPositionTracker(); + var set = new IndexedSet<>(tracker); + + set.add("A"); + set.add("B"); + set.remove("B"); + + assertThat(set.size()).isEqualTo(1); + assertThat(tracker.hasPosition("A")).isTrue(); + assertThat(tracker.hasPosition("B")).isFalse(); + } + + @Test + void removeMiddleElement() { + var tracker = new TestElementPositionTracker(); + var set = new IndexedSet<>(tracker); + + set.add("A"); + set.add("B"); + set.add("C"); + set.remove("B"); + + assertThat(set.size()).isEqualTo(2); + assertThat(tracker.hasPosition("A")).isTrue(); + assertThat(tracker.hasPosition("B")).isFalse(); + assertThat(tracker.hasPosition("C")).isTrue(); + } + + @Test + void removeNonExistentElement() { + var tracker = new TestElementPositionTracker(); + var set = new IndexedSet<>(tracker); + + set.add("A"); + + assertThatThrownBy(() -> set.remove("B")) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("was not found in the IndexedSet"); + } + + @Test + void removeAllElements() { + var tracker = new TestElementPositionTracker(); + var set = new IndexedSet<>(tracker); + + set.add("A"); + set.add("B"); + set.remove("A"); + set.remove("B"); + + assertThat(set.size()).isEqualTo(0); + assertThat(set.isEmpty()).isTrue(); + } + + @Test + void removeAllElementsCreatingGaps() { + var tracker = new TestElementPositionTracker(); + var set = new IndexedSet<>(tracker); + + set.add("A"); + set.add("B"); + set.add("C"); + set.remove("B"); + set.remove("A"); + set.remove("C"); + + assertThat(set.size()).isEqualTo(0); + assertThat(set.isEmpty()).isTrue(); + } + + @Test + void forEachEmptySet() { + var tracker = new TestElementPositionTracker(); + var set = new IndexedSet<>(tracker); + var result = new ArrayList(); + + set.forEach(result::add); + + assertThat(result).isEmpty(); + } + + @Test + void forEachWithoutGaps() { + var tracker = new TestElementPositionTracker(); + var set = new IndexedSet<>(tracker); + var result = new ArrayList(); + + set.add("A"); + set.add("B"); + set.add("C"); + set.forEach(result::add); + + assertThat(result).containsExactlyInAnyOrder("A", "B", "C"); + } + + @Test + void forEachWithGapsNoCompaction() { + var tracker = new TestElementPositionTracker(); + var set = new IndexedSet<>(tracker); + var result = new ArrayList(); + + // Add fewer elements than MINIMUM_ELEMENT_COUNT_FOR_COMPACTION + for (int i = 0; i < IndexedSet.MINIMUM_ELEMENT_COUNT_FOR_COMPACTION - 1; i++) { + set.add("Element" + i); + } + set.remove("Element5"); + set.forEach(result::add); + + assertThat(result).hasSize(IndexedSet.MINIMUM_ELEMENT_COUNT_FOR_COMPACTION - 2); + } + + @Test + void forEachWithGapsTriggersCompaction() { + var tracker = new TestElementPositionTracker(); + var set = new IndexedSet<>(tracker); + var result = new ArrayList(); + + // Add enough elements to trigger compaction + int elementCount = IndexedSet.MINIMUM_ELEMENT_COUNT_FOR_COMPACTION; + for (int i = 0; i < elementCount; i++) { + set.add("Element" + i); + } + + // Remove enough elements to exceed GAP_RATIO_FOR_COMPACTION + int gapsNeeded = (int) Math.ceil(elementCount * IndexedSet.GAP_RATIO_FOR_COMPACTION) + 1; + for (int i = 0; i < gapsNeeded; i++) { + set.remove("Element" + i); + } + + set.forEach(result::add); + + assertThat(result).hasSize(elementCount - gapsNeeded); + } + + @Test + void findFirstEmptySet() { + var tracker = new TestElementPositionTracker(); + var set = new IndexedSet<>(tracker); + + var result = set.findFirst(s -> s.equals("A")); + + assertThat(result).isNull(); + } + + @Test + void findFirstWithoutGaps() { + var tracker = new TestElementPositionTracker(); + var set = new IndexedSet<>(tracker); + + set.add("A"); + set.add("B"); + set.add("C"); + + var result = set.findFirst(s -> s.equals("B")); + + assertThat(result).isEqualTo("B"); + } + + @Test + void findFirstNotFound() { + var tracker = new TestElementPositionTracker(); + var set = new IndexedSet<>(tracker); + + set.add("A"); + set.add("B"); + + var result = set.findFirst(s -> s.equals("C")); + + assertThat(result).isNull(); + } + + @Test + void findFirstWithGapsNoCompaction() { + var tracker = new TestElementPositionTracker(); + var set = new IndexedSet<>(tracker); + + for (int i = 0; i < IndexedSet.MINIMUM_ELEMENT_COUNT_FOR_COMPACTION - 1; i++) { + set.add("Element" + i); + } + set.remove("Element5"); + + var result = set.findFirst(s -> s.equals("Element10")); + + assertThat(result).isEqualTo("Element10"); + } + + @Test + void findFirstWithGapsTriggersCompaction() { + var tracker = new TestElementPositionTracker(); + var set = new IndexedSet<>(tracker); + + int elementCount = IndexedSet.MINIMUM_ELEMENT_COUNT_FOR_COMPACTION; + for (int i = 0; i < elementCount; i++) { + set.add("Element" + i); + } + + int gapsNeeded = (int) Math.ceil(elementCount * IndexedSet.GAP_RATIO_FOR_COMPACTION) + 1; + for (int i = 0; i < gapsNeeded; i++) { + set.remove("Element" + i); + } + + var result = set.findFirst(s -> s.startsWith("Element")); + + assertThat(result).isNotNull(); + } + + @Test + void clearEmptySet() { + var tracker = new TestElementPositionTracker(); + var set = new IndexedSet<>(tracker); + var result = new ArrayList(); + + set.clear(result::add); + + assertThat(result).isEmpty(); + assertThat(set.isEmpty()).isTrue(); + } + + @Test + void clearWithElements() { + var tracker = new TestElementPositionTracker(); + var set = new IndexedSet<>(tracker); + var result = new ArrayList(); + + set.add("A"); + set.add("B"); + set.add("C"); + set.clear(result::add); + + assertThat(result).containsExactlyInAnyOrder("A", "B", "C"); + assertThat(set.isEmpty()).isTrue(); + assertThat(tracker.hasPosition("A")).isFalse(); + assertThat(tracker.hasPosition("B")).isFalse(); + assertThat(tracker.hasPosition("C")).isFalse(); + } + + @Test + void clearWithGaps() { + var tracker = new TestElementPositionTracker(); + var set = new IndexedSet<>(tracker); + var result = new ArrayList(); + + set.add("A"); + set.add("B"); + set.add("C"); + set.remove("B"); + set.clear(result::add); + + assertThat(result).containsExactlyInAnyOrder("A", "C"); + assertThat(set.isEmpty()).isTrue(); + } + + @Test + void asListEmptySet() { + var tracker = new TestElementPositionTracker(); + var set = new IndexedSet<>(tracker); + + var list = set.asList(); + + assertThat(list).isEmpty(); + } + + @Test + void asListWithoutGaps() { + var tracker = new TestElementPositionTracker(); + var set = new IndexedSet<>(tracker); + + set.add("A"); + set.add("B"); + set.add("C"); + + var list = set.asList(); + + assertThat(list).containsExactly("A", "B", "C"); + } + + @Test + void asListWithGapsForcesCompaction() { + var tracker = new TestElementPositionTracker(); + var set = new IndexedSet<>(tracker); + + set.add("A"); + set.add("B"); + set.add("C"); + set.remove("B"); + + var list = set.asList(); + + assertThat(list) + .hasSize(2) + .doesNotContainNull() + .containsExactlyInAnyOrder("A", "C"); + } + + @Test + void asListAfterClearReturnsEmptyList() { + var tracker = new TestElementPositionTracker(); + var set = new IndexedSet<>(tracker); + + set.add("A"); + set.add("B"); + set.remove("A"); + set.remove("B"); + + var list = set.asList(); + + assertThat(list).isEmpty(); + } + + @Test + void compactionFillsGapsCorrectly() { + var tracker = new TestElementPositionTracker(); + var set = new IndexedSet<>(tracker); + + // Create scenario where compaction will move elements + int elementCount = IndexedSet.MINIMUM_ELEMENT_COUNT_FOR_COMPACTION; + for (int i = 0; i < elementCount; i++) { + set.add("Element" + i); + } + + // Remove elements from the beginning to create gaps + int gapsNeeded = (int) Math.ceil(elementCount * IndexedSet.GAP_RATIO_FOR_COMPACTION) + 1; + for (int i = 0; i < gapsNeeded; i++) { + set.remove("Element" + i); + } + + // Force compaction via asList + var list = set.asList(); + + assertThat(list) + .hasSize(elementCount - gapsNeeded) + .doesNotContainNull(); + } + + private static final class TestElementPositionTracker implements ElementPositionTracker { + + private final Map positionMap = new HashMap<>(); + + @Override + public void setPosition(T element, int position) { + positionMap.put(element, position); + } + + @Override + public int clearPosition(T element) { + var position = positionMap.remove(element); + return position != null ? position : -1; + } + + public boolean hasPosition(T element) { + return positionMap.containsKey(element); + } + } + +} diff --git a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/NoneIndexerTest.java b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/NoneIndexerTest.java index 2f1ea45a70..8d0aaf3863 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/NoneIndexerTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/bavet/common/index/NoneIndexerTest.java @@ -4,15 +4,18 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.SoftAssertions.assertSoftly; +import ai.timefold.solver.core.impl.bavet.common.TuplePositionTracker; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; import org.junit.jupiter.api.Test; class NoneIndexerTest extends AbstractIndexerTest { + private final ElementPositionTracker> elementPositionTracker = new TuplePositionTracker<>(0); + @Test void isEmpty() { - var indexer = new NoneIndexer<>(); + var indexer = new NoneIndexer<>(elementPositionTracker); assertSoftly(softly -> { softly.assertThat(getTuples(indexer)).isEmpty(); softly.assertThat(indexer.isEmpty()).isTrue(); @@ -21,7 +24,7 @@ void isEmpty() { @Test void put() { - var indexer = new NoneIndexer<>(); + var indexer = new NoneIndexer<>(elementPositionTracker); var annTuple = newTuple("Ann-F-40"); assertThat(indexer.size(IndexKeys.none())).isEqualTo(0); indexer.put(IndexKeys.none(), annTuple); @@ -34,26 +37,26 @@ void put() { @Test void removeTwice() { - var indexer = new NoneIndexer<>(); + var indexer = new NoneIndexer<>(elementPositionTracker); var annTuple = newTuple("Ann-F-40"); - var annEntry = indexer.put(IndexKeys.none(), annTuple); + indexer.put(IndexKeys.none(), annTuple); assertSoftly(softly -> { softly.assertThat(indexer.isEmpty()).isFalse(); softly.assertThat(getTuples(indexer)).containsExactly(annTuple); }); - indexer.remove(IndexKeys.none(), annEntry); + indexer.remove(IndexKeys.none(), annTuple); assertSoftly(softly -> { softly.assertThat(indexer.isEmpty()).isTrue(); softly.assertThat(getTuples(indexer)).isEmpty(); }); - assertThatThrownBy(() -> indexer.remove(IndexKeys.none(), annEntry)) + assertThatThrownBy(() -> indexer.remove(IndexKeys.none(), annTuple)) .isInstanceOf(IllegalStateException.class); } @Test void visit() { - var indexer = new NoneIndexer<>(); + var indexer = new NoneIndexer<>(elementPositionTracker); var annTuple = newTuple("Ann-F-40"); indexer.put(IndexKeys.none(), annTuple); @@ -64,7 +67,7 @@ void visit() { } private static UniTuple newTuple(String factA) { - return new UniTuple<>(factA, 0); + return new UniTuple<>(factA, 1); } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/ChangeMoveDefinitionTest.java b/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/ChangeMoveDefinitionTest.java index cc47071583..264abd99a5 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/ChangeMoveDefinitionTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/ChangeMoveDefinitionTest.java @@ -59,35 +59,35 @@ void fromSolution() { .toList(); assertThat(moveList).hasSize(4); - var firstMove = moveList.get(0); + var move1 = moveList.get(0); assertSoftly(softly -> { - softly.assertThat(firstMove.extractPlanningEntities()) + softly.assertThat(move1.extractPlanningEntities()) .containsExactly(firstEntity); - softly.assertThat(firstMove.extractPlanningValues()) + softly.assertThat(move1.extractPlanningValues()) .containsExactly(firstValue); }); - var secondMove = moveList.get(1); + var move2 = moveList.get(1); assertSoftly(softly -> { - softly.assertThat(secondMove.extractPlanningEntities()) + softly.assertThat(move2.extractPlanningEntities()) .containsExactly(firstEntity); - softly.assertThat(secondMove.extractPlanningValues()) + softly.assertThat(move2.extractPlanningValues()) .containsExactly(secondValue); }); - var thirdMove = moveList.get(2); + var move3 = moveList.get(2); assertSoftly(softly -> { - softly.assertThat(thirdMove.extractPlanningEntities()) + softly.assertThat(move3.extractPlanningEntities()) .containsExactly(secondEntity); - softly.assertThat(thirdMove.extractPlanningValues()) + softly.assertThat(move3.extractPlanningValues()) .containsExactly(firstValue); }); - var fourthMove = moveList.get(3); + var move4 = moveList.get(3); assertSoftly(softly -> { - softly.assertThat(fourthMove.extractPlanningEntities()) + softly.assertThat(move4.extractPlanningEntities()) .containsExactly(secondEntity); - softly.assertThat(fourthMove.extractPlanningValues()) + softly.assertThat(move4.extractPlanningValues()) .containsExactly(secondValue); }); } @@ -119,35 +119,35 @@ void fromSolutionIncompleteValueRange() { .toList(); assertThat(moveList).hasSize(4); - var firstMove = moveList.get(0); + var move1 = moveList.get(0); assertSoftly(softly -> { - softly.assertThat(firstMove.extractPlanningEntities()) + softly.assertThat(move1.extractPlanningEntities()) .containsExactly(firstEntity); - softly.assertThat(firstMove.extractPlanningValues()) + softly.assertThat(move1.extractPlanningValues()) .containsExactly(firstValue); }); - var secondMove = moveList.get(1); + var move2 = moveList.get(1); assertSoftly(softly -> { - softly.assertThat(secondMove.extractPlanningEntities()) + softly.assertThat(move2.extractPlanningEntities()) .containsExactly(firstEntity); - softly.assertThat(secondMove.extractPlanningValues()) + softly.assertThat(move2.extractPlanningValues()) .containsExactly(secondValue); }); - var thirdMove = moveList.get(2); + var move3 = moveList.get(2); assertSoftly(softly -> { - softly.assertThat(thirdMove.extractPlanningEntities()) + softly.assertThat(move3.extractPlanningEntities()) .containsExactly(secondEntity); - softly.assertThat(thirdMove.extractPlanningValues()) + softly.assertThat(move3.extractPlanningValues()) .containsExactly(firstValue); }); - var fourthMove = moveList.get(3); + var move4 = moveList.get(3); assertSoftly(softly -> { - softly.assertThat(fourthMove.extractPlanningEntities()) + softly.assertThat(move4.extractPlanningEntities()) .containsExactly(secondEntity); - softly.assertThat(fourthMove.extractPlanningValues()) + softly.assertThat(move4.extractPlanningValues()) .containsExactly(secondValue); }); } @@ -213,29 +213,29 @@ void fromEntityAllowsUnassigned() { .toList(); assertThat(moveList).hasSize(3); - var firstMove = moveList.get(0); + var move1 = moveList.get(0); assertSoftly(softly -> { - softly.assertThat(firstMove.extractPlanningEntities()) + softly.assertThat(move1.extractPlanningEntities()) .containsExactly(firstEntity); - softly.assertThat(firstMove.extractPlanningValues()) + softly.assertThat(move1.extractPlanningValues()) .hasSize(1) .containsNull(); }); - var secondMove = moveList.get(1); + var move2 = moveList.get(1); assertSoftly(softly -> { - softly.assertThat(secondMove.extractPlanningEntities()) + softly.assertThat(move2.extractPlanningEntities()) .containsExactly(secondEntity); - softly.assertThat(secondMove.extractPlanningValues()) + softly.assertThat(move2.extractPlanningValues()) .hasSize(1) .containsNull(); }); - var thirdMove = moveList.get(2); + var move3 = moveList.get(2); assertSoftly(softly -> { - softly.assertThat(thirdMove.extractPlanningEntities()) + softly.assertThat(move3.extractPlanningEntities()) .containsExactly(secondEntity); - softly.assertThat(thirdMove.extractPlanningValues()) + softly.assertThat(move3.extractPlanningValues()) .containsExactly(firstValue); }); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/ListSwapMoveDefinitionTest.java b/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/ListSwapMoveDefinitionTest.java index 6d4b1fb617..224c4991a8 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/ListSwapMoveDefinitionTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/ListSwapMoveDefinitionTest.java @@ -83,6 +83,7 @@ void fromSolution() { softly.assertThat(move3.extractPlanningValues()) .containsExactly(assignedValue3, assignedValue2); }); + } @Test