From b145db1299a1432f5e3fbd10fb1a187bec31b7dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Tue, 29 Jul 2025 10:10:45 +0200 Subject: [PATCH 01/18] Fix contract --- .../maybeapi/stream/MoveStreamFactory.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/MoveStreamFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/MoveStreamFactory.java index 9e3a8753c8..59a004a888 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/MoveStreamFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/MoveStreamFactory.java @@ -5,10 +5,8 @@ import ai.timefold.solver.core.api.domain.entity.PlanningPinToIndex; import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty; import ai.timefold.solver.core.api.domain.variable.PlanningListVariable; -import ai.timefold.solver.core.api.score.stream.ConstraintStream; import ai.timefold.solver.core.impl.move.streams.maybeapi.UniDataFilter; import ai.timefold.solver.core.preview.api.domain.metamodel.GenuineVariableMetaModel; -import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningVariableMetaModel; import org.jspecify.annotations.NullMarked; @@ -16,11 +14,12 @@ public interface MoveStreamFactory { /** - * Start a {@link ConstraintStream} of all instances of the sourceClass - * that are known as {@link ProblemFactCollectionProperty problem facts} or {@link PlanningEntity planning entities}. + * Start a {@link DataStream} of all instances of the sourceClass + * that are known as {@link ProblemFactCollectionProperty problem facts} + * or {@link PlanningEntity planning entities}. *

* If the sourceClass is a {@link PlanningEntity}, then it is automatically - * {@link UniDataStream#filter(UniDataFilter)} filtered} to only contain entities + * {@link UniDataStream#filter(UniDataFilter) filtered} to only contain entities * which are not pinned. *

* If the sourceClass is a shadow entity (an entity without any genuine planning variables), @@ -31,7 +30,7 @@ public interface MoveStreamFactory { *

* This stream returns genuine entities regardless of whether they have any null genuine planning variables. * This stream returns shadow entities regardless of whether they are assigned to any genuine entity. - * They can easily be {@link UniDataStream#filter(UniDataFilter)} filtered out}. + * They can easily be {@link UniDataStream#filter(UniDataFilter) filtered out}. * * @return A stream containing a tuple for each of the entities as described above. * @see PlanningPin An annotation to mark the entire entity as pinned. @@ -42,8 +41,9 @@ public interface MoveStreamFactory { UniDataStream enumerate(Class sourceClass, boolean includeNull); /** - * Start a {@link ConstraintStream} of all instances of the sourceClass - * that are known as {@link ProblemFactCollectionProperty problem facts} or {@link PlanningEntity planning entities}. + * Start a {@link DataStream} of all instances of the sourceClass + * that are known as {@link ProblemFactCollectionProperty problem facts} + * or {@link PlanningEntity planning entities}. * If the sourceClass is a genuine or shadow entity, * it returns instances regardless of their pinning status. * Otherwise as defined by {@link #enumerate(Class, boolean)}. @@ -60,7 +60,7 @@ public interface MoveStreamFactory { * @return data stream with all possible values of a given variable */ default BiDataStream enumerateEntityValuePairs( - PlanningVariableMetaModel variableMetaModel) { + GenuineVariableMetaModel variableMetaModel) { return enumerateEntityValuePairs(variableMetaModel, enumerate(variableMetaModel.entity().type(), false)); } From a3e3fe6970287d731155ede682dbdf8e5d838439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Wed, 6 Aug 2025 10:02:02 +0200 Subject: [PATCH 02/18] Initial move impl (does not work yet) --- .../core/impl/move/director/MoveDirector.java | 14 +-- .../streams/DefaultMoveStreamFactory.java | 2 +- .../streams/DefaultMoveStreamSession.java | 16 ++-- .../move/streams/FromBiUniMoveProducer.java | 24 +++--- .../move/streams/FromUniBiMoveProducer.java | 26 +++--- .../streams/dataset/AbstractBiDataStream.java | 17 ++++ .../dataset/AbstractUniDataStream.java | 16 ++++ .../move/streams/maybeapi/BiDataMapper.java | 29 +++++++ .../move/streams/maybeapi/UniDataMapper.java | 28 ++++++ .../generic/move/ListUnassignMove.java | 11 ++- .../generic/provider/ChangeMoveProvider.java | 10 +-- .../provider/ListChangeMoveProvider.java | 86 +++++++++++++++++++ .../streams/maybeapi/stream/BiDataStream.java | 34 ++++++++ .../maybeapi/stream/BiMoveConstructor.java | 6 +- .../maybeapi/stream/MoveConstructor.java | 3 + .../maybeapi/stream/UniDataStream.java | 83 ++++++++++++++++-- .../core/preview/api/move/SolutionView.java | 11 +++ 17 files changed, 358 insertions(+), 58 deletions(-) create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/BiDataMapper.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/UniDataMapper.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/director/MoveDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/move/director/MoveDirector.java index 04ccf2efe9..b18224db49 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/director/MoveDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/director/MoveDirector.java @@ -1,9 +1,5 @@ package ai.timefold.solver.core.impl.move.director; -import java.util.List; -import java.util.Objects; -import java.util.function.BiFunction; - import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultPlanningListVariableMetaModel; @@ -22,10 +18,13 @@ import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningVariableMetaModel; import ai.timefold.solver.core.preview.api.move.Move; import ai.timefold.solver.core.preview.api.move.Rebaser; - import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; +import java.util.List; +import java.util.Objects; +import java.util.function.BiFunction; + @NullMarked public sealed class MoveDirector> implements InnerMutableSolutionView, Rebaser @@ -185,6 +184,11 @@ public final Value_ getValue(PlanningVariableMetaModel int countValues(PlanningListVariableMetaModel variableMetaModel, Entity_ entity) { + return extractVariableDescriptor(variableMetaModel).getValue(entity).size(); + } + @SuppressWarnings("unchecked") @Override public final Value_ getValueAtIndex( diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamFactory.java index 4e1414ec6d..575f4f91a0 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamFactory.java @@ -35,7 +35,7 @@ public DefaultMoveStreamFactory(SolutionDescriptor solutionDescriptor public DefaultMoveStreamSession createSession(SessionContext context) { var session = datasetSessionFactory.buildSession(context); - return new DefaultMoveStreamSession<>(session, context.workingSolution()); + return new DefaultMoveStreamSession<>(session, context.solutionView()); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamSession.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamSession.java index 132c976fdc..019eb23879 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamSession.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamSession.java @@ -1,26 +1,26 @@ package ai.timefold.solver.core.impl.move.streams; -import java.util.Objects; - import ai.timefold.solver.core.impl.move.streams.dataset.BiDataset; import ai.timefold.solver.core.impl.move.streams.dataset.BiDatasetInstance; import ai.timefold.solver.core.impl.move.streams.dataset.DatasetSession; import ai.timefold.solver.core.impl.move.streams.dataset.UniDataset; import ai.timefold.solver.core.impl.move.streams.dataset.UniDatasetInstance; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.MoveStreamSession; - +import ai.timefold.solver.core.preview.api.move.SolutionView; import org.jspecify.annotations.NullMarked; +import java.util.Objects; + @NullMarked public final class DefaultMoveStreamSession implements MoveStreamSession, AutoCloseable { private final DatasetSession datasetSession; - private final Solution_ workingSolution; + private final SolutionView solutionView; - public DefaultMoveStreamSession(DatasetSession datasetSession, Solution_ workingSolution) { + public DefaultMoveStreamSession(DatasetSession datasetSession, SolutionView solutionView) { this.datasetSession = Objects.requireNonNull(datasetSession); - this.workingSolution = Objects.requireNonNull(workingSolution); + this.solutionView = Objects.requireNonNull(solutionView); } public UniDatasetInstance getDatasetInstance(UniDataset dataset) { @@ -47,8 +47,8 @@ public void settle() { datasetSession.settle(); } - public Solution_ getWorkingSolution() { - return workingSolution; + public SolutionView getSolutionView() { + return solutionView; } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/FromBiUniMoveProducer.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/FromBiUniMoveProducer.java index 42362d5b92..595ad19dab 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/FromBiUniMoveProducer.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/FromBiUniMoveProducer.java @@ -1,22 +1,22 @@ package ai.timefold.solver.core.impl.move.streams; -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.Objects; -import java.util.Random; -import java.util.Set; -import java.util.function.Supplier; - import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; import ai.timefold.solver.core.impl.move.streams.dataset.AbstractDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.BiDataset; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.BiMoveConstructor; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.MoveStreamSession; import ai.timefold.solver.core.preview.api.move.Move; - +import ai.timefold.solver.core.preview.api.move.SolutionView; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Random; +import java.util.Set; +import java.util.function.Supplier; + @NullMarked public final class FromBiUniMoveProducer implements InnerMoveProducer { @@ -42,7 +42,7 @@ public void collectActiveDataStreams(Set> activeDa private final class InnerMoveIterator implements Iterator> { private final IteratorSupplier iteratorSupplier; - private final Solution_ solution; + private final SolutionView solutionView; // Fields required for iteration. private @Nullable Move nextMove; @@ -51,13 +51,13 @@ private final class InnerMoveIterator implements Iterator> { public InnerMoveIterator(DefaultMoveStreamSession moveStreamSession) { var aInstance = moveStreamSession.getDatasetInstance(aDataset); this.iteratorSupplier = aInstance::iterator; - this.solution = moveStreamSession.getWorkingSolution(); + this.solutionView = moveStreamSession.getSolutionView(); } public InnerMoveIterator(DefaultMoveStreamSession moveStreamSession, Random random) { var aInstance = moveStreamSession.getDatasetInstance(aDataset); this.iteratorSupplier = () -> aInstance.iterator(random); - this.solution = moveStreamSession.getWorkingSolution(); + this.solutionView = moveStreamSession.getSolutionView(); } @Override @@ -78,7 +78,7 @@ public boolean hasNext() { } var tuple = iterator.next(); - nextMove = moveConstructor.apply(solution, tuple.factA, tuple.factB); + nextMove = moveConstructor.apply(solutionView, tuple.factA, tuple.factB); return true; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/FromUniBiMoveProducer.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/FromUniBiMoveProducer.java index 1544877523..d009112158 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/FromUniBiMoveProducer.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/FromUniBiMoveProducer.java @@ -1,23 +1,23 @@ package ai.timefold.solver.core.impl.move.streams; -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.Objects; -import java.util.Random; -import java.util.Set; -import java.util.function.BiPredicate; -import java.util.function.Supplier; - import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; import ai.timefold.solver.core.impl.move.streams.dataset.AbstractDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.UniDataset; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.BiMoveConstructor; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.MoveStreamSession; import ai.timefold.solver.core.preview.api.move.Move; - +import ai.timefold.solver.core.preview.api.move.SolutionView; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Random; +import java.util.Set; +import java.util.function.BiPredicate; +import java.util.function.Supplier; + @NullMarked public final class FromUniBiMoveProducer implements InnerMoveProducer { @@ -49,7 +49,7 @@ private final class BiMoveIterator implements Iterator> { private final IteratorSupplier aIteratorSupplier; private final IteratorSupplier bIteratorSupplier; - private final Solution_ solution; + private final SolutionView solutionView; // Fields required for iteration. private @Nullable Move nextMove; @@ -62,7 +62,7 @@ public BiMoveIterator(DefaultMoveStreamSession moveStreamSession) { this.aIteratorSupplier = aInstance::iterator; var bInstance = moveStreamSession.getDatasetInstance(bDataset); this.bIteratorSupplier = bInstance::iterator; - this.solution = moveStreamSession.getWorkingSolution(); + this.solutionView = moveStreamSession.getSolutionView(); } public BiMoveIterator(DefaultMoveStreamSession moveStreamSession, Random random) { @@ -70,7 +70,7 @@ public BiMoveIterator(DefaultMoveStreamSession moveStreamSession, Ran this.aIteratorSupplier = () -> aInstance.iterator(random); var bInstance = moveStreamSession.getDatasetInstance(bDataset); this.bIteratorSupplier = () -> bInstance.iterator(random); - this.solution = moveStreamSession.getWorkingSolution(); + this.solutionView = moveStreamSession.getSolutionView(); } @Override @@ -101,7 +101,7 @@ public boolean hasNext() { // Check if this pair passes the filter... if (filter.test(currentA, currentB)) { // ... and create the next move. - nextMove = moveConstructor.apply(solution, currentA, currentB); + nextMove = moveConstructor.apply(solutionView, currentA, currentB); return true; } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractBiDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractBiDataStream.java index fcb49755c8..cf1c5b55fa 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractBiDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractBiDataStream.java @@ -1,8 +1,10 @@ package ai.timefold.solver.core.impl.move.streams.dataset; import ai.timefold.solver.core.impl.move.streams.maybeapi.BiDataFilter; +import ai.timefold.solver.core.impl.move.streams.maybeapi.BiDataMapper; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.BiDataStream; +import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.UniDataStream; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -24,6 +26,21 @@ public final BiDataStream filter(BiDataFilter return shareAndAddChild(new FilterBiDataStream<>(dataStreamFactory, this, filter)); } + @Override + public UniDataStream map(BiDataMapper mapping) { + return null; + } + + @Override + public BiDataStream map(BiDataMapper mappingA, BiDataMapper mappingB) { + return null; + } + + @Override + public BiDataStream distinct() { + return null; + } + public BiDataset createDataset() { var stream = shareAndAddChild(new TerminalBiDataStream<>(dataStreamFactory, this)); return stream.getDataset(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractUniDataStream.java index e8188c6ddf..28d287f6f9 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractUniDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractUniDataStream.java @@ -4,6 +4,7 @@ import ai.timefold.solver.core.impl.move.streams.dataset.joiner.BiDataJoinerComber; import ai.timefold.solver.core.impl.move.streams.maybeapi.BiDataJoiner; import ai.timefold.solver.core.impl.move.streams.maybeapi.UniDataFilter; +import ai.timefold.solver.core.impl.move.streams.maybeapi.UniDataMapper; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.BiDataStream; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.UniDataStream; @@ -86,6 +87,21 @@ private UniDataStream ifExistsOrNot(boolean shouldExist, UniDa joinerComber.mergedJoiner(), joinerComber.mergedFiltering()), childStreamList::add); } + @Override + public UniDataStream map(UniDataMapper mapping) { + return null; + } + + @Override + public BiDataStream map(UniDataMapper mappingA, UniDataMapper mappingB) { + return null; + } + + @Override + public UniDataStream distinct() { + return null; + } + public UniDataset createDataset() { var stream = shareAndAddChild(new TerminalUniDataStream<>(dataStreamFactory, this)); return stream.getDataset(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/BiDataMapper.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/BiDataMapper.java new file mode 100644 index 0000000000..b2b109255b --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/BiDataMapper.java @@ -0,0 +1,29 @@ +package ai.timefold.solver.core.impl.move.streams.maybeapi; + +import ai.timefold.solver.core.api.function.TriFunction; +import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.BiDataStream; +import ai.timefold.solver.core.preview.api.move.SolutionView; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.function.BiFunction; + +/** + * A mapping function that can be applied to {@link BiDataStream} to transform data, + * optionally using {@link SolutionView} to query for solution state. + * + * @param the type of the solution + * @param the type of the first parameter + * @param the type of the second parameter + */ +@NullMarked +public interface BiDataMapper extends TriFunction, A, B, Result_> { + + @Override + Result_ apply(SolutionView solutionSolutionView, @Nullable A a, @Nullable B b); + + default BiFunction toBiFunction(SolutionView solutionView) { + return (a, b) -> apply(solutionView, a, b); + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/UniDataMapper.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/UniDataMapper.java new file mode 100644 index 0000000000..074fb2997f --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/UniDataMapper.java @@ -0,0 +1,28 @@ +package ai.timefold.solver.core.impl.move.streams.maybeapi; + +import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.UniDataStream; +import ai.timefold.solver.core.preview.api.move.SolutionView; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * A mapping function that can be applied to {@link UniDataStream} to transform data, + * optionally using {@link SolutionView} to query for solution state. + * + * @param the type of the solution + * @param the type of the first parameter + */ +@NullMarked +public interface UniDataMapper extends BiFunction, A, Result_> { + + @Override + Result_ apply(SolutionView solutionSolutionView, @Nullable A a); + + default Function toFunction(SolutionView solutionView) { + return a -> apply(solutionView, a); + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListUnassignMove.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListUnassignMove.java index de52cc3194..927111d709 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListUnassignMove.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListUnassignMove.java @@ -1,18 +1,17 @@ package ai.timefold.solver.core.impl.move.streams.maybeapi.generic.move; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Objects; - import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningListVariableMetaModel; import ai.timefold.solver.core.preview.api.domain.metamodel.VariableMetaModel; import ai.timefold.solver.core.preview.api.move.Move; import ai.timefold.solver.core.preview.api.move.MutableSolutionView; import ai.timefold.solver.core.preview.api.move.Rebaser; - import org.jspecify.annotations.NonNull; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + public final class ListUnassignMove extends AbstractMove { private final PlanningListVariableMetaModel variableMetaModel; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ChangeMoveProvider.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ChangeMoveProvider.java index fab7691ba4..09d1e4f729 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ChangeMoveProvider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ChangeMoveProvider.java @@ -1,15 +1,13 @@ package ai.timefold.solver.core.impl.move.streams.maybeapi.generic.provider; -import java.util.Objects; - import ai.timefold.solver.core.impl.move.streams.maybeapi.generic.move.ChangeMove; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.MoveProducer; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.MoveProvider; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.MoveStreamFactory; import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningVariableMetaModel; - import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; + +import java.util.Objects; @NullMarked public class ChangeMoveProvider @@ -25,12 +23,12 @@ public ChangeMoveProvider(PlanningVariableMetaModel public MoveProducer apply(MoveStreamFactory moveStreamFactory) { var dataStream = moveStreamFactory.enumerateEntityValuePairs(variableMetaModel) .filter((solutionView, entity, value) -> { - @Nullable Value_ currentValue = solutionView.getValue(variableMetaModel, Objects.requireNonNull(entity)); return !Objects.equals(currentValue, value); }); return moveStreamFactory.pick(dataStream) - .asMove((solution, entity, value) -> new ChangeMove<>(variableMetaModel, entity, value)); + .asMove((solution, entity, value) -> new ChangeMove<>(variableMetaModel, Objects.requireNonNull(entity), + value)); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java new file mode 100644 index 0000000000..389c7b450f --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java @@ -0,0 +1,86 @@ +package ai.timefold.solver.core.impl.move.streams.maybeapi.generic.provider; + +import ai.timefold.solver.core.impl.move.streams.maybeapi.BiDataFilter; +import ai.timefold.solver.core.impl.move.streams.maybeapi.DataJoiners; +import ai.timefold.solver.core.impl.move.streams.maybeapi.generic.move.ListAssignMove; +import ai.timefold.solver.core.impl.move.streams.maybeapi.generic.move.ListChangeMove; +import ai.timefold.solver.core.impl.move.streams.maybeapi.generic.move.ListUnassignMove; +import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.MoveProducer; +import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.MoveProvider; +import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.MoveStreamFactory; +import ai.timefold.solver.core.preview.api.domain.metamodel.ElementPosition; +import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningListVariableMetaModel; +import ai.timefold.solver.core.preview.api.domain.metamodel.UnassignedElement; +import org.jspecify.annotations.NullMarked; + +import java.util.Objects; + +@NullMarked +public class ListChangeMoveProvider + implements MoveProvider { + + private final PlanningListVariableMetaModel variableMetaModel; + private final BiDataFilter isValueInListFilter; + private final BiDataFilter noChangeDetectionFilter; + + public ListChangeMoveProvider(PlanningListVariableMetaModel variableMetaModel) { + this.variableMetaModel = Objects.requireNonNull(variableMetaModel); + this.isValueInListFilter = (solution, entity, value) -> { + if (entity == null) { + // Necessary for the null entity to survive until the later stage, + // where we will use it as a marker to unassigned the value. + return true; + } + return solution.isValueInRange(variableMetaModel, entity, value); + }; + this.noChangeDetectionFilter = (solutionView, value, targetPosition) -> { + var currentPosition = solutionView.getPositionOf(variableMetaModel, Objects.requireNonNull(value)); + return !currentPosition.equals(targetPosition); + }; + } + + @Override + public MoveProducer apply(MoveStreamFactory moveStreamFactory) { + // For each unassigned value, we need to create a move to assign it to same position of some list variable. + // For each assigned value that is not pinned, we need to create: + // - A move to unassign it. + // - A move to reassign it to another position if assigned. + // To assign or reassign a value, we need to create: + // - A move for every unpinned value in every entity's list variable to assign the value before that position. + // - A move for every entity to assign it to the last position in the list variable. + var unpinnedValuesToChange = moveStreamFactory.enumerate(variableMetaModel.type(), false); + var unpinnedEntities = moveStreamFactory.enumerate(variableMetaModel.entity().type(), true); + var unpinnedValues = moveStreamFactory.enumerate(variableMetaModel.type(), true); + var entityValuePairs = unpinnedEntities.join(unpinnedValues, DataJoiners.filtering(isValueInListFilter)) + .map((solutionView, entity, value) -> { + if (entity == null) { // This will trigger unassignment of the value. + return ElementPosition.unassigned(); + } else if (value == null) { // This will trigger assignment of the value at the end of the list. + return ElementPosition.of(entity, solutionView.countValues(variableMetaModel, entity)); + } else { // This will trigger assignment of the value immediately before this value. + return solutionView.getPositionOf(variableMetaModel, value); + } + }) + .distinct(); + var dataStream = unpinnedValuesToChange.join(entityValuePairs, + DataJoiners.filtering(noChangeDetectionFilter)); + return moveStreamFactory.pick(dataStream) + .asMove((solutionView, value, targetPosition) -> { + var currentPosition = solutionView.getPositionOf(variableMetaModel, Objects.requireNonNull(value)); + if (targetPosition instanceof UnassignedElement) { + var currentElementPosition = currentPosition.ensureAssigned(); + return new ListUnassignMove<>(variableMetaModel, value, currentElementPosition.entity(), + currentElementPosition.index()); + } + var targetElementPosition = Objects.requireNonNull(targetPosition).ensureAssigned(); + if (currentPosition instanceof UnassignedElement) { + return new ListAssignMove<>(variableMetaModel, value, targetElementPosition.entity(), + targetElementPosition.index()); + } + var currentElementPosition = currentPosition.ensureAssigned(); + return new ListChangeMove<>(variableMetaModel, currentElementPosition.entity(), + currentElementPosition.index(), targetElementPosition.entity(), targetElementPosition.index()); + }); + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/BiDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/BiDataStream.java index 4b750ebd88..ace6d7515f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/BiDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/BiDataStream.java @@ -1,6 +1,8 @@ package ai.timefold.solver.core.impl.move.streams.maybeapi.stream; import ai.timefold.solver.core.impl.move.streams.maybeapi.BiDataFilter; +import ai.timefold.solver.core.impl.move.streams.maybeapi.BiDataMapper; +import ai.timefold.solver.core.impl.move.streams.maybeapi.UniDataMapper; import ai.timefold.solver.core.preview.api.move.SolutionView; import org.jspecify.annotations.NullMarked; @@ -14,4 +16,36 @@ public interface BiDataStream extends DataStream { */ BiDataStream filter(BiDataFilter filter); + // ************************************************************************ + // Operations with duplicate tuple possibility + // ************************************************************************ + + /** + * As defined by {@link UniDataStream#map(UniDataMapper)}. + * + *

+ * Use with caution, + * as the increased memory allocation rates coming from tuple creation may negatively affect performance. + * + * @param mapping function to convert the original tuple into the new tuple + * @param the type of the only fact in the resulting {@link UniDataStream}'s tuple + */ + UniDataStream map(BiDataMapper mapping); + + /** + * As defined by {@link #map(BiDataMapper)}, only resulting in {@link BiDataStream}. + * + * @param mappingA function to convert the original tuple into the first fact of a new tuple + * @param mappingB function to convert the original tuple into the second fact of a new tuple + * @param the type of the first fact in the resulting {@link BiDataStream}'s tuple + * @param the type of the first fact in the resulting {@link BiDataStream}'s tuple + */ + BiDataStream map(BiDataMapper mappingA, + BiDataMapper mappingB); + + /** + * As defined by {@link UniDataStream#distinct()}. + */ + BiDataStream distinct(); + } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/BiMoveConstructor.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/BiMoveConstructor.java index a221557e4f..5dc0140ebd 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/BiMoveConstructor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/BiMoveConstructor.java @@ -1,11 +1,15 @@ package ai.timefold.solver.core.impl.move.streams.maybeapi.stream; import ai.timefold.solver.core.preview.api.move.Move; +import ai.timefold.solver.core.preview.api.move.SolutionView; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +@NullMarked @FunctionalInterface public non-sealed interface BiMoveConstructor extends MoveConstructor { - Move apply(Solution_ solution, A a, B b); + Move apply(SolutionView solutionView, @Nullable A a, @Nullable B b); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/MoveConstructor.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/MoveConstructor.java index dfc03e9d1c..bc1f505897 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/MoveConstructor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/MoveConstructor.java @@ -1,5 +1,8 @@ package ai.timefold.solver.core.impl.move.streams.maybeapi.stream; +import org.jspecify.annotations.NullMarked; + +@NullMarked public sealed interface MoveConstructor permits BiMoveConstructor { } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/UniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/UniDataStream.java index 74e6099a3c..6180f5f338 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/UniDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/UniDataStream.java @@ -1,18 +1,18 @@ package ai.timefold.solver.core.impl.move.streams.maybeapi.stream; -import static ai.timefold.solver.core.impl.util.ConstantLambdaUtils.notEqualsForDataStreams; - -import java.util.Arrays; -import java.util.stream.Stream; - import ai.timefold.solver.core.impl.move.streams.maybeapi.BiDataFilter; import ai.timefold.solver.core.impl.move.streams.maybeapi.BiDataJoiner; import ai.timefold.solver.core.impl.move.streams.maybeapi.DataJoiners; import ai.timefold.solver.core.impl.move.streams.maybeapi.UniDataFilter; +import ai.timefold.solver.core.impl.move.streams.maybeapi.UniDataMapper; import ai.timefold.solver.core.preview.api.move.SolutionView; - import org.jspecify.annotations.NullMarked; +import java.util.Arrays; +import java.util.stream.Stream; + +import static ai.timefold.solver.core.impl.util.ConstantLambdaUtils.notEqualsForDataStreams; + @NullMarked public interface UniDataStream extends DataStream { @@ -123,4 +123,75 @@ default UniDataStream ifNotExistsOther(Class otherClass, BiData return ifNotExists(otherClass, allJoiners); } + // ************************************************************************ + // Operations with duplicate tuple possibility + // ************************************************************************ + + /** + * Transforms the stream in such a way that tuples are remapped using the given function. + * This may produce a stream with duplicate tuples. + * See {@link #distinct()} for details. + *

+ * There are several recommendations for implementing the mapping function: + * + *

+ * + *

+ * Simple example: assuming a constraint stream of tuples of {@code Person}s + * {@code [Ann(age = 20), Beth(age = 25), Cathy(age = 30)]}, + * calling {@code map(Person::getAge)} on such stream will produce a stream of {@link Integer}s + * {@code [20, 25, 30]}, + * + *

+ * Example with a non-bijective mapping function: assuming a constraint stream of tuples of {@code Person}s + * {@code [Ann(age = 20), Beth(age = 25), Cathy(age = 30), David(age = 30), Eric(age = 20)]}, + * calling {@code map(Person::getAge)} on such stream will produce a stream of {@link Integer}s + * {@code [20, 25, 30, 30, 20]}. + * + *

+ * Use with caution, + * as the increased memory allocation rates coming from tuple creation may negatively affect performance. + * + * @param mapping function to convert the original tuple into the new tuple + * @param the type of the only fact in the resulting {@link UniDataStream}'s tuple + */ + UniDataStream map(UniDataMapper mapping); + + /** + * As defined by {@link #map(UniDataMapper)}, only resulting in {@link BiDataStream}. + * + * @param mappingA function to convert the original tuple into the first fact of a new tuple + * @param mappingB function to convert the original tuple into the second fact of a new tuple + * @param the type of the first fact in the resulting {@link BiDataStream}'s tuple + * @param the type of the first fact in the resulting {@link BiDataStream}'s tuple + */ + BiDataStream map(UniDataMapper mappingA, UniDataMapper mappingB); + + /** + * Transforms the stream in such a way that all the tuples going through it are distinct. + * (No two tuples will {@link Object#equals(Object) equal}.) + * + *

+ * By default, tuples going through a constraint stream are distinct. + * However, operations such as {@link #map(UniDataMapper)} may create a stream which breaks that promise. + * By calling this method on such a stream, + * duplicate copies of the same tuple will be omitted at a performance cost. + */ + UniDataStream distinct(); + } diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/SolutionView.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/SolutionView.java index c1debc0793..17d1da2a67 100644 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/move/SolutionView.java +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/move/SolutionView.java @@ -43,6 +43,17 @@ public interface SolutionView { @Nullable Value_ getValue(PlanningVariableMetaModel variableMetaModel, Entity_ entity); + /** + * Reads the value of a @{@link PlanningListVariable list planning variable} and returns its length. + * + * @param variableMetaModel Describes the variable whose value is to be read. + * @param entity The entity whose variable is to be read. + * @return The number of values in the list variable. + * @throws NullPointerException if the value of the list variable is null + * @throws IndexOutOfBoundsException if the index is out of bounds + */ + int countValues(PlanningListVariableMetaModel variableMetaModel, Entity_ entity); + /** * Reads the value of a @{@link PlanningListVariable list planning variable} of a given entity at a specific index. * From 498b9f87da1df8fadaf95924abff2167a908ac18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Wed, 6 Aug 2025 10:32:59 +0200 Subject: [PATCH 03/18] Implement mapping --- .../streams/dataset/AbstractBiDataStream.java | 15 +++- .../streams/dataset/AbstractDataStream.java | 13 ++-- .../dataset/AbstractUniDataStream.java | 25 +++--- .../streams/dataset/BiMapBiDataStream.java | 70 +++++++++++++++++ .../streams/dataset/BiMapUniDataStream.java | 66 ++++++++++++++++ .../streams/dataset/UniMapBiDataStream.java | 69 +++++++++++++++++ .../streams/dataset/UniMapUniDataStream.java | 77 +++++++++++++++++++ .../common/bridge/AftBridgeBiDataStream.java | 41 ++++++++++ .../common/bridge/AftBridgeUniDataStream.java | 7 +- 9 files changed, 360 insertions(+), 23 deletions(-) create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/BiMapBiDataStream.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/BiMapUniDataStream.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/UniMapBiDataStream.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/UniMapUniDataStream.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/AftBridgeBiDataStream.java diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractBiDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractBiDataStream.java index cf1c5b55fa..83890a1f70 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractBiDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractBiDataStream.java @@ -1,10 +1,12 @@ package ai.timefold.solver.core.impl.move.streams.dataset; +import ai.timefold.solver.core.impl.move.streams.dataset.common.bridge.AftBridgeBiDataStream; +import ai.timefold.solver.core.impl.move.streams.dataset.common.bridge.AftBridgeUniDataStream; import ai.timefold.solver.core.impl.move.streams.maybeapi.BiDataFilter; import ai.timefold.solver.core.impl.move.streams.maybeapi.BiDataMapper; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.BiDataStream; - import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.UniDataStream; + import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -28,17 +30,22 @@ public final BiDataStream filter(BiDataFilter @Override public UniDataStream map(BiDataMapper mapping) { - return null; + var stream = shareAndAddChild(new BiMapUniDataStream<>(dataStreamFactory, this, mapping)); + return dataStreamFactory.share(new AftBridgeUniDataStream<>(dataStreamFactory, stream), stream::setAftBridge); } @Override public BiDataStream map(BiDataMapper mappingA, BiDataMapper mappingB) { - return null; + var stream = shareAndAddChild(new BiMapBiDataStream<>(dataStreamFactory, this, mappingA, mappingB)); + return dataStreamFactory.share(new AftBridgeBiDataStream<>(dataStreamFactory, stream), stream::setAftBridge); } @Override public BiDataStream distinct() { - return null; + if (guaranteesDistinct()) { + return this; // Already distinct, no need to create a new stream. + } + throw new UnsupportedOperationException(); } public BiDataset createDataset() { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractDataStream.java index 8e902cc071..7cb64e149a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractDataStream.java @@ -1,16 +1,15 @@ package ai.timefold.solver.core.impl.move.streams.dataset; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; - import ai.timefold.solver.core.impl.bavet.common.BavetStream; import ai.timefold.solver.core.impl.bavet.common.TupleSource; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; - import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + @NullMarked public abstract class AbstractDataStream implements BavetStream { @@ -29,6 +28,10 @@ public final > Stream_ shareAndAdd return dataStreamFactory.share(stream, childStreamList::add); } + boolean guaranteesDistinct() { + return true; // Default implementation, can be overridden by subclasses. + } + // ************************************************************************ // Node creation // ************************************************************************ diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractUniDataStream.java index 28d287f6f9..8fa4b9c46c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractUniDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractUniDataStream.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.move.streams.dataset; +import ai.timefold.solver.core.impl.move.streams.dataset.common.bridge.AftBridgeBiDataStream; +import ai.timefold.solver.core.impl.move.streams.dataset.common.bridge.AftBridgeUniDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.common.bridge.ForeBridgeUniDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.joiner.BiDataJoinerComber; import ai.timefold.solver.core.impl.move.streams.maybeapi.BiDataJoiner; @@ -8,7 +10,6 @@ import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.BiDataStream; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.UniDataStream; -import org.jspecify.annotations.NonNull; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -31,8 +32,7 @@ public final UniDataStream filter(UniDataFilter filt } @Override - public @NonNull BiDataStream join(@NonNull UniDataStream otherStream, - @NonNull BiDataJoiner... joiners) { + public BiDataStream join(UniDataStream otherStream, BiDataJoiner... joiners) { var other = (AbstractUniDataStream) otherStream; var leftBridge = new ForeBridgeUniDataStream(dataStreamFactory, this); var rightBridge = new ForeBridgeUniDataStream(dataStreamFactory, other); @@ -47,8 +47,7 @@ public final UniDataStream filter(UniDataFilter filt } @Override - public @NonNull BiDataStream join(@NonNull Class otherClass, - @NonNull BiDataJoiner... joiners) { + public BiDataStream join(Class otherClass, BiDataJoiner... joiners) { return join(dataStreamFactory.forEachNonDiscriminating(otherClass, false), joiners); } @@ -89,17 +88,23 @@ private UniDataStream ifExistsOrNot(boolean shouldExist, UniDa @Override public UniDataStream map(UniDataMapper mapping) { - return null; + var stream = shareAndAddChild(new UniMapUniDataStream<>(dataStreamFactory, this, mapping)); + return dataStreamFactory.share(new AftBridgeUniDataStream<>(dataStreamFactory, stream), stream::setAftBridge); } @Override - public BiDataStream map(UniDataMapper mappingA, UniDataMapper mappingB) { - return null; + public BiDataStream map(UniDataMapper mappingA, + UniDataMapper mappingB) { + var stream = shareAndAddChild(new UniMapBiDataStream<>(dataStreamFactory, this, mappingA, mappingB)); + return dataStreamFactory.share(new AftBridgeBiDataStream<>(dataStreamFactory, stream), stream::setAftBridge); } @Override - public UniDataStream distinct() { - return null; + public AbstractUniDataStream distinct() { + if (guaranteesDistinct()) { + return this; // Already distinct, no need to create a new stream. + } + throw new UnsupportedOperationException(); } public UniDataset createDataset() { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/BiMapBiDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/BiMapBiDataStream.java new file mode 100644 index 0000000000..e99e74c977 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/BiMapBiDataStream.java @@ -0,0 +1,70 @@ +package ai.timefold.solver.core.impl.move.streams.dataset; + +import ai.timefold.solver.core.impl.bavet.bi.MapBiToBiNode; +import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; +import ai.timefold.solver.core.impl.move.streams.dataset.common.bridge.AftBridgeBiDataStream; +import ai.timefold.solver.core.impl.move.streams.maybeapi.BiDataMapper; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.Objects; + +@NullMarked +final class BiMapBiDataStream + extends AbstractBiDataStream { + + private final BiDataMapper mappingFunctionA; + private final BiDataMapper mappingFunctionB; + private @Nullable AftBridgeBiDataStream aftStream; + + public BiMapBiDataStream(DataStreamFactory constraintFactory, AbstractBiDataStream parent, + BiDataMapper mappingFunctionA, BiDataMapper mappingFunctionB) { + super(constraintFactory, parent); + this.mappingFunctionA = mappingFunctionA; + this.mappingFunctionB = mappingFunctionB; + } + + public void setAftBridge(AftBridgeBiDataStream aftStream) { + this.aftStream = aftStream; + } + + @Override + public boolean guaranteesDistinct() { + return false; + } + + @Override + public void buildNode(DataNodeBuildHelper buildHelper) { + assertEmptyChildStreamList(); + int inputStoreIndex = buildHelper.reserveTupleStoreIndex(parent.getTupleSource()); + int outputStoreSize = buildHelper.extractTupleStoreSize(aftStream); + var node = new MapBiToBiNode<>(inputStoreIndex, + mappingFunctionA.toBiFunction(buildHelper.getSessionContext().solutionView()), + mappingFunctionB.toBiFunction(buildHelper.getSessionContext().solutionView()), + buildHelper.getAggregatedTupleLifecycle(aftStream.getChildStreamList()), outputStoreSize); + buildHelper.addNode(node, this); + } + + @Override + public boolean equals(Object object) { + if (this == object) + return true; + if (object == null || getClass() != object.getClass()) + return false; + BiMapBiDataStream that = (BiMapBiDataStream) object; + return Objects.equals(parent, that.parent) && + Objects.equals(mappingFunctionA, that.mappingFunctionA) && + Objects.equals(mappingFunctionB, that.mappingFunctionB); + } + + @Override + public int hashCode() { + return Objects.hash(parent, mappingFunctionA, mappingFunctionB); + } + + @Override + public String toString() { + return "BiMap()"; + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/BiMapUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/BiMapUniDataStream.java new file mode 100644 index 0000000000..a2bfcad567 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/BiMapUniDataStream.java @@ -0,0 +1,66 @@ +package ai.timefold.solver.core.impl.move.streams.dataset; + +import ai.timefold.solver.core.impl.bavet.bi.MapBiToUniNode; +import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; +import ai.timefold.solver.core.impl.move.streams.dataset.common.bridge.AftBridgeUniDataStream; +import ai.timefold.solver.core.impl.move.streams.maybeapi.BiDataMapper; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.Objects; + +@NullMarked +final class BiMapUniDataStream + extends AbstractUniDataStream { + + private final BiDataMapper mappingFunction; + private @Nullable AftBridgeUniDataStream aftStream; + + public BiMapUniDataStream(DataStreamFactory constraintFactory, AbstractBiDataStream parent, + BiDataMapper mappingFunction) { + super(constraintFactory, parent); + this.mappingFunction = mappingFunction; + } + + public void setAftBridge(AftBridgeUniDataStream aftStream) { + this.aftStream = aftStream; + } + + @Override + public boolean guaranteesDistinct() { + return false; + } + + @Override + public void buildNode(DataNodeBuildHelper buildHelper) { + assertEmptyChildStreamList(); + int inputStoreIndex = buildHelper.reserveTupleStoreIndex(parent.getTupleSource()); + int outputStoreSize = buildHelper.extractTupleStoreSize(aftStream); + var node = new MapBiToUniNode<>(inputStoreIndex, + mappingFunction.toBiFunction(buildHelper.getSessionContext().solutionView()), + buildHelper.getAggregatedTupleLifecycle(aftStream.getChildStreamList()), outputStoreSize); + buildHelper.addNode(node, this); + } + + @Override + public boolean equals(Object object) { + if (this == object) + return true; + if (object == null || getClass() != object.getClass()) + return false; + BiMapUniDataStream that = (BiMapUniDataStream) object; + return Objects.equals(parent, that.parent) && + Objects.equals(mappingFunction, that.mappingFunction); + } + + @Override + public int hashCode() { + return Objects.hash(parent, mappingFunction); + } + + @Override + public String toString() { + return "BiMap()"; + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/UniMapBiDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/UniMapBiDataStream.java new file mode 100644 index 0000000000..7f69354b5b --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/UniMapBiDataStream.java @@ -0,0 +1,69 @@ +package ai.timefold.solver.core.impl.move.streams.dataset; + +import ai.timefold.solver.core.impl.bavet.uni.MapUniToBiNode; +import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; +import ai.timefold.solver.core.impl.move.streams.dataset.common.bridge.AftBridgeBiDataStream; +import ai.timefold.solver.core.impl.move.streams.maybeapi.UniDataMapper; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.Objects; + +@NullMarked +final class UniMapBiDataStream + extends AbstractBiDataStream { + + private final UniDataMapper mappingFunctionA; + private final UniDataMapper mappingFunctionB; + private @Nullable AftBridgeBiDataStream aftStream; + + public UniMapBiDataStream(DataStreamFactory constraintFactory, AbstractUniDataStream parent, UniDataMapper mappingFunctionA, UniDataMapper mappingFunctionB) { + super(constraintFactory, parent); + this.mappingFunctionA = mappingFunctionA; + this.mappingFunctionB = mappingFunctionB; + } + + public void setAftBridge(AftBridgeBiDataStream aftStream) { + this.aftStream = aftStream; + } + + @Override + public boolean guaranteesDistinct() { + return false; + } + + @Override + public void buildNode(DataNodeBuildHelper buildHelper) { + assertEmptyChildStreamList(); + int inputStoreIndex = buildHelper.reserveTupleStoreIndex(parent.getTupleSource()); + int outputStoreSize = buildHelper.extractTupleStoreSize(aftStream); + var node = new MapUniToBiNode<>(inputStoreIndex, + mappingFunctionA.toFunction(buildHelper.getSessionContext().solutionView()), + mappingFunctionB.toFunction(buildHelper.getSessionContext().solutionView()), + buildHelper.getAggregatedTupleLifecycle(aftStream.getChildStreamList()), outputStoreSize); + buildHelper.addNode(node, this); + } + + @Override + public boolean equals(Object object) { + if (this == object) + return true; + if (object == null || getClass() != object.getClass()) + return false; + UniMapBiDataStream that = (UniMapBiDataStream) object; + return Objects.equals(parent, that.parent) && + Objects.equals(mappingFunctionA, that.mappingFunctionA) && + Objects.equals(mappingFunctionB, that.mappingFunctionB); + } + + @Override + public int hashCode() { + return Objects.hash(parent, mappingFunctionA, mappingFunctionB); + } + + @Override + public String toString() { + return "UniMap()"; + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/UniMapUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/UniMapUniDataStream.java new file mode 100644 index 0000000000..4f677e6861 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/UniMapUniDataStream.java @@ -0,0 +1,77 @@ +package ai.timefold.solver.core.impl.move.streams.dataset; + +import ai.timefold.solver.core.impl.bavet.uni.MapUniToUniNode; +import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; +import ai.timefold.solver.core.impl.move.streams.dataset.common.bridge.AftBridgeUniDataStream; +import ai.timefold.solver.core.impl.move.streams.maybeapi.UniDataMapper; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.Objects; + +@NullMarked +final class UniMapUniDataStream + extends AbstractUniDataStream { + + private final UniDataMapper mappingFunction; + private @Nullable AftBridgeUniDataStream aftStream; + + public UniMapUniDataStream(DataStreamFactory constraintFactory, AbstractUniDataStream parent, + UniDataMapper mappingFunction) { + super(constraintFactory, parent); + this.mappingFunction = mappingFunction; + } + + public void setAftBridge(AftBridgeUniDataStream aftStream) { + this.aftStream = aftStream; + } + + // ************************************************************************ + // Node creation + // ************************************************************************ + + @Override + public boolean guaranteesDistinct() { + return false; + } + + @Override + public void buildNode(DataNodeBuildHelper buildHelper) { + assertEmptyChildStreamList(); + int inputStoreIndex = buildHelper.reserveTupleStoreIndex(parent.getTupleSource()); + int outputStoreSize = buildHelper.extractTupleStoreSize(aftStream); + var node = new MapUniToUniNode<>(inputStoreIndex, + mappingFunction.toFunction(buildHelper.getSessionContext().solutionView()), + buildHelper.getAggregatedTupleLifecycle(aftStream.getChildStreamList()), outputStoreSize); + buildHelper.addNode(node, this); + } + + // ************************************************************************ + // Equality for node sharing + // ************************************************************************ + + @Override + public boolean equals(Object object) { + if (this == object) + return true; + if (object == null || getClass() != object.getClass()) + return false; + UniMapUniDataStream that = (UniMapUniDataStream) object; + return Objects.equals(parent, that.parent) && Objects.equals(mappingFunction, that.mappingFunction); + } + + @Override + public int hashCode() { + return Objects.hash(parent, mappingFunction); + } + + // ************************************************************************ + // Getters/setters + // ************************************************************************ + + @Override + public String toString() { + return "UniMap()"; + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/AftBridgeBiDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/AftBridgeBiDataStream.java new file mode 100644 index 0000000000..d4d04654a3 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/AftBridgeBiDataStream.java @@ -0,0 +1,41 @@ +package ai.timefold.solver.core.impl.move.streams.dataset.common.bridge; + +import ai.timefold.solver.core.impl.bavet.common.TupleSource; +import ai.timefold.solver.core.impl.move.streams.dataset.AbstractBiDataStream; +import ai.timefold.solver.core.impl.move.streams.dataset.AbstractDataStream; +import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; +import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; +import org.jspecify.annotations.NullMarked; + +import java.util.Objects; + +@NullMarked +public final class AftBridgeBiDataStream + extends AbstractBiDataStream + implements TupleSource { + + public AftBridgeBiDataStream(DataStreamFactory dataStreamFactory, AbstractDataStream parent) { + super(dataStreamFactory, parent); + } + + @Override + public void buildNode(DataNodeBuildHelper buildHelper) { + // Do nothing. The parent stream builds everything. + } + + @Override + public boolean equals(Object o) { + return o instanceof AftBridgeBiDataStream that && Objects.equals(parent, that.parent); + } + + @Override + public int hashCode() { + return Objects.requireNonNull(parent).hashCode(); + } + + @Override + public String toString() { + return "Bridge from " + parent + " with " + childStreamList.size() + " children"; + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/AftBridgeUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/AftBridgeUniDataStream.java index 2ac331bd56..d068a7a55b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/AftBridgeUniDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/AftBridgeUniDataStream.java @@ -1,15 +1,14 @@ package ai.timefold.solver.core.impl.move.streams.dataset.common.bridge; -import java.util.Objects; - import ai.timefold.solver.core.impl.bavet.common.TupleSource; import ai.timefold.solver.core.impl.move.streams.dataset.AbstractDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.AbstractUniDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; - import org.jspecify.annotations.NullMarked; +import java.util.Objects; + @NullMarked public final class AftBridgeUniDataStream extends AbstractUniDataStream @@ -31,7 +30,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return parent.hashCode(); + return Objects.requireNonNull(parent).hashCode(); } @Override From 1cc5346b70a1c2468808c9f3b0dfc21142c645a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Wed, 6 Aug 2025 10:40:51 +0200 Subject: [PATCH 04/18] Move stuff around --- .../impl/move/streams/DefaultBiFromBiMoveStream.java | 2 +- .../move/streams/DefaultBiFromUnisMoveStream.java | 2 +- .../impl/move/streams/DefaultMoveStreamFactory.java | 4 ++-- .../impl/move/streams/DefaultMoveStreamSession.java | 8 ++++---- .../core/impl/move/streams/DefaultUniMoveStream.java | 4 ++-- .../impl/move/streams/FromBiUniMoveProducer.java | 4 ++-- .../impl/move/streams/FromUniBiMoveProducer.java | 4 ++-- .../core/impl/move/streams/InnerMoveProducer.java | 2 +- .../core/impl/move/streams/InnerMoveStream.java | 2 +- .../core/impl/move/streams/InnerUniMoveStream.java | 2 +- .../impl/move/streams/dataset/DataStreamFactory.java | 5 +++++ .../impl/move/streams/dataset/DatasetSession.java | 2 ++ .../move/streams/dataset/DatasetSessionFactory.java | 1 + .../dataset/{ => bi}/AbstractBiDataStream.java | 6 ++++-- .../move/streams/dataset/{ => bi}/BiDataset.java | 4 +++- .../streams/dataset/{ => bi}/BiDatasetInstance.java | 4 +++- .../streams/dataset/{ => bi}/BiMapBiDataStream.java | 5 +++-- .../streams/dataset/{ => bi}/FilterBiDataStream.java | 5 +++-- .../streams/dataset/{ => bi}/JoinBiDataStream.java | 8 +++++--- .../dataset/{ => bi}/TerminalBiDataStream.java | 3 ++- .../UniMapBiDataStream.java} | 12 +++++++----- .../dataset/{ => common}/AbstractDataStream.java | 6 +++--- .../dataset/{ => common}/AbstractDataset.java | 10 +++++----- .../{ => common}/AbstractDatasetInstance.java | 2 +- .../streams/dataset/common/DataNodeBuildHelper.java | 2 -- .../dataset/common/DataStreamBinaryOperation.java | 1 - .../streams/dataset/common/TerminalDataStream.java | 1 - .../dataset/common/bridge/AftBridgeBiDataStream.java | 4 ++-- .../common/bridge/AftBridgeUniDataStream.java | 4 ++-- .../common/bridge/ForeBridgeUniDataStream.java | 4 ++-- .../dataset/{ => uni}/AbstractForEachDataStream.java | 6 ++++-- .../dataset/{ => uni}/AbstractUniDataStream.java | 7 +++++-- .../BiMapUniDataStream.java} | 10 ++++++---- .../dataset/{ => uni}/FilterUniDataStream.java | 10 +++++----- .../{ => uni}/ForEachFromSolutionDataStream.java | 5 +++-- .../{ => uni}/ForEachIncludingPinnedDataStream.java | 5 +++-- .../dataset/{ => uni}/IfExistsUniDataStream.java | 8 +++++--- .../dataset/{ => uni}/TerminalUniDataStream.java | 3 ++- .../move/streams/dataset/{ => uni}/UniDataset.java | 4 +++- .../dataset/{ => uni}/UniDatasetInstance.java | 4 +++- .../dataset/{ => uni}/UniMapUniDataStream.java | 5 +++-- .../move/streams/dataset/UniDatasetStreamTest.java | 1 + 42 files changed, 113 insertions(+), 78 deletions(-) rename core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/{ => bi}/AbstractBiDataStream.java (89%) rename core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/{ => bi}/BiDataset.java (71%) rename core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/{ => bi}/BiDatasetInstance.java (96%) rename core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/{ => bi}/BiMapBiDataStream.java (90%) rename core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/{ => bi}/FilterBiDataStream.java (88%) rename core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/{ => bi}/JoinBiDataStream.java (91%) rename core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/{ => bi}/TerminalBiDataStream.java (90%) rename core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/{BiMapUniDataStream.java => bi/UniMapBiDataStream.java} (81%) rename core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/{ => common}/AbstractDataStream.java (94%) rename core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/{ => common}/AbstractDataset.java (91%) rename core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/{ => common}/AbstractDatasetInstance.java (93%) rename core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/{ => uni}/AbstractForEachDataStream.java (87%) rename core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/{ => uni}/AbstractUniDataStream.java (93%) rename core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/{UniMapBiDataStream.java => uni/BiMapUniDataStream.java} (86%) rename core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/{ => uni}/FilterUniDataStream.java (89%) rename core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/{ => uni}/ForEachFromSolutionDataStream.java (87%) rename core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/{ => uni}/ForEachIncludingPinnedDataStream.java (87%) rename core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/{ => uni}/IfExistsUniDataStream.java (92%) rename core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/{ => uni}/TerminalUniDataStream.java (90%) rename core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/{ => uni}/UniDataset.java (71%) rename core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/{ => uni}/UniDatasetInstance.java (83%) rename core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/{ => uni}/UniMapUniDataStream.java (92%) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultBiFromBiMoveStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultBiFromBiMoveStream.java index e41a4e4c49..b4e65aef88 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultBiFromBiMoveStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultBiFromBiMoveStream.java @@ -2,7 +2,7 @@ import java.util.Objects; -import ai.timefold.solver.core.impl.move.streams.dataset.BiDataset; +import ai.timefold.solver.core.impl.move.streams.dataset.bi.BiDataset; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.BiMoveConstructor; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.BiMoveStream; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.MoveProducer; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultBiFromUnisMoveStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultBiFromUnisMoveStream.java index eb2363649e..7347f6d44b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultBiFromUnisMoveStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultBiFromUnisMoveStream.java @@ -3,7 +3,7 @@ import java.util.Objects; import java.util.function.BiPredicate; -import ai.timefold.solver.core.impl.move.streams.dataset.UniDataset; +import ai.timefold.solver.core.impl.move.streams.dataset.uni.UniDataset; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.BiMoveConstructor; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.BiMoveStream; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.MoveProducer; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamFactory.java index 575f4f91a0..af61b9b6a3 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamFactory.java @@ -4,8 +4,8 @@ import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultPlanningVariableMetaModel; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor; -import ai.timefold.solver.core.impl.move.streams.dataset.AbstractBiDataStream; -import ai.timefold.solver.core.impl.move.streams.dataset.AbstractUniDataStream; +import ai.timefold.solver.core.impl.move.streams.dataset.bi.AbstractBiDataStream; +import ai.timefold.solver.core.impl.move.streams.dataset.uni.AbstractUniDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.DatasetSessionFactory; import ai.timefold.solver.core.impl.move.streams.maybeapi.DataJoiners; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamSession.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamSession.java index 019eb23879..fb8fab362d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamSession.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamSession.java @@ -1,10 +1,10 @@ package ai.timefold.solver.core.impl.move.streams; -import ai.timefold.solver.core.impl.move.streams.dataset.BiDataset; -import ai.timefold.solver.core.impl.move.streams.dataset.BiDatasetInstance; +import ai.timefold.solver.core.impl.move.streams.dataset.bi.BiDataset; +import ai.timefold.solver.core.impl.move.streams.dataset.bi.BiDatasetInstance; import ai.timefold.solver.core.impl.move.streams.dataset.DatasetSession; -import ai.timefold.solver.core.impl.move.streams.dataset.UniDataset; -import ai.timefold.solver.core.impl.move.streams.dataset.UniDatasetInstance; +import ai.timefold.solver.core.impl.move.streams.dataset.uni.UniDataset; +import ai.timefold.solver.core.impl.move.streams.dataset.uni.UniDatasetInstance; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.MoveStreamSession; import ai.timefold.solver.core.preview.api.move.SolutionView; import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultUniMoveStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultUniMoveStream.java index e3bbc4f2ad..d976eb609d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultUniMoveStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultUniMoveStream.java @@ -3,8 +3,8 @@ import java.util.Objects; import java.util.function.BiPredicate; -import ai.timefold.solver.core.impl.move.streams.dataset.AbstractUniDataStream; -import ai.timefold.solver.core.impl.move.streams.dataset.UniDataset; +import ai.timefold.solver.core.impl.move.streams.dataset.uni.AbstractUniDataStream; +import ai.timefold.solver.core.impl.move.streams.dataset.uni.UniDataset; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.BiMoveStream; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.UniDataStream; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/FromBiUniMoveProducer.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/FromBiUniMoveProducer.java index 595ad19dab..2c8190994e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/FromBiUniMoveProducer.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/FromBiUniMoveProducer.java @@ -1,8 +1,8 @@ package ai.timefold.solver.core.impl.move.streams; import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; -import ai.timefold.solver.core.impl.move.streams.dataset.AbstractDataStream; -import ai.timefold.solver.core.impl.move.streams.dataset.BiDataset; +import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataStream; +import ai.timefold.solver.core.impl.move.streams.dataset.bi.BiDataset; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.BiMoveConstructor; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.MoveStreamSession; import ai.timefold.solver.core.preview.api.move.Move; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/FromUniBiMoveProducer.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/FromUniBiMoveProducer.java index d009112158..87352ecd2c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/FromUniBiMoveProducer.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/FromUniBiMoveProducer.java @@ -1,8 +1,8 @@ package ai.timefold.solver.core.impl.move.streams; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; -import ai.timefold.solver.core.impl.move.streams.dataset.AbstractDataStream; -import ai.timefold.solver.core.impl.move.streams.dataset.UniDataset; +import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataStream; +import ai.timefold.solver.core.impl.move.streams.dataset.uni.UniDataset; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.BiMoveConstructor; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.MoveStreamSession; import ai.timefold.solver.core.preview.api.move.Move; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/InnerMoveProducer.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/InnerMoveProducer.java index f9fa0c1c77..e0f9b7ba39 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/InnerMoveProducer.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/InnerMoveProducer.java @@ -2,7 +2,7 @@ import java.util.Set; -import ai.timefold.solver.core.impl.move.streams.dataset.AbstractDataStream; +import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataStream; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.MoveProducer; import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/InnerMoveStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/InnerMoveStream.java index 1d56b7fc86..ff8cc117e6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/InnerMoveStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/InnerMoveStream.java @@ -1,7 +1,7 @@ package ai.timefold.solver.core.impl.move.streams; import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; -import ai.timefold.solver.core.impl.move.streams.dataset.AbstractDataset; +import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataset; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.MoveStream; import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/InnerUniMoveStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/InnerUniMoveStream.java index 200b1caa76..e842401dd4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/InnerUniMoveStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/InnerUniMoveStream.java @@ -1,7 +1,7 @@ package ai.timefold.solver.core.impl.move.streams; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; -import ai.timefold.solver.core.impl.move.streams.dataset.UniDataset; +import ai.timefold.solver.core.impl.move.streams.dataset.uni.UniDataset; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.UniMoveStream; import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/DataStreamFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/DataStreamFactory.java index a70f79b977..f7b4a19cce 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/DataStreamFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/DataStreamFactory.java @@ -10,7 +10,12 @@ import ai.timefold.solver.core.impl.domain.solution.descriptor.InnerGenuineVariableMetaModel; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataStream; +import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataset; import ai.timefold.solver.core.impl.move.streams.dataset.common.TerminalDataStream; +import ai.timefold.solver.core.impl.move.streams.dataset.uni.AbstractUniDataStream; +import ai.timefold.solver.core.impl.move.streams.dataset.uni.ForEachFromSolutionDataStream; +import ai.timefold.solver.core.impl.move.streams.dataset.uni.ForEachIncludingPinnedDataStream; import ai.timefold.solver.core.impl.move.streams.maybeapi.DataJoiners; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.UniDataStream; import ai.timefold.solver.core.impl.score.director.SessionContext; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/DatasetSession.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/DatasetSession.java index 4c57861b78..1a196cf448 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/DatasetSession.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/DatasetSession.java @@ -7,6 +7,8 @@ import ai.timefold.solver.core.impl.bavet.AbstractSession; import ai.timefold.solver.core.impl.bavet.NodeNetwork; import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; +import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataset; +import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDatasetInstance; public final class DatasetSession extends AbstractSession { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/DatasetSessionFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/DatasetSessionFactory.java index a19eeb7efd..b837a13636 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/DatasetSessionFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/DatasetSessionFactory.java @@ -10,6 +10,7 @@ import ai.timefold.solver.core.impl.bavet.NodeNetwork; import ai.timefold.solver.core.impl.bavet.common.AbstractNodeBuildHelper; import ai.timefold.solver.core.impl.bavet.uni.AbstractForEachUniNode; +import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; import ai.timefold.solver.core.impl.score.director.SessionContext; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractBiDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/AbstractBiDataStream.java similarity index 89% rename from core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractBiDataStream.java rename to core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/AbstractBiDataStream.java index 83890a1f70..3a795bc5fb 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractBiDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/AbstractBiDataStream.java @@ -1,5 +1,7 @@ -package ai.timefold.solver.core.impl.move.streams.dataset; +package ai.timefold.solver.core.impl.move.streams.dataset.bi; +import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; +import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.common.bridge.AftBridgeBiDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.common.bridge.AftBridgeUniDataStream; import ai.timefold.solver.core.impl.move.streams.maybeapi.BiDataFilter; @@ -30,7 +32,7 @@ public final BiDataStream filter(BiDataFilter @Override public UniDataStream map(BiDataMapper mapping) { - var stream = shareAndAddChild(new BiMapUniDataStream<>(dataStreamFactory, this, mapping)); + var stream = shareAndAddChild(new UniMapBiDataStream<>(dataStreamFactory, this, mapping)); return dataStreamFactory.share(new AftBridgeUniDataStream<>(dataStreamFactory, stream), stream::setAftBridge); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/BiDataset.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiDataset.java similarity index 71% rename from core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/BiDataset.java rename to core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiDataset.java index 16f204e08d..d3cec1c721 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/BiDataset.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiDataset.java @@ -1,7 +1,9 @@ -package ai.timefold.solver.core.impl.move.streams.dataset; +package ai.timefold.solver.core.impl.move.streams.dataset.bi; import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; +import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; +import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataset; import org.jspecify.annotations.NullMarked; @NullMarked diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/BiDatasetInstance.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiDatasetInstance.java similarity index 96% rename from core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/BiDatasetInstance.java rename to core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiDatasetInstance.java index 4fc6c4ca8a..be8bc13462 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/BiDatasetInstance.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiDatasetInstance.java @@ -1,4 +1,4 @@ -package ai.timefold.solver.core.impl.move.streams.dataset; +package ai.timefold.solver.core.impl.move.streams.dataset.bi; import java.util.ArrayList; import java.util.Iterator; @@ -9,6 +9,8 @@ import java.util.Random; import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; +import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataset; +import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDatasetInstance; import ai.timefold.solver.core.impl.util.CollectionUtils; import ai.timefold.solver.core.impl.util.ElementAwareList; import ai.timefold.solver.core.impl.util.ElementAwareListEntry; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/BiMapBiDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiMapBiDataStream.java similarity index 90% rename from core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/BiMapBiDataStream.java rename to core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiMapBiDataStream.java index e99e74c977..893beca2fa 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/BiMapBiDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiMapBiDataStream.java @@ -1,6 +1,7 @@ -package ai.timefold.solver.core.impl.move.streams.dataset; +package ai.timefold.solver.core.impl.move.streams.dataset.bi; import ai.timefold.solver.core.impl.bavet.bi.MapBiToBiNode; +import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; import ai.timefold.solver.core.impl.move.streams.dataset.common.bridge.AftBridgeBiDataStream; import ai.timefold.solver.core.impl.move.streams.maybeapi.BiDataMapper; @@ -18,7 +19,7 @@ final class BiMapBiDataStream private @Nullable AftBridgeBiDataStream aftStream; public BiMapBiDataStream(DataStreamFactory constraintFactory, AbstractBiDataStream parent, - BiDataMapper mappingFunctionA, BiDataMapper mappingFunctionB) { + BiDataMapper mappingFunctionA, BiDataMapper mappingFunctionB) { super(constraintFactory, parent); this.mappingFunctionA = mappingFunctionA; this.mappingFunctionB = mappingFunctionB; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/FilterBiDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/FilterBiDataStream.java similarity index 88% rename from core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/FilterBiDataStream.java rename to core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/FilterBiDataStream.java index 6559d163b8..cbdfc34129 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/FilterBiDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/FilterBiDataStream.java @@ -1,9 +1,10 @@ -package ai.timefold.solver.core.impl.move.streams.dataset; +package ai.timefold.solver.core.impl.move.streams.dataset.bi; import java.util.Objects; 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.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; import ai.timefold.solver.core.impl.move.streams.maybeapi.BiDataFilter; @@ -16,7 +17,7 @@ final class FilterBiDataStream private final BiDataFilter filter; public FilterBiDataStream(DataStreamFactory dataStreamFactory, AbstractBiDataStream parent, - BiDataFilter filter) { + BiDataFilter filter) { super(dataStreamFactory, parent); this.filter = Objects.requireNonNull(filter, "The filter cannot be null."); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/JoinBiDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/JoinBiDataStream.java similarity index 91% rename from core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/JoinBiDataStream.java rename to core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/JoinBiDataStream.java index 9bf560720b..fba6ea3376 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/JoinBiDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/JoinBiDataStream.java @@ -1,4 +1,4 @@ -package ai.timefold.solver.core.impl.move.streams.dataset; +package ai.timefold.solver.core.impl.move.streams.dataset.bi; import java.util.Objects; import java.util.Set; @@ -8,6 +8,8 @@ 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.move.streams.dataset.DataStreamFactory; +import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; import ai.timefold.solver.core.impl.move.streams.dataset.common.JoinDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.common.bridge.ForeBridgeUniDataStream; @@ -23,8 +25,8 @@ public final class JoinBiDataStream extends AbstractBiDataStrea private final BiDataFilter filtering; public JoinBiDataStream(DataStreamFactory dataStreamFactory, - ForeBridgeUniDataStream leftParent, ForeBridgeUniDataStream rightParent, - DefaultBiDataJoiner joiner, BiDataFilter filtering) { + ForeBridgeUniDataStream leftParent, ForeBridgeUniDataStream rightParent, + DefaultBiDataJoiner joiner, BiDataFilter filtering) { super(dataStreamFactory); this.leftParent = leftParent; this.rightParent = rightParent; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/TerminalBiDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/TerminalBiDataStream.java similarity index 90% rename from core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/TerminalBiDataStream.java rename to core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/TerminalBiDataStream.java index 739f32b6ba..55c305dfa2 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/TerminalBiDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/TerminalBiDataStream.java @@ -1,6 +1,7 @@ -package ai.timefold.solver.core.impl.move.streams.dataset; +package ai.timefold.solver.core.impl.move.streams.dataset.bi; import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; +import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; import ai.timefold.solver.core.impl.move.streams.dataset.common.TerminalDataStream; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/BiMapUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/UniMapBiDataStream.java similarity index 81% rename from core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/BiMapUniDataStream.java rename to core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/UniMapBiDataStream.java index a2bfcad567..cdc83e5051 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/BiMapUniDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/UniMapBiDataStream.java @@ -1,8 +1,10 @@ -package ai.timefold.solver.core.impl.move.streams.dataset; +package ai.timefold.solver.core.impl.move.streams.dataset.bi; import ai.timefold.solver.core.impl.bavet.bi.MapBiToUniNode; +import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; import ai.timefold.solver.core.impl.move.streams.dataset.common.bridge.AftBridgeUniDataStream; +import ai.timefold.solver.core.impl.move.streams.dataset.uni.AbstractUniDataStream; import ai.timefold.solver.core.impl.move.streams.maybeapi.BiDataMapper; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -10,14 +12,14 @@ import java.util.Objects; @NullMarked -final class BiMapUniDataStream +final class UniMapBiDataStream extends AbstractUniDataStream { private final BiDataMapper mappingFunction; private @Nullable AftBridgeUniDataStream aftStream; - public BiMapUniDataStream(DataStreamFactory constraintFactory, AbstractBiDataStream parent, - BiDataMapper mappingFunction) { + public UniMapBiDataStream(DataStreamFactory constraintFactory, AbstractBiDataStream parent, + BiDataMapper mappingFunction) { super(constraintFactory, parent); this.mappingFunction = mappingFunction; } @@ -48,7 +50,7 @@ public boolean equals(Object object) { return true; if (object == null || getClass() != object.getClass()) return false; - BiMapUniDataStream that = (BiMapUniDataStream) object; + UniMapBiDataStream that = (UniMapBiDataStream) object; return Objects.equals(parent, that.parent) && Objects.equals(mappingFunction, that.mappingFunction); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDataStream.java similarity index 94% rename from core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractDataStream.java rename to core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDataStream.java index 7cb64e149a..b9ac12f182 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDataStream.java @@ -1,8 +1,8 @@ -package ai.timefold.solver.core.impl.move.streams.dataset; +package ai.timefold.solver.core.impl.move.streams.dataset.common; import ai.timefold.solver.core.impl.bavet.common.BavetStream; import ai.timefold.solver.core.impl.bavet.common.TupleSource; -import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; +import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -28,7 +28,7 @@ public final > Stream_ shareAndAdd return dataStreamFactory.share(stream, childStreamList::add); } - boolean guaranteesDistinct() { + protected boolean guaranteesDistinct() { return true; // Default implementation, can be overridden by subclasses. } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractDataset.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDataset.java similarity index 91% rename from core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractDataset.java rename to core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDataset.java index 08af802a4e..96c19b6003 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractDataset.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDataset.java @@ -1,12 +1,12 @@ -package ai.timefold.solver.core.impl.move.streams.dataset; - -import java.util.Objects; -import java.util.Set; +package ai.timefold.solver.core.impl.move.streams.dataset.common; import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; - +import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import org.jspecify.annotations.NullMarked; +import java.util.Objects; +import java.util.Set; + @NullMarked public abstract class AbstractDataset { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractDatasetInstance.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDatasetInstance.java similarity index 93% rename from core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractDatasetInstance.java rename to core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDatasetInstance.java index 9b2f5e3e50..71fb763b5a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractDatasetInstance.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDatasetInstance.java @@ -1,4 +1,4 @@ -package ai.timefold.solver.core.impl.move.streams.dataset; +package ai.timefold.solver.core.impl.move.streams.dataset.common; import java.util.Iterator; import java.util.Objects; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/DataNodeBuildHelper.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/DataNodeBuildHelper.java index b29da24602..5de65cfc4c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/DataNodeBuildHelper.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/DataNodeBuildHelper.java @@ -9,8 +9,6 @@ import ai.timefold.solver.core.impl.bavet.common.AbstractNodeBuildHelper; 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.move.streams.dataset.AbstractDataStream; -import ai.timefold.solver.core.impl.move.streams.dataset.AbstractDatasetInstance; import ai.timefold.solver.core.impl.score.director.SessionContext; import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/DataStreamBinaryOperation.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/DataStreamBinaryOperation.java index 0c6fcade3f..afc1e298c7 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/DataStreamBinaryOperation.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/DataStreamBinaryOperation.java @@ -1,7 +1,6 @@ package ai.timefold.solver.core.impl.move.streams.dataset.common; import ai.timefold.solver.core.impl.bavet.common.BavetStreamBinaryOperation; -import ai.timefold.solver.core.impl.move.streams.dataset.AbstractDataStream; import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/TerminalDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/TerminalDataStream.java index 7d293c9ef4..ad31999df4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/TerminalDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/TerminalDataStream.java @@ -1,7 +1,6 @@ package ai.timefold.solver.core.impl.move.streams.dataset.common; import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; -import ai.timefold.solver.core.impl.move.streams.dataset.AbstractDataset; import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/AftBridgeBiDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/AftBridgeBiDataStream.java index d4d04654a3..d9c5f0e264 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/AftBridgeBiDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/AftBridgeBiDataStream.java @@ -1,8 +1,8 @@ package ai.timefold.solver.core.impl.move.streams.dataset.common.bridge; import ai.timefold.solver.core.impl.bavet.common.TupleSource; -import ai.timefold.solver.core.impl.move.streams.dataset.AbstractBiDataStream; -import ai.timefold.solver.core.impl.move.streams.dataset.AbstractDataStream; +import ai.timefold.solver.core.impl.move.streams.dataset.bi.AbstractBiDataStream; +import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/AftBridgeUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/AftBridgeUniDataStream.java index d068a7a55b..aa872610c2 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/AftBridgeUniDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/AftBridgeUniDataStream.java @@ -1,8 +1,8 @@ package ai.timefold.solver.core.impl.move.streams.dataset.common.bridge; import ai.timefold.solver.core.impl.bavet.common.TupleSource; -import ai.timefold.solver.core.impl.move.streams.dataset.AbstractDataStream; -import ai.timefold.solver.core.impl.move.streams.dataset.AbstractUniDataStream; +import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataStream; +import ai.timefold.solver.core.impl.move.streams.dataset.uni.AbstractUniDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/ForeBridgeUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/ForeBridgeUniDataStream.java index 73f68025a8..6996946c7e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/ForeBridgeUniDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/ForeBridgeUniDataStream.java @@ -1,7 +1,7 @@ package ai.timefold.solver.core.impl.move.streams.dataset.common.bridge; -import ai.timefold.solver.core.impl.move.streams.dataset.AbstractDataStream; -import ai.timefold.solver.core.impl.move.streams.dataset.AbstractUniDataStream; +import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataStream; +import ai.timefold.solver.core.impl.move.streams.dataset.uni.AbstractUniDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.UniDataStream; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractForEachDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractForEachDataStream.java similarity index 87% rename from core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractForEachDataStream.java rename to core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractForEachDataStream.java index 9f8ca0cfff..fdf1dabab2 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractForEachDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractForEachDataStream.java @@ -1,4 +1,4 @@ -package ai.timefold.solver.core.impl.move.streams.dataset; +package ai.timefold.solver.core.impl.move.streams.dataset.uni; import static ai.timefold.solver.core.impl.bavet.uni.AbstractForEachUniNode.LifecycleOperation; @@ -9,6 +9,8 @@ import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; import ai.timefold.solver.core.impl.bavet.uni.AbstractForEachUniNode; +import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; +import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; import org.jspecify.annotations.NullMarked; @@ -23,7 +25,7 @@ abstract sealed class AbstractForEachDataStream private final boolean shouldIncludeNull; protected AbstractForEachDataStream(DataStreamFactory dataStreamFactory, Class forEachClass, - boolean includeNull) { + boolean includeNull) { super(dataStreamFactory, null); this.forEachClass = Objects.requireNonNull(forEachClass); this.shouldIncludeNull = includeNull; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractUniDataStream.java similarity index 93% rename from core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractUniDataStream.java rename to core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractUniDataStream.java index 8fa4b9c46c..a2245c2cd8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractUniDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractUniDataStream.java @@ -1,5 +1,8 @@ -package ai.timefold.solver.core.impl.move.streams.dataset; +package ai.timefold.solver.core.impl.move.streams.dataset.uni; +import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; +import ai.timefold.solver.core.impl.move.streams.dataset.bi.JoinBiDataStream; +import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.common.bridge.AftBridgeBiDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.common.bridge.AftBridgeUniDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.common.bridge.ForeBridgeUniDataStream; @@ -95,7 +98,7 @@ public UniDataStream map(UniDataMapper BiDataStream map(UniDataMapper mappingA, UniDataMapper mappingB) { - var stream = shareAndAddChild(new UniMapBiDataStream<>(dataStreamFactory, this, mappingA, mappingB)); + var stream = shareAndAddChild(new BiMapUniDataStream<>(dataStreamFactory, this, mappingA, mappingB)); return dataStreamFactory.share(new AftBridgeBiDataStream<>(dataStreamFactory, stream), stream::setAftBridge); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/UniMapBiDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/BiMapUniDataStream.java similarity index 86% rename from core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/UniMapBiDataStream.java rename to core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/BiMapUniDataStream.java index 7f69354b5b..113da91616 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/UniMapBiDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/BiMapUniDataStream.java @@ -1,6 +1,8 @@ -package ai.timefold.solver.core.impl.move.streams.dataset; +package ai.timefold.solver.core.impl.move.streams.dataset.uni; import ai.timefold.solver.core.impl.bavet.uni.MapUniToBiNode; +import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; +import ai.timefold.solver.core.impl.move.streams.dataset.bi.AbstractBiDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; import ai.timefold.solver.core.impl.move.streams.dataset.common.bridge.AftBridgeBiDataStream; import ai.timefold.solver.core.impl.move.streams.maybeapi.UniDataMapper; @@ -10,14 +12,14 @@ import java.util.Objects; @NullMarked -final class UniMapBiDataStream +final class BiMapUniDataStream extends AbstractBiDataStream { private final UniDataMapper mappingFunctionA; private final UniDataMapper mappingFunctionB; private @Nullable AftBridgeBiDataStream aftStream; - public UniMapBiDataStream(DataStreamFactory constraintFactory, AbstractUniDataStream parent, UniDataMapper mappingFunctionA, UniDataMapper mappingFunctionB) { + public BiMapUniDataStream(DataStreamFactory constraintFactory, AbstractUniDataStream parent, UniDataMapper mappingFunctionA, UniDataMapper mappingFunctionB) { super(constraintFactory, parent); this.mappingFunctionA = mappingFunctionA; this.mappingFunctionB = mappingFunctionB; @@ -50,7 +52,7 @@ public boolean equals(Object object) { return true; if (object == null || getClass() != object.getClass()) return false; - UniMapBiDataStream that = (UniMapBiDataStream) object; + BiMapUniDataStream that = (BiMapUniDataStream) object; return Objects.equals(parent, that.parent) && Objects.equals(mappingFunctionA, that.mappingFunctionA) && Objects.equals(mappingFunctionB, that.mappingFunctionB); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/FilterUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/FilterUniDataStream.java similarity index 89% rename from core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/FilterUniDataStream.java rename to core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/FilterUniDataStream.java index 30981d9571..31620922ce 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/FilterUniDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/FilterUniDataStream.java @@ -1,14 +1,14 @@ -package ai.timefold.solver.core.impl.move.streams.dataset; - -import java.util.Objects; +package ai.timefold.solver.core.impl.move.streams.dataset.uni; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; +import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; import ai.timefold.solver.core.impl.move.streams.maybeapi.UniDataFilter; - import org.jspecify.annotations.NullMarked; +import java.util.Objects; + @NullMarked final class FilterUniDataStream extends AbstractUniDataStream { @@ -16,7 +16,7 @@ final class FilterUniDataStream private final UniDataFilter filter; public FilterUniDataStream(DataStreamFactory dataStreamFactory, AbstractUniDataStream parent, - UniDataFilter filter) { + UniDataFilter filter) { super(dataStreamFactory, parent); this.filter = Objects.requireNonNull(filter, "The filter cannot be null."); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/ForEachFromSolutionDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachFromSolutionDataStream.java similarity index 87% rename from core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/ForEachFromSolutionDataStream.java rename to core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachFromSolutionDataStream.java index aa2aaeb166..dd2fc9afcd 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/ForEachFromSolutionDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachFromSolutionDataStream.java @@ -1,4 +1,4 @@ -package ai.timefold.solver.core.impl.move.streams.dataset; +package ai.timefold.solver.core.impl.move.streams.dataset.uni; import java.util.Objects; @@ -9,6 +9,7 @@ import ai.timefold.solver.core.impl.bavet.uni.ForEachFromSolutionUniNode; import ai.timefold.solver.core.impl.domain.valuerange.descriptor.ValueRangeDescriptor; +import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import org.jspecify.annotations.NullMarked; @NullMarked @@ -19,7 +20,7 @@ public final class ForEachFromSolutionDataStream private final ValueRangeDescriptor valueRangeDescriptor; public ForEachFromSolutionDataStream(DataStreamFactory dataStreamFactory, - ValueRangeDescriptor valueRangeDescriptor, boolean includeNull) { + ValueRangeDescriptor valueRangeDescriptor, boolean includeNull) { super(dataStreamFactory, (Class) valueRangeDescriptor.getVariableDescriptor().getVariablePropertyType(), includeNull); this.valueRangeDescriptor = Objects.requireNonNull(valueRangeDescriptor); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/ForEachIncludingPinnedDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachIncludingPinnedDataStream.java similarity index 87% rename from core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/ForEachIncludingPinnedDataStream.java rename to core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachIncludingPinnedDataStream.java index 58e9d1ddde..2d7f74d5c8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/ForEachIncludingPinnedDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachIncludingPinnedDataStream.java @@ -1,4 +1,4 @@ -package ai.timefold.solver.core.impl.move.streams.dataset; +package ai.timefold.solver.core.impl.move.streams.dataset.uni; import java.util.Objects; @@ -8,6 +8,7 @@ import ai.timefold.solver.core.impl.bavet.uni.AbstractForEachUniNode; import ai.timefold.solver.core.impl.bavet.uni.ForEachIncludingUnassignedUniNode; +import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import org.jspecify.annotations.NullMarked; @NullMarked @@ -16,7 +17,7 @@ public final class ForEachIncludingPinnedDataStream implements TupleSource { public ForEachIncludingPinnedDataStream(DataStreamFactory dataStreamFactory, Class forEachClass, - boolean includeNull) { + boolean includeNull) { super(dataStreamFactory, forEachClass, includeNull); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/IfExistsUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/IfExistsUniDataStream.java similarity index 92% rename from core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/IfExistsUniDataStream.java rename to core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/IfExistsUniDataStream.java index 6a19cb781e..f45390fefd 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/IfExistsUniDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/IfExistsUniDataStream.java @@ -1,4 +1,4 @@ -package ai.timefold.solver.core.impl.move.streams.dataset; +package ai.timefold.solver.core.impl.move.streams.dataset.uni; import java.util.Objects; import java.util.Set; @@ -9,6 +9,8 @@ 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; +import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; +import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; import ai.timefold.solver.core.impl.move.streams.dataset.common.IfExistsDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.common.bridge.ForeBridgeUniDataStream; @@ -30,8 +32,8 @@ final class IfExistsUniDataStream private final @Nullable BiDataFilter filtering; public IfExistsUniDataStream(DataStreamFactory dataStreamFactory, AbstractUniDataStream parentA, - ForeBridgeUniDataStream parentBridgeB, boolean shouldExist, DefaultBiDataJoiner joiner, - @Nullable BiDataFilter filtering) { + ForeBridgeUniDataStream parentBridgeB, boolean shouldExist, DefaultBiDataJoiner joiner, + @Nullable BiDataFilter filtering) { super(dataStreamFactory); this.parentA = parentA; this.parentBridgeB = parentBridgeB; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/TerminalUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/TerminalUniDataStream.java similarity index 90% rename from core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/TerminalUniDataStream.java rename to core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/TerminalUniDataStream.java index 31225f0b91..c4e3421a53 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/TerminalUniDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/TerminalUniDataStream.java @@ -1,6 +1,7 @@ -package ai.timefold.solver.core.impl.move.streams.dataset; +package ai.timefold.solver.core.impl.move.streams.dataset.uni; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; +import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; import ai.timefold.solver.core.impl.move.streams.dataset.common.TerminalDataStream; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/UniDataset.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniDataset.java similarity index 71% rename from core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/UniDataset.java rename to core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniDataset.java index e970fc6566..3980a20435 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/UniDataset.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniDataset.java @@ -1,7 +1,9 @@ -package ai.timefold.solver.core.impl.move.streams.dataset; +package ai.timefold.solver.core.impl.move.streams.dataset.uni; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; +import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; +import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataset; import org.jspecify.annotations.NullMarked; @NullMarked diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/UniDatasetInstance.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniDatasetInstance.java similarity index 83% rename from core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/UniDatasetInstance.java rename to core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniDatasetInstance.java index 9173f30181..8f7616ca4a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/UniDatasetInstance.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniDatasetInstance.java @@ -1,9 +1,11 @@ -package ai.timefold.solver.core.impl.move.streams.dataset; +package ai.timefold.solver.core.impl.move.streams.dataset.uni; import java.util.Iterator; import java.util.Random; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; +import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataset; +import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDatasetInstance; import ai.timefold.solver.core.impl.util.ElementAwareList; import ai.timefold.solver.core.impl.util.ElementAwareListEntry; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/UniMapUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniMapUniDataStream.java similarity index 92% rename from core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/UniMapUniDataStream.java rename to core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniMapUniDataStream.java index 4f677e6861..1cd993b3de 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/UniMapUniDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniMapUniDataStream.java @@ -1,6 +1,7 @@ -package ai.timefold.solver.core.impl.move.streams.dataset; +package ai.timefold.solver.core.impl.move.streams.dataset.uni; import ai.timefold.solver.core.impl.bavet.uni.MapUniToUniNode; +import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; import ai.timefold.solver.core.impl.move.streams.dataset.common.bridge.AftBridgeUniDataStream; import ai.timefold.solver.core.impl.move.streams.maybeapi.UniDataMapper; @@ -17,7 +18,7 @@ final class UniMapUniDataStream private @Nullable AftBridgeUniDataStream aftStream; public UniMapUniDataStream(DataStreamFactory constraintFactory, AbstractUniDataStream parent, - UniDataMapper mappingFunction) { + UniDataMapper mappingFunction) { super(constraintFactory, parent); this.mappingFunction = mappingFunction; } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/move/streams/dataset/UniDatasetStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/move/streams/dataset/UniDatasetStreamTest.java index a5d60aaf87..c2adebc822 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/move/streams/dataset/UniDatasetStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/move/streams/dataset/UniDatasetStreamTest.java @@ -5,6 +5,7 @@ import java.util.List; import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; +import ai.timefold.solver.core.impl.move.streams.dataset.uni.AbstractUniDataStream; import ai.timefold.solver.core.impl.score.director.SessionContext; import ai.timefold.solver.core.impl.score.director.easy.EasyScoreDirectorFactory; import ai.timefold.solver.core.testdomain.TestdataEntity; From ef68b6625c8334c51b0b27c497a1e821488b4eec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Wed, 6 Aug 2025 11:07:10 +0200 Subject: [PATCH 05/18] Implement distinct() --- .../DefaultLocalSearchPhaseFactory.java | 12 ++-- .../streams/DefaultMoveStreamFactory.java | 5 +- .../streams/dataset/DataStreamFactory.java | 26 +++++--- .../dataset/bi/AbstractBiDataStream.java | 23 ++++++- .../dataset/bi/BiGroupBiDataStream.java | 57 ++++++++++++++++ .../dataset/uni/AbstractUniDataStream.java | 37 ++++++++++- .../dataset/uni/UniGroupUniDataStream.java | 65 +++++++++++++++++++ .../move/MoveStreamsBasedLocalSearchTest.java | 18 ++--- .../streams/dataset/UniDatasetStreamTest.java | 34 +++++----- .../provider/ChangeMoveProviderTest.java | 23 ++++--- 10 files changed, 239 insertions(+), 61 deletions(-) create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiGroupBiDataStream.java create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniGroupUniDataStream.java diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java index 0229c1284f..3621a961c6 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java @@ -1,10 +1,5 @@ package ai.timefold.solver.core.impl.localsearch; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; - import ai.timefold.solver.core.api.domain.entity.PinningFilter; import ai.timefold.solver.core.api.domain.entity.PlanningPin; import ai.timefold.solver.core.api.domain.variable.PlanningListVariable; @@ -50,6 +45,11 @@ import ai.timefold.solver.core.impl.solver.termination.SolverTermination; import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningVariableMetaModel; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + public class DefaultLocalSearchPhaseFactory extends AbstractPhaseFactory { public DefaultLocalSearchPhaseFactory(LocalSearchPhaseConfig phaseConfig) { @@ -134,7 +134,7 @@ Convert your entities (%s) to use @%s instead.""" .formatted(moveProvidersClass, moveProviderList.size())); } var moveProvider = moveProviderList.get(0); - var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor); + var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, configPolicy.getEnvironmentMode()); var moveProducer = moveProvider.apply(moveStreamFactory); var moveRepository = new MoveStreamsBasedMoveRepository<>(moveStreamFactory, moveProducer, pickSelectionOrder() == SelectionOrder.RANDOM); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamFactory.java index af61b9b6a3..d9fb7b9e49 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamFactory.java @@ -1,5 +1,6 @@ package ai.timefold.solver.core.impl.move.streams; +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultPlanningListVariableMetaModel; import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultPlanningVariableMetaModel; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; @@ -28,8 +29,8 @@ public final class DefaultMoveStreamFactory private final DataStreamFactory dataStreamFactory; private final DatasetSessionFactory datasetSessionFactory; - public DefaultMoveStreamFactory(SolutionDescriptor solutionDescriptor) { - this.dataStreamFactory = new DataStreamFactory<>(solutionDescriptor); + public DefaultMoveStreamFactory(SolutionDescriptor solutionDescriptor, EnvironmentMode environmentMode) { + this.dataStreamFactory = new DataStreamFactory<>(solutionDescriptor, environmentMode); this.datasetSessionFactory = new DatasetSessionFactory<>(dataStreamFactory); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/DataStreamFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/DataStreamFactory.java index f7b4a19cce..92fa2e7416 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/DataStreamFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/DataStreamFactory.java @@ -1,13 +1,6 @@ package ai.timefold.solver.core.impl.move.streams.dataset; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.function.Consumer; -import java.util.stream.Collectors; -import java.util.stream.Stream; - +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.domain.solution.descriptor.InnerGenuineVariableMetaModel; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataStream; @@ -20,17 +13,26 @@ import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.UniDataStream; import ai.timefold.solver.core.impl.score.director.SessionContext; import ai.timefold.solver.core.preview.api.domain.metamodel.GenuineVariableMetaModel; - import org.jspecify.annotations.NullMarked; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + @NullMarked public final class DataStreamFactory { private final SolutionDescriptor solutionDescriptor; + private final EnvironmentMode environmentMode; private final Map, AbstractDataStream> sharingStreamMap = new HashMap<>(256); - public DataStreamFactory(SolutionDescriptor solutionDescriptor) { + public DataStreamFactory(SolutionDescriptor solutionDescriptor, EnvironmentMode environmentMode) { this.solutionDescriptor = Objects.requireNonNull(solutionDescriptor); + this.environmentMode = Objects.requireNonNull(environmentMode); } public UniDataStream forEachNonDiscriminating(Class sourceClass, boolean includeNull) { @@ -131,6 +133,10 @@ public SolutionDescriptor getSolutionDescriptor() { return solutionDescriptor; } + public EnvironmentMode getEnvironmentMode() { + return environmentMode; + } + @SuppressWarnings("unchecked") public List> getDatasets() { return sharingStreamMap.values().stream() diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/AbstractBiDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/AbstractBiDataStream.java index 3a795bc5fb..61adfa6349 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/AbstractBiDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/AbstractBiDataStream.java @@ -1,5 +1,8 @@ package ai.timefold.solver.core.impl.move.streams.dataset.bi; +import ai.timefold.solver.core.impl.bavet.bi.Group2Mapping0CollectorBiNode; +import ai.timefold.solver.core.impl.bavet.common.GroupNodeConstructor; +import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.common.bridge.AftBridgeBiDataStream; @@ -8,10 +11,12 @@ import ai.timefold.solver.core.impl.move.streams.maybeapi.BiDataMapper; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.BiDataStream; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.UniDataStream; - +import ai.timefold.solver.core.impl.util.ConstantLambdaUtils; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; +import java.util.function.BiFunction; + @NullMarked public abstract class AbstractBiDataStream extends AbstractDataStream implements BiDataStream { @@ -30,6 +35,18 @@ public final BiDataStream filter(BiDataFilter return shareAndAddChild(new FilterBiDataStream<>(dataStreamFactory, this, filter)); } + + protected AbstractBiDataStream groupBy(BiFunction groupKeyAMapping, BiFunction groupKeyBMapping) { + GroupNodeConstructor> nodeConstructor = + GroupNodeConstructor.twoKeysGroupBy(groupKeyAMapping, groupKeyBMapping, Group2Mapping0CollectorBiNode::new); + return buildBiGroupBy(nodeConstructor); + } + + private AbstractBiDataStream buildBiGroupBy(GroupNodeConstructor> nodeConstructor) { + var stream = shareAndAddChild(new BiGroupBiDataStream<>(dataStreamFactory, this, nodeConstructor)); + return dataStreamFactory.share(new AftBridgeBiDataStream<>(dataStreamFactory, stream), stream::setAftBridge); + } + @Override public UniDataStream map(BiDataMapper mapping) { var stream = shareAndAddChild(new UniMapBiDataStream<>(dataStreamFactory, this, mapping)); @@ -43,11 +60,11 @@ public BiDataStream map(BiDa } @Override - public BiDataStream distinct() { + public AbstractBiDataStream distinct() { if (guaranteesDistinct()) { return this; // Already distinct, no need to create a new stream. } - throw new UnsupportedOperationException(); + return groupBy(ConstantLambdaUtils.biPickFirst(), ConstantLambdaUtils.biPickSecond()); } public BiDataset createDataset() { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiGroupBiDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiGroupBiDataStream.java new file mode 100644 index 0000000000..e5beca28f5 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiGroupBiDataStream.java @@ -0,0 +1,57 @@ +package ai.timefold.solver.core.impl.move.streams.dataset.bi; + +import ai.timefold.solver.core.impl.bavet.common.GroupNodeConstructor; +import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; +import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; +import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; +import ai.timefold.solver.core.impl.move.streams.dataset.common.bridge.AftBridgeBiDataStream; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.Objects; + +@NullMarked +final class BiGroupBiDataStream + extends AbstractBiDataStream { + + private final GroupNodeConstructor> nodeConstructor; + private @Nullable AftBridgeBiDataStream aftStream; + + public BiGroupBiDataStream(DataStreamFactory dataStreamFactory, AbstractBiDataStream parent, + GroupNodeConstructor> nodeConstructor) { + super(dataStreamFactory, parent); + this.nodeConstructor = nodeConstructor; + } + + public void setAftBridge(AftBridgeBiDataStream aftStream) { + this.aftStream = aftStream; + } + + @Override + public void buildNode(DataNodeBuildHelper buildHelper) { + var aftStreamChildList = aftStream.getChildStreamList(); + nodeConstructor.build(buildHelper, parent.getTupleSource(), aftStream, aftStreamChildList, this, + dataStreamFactory.getEnvironmentMode()); + } + + @Override + public boolean equals(Object object) { + if (this == object) + return true; + if (object == null || getClass() != object.getClass()) + return false; + var that = (BiGroupBiDataStream) object; + return Objects.equals(parent, that.parent) && Objects.equals(nodeConstructor, that.nodeConstructor); + } + + @Override + public int hashCode() { + return Objects.hash(parent, nodeConstructor); + } + + @Override + public String toString() { + return "BiGroup()"; + } + +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractUniDataStream.java index a2245c2cd8..9d14e435ed 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractUniDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractUniDataStream.java @@ -1,5 +1,9 @@ package ai.timefold.solver.core.impl.move.streams.dataset.uni; +import ai.timefold.solver.core.api.score.stream.uni.UniConstraintStream; +import ai.timefold.solver.core.impl.bavet.common.GroupNodeConstructor; +import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; +import ai.timefold.solver.core.impl.bavet.uni.Group1Mapping0CollectorUniNode; import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.bi.JoinBiDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataStream; @@ -12,10 +16,15 @@ import ai.timefold.solver.core.impl.move.streams.maybeapi.UniDataMapper; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.BiDataStream; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.UniDataStream; - +import ai.timefold.solver.core.impl.util.ConstantLambdaUtils; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; +import java.util.Objects; +import java.util.function.Function; + +import static ai.timefold.solver.core.impl.bavet.common.GroupNodeConstructor.oneKeyGroupBy; + @NullMarked public abstract class AbstractUniDataStream extends AbstractDataStream implements UniDataStream { @@ -89,6 +98,30 @@ private UniDataStream ifExistsOrNot(boolean shouldExist, UniDa joinerComber.mergedJoiner(), joinerComber.mergedFiltering()), childStreamList::add); } + /** + * Convert the {@link UniConstraintStream} to a different {@link UniConstraintStream}, + * containing the set of tuples resulting from applying the group key mapping function + * on all tuples of the original stream. + * Neither tuple of the new stream {@link Objects#equals(Object, Object)} any other. + * + * @param groupKeyMapping mapping function to convert each element in the stream to a different element + * @param the type of a fact in the destination {@link UniConstraintStream}'s tuple; + * must honor {@link Object#hashCode() the general contract of hashCode}. + */ + protected AbstractUniDataStream groupBy(Function groupKeyMapping) { + // We do not expose this on the API, as this operation is not yet needed in any of the moves. + // The groupBy API will need revisiting if exposed as a feature of Move Streams, do not expose as is. + GroupNodeConstructor> nodeConstructor = + oneKeyGroupBy(groupKeyMapping, Group1Mapping0CollectorUniNode::new); + return buildUniGroupBy(nodeConstructor); + } + + private AbstractUniDataStream buildUniGroupBy(GroupNodeConstructor> nodeConstructor) { + var stream = shareAndAddChild(new UniGroupUniDataStream<>(dataStreamFactory, this, nodeConstructor)); + return dataStreamFactory.share(new AftBridgeUniDataStream<>(dataStreamFactory, stream), + stream::setAftBridge); + } + @Override public UniDataStream map(UniDataMapper mapping) { var stream = shareAndAddChild(new UniMapUniDataStream<>(dataStreamFactory, this, mapping)); @@ -107,7 +140,7 @@ public AbstractUniDataStream distinct() { if (guaranteesDistinct()) { return this; // Already distinct, no need to create a new stream. } - throw new UnsupportedOperationException(); + return groupBy(ConstantLambdaUtils.identity()); } public UniDataset createDataset() { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniGroupUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniGroupUniDataStream.java new file mode 100644 index 0000000000..db37f5e6c8 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniGroupUniDataStream.java @@ -0,0 +1,65 @@ +package ai.timefold.solver.core.impl.move.streams.dataset.uni; + +import ai.timefold.solver.core.impl.bavet.common.GroupNodeConstructor; +import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; +import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; +import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; +import ai.timefold.solver.core.impl.move.streams.dataset.common.bridge.AftBridgeUniDataStream; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.Objects; + +@NullMarked +final class UniGroupUniDataStream + extends AbstractUniDataStream { + + private final GroupNodeConstructor> nodeConstructor; + private @Nullable AftBridgeUniDataStream aftStream; + + public UniGroupUniDataStream(DataStreamFactory dataStreamFactory, AbstractUniDataStream parent, + GroupNodeConstructor> nodeConstructor) { + super(dataStreamFactory, parent); + this.nodeConstructor = nodeConstructor; + } + + public void setAftBridge(AftBridgeUniDataStream aftStream) { + this.aftStream = aftStream; + } + + // ************************************************************************ + // Node creation + // ************************************************************************ + + @Override + public void buildNode(DataNodeBuildHelper buildHelper) { + var aftStreamChildList = aftStream.getChildStreamList(); + nodeConstructor.build(buildHelper, parent.getTupleSource(), aftStream, aftStreamChildList, this, + dataStreamFactory.getEnvironmentMode()); + } + + // ************************************************************************ + // Equality for node sharing + // ************************************************************************ + + @Override + public boolean equals(Object object) { + if (this == object) + return true; + if (object == null || getClass() != object.getClass()) + return false; + var that = (UniGroupUniDataStream) object; + return Objects.equals(parent, that.parent) && Objects.equals(nodeConstructor, that.nodeConstructor); + } + + @Override + public int hashCode() { + return Objects.hash(parent, nodeConstructor); + } + + @Override + public String toString() { + return "UniGroup()"; + } + +} diff --git a/core/src/test/java/ai/timefold/solver/core/impl/move/MoveStreamsBasedLocalSearchTest.java b/core/src/test/java/ai/timefold/solver/core/impl/move/MoveStreamsBasedLocalSearchTest.java index 5472fa7d09..fd278678d2 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/move/MoveStreamsBasedLocalSearchTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/move/MoveStreamsBasedLocalSearchTest.java @@ -1,16 +1,10 @@ package ai.timefold.solver.core.impl.move; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; - -import java.util.HashSet; -import java.util.Random; - import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; import ai.timefold.solver.core.config.localsearch.decider.acceptor.LocalSearchAcceptorConfig; import ai.timefold.solver.core.config.localsearch.decider.forager.LocalSearchForagerConfig; +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.config.solver.termination.TerminationConfig; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; @@ -30,10 +24,16 @@ import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataSolution; import ai.timefold.solver.core.testdomain.TestdataValue; - import org.jspecify.annotations.NonNull; import org.junit.jupiter.api.Test; +import java.util.HashSet; +import java.util.Random; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + class MoveStreamsBasedLocalSearchTest { @Test @@ -95,7 +95,7 @@ void changeMoveBasedLocalSearch() { .genuineVariable() .ensurePlanningVariable(); var moveProvider = new ChangeMoveProvider<>(variableMetaModel); - var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor); + var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); var moveProducer = moveProvider.apply(moveStreamFactory); // Random selection otherwise LS gets stuck in an endless loop. return new MoveStreamsBasedMoveRepository<>(moveStreamFactory, moveProducer, true); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/move/streams/dataset/UniDatasetStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/move/streams/dataset/UniDatasetStreamTest.java index c2adebc822..a2d3b9e00b 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/move/streams/dataset/UniDatasetStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/move/streams/dataset/UniDatasetStreamTest.java @@ -1,10 +1,7 @@ package ai.timefold.solver.core.impl.move.streams.dataset; -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.List; - import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.move.streams.dataset.uni.AbstractUniDataStream; import ai.timefold.solver.core.impl.score.director.SessionContext; import ai.timefold.solver.core.impl.score.director.easy.EasyScoreDirectorFactory; @@ -15,14 +12,17 @@ import ai.timefold.solver.core.testdomain.list.pinned.index.TestdataPinnedWithIndexListEntity; import ai.timefold.solver.core.testdomain.list.pinned.index.TestdataPinnedWithIndexListSolution; import ai.timefold.solver.core.testdomain.list.pinned.index.TestdataPinnedWithIndexListValue; - import org.junit.jupiter.api.Test; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + class UniDatasetStreamTest { @Test void forEachBasicVariable() { - var dataStreamFactory = new DataStreamFactory<>(TestdataSolution.buildSolutionDescriptor()); + var dataStreamFactory = new DataStreamFactory<>(TestdataSolution.buildSolutionDescriptor(), EnvironmentMode.PHASE_ASSERT); var uniDataset = ((AbstractUniDataStream) dataStreamFactory .forEachNonDiscriminating(TestdataEntity.class, false)) .createDataset(); @@ -54,7 +54,7 @@ void forEachBasicVariable() { @Test void forEachBasicVariableIncludingNull() { - var dataStreamFactory = new DataStreamFactory<>(TestdataSolution.buildSolutionDescriptor()); + var dataStreamFactory = new DataStreamFactory<>(TestdataSolution.buildSolutionDescriptor(), EnvironmentMode.PHASE_ASSERT); var uniDataset = ((AbstractUniDataStream) dataStreamFactory .forEachNonDiscriminating(TestdataEntity.class, true)) .createDataset(); @@ -86,7 +86,7 @@ void forEachBasicVariableIncludingNull() { @Test void forEachListVariable() { - var dataStreamFactory = new DataStreamFactory<>(TestdataListSolution.buildSolutionDescriptor()); + var dataStreamFactory = new DataStreamFactory<>(TestdataListSolution.buildSolutionDescriptor(), EnvironmentMode.PHASE_ASSERT); var uniDataset = ((AbstractUniDataStream) dataStreamFactory .forEachNonDiscriminating(TestdataListEntity.class, false)) .createDataset(); @@ -118,7 +118,7 @@ void forEachListVariable() { @Test void forEachListVariableIncludingNull() { - var dataStreamFactory = new DataStreamFactory<>(TestdataListSolution.buildSolutionDescriptor()); + var dataStreamFactory = new DataStreamFactory<>(TestdataListSolution.buildSolutionDescriptor(), EnvironmentMode.PHASE_ASSERT); var uniDataset = ((AbstractUniDataStream) dataStreamFactory .forEachNonDiscriminating(TestdataListEntity.class, true)) .createDataset(); @@ -166,7 +166,7 @@ private static DatasetSession createSession(DataStreamFac @Test void forEachListVariableIncludingPinned() { - var dataStreamFactory = new DataStreamFactory<>(TestdataPinnedWithIndexListSolution.buildSolutionDescriptor()); + var dataStreamFactory = new DataStreamFactory<>(TestdataPinnedWithIndexListSolution.buildSolutionDescriptor(), EnvironmentMode.PHASE_ASSERT); var uniDataset = ((AbstractUniDataStream) dataStreamFactory .forEachNonDiscriminating(TestdataPinnedWithIndexListEntity.class, false)) @@ -209,7 +209,7 @@ void forEachListVariableIncludingPinned() { @Test void forEachListVariableIncludingPinnedAndNull() { - var dataStreamFactory = new DataStreamFactory<>(TestdataPinnedWithIndexListSolution.buildSolutionDescriptor()); + var dataStreamFactory = new DataStreamFactory<>(TestdataPinnedWithIndexListSolution.buildSolutionDescriptor(), EnvironmentMode.PHASE_ASSERT); var uniDataset = ((AbstractUniDataStream) dataStreamFactory .forEachNonDiscriminating(TestdataPinnedWithIndexListEntity.class, true)) @@ -252,7 +252,7 @@ void forEachListVariableIncludingPinnedAndNull() { @Test void forEachListVariableExcludingPinned() { // Entities with planningPin true will be skipped. - var dataStreamFactory = new DataStreamFactory<>(TestdataPinnedWithIndexListSolution.buildSolutionDescriptor()); + var dataStreamFactory = new DataStreamFactory<>(TestdataPinnedWithIndexListSolution.buildSolutionDescriptor(), EnvironmentMode.PHASE_ASSERT); var uniDataset = ((AbstractUniDataStream) dataStreamFactory .forEachExcludingPinned(TestdataPinnedWithIndexListEntity.class, false)) @@ -296,7 +296,7 @@ void forEachListVariableExcludingPinned() { // Entities with planningPin true wi @Test void forEachListVariableExcludingPinnedIncludingNull() { // Entities with planningPin true will be skipped. - var dataStreamFactory = new DataStreamFactory<>(TestdataPinnedWithIndexListSolution.buildSolutionDescriptor()); + var dataStreamFactory = new DataStreamFactory<>(TestdataPinnedWithIndexListSolution.buildSolutionDescriptor(), EnvironmentMode.PHASE_ASSERT); var uniDataset = ((AbstractUniDataStream) dataStreamFactory .forEachExcludingPinned(TestdataPinnedWithIndexListEntity.class, true)) @@ -340,7 +340,7 @@ void forEachListVariableExcludingPinnedIncludingNull() { // Entities with planni @Test void forEachListVariableIncludingPinnedValues() { - var dataStreamFactory = new DataStreamFactory<>(TestdataPinnedWithIndexListSolution.buildSolutionDescriptor()); + var dataStreamFactory = new DataStreamFactory<>(TestdataPinnedWithIndexListSolution.buildSolutionDescriptor(), EnvironmentMode.PHASE_ASSERT); var uniDataset = ((AbstractUniDataStream) dataStreamFactory .forEachNonDiscriminating(TestdataPinnedWithIndexListValue.class, false)) @@ -381,7 +381,7 @@ void forEachListVariableIncludingPinnedValues() { @Test void forEachListVariableIncludingPinnedValuesAndNull() { - var dataStreamFactory = new DataStreamFactory<>(TestdataPinnedWithIndexListSolution.buildSolutionDescriptor()); + var dataStreamFactory = new DataStreamFactory<>(TestdataPinnedWithIndexListSolution.buildSolutionDescriptor(), EnvironmentMode.PHASE_ASSERT); var uniDataset = ((AbstractUniDataStream) dataStreamFactory .forEachNonDiscriminating(TestdataPinnedWithIndexListValue.class, true)) @@ -423,7 +423,7 @@ void forEachListVariableIncludingPinnedValuesAndNull() { @Test void forEachListVariableExcludingPinnedValues() { var solutionDescriptor = TestdataPinnedWithIndexListSolution.buildSolutionDescriptor(); - var dataStreamFactory = new DataStreamFactory<>(solutionDescriptor); + var dataStreamFactory = new DataStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); var uniDataset = ((AbstractUniDataStream) dataStreamFactory .forEachExcludingPinned(TestdataPinnedWithIndexListValue.class, false)) @@ -468,7 +468,7 @@ void forEachListVariableExcludingPinnedValues() { @Test void forEachListVariableExcludingPinnedValuesIncludingNull() { var solutionDescriptor = TestdataPinnedWithIndexListSolution.buildSolutionDescriptor(); - var dataStreamFactory = new DataStreamFactory<>(solutionDescriptor); + var dataStreamFactory = new DataStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); var uniDataset = ((AbstractUniDataStream) dataStreamFactory .forEachExcludingPinned(TestdataPinnedWithIndexListValue.class, true)) diff --git a/core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ChangeMoveProviderTest.java b/core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ChangeMoveProviderTest.java index b9fa1e2eb3..3770adc6b3 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ChangeMoveProviderTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ChangeMoveProviderTest.java @@ -1,11 +1,5 @@ package ai.timefold.solver.core.impl.move.streams.maybeapi.provider; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.SoftAssertions.assertSoftly; - -import java.util.Collections; -import java.util.stream.StreamSupport; - import ai.timefold.solver.core.api.score.stream.ConstraintProvider; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; @@ -32,9 +26,14 @@ import ai.timefold.solver.core.testdomain.valuerange.incomplete.TestdataIncompleteValueRangeConstraintProvider; import ai.timefold.solver.core.testdomain.valuerange.incomplete.TestdataIncompleteValueRangeEntity; import ai.timefold.solver.core.testdomain.valuerange.incomplete.TestdataIncompleteValueRangeSolution; - import org.junit.jupiter.api.Test; +import java.util.Collections; +import java.util.stream.StreamSupport; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + class ChangeMoveProviderTest { @Test @@ -54,7 +53,7 @@ void fromSolutionBasicVariable() { var secondValue = solution.getValueList().get(1); var scoreDirector = createScoreDirector(solutionDescriptor, new TestdataConstraintProvider(), solution); - var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor); + var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); var moveProvider = new ChangeMoveProvider<>(variableMetaModel); var moveProducer = moveProvider.apply(moveStreamFactory); var moveStreamSession = createSession(moveStreamFactory, scoreDirector); @@ -122,7 +121,7 @@ void fromSolutionBasicVariableIncompleteValueRange() { var scoreDirector = createScoreDirector(solutionDescriptor, new TestdataIncompleteValueRangeConstraintProvider(), solution); - var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor); + var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); var moveProvider = new ChangeMoveProvider<>(variableMetaModel); var moveProducer = moveProvider.apply(moveStreamFactory); var moveStreamSession = createSession(moveStreamFactory, scoreDirector); @@ -182,7 +181,7 @@ void fromEntityBasicVariable() { var firstValue = firstEntity.getValueRange().get(0); var scoreDirector = createScoreDirector(solutionDescriptor, new TestdataEntityProvidingConstraintProvider(), solution); - var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor); + var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); var moveProvider = new ChangeMoveProvider<>(variableMetaModel); var moveProducer = moveProvider.apply(moveStreamFactory); var moveStreamSession = createSession(moveStreamFactory, scoreDirector); @@ -224,7 +223,7 @@ void fromEntityBasicVariableAllowsUnassigned() { var scoreDirector = createScoreDirector(solutionDescriptor, new TestdataAllowsUnassignedEntityProvidingConstraintProvider(), solution); - var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor); + var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); var moveProvider = new ChangeMoveProvider<>(variableMetaModel); var moveProducer = moveProvider.apply(moveStreamFactory); var moveStreamSession = createSession(moveStreamFactory, scoreDirector); @@ -285,7 +284,7 @@ void fromSolutionBasicVariableAllowsUnassigned() { var secondValue = solution.getValueList().get(1); var scoreDirector = createScoreDirector(solutionDescriptor, new TestdataAllowsUnassignedConstraintProvider(), solution); - var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor); + var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); var moveProvider = new ChangeMoveProvider<>(variableMetaModel); var moveProducer = moveProvider.apply(moveStreamFactory); var moveStreamSession = createSession(moveStreamFactory, scoreDirector); From 30eef6a3c9cc6e512a3e395a69d84beadc0d8d5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Wed, 6 Aug 2025 12:06:26 +0200 Subject: [PATCH 06/18] Fix an update issue --- .../core/impl/move/director/MoveDirector.java | 9 +- .../streams/dataset/bi/BiDatasetInstance.java | 32 +++-- .../common/AbstractDatasetInstance.java | 14 +- .../dataset/uni/UniDatasetInstance.java | 12 +- .../maybeapi/generic/move/ListChangeMove.java | 13 +- .../provider/ListChangeMoveProvider.java | 23 +++- .../preview/api/move/MutableSolutionView.java | 2 +- .../impl/move/director/MoveDirectorTest.java | 6 +- .../core/impl/solver/DefaultSolverTest.java | 121 ++++++++++++------ 9 files changed, 157 insertions(+), 75 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/director/MoveDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/move/director/MoveDirector.java index b18224db49..9a952f1edc 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/director/MoveDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/director/MoveDirector.java @@ -88,7 +88,7 @@ public final void changeVariable(PlanningVariableMetaModel variableMetaModel, Entity_ sourceEntity, int sourceIndex, Entity_ destinationEntity, int destinationIndex) { if (sourceEntity == destinationEntity) { - return moveValueInList(variableMetaModel, sourceEntity, sourceIndex, destinationIndex); + return swapValues(variableMetaModel, sourceEntity, sourceIndex, destinationIndex); } var variableDescriptor = extractVariableDescriptor(variableMetaModel); externalScoreDirector.beforeListVariableChanged(variableDescriptor, sourceEntity, sourceIndex, sourceIndex + 1); @@ -106,13 +106,13 @@ public final void changeVariable(PlanningVariableMetaModel @Nullable Value_ moveValueInList( + public final @Nullable Value_ swapValues( PlanningListVariableMetaModel variableMetaModel, Entity_ entity, int sourceIndex, int destinationIndex) { if (sourceIndex == destinationIndex) { return null; } else if (sourceIndex > destinationIndex) { // Always start from the lower index. - return moveValueInList(variableMetaModel, entity, destinationIndex, sourceIndex); + return swapValues(variableMetaModel, entity, destinationIndex, sourceIndex); } var variableDescriptor = extractVariableDescriptor(variableMetaModel); var toIndex = destinationIndex + 1; @@ -185,7 +185,8 @@ public final Value_ getValue(PlanningVariableMetaModel int countValues(PlanningListVariableMetaModel variableMetaModel, Entity_ entity) { + public int countValues(PlanningListVariableMetaModel variableMetaModel, + Entity_ entity) { return extractVariableDescriptor(variableMetaModel).getValue(entity).size(); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiDatasetInstance.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiDatasetInstance.java index be8bc13462..6525317754 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiDatasetInstance.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiDatasetInstance.java @@ -1,23 +1,22 @@ package ai.timefold.solver.core.impl.move.streams.dataset.bi; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Random; - import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataset; import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDatasetInstance; import ai.timefold.solver.core.impl.util.CollectionUtils; import ai.timefold.solver.core.impl.util.ElementAwareList; import ai.timefold.solver.core.impl.util.ElementAwareListEntry; - import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Random; + @NullMarked public final class BiDatasetInstance extends AbstractDatasetInstance> { @@ -35,6 +34,21 @@ public void insert(BiTuple tuple) { tuple.setStore(inputStoreIndex, entry); } + @Override + public void update(BiTuple tuple) { + var actualTupleList = tuple.getStore(inputStoreIndex); + if (actualTupleList == null) { // The tuple was not inserted yet. + insert(tuple); + return; + } + var expectedTupleList = tupleListMap.get(tuple.factA); + if (actualTupleList == expectedTupleList) { + return; // Changing the tuple did not change the key. + } + retract(tuple); + insert(tuple); + } + @Override public void retract(BiTuple tuple) { ElementAwareListEntry> entry = tuple.removeStore(inputStoreIndex); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDatasetInstance.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDatasetInstance.java index 71fb763b5a..b0e9444ff8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDatasetInstance.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDatasetInstance.java @@ -1,14 +1,13 @@ package ai.timefold.solver.core.impl.move.streams.dataset.common; -import java.util.Iterator; -import java.util.Objects; -import java.util.Random; - import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; - import org.jspecify.annotations.NullMarked; +import java.util.Iterator; +import java.util.Objects; +import java.util.Random; + @NullMarked public abstract class AbstractDatasetInstance implements TupleLifecycle { @@ -25,11 +24,6 @@ public AbstractDataset getParent() { return parent; } - @Override - public void update(Tuple_ tuple) { - // No need to do anything. - } - public abstract Iterator iterator(); public abstract Iterator iterator(Random workingRandom); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniDatasetInstance.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniDatasetInstance.java index 8f7616ca4a..fb5a4d2799 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniDatasetInstance.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniDatasetInstance.java @@ -1,16 +1,15 @@ package ai.timefold.solver.core.impl.move.streams.dataset.uni; -import java.util.Iterator; -import java.util.Random; - import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataset; import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDatasetInstance; import ai.timefold.solver.core.impl.util.ElementAwareList; import ai.timefold.solver.core.impl.util.ElementAwareListEntry; - import org.jspecify.annotations.NullMarked; +import java.util.Iterator; +import java.util.Random; + @NullMarked public final class UniDatasetInstance extends AbstractDatasetInstance> { @@ -27,6 +26,11 @@ public void insert(UniTuple tuple) { tuple.setStore(inputStoreIndex, entry); } + @Override + public void update(UniTuple tuple) { + // No need to do anything. + } + @Override public void retract(UniTuple tuple) { ElementAwareListEntry> entry = tuple.removeStore(inputStoreIndex); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListChangeMove.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListChangeMove.java index faad305891..b7e9f3cc30 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListChangeMove.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListChangeMove.java @@ -1,19 +1,18 @@ package ai.timefold.solver.core.impl.move.streams.maybeapi.generic.move; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Objects; - import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.domain.variable.PlanningListVariable; import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningListVariableMetaModel; import ai.timefold.solver.core.preview.api.domain.metamodel.VariableMetaModel; import ai.timefold.solver.core.preview.api.move.MutableSolutionView; import ai.timefold.solver.core.preview.api.move.Rebaser; - import org.jspecify.annotations.NonNull; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + /** * Moves an element of a {@link PlanningListVariable list variable}. The moved element is identified * by an entity instance and a position in that entity's list variable. The element is inserted at the given index @@ -121,7 +120,7 @@ private Value_ getMovedValue() { @Override public void execute(@NonNull MutableSolutionView solutionView) { if (sourceEntity == destinationEntity) { - planningValue = solutionView.moveValueInList(variableMetaModel, sourceEntity, sourceIndex, destinationIndex); + planningValue = solutionView.swapValues(variableMetaModel, sourceEntity, sourceIndex, destinationIndex); } else { planningValue = solutionView.moveValueBetweenLists(variableMetaModel, sourceEntity, sourceIndex, destinationEntity, destinationIndex); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java index 389c7b450f..fa0dfa49e3 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java @@ -10,6 +10,7 @@ import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.MoveStreamFactory; import ai.timefold.solver.core.preview.api.domain.metamodel.ElementPosition; import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningListVariableMetaModel; +import ai.timefold.solver.core.preview.api.domain.metamodel.PositionInList; import ai.timefold.solver.core.preview.api.domain.metamodel.UnassignedElement; import org.jspecify.annotations.NullMarked; @@ -35,7 +36,27 @@ public ListChangeMoveProvider(PlanningListVariableMetaModel { var currentPosition = solutionView.getPositionOf(variableMetaModel, Objects.requireNonNull(value)); - return !currentPosition.equals(targetPosition); + if (currentPosition.equals(targetPosition)) { // The target position is the same as the current position. + return false; + } + if (!(currentPosition instanceof PositionInList currentPositionInList)) { + // The current position is unassigned, so we can assign the value. + return true; + } + if (!(targetPosition instanceof PositionInList targetPositionInList)) { + // The target position is unassigned, so we can unassign the value. + return true; + } + if (currentPositionInList.entity() != targetPositionInList.entity()) { + return true; // Different entities, so we can freely change the position. + } + var listLength = solutionView.countValues(variableMetaModel, currentPositionInList.entity()); + if (listLength == 1) { + return false; // No need to change the position, as the value is the only one in this list. + } + // Make sure we're not moving the value past the list end. + // This would happen if the value was already at the end of the list. + return currentPositionInList.index() != listLength - 1 || targetPositionInList.index() != listLength; }; } diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/MutableSolutionView.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/MutableSolutionView.java index 87fb50a978..72a4de3659 100644 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/move/MutableSolutionView.java +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/move/MutableSolutionView.java @@ -125,7 +125,7 @@ void changeVariable(PlanningVariableMetaModel @Nullable Value_ moveValueInList( + @Nullable Value_ swapValues( PlanningListVariableMetaModel variableMetaModel, Entity_ entity, int sourceIndex, int destinationIndex); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/move/director/MoveDirectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/move/director/MoveDirectorTest.java index 6040c49c4b..d6c008cc89 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/move/director/MoveDirectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/move/director/MoveDirectorTest.java @@ -120,7 +120,7 @@ void readListVariable() { } @Test - void moveValueInList() { + void swapValues() { var solutionMetaModel = TestdataListSolution.buildSolutionDescriptor() .getMetaModel(); var variableMetaModel = solutionMetaModel.entity(TestdataListEntity.class) @@ -137,7 +137,7 @@ void moveValueInList() { // Swap between second and last position. var mockScoreDirector = (InnerScoreDirector) mock(InnerScoreDirector.class); var moveDirector = new MoveDirector<>(mockScoreDirector).ephemeral(); - moveDirector.moveValueInList(variableMetaModel, entity, 1, 2); + moveDirector.swapValues(variableMetaModel, entity, 1, 2); assertThat(entity.getValueList()).containsExactly(expectedValue1, expectedValue3, expectedValue2); verify(mockScoreDirector).beforeListVariableChanged(variableDescriptor, entity, 1, 3); verify(mockScoreDirector).afterListVariableChanged(variableDescriptor, entity, 1, 3); @@ -151,7 +151,7 @@ void moveValueInList() { // Do the same in reverse. moveDirector = new MoveDirector<>(mockScoreDirector).ephemeral(); - moveDirector.moveValueInList(variableMetaModel, entity, 2, 1); + moveDirector.swapValues(variableMetaModel, entity, 2, 1); assertThat(entity.getValueList()).containsExactly(expectedValue1, expectedValue3, expectedValue2); verify(mockScoreDirector).beforeListVariableChanged(variableDescriptor, entity, 1, 3); verify(mockScoreDirector).afterListVariableChanged(variableDescriptor, entity, 1, 3); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java index dba6a02ccf..d528252162 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java @@ -1,32 +1,5 @@ package ai.timefold.solver.core.impl.solver; -import static ai.timefold.solver.core.config.heuristic.selector.entity.EntitySorterManner.DECREASING_DIFFICULTY; -import static ai.timefold.solver.core.config.heuristic.selector.entity.EntitySorterManner.DECREASING_DIFFICULTY_IF_AVAILABLE; -import static ai.timefold.solver.core.config.solver.PreviewFeature.DECLARATIVE_SHADOW_VARIABLES; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.fail; -import static org.assertj.core.api.SoftAssertions.assertSoftly; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.UUID; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.BooleanSupplier; -import java.util.stream.IntStream; - import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.score.calculator.ConstraintMatchAwareIncrementalScoreCalculator; @@ -81,6 +54,7 @@ import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionFilter; import ai.timefold.solver.core.impl.heuristic.selector.move.generic.ChangeMove; import ai.timefold.solver.core.impl.move.streams.maybeapi.generic.provider.ChangeMoveProvider; +import ai.timefold.solver.core.impl.move.streams.maybeapi.generic.provider.ListChangeMoveProvider; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.MoveProvider; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.MoveProviders; import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleListenerAdapter; @@ -90,7 +64,6 @@ import ai.timefold.solver.core.impl.score.constraint.DefaultIndictment; import ai.timefold.solver.core.impl.util.Pair; import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningSolutionMetaModel; -import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningVariableMetaModel; import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataSolution; import ai.timefold.solver.core.testdomain.TestdataValue; @@ -146,11 +119,14 @@ import ai.timefold.solver.core.testutil.AbstractMeterTest; import ai.timefold.solver.core.testutil.NoChangeCustomPhaseCommand; import ai.timefold.solver.core.testutil.PlannerTestUtils; - +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tags; import org.assertj.core.api.Assertions; import org.assertj.core.api.SoftAssertions; import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; @@ -158,9 +134,33 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import io.micrometer.core.instrument.Meter; -import io.micrometer.core.instrument.Metrics; -import io.micrometer.core.instrument.Tags; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.LongAdder; +import java.util.function.BooleanSupplier; +import java.util.stream.IntStream; + +import static ai.timefold.solver.core.config.heuristic.selector.entity.EntitySorterManner.DECREASING_DIFFICULTY; +import static ai.timefold.solver.core.config.heuristic.selector.entity.EntitySorterManner.DECREASING_DIFFICULTY_IF_AVAILABLE; +import static ai.timefold.solver.core.config.solver.PreviewFeature.DECLARATIVE_SHADOW_VARIABLES; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.fail; +import static org.assertj.core.api.SoftAssertions.assertSoftly; @ExtendWith(SoftAssertionsExtension.class) class DefaultSolverTest extends AbstractMeterTest { @@ -193,12 +193,30 @@ void solveWithMoveStreams() { var solution = TestdataSolution.generateSolution(3, 2); - solution = PlannerTestUtils.solve(solverConfig, solution, false); + solution = PlannerTestUtils.solve(solverConfig, solution, true); assertThat(solution).isNotNull(); assertThat(solution.getEntityList().stream() .filter(e -> e.getValue() == null)).isEmpty(); } + @Test + void solveListVarWithMoveStreams() { + var solverConfig = new SolverConfig() + .withPreviewFeature(PreviewFeature.MOVE_STREAMS) + .withSolutionClass(TestdataListSolution.class) + .withEntityClasses(TestdataListEntity.class, TestdataListValue.class) + .withEasyScoreCalculatorClass(TestingListEasyScoreCalculator.class) + .withTerminationConfig(new TerminationConfig() + .withBestScoreLimit("0")) // Should get there quickly. + .withPhases(new LocalSearchPhaseConfig() + .withMoveProvidersClass(TestingListMoveProviders.class)); + + var solution = TestdataListSolution.generateInitializedSolution(3, 2); + + solution = PlannerTestUtils.solve(solverConfig, solution, false); + assertThat(solution).isNotNull(); + } + @Test void solveWithMoveStreamsNotEnabled() { var solverConfig = new SolverConfig() // Preview feature not enabled. @@ -2367,13 +2385,13 @@ public void afterEntityRemoved(@NonNull Object entity) { } } + @NullMarked public static final class TestingMoveProviders implements MoveProviders { @Override public List> defineMoves(PlanningSolutionMetaModel solutionMetaModel) { var variableMetamodel = solutionMetaModel.entity(TestdataEntity.class) - . genuineVariable("value"); - return List.of(new ChangeMoveProvider<>( - (PlanningVariableMetaModel) variableMetamodel)); + . planningVariable("value"); + return List.of(new ChangeMoveProvider<>(variableMetamodel)); } } @@ -2397,4 +2415,35 @@ public static final class TestingEasyScoreCalculator implements EasyScoreCalcula } + @NullMarked + public static final class TestingListMoveProviders implements MoveProviders { + @Override + public List> defineMoves(PlanningSolutionMetaModel solutionMetaModel) { + var variableMetamodel = solutionMetaModel.entity(TestdataListEntity.class) + .planningListVariable("valueList"); + return List.of(new ListChangeMoveProvider<>(variableMetamodel)); + } + } + + /** + * Penalizes the number of values in the list, exponentially. + * Only penalizes is length of the list is greater than 1. + */ + public static final class TestingListEasyScoreCalculator implements EasyScoreCalculator { + + @Override + public @NonNull SimpleScore calculateScore(@NonNull TestdataListSolution testdataSolution) { + var sum = new LongAdder(); + testdataSolution.getEntityList().forEach(e -> { + var size = e.getValueList().size(); + if (size > 1) { + var penalty = Math.pow(size - 1, 2); + sum.add((long) penalty); + } + }); + return SimpleScore.of(-sum.intValue()); + } + + } + } From fbdc865a33a3435c4356bdeaf11d6e1ee1609293 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Wed, 6 Aug 2025 13:08:08 +0200 Subject: [PATCH 07/18] First working test --- .../dataset/common/AbstractDataStream.java | 7 +- .../maybeapi/generic/move/ListAssignMove.java | 36 +- .../maybeapi/generic/move/ListChangeMove.java | 41 +- .../generic/move/ListUnassignMove.java | 25 +- .../provider/ListChangeMoveProvider.java | 18 +- .../metamodel/PlanningEntityMetaModel.java | 23 +- .../provider/ChangeMoveProviderTest.java | 10 +- .../provider/ListChangeMoveProviderTest.java | 402 ++++++++++++++++++ .../testdomain/list/TestdataListEntity.java | 12 +- 9 files changed, 520 insertions(+), 54 deletions(-) create mode 100644 core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ListChangeMoveProviderTest.java diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDataStream.java index b9ac12f182..6b40ff85c8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDataStream.java @@ -29,7 +29,12 @@ public final > Stream_ shareAndAdd } protected boolean guaranteesDistinct() { - return true; // Default implementation, can be overridden by subclasses. + if (parent != null) { + // It is generally safe to take this from the parent; if the stream disagrees, it may override. + return parent.guaranteesDistinct(); + } else { // Streams need to explicitly opt-in by overriding this method. + return false; + } } // ************************************************************************ diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListAssignMove.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListAssignMove.java index 599583df70..e47067c006 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListAssignMove.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListAssignMove.java @@ -1,17 +1,17 @@ package ai.timefold.solver.core.impl.move.streams.maybeapi.generic.move; -import java.util.Collection; -import java.util.List; -import java.util.Objects; - import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningListVariableMetaModel; import ai.timefold.solver.core.preview.api.domain.metamodel.VariableMetaModel; import ai.timefold.solver.core.preview.api.move.Move; import ai.timefold.solver.core.preview.api.move.MutableSolutionView; import ai.timefold.solver.core.preview.api.move.Rebaser; +import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.NonNull; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +@NullMarked public final class ListAssignMove extends AbstractMove { private final PlanningListVariableMetaModel variableMetaModel; @@ -31,23 +31,23 @@ public ListAssignMove(PlanningListVariableMetaModel } @Override - public void execute(@NonNull MutableSolutionView mutableSolutionView) { + public void execute(MutableSolutionView mutableSolutionView) { mutableSolutionView.assignValue(variableMetaModel, planningValue, destinationEntity, destinationIndex); } @Override - public @NonNull Move rebase(@NonNull Rebaser rebaser) { - return new ListAssignMove<>(variableMetaModel, rebaser.rebase(planningValue), - rebaser.rebase(destinationEntity), destinationIndex); + public Move rebase(Rebaser rebaser) { + return new ListAssignMove<>(variableMetaModel, Objects.requireNonNull(rebaser.rebase(planningValue)), + Objects.requireNonNull(rebaser.rebase(destinationEntity)), destinationIndex); } @Override - public @NonNull Collection extractPlanningEntities() { + public Collection extractPlanningEntities() { return List.of(destinationEntity); } @Override - public @NonNull Collection extractPlanningValues() { + public Collection extractPlanningValues() { return List.of(planningValue); } @@ -56,8 +56,20 @@ public void execute(@NonNull MutableSolutionView mutableSolutionView) return List.of(variableMetaModel); } + public Value_ getPlanningValue() { + return planningValue; + } + + public Entity_ getDestinationEntity() { + return destinationEntity; + } + + public int getDestinationIndex() { + return destinationIndex; + } + @Override - public @NonNull String toString() { + public String toString() { return String.format("%s {null -> %s[%d]}", planningValue, destinationEntity, destinationIndex); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListChangeMove.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListChangeMove.java index b7e9f3cc30..690cd113b3 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListChangeMove.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListChangeMove.java @@ -6,7 +6,8 @@ import ai.timefold.solver.core.preview.api.domain.metamodel.VariableMetaModel; import ai.timefold.solver.core.preview.api.move.MutableSolutionView; import ai.timefold.solver.core.preview.api.move.Rebaser; -import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import java.util.Collection; import java.util.Collections; @@ -22,6 +23,7 @@ * * @param the solution type, the class with the {@link PlanningSolution} annotation */ +@NullMarked public final class ListChangeMove extends AbstractMove { private final PlanningListVariableMetaModel variableMetaModel; @@ -30,7 +32,7 @@ public final class ListChangeMove extends AbstractMo private final Entity_ destinationEntity; private final int destinationIndex; - private Value_ planningValue; + private @Nullable Value_ planningValue; /** * The move removes a planning value element from {@code sourceEntity.listVariable[sourceIndex]} @@ -118,7 +120,7 @@ private Value_ getMovedValue() { // ************************************************************************ @Override - public void execute(@NonNull MutableSolutionView solutionView) { + public void execute(MutableSolutionView solutionView) { if (sourceEntity == destinationEntity) { planningValue = solutionView.swapValues(variableMetaModel, sourceEntity, sourceIndex, destinationIndex); } else { @@ -128,14 +130,14 @@ public void execute(@NonNull MutableSolutionView solutionView) { } @Override - public @NonNull ListChangeMove rebase(@NonNull Rebaser rebaser) { + public ListChangeMove rebase(Rebaser rebaser) { return new ListChangeMove<>(variableMetaModel, - rebaser.rebase(sourceEntity), sourceIndex, - rebaser.rebase(destinationEntity), destinationIndex); + Objects.requireNonNull(rebaser.rebase(sourceEntity)), sourceIndex, + Objects.requireNonNull(rebaser.rebase(destinationEntity)), destinationIndex); } @Override - public @NonNull Collection extractPlanningEntities() { + public Collection extractPlanningEntities() { if (sourceEntity == destinationEntity) { return Collections.singleton(sourceEntity); } else { @@ -144,8 +146,8 @@ public void execute(@NonNull MutableSolutionView solutionView) { } @Override - public @NonNull Collection extractPlanningValues() { - return Collections.singleton(planningValue); + public Collection extractPlanningValues() { + return Collections.singleton(getMovedValue()); } @Override @@ -153,6 +155,22 @@ public void execute(@NonNull MutableSolutionView solutionView) { return List.of(variableMetaModel); } + public Entity_ getSourceEntity() { + return sourceEntity; + } + + public int getSourceIndex() { + return sourceIndex; + } + + public Entity_ getDestinationEntity() { + return destinationEntity; + } + + public int getDestinationIndex() { + return destinationIndex; + } + @Override public boolean equals(Object o) { if (this == o) @@ -160,7 +178,8 @@ public boolean equals(Object o) { if (!(o instanceof ListChangeMove that)) return false; return sourceIndex == that.sourceIndex && destinationIndex == that.destinationIndex - && Objects.equals(variableMetaModel, that.variableMetaModel) && Objects.equals(sourceEntity, that.sourceEntity) + && Objects.equals(variableMetaModel, that.variableMetaModel) + && Objects.equals(sourceEntity, that.sourceEntity) && Objects.equals(destinationEntity, that.destinationEntity); } @@ -170,7 +189,7 @@ public int hashCode() { } @Override - public @NonNull String toString() { + public String toString() { return String.format("%s {%s[%d] -> %s[%d]}", getMovedValue(), sourceEntity, sourceIndex, destinationEntity, destinationIndex); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListUnassignMove.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListUnassignMove.java index 927111d709..c010cdee43 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListUnassignMove.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListUnassignMove.java @@ -5,13 +5,14 @@ import ai.timefold.solver.core.preview.api.move.Move; import ai.timefold.solver.core.preview.api.move.MutableSolutionView; import ai.timefold.solver.core.preview.api.move.Rebaser; -import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullMarked; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; +@NullMarked public final class ListUnassignMove extends AbstractMove { private final PlanningListVariableMetaModel variableMetaModel; @@ -31,23 +32,23 @@ public ListUnassignMove(PlanningListVariableMetaModel solutionView) { + public void execute(MutableSolutionView solutionView) { solutionView.unassignValue(variableMetaModel, movedValue, sourceEntity, sourceIndex); } @Override - public @NonNull Move rebase(@NonNull Rebaser rebaser) { - return new ListUnassignMove<>(variableMetaModel, rebaser.rebase(movedValue), rebaser.rebase(sourceEntity), - sourceIndex); + public Move rebase(Rebaser rebaser) { + return new ListUnassignMove<>(variableMetaModel, Objects.requireNonNull(rebaser.rebase(movedValue)), + Objects.requireNonNull(rebaser.rebase(sourceEntity)), sourceIndex); } @Override - public @NonNull Collection extractPlanningEntities() { + public Collection extractPlanningEntities() { return Collections.singleton(sourceEntity); } @Override - public @NonNull Collection extractPlanningValues() { + public Collection extractPlanningValues() { return Collections.singleton(movedValue); } @@ -56,6 +57,14 @@ public void execute(@NonNull MutableSolutionView solutionView) { return List.of(variableMetaModel); } + public Entity_ getSourceEntity() { + return sourceEntity; + } + + public int getSourceIndex() { + return sourceIndex; + } + @Override public boolean equals(Object o) { if (this == o) @@ -72,7 +81,7 @@ public int hashCode() { } @Override - public @NonNull String toString() { + public String toString() { return String.format("%s {%s[%d] -> null}", movedValue, sourceEntity, sourceIndex); } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java index fa0dfa49e3..d139f0675e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java @@ -36,17 +36,17 @@ public ListChangeMoveProvider(PlanningListVariableMetaModel { var currentPosition = solutionView.getPositionOf(variableMetaModel, Objects.requireNonNull(value)); - if (currentPosition.equals(targetPosition)) { // The target position is the same as the current position. - return false; - } if (!(currentPosition instanceof PositionInList currentPositionInList)) { - // The current position is unassigned, so we can assign the value. - return true; + // The current position is unassigned, which is acceptable if we can assign it to a list. + return targetPosition instanceof PositionInList; } if (!(targetPosition instanceof PositionInList targetPositionInList)) { - // The target position is unassigned, so we can unassign the value. + // The target position is unassigned, which is only acceptable if we can unassign the value. return true; } + if (Objects.equals(currentPositionInList, targetPositionInList)) { + return false; // No change in position, so no need to create a move. + } if (currentPositionInList.entity() != targetPositionInList.entity()) { return true; // Different entities, so we can freely change the position. } @@ -76,8 +76,10 @@ public MoveProducer apply(MoveStreamFactory moveStreamFact .map((solutionView, entity, value) -> { if (entity == null) { // This will trigger unassignment of the value. return ElementPosition.unassigned(); - } else if (value == null) { // This will trigger assignment of the value at the end of the list. - return ElementPosition.of(entity, solutionView.countValues(variableMetaModel, entity)); + } + var valueCount = solutionView.countValues(variableMetaModel, entity); + if (value == null || valueCount == 0) { // This will trigger assignment of the value at the end of the list. + return ElementPosition.of(entity, valueCount); } else { // This will trigger assignment of the value immediately before this value. return solutionView.getPositionOf(variableMetaModel, value); } diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/PlanningEntityMetaModel.java b/core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/PlanningEntityMetaModel.java index 5d1f25449d..2863df5abd 100644 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/PlanningEntityMetaModel.java +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/PlanningEntityMetaModel.java @@ -1,13 +1,12 @@ package ai.timefold.solver.core.preview.api.domain.metamodel; -import java.util.List; - import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.domain.variable.PlanningListVariable; import ai.timefold.solver.core.api.domain.variable.PlanningVariable; - import org.jspecify.annotations.NullMarked; +import java.util.List; + /** * Represents the meta-model of an entity. * Gives access to the entity's variable meta-models. @@ -131,6 +130,15 @@ default boolean hasVariable(String variableName) { return false; } + /** + * As defined by {@link #genuineVariable()} ()}, + * but only succeeds if the variable is a {@link PlanningVariable basic planning variable}. + */ + @SuppressWarnings("unchecked") + default PlanningVariableMetaModel planningVariable() { + return (PlanningVariableMetaModel) genuineVariable(); + } + /** * As defined by {@link #variable(String)}, * but only succeeds if the variable is a {@link PlanningVariable basic planning variable}. @@ -140,6 +148,15 @@ default PlanningVariableMetaModel planningV return (PlanningVariableMetaModel) variable(variableName); } + /** + * As defined by {@link #genuineVariable()}, + * but only succeeds if the variable is a {@link PlanningListVariable planning list variable}. + */ + @SuppressWarnings("unchecked") + default PlanningListVariableMetaModel planningListVariable() { + return (PlanningListVariableMetaModel) genuineVariable(); + } + /** * As defined by {@link #variable(String)}, * but only succeeds if the variable is a {@link PlanningListVariable planning list variable}. diff --git a/core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ChangeMoveProviderTest.java b/core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ChangeMoveProviderTest.java index 3770adc6b3..6018487c52 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ChangeMoveProviderTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ChangeMoveProviderTest.java @@ -37,7 +37,7 @@ class ChangeMoveProviderTest { @Test - void fromSolutionBasicVariable() { + void fromSolution() { var solutionDescriptor = TestdataSolution.buildSolutionDescriptor(); var variableMetaModel = solutionDescriptor.getMetaModel() .entity(TestdataEntity.class) @@ -100,7 +100,7 @@ void fromSolutionBasicVariable() { } @Test - void fromSolutionBasicVariableIncompleteValueRange() { + void fromSolutionIncompleteValueRange() { var solutionDescriptor = TestdataIncompleteValueRangeSolution.buildSolutionDescriptor(); var variableMetaModel = solutionDescriptor.getMetaModel() .entity(TestdataIncompleteValueRangeEntity.class) @@ -168,7 +168,7 @@ void fromSolutionBasicVariableIncompleteValueRange() { } @Test - void fromEntityBasicVariable() { + void fromEntity() { var solutionDescriptor = TestdataEntityProvidingSolution.buildSolutionDescriptor(); var variableMetaModel = solutionDescriptor.getMetaModel() .entity(TestdataEntityProvidingEntity.class) @@ -209,7 +209,7 @@ void fromEntityBasicVariable() { } @Test - void fromEntityBasicVariableAllowsUnassigned() { + void fromEntityAllowsUnassigned() { var solutionDescriptor = TestdataAllowsUnassignedEntityProvidingSolution.buildSolutionDescriptor(); var variableMetaModel = solutionDescriptor.getMetaModel() .entity(TestdataAllowsUnassignedEntityProvidingEntity.class) @@ -271,7 +271,7 @@ void fromEntityBasicVariableAllowsUnassigned() { } @Test - void fromSolutionBasicVariableAllowsUnassigned() { + void fromSolutionAllowsUnassigned() { var solutionDescriptor = TestdataAllowsUnassignedSolution.buildSolutionDescriptor(); var variableMetaModel = solutionDescriptor.getMetaModel() .entity(TestdataAllowsUnassignedEntity.class) diff --git a/core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ListChangeMoveProviderTest.java b/core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ListChangeMoveProviderTest.java new file mode 100644 index 0000000000..58592f9db5 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ListChangeMoveProviderTest.java @@ -0,0 +1,402 @@ +package ai.timefold.solver.core.impl.move.streams.maybeapi.provider; + +import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; +import ai.timefold.solver.core.api.score.stream.Constraint; +import ai.timefold.solver.core.api.score.stream.ConstraintFactory; +import ai.timefold.solver.core.api.score.stream.ConstraintProvider; +import ai.timefold.solver.core.config.solver.EnvironmentMode; +import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.move.streams.DefaultMoveStreamFactory; +import ai.timefold.solver.core.impl.move.streams.maybeapi.generic.move.ListAssignMove; +import ai.timefold.solver.core.impl.move.streams.maybeapi.generic.move.ListChangeMove; +import ai.timefold.solver.core.impl.move.streams.maybeapi.generic.move.ListUnassignMove; +import ai.timefold.solver.core.impl.move.streams.maybeapi.generic.provider.ListChangeMoveProvider; +import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.MoveStreamSession; +import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; +import ai.timefold.solver.core.impl.score.director.SessionContext; +import ai.timefold.solver.core.impl.score.director.stream.BavetConstraintStreamScoreDirectorFactory; +import ai.timefold.solver.core.testdomain.list.TestdataListEntity; +import ai.timefold.solver.core.testdomain.list.TestdataListSolution; +import ai.timefold.solver.core.testdomain.list.TestdataListValue; +import org.jspecify.annotations.NonNull; +import org.junit.jupiter.api.Test; + +import java.util.stream.StreamSupport; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +class ListChangeMoveProviderTest { + + @Test + void fromSolution() { + var solutionDescriptor = TestdataListSolution.buildSolutionDescriptor(); + var variableMetaModel = solutionDescriptor.getMetaModel() + .entity(TestdataListEntity.class) + .planningListVariable(); + + var solution = TestdataListSolution.generateUninitializedSolution(2, 2); + var emptyEntity = solution.getEntityList().get(0); + var entityWithOneValue = solution.getEntityList().get(1); + var unassignedValue = solution.getValueList().get(0); + var initiallyAssignedValue1 = solution.getValueList().get(1); + entityWithOneValue.getValueList().add(initiallyAssignedValue1); + solution.getEntityList().forEach(TestdataListEntity::setUpShadowVariables); + + var scoreDirector = createScoreDirector(solutionDescriptor, new TestdataListConstraintProvider(), solution); + + var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); + var moveProvider = new ListChangeMoveProvider<>(variableMetaModel); + var moveProducer = moveProvider.apply(moveStreamFactory); + var moveStreamSession = createSession(moveStreamFactory, scoreDirector); + + var moveIterable = moveProducer.getMoveIterable(moveStreamSession); + assertThat(moveIterable).hasSize(4); + + var moveList = StreamSupport.stream(moveIterable.spliterator(), false) + .toList(); + assertThat(moveList).hasSize(4); + + var firstMove = (ListUnassignMove) moveList.get(0); + assertSoftly(softly -> { + softly.assertThat(firstMove.getSourceEntity()).isEqualTo(entityWithOneValue); + softly.assertThat(firstMove.getSourceIndex()).isEqualTo(0); + softly.assertThat(firstMove.extractPlanningEntities()) + .containsExactly(entityWithOneValue); + softly.assertThat(firstMove.extractPlanningValues()) + .containsExactly(initiallyAssignedValue1); + }); + + var secondMove = (ListChangeMove) moveList.get(1); + assertSoftly(softly -> { + softly.assertThat(secondMove.getSourceEntity()).isEqualTo(entityWithOneValue); + softly.assertThat(secondMove.getSourceIndex()).isEqualTo(0); + softly.assertThat(secondMove.getDestinationEntity()).isEqualTo(emptyEntity); + softly.assertThat(secondMove.getDestinationIndex()).isEqualTo(0); + softly.assertThat(secondMove.extractPlanningEntities()) + .containsExactly(entityWithOneValue, emptyEntity); + softly.assertThat(secondMove.extractPlanningValues()) + .containsExactly(initiallyAssignedValue1); + }); + + var thirdMove = (ListAssignMove) moveList.get(2); + assertSoftly(softly -> { + softly.assertThat(thirdMove.getDestinationEntity()).isEqualTo(emptyEntity); + softly.assertThat(thirdMove.getDestinationIndex()).isEqualTo(0); + softly.assertThat(thirdMove.extractPlanningEntities()) + .containsExactly(emptyEntity); + softly.assertThat(thirdMove.extractPlanningValues()) + .containsExactly(unassignedValue); + }); + + var fourthMove = (ListAssignMove) moveList.get(3); + assertSoftly(softly -> { + softly.assertThat(fourthMove.getDestinationEntity()).isEqualTo(entityWithOneValue); + softly.assertThat(fourthMove.getDestinationIndex()).isEqualTo(0); + softly.assertThat(fourthMove.extractPlanningEntities()) + .containsExactly(entityWithOneValue); + softly.assertThat(fourthMove.extractPlanningValues()) + .containsExactly(unassignedValue); + }); + } + + + /* + * @Test + * void fromSolutionIncompleteValueRange() { + * var solutionDescriptor = TestdataIncompleteValueRangeSolution.buildSolutionDescriptor(); + * var variableMetaModel = solutionDescriptor.getMetaModel() + * .entity(TestdataIncompleteValueRangeEntity.class) + * .genuineVariable() + * .ensurePlanningVariable(); + * + * // The point of this test is to ensure that the move provider skips values that are not in the value range. + * var solution = TestdataIncompleteValueRangeSolution.generateSolution(2, 2); + * var valueNotInValueRange = new TestdataValue("third"); + * solution.setValueListNotInValueRange(Collections.singletonList(valueNotInValueRange)); + * + * var firstEntity = solution.getEntityList().get(0); + * firstEntity.setValue(null); + * var secondEntity = solution.getEntityList().get(1); + * secondEntity.setValue(null); + * var firstValue = solution.getValueList().get(0); + * var secondValue = solution.getValueList().get(1); + * var scoreDirector = + * createScoreDirector(solutionDescriptor, new TestdataIncompleteValueRangeConstraintProvider(), solution); + * + * var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); + * var moveProvider = new ChangeMoveProvider<>(variableMetaModel); + * var moveProducer = moveProvider.apply(moveStreamFactory); + * var moveStreamSession = createSession(moveStreamFactory, scoreDirector); + * + * var moveIterable = moveProducer.getMoveIterable(moveStreamSession); + * assertThat(moveIterable).hasSize(4); + * + * var moveList = StreamSupport.stream(moveIterable.spliterator(), false) + * .map(m -> (ChangeMove) m) + * .toList(); + * assertThat(moveList).hasSize(4); + * + * var firstMove = moveList.get(0); + * assertSoftly(softly -> { + * softly.assertThat(firstMove.extractPlanningEntities()) + * .containsExactly(firstEntity); + * softly.assertThat(firstMove.extractPlanningValues()) + * .containsExactly(firstValue); + * }); + * + * var secondMove = moveList.get(1); + * assertSoftly(softly -> { + * softly.assertThat(secondMove.extractPlanningEntities()) + * .containsExactly(firstEntity); + * softly.assertThat(secondMove.extractPlanningValues()) + * .containsExactly(secondValue); + * }); + * + * var thirdMove = moveList.get(2); + * assertSoftly(softly -> { + * softly.assertThat(thirdMove.extractPlanningEntities()) + * .containsExactly(secondEntity); + * softly.assertThat(thirdMove.extractPlanningValues()) + * .containsExactly(firstValue); + * }); + * + * var fourthMove = moveList.get(3); + * assertSoftly(softly -> { + * softly.assertThat(fourthMove.extractPlanningEntities()) + * .containsExactly(secondEntity); + * softly.assertThat(fourthMove.extractPlanningValues()) + * .containsExactly(secondValue); + * }); + * } + * + * @Test + * void fromEntity() { + * var solutionDescriptor = TestdataEntityProvidingSolution.buildSolutionDescriptor(); + * var variableMetaModel = solutionDescriptor.getMetaModel() + * .entity(TestdataEntityProvidingEntity.class) + * .genuineVariable() + * .ensurePlanningVariable(); + * + * var solution = TestdataEntityProvidingSolution.generateSolution(2, 2); + * var firstEntity = solution.getEntityList().get(0); + * var secondEntity = solution.getEntityList().get(1); + * var firstValue = firstEntity.getValueRange().get(0); + * var scoreDirector = createScoreDirector(solutionDescriptor, new TestdataEntityProvidingConstraintProvider(), solution); + * + * var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); + * var moveProvider = new ChangeMoveProvider<>(variableMetaModel); + * var moveProducer = moveProvider.apply(moveStreamFactory); + * var moveStreamSession = createSession(moveStreamFactory, scoreDirector); + * + * // One move is expected: + * // - firstEntity is already assigned to firstValue, the only possible value; skip. + * // - Assign secondEntity to firstValue, + * // as it is currently assigned to secondValue, and the value range only contains firstValue. + * var moveIterable = moveProducer.getMoveIterable(moveStreamSession); + * assertThat(moveIterable).hasSize(1); + * + * var moveList = StreamSupport.stream(moveIterable.spliterator(), false) + * .map(m -> (ChangeMove) m) + * .toList(); + * assertThat(moveList).hasSize(1); + * + * var firstMove = moveList.get(0); + * assertSoftly(softly -> { + * softly.assertThat(firstMove.extractPlanningEntities()) + * .containsExactly(secondEntity); + * softly.assertThat(firstMove.extractPlanningValues()) + * .hasSize(1) + * .containsExactly(firstValue); + * }); + * } + * + * @Test + * void fromEntityAllowsUnassigned() { + * var solutionDescriptor = TestdataAllowsUnassignedEntityProvidingSolution.buildSolutionDescriptor(); + * var variableMetaModel = solutionDescriptor.getMetaModel() + * .entity(TestdataAllowsUnassignedEntityProvidingEntity.class) + * .genuineVariable() + * .ensurePlanningVariable(); + * + * var solution = TestdataAllowsUnassignedEntityProvidingSolution.generateSolution(2, 2); + * var firstEntity = solution.getEntityList().get(0); + * var secondEntity = solution.getEntityList().get(1); + * var firstValue = firstEntity.getValueRange().get(0); + * var scoreDirector = createScoreDirector(solutionDescriptor, + * new TestdataAllowsUnassignedEntityProvidingConstraintProvider(), solution); + * + * var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); + * var moveProvider = new ChangeMoveProvider<>(variableMetaModel); + * var moveProducer = moveProvider.apply(moveStreamFactory); + * var moveStreamSession = createSession(moveStreamFactory, scoreDirector); + * + * // Three moves are expected: + * // - Assign firstEntity to null, + * // as it is currently assigned to firstValue, and the value range only contains firstValue. + * // - Assign secondEntity to null and to firstValue, + * // as it is currently assigned to secondValue, and the value range only contains firstValue. + * // Null is not in the value range, but as documented, + * // null is added automatically to value ranges when allowsUnassigned is true. + * var moveIterable = moveProducer.getMoveIterable(moveStreamSession); + * assertThat(moveIterable).hasSize(3); + * + * var moveList = StreamSupport.stream(moveIterable.spliterator(), false) + * .map(m -> (ChangeMove) m) + * .toList(); + * assertThat(moveList).hasSize(3); + * + * var firstMove = moveList.get(0); + * assertSoftly(softly -> { + * softly.assertThat(firstMove.extractPlanningEntities()) + * .containsExactly(firstEntity); + * softly.assertThat(firstMove.extractPlanningValues()) + * .hasSize(1) + * .containsNull(); + * }); + * + * var secondMove = moveList.get(1); + * assertSoftly(softly -> { + * softly.assertThat(secondMove.extractPlanningEntities()) + * .containsExactly(secondEntity); + * softly.assertThat(secondMove.extractPlanningValues()) + * .hasSize(1) + * .containsNull(); + * }); + * + * var thirdMove = moveList.get(2); + * assertSoftly(softly -> { + * softly.assertThat(thirdMove.extractPlanningEntities()) + * .containsExactly(secondEntity); + * softly.assertThat(thirdMove.extractPlanningValues()) + * .containsExactly(firstValue); + * }); + * } + * + * @Test + * void fromSolutionAllowsUnassigned() { + * var solutionDescriptor = TestdataAllowsUnassignedSolution.buildSolutionDescriptor(); + * var variableMetaModel = solutionDescriptor.getMetaModel() + * .entity(TestdataAllowsUnassignedEntity.class) + * .genuineVariable() + * .ensurePlanningVariable(); + * var solution = TestdataAllowsUnassignedSolution.generateSolution(2, 2); + * var firstEntity = solution.getEntityList().get(0); // Assigned to null. + * var secondEntity = solution.getEntityList().get(1); // Assigned to secondValue. + * var firstValue = solution.getValueList().get(0); // Not assigned to any entity. + * var secondValue = solution.getValueList().get(1); + * var scoreDirector = createScoreDirector(solutionDescriptor, new TestdataAllowsUnassignedConstraintProvider(), solution); + * + * var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); + * var moveProvider = new ChangeMoveProvider<>(variableMetaModel); + * var moveProducer = moveProvider.apply(moveStreamFactory); + * var moveStreamSession = createSession(moveStreamFactory, scoreDirector); + * + * // Filters out moves that would change the value to the value the entity already has. + * // Therefore this will have 4 moves (2 entities * 2 values) as opposed to 6 (2 entities * 3 values). + * var moveIterable = moveProducer.getMoveIterable(moveStreamSession); + * assertThat(moveIterable).hasSize(4); + * + * var moveList = StreamSupport.stream(moveIterable.spliterator(), false) + * .map(m -> (ChangeMove) m) + * .toList(); + * assertThat(moveList).hasSize(4); + * + * // TODO There is a strange issue here that needs to be investigated, + * // as it potentially breaks difficulty comparators. + * + * // The node network receives: + * // firstEntity + null; filtered out + * // secondEntity + null + * // firstEntity + firstValue + * // secondEntity + firstValue + * // firstEntity + secondValue + * // secondEntity + secondValue; filtered out + * + * // Therefore the iterator receives: + * // secondEntity + null + * // firstEntity + firstValue + * // secondEntity + firstValue + * // firstEntity + secondValue + * + * // This means that secondEntity is actually encountered first, and therefore will be iterated first. + * // A strange behavior of original iteration when combined with dataset caching before picking, + * // where the node network (= cache) is fully built long before the iteration starts. + * // A possible fix would be to refactor the node network to first iterate right inputs + * // (values first in this case) + * // but wouldn't that just create a similar issue in other places? + * + * // Second entity is assigned to secondValue, therefore the applicable moves assign to null and firstValue. + * var firstMove = moveList.get(0); + * assertSoftly(softly -> { + * softly.assertThat(firstMove.extractPlanningEntities()) + * .containsExactly(secondEntity); + * softly.assertThat(firstMove.extractPlanningValues()) + * .containsExactly(new TestdataValue[] { null }); + * }); + * + * var secondMove = moveList.get(1); + * assertSoftly(softly -> { + * softly.assertThat(secondMove.extractPlanningEntities()) + * .containsExactly(secondEntity); + * softly.assertThat(secondMove.extractPlanningValues()) + * .containsExactly(firstValue); + * }); + * + * // First entity is assigned to null, therefore the applicable moves assign to firstValue and secondValue. + * var thirdMove = moveList.get(2); + * assertSoftly(softly -> { + * softly.assertThat(thirdMove.extractPlanningEntities()) + * .containsExactly(firstEntity); + * softly.assertThat(thirdMove.extractPlanningValues()) + * .containsExactly(firstValue); + * }); + * + * var fourthMove = moveList.get(3); + * assertSoftly(softly -> { + * softly.assertThat(fourthMove.extractPlanningEntities()) + * .containsExactly(firstEntity); + * softly.assertThat(fourthMove.extractPlanningValues()) + * .containsExactly(secondValue); + * }); + * + * } + * + */ + + private InnerScoreDirector createScoreDirector(SolutionDescriptor solutionDescriptor, + ConstraintProvider constraintProvider, Solution_ solution) { + var scoreDirectorFactory = + new BavetConstraintStreamScoreDirectorFactory<>(solutionDescriptor, constraintProvider, + EnvironmentMode.TRACKED_FULL_ASSERT); + var scoreDirector = scoreDirectorFactory.buildScoreDirector(); + scoreDirector.setWorkingSolution(solution); + return scoreDirector; + } + + private MoveStreamSession createSession(DefaultMoveStreamFactory moveStreamFactory, + InnerScoreDirector scoreDirector) { + var solution = scoreDirector.getWorkingSolution(); + var moveStreamSession = moveStreamFactory.createSession(new SessionContext<>(scoreDirector)); + scoreDirector.getSolutionDescriptor().visitAll(solution, moveStreamSession::insert); + moveStreamSession.settle(); + return moveStreamSession; + } + + public final class TestdataListConstraintProvider implements ConstraintProvider { + + @Override + public Constraint @NonNull [] defineConstraints(@NonNull ConstraintFactory constraintFactory) { + return new Constraint[] { alwaysPenalizingConstraint(constraintFactory) }; + } + + private Constraint alwaysPenalizingConstraint(ConstraintFactory constraintFactory) { + return constraintFactory.forEach(TestdataListValue.class) + .penalize(SimpleScore.ONE) + .asConstraint("Always penalize"); + } + + } + +} \ No newline at end of file diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListEntity.java index a6e08ff191..af07edbe5f 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListEntity.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListEntity.java @@ -1,16 +1,16 @@ package ai.timefold.solver.core.testdomain.list; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; - import ai.timefold.solver.core.api.domain.entity.PlanningEntity; import ai.timefold.solver.core.api.domain.variable.PlanningListVariable; import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.testdomain.TestdataObject; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + @PlanningEntity public class TestdataListEntity extends TestdataObject { @@ -27,7 +27,7 @@ public static TestdataListEntity createWithValues(String code, TestdataListValue return new TestdataListEntity(code, values).setUpShadowVariables(); } - TestdataListEntity setUpShadowVariables() { + public TestdataListEntity setUpShadowVariables() { valueList.forEach(testdataListValue -> { testdataListValue.setEntity(this); testdataListValue.setIndex(valueList.indexOf(testdataListValue)); From 58df66341760afaeba55aea0941066e3722ad382 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Wed, 6 Aug 2025 16:28:47 +0200 Subject: [PATCH 08/18] Second working test --- .../bavet/uni/AbstractForEachUniNode.java | 7 +- .../uni/AbstractForEachDataStream.java | 16 +- .../uni/ForEachFromSolutionDataStream.java | 8 +- .../uni/ForEachIncludingPinnedDataStream.java | 11 +- .../provider/ListChangeMoveProvider.java | 65 ++++---- .../provider/ListChangeMoveProviderTest.java | 146 +++++++++++++++--- .../TestdataListEntityProvidingEntity.java | 14 +- .../TestdataListEntityProvidingSolution.java | 14 +- 8 files changed, 196 insertions(+), 85 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractForEachUniNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractForEachUniNode.java index 116e705e22..6d4c6b6d34 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractForEachUniNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractForEachUniNode.java @@ -1,8 +1,5 @@ package ai.timefold.solver.core.impl.bavet.uni; -import java.util.IdentityHashMap; -import java.util.Map; - import ai.timefold.solver.core.impl.bavet.common.AbstractNode; import ai.timefold.solver.core.impl.bavet.common.Propagator; import ai.timefold.solver.core.impl.bavet.common.StaticPropagationQueue; @@ -10,10 +7,12 @@ import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; import ai.timefold.solver.core.impl.score.director.SessionContext; - import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; +import java.util.IdentityHashMap; +import java.util.Map; + /** * Filtering nodes are expensive. * Considering that most streams start with a nullity check on genuine planning variables, diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractForEachDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractForEachDataStream.java index fdf1dabab2..f8e3425fae 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractForEachDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractForEachDataStream.java @@ -1,10 +1,5 @@ package ai.timefold.solver.core.impl.move.streams.dataset.uni; -import static ai.timefold.solver.core.impl.bavet.uni.AbstractForEachUniNode.LifecycleOperation; - -import java.util.Objects; -import java.util.Set; - import ai.timefold.solver.core.impl.bavet.common.TupleSource; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; @@ -12,9 +7,13 @@ import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; - import org.jspecify.annotations.NullMarked; +import java.util.Objects; +import java.util.Set; + +import static ai.timefold.solver.core.impl.bavet.uni.AbstractForEachUniNode.LifecycleOperation; + @NullMarked abstract sealed class AbstractForEachDataStream extends AbstractUniDataStream @@ -22,10 +21,9 @@ abstract sealed class AbstractForEachDataStream permits ForEachIncludingPinnedDataStream, ForEachFromSolutionDataStream { protected final Class forEachClass; - private final boolean shouldIncludeNull; + final boolean shouldIncludeNull; - protected AbstractForEachDataStream(DataStreamFactory dataStreamFactory, Class forEachClass, - boolean includeNull) { + protected AbstractForEachDataStream(DataStreamFactory dataStreamFactory, Class forEachClass, boolean includeNull) { super(dataStreamFactory, null); this.forEachClass = Objects.requireNonNull(forEachClass); this.shouldIncludeNull = includeNull; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachFromSolutionDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachFromSolutionDataStream.java index dd2fc9afcd..15486ff78b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachFromSolutionDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachFromSolutionDataStream.java @@ -1,17 +1,16 @@ package ai.timefold.solver.core.impl.move.streams.dataset.uni; -import java.util.Objects; - import ai.timefold.solver.core.impl.bavet.common.TupleSource; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; import ai.timefold.solver.core.impl.bavet.uni.AbstractForEachUniNode; import ai.timefold.solver.core.impl.bavet.uni.ForEachFromSolutionUniNode; import ai.timefold.solver.core.impl.domain.valuerange.descriptor.ValueRangeDescriptor; - import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import org.jspecify.annotations.NullMarked; +import java.util.Objects; + @NullMarked public final class ForEachFromSolutionDataStream extends AbstractForEachDataStream @@ -34,13 +33,14 @@ protected AbstractForEachUniNode getNode(TupleLifecycle> tupleLif @Override public boolean equals(Object o) { return o instanceof ForEachFromSolutionDataStream that && + Objects.equals(shouldIncludeNull, that.shouldIncludeNull) && Objects.equals(forEachClass, that.forEachClass) && Objects.equals(valueRangeDescriptor, that.valueRangeDescriptor); } @Override public int hashCode() { - return Objects.hash(forEachClass, valueRangeDescriptor); + return Objects.hash(shouldIncludeNull, forEachClass, valueRangeDescriptor); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachIncludingPinnedDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachIncludingPinnedDataStream.java index 2d7f74d5c8..b879c81dde 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachIncludingPinnedDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachIncludingPinnedDataStream.java @@ -1,23 +1,21 @@ package ai.timefold.solver.core.impl.move.streams.dataset.uni; -import java.util.Objects; - import ai.timefold.solver.core.impl.bavet.common.TupleSource; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; import ai.timefold.solver.core.impl.bavet.uni.AbstractForEachUniNode; import ai.timefold.solver.core.impl.bavet.uni.ForEachIncludingUnassignedUniNode; - import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import org.jspecify.annotations.NullMarked; +import java.util.Objects; + @NullMarked public final class ForEachIncludingPinnedDataStream extends AbstractForEachDataStream implements TupleSource { - public ForEachIncludingPinnedDataStream(DataStreamFactory dataStreamFactory, Class forEachClass, - boolean includeNull) { + public ForEachIncludingPinnedDataStream(DataStreamFactory dataStreamFactory, Class forEachClass, boolean includeNull) { super(dataStreamFactory, forEachClass, includeNull); } @@ -29,12 +27,13 @@ protected AbstractForEachUniNode getNode(TupleLifecycle> tupleLif @Override public boolean equals(Object o) { return o instanceof ForEachIncludingPinnedDataStream that && + Objects.equals(shouldIncludeNull, that.shouldIncludeNull) && Objects.equals(forEachClass, that.forEachClass); } @Override public int hashCode() { - return forEachClass.hashCode(); + return Objects.hash(shouldIncludeNull, forEachClass); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java index d139f0675e..b99c20d444 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java @@ -22,41 +22,43 @@ public class ListChangeMoveProvider private final PlanningListVariableMetaModel variableMetaModel; private final BiDataFilter isValueInListFilter; - private final BiDataFilter noChangeDetectionFilter; + private final BiDataFilter validChangeFilter; public ListChangeMoveProvider(PlanningListVariableMetaModel variableMetaModel) { this.variableMetaModel = Objects.requireNonNull(variableMetaModel); this.isValueInListFilter = (solution, entity, value) -> { - if (entity == null) { - // Necessary for the null entity to survive until the later stage, - // where we will use it as a marker to unassigned the value. + if (entity == null || value == null) { + // Necessary for the null to survive until the later stage, + // where we will use it as a marker to unassign the value. return true; } return solution.isValueInRange(variableMetaModel, entity, value); }; - this.noChangeDetectionFilter = (solutionView, value, targetPosition) -> { - var currentPosition = solutionView.getPositionOf(variableMetaModel, Objects.requireNonNull(value)); - if (!(currentPosition instanceof PositionInList currentPositionInList)) { - // The current position is unassigned, which is acceptable if we can assign it to a list. - return targetPosition instanceof PositionInList; + this.validChangeFilter = (solutionView, value, targetPosition) -> { + var currentPosition = solutionView.getPositionOf(variableMetaModel, value); + if (currentPosition.equals(targetPosition)) { + return false; } - if (!(targetPosition instanceof PositionInList targetPositionInList)) { - // The target position is unassigned, which is only acceptable if we can unassign the value. - return true; - } - if (Objects.equals(currentPositionInList, targetPositionInList)) { - return false; // No change in position, so no need to create a move. - } - if (currentPositionInList.entity() != targetPositionInList.entity()) { - return true; // Different entities, so we can freely change the position. - } - var listLength = solutionView.countValues(variableMetaModel, currentPositionInList.entity()); - if (listLength == 1) { - return false; // No need to change the position, as the value is the only one in this list. + if (currentPosition instanceof UnassignedElement) { + var targetPositionInList = targetPosition.ensureAssigned(); + return solutionView.isValueInRange(variableMetaModel, targetPositionInList.entity(), value); + } else { + if (!(targetPosition instanceof PositionInList targetPositionInList)) { // Unassigning a value. + return true; + } + var currentPositionInList = currentPosition.ensureAssigned(); + if (currentPositionInList.entity() == targetPositionInList.entity()) { + var valueCount = solutionView.countValues(variableMetaModel, currentPositionInList.entity()); + if (valueCount == 1) { + return false; // The value is already in the list, and it is the only one. + } else { + return currentPositionInList.index() != targetPositionInList.index(); + } + } else { + // We can move freely between entities. + return solutionView.isValueInRange(variableMetaModel, targetPositionInList.entity(), value); + } } - // Make sure we're not moving the value past the list end. - // This would happen if the value was already at the end of the list. - return currentPositionInList.index() != listLength - 1 || targetPositionInList.index() != listLength; }; } @@ -69,14 +71,11 @@ public MoveProducer apply(MoveStreamFactory moveStreamFact // To assign or reassign a value, we need to create: // - A move for every unpinned value in every entity's list variable to assign the value before that position. // - A move for every entity to assign it to the last position in the list variable. - var unpinnedValuesToChange = moveStreamFactory.enumerate(variableMetaModel.type(), false); - var unpinnedEntities = moveStreamFactory.enumerate(variableMetaModel.entity().type(), true); - var unpinnedValues = moveStreamFactory.enumerate(variableMetaModel.type(), true); + var unpinnedEntities = moveStreamFactory.enumerate(variableMetaModel.entity().type(), false); + var unpinnedValues = moveStreamFactory.enumerate(variableMetaModel.type(), true) + .filter((solutionView, value) -> value == null || solutionView.getPositionOf(variableMetaModel, value) instanceof PositionInList); var entityValuePairs = unpinnedEntities.join(unpinnedValues, DataJoiners.filtering(isValueInListFilter)) .map((solutionView, entity, value) -> { - if (entity == null) { // This will trigger unassignment of the value. - return ElementPosition.unassigned(); - } var valueCount = solutionView.countValues(variableMetaModel, entity); if (value == null || valueCount == 0) { // This will trigger assignment of the value at the end of the list. return ElementPosition.of(entity, valueCount); @@ -85,8 +84,8 @@ public MoveProducer apply(MoveStreamFactory moveStreamFact } }) .distinct(); - var dataStream = unpinnedValuesToChange.join(entityValuePairs, - DataJoiners.filtering(noChangeDetectionFilter)); + var dataStream = moveStreamFactory.enumerate(variableMetaModel.type(), false) + .join(entityValuePairs, DataJoiners.filtering(validChangeFilter)); return moveStreamFactory.pick(dataStream) .asMove((solutionView, value, targetPosition) -> { var currentPosition = solutionView.getPositionOf(variableMetaModel, Objects.requireNonNull(value)); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ListChangeMoveProviderTest.java b/core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ListChangeMoveProviderTest.java index 58592f9db5..b39ab7dd50 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ListChangeMoveProviderTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ListChangeMoveProviderTest.java @@ -9,7 +9,6 @@ import ai.timefold.solver.core.impl.move.streams.DefaultMoveStreamFactory; import ai.timefold.solver.core.impl.move.streams.maybeapi.generic.move.ListAssignMove; import ai.timefold.solver.core.impl.move.streams.maybeapi.generic.move.ListChangeMove; -import ai.timefold.solver.core.impl.move.streams.maybeapi.generic.move.ListUnassignMove; import ai.timefold.solver.core.impl.move.streams.maybeapi.generic.provider.ListChangeMoveProvider; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.MoveStreamSession; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; @@ -18,6 +17,9 @@ import ai.timefold.solver.core.testdomain.list.TestdataListEntity; import ai.timefold.solver.core.testdomain.list.TestdataListSolution; import ai.timefold.solver.core.testdomain.list.TestdataListValue; +import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingEntity; +import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingSolution; +import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingValue; import org.jspecify.annotations.NonNull; import org.junit.jupiter.api.Test; @@ -36,11 +38,11 @@ void fromSolution() { .planningListVariable(); var solution = TestdataListSolution.generateUninitializedSolution(2, 2); - var emptyEntity = solution.getEntityList().get(0); - var entityWithOneValue = solution.getEntityList().get(1); + var e1 = solution.getEntityList().get(0); + var e2 = solution.getEntityList().get(1); var unassignedValue = solution.getValueList().get(0); - var initiallyAssignedValue1 = solution.getValueList().get(1); - entityWithOneValue.getValueList().add(initiallyAssignedValue1); + var initiallyAssignedValue = solution.getValueList().get(1); + e2.getValueList().add(initiallyAssignedValue); solution.getEntityList().forEach(TestdataListEntity::setUpShadowVariables); var scoreDirector = createScoreDirector(solutionDescriptor, new TestdataListConstraintProvider(), solution); @@ -57,49 +59,132 @@ void fromSolution() { .toList(); assertThat(moveList).hasSize(4); - var firstMove = (ListUnassignMove) moveList.get(0); + // Unassign moves are not generated, because the solution does not allow unassigned values. + // Assign moves are generated for all three positions in e1 and e2. + // Change move is generated for moving the initially assigned value from e2 to e1. + + var firstMove = (ListAssignMove) moveList.get(0); assertSoftly(softly -> { - softly.assertThat(firstMove.getSourceEntity()).isEqualTo(entityWithOneValue); - softly.assertThat(firstMove.getSourceIndex()).isEqualTo(0); + softly.assertThat(firstMove.getDestinationEntity()).isEqualTo(e1); + softly.assertThat(firstMove.getDestinationIndex()).isEqualTo(0); softly.assertThat(firstMove.extractPlanningEntities()) - .containsExactly(entityWithOneValue); + .containsExactly(e1); softly.assertThat(firstMove.extractPlanningValues()) - .containsExactly(initiallyAssignedValue1); + .containsExactly(unassignedValue); }); - var secondMove = (ListChangeMove) moveList.get(1); + var secondMove = (ListAssignMove) moveList.get(1); assertSoftly(softly -> { - softly.assertThat(secondMove.getSourceEntity()).isEqualTo(entityWithOneValue); - softly.assertThat(secondMove.getSourceIndex()).isEqualTo(0); - softly.assertThat(secondMove.getDestinationEntity()).isEqualTo(emptyEntity); - softly.assertThat(secondMove.getDestinationIndex()).isEqualTo(0); + softly.assertThat(secondMove.getDestinationEntity()).isEqualTo(e2); + softly.assertThat(secondMove.getDestinationIndex()).isEqualTo(1); softly.assertThat(secondMove.extractPlanningEntities()) - .containsExactly(entityWithOneValue, emptyEntity); + .containsExactly(e2); softly.assertThat(secondMove.extractPlanningValues()) - .containsExactly(initiallyAssignedValue1); + .containsExactly(unassignedValue); }); var thirdMove = (ListAssignMove) moveList.get(2); assertSoftly(softly -> { - softly.assertThat(thirdMove.getDestinationEntity()).isEqualTo(emptyEntity); + softly.assertThat(thirdMove.getDestinationEntity()).isEqualTo(e2); softly.assertThat(thirdMove.getDestinationIndex()).isEqualTo(0); softly.assertThat(thirdMove.extractPlanningEntities()) - .containsExactly(emptyEntity); + .containsExactly(e2); softly.assertThat(thirdMove.extractPlanningValues()) .containsExactly(unassignedValue); }); - var fourthMove = (ListAssignMove) moveList.get(3); + var fourthMove = (ListChangeMove) moveList.get(3); assertSoftly(softly -> { - softly.assertThat(fourthMove.getDestinationEntity()).isEqualTo(entityWithOneValue); + softly.assertThat(fourthMove.getSourceEntity()).isEqualTo(e2); + softly.assertThat(fourthMove.getSourceIndex()).isEqualTo(0); + softly.assertThat(fourthMove.getDestinationEntity()).isEqualTo(e1); softly.assertThat(fourthMove.getDestinationIndex()).isEqualTo(0); softly.assertThat(fourthMove.extractPlanningEntities()) - .containsExactly(entityWithOneValue); + .containsExactly(e2, e1); softly.assertThat(fourthMove.extractPlanningValues()) - .containsExactly(unassignedValue); + .containsExactly(initiallyAssignedValue); }); } + @Test + void fromEntity() { + var solutionDescriptor = TestdataListEntityProvidingSolution.buildSolutionDescriptor(); + var variableMetaModel = solutionDescriptor.getMetaModel() + .entity(TestdataListEntityProvidingEntity.class) + .planningListVariable(); + + var solution = TestdataListEntityProvidingSolution.generateSolution(); + var e1 = solution.getEntityList().get(0); + var v1 = solution.getValueList().get(0); + var v2 = solution.getValueList().get(1); + var v3 = solution.getValueList().get(2); + e1.getValueList().clear(); + var e2 = solution.getEntityList().get(1); + var initiallyAssignedValue = e2.getValueRange().get(0); + e2.getValueList().add(initiallyAssignedValue); + solution.getEntityList().forEach(TestdataListEntityProvidingEntity::setUpShadowVariables); + + var scoreDirector = createScoreDirector(solutionDescriptor, new TestdataEntityProvidingListConstraintProvider(), solution); + + var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); + var moveProvider = new ListChangeMoveProvider<>(variableMetaModel); + var moveProducer = moveProvider.apply(moveStreamFactory); + var moveStreamSession = createSession(moveStreamFactory, scoreDirector); + + var moveIterable = moveProducer.getMoveIterable(moveStreamSession); + assertThat(moveIterable).hasSize(4); + + var moveList = StreamSupport.stream(moveIterable.spliterator(), false) + .toList(); + assertThat(moveList).hasSize(4); + + // v1 can be moved from e2 to e1, because it's in the range for both. + // v2 is unassigned; it can be assigned to e1, but not to e2. + // v3 is unassigned; it can be assigned to e2, but not to e1. + // e2 has one value already, and therefore two possible assignments, 0 and 1. + + var firstMove = (ListChangeMove) moveList.get(0); + assertSoftly(softly -> { + softly.assertThat(firstMove.getSourceEntity()).isEqualTo(e2); + softly.assertThat(firstMove.getSourceIndex()).isEqualTo(0); + softly.assertThat(firstMove.getDestinationEntity()).isEqualTo(e1); + softly.assertThat(firstMove.getDestinationIndex()).isEqualTo(0); + softly.assertThat(firstMove.extractPlanningEntities()) + .containsExactly(e2, e1); + softly.assertThat(firstMove.extractPlanningValues()) + .containsExactly(v1); + }); + + var secondMove = (ListAssignMove) moveList.get(1); + assertSoftly(softly -> { + softly.assertThat(secondMove.getDestinationEntity()).isEqualTo(e1); + softly.assertThat(secondMove.getDestinationIndex()).isEqualTo(0); + softly.assertThat(secondMove.extractPlanningEntities()) + .containsExactly(e1); + softly.assertThat(secondMove.extractPlanningValues()) + .containsExactly(v2); + }); + + var thirdMove = (ListAssignMove) moveList.get(2); + assertSoftly(softly -> { + softly.assertThat(thirdMove.getDestinationEntity()).isEqualTo(e2); + softly.assertThat(thirdMove.getDestinationIndex()).isEqualTo(1); + softly.assertThat(thirdMove.extractPlanningEntities()) + .containsExactly(e2); + softly.assertThat(thirdMove.extractPlanningValues()) + .containsExactly(v3); + }); + + var fourthMove = (ListAssignMove) moveList.get(3); + assertSoftly(softly -> { + softly.assertThat(fourthMove.getDestinationEntity()).isEqualTo(e2); + softly.assertThat(fourthMove.getDestinationIndex()).isEqualTo(0); + softly.assertThat(fourthMove.extractPlanningEntities()) + .containsExactly(e2); + softly.assertThat(fourthMove.extractPlanningValues()) + .containsExactly(v3); + }); + } /* * @Test @@ -399,4 +484,19 @@ private Constraint alwaysPenalizingConstraint(ConstraintFactory constraintFactor } + public final class TestdataEntityProvidingListConstraintProvider implements ConstraintProvider { + + @Override + public Constraint @NonNull [] defineConstraints(@NonNull ConstraintFactory constraintFactory) { + return new Constraint[] { alwaysPenalizingConstraint(constraintFactory) }; + } + + private Constraint alwaysPenalizingConstraint(ConstraintFactory constraintFactory) { + return constraintFactory.forEach(TestdataListEntityProvidingValue.class) + .penalize(SimpleScore.ONE) + .asConstraint("Always penalize"); + } + + } + } \ No newline at end of file diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/TestdataListEntityProvidingEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/TestdataListEntityProvidingEntity.java index 7f177e1984..87d9dc5599 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/TestdataListEntityProvidingEntity.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/TestdataListEntityProvidingEntity.java @@ -1,8 +1,5 @@ package ai.timefold.solver.core.testdomain.list.valuerange; -import java.util.ArrayList; -import java.util.List; - import ai.timefold.solver.core.api.domain.entity.PlanningEntity; import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; import ai.timefold.solver.core.api.domain.variable.PlanningListVariable; @@ -10,6 +7,9 @@ import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.testdomain.TestdataObject; +import java.util.ArrayList; +import java.util.List; + @PlanningEntity public class TestdataListEntityProvidingEntity extends TestdataObject { @@ -52,6 +52,14 @@ public TestdataListEntityProvidingEntity(String code, List { + testdataListValue.setEntity(this); + testdataListValue.setIndex(valueList.indexOf(testdataListValue)); + }); + return this; + } + public List getValueRange() { return valueRange; } diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/TestdataListEntityProvidingSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/TestdataListEntityProvidingSolution.java index 73679393c7..cbfa683602 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/TestdataListEntityProvidingSolution.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/TestdataListEntityProvidingSolution.java @@ -1,19 +1,19 @@ package ai.timefold.solver.core.testdomain.list.valuerange; -import java.util.List; - import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; import ai.timefold.solver.core.api.domain.solution.PlanningScore; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import java.util.List; + @PlanningSolution public class TestdataListEntityProvidingSolution { public static SolutionDescriptor buildSolutionDescriptor() { return SolutionDescriptor.buildSolutionDescriptor(TestdataListEntityProvidingSolution.class, - TestdataListEntityProvidingEntity.class); + TestdataListEntityProvidingEntity.class, TestdataListEntityProvidingValue.class); } public static TestdataListEntityProvidingSolution generateSolution() { @@ -40,6 +40,14 @@ public void setEntityList(List entityList) { this.entityList = entityList; } + @PlanningEntityCollectionProperty + public List getValueList() { + return entityList.stream() + .flatMap(entity -> entity.getValueRange().stream()) + .distinct() + .toList(); + } + @PlanningScore public SimpleScore getScore() { return score; From 28728bc3442489abb4eb168906da49cb3cdffe01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Wed, 6 Aug 2025 17:18:43 +0200 Subject: [PATCH 09/18] Fix unrelated test --- ...DefaultConstructionHeuristicPhaseTest.java | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java index 8b17c05a6d..033a705970 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java @@ -1,18 +1,5 @@ package ai.timefold.solver.core.impl.constructionheuristic; -import static ai.timefold.solver.core.config.solver.PreviewFeature.DECLARATIVE_SHADOW_VARIABLES; -import static ai.timefold.solver.core.testutil.PlannerAssert.assertCode; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.SoftAssertions.assertSoftly; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.atomic.AtomicLong; - import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.solver.Solver; import ai.timefold.solver.core.api.solver.SolverFactory; @@ -53,10 +40,21 @@ import ai.timefold.solver.core.testdomain.valuerange.entityproviding.unassignedvar.TestdataAllowsUnassignedEntityProvidingSolution; import ai.timefold.solver.core.testutil.AbstractMeterTest; import ai.timefold.solver.core.testutil.PlannerTestUtils; - +import io.micrometer.core.instrument.Metrics; import org.junit.jupiter.api.Test; -import io.micrometer.core.instrument.Metrics; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +import static ai.timefold.solver.core.config.solver.PreviewFeature.DECLARATIVE_SHADOW_VARIABLES; +import static ai.timefold.solver.core.testutil.PlannerAssert.assertCode; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.SoftAssertions.assertSoftly; class DefaultConstructionHeuristicPhaseTest extends AbstractMeterTest { @@ -365,7 +363,8 @@ void solveWithEntityValueRangeBasicVariable() { @Test void solveWithEntityValueRangeListVariable() { var solverConfig = PlannerTestUtils - .buildSolverConfig(TestdataListEntityProvidingSolution.class, TestdataListEntityProvidingEntity.class) + .buildSolverConfig(TestdataListEntityProvidingSolution.class, TestdataListEntityProvidingEntity.class, + TestdataListEntityProvidingValue.class) .withEasyScoreCalculatorClass(TestdataListEntityProvidingScoreCalculator.class) .withPhases(new ConstructionHeuristicPhaseConfig()); @@ -381,8 +380,10 @@ void solveWithEntityValueRangeListVariable() { var bestSolution = PlannerTestUtils.solve(solverConfig, solution, true); assertThat(bestSolution).isNotNull(); // Only one entity should provide the value list and assign the values. - assertThat(bestSolution.getEntityList().get(0).getValueList()).hasSameElementsAs(List.of(value1, value2)); - assertThat(bestSolution.getEntityList().get(1).getValueList()).hasSameElementsAs(List.of(value3)); + assertThat(bestSolution.getEntityList().get(0).getValueList().stream().map(TestdataListEntityProvidingValue::getCode)) + .hasSameElementsAs(List.of("v1", "v2")); + assertThat(bestSolution.getEntityList().get(1).getValueList().stream().map(TestdataListEntityProvidingValue::getCode)) + .hasSameElementsAs(List.of("v3")); } @Test From cede2144ae70fadfc9d26bc78e30680161db1cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Thu, 7 Aug 2025 08:15:31 +0200 Subject: [PATCH 10/18] Complete the first level of test coverage --- .../bavet/uni/AbstractForEachUniNode.java | 7 +- .../DefaultPlanningListVariableMetaModel.java | 7 +- .../DefaultPlanningVariableMetaModel.java | 5 - .../DefaultLocalSearchPhaseFactory.java | 10 +- .../core/impl/move/director/MoveDirector.java | 9 +- .../streams/DefaultMoveStreamFactory.java | 4 +- .../streams/DefaultMoveStreamSession.java | 7 +- .../move/streams/FromBiUniMoveProducer.java | 17 +- .../move/streams/FromUniBiMoveProducer.java | 17 +- .../streams/dataset/DataStreamFactory.java | 17 +- .../dataset/bi/AbstractBiDataStream.java | 15 +- .../move/streams/dataset/bi/BiDataset.java | 2 +- .../streams/dataset/bi/BiDatasetInstance.java | 17 +- .../dataset/bi/BiGroupBiDataStream.java | 5 +- .../streams/dataset/bi/BiMapBiDataStream.java | 7 +- .../dataset/bi/FilterBiDataStream.java | 2 +- .../streams/dataset/bi/JoinBiDataStream.java | 4 +- .../dataset/bi/UniMapBiDataStream.java | 7 +- .../dataset/common/AbstractDataStream.java | 9 +- .../dataset/common/AbstractDataset.java | 7 +- .../common/AbstractDatasetInstance.java | 9 +- .../common/bridge/AftBridgeBiDataStream.java | 7 +- .../common/bridge/AftBridgeUniDataStream.java | 9 +- .../bridge/ForeBridgeUniDataStream.java | 4 +- .../uni/AbstractForEachDataStream.java | 14 +- .../dataset/uni/AbstractUniDataStream.java | 14 +- .../dataset/uni/BiMapUniDataStream.java | 8 +- .../dataset/uni/FilterUniDataStream.java | 7 +- .../uni/ForEachFromSolutionDataStream.java | 7 +- .../uni/ForEachIncludingPinnedDataStream.java | 8 +- .../dataset/uni/IfExistsUniDataStream.java | 4 +- .../move/streams/dataset/uni/UniDataset.java | 2 +- .../dataset/uni/UniDatasetInstance.java | 7 +- .../dataset/uni/UniGroupUniDataStream.java | 5 +- .../dataset/uni/UniMapUniDataStream.java | 7 +- .../move/streams/maybeapi/BiDataMapper.java | 5 +- .../move/streams/maybeapi/UniDataMapper.java | 7 +- .../maybeapi/generic/move/ListAssignMove.java | 9 +- .../maybeapi/generic/move/ListChangeMove.java | 11 +- .../generic/move/ListUnassignMove.java | 11 +- .../generic/provider/ChangeMoveProvider.java | 5 +- .../provider/ListChangeMoveProvider.java | 17 +- .../maybeapi/stream/BiMoveConstructor.java | 1 + .../maybeapi/stream/UniDataStream.java | 14 +- .../metamodel/GenuineVariableMetaModel.java | 21 - .../metamodel/PlanningEntityMetaModel.java | 5 +- .../core/preview/api/move/SolutionView.java | 3 +- ...DefaultConstructionHeuristicPhaseTest.java | 28 +- .../move/MoveStreamsBasedLocalSearchTest.java | 18 +- .../streams/dataset/UniDatasetStreamTest.java | 39 +- .../provider/ChangeMoveProviderTest.java | 57 +- .../provider/ListChangeMoveProviderTest.java | 609 ++++++++---------- .../core/impl/solver/DefaultSolverTest.java | 65 +- .../testdomain/list/TestdataListEntity.java | 10 +- ...tdataAllowsUnassignedValuesListEntity.java | 2 +- .../TestdataListEntityProvidingEntity.java | 6 +- .../TestdataListEntityProvidingSolution.java | 4 +- ...ListUnassignedEntityProvidingSolution.java | 9 + 58 files changed, 612 insertions(+), 631 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractForEachUniNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractForEachUniNode.java index 6d4c6b6d34..116e705e22 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractForEachUniNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractForEachUniNode.java @@ -1,5 +1,8 @@ package ai.timefold.solver.core.impl.bavet.uni; +import java.util.IdentityHashMap; +import java.util.Map; + import ai.timefold.solver.core.impl.bavet.common.AbstractNode; import ai.timefold.solver.core.impl.bavet.common.Propagator; import ai.timefold.solver.core.impl.bavet.common.StaticPropagationQueue; @@ -7,12 +10,10 @@ import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; import ai.timefold.solver.core.impl.score.director.SessionContext; + import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; -import java.util.IdentityHashMap; -import java.util.Map; - /** * Filtering nodes are expensive. * Considering that most streams start with a nullity check on genuine planning variables, diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/DefaultPlanningListVariableMetaModel.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/DefaultPlanningListVariableMetaModel.java index 348394b9c7..eb07df177d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/DefaultPlanningListVariableMetaModel.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/DefaultPlanningListVariableMetaModel.java @@ -27,14 +27,9 @@ public String name() { return variableDescriptor.getVariableName(); } - @Override - public boolean hasValueRangeOnEntity() { - return !variableDescriptor.canExtractValueRangeFromSolution(); - } - @Override public boolean allowsUnassignedValues() { - return false; + return variableDescriptor.allowsUnassignedValues(); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/DefaultPlanningVariableMetaModel.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/DefaultPlanningVariableMetaModel.java index 78766fb813..396fab4666 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/DefaultPlanningVariableMetaModel.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/DefaultPlanningVariableMetaModel.java @@ -27,11 +27,6 @@ public String name() { return variableDescriptor.getVariableName(); } - @Override - public boolean hasValueRangeOnEntity() { - return !variableDescriptor.canExtractValueRangeFromSolution(); - } - @Override public boolean allowsUnassigned() { return variableDescriptor.allowsUnassigned(); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java index 3621a961c6..b1d231d7ee 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/localsearch/DefaultLocalSearchPhaseFactory.java @@ -1,5 +1,10 @@ package ai.timefold.solver.core.impl.localsearch; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + import ai.timefold.solver.core.api.domain.entity.PinningFilter; import ai.timefold.solver.core.api.domain.entity.PlanningPin; import ai.timefold.solver.core.api.domain.variable.PlanningListVariable; @@ -45,11 +50,6 @@ import ai.timefold.solver.core.impl.solver.termination.SolverTermination; import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningVariableMetaModel; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; - public class DefaultLocalSearchPhaseFactory extends AbstractPhaseFactory { public DefaultLocalSearchPhaseFactory(LocalSearchPhaseConfig phaseConfig) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/director/MoveDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/move/director/MoveDirector.java index 9a952f1edc..5185460660 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/director/MoveDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/director/MoveDirector.java @@ -1,5 +1,9 @@ package ai.timefold.solver.core.impl.move.director; +import java.util.List; +import java.util.Objects; +import java.util.function.BiFunction; + import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultPlanningListVariableMetaModel; @@ -18,13 +22,10 @@ import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningVariableMetaModel; import ai.timefold.solver.core.preview.api.move.Move; import ai.timefold.solver.core.preview.api.move.Rebaser; + import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; -import java.util.List; -import java.util.Objects; -import java.util.function.BiFunction; - @NullMarked public sealed class MoveDirector> implements InnerMutableSolutionView, Rebaser diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamFactory.java index d9fb7b9e49..1315be1004 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamFactory.java @@ -5,10 +5,10 @@ import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultPlanningVariableMetaModel; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor; -import ai.timefold.solver.core.impl.move.streams.dataset.bi.AbstractBiDataStream; -import ai.timefold.solver.core.impl.move.streams.dataset.uni.AbstractUniDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.DatasetSessionFactory; +import ai.timefold.solver.core.impl.move.streams.dataset.bi.AbstractBiDataStream; +import ai.timefold.solver.core.impl.move.streams.dataset.uni.AbstractUniDataStream; import ai.timefold.solver.core.impl.move.streams.maybeapi.DataJoiners; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.BiDataStream; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.BiMoveStream; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamSession.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamSession.java index fb8fab362d..2b668ff9e4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamSession.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamSession.java @@ -1,15 +1,16 @@ package ai.timefold.solver.core.impl.move.streams; +import java.util.Objects; + +import ai.timefold.solver.core.impl.move.streams.dataset.DatasetSession; import ai.timefold.solver.core.impl.move.streams.dataset.bi.BiDataset; import ai.timefold.solver.core.impl.move.streams.dataset.bi.BiDatasetInstance; -import ai.timefold.solver.core.impl.move.streams.dataset.DatasetSession; import ai.timefold.solver.core.impl.move.streams.dataset.uni.UniDataset; import ai.timefold.solver.core.impl.move.streams.dataset.uni.UniDatasetInstance; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.MoveStreamSession; import ai.timefold.solver.core.preview.api.move.SolutionView; -import org.jspecify.annotations.NullMarked; -import java.util.Objects; +import org.jspecify.annotations.NullMarked; @NullMarked public final class DefaultMoveStreamSession diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/FromBiUniMoveProducer.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/FromBiUniMoveProducer.java index 2c8190994e..9906fe055e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/FromBiUniMoveProducer.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/FromBiUniMoveProducer.java @@ -1,22 +1,23 @@ package ai.timefold.solver.core.impl.move.streams; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Random; +import java.util.Set; +import java.util.function.Supplier; + import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; -import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.bi.BiDataset; +import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataStream; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.BiMoveConstructor; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.MoveStreamSession; import ai.timefold.solver.core.preview.api.move.Move; import ai.timefold.solver.core.preview.api.move.SolutionView; + import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.Objects; -import java.util.Random; -import java.util.Set; -import java.util.function.Supplier; - @NullMarked public final class FromBiUniMoveProducer implements InnerMoveProducer { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/FromUniBiMoveProducer.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/FromUniBiMoveProducer.java index 87352ecd2c..470ac68838 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/FromUniBiMoveProducer.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/FromUniBiMoveProducer.java @@ -1,5 +1,13 @@ package ai.timefold.solver.core.impl.move.streams; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Random; +import java.util.Set; +import java.util.function.BiPredicate; +import java.util.function.Supplier; + import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.uni.UniDataset; @@ -7,17 +15,10 @@ import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.MoveStreamSession; import ai.timefold.solver.core.preview.api.move.Move; import ai.timefold.solver.core.preview.api.move.SolutionView; + import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; -import java.util.Iterator; -import java.util.NoSuchElementException; -import java.util.Objects; -import java.util.Random; -import java.util.Set; -import java.util.function.BiPredicate; -import java.util.function.Supplier; - @NullMarked public final class FromUniBiMoveProducer implements InnerMoveProducer { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/DataStreamFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/DataStreamFactory.java index 92fa2e7416..200c4255bc 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/DataStreamFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/DataStreamFactory.java @@ -1,5 +1,13 @@ package ai.timefold.solver.core.impl.move.streams.dataset; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.domain.solution.descriptor.InnerGenuineVariableMetaModel; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; @@ -13,15 +21,8 @@ import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.UniDataStream; import ai.timefold.solver.core.impl.score.director.SessionContext; import ai.timefold.solver.core.preview.api.domain.metamodel.GenuineVariableMetaModel; -import org.jspecify.annotations.NullMarked; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.function.Consumer; -import java.util.stream.Collectors; -import java.util.stream.Stream; +import org.jspecify.annotations.NullMarked; @NullMarked public final class DataStreamFactory { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/AbstractBiDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/AbstractBiDataStream.java index 61adfa6349..04edf5f270 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/AbstractBiDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/AbstractBiDataStream.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.move.streams.dataset.bi; +import java.util.function.BiFunction; + import ai.timefold.solver.core.impl.bavet.bi.Group2Mapping0CollectorBiNode; import ai.timefold.solver.core.impl.bavet.common.GroupNodeConstructor; import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; @@ -12,11 +14,10 @@ import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.BiDataStream; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.UniDataStream; import ai.timefold.solver.core.impl.util.ConstantLambdaUtils; + import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; -import java.util.function.BiFunction; - @NullMarked public abstract class AbstractBiDataStream extends AbstractDataStream implements BiDataStream { @@ -35,14 +36,15 @@ public final BiDataStream filter(BiDataFilter return shareAndAddChild(new FilterBiDataStream<>(dataStreamFactory, this, filter)); } - - protected AbstractBiDataStream groupBy(BiFunction groupKeyAMapping, BiFunction groupKeyBMapping) { + protected AbstractBiDataStream + groupBy(BiFunction groupKeyAMapping, BiFunction groupKeyBMapping) { GroupNodeConstructor> nodeConstructor = GroupNodeConstructor.twoKeysGroupBy(groupKeyAMapping, groupKeyBMapping, Group2Mapping0CollectorBiNode::new); return buildBiGroupBy(nodeConstructor); } - private AbstractBiDataStream buildBiGroupBy(GroupNodeConstructor> nodeConstructor) { + private AbstractBiDataStream + buildBiGroupBy(GroupNodeConstructor> nodeConstructor) { var stream = shareAndAddChild(new BiGroupBiDataStream<>(dataStreamFactory, this, nodeConstructor)); return dataStreamFactory.share(new AftBridgeBiDataStream<>(dataStreamFactory, stream), stream::setAftBridge); } @@ -54,7 +56,8 @@ public UniDataStream map(BiDataMapper BiDataStream map(BiDataMapper mappingA, BiDataMapper mappingB) { + public BiDataStream + map(BiDataMapper mappingA, BiDataMapper mappingB) { var stream = shareAndAddChild(new BiMapBiDataStream<>(dataStreamFactory, this, mappingA, mappingB)); return dataStreamFactory.share(new AftBridgeBiDataStream<>(dataStreamFactory, stream), stream::setAftBridge); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiDataset.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiDataset.java index d3cec1c721..72a3667dbe 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiDataset.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiDataset.java @@ -1,9 +1,9 @@ package ai.timefold.solver.core.impl.move.streams.dataset.bi; import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; - import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataset; + import org.jspecify.annotations.NullMarked; @NullMarked diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiDatasetInstance.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiDatasetInstance.java index 6525317754..2f7c2e9dfd 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiDatasetInstance.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiDatasetInstance.java @@ -1,22 +1,23 @@ package ai.timefold.solver.core.impl.move.streams.dataset.bi; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Random; + import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataset; import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDatasetInstance; import ai.timefold.solver.core.impl.util.CollectionUtils; import ai.timefold.solver.core.impl.util.ElementAwareList; import ai.timefold.solver.core.impl.util.ElementAwareListEntry; + import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Random; - @NullMarked public final class BiDatasetInstance extends AbstractDatasetInstance> { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiGroupBiDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiGroupBiDataStream.java index e5beca28f5..9a06430107 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiGroupBiDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiGroupBiDataStream.java @@ -1,15 +1,16 @@ package ai.timefold.solver.core.impl.move.streams.dataset.bi; +import java.util.Objects; + import ai.timefold.solver.core.impl.bavet.common.GroupNodeConstructor; import ai.timefold.solver.core.impl.bavet.common.tuple.BiTuple; import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; import ai.timefold.solver.core.impl.move.streams.dataset.common.bridge.AftBridgeBiDataStream; + import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; -import java.util.Objects; - @NullMarked final class BiGroupBiDataStream extends AbstractBiDataStream { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiMapBiDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiMapBiDataStream.java index 893beca2fa..c802eb4b65 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiMapBiDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiMapBiDataStream.java @@ -1,15 +1,16 @@ package ai.timefold.solver.core.impl.move.streams.dataset.bi; +import java.util.Objects; + import ai.timefold.solver.core.impl.bavet.bi.MapBiToBiNode; import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; import ai.timefold.solver.core.impl.move.streams.dataset.common.bridge.AftBridgeBiDataStream; import ai.timefold.solver.core.impl.move.streams.maybeapi.BiDataMapper; + import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; -import java.util.Objects; - @NullMarked final class BiMapBiDataStream extends AbstractBiDataStream { @@ -19,7 +20,7 @@ final class BiMapBiDataStream private @Nullable AftBridgeBiDataStream aftStream; public BiMapBiDataStream(DataStreamFactory constraintFactory, AbstractBiDataStream parent, - BiDataMapper mappingFunctionA, BiDataMapper mappingFunctionB) { + BiDataMapper mappingFunctionA, BiDataMapper mappingFunctionB) { super(constraintFactory, parent); this.mappingFunctionA = mappingFunctionA; this.mappingFunctionB = mappingFunctionB; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/FilterBiDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/FilterBiDataStream.java index cbdfc34129..2cc285e295 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/FilterBiDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/FilterBiDataStream.java @@ -17,7 +17,7 @@ final class FilterBiDataStream private final BiDataFilter filter; public FilterBiDataStream(DataStreamFactory dataStreamFactory, AbstractBiDataStream parent, - BiDataFilter filter) { + BiDataFilter filter) { super(dataStreamFactory, parent); this.filter = Objects.requireNonNull(filter, "The filter cannot be null."); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/JoinBiDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/JoinBiDataStream.java index fba6ea3376..a1997fcaf1 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/JoinBiDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/JoinBiDataStream.java @@ -25,8 +25,8 @@ public final class JoinBiDataStream extends AbstractBiDataStrea private final BiDataFilter filtering; public JoinBiDataStream(DataStreamFactory dataStreamFactory, - ForeBridgeUniDataStream leftParent, ForeBridgeUniDataStream rightParent, - DefaultBiDataJoiner joiner, BiDataFilter filtering) { + ForeBridgeUniDataStream leftParent, ForeBridgeUniDataStream rightParent, + DefaultBiDataJoiner joiner, BiDataFilter filtering) { super(dataStreamFactory); this.leftParent = leftParent; this.rightParent = rightParent; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/UniMapBiDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/UniMapBiDataStream.java index cdc83e5051..c0ae6a2efc 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/UniMapBiDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/UniMapBiDataStream.java @@ -1,16 +1,17 @@ package ai.timefold.solver.core.impl.move.streams.dataset.bi; +import java.util.Objects; + import ai.timefold.solver.core.impl.bavet.bi.MapBiToUniNode; import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; import ai.timefold.solver.core.impl.move.streams.dataset.common.bridge.AftBridgeUniDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.uni.AbstractUniDataStream; import ai.timefold.solver.core.impl.move.streams.maybeapi.BiDataMapper; + import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; -import java.util.Objects; - @NullMarked final class UniMapBiDataStream extends AbstractUniDataStream { @@ -19,7 +20,7 @@ final class UniMapBiDataStream private @Nullable AftBridgeUniDataStream aftStream; public UniMapBiDataStream(DataStreamFactory constraintFactory, AbstractBiDataStream parent, - BiDataMapper mappingFunction) { + BiDataMapper mappingFunction) { super(constraintFactory, parent); this.mappingFunction = mappingFunction; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDataStream.java index 6b40ff85c8..2378b65ef2 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDataStream.java @@ -1,15 +1,16 @@ package ai.timefold.solver.core.impl.move.streams.dataset.common; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + import ai.timefold.solver.core.impl.bavet.common.BavetStream; import ai.timefold.solver.core.impl.bavet.common.TupleSource; import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; + import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; - @NullMarked public abstract class AbstractDataStream implements BavetStream { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDataset.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDataset.java index 96c19b6003..99fb5a8bfc 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDataset.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDataset.java @@ -1,11 +1,12 @@ package ai.timefold.solver.core.impl.move.streams.dataset.common; +import java.util.Objects; +import java.util.Set; + import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; -import org.jspecify.annotations.NullMarked; -import java.util.Objects; -import java.util.Set; +import org.jspecify.annotations.NullMarked; @NullMarked public abstract class AbstractDataset { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDatasetInstance.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDatasetInstance.java index b0e9444ff8..28d9744760 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDatasetInstance.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDatasetInstance.java @@ -1,13 +1,14 @@ package ai.timefold.solver.core.impl.move.streams.dataset.common; -import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; -import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; -import org.jspecify.annotations.NullMarked; - import java.util.Iterator; import java.util.Objects; import java.util.Random; +import ai.timefold.solver.core.impl.bavet.common.tuple.AbstractTuple; +import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; + +import org.jspecify.annotations.NullMarked; + @NullMarked public abstract class AbstractDatasetInstance implements TupleLifecycle { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/AftBridgeBiDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/AftBridgeBiDataStream.java index d9c5f0e264..343624b86c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/AftBridgeBiDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/AftBridgeBiDataStream.java @@ -1,13 +1,14 @@ package ai.timefold.solver.core.impl.move.streams.dataset.common.bridge; +import java.util.Objects; + import ai.timefold.solver.core.impl.bavet.common.TupleSource; +import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.bi.AbstractBiDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataStream; -import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; -import org.jspecify.annotations.NullMarked; -import java.util.Objects; +import org.jspecify.annotations.NullMarked; @NullMarked public final class AftBridgeBiDataStream diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/AftBridgeUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/AftBridgeUniDataStream.java index aa872610c2..ed50b5119f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/AftBridgeUniDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/AftBridgeUniDataStream.java @@ -1,13 +1,14 @@ package ai.timefold.solver.core.impl.move.streams.dataset.common.bridge; +import java.util.Objects; + import ai.timefold.solver.core.impl.bavet.common.TupleSource; -import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataStream; -import ai.timefold.solver.core.impl.move.streams.dataset.uni.AbstractUniDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; +import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; -import org.jspecify.annotations.NullMarked; +import ai.timefold.solver.core.impl.move.streams.dataset.uni.AbstractUniDataStream; -import java.util.Objects; +import org.jspecify.annotations.NullMarked; @NullMarked public final class AftBridgeUniDataStream diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/ForeBridgeUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/ForeBridgeUniDataStream.java index 6996946c7e..8a4c42e801 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/ForeBridgeUniDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/ForeBridgeUniDataStream.java @@ -1,9 +1,9 @@ package ai.timefold.solver.core.impl.move.streams.dataset.common.bridge; -import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataStream; -import ai.timefold.solver.core.impl.move.streams.dataset.uni.AbstractUniDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; +import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; +import ai.timefold.solver.core.impl.move.streams.dataset.uni.AbstractUniDataStream; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.UniDataStream; import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractForEachDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractForEachDataStream.java index f8e3425fae..1aa108ac8f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractForEachDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractForEachDataStream.java @@ -1,5 +1,10 @@ package ai.timefold.solver.core.impl.move.streams.dataset.uni; +import static ai.timefold.solver.core.impl.bavet.uni.AbstractForEachUniNode.LifecycleOperation; + +import java.util.Objects; +import java.util.Set; + import ai.timefold.solver.core.impl.bavet.common.TupleSource; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; @@ -7,12 +12,8 @@ import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; -import org.jspecify.annotations.NullMarked; -import java.util.Objects; -import java.util.Set; - -import static ai.timefold.solver.core.impl.bavet.uni.AbstractForEachUniNode.LifecycleOperation; +import org.jspecify.annotations.NullMarked; @NullMarked abstract sealed class AbstractForEachDataStream @@ -23,7 +24,8 @@ abstract sealed class AbstractForEachDataStream protected final Class forEachClass; final boolean shouldIncludeNull; - protected AbstractForEachDataStream(DataStreamFactory dataStreamFactory, Class forEachClass, boolean includeNull) { + protected AbstractForEachDataStream(DataStreamFactory dataStreamFactory, Class forEachClass, + boolean includeNull) { super(dataStreamFactory, null); this.forEachClass = Objects.requireNonNull(forEachClass); this.shouldIncludeNull = includeNull; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractUniDataStream.java index 9d14e435ed..2ddf62a680 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractUniDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractUniDataStream.java @@ -1,5 +1,10 @@ package ai.timefold.solver.core.impl.move.streams.dataset.uni; +import static ai.timefold.solver.core.impl.bavet.common.GroupNodeConstructor.oneKeyGroupBy; + +import java.util.Objects; +import java.util.function.Function; + import ai.timefold.solver.core.api.score.stream.uni.UniConstraintStream; import ai.timefold.solver.core.impl.bavet.common.GroupNodeConstructor; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; @@ -17,14 +22,10 @@ import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.BiDataStream; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.UniDataStream; import ai.timefold.solver.core.impl.util.ConstantLambdaUtils; + import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; -import java.util.Objects; -import java.util.function.Function; - -import static ai.timefold.solver.core.impl.bavet.common.GroupNodeConstructor.oneKeyGroupBy; - @NullMarked public abstract class AbstractUniDataStream extends AbstractDataStream implements UniDataStream { @@ -116,7 +117,8 @@ protected AbstractUniDataStream groupBy(Functi return buildUniGroupBy(nodeConstructor); } - private AbstractUniDataStream buildUniGroupBy(GroupNodeConstructor> nodeConstructor) { + private AbstractUniDataStream + buildUniGroupBy(GroupNodeConstructor> nodeConstructor) { var stream = shareAndAddChild(new UniGroupUniDataStream<>(dataStreamFactory, this, nodeConstructor)); return dataStreamFactory.share(new AftBridgeUniDataStream<>(dataStreamFactory, stream), stream::setAftBridge); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/BiMapUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/BiMapUniDataStream.java index 113da91616..497549acec 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/BiMapUniDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/BiMapUniDataStream.java @@ -1,16 +1,17 @@ package ai.timefold.solver.core.impl.move.streams.dataset.uni; +import java.util.Objects; + import ai.timefold.solver.core.impl.bavet.uni.MapUniToBiNode; import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.bi.AbstractBiDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; import ai.timefold.solver.core.impl.move.streams.dataset.common.bridge.AftBridgeBiDataStream; import ai.timefold.solver.core.impl.move.streams.maybeapi.UniDataMapper; + import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; -import java.util.Objects; - @NullMarked final class BiMapUniDataStream extends AbstractBiDataStream { @@ -19,7 +20,8 @@ final class BiMapUniDataStream private final UniDataMapper mappingFunctionB; private @Nullable AftBridgeBiDataStream aftStream; - public BiMapUniDataStream(DataStreamFactory constraintFactory, AbstractUniDataStream parent, UniDataMapper mappingFunctionA, UniDataMapper mappingFunctionB) { + public BiMapUniDataStream(DataStreamFactory constraintFactory, AbstractUniDataStream parent, + UniDataMapper mappingFunctionA, UniDataMapper mappingFunctionB) { super(constraintFactory, parent); this.mappingFunctionA = mappingFunctionA; this.mappingFunctionB = mappingFunctionB; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/FilterUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/FilterUniDataStream.java index 31620922ce..5c03988605 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/FilterUniDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/FilterUniDataStream.java @@ -1,13 +1,14 @@ package ai.timefold.solver.core.impl.move.streams.dataset.uni; +import java.util.Objects; + import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; import ai.timefold.solver.core.impl.move.streams.maybeapi.UniDataFilter; -import org.jspecify.annotations.NullMarked; -import java.util.Objects; +import org.jspecify.annotations.NullMarked; @NullMarked final class FilterUniDataStream @@ -16,7 +17,7 @@ final class FilterUniDataStream private final UniDataFilter filter; public FilterUniDataStream(DataStreamFactory dataStreamFactory, AbstractUniDataStream parent, - UniDataFilter filter) { + UniDataFilter filter) { super(dataStreamFactory, parent); this.filter = Objects.requireNonNull(filter, "The filter cannot be null."); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachFromSolutionDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachFromSolutionDataStream.java index 15486ff78b..d296a24434 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachFromSolutionDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachFromSolutionDataStream.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.move.streams.dataset.uni; +import java.util.Objects; + import ai.timefold.solver.core.impl.bavet.common.TupleSource; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; @@ -7,9 +9,8 @@ import ai.timefold.solver.core.impl.bavet.uni.ForEachFromSolutionUniNode; import ai.timefold.solver.core.impl.domain.valuerange.descriptor.ValueRangeDescriptor; import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; -import org.jspecify.annotations.NullMarked; -import java.util.Objects; +import org.jspecify.annotations.NullMarked; @NullMarked public final class ForEachFromSolutionDataStream @@ -19,7 +20,7 @@ public final class ForEachFromSolutionDataStream private final ValueRangeDescriptor valueRangeDescriptor; public ForEachFromSolutionDataStream(DataStreamFactory dataStreamFactory, - ValueRangeDescriptor valueRangeDescriptor, boolean includeNull) { + ValueRangeDescriptor valueRangeDescriptor, boolean includeNull) { super(dataStreamFactory, (Class) valueRangeDescriptor.getVariableDescriptor().getVariablePropertyType(), includeNull); this.valueRangeDescriptor = Objects.requireNonNull(valueRangeDescriptor); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachIncludingPinnedDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachIncludingPinnedDataStream.java index b879c81dde..5a349fe61a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachIncludingPinnedDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachIncludingPinnedDataStream.java @@ -1,21 +1,23 @@ package ai.timefold.solver.core.impl.move.streams.dataset.uni; +import java.util.Objects; + import ai.timefold.solver.core.impl.bavet.common.TupleSource; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; import ai.timefold.solver.core.impl.bavet.uni.AbstractForEachUniNode; import ai.timefold.solver.core.impl.bavet.uni.ForEachIncludingUnassignedUniNode; import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; -import org.jspecify.annotations.NullMarked; -import java.util.Objects; +import org.jspecify.annotations.NullMarked; @NullMarked public final class ForEachIncludingPinnedDataStream extends AbstractForEachDataStream implements TupleSource { - public ForEachIncludingPinnedDataStream(DataStreamFactory dataStreamFactory, Class forEachClass, boolean includeNull) { + public ForEachIncludingPinnedDataStream(DataStreamFactory dataStreamFactory, Class forEachClass, + boolean includeNull) { super(dataStreamFactory, forEachClass, includeNull); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/IfExistsUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/IfExistsUniDataStream.java index f45390fefd..f9a93b8fb9 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/IfExistsUniDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/IfExistsUniDataStream.java @@ -32,8 +32,8 @@ final class IfExistsUniDataStream private final @Nullable BiDataFilter filtering; public IfExistsUniDataStream(DataStreamFactory dataStreamFactory, AbstractUniDataStream parentA, - ForeBridgeUniDataStream parentBridgeB, boolean shouldExist, DefaultBiDataJoiner joiner, - @Nullable BiDataFilter filtering) { + ForeBridgeUniDataStream parentBridgeB, boolean shouldExist, DefaultBiDataJoiner joiner, + @Nullable BiDataFilter filtering) { super(dataStreamFactory); this.parentA = parentA; this.parentBridgeB = parentBridgeB; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniDataset.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniDataset.java index 3980a20435..11225e0cf4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniDataset.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniDataset.java @@ -1,9 +1,9 @@ package ai.timefold.solver.core.impl.move.streams.dataset.uni; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; - import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataset; + import org.jspecify.annotations.NullMarked; @NullMarked diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniDatasetInstance.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniDatasetInstance.java index fb5a4d2799..b0aa6205ba 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniDatasetInstance.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniDatasetInstance.java @@ -1,14 +1,15 @@ package ai.timefold.solver.core.impl.move.streams.dataset.uni; +import java.util.Iterator; +import java.util.Random; + import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataset; import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDatasetInstance; import ai.timefold.solver.core.impl.util.ElementAwareList; import ai.timefold.solver.core.impl.util.ElementAwareListEntry; -import org.jspecify.annotations.NullMarked; -import java.util.Iterator; -import java.util.Random; +import org.jspecify.annotations.NullMarked; @NullMarked public final class UniDatasetInstance diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniGroupUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniGroupUniDataStream.java index db37f5e6c8..f68580ac0e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniGroupUniDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniGroupUniDataStream.java @@ -1,15 +1,16 @@ package ai.timefold.solver.core.impl.move.streams.dataset.uni; +import java.util.Objects; + import ai.timefold.solver.core.impl.bavet.common.GroupNodeConstructor; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; import ai.timefold.solver.core.impl.move.streams.dataset.common.bridge.AftBridgeUniDataStream; + import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; -import java.util.Objects; - @NullMarked final class UniGroupUniDataStream extends AbstractUniDataStream { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniMapUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniMapUniDataStream.java index 1cd993b3de..f3b78dca30 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniMapUniDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniMapUniDataStream.java @@ -1,15 +1,16 @@ package ai.timefold.solver.core.impl.move.streams.dataset.uni; +import java.util.Objects; + import ai.timefold.solver.core.impl.bavet.uni.MapUniToUniNode; import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; import ai.timefold.solver.core.impl.move.streams.dataset.common.bridge.AftBridgeUniDataStream; import ai.timefold.solver.core.impl.move.streams.maybeapi.UniDataMapper; + import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; -import java.util.Objects; - @NullMarked final class UniMapUniDataStream extends AbstractUniDataStream { @@ -18,7 +19,7 @@ final class UniMapUniDataStream private @Nullable AftBridgeUniDataStream aftStream; public UniMapUniDataStream(DataStreamFactory constraintFactory, AbstractUniDataStream parent, - UniDataMapper mappingFunction) { + UniDataMapper mappingFunction) { super(constraintFactory, parent); this.mappingFunction = mappingFunction; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/BiDataMapper.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/BiDataMapper.java index b2b109255b..7d8ddc1346 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/BiDataMapper.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/BiDataMapper.java @@ -1,13 +1,14 @@ package ai.timefold.solver.core.impl.move.streams.maybeapi; +import java.util.function.BiFunction; + import ai.timefold.solver.core.api.function.TriFunction; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.BiDataStream; import ai.timefold.solver.core.preview.api.move.SolutionView; + import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; -import java.util.function.BiFunction; - /** * A mapping function that can be applied to {@link BiDataStream} to transform data, * optionally using {@link SolutionView} to query for solution state. diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/UniDataMapper.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/UniDataMapper.java index 074fb2997f..81358cad1c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/UniDataMapper.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/UniDataMapper.java @@ -1,13 +1,14 @@ package ai.timefold.solver.core.impl.move.streams.maybeapi; +import java.util.function.BiFunction; +import java.util.function.Function; + import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.UniDataStream; import ai.timefold.solver.core.preview.api.move.SolutionView; + import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; -import java.util.function.BiFunction; -import java.util.function.Function; - /** * A mapping function that can be applied to {@link UniDataStream} to transform data, * optionally using {@link SolutionView} to query for solution state. diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListAssignMove.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListAssignMove.java index e47067c006..c230c0ae31 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListAssignMove.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListAssignMove.java @@ -1,15 +1,16 @@ package ai.timefold.solver.core.impl.move.streams.maybeapi.generic.move; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningListVariableMetaModel; import ai.timefold.solver.core.preview.api.domain.metamodel.VariableMetaModel; import ai.timefold.solver.core.preview.api.move.Move; import ai.timefold.solver.core.preview.api.move.MutableSolutionView; import ai.timefold.solver.core.preview.api.move.Rebaser; -import org.jspecify.annotations.NullMarked; -import java.util.Collection; -import java.util.List; -import java.util.Objects; +import org.jspecify.annotations.NullMarked; @NullMarked public final class ListAssignMove extends AbstractMove { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListChangeMove.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListChangeMove.java index 690cd113b3..9db76a8ca4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListChangeMove.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListChangeMove.java @@ -1,19 +1,20 @@ package ai.timefold.solver.core.impl.move.streams.maybeapi.generic.move; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.domain.variable.PlanningListVariable; import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningListVariableMetaModel; import ai.timefold.solver.core.preview.api.domain.metamodel.VariableMetaModel; import ai.timefold.solver.core.preview.api.move.MutableSolutionView; import ai.timefold.solver.core.preview.api.move.Rebaser; + import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Objects; - /** * Moves an element of a {@link PlanningListVariable list variable}. The moved element is identified * by an entity instance and a position in that entity's list variable. The element is inserted at the given index diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListUnassignMove.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListUnassignMove.java index c010cdee43..78db6cd3e3 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListUnassignMove.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListUnassignMove.java @@ -1,16 +1,17 @@ package ai.timefold.solver.core.impl.move.streams.maybeapi.generic.move; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningListVariableMetaModel; import ai.timefold.solver.core.preview.api.domain.metamodel.VariableMetaModel; import ai.timefold.solver.core.preview.api.move.Move; import ai.timefold.solver.core.preview.api.move.MutableSolutionView; import ai.timefold.solver.core.preview.api.move.Rebaser; -import org.jspecify.annotations.NullMarked; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Objects; +import org.jspecify.annotations.NullMarked; @NullMarked public final class ListUnassignMove extends AbstractMove { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ChangeMoveProvider.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ChangeMoveProvider.java index 09d1e4f729..ce3a5ab4c5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ChangeMoveProvider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ChangeMoveProvider.java @@ -1,13 +1,14 @@ package ai.timefold.solver.core.impl.move.streams.maybeapi.generic.provider; +import java.util.Objects; + import ai.timefold.solver.core.impl.move.streams.maybeapi.generic.move.ChangeMove; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.MoveProducer; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.MoveProvider; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.MoveStreamFactory; import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningVariableMetaModel; -import org.jspecify.annotations.NullMarked; -import java.util.Objects; +import org.jspecify.annotations.NullMarked; @NullMarked public class ChangeMoveProvider diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java index b99c20d444..0773c3fbe7 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.move.streams.maybeapi.generic.provider; +import java.util.Objects; + import ai.timefold.solver.core.impl.move.streams.maybeapi.BiDataFilter; import ai.timefold.solver.core.impl.move.streams.maybeapi.DataJoiners; import ai.timefold.solver.core.impl.move.streams.maybeapi.generic.move.ListAssignMove; @@ -12,9 +14,8 @@ import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningListVariableMetaModel; import ai.timefold.solver.core.preview.api.domain.metamodel.PositionInList; import ai.timefold.solver.core.preview.api.domain.metamodel.UnassignedElement; -import org.jspecify.annotations.NullMarked; -import java.util.Objects; +import org.jspecify.annotations.NullMarked; @NullMarked public class ListChangeMoveProvider @@ -29,7 +30,8 @@ public ListChangeMoveProvider(PlanningListVariableMetaModel { if (entity == null || value == null) { // Necessary for the null to survive until the later stage, - // where we will use it as a marker to unassign the value. + // where we will use it as a special marker to either unassign the value, + // or move it to the end of list. return true; } return solution.isValueInRange(variableMetaModel, entity, value); @@ -71,11 +73,16 @@ public MoveProducer apply(MoveStreamFactory moveStreamFact // To assign or reassign a value, we need to create: // - A move for every unpinned value in every entity's list variable to assign the value before that position. // - A move for every entity to assign it to the last position in the list variable. - var unpinnedEntities = moveStreamFactory.enumerate(variableMetaModel.entity().type(), false); + var unpinnedEntities = + moveStreamFactory.enumerate(variableMetaModel.entity().type(), variableMetaModel.allowsUnassignedValues()); var unpinnedValues = moveStreamFactory.enumerate(variableMetaModel.type(), true) - .filter((solutionView, value) -> value == null || solutionView.getPositionOf(variableMetaModel, value) instanceof PositionInList); + .filter((solutionView, value) -> value == null + || solutionView.getPositionOf(variableMetaModel, value) instanceof PositionInList); var entityValuePairs = unpinnedEntities.join(unpinnedValues, DataJoiners.filtering(isValueInListFilter)) .map((solutionView, entity, value) -> { + if (entity == null) { // Null entity means we need to unassign the value. + return ElementPosition.unassigned(); + } var valueCount = solutionView.countValues(variableMetaModel, entity); if (value == null || valueCount == 0) { // This will trigger assignment of the value at the end of the list. return ElementPosition.of(entity, valueCount); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/BiMoveConstructor.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/BiMoveConstructor.java index 5dc0140ebd..500aee5c17 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/BiMoveConstructor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/BiMoveConstructor.java @@ -2,6 +2,7 @@ import ai.timefold.solver.core.preview.api.move.Move; import ai.timefold.solver.core.preview.api.move.SolutionView; + import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/UniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/UniDataStream.java index 6180f5f338..216e453f9a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/UniDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/UniDataStream.java @@ -1,17 +1,18 @@ package ai.timefold.solver.core.impl.move.streams.maybeapi.stream; +import static ai.timefold.solver.core.impl.util.ConstantLambdaUtils.notEqualsForDataStreams; + +import java.util.Arrays; +import java.util.stream.Stream; + import ai.timefold.solver.core.impl.move.streams.maybeapi.BiDataFilter; import ai.timefold.solver.core.impl.move.streams.maybeapi.BiDataJoiner; import ai.timefold.solver.core.impl.move.streams.maybeapi.DataJoiners; import ai.timefold.solver.core.impl.move.streams.maybeapi.UniDataFilter; import ai.timefold.solver.core.impl.move.streams.maybeapi.UniDataMapper; import ai.timefold.solver.core.preview.api.move.SolutionView; -import org.jspecify.annotations.NullMarked; -import java.util.Arrays; -import java.util.stream.Stream; - -import static ai.timefold.solver.core.impl.util.ConstantLambdaUtils.notEqualsForDataStreams; +import org.jspecify.annotations.NullMarked; @NullMarked public interface UniDataStream extends DataStream { @@ -180,7 +181,8 @@ default UniDataStream ifNotExistsOther(Class otherClass, BiData * @param the type of the first fact in the resulting {@link BiDataStream}'s tuple * @param the type of the first fact in the resulting {@link BiDataStream}'s tuple */ - BiDataStream map(UniDataMapper mappingA, UniDataMapper mappingB); + BiDataStream map(UniDataMapper mappingA, + UniDataMapper mappingB); /** * Transforms the stream in such a way that all the tuples going through it are distinct. diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/GenuineVariableMetaModel.java b/core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/GenuineVariableMetaModel.java index 9b549369bf..7bcf276346 100644 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/GenuineVariableMetaModel.java +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/GenuineVariableMetaModel.java @@ -1,6 +1,5 @@ package ai.timefold.solver.core.preview.api.domain.metamodel; -import ai.timefold.solver.core.api.domain.variable.PlanningListVariable; import ai.timefold.solver.core.api.domain.variable.PlanningVariable; import org.jspecify.annotations.NullMarked; @@ -34,24 +33,4 @@ default boolean isGenuine() { return true; } - boolean hasValueRangeOnEntity(); - - default PlanningVariableMetaModel ensurePlanningVariable() { - if (this instanceof PlanningVariableMetaModel planningVariableMetaModel) { - return planningVariableMetaModel; - } else { - throw new IllegalStateException("Genuine variable (%s) is not @%s." - .formatted(this, PlanningVariable.class.getSimpleName())); - } - } - - default PlanningListVariableMetaModel ensurePlanningListVariable() { - if (this instanceof PlanningListVariableMetaModel planningListVariableMetaModel) { - return planningListVariableMetaModel; - } else { - throw new IllegalStateException("Genuine variable (%s) is not @%s." - .formatted(this, PlanningListVariable.class.getSimpleName())); - } - } - } diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/PlanningEntityMetaModel.java b/core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/PlanningEntityMetaModel.java index 2863df5abd..67c554174e 100644 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/PlanningEntityMetaModel.java +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/domain/metamodel/PlanningEntityMetaModel.java @@ -1,11 +1,12 @@ package ai.timefold.solver.core.preview.api.domain.metamodel; +import java.util.List; + import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.domain.variable.PlanningListVariable; import ai.timefold.solver.core.api.domain.variable.PlanningVariable; -import org.jspecify.annotations.NullMarked; -import java.util.List; +import org.jspecify.annotations.NullMarked; /** * Represents the meta-model of an entity. diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/SolutionView.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/SolutionView.java index 17d1da2a67..81b3e9be42 100644 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/move/SolutionView.java +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/move/SolutionView.java @@ -52,7 +52,8 @@ public interface SolutionView { * @throws NullPointerException if the value of the list variable is null * @throws IndexOutOfBoundsException if the index is out of bounds */ - int countValues(PlanningListVariableMetaModel variableMetaModel, Entity_ entity); + int countValues(PlanningListVariableMetaModel variableMetaModel, + Entity_ entity); /** * Reads the value of a @{@link PlanningListVariable list planning variable} of a given entity at a specific index. diff --git a/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java index 033a705970..c96c176330 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java @@ -1,5 +1,18 @@ package ai.timefold.solver.core.impl.constructionheuristic; +import static ai.timefold.solver.core.config.solver.PreviewFeature.DECLARATIVE_SHADOW_VARIABLES; +import static ai.timefold.solver.core.testutil.PlannerAssert.assertCode; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.solver.Solver; import ai.timefold.solver.core.api.solver.SolverFactory; @@ -40,21 +53,10 @@ import ai.timefold.solver.core.testdomain.valuerange.entityproviding.unassignedvar.TestdataAllowsUnassignedEntityProvidingSolution; import ai.timefold.solver.core.testutil.AbstractMeterTest; import ai.timefold.solver.core.testutil.PlannerTestUtils; -import io.micrometer.core.instrument.Metrics; -import org.junit.jupiter.api.Test; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.Test; -import static ai.timefold.solver.core.config.solver.PreviewFeature.DECLARATIVE_SHADOW_VARIABLES; -import static ai.timefold.solver.core.testutil.PlannerAssert.assertCode; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.SoftAssertions.assertSoftly; +import io.micrometer.core.instrument.Metrics; class DefaultConstructionHeuristicPhaseTest extends AbstractMeterTest { diff --git a/core/src/test/java/ai/timefold/solver/core/impl/move/MoveStreamsBasedLocalSearchTest.java b/core/src/test/java/ai/timefold/solver/core/impl/move/MoveStreamsBasedLocalSearchTest.java index fd278678d2..2fae55a3f5 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/move/MoveStreamsBasedLocalSearchTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/move/MoveStreamsBasedLocalSearchTest.java @@ -1,5 +1,12 @@ package ai.timefold.solver.core.impl.move; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +import java.util.HashSet; +import java.util.Random; + import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; import ai.timefold.solver.core.config.localsearch.decider.acceptor.LocalSearchAcceptorConfig; @@ -24,16 +31,10 @@ import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataSolution; import ai.timefold.solver.core.testdomain.TestdataValue; + import org.jspecify.annotations.NonNull; import org.junit.jupiter.api.Test; -import java.util.HashSet; -import java.util.Random; - -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; - class MoveStreamsBasedLocalSearchTest { @Test @@ -92,8 +93,7 @@ void changeMoveBasedLocalSearch() { getMoveRepository(SolutionDescriptor solutionDescriptor) { var variableMetaModel = solutionDescriptor.getMetaModel() .entity(TestdataEntity.class) - .genuineVariable() - .ensurePlanningVariable(); + .planningVariable(); var moveProvider = new ChangeMoveProvider<>(variableMetaModel); var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); var moveProducer = moveProvider.apply(moveStreamFactory); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/move/streams/dataset/UniDatasetStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/move/streams/dataset/UniDatasetStreamTest.java index a2d3b9e00b..b8d81068c5 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/move/streams/dataset/UniDatasetStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/move/streams/dataset/UniDatasetStreamTest.java @@ -1,5 +1,9 @@ package ai.timefold.solver.core.impl.move.streams.dataset; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.move.streams.dataset.uni.AbstractUniDataStream; @@ -12,17 +16,15 @@ import ai.timefold.solver.core.testdomain.list.pinned.index.TestdataPinnedWithIndexListEntity; import ai.timefold.solver.core.testdomain.list.pinned.index.TestdataPinnedWithIndexListSolution; import ai.timefold.solver.core.testdomain.list.pinned.index.TestdataPinnedWithIndexListValue; -import org.junit.jupiter.api.Test; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Test; class UniDatasetStreamTest { @Test void forEachBasicVariable() { - var dataStreamFactory = new DataStreamFactory<>(TestdataSolution.buildSolutionDescriptor(), EnvironmentMode.PHASE_ASSERT); + var dataStreamFactory = + new DataStreamFactory<>(TestdataSolution.buildSolutionDescriptor(), EnvironmentMode.PHASE_ASSERT); var uniDataset = ((AbstractUniDataStream) dataStreamFactory .forEachNonDiscriminating(TestdataEntity.class, false)) .createDataset(); @@ -54,7 +56,8 @@ void forEachBasicVariable() { @Test void forEachBasicVariableIncludingNull() { - var dataStreamFactory = new DataStreamFactory<>(TestdataSolution.buildSolutionDescriptor(), EnvironmentMode.PHASE_ASSERT); + var dataStreamFactory = + new DataStreamFactory<>(TestdataSolution.buildSolutionDescriptor(), EnvironmentMode.PHASE_ASSERT); var uniDataset = ((AbstractUniDataStream) dataStreamFactory .forEachNonDiscriminating(TestdataEntity.class, true)) .createDataset(); @@ -86,7 +89,8 @@ void forEachBasicVariableIncludingNull() { @Test void forEachListVariable() { - var dataStreamFactory = new DataStreamFactory<>(TestdataListSolution.buildSolutionDescriptor(), EnvironmentMode.PHASE_ASSERT); + var dataStreamFactory = + new DataStreamFactory<>(TestdataListSolution.buildSolutionDescriptor(), EnvironmentMode.PHASE_ASSERT); var uniDataset = ((AbstractUniDataStream) dataStreamFactory .forEachNonDiscriminating(TestdataListEntity.class, false)) .createDataset(); @@ -118,7 +122,8 @@ void forEachListVariable() { @Test void forEachListVariableIncludingNull() { - var dataStreamFactory = new DataStreamFactory<>(TestdataListSolution.buildSolutionDescriptor(), EnvironmentMode.PHASE_ASSERT); + var dataStreamFactory = + new DataStreamFactory<>(TestdataListSolution.buildSolutionDescriptor(), EnvironmentMode.PHASE_ASSERT); var uniDataset = ((AbstractUniDataStream) dataStreamFactory .forEachNonDiscriminating(TestdataListEntity.class, true)) .createDataset(); @@ -166,7 +171,8 @@ private static DatasetSession createSession(DataStreamFac @Test void forEachListVariableIncludingPinned() { - var dataStreamFactory = new DataStreamFactory<>(TestdataPinnedWithIndexListSolution.buildSolutionDescriptor(), EnvironmentMode.PHASE_ASSERT); + var dataStreamFactory = new DataStreamFactory<>(TestdataPinnedWithIndexListSolution.buildSolutionDescriptor(), + EnvironmentMode.PHASE_ASSERT); var uniDataset = ((AbstractUniDataStream) dataStreamFactory .forEachNonDiscriminating(TestdataPinnedWithIndexListEntity.class, false)) @@ -209,7 +215,8 @@ void forEachListVariableIncludingPinned() { @Test void forEachListVariableIncludingPinnedAndNull() { - var dataStreamFactory = new DataStreamFactory<>(TestdataPinnedWithIndexListSolution.buildSolutionDescriptor(), EnvironmentMode.PHASE_ASSERT); + var dataStreamFactory = new DataStreamFactory<>(TestdataPinnedWithIndexListSolution.buildSolutionDescriptor(), + EnvironmentMode.PHASE_ASSERT); var uniDataset = ((AbstractUniDataStream) dataStreamFactory .forEachNonDiscriminating(TestdataPinnedWithIndexListEntity.class, true)) @@ -252,7 +259,8 @@ void forEachListVariableIncludingPinnedAndNull() { @Test void forEachListVariableExcludingPinned() { // Entities with planningPin true will be skipped. - var dataStreamFactory = new DataStreamFactory<>(TestdataPinnedWithIndexListSolution.buildSolutionDescriptor(), EnvironmentMode.PHASE_ASSERT); + var dataStreamFactory = new DataStreamFactory<>(TestdataPinnedWithIndexListSolution.buildSolutionDescriptor(), + EnvironmentMode.PHASE_ASSERT); var uniDataset = ((AbstractUniDataStream) dataStreamFactory .forEachExcludingPinned(TestdataPinnedWithIndexListEntity.class, false)) @@ -296,7 +304,8 @@ void forEachListVariableExcludingPinned() { // Entities with planningPin true wi @Test void forEachListVariableExcludingPinnedIncludingNull() { // Entities with planningPin true will be skipped. - var dataStreamFactory = new DataStreamFactory<>(TestdataPinnedWithIndexListSolution.buildSolutionDescriptor(), EnvironmentMode.PHASE_ASSERT); + var dataStreamFactory = new DataStreamFactory<>(TestdataPinnedWithIndexListSolution.buildSolutionDescriptor(), + EnvironmentMode.PHASE_ASSERT); var uniDataset = ((AbstractUniDataStream) dataStreamFactory .forEachExcludingPinned(TestdataPinnedWithIndexListEntity.class, true)) @@ -340,7 +349,8 @@ void forEachListVariableExcludingPinnedIncludingNull() { // Entities with planni @Test void forEachListVariableIncludingPinnedValues() { - var dataStreamFactory = new DataStreamFactory<>(TestdataPinnedWithIndexListSolution.buildSolutionDescriptor(), EnvironmentMode.PHASE_ASSERT); + var dataStreamFactory = new DataStreamFactory<>(TestdataPinnedWithIndexListSolution.buildSolutionDescriptor(), + EnvironmentMode.PHASE_ASSERT); var uniDataset = ((AbstractUniDataStream) dataStreamFactory .forEachNonDiscriminating(TestdataPinnedWithIndexListValue.class, false)) @@ -381,7 +391,8 @@ void forEachListVariableIncludingPinnedValues() { @Test void forEachListVariableIncludingPinnedValuesAndNull() { - var dataStreamFactory = new DataStreamFactory<>(TestdataPinnedWithIndexListSolution.buildSolutionDescriptor(), EnvironmentMode.PHASE_ASSERT); + var dataStreamFactory = new DataStreamFactory<>(TestdataPinnedWithIndexListSolution.buildSolutionDescriptor(), + EnvironmentMode.PHASE_ASSERT); var uniDataset = ((AbstractUniDataStream) dataStreamFactory .forEachNonDiscriminating(TestdataPinnedWithIndexListValue.class, true)) diff --git a/core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ChangeMoveProviderTest.java b/core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ChangeMoveProviderTest.java index 6018487c52..61405d88d0 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ChangeMoveProviderTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ChangeMoveProviderTest.java @@ -1,5 +1,8 @@ package ai.timefold.solver.core.impl.move.streams.maybeapi.provider; +import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; +import ai.timefold.solver.core.api.score.stream.Constraint; +import ai.timefold.solver.core.api.score.stream.ConstraintFactory; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; @@ -10,22 +13,18 @@ import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.score.director.SessionContext; import ai.timefold.solver.core.impl.score.director.stream.BavetConstraintStreamScoreDirectorFactory; -import ai.timefold.solver.core.testdomain.TestdataConstraintProvider; import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataSolution; import ai.timefold.solver.core.testdomain.TestdataValue; -import ai.timefold.solver.core.testdomain.unassignedvar.TestdataAllowsUnassignedConstraintProvider; import ai.timefold.solver.core.testdomain.unassignedvar.TestdataAllowsUnassignedEntity; import ai.timefold.solver.core.testdomain.unassignedvar.TestdataAllowsUnassignedSolution; -import ai.timefold.solver.core.testdomain.valuerange.entityproviding.TestdataEntityProvidingConstraintProvider; import ai.timefold.solver.core.testdomain.valuerange.entityproviding.TestdataEntityProvidingEntity; import ai.timefold.solver.core.testdomain.valuerange.entityproviding.TestdataEntityProvidingSolution; -import ai.timefold.solver.core.testdomain.valuerange.entityproviding.unassignedvar.TestdataAllowsUnassignedEntityProvidingConstraintProvider; import ai.timefold.solver.core.testdomain.valuerange.entityproviding.unassignedvar.TestdataAllowsUnassignedEntityProvidingEntity; import ai.timefold.solver.core.testdomain.valuerange.entityproviding.unassignedvar.TestdataAllowsUnassignedEntityProvidingSolution; -import ai.timefold.solver.core.testdomain.valuerange.incomplete.TestdataIncompleteValueRangeConstraintProvider; import ai.timefold.solver.core.testdomain.valuerange.incomplete.TestdataIncompleteValueRangeEntity; import ai.timefold.solver.core.testdomain.valuerange.incomplete.TestdataIncompleteValueRangeSolution; +import org.jspecify.annotations.NullMarked; import org.junit.jupiter.api.Test; import java.util.Collections; @@ -34,6 +33,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.SoftAssertions.assertSoftly; +@NullMarked class ChangeMoveProviderTest { @Test @@ -41,8 +41,7 @@ void fromSolution() { var solutionDescriptor = TestdataSolution.buildSolutionDescriptor(); var variableMetaModel = solutionDescriptor.getMetaModel() .entity(TestdataEntity.class) - .genuineVariable() - .ensurePlanningVariable(); + .planningVariable(); var solution = TestdataSolution.generateSolution(2, 2); var firstEntity = solution.getEntityList().get(0); @@ -51,7 +50,7 @@ void fromSolution() { secondEntity.setValue(null); var firstValue = solution.getValueList().get(0); var secondValue = solution.getValueList().get(1); - var scoreDirector = createScoreDirector(solutionDescriptor, new TestdataConstraintProvider(), solution); + var scoreDirector = createScoreDirector(solutionDescriptor, solution); var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); var moveProvider = new ChangeMoveProvider<>(variableMetaModel); @@ -104,8 +103,7 @@ void fromSolutionIncompleteValueRange() { var solutionDescriptor = TestdataIncompleteValueRangeSolution.buildSolutionDescriptor(); var variableMetaModel = solutionDescriptor.getMetaModel() .entity(TestdataIncompleteValueRangeEntity.class) - .genuineVariable() - .ensurePlanningVariable(); + .planningVariable(); // The point of this test is to ensure that the move provider skips values that are not in the value range. var solution = TestdataIncompleteValueRangeSolution.generateSolution(2, 2); @@ -119,7 +117,7 @@ void fromSolutionIncompleteValueRange() { var firstValue = solution.getValueList().get(0); var secondValue = solution.getValueList().get(1); var scoreDirector = - createScoreDirector(solutionDescriptor, new TestdataIncompleteValueRangeConstraintProvider(), solution); + createScoreDirector(solutionDescriptor, solution); var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); var moveProvider = new ChangeMoveProvider<>(variableMetaModel); @@ -172,14 +170,13 @@ void fromEntity() { var solutionDescriptor = TestdataEntityProvidingSolution.buildSolutionDescriptor(); var variableMetaModel = solutionDescriptor.getMetaModel() .entity(TestdataEntityProvidingEntity.class) - .genuineVariable() - .ensurePlanningVariable(); + .planningVariable(); var solution = TestdataEntityProvidingSolution.generateSolution(2, 2); var firstEntity = solution.getEntityList().get(0); var secondEntity = solution.getEntityList().get(1); var firstValue = firstEntity.getValueRange().get(0); - var scoreDirector = createScoreDirector(solutionDescriptor, new TestdataEntityProvidingConstraintProvider(), solution); + var scoreDirector = createScoreDirector(solutionDescriptor, solution); var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); var moveProvider = new ChangeMoveProvider<>(variableMetaModel); @@ -213,15 +210,14 @@ void fromEntityAllowsUnassigned() { var solutionDescriptor = TestdataAllowsUnassignedEntityProvidingSolution.buildSolutionDescriptor(); var variableMetaModel = solutionDescriptor.getMetaModel() .entity(TestdataAllowsUnassignedEntityProvidingEntity.class) - .genuineVariable() - .ensurePlanningVariable(); + .planningVariable(); var solution = TestdataAllowsUnassignedEntityProvidingSolution.generateSolution(2, 2); var firstEntity = solution.getEntityList().get(0); var secondEntity = solution.getEntityList().get(1); var firstValue = firstEntity.getValueRange().get(0); var scoreDirector = createScoreDirector(solutionDescriptor, - new TestdataAllowsUnassignedEntityProvidingConstraintProvider(), solution); + solution); var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); var moveProvider = new ChangeMoveProvider<>(variableMetaModel); @@ -275,14 +271,14 @@ void fromSolutionAllowsUnassigned() { var solutionDescriptor = TestdataAllowsUnassignedSolution.buildSolutionDescriptor(); var variableMetaModel = solutionDescriptor.getMetaModel() .entity(TestdataAllowsUnassignedEntity.class) - .genuineVariable() - .ensurePlanningVariable(); + .planningVariable(); + var solution = TestdataAllowsUnassignedSolution.generateSolution(2, 2); var firstEntity = solution.getEntityList().get(0); // Assigned to null. var secondEntity = solution.getEntityList().get(1); // Assigned to secondValue. var firstValue = solution.getValueList().get(0); // Not assigned to any entity. var secondValue = solution.getValueList().get(1); - var scoreDirector = createScoreDirector(solutionDescriptor, new TestdataAllowsUnassignedConstraintProvider(), solution); + var scoreDirector = createScoreDirector(solutionDescriptor, solution); var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); var moveProvider = new ChangeMoveProvider<>(variableMetaModel); @@ -360,7 +356,9 @@ void fromSolutionAllowsUnassigned() { } private InnerScoreDirector createScoreDirector(SolutionDescriptor solutionDescriptor, - ConstraintProvider constraintProvider, Solution_ solution) { + Solution_ solution) { + var constraintProvider = new TestingConstraintProvider(solutionDescriptor.getMetaModel().genuineEntities() + .get(0).type()); var scoreDirectorFactory = new BavetConstraintStreamScoreDirectorFactory<>(solutionDescriptor, constraintProvider, EnvironmentMode.TRACKED_FULL_ASSERT); @@ -378,4 +376,21 @@ private MoveStreamSession createSession(DefaultMoveStream return moveStreamSession; } + // The specifics of the constraint provider are not important for this test, + // as the score will never be calculated. + private record TestingConstraintProvider(Class entityClass) implements ConstraintProvider { + + @Override + public Constraint[] defineConstraints(ConstraintFactory constraintFactory) { + return new Constraint[] { alwaysPenalizingConstraint(constraintFactory) }; + } + + private Constraint alwaysPenalizingConstraint(ConstraintFactory constraintFactory) { + return constraintFactory.forEach(entityClass) + .penalize(SimpleScore.ONE) + .asConstraint("Always penalize"); + } + + } + } \ No newline at end of file diff --git a/core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ListChangeMoveProviderTest.java b/core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ListChangeMoveProviderTest.java index b39ab7dd50..f4006cbe7d 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ListChangeMoveProviderTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ListChangeMoveProviderTest.java @@ -1,5 +1,11 @@ package ai.timefold.solver.core.impl.move.streams.maybeapi.provider; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +import java.util.List; +import java.util.stream.StreamSupport; + import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintFactory; @@ -9,25 +15,26 @@ import ai.timefold.solver.core.impl.move.streams.DefaultMoveStreamFactory; import ai.timefold.solver.core.impl.move.streams.maybeapi.generic.move.ListAssignMove; import ai.timefold.solver.core.impl.move.streams.maybeapi.generic.move.ListChangeMove; +import ai.timefold.solver.core.impl.move.streams.maybeapi.generic.move.ListUnassignMove; import ai.timefold.solver.core.impl.move.streams.maybeapi.generic.provider.ListChangeMoveProvider; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.MoveStreamSession; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.score.director.SessionContext; import ai.timefold.solver.core.impl.score.director.stream.BavetConstraintStreamScoreDirectorFactory; +import ai.timefold.solver.core.preview.api.move.Move; import ai.timefold.solver.core.testdomain.list.TestdataListEntity; import ai.timefold.solver.core.testdomain.list.TestdataListSolution; -import ai.timefold.solver.core.testdomain.list.TestdataListValue; +import ai.timefold.solver.core.testdomain.list.unassignedvar.TestdataAllowsUnassignedValuesListEntity; +import ai.timefold.solver.core.testdomain.list.unassignedvar.TestdataAllowsUnassignedValuesListSolution; import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingEntity; import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingSolution; -import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingValue; -import org.jspecify.annotations.NonNull; -import org.junit.jupiter.api.Test; - -import java.util.stream.StreamSupport; +import ai.timefold.solver.core.testdomain.list.valuerange.unassignedvar.TestdataListUnassignedEntityProvidingEntity; +import ai.timefold.solver.core.testdomain.list.valuerange.unassignedvar.TestdataListUnassignedEntityProvidingSolution; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.SoftAssertions.assertSoftly; +import org.jspecify.annotations.NullMarked; +import org.junit.jupiter.api.Test; +@NullMarked class ListChangeMoveProviderTest { @Test @@ -45,7 +52,7 @@ void fromSolution() { e2.getValueList().add(initiallyAssignedValue); solution.getEntityList().forEach(TestdataListEntity::setUpShadowVariables); - var scoreDirector = createScoreDirector(solutionDescriptor, new TestdataListConstraintProvider(), solution); + var scoreDirector = createScoreDirector(solutionDescriptor, solution); var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); var moveProvider = new ListChangeMoveProvider<>(variableMetaModel); @@ -63,45 +70,45 @@ void fromSolution() { // Assign moves are generated for all three positions in e1 and e2. // Change move is generated for moving the initially assigned value from e2 to e1. - var firstMove = (ListAssignMove) moveList.get(0); + var move1 = getListAssignMove(moveList, 0); assertSoftly(softly -> { - softly.assertThat(firstMove.getDestinationEntity()).isEqualTo(e1); - softly.assertThat(firstMove.getDestinationIndex()).isEqualTo(0); - softly.assertThat(firstMove.extractPlanningEntities()) + softly.assertThat(move1.getDestinationEntity()).isEqualTo(e1); + softly.assertThat(move1.getDestinationIndex()).isEqualTo(0); + softly.assertThat(move1.extractPlanningEntities()) .containsExactly(e1); - softly.assertThat(firstMove.extractPlanningValues()) + softly.assertThat(move1.extractPlanningValues()) .containsExactly(unassignedValue); }); - var secondMove = (ListAssignMove) moveList.get(1); + var move2 = getListAssignMove(moveList, 1); assertSoftly(softly -> { - softly.assertThat(secondMove.getDestinationEntity()).isEqualTo(e2); - softly.assertThat(secondMove.getDestinationIndex()).isEqualTo(1); - softly.assertThat(secondMove.extractPlanningEntities()) + softly.assertThat(move2.getDestinationEntity()).isEqualTo(e2); + softly.assertThat(move2.getDestinationIndex()).isEqualTo(1); + softly.assertThat(move2.extractPlanningEntities()) .containsExactly(e2); - softly.assertThat(secondMove.extractPlanningValues()) + softly.assertThat(move2.extractPlanningValues()) .containsExactly(unassignedValue); }); - var thirdMove = (ListAssignMove) moveList.get(2); + var move3 = getListAssignMove(moveList, 2); assertSoftly(softly -> { - softly.assertThat(thirdMove.getDestinationEntity()).isEqualTo(e2); - softly.assertThat(thirdMove.getDestinationIndex()).isEqualTo(0); - softly.assertThat(thirdMove.extractPlanningEntities()) + softly.assertThat(move3.getDestinationEntity()).isEqualTo(e2); + softly.assertThat(move3.getDestinationIndex()).isEqualTo(0); + softly.assertThat(move3.extractPlanningEntities()) .containsExactly(e2); - softly.assertThat(thirdMove.extractPlanningValues()) + softly.assertThat(move3.extractPlanningValues()) .containsExactly(unassignedValue); }); - var fourthMove = (ListChangeMove) moveList.get(3); + var move4 = getListChangeMove(moveList, 3); assertSoftly(softly -> { - softly.assertThat(fourthMove.getSourceEntity()).isEqualTo(e2); - softly.assertThat(fourthMove.getSourceIndex()).isEqualTo(0); - softly.assertThat(fourthMove.getDestinationEntity()).isEqualTo(e1); - softly.assertThat(fourthMove.getDestinationIndex()).isEqualTo(0); - softly.assertThat(fourthMove.extractPlanningEntities()) + softly.assertThat(move4.getSourceEntity()).isEqualTo(e2); + softly.assertThat(move4.getSourceIndex()).isEqualTo(0); + softly.assertThat(move4.getDestinationEntity()).isEqualTo(e1); + softly.assertThat(move4.getDestinationIndex()).isEqualTo(0); + softly.assertThat(move4.extractPlanningEntities()) .containsExactly(e2, e1); - softly.assertThat(fourthMove.extractPlanningValues()) + softly.assertThat(move4.extractPlanningValues()) .containsExactly(initiallyAssignedValue); }); } @@ -124,7 +131,8 @@ void fromEntity() { e2.getValueList().add(initiallyAssignedValue); solution.getEntityList().forEach(TestdataListEntityProvidingEntity::setUpShadowVariables); - var scoreDirector = createScoreDirector(solutionDescriptor, new TestdataEntityProvidingListConstraintProvider(), solution); + var scoreDirector = + createScoreDirector(solutionDescriptor, solution); var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); var moveProvider = new ListChangeMoveProvider<>(variableMetaModel); @@ -143,315 +151,243 @@ void fromEntity() { // v3 is unassigned; it can be assigned to e2, but not to e1. // e2 has one value already, and therefore two possible assignments, 0 and 1. - var firstMove = (ListChangeMove) moveList.get(0); + var move1 = getListChangeMove(moveList, 0); + assertSoftly(softly -> { + softly.assertThat(move1.getSourceEntity()).isEqualTo(e2); + softly.assertThat(move1.getSourceIndex()).isEqualTo(0); + softly.assertThat(move1.getDestinationEntity()).isEqualTo(e1); + softly.assertThat(move1.getDestinationIndex()).isEqualTo(0); + softly.assertThat(move1.extractPlanningEntities()) + .containsExactly(e2, e1); + softly.assertThat(move1.extractPlanningValues()) + .containsExactly(v1); + }); + + var move2 = getListAssignMove(moveList, 1); + assertSoftly(softly -> { + softly.assertThat(move2.getDestinationEntity()).isEqualTo(e1); + softly.assertThat(move2.getDestinationIndex()).isEqualTo(0); + softly.assertThat(move2.extractPlanningEntities()) + .containsExactly(e1); + softly.assertThat(move2.extractPlanningValues()) + .containsExactly(v2); + }); + + var move3 = getListAssignMove(moveList, 2); + assertSoftly(softly -> { + softly.assertThat(move3.getDestinationEntity()).isEqualTo(e2); + softly.assertThat(move3.getDestinationIndex()).isEqualTo(1); + softly.assertThat(move3.extractPlanningEntities()) + .containsExactly(e2); + softly.assertThat(move3.extractPlanningValues()) + .containsExactly(v3); + }); + + var move4 = getListAssignMove(moveList, 3); + assertSoftly(softly -> { + softly.assertThat(move4.getDestinationEntity()).isEqualTo(e2); + softly.assertThat(move4.getDestinationIndex()).isEqualTo(0); + softly.assertThat(move4.extractPlanningEntities()) + .containsExactly(e2); + softly.assertThat(move4.extractPlanningValues()) + .containsExactly(v3); + }); + } + + @Test + void fromEntityAllowsUnassigned() { + var solutionDescriptor = TestdataListUnassignedEntityProvidingSolution.buildSolutionDescriptor(); + var variableMetaModel = solutionDescriptor.getMetaModel() + .entity(TestdataListUnassignedEntityProvidingEntity.class) + .planningListVariable(); + + var solution = TestdataListUnassignedEntityProvidingSolution.generateSolution(); + var e1 = solution.getEntityList().get(0); + var e2 = solution.getEntityList().get(1); + var v1 = solution.getValueList().get(0); + var v2 = solution.getValueList().get(1); + var v3 = solution.getValueList().get(2); + e2.getValueList().add(v1); + + var scoreDirector = createScoreDirector(solutionDescriptor, + solution); + + var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); + var moveProvider = new ListChangeMoveProvider<>(variableMetaModel); + var moveProducer = moveProvider.apply(moveStreamFactory); + var moveStreamSession = createSession(moveStreamFactory, scoreDirector); + + var moveIterable = moveProducer.getMoveIterable(moveStreamSession); + assertThat(moveIterable).hasSize(5); + + var moveList = StreamSupport.stream(moveIterable.spliterator(), false) + .toList(); + assertThat(moveList).hasSize(5); + + // v1 is assigned to e2, so it can be unassigned. + // v1 can also be moved from e2 to e1, because it's in the range for both. + // v2 is unassigned; it can be assigned to e1, but not to e2. + // v3 is unassigned; it can be assigned to e2, but not to e1. + // e2 has one value already, and therefore two possible assignments, 0 and 1. + + var move1 = getListUnassignMove(moveList, 0); + assertSoftly(softly -> { + softly.assertThat(move1.getSourceEntity()).isEqualTo(e2); + softly.assertThat(move1.getSourceIndex()).isEqualTo(0); + softly.assertThat(move1.extractPlanningEntities()) + .containsExactly(e2); + softly.assertThat(move1.extractPlanningValues()) + .containsExactly(v1); + }); + + var move2 = getListChangeMove(moveList, 1); assertSoftly(softly -> { - softly.assertThat(firstMove.getSourceEntity()).isEqualTo(e2); - softly.assertThat(firstMove.getSourceIndex()).isEqualTo(0); - softly.assertThat(firstMove.getDestinationEntity()).isEqualTo(e1); - softly.assertThat(firstMove.getDestinationIndex()).isEqualTo(0); - softly.assertThat(firstMove.extractPlanningEntities()) + softly.assertThat(move2.getSourceEntity()).isEqualTo(e2); + softly.assertThat(move2.getSourceIndex()).isEqualTo(0); + softly.assertThat(move2.getDestinationEntity()).isEqualTo(e1); + softly.assertThat(move2.getDestinationIndex()).isEqualTo(0); + softly.assertThat(move2.extractPlanningEntities()) .containsExactly(e2, e1); - softly.assertThat(firstMove.extractPlanningValues()) + softly.assertThat(move2.extractPlanningValues()) .containsExactly(v1); }); - var secondMove = (ListAssignMove) moveList.get(1); + var move3 = getListAssignMove(moveList, 2); assertSoftly(softly -> { - softly.assertThat(secondMove.getDestinationEntity()).isEqualTo(e1); - softly.assertThat(secondMove.getDestinationIndex()).isEqualTo(0); - softly.assertThat(secondMove.extractPlanningEntities()) + softly.assertThat(move3.getDestinationEntity()).isEqualTo(e1); + softly.assertThat(move3.getDestinationIndex()).isEqualTo(0); + softly.assertThat(move3.extractPlanningEntities()) .containsExactly(e1); - softly.assertThat(secondMove.extractPlanningValues()) + softly.assertThat(move3.extractPlanningValues()) .containsExactly(v2); }); - var thirdMove = (ListAssignMove) moveList.get(2); + var move4 = getListAssignMove(moveList, 3); assertSoftly(softly -> { - softly.assertThat(thirdMove.getDestinationEntity()).isEqualTo(e2); - softly.assertThat(thirdMove.getDestinationIndex()).isEqualTo(1); - softly.assertThat(thirdMove.extractPlanningEntities()) + softly.assertThat(move4.getDestinationEntity()).isEqualTo(e2); + softly.assertThat(move4.getDestinationIndex()).isEqualTo(1); + softly.assertThat(move4.extractPlanningEntities()) .containsExactly(e2); - softly.assertThat(thirdMove.extractPlanningValues()) + softly.assertThat(move4.extractPlanningValues()) .containsExactly(v3); }); - var fourthMove = (ListAssignMove) moveList.get(3); + var move5 = getListAssignMove(moveList, 4); assertSoftly(softly -> { - softly.assertThat(fourthMove.getDestinationEntity()).isEqualTo(e2); - softly.assertThat(fourthMove.getDestinationIndex()).isEqualTo(0); - softly.assertThat(fourthMove.extractPlanningEntities()) + softly.assertThat(move5.getDestinationEntity()).isEqualTo(e2); + softly.assertThat(move5.getDestinationIndex()).isEqualTo(0); + softly.assertThat(move5.extractPlanningEntities()) .containsExactly(e2); - softly.assertThat(fourthMove.extractPlanningValues()) + softly.assertThat(move5.extractPlanningValues()) .containsExactly(v3); }); } - /* - * @Test - * void fromSolutionIncompleteValueRange() { - * var solutionDescriptor = TestdataIncompleteValueRangeSolution.buildSolutionDescriptor(); - * var variableMetaModel = solutionDescriptor.getMetaModel() - * .entity(TestdataIncompleteValueRangeEntity.class) - * .genuineVariable() - * .ensurePlanningVariable(); - * - * // The point of this test is to ensure that the move provider skips values that are not in the value range. - * var solution = TestdataIncompleteValueRangeSolution.generateSolution(2, 2); - * var valueNotInValueRange = new TestdataValue("third"); - * solution.setValueListNotInValueRange(Collections.singletonList(valueNotInValueRange)); - * - * var firstEntity = solution.getEntityList().get(0); - * firstEntity.setValue(null); - * var secondEntity = solution.getEntityList().get(1); - * secondEntity.setValue(null); - * var firstValue = solution.getValueList().get(0); - * var secondValue = solution.getValueList().get(1); - * var scoreDirector = - * createScoreDirector(solutionDescriptor, new TestdataIncompleteValueRangeConstraintProvider(), solution); - * - * var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); - * var moveProvider = new ChangeMoveProvider<>(variableMetaModel); - * var moveProducer = moveProvider.apply(moveStreamFactory); - * var moveStreamSession = createSession(moveStreamFactory, scoreDirector); - * - * var moveIterable = moveProducer.getMoveIterable(moveStreamSession); - * assertThat(moveIterable).hasSize(4); - * - * var moveList = StreamSupport.stream(moveIterable.spliterator(), false) - * .map(m -> (ChangeMove) m) - * .toList(); - * assertThat(moveList).hasSize(4); - * - * var firstMove = moveList.get(0); - * assertSoftly(softly -> { - * softly.assertThat(firstMove.extractPlanningEntities()) - * .containsExactly(firstEntity); - * softly.assertThat(firstMove.extractPlanningValues()) - * .containsExactly(firstValue); - * }); - * - * var secondMove = moveList.get(1); - * assertSoftly(softly -> { - * softly.assertThat(secondMove.extractPlanningEntities()) - * .containsExactly(firstEntity); - * softly.assertThat(secondMove.extractPlanningValues()) - * .containsExactly(secondValue); - * }); - * - * var thirdMove = moveList.get(2); - * assertSoftly(softly -> { - * softly.assertThat(thirdMove.extractPlanningEntities()) - * .containsExactly(secondEntity); - * softly.assertThat(thirdMove.extractPlanningValues()) - * .containsExactly(firstValue); - * }); - * - * var fourthMove = moveList.get(3); - * assertSoftly(softly -> { - * softly.assertThat(fourthMove.extractPlanningEntities()) - * .containsExactly(secondEntity); - * softly.assertThat(fourthMove.extractPlanningValues()) - * .containsExactly(secondValue); - * }); - * } - * - * @Test - * void fromEntity() { - * var solutionDescriptor = TestdataEntityProvidingSolution.buildSolutionDescriptor(); - * var variableMetaModel = solutionDescriptor.getMetaModel() - * .entity(TestdataEntityProvidingEntity.class) - * .genuineVariable() - * .ensurePlanningVariable(); - * - * var solution = TestdataEntityProvidingSolution.generateSolution(2, 2); - * var firstEntity = solution.getEntityList().get(0); - * var secondEntity = solution.getEntityList().get(1); - * var firstValue = firstEntity.getValueRange().get(0); - * var scoreDirector = createScoreDirector(solutionDescriptor, new TestdataEntityProvidingConstraintProvider(), solution); - * - * var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); - * var moveProvider = new ChangeMoveProvider<>(variableMetaModel); - * var moveProducer = moveProvider.apply(moveStreamFactory); - * var moveStreamSession = createSession(moveStreamFactory, scoreDirector); - * - * // One move is expected: - * // - firstEntity is already assigned to firstValue, the only possible value; skip. - * // - Assign secondEntity to firstValue, - * // as it is currently assigned to secondValue, and the value range only contains firstValue. - * var moveIterable = moveProducer.getMoveIterable(moveStreamSession); - * assertThat(moveIterable).hasSize(1); - * - * var moveList = StreamSupport.stream(moveIterable.spliterator(), false) - * .map(m -> (ChangeMove) m) - * .toList(); - * assertThat(moveList).hasSize(1); - * - * var firstMove = moveList.get(0); - * assertSoftly(softly -> { - * softly.assertThat(firstMove.extractPlanningEntities()) - * .containsExactly(secondEntity); - * softly.assertThat(firstMove.extractPlanningValues()) - * .hasSize(1) - * .containsExactly(firstValue); - * }); - * } - * - * @Test - * void fromEntityAllowsUnassigned() { - * var solutionDescriptor = TestdataAllowsUnassignedEntityProvidingSolution.buildSolutionDescriptor(); - * var variableMetaModel = solutionDescriptor.getMetaModel() - * .entity(TestdataAllowsUnassignedEntityProvidingEntity.class) - * .genuineVariable() - * .ensurePlanningVariable(); - * - * var solution = TestdataAllowsUnassignedEntityProvidingSolution.generateSolution(2, 2); - * var firstEntity = solution.getEntityList().get(0); - * var secondEntity = solution.getEntityList().get(1); - * var firstValue = firstEntity.getValueRange().get(0); - * var scoreDirector = createScoreDirector(solutionDescriptor, - * new TestdataAllowsUnassignedEntityProvidingConstraintProvider(), solution); - * - * var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); - * var moveProvider = new ChangeMoveProvider<>(variableMetaModel); - * var moveProducer = moveProvider.apply(moveStreamFactory); - * var moveStreamSession = createSession(moveStreamFactory, scoreDirector); - * - * // Three moves are expected: - * // - Assign firstEntity to null, - * // as it is currently assigned to firstValue, and the value range only contains firstValue. - * // - Assign secondEntity to null and to firstValue, - * // as it is currently assigned to secondValue, and the value range only contains firstValue. - * // Null is not in the value range, but as documented, - * // null is added automatically to value ranges when allowsUnassigned is true. - * var moveIterable = moveProducer.getMoveIterable(moveStreamSession); - * assertThat(moveIterable).hasSize(3); - * - * var moveList = StreamSupport.stream(moveIterable.spliterator(), false) - * .map(m -> (ChangeMove) m) - * .toList(); - * assertThat(moveList).hasSize(3); - * - * var firstMove = moveList.get(0); - * assertSoftly(softly -> { - * softly.assertThat(firstMove.extractPlanningEntities()) - * .containsExactly(firstEntity); - * softly.assertThat(firstMove.extractPlanningValues()) - * .hasSize(1) - * .containsNull(); - * }); - * - * var secondMove = moveList.get(1); - * assertSoftly(softly -> { - * softly.assertThat(secondMove.extractPlanningEntities()) - * .containsExactly(secondEntity); - * softly.assertThat(secondMove.extractPlanningValues()) - * .hasSize(1) - * .containsNull(); - * }); - * - * var thirdMove = moveList.get(2); - * assertSoftly(softly -> { - * softly.assertThat(thirdMove.extractPlanningEntities()) - * .containsExactly(secondEntity); - * softly.assertThat(thirdMove.extractPlanningValues()) - * .containsExactly(firstValue); - * }); - * } - * - * @Test - * void fromSolutionAllowsUnassigned() { - * var solutionDescriptor = TestdataAllowsUnassignedSolution.buildSolutionDescriptor(); - * var variableMetaModel = solutionDescriptor.getMetaModel() - * .entity(TestdataAllowsUnassignedEntity.class) - * .genuineVariable() - * .ensurePlanningVariable(); - * var solution = TestdataAllowsUnassignedSolution.generateSolution(2, 2); - * var firstEntity = solution.getEntityList().get(0); // Assigned to null. - * var secondEntity = solution.getEntityList().get(1); // Assigned to secondValue. - * var firstValue = solution.getValueList().get(0); // Not assigned to any entity. - * var secondValue = solution.getValueList().get(1); - * var scoreDirector = createScoreDirector(solutionDescriptor, new TestdataAllowsUnassignedConstraintProvider(), solution); - * - * var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); - * var moveProvider = new ChangeMoveProvider<>(variableMetaModel); - * var moveProducer = moveProvider.apply(moveStreamFactory); - * var moveStreamSession = createSession(moveStreamFactory, scoreDirector); - * - * // Filters out moves that would change the value to the value the entity already has. - * // Therefore this will have 4 moves (2 entities * 2 values) as opposed to 6 (2 entities * 3 values). - * var moveIterable = moveProducer.getMoveIterable(moveStreamSession); - * assertThat(moveIterable).hasSize(4); - * - * var moveList = StreamSupport.stream(moveIterable.spliterator(), false) - * .map(m -> (ChangeMove) m) - * .toList(); - * assertThat(moveList).hasSize(4); - * - * // TODO There is a strange issue here that needs to be investigated, - * // as it potentially breaks difficulty comparators. - * - * // The node network receives: - * // firstEntity + null; filtered out - * // secondEntity + null - * // firstEntity + firstValue - * // secondEntity + firstValue - * // firstEntity + secondValue - * // secondEntity + secondValue; filtered out - * - * // Therefore the iterator receives: - * // secondEntity + null - * // firstEntity + firstValue - * // secondEntity + firstValue - * // firstEntity + secondValue - * - * // This means that secondEntity is actually encountered first, and therefore will be iterated first. - * // A strange behavior of original iteration when combined with dataset caching before picking, - * // where the node network (= cache) is fully built long before the iteration starts. - * // A possible fix would be to refactor the node network to first iterate right inputs - * // (values first in this case) - * // but wouldn't that just create a similar issue in other places? - * - * // Second entity is assigned to secondValue, therefore the applicable moves assign to null and firstValue. - * var firstMove = moveList.get(0); - * assertSoftly(softly -> { - * softly.assertThat(firstMove.extractPlanningEntities()) - * .containsExactly(secondEntity); - * softly.assertThat(firstMove.extractPlanningValues()) - * .containsExactly(new TestdataValue[] { null }); - * }); - * - * var secondMove = moveList.get(1); - * assertSoftly(softly -> { - * softly.assertThat(secondMove.extractPlanningEntities()) - * .containsExactly(secondEntity); - * softly.assertThat(secondMove.extractPlanningValues()) - * .containsExactly(firstValue); - * }); - * - * // First entity is assigned to null, therefore the applicable moves assign to firstValue and secondValue. - * var thirdMove = moveList.get(2); - * assertSoftly(softly -> { - * softly.assertThat(thirdMove.extractPlanningEntities()) - * .containsExactly(firstEntity); - * softly.assertThat(thirdMove.extractPlanningValues()) - * .containsExactly(firstValue); - * }); - * - * var fourthMove = moveList.get(3); - * assertSoftly(softly -> { - * softly.assertThat(fourthMove.extractPlanningEntities()) - * .containsExactly(firstEntity); - * softly.assertThat(fourthMove.extractPlanningValues()) - * .containsExactly(secondValue); - * }); - * - * } - * - */ + @Test + void fromSolutionAllowsUnassigned() { + var solutionDescriptor = TestdataAllowsUnassignedValuesListSolution.buildSolutionDescriptor(); + var variableMetaModel = solutionDescriptor.getMetaModel() + .entity(TestdataAllowsUnassignedValuesListEntity.class) + .planningListVariable(); + var solution = TestdataAllowsUnassignedValuesListSolution.generateUninitializedSolution(2, 2); + var e1 = solution.getEntityList().get(0); + var e2 = solution.getEntityList().get(1); + var v1 = solution.getValueList().get(0); + var v2 = solution.getValueList().get(1); + e2.getValueList().add(v1); + solution.getEntityList().forEach(TestdataAllowsUnassignedValuesListEntity::setUpShadowVariables); + + var scoreDirector = createScoreDirector(solutionDescriptor, solution); + + var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); + var moveProvider = new ListChangeMoveProvider<>(variableMetaModel); + var moveProducer = moveProvider.apply(moveStreamFactory); + var moveStreamSession = createSession(moveStreamFactory, scoreDirector); + + var moveIterable = moveProducer.getMoveIterable(moveStreamSession); + assertThat(moveIterable).hasSize(5); + + var moveList = StreamSupport.stream(moveIterable.spliterator(), false) + .toList(); + assertThat(moveList).hasSize(5); + + // v1 is assigned to e2, so it can be unassigned. + // v1 can also be moved from e2 to e1. + // v2 is unassigned; it can be assigned to e1 or e2. + // e2 has one value already, and therefore two possible assignments, 0 and 1. + + var move1 = getListUnassignMove(moveList, 0); + assertSoftly(softly -> { + softly.assertThat(move1.getSourceEntity()).isEqualTo(e2); + softly.assertThat(move1.getSourceIndex()).isEqualTo(0); + softly.assertThat(move1.extractPlanningEntities()) + .containsExactly(e2); + softly.assertThat(move1.extractPlanningValues()) + .containsExactly(v1); + }); + + var move2 = getListChangeMove(moveList, 1); + assertSoftly(softly -> { + softly.assertThat(move2.getSourceEntity()).isEqualTo(e2); + softly.assertThat(move2.getSourceIndex()).isEqualTo(0); + softly.assertThat(move2.getDestinationEntity()).isEqualTo(e1); + softly.assertThat(move2.getDestinationIndex()).isEqualTo(0); + softly.assertThat(move2.extractPlanningEntities()) + .containsExactly(e2, e1); + softly.assertThat(move2.extractPlanningValues()) + .containsExactly(v1); + }); + + var move3 = getListAssignMove(moveList, 2); + assertSoftly(softly -> { + softly.assertThat(move3.getDestinationEntity()).isEqualTo(e1); + softly.assertThat(move3.getDestinationIndex()).isEqualTo(0); + softly.assertThat(move3.extractPlanningEntities()) + .containsExactly(e1); + softly.assertThat(move3.extractPlanningValues()) + .containsExactly(v2); + }); + + var move4 = getListAssignMove(moveList, 3); + assertSoftly(softly -> { + softly.assertThat(move4.getDestinationEntity()).isEqualTo(e2); + softly.assertThat(move4.getDestinationIndex()).isEqualTo(1); + softly.assertThat(move4.extractPlanningEntities()) + .containsExactly(e2); + softly.assertThat(move4.extractPlanningValues()) + .containsExactly(v2); + }); + + var move5 = getListAssignMove(moveList, 4); + assertSoftly(softly -> { + softly.assertThat(move5.getDestinationEntity()).isEqualTo(e2); + softly.assertThat(move5.getDestinationIndex()).isEqualTo(0); + softly.assertThat(move5.extractPlanningEntities()) + .containsExactly(e2); + softly.assertThat(move5.extractPlanningValues()) + .containsExactly(v2); + }); + } + + private static ListUnassignMove + getListUnassignMove(List> moveList, int index) { + return (ListUnassignMove) moveList.get(index); + } + + private static ListChangeMove + getListChangeMove(List> moveList, int index) { + return (ListChangeMove) moveList.get(index); + } + + private static ListAssignMove + getListAssignMove(List> moveList, int index) { + return (ListAssignMove) moveList.get(index); + } private InnerScoreDirector createScoreDirector(SolutionDescriptor solutionDescriptor, - ConstraintProvider constraintProvider, Solution_ solution) { + Solution_ solution) { + var constraintProvider = new TestingConstraintProvider( + solutionDescriptor.getListVariableDescriptor().getEntityDescriptor().getEntityClass()); var scoreDirectorFactory = new BavetConstraintStreamScoreDirectorFactory<>(solutionDescriptor, constraintProvider, EnvironmentMode.TRACKED_FULL_ASSERT); @@ -460,43 +396,30 @@ void fromEntity() { return scoreDirector; } - private MoveStreamSession createSession(DefaultMoveStreamFactory moveStreamFactory, - InnerScoreDirector scoreDirector) { - var solution = scoreDirector.getWorkingSolution(); - var moveStreamSession = moveStreamFactory.createSession(new SessionContext<>(scoreDirector)); - scoreDirector.getSolutionDescriptor().visitAll(solution, moveStreamSession::insert); - moveStreamSession.settle(); - return moveStreamSession; - } - - public final class TestdataListConstraintProvider implements ConstraintProvider { + // The specifics of the constraint provider are not important for this test, + // as the score will never be calculated. + private record TestingConstraintProvider(Class entityClass) implements ConstraintProvider { @Override - public Constraint @NonNull [] defineConstraints(@NonNull ConstraintFactory constraintFactory) { + public Constraint[] defineConstraints(ConstraintFactory constraintFactory) { return new Constraint[] { alwaysPenalizingConstraint(constraintFactory) }; } private Constraint alwaysPenalizingConstraint(ConstraintFactory constraintFactory) { - return constraintFactory.forEach(TestdataListValue.class) + return constraintFactory.forEach(entityClass) .penalize(SimpleScore.ONE) .asConstraint("Always penalize"); } } - public final class TestdataEntityProvidingListConstraintProvider implements ConstraintProvider { - - @Override - public Constraint @NonNull [] defineConstraints(@NonNull ConstraintFactory constraintFactory) { - return new Constraint[] { alwaysPenalizingConstraint(constraintFactory) }; - } - - private Constraint alwaysPenalizingConstraint(ConstraintFactory constraintFactory) { - return constraintFactory.forEach(TestdataListEntityProvidingValue.class) - .penalize(SimpleScore.ONE) - .asConstraint("Always penalize"); - } - + private MoveStreamSession createSession(DefaultMoveStreamFactory moveStreamFactory, + InnerScoreDirector scoreDirector) { + var solution = scoreDirector.getWorkingSolution(); + var moveStreamSession = moveStreamFactory.createSession(new SessionContext<>(scoreDirector)); + scoreDirector.getSolutionDescriptor().visitAll(solution, moveStreamSession::insert); + moveStreamSession.settle(); + return moveStreamSession; } } \ No newline at end of file diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java index d528252162..f25005a1b2 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java @@ -1,5 +1,33 @@ package ai.timefold.solver.core.impl.solver; +import static ai.timefold.solver.core.config.heuristic.selector.entity.EntitySorterManner.DECREASING_DIFFICULTY; +import static ai.timefold.solver.core.config.heuristic.selector.entity.EntitySorterManner.DECREASING_DIFFICULTY_IF_AVAILABLE; +import static ai.timefold.solver.core.config.solver.PreviewFeature.DECLARATIVE_SHADOW_VARIABLES; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.fail; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.LongAdder; +import java.util.function.BooleanSupplier; +import java.util.stream.IntStream; + import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.score.calculator.ConstraintMatchAwareIncrementalScoreCalculator; @@ -119,9 +147,7 @@ import ai.timefold.solver.core.testutil.AbstractMeterTest; import ai.timefold.solver.core.testutil.NoChangeCustomPhaseCommand; import ai.timefold.solver.core.testutil.PlannerTestUtils; -import io.micrometer.core.instrument.Meter; -import io.micrometer.core.instrument.Metrics; -import io.micrometer.core.instrument.Tags; + import org.assertj.core.api.Assertions; import org.assertj.core.api.SoftAssertions; import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension; @@ -134,33 +160,9 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.UUID; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.atomic.LongAdder; -import java.util.function.BooleanSupplier; -import java.util.stream.IntStream; - -import static ai.timefold.solver.core.config.heuristic.selector.entity.EntitySorterManner.DECREASING_DIFFICULTY; -import static ai.timefold.solver.core.config.heuristic.selector.entity.EntitySorterManner.DECREASING_DIFFICULTY_IF_AVAILABLE; -import static ai.timefold.solver.core.config.solver.PreviewFeature.DECLARATIVE_SHADOW_VARIABLES; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.fail; -import static org.assertj.core.api.SoftAssertions.assertSoftly; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tags; @ExtendWith(SoftAssertionsExtension.class) class DefaultSolverTest extends AbstractMeterTest { @@ -2418,7 +2420,8 @@ public static final class TestingEasyScoreCalculator implements EasyScoreCalcula @NullMarked public static final class TestingListMoveProviders implements MoveProviders { @Override - public List> defineMoves(PlanningSolutionMetaModel solutionMetaModel) { + public List> + defineMoves(PlanningSolutionMetaModel solutionMetaModel) { var variableMetamodel = solutionMetaModel.entity(TestdataListEntity.class) .planningListVariable("valueList"); return List.of(new ListChangeMoveProvider<>(variableMetamodel)); diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListEntity.java index af07edbe5f..5cc9196be1 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListEntity.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListEntity.java @@ -1,16 +1,16 @@ package ai.timefold.solver.core.testdomain.list; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + import ai.timefold.solver.core.api.domain.entity.PlanningEntity; import ai.timefold.solver.core.api.domain.variable.PlanningListVariable; import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.testdomain.TestdataObject; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; - @PlanningEntity public class TestdataListEntity extends TestdataObject { diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/unassignedvar/TestdataAllowsUnassignedValuesListEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/unassignedvar/TestdataAllowsUnassignedValuesListEntity.java index 26a089f62b..ea8049200b 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/list/unassignedvar/TestdataAllowsUnassignedValuesListEntity.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/unassignedvar/TestdataAllowsUnassignedValuesListEntity.java @@ -29,7 +29,7 @@ public static TestdataAllowsUnassignedValuesListEntity createWithValues(String c return new TestdataAllowsUnassignedValuesListEntity(code, values).setUpShadowVariables(); } - TestdataAllowsUnassignedValuesListEntity setUpShadowVariables() { + public TestdataAllowsUnassignedValuesListEntity setUpShadowVariables() { for (int i = 0; i < valueList.size(); i++) { var testdataListValue = valueList.get(i); testdataListValue.setEntity(this); diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/TestdataListEntityProvidingEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/TestdataListEntityProvidingEntity.java index 87d9dc5599..67ca48e0b0 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/TestdataListEntityProvidingEntity.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/TestdataListEntityProvidingEntity.java @@ -1,5 +1,8 @@ package ai.timefold.solver.core.testdomain.list.valuerange; +import java.util.ArrayList; +import java.util.List; + import ai.timefold.solver.core.api.domain.entity.PlanningEntity; import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; import ai.timefold.solver.core.api.domain.variable.PlanningListVariable; @@ -7,9 +10,6 @@ import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.testdomain.TestdataObject; -import java.util.ArrayList; -import java.util.List; - @PlanningEntity public class TestdataListEntityProvidingEntity extends TestdataObject { diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/TestdataListEntityProvidingSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/TestdataListEntityProvidingSolution.java index cbfa683602..632a8732b8 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/TestdataListEntityProvidingSolution.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/TestdataListEntityProvidingSolution.java @@ -1,13 +1,13 @@ package ai.timefold.solver.core.testdomain.list.valuerange; +import java.util.List; + import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; import ai.timefold.solver.core.api.domain.solution.PlanningScore; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; -import java.util.List; - @PlanningSolution public class TestdataListEntityProvidingSolution { diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/unassignedvar/TestdataListUnassignedEntityProvidingSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/unassignedvar/TestdataListUnassignedEntityProvidingSolution.java index 865d4563b7..9c09728f8d 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/unassignedvar/TestdataListUnassignedEntityProvidingSolution.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/unassignedvar/TestdataListUnassignedEntityProvidingSolution.java @@ -5,6 +5,7 @@ import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; import ai.timefold.solver.core.api.domain.solution.PlanningScore; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; +import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty; import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.testdomain.TestdataValue; @@ -41,6 +42,14 @@ public void setEntityList(List enti this.entityList = entityList; } + @ProblemFactCollectionProperty + public List getValueList() { + return entityList.stream() + .flatMap(entity -> entity.getValueRange().stream()) + .distinct() + .toList(); + } + @PlanningScore public SimpleScore getScore() { return score; From 7f4744ac3b627997532ef014bc539c65cc8aa6a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Thu, 7 Aug 2025 08:47:09 +0200 Subject: [PATCH 11/18] Fix the final test --- .../provider/ListChangeMoveProvider.java | 18 +++++++++------- .../provider/ChangeMoveProviderTest.java | 13 ++++++------ .../core/impl/solver/DefaultSolverTest.java | 21 ++++++++++++++----- .../testdomain/list/TestdataListEntity.java | 1 + 4 files changed, 34 insertions(+), 19 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java index 0773c3fbe7..2c06e55039 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java @@ -1,7 +1,5 @@ package ai.timefold.solver.core.impl.move.streams.maybeapi.generic.provider; -import java.util.Objects; - import ai.timefold.solver.core.impl.move.streams.maybeapi.BiDataFilter; import ai.timefold.solver.core.impl.move.streams.maybeapi.DataJoiners; import ai.timefold.solver.core.impl.move.streams.maybeapi.generic.move.ListAssignMove; @@ -14,9 +12,10 @@ import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningListVariableMetaModel; import ai.timefold.solver.core.preview.api.domain.metamodel.PositionInList; import ai.timefold.solver.core.preview.api.domain.metamodel.UnassignedElement; - import org.jspecify.annotations.NullMarked; +import java.util.Objects; + @NullMarked public class ListChangeMoveProvider implements MoveProvider { @@ -38,10 +37,11 @@ public ListChangeMoveProvider(PlanningListVariableMetaModel { var currentPosition = solutionView.getPositionOf(variableMetaModel, value); - if (currentPosition.equals(targetPosition)) { + if (currentPosition.equals(targetPosition)) { // No change needed. return false; } if (currentPosition instanceof UnassignedElement) { + // Only assign the value if the target entity will accept it. var targetPositionInList = targetPosition.ensureAssigned(); return solutionView.isValueInRange(variableMetaModel, targetPositionInList.entity(), value); } else { @@ -53,11 +53,13 @@ public ListChangeMoveProvider(PlanningListVariableMetaModel apply(MoveStreamFactory moveStreamFactory) { - // For each unassigned value, we need to create a move to assign it to same position of some list variable. + // For each unassigned value, we need to create a move to assign it to some position of some list variable. // For each assigned value that is not pinned, we need to create: // - A move to unassign it. // - A move to reassign it to another position if assigned. diff --git a/core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ChangeMoveProviderTest.java b/core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ChangeMoveProviderTest.java index 61405d88d0..23480050af 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ChangeMoveProviderTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ChangeMoveProviderTest.java @@ -1,5 +1,11 @@ package ai.timefold.solver.core.impl.move.streams.maybeapi.provider; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +import java.util.Collections; +import java.util.stream.StreamSupport; + import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintFactory; @@ -24,15 +30,10 @@ import ai.timefold.solver.core.testdomain.valuerange.entityproviding.unassignedvar.TestdataAllowsUnassignedEntityProvidingSolution; import ai.timefold.solver.core.testdomain.valuerange.incomplete.TestdataIncompleteValueRangeEntity; import ai.timefold.solver.core.testdomain.valuerange.incomplete.TestdataIncompleteValueRangeSolution; + import org.jspecify.annotations.NullMarked; import org.junit.jupiter.api.Test; -import java.util.Collections; -import java.util.stream.StreamSupport; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.SoftAssertions.assertSoftly; - @NullMarked class ChangeMoveProviderTest { diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java index f25005a1b2..de153bfef3 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java @@ -202,7 +202,7 @@ void solveWithMoveStreams() { } @Test - void solveListVarWithMoveStreams() { + void solveWithMoveStreamsListVar() { var solverConfig = new SolverConfig() .withPreviewFeature(PreviewFeature.MOVE_STREAMS) .withSolutionClass(TestdataListSolution.class) @@ -213,9 +213,16 @@ void solveListVarWithMoveStreams() { .withPhases(new LocalSearchPhaseConfig() .withMoveProvidersClass(TestingListMoveProviders.class)); - var solution = TestdataListSolution.generateInitializedSolution(3, 2); + // Both values are on the same entity; the goal of the solver is to move one of them to the other entity. + var solution = TestdataListSolution.generateUninitializedSolution(2, 2); + var v1 = solution.getValueList().get(0); + var v2 = solution.getValueList().get(1); + var e1 = solution.getEntityList().get(0); + e1.addValue(v1); + e1.addValue(v2); + solution.getEntityList().forEach(TestdataListEntity::setUpShadowVariables); - solution = PlannerTestUtils.solve(solverConfig, solution, false); + solution = PlannerTestUtils.solve(solverConfig, solution, true); assertThat(solution).isNotNull(); } @@ -2389,12 +2396,14 @@ public void afterEntityRemoved(@NonNull Object entity) { @NullMarked public static final class TestingMoveProviders implements MoveProviders { + @Override public List> defineMoves(PlanningSolutionMetaModel solutionMetaModel) { var variableMetamodel = solutionMetaModel.entity(TestdataEntity.class) - . planningVariable("value"); + . planningVariable(); return List.of(new ChangeMoveProvider<>(variableMetamodel)); } + } /** @@ -2419,13 +2428,15 @@ public static final class TestingEasyScoreCalculator implements EasyScoreCalcula @NullMarked public static final class TestingListMoveProviders implements MoveProviders { + @Override public List> defineMoves(PlanningSolutionMetaModel solutionMetaModel) { var variableMetamodel = solutionMetaModel.entity(TestdataListEntity.class) - .planningListVariable("valueList"); + .planningListVariable(); return List.of(new ListChangeMoveProvider<>(variableMetamodel)); } + } /** diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListEntity.java index 5cc9196be1..1f2b81fec1 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListEntity.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListEntity.java @@ -73,4 +73,5 @@ public void removeValue(TestdataListValue value) { .filter(v -> !Objects.equals(v, value)) .toList(); } + } From 7d78457b8b24eb121aeed7eb44db95698e24df8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Thu, 7 Aug 2025 08:55:29 +0200 Subject: [PATCH 12/18] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../ai/timefold/solver/core/preview/api/move/SolutionView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/SolutionView.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/SolutionView.java index 81b3e9be42..1d9029fd50 100644 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/move/SolutionView.java +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/move/SolutionView.java @@ -44,7 +44,7 @@ public interface SolutionView { getValue(PlanningVariableMetaModel variableMetaModel, Entity_ entity); /** - * Reads the value of a @{@link PlanningListVariable list planning variable} and returns its length. + * Reads the value of a {@link PlanningListVariable list planning variable} and returns its length. * * @param variableMetaModel Describes the variable whose value is to be read. * @param entity The entity whose variable is to be read. From f284549024b9cdf9cb0d63190ce1c3297ec5545f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Thu, 7 Aug 2025 09:03:23 +0200 Subject: [PATCH 13/18] Formatting --- .../maybeapi/generic/provider/ListChangeMoveProvider.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java index 2c06e55039..3a908128d0 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.move.streams.maybeapi.generic.provider; +import java.util.Objects; + import ai.timefold.solver.core.impl.move.streams.maybeapi.BiDataFilter; import ai.timefold.solver.core.impl.move.streams.maybeapi.DataJoiners; import ai.timefold.solver.core.impl.move.streams.maybeapi.generic.move.ListAssignMove; @@ -12,9 +14,8 @@ import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningListVariableMetaModel; import ai.timefold.solver.core.preview.api.domain.metamodel.PositionInList; import ai.timefold.solver.core.preview.api.domain.metamodel.UnassignedElement; -import org.jspecify.annotations.NullMarked; -import java.util.Objects; +import org.jspecify.annotations.NullMarked; @NullMarked public class ListChangeMoveProvider From 46e9e5224fbfd6f2563fb5b26d2c7ba4738c3c90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Thu, 7 Aug 2025 09:06:44 +0200 Subject: [PATCH 14/18] Better name --- .../solver/core/impl/move/director/MoveDirector.java | 6 +++--- .../move/streams/maybeapi/generic/move/ListChangeMove.java | 2 +- .../solver/core/preview/api/move/MutableSolutionView.java | 2 +- .../solver/core/impl/move/director/MoveDirectorTest.java | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/director/MoveDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/move/director/MoveDirector.java index 5185460660..57ef79f09a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/director/MoveDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/director/MoveDirector.java @@ -89,7 +89,7 @@ public final void changeVariable(PlanningVariableMetaModel variableMetaModel, Entity_ sourceEntity, int sourceIndex, Entity_ destinationEntity, int destinationIndex) { if (sourceEntity == destinationEntity) { - return swapValues(variableMetaModel, sourceEntity, sourceIndex, destinationIndex); + return moveValueInList(variableMetaModel, sourceEntity, sourceIndex, destinationIndex); } var variableDescriptor = extractVariableDescriptor(variableMetaModel); externalScoreDirector.beforeListVariableChanged(variableDescriptor, sourceEntity, sourceIndex, sourceIndex + 1); @@ -107,13 +107,13 @@ public final void changeVariable(PlanningVariableMetaModel @Nullable Value_ swapValues( + public final @Nullable Value_ moveValueInList( PlanningListVariableMetaModel variableMetaModel, Entity_ entity, int sourceIndex, int destinationIndex) { if (sourceIndex == destinationIndex) { return null; } else if (sourceIndex > destinationIndex) { // Always start from the lower index. - return swapValues(variableMetaModel, entity, destinationIndex, sourceIndex); + return moveValueInList(variableMetaModel, entity, destinationIndex, sourceIndex); } var variableDescriptor = extractVariableDescriptor(variableMetaModel); var toIndex = destinationIndex + 1; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListChangeMove.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListChangeMove.java index 9db76a8ca4..43eb178563 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListChangeMove.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListChangeMove.java @@ -123,7 +123,7 @@ private Value_ getMovedValue() { @Override public void execute(MutableSolutionView solutionView) { if (sourceEntity == destinationEntity) { - planningValue = solutionView.swapValues(variableMetaModel, sourceEntity, sourceIndex, destinationIndex); + planningValue = solutionView.moveValueInList(variableMetaModel, sourceEntity, sourceIndex, destinationIndex); } else { planningValue = solutionView.moveValueBetweenLists(variableMetaModel, sourceEntity, sourceIndex, destinationEntity, destinationIndex); diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/MutableSolutionView.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/MutableSolutionView.java index 72a4de3659..87fb50a978 100644 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/move/MutableSolutionView.java +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/move/MutableSolutionView.java @@ -125,7 +125,7 @@ void changeVariable(PlanningVariableMetaModel @Nullable Value_ swapValues( + @Nullable Value_ moveValueInList( PlanningListVariableMetaModel variableMetaModel, Entity_ entity, int sourceIndex, int destinationIndex); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/move/director/MoveDirectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/move/director/MoveDirectorTest.java index d6c008cc89..6040c49c4b 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/move/director/MoveDirectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/move/director/MoveDirectorTest.java @@ -120,7 +120,7 @@ void readListVariable() { } @Test - void swapValues() { + void moveValueInList() { var solutionMetaModel = TestdataListSolution.buildSolutionDescriptor() .getMetaModel(); var variableMetaModel = solutionMetaModel.entity(TestdataListEntity.class) @@ -137,7 +137,7 @@ void swapValues() { // Swap between second and last position. var mockScoreDirector = (InnerScoreDirector) mock(InnerScoreDirector.class); var moveDirector = new MoveDirector<>(mockScoreDirector).ephemeral(); - moveDirector.swapValues(variableMetaModel, entity, 1, 2); + moveDirector.moveValueInList(variableMetaModel, entity, 1, 2); assertThat(entity.getValueList()).containsExactly(expectedValue1, expectedValue3, expectedValue2); verify(mockScoreDirector).beforeListVariableChanged(variableDescriptor, entity, 1, 3); verify(mockScoreDirector).afterListVariableChanged(variableDescriptor, entity, 1, 3); @@ -151,7 +151,7 @@ void swapValues() { // Do the same in reverse. moveDirector = new MoveDirector<>(mockScoreDirector).ephemeral(); - moveDirector.swapValues(variableMetaModel, entity, 2, 1); + moveDirector.moveValueInList(variableMetaModel, entity, 2, 1); assertThat(entity.getValueList()).containsExactly(expectedValue1, expectedValue3, expectedValue2); verify(mockScoreDirector).beforeListVariableChanged(variableDescriptor, entity, 1, 3); verify(mockScoreDirector).afterListVariableChanged(variableDescriptor, entity, 1, 3); From a5c33302660ca03ccc73b344cce5c2284add3996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Thu, 7 Aug 2025 10:01:48 +0200 Subject: [PATCH 15/18] Another better name --- .../bavet/common/AbstractNodeBuildHelper.java | 14 +++++++------- .../dataset/uni/AbstractForEachDataStream.java | 16 ++++++++-------- .../uni/ForEachFromSolutionDataStream.java | 2 +- .../uni/ForEachIncludingPinnedDataStream.java | 2 +- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractNodeBuildHelper.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractNodeBuildHelper.java index 4bb364e006..eb2dd0e85e 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractNodeBuildHelper.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractNodeBuildHelper.java @@ -1,5 +1,12 @@ package ai.timefold.solver.core.impl.bavet.common; +import ai.timefold.solver.core.impl.bavet.NodeNetwork; +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.uni.AbstractForEachUniNode; + import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -11,13 +18,6 @@ import java.util.function.Consumer; import java.util.function.UnaryOperator; -import ai.timefold.solver.core.impl.bavet.NodeNetwork; -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.uni.AbstractForEachUniNode; - public abstract class AbstractNodeBuildHelper { private final Set activeStreamSet; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractForEachDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractForEachDataStream.java index 1aa108ac8f..795c73dfca 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractForEachDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractForEachDataStream.java @@ -1,10 +1,5 @@ package ai.timefold.solver.core.impl.move.streams.dataset.uni; -import static ai.timefold.solver.core.impl.bavet.uni.AbstractForEachUniNode.LifecycleOperation; - -import java.util.Objects; -import java.util.Set; - import ai.timefold.solver.core.impl.bavet.common.TupleSource; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; @@ -12,9 +7,13 @@ import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; - import org.jspecify.annotations.NullMarked; +import java.util.Objects; +import java.util.Set; + +import static ai.timefold.solver.core.impl.bavet.uni.AbstractForEachUniNode.LifecycleOperation; + @NullMarked abstract sealed class AbstractForEachDataStream extends AbstractUniDataStream @@ -40,14 +39,15 @@ public final void collectActiveDataStreams(Set> da public final void buildNode(DataNodeBuildHelper buildHelper) { TupleLifecycle> tupleLifecycle = buildHelper.getAggregatedTupleLifecycle(childStreamList); var outputStoreSize = buildHelper.extractTupleStoreSize(this); - var node = getNode(tupleLifecycle, outputStoreSize); + var node = createNodeInstance(tupleLifecycle, outputStoreSize); if (shouldIncludeNull && node.supports(LifecycleOperation.INSERT)) { node.insert(null); } buildHelper.addNode(node, this, null); } - protected abstract AbstractForEachUniNode getNode(TupleLifecycle> tupleLifecycle, int outputStoreSize); + protected abstract AbstractForEachUniNode createNodeInstance(TupleLifecycle> tupleLifecycle, + int outputStoreSize); @Override public abstract boolean equals(Object o); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachFromSolutionDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachFromSolutionDataStream.java index d296a24434..2bf679ab63 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachFromSolutionDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachFromSolutionDataStream.java @@ -27,7 +27,7 @@ public ForEachFromSolutionDataStream(DataStreamFactory dataStreamFact } @Override - protected AbstractForEachUniNode getNode(TupleLifecycle> tupleLifecycle, int outputStoreSize) { + protected AbstractForEachUniNode createNodeInstance(TupleLifecycle> tupleLifecycle, int outputStoreSize) { return new ForEachFromSolutionUniNode<>(valueRangeDescriptor, tupleLifecycle, outputStoreSize); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachIncludingPinnedDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachIncludingPinnedDataStream.java index 5a349fe61a..642518c948 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachIncludingPinnedDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachIncludingPinnedDataStream.java @@ -22,7 +22,7 @@ public ForEachIncludingPinnedDataStream(DataStreamFactory dataStreamF } @Override - protected AbstractForEachUniNode getNode(TupleLifecycle> tupleLifecycle, int outputStoreSize) { + protected AbstractForEachUniNode createNodeInstance(TupleLifecycle> tupleLifecycle, int outputStoreSize) { return new ForEachIncludingUnassignedUniNode<>(forEachClass, tupleLifecycle, outputStoreSize); } From 17486c628161e40c007a8609c53e142220065595 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Thu, 7 Aug 2025 10:10:09 +0200 Subject: [PATCH 16/18] Remove unnecessary code --- .../core/impl/bavet/AbstractSession.java | 40 --------- .../bavet/common/AbstractNodeBuildHelper.java | 14 ++-- .../bavet/uni/AbstractForEachUniNode.java | 10 --- .../bavet/uni/ForEachFromSolutionUniNode.java | 83 ------------------- .../ForEachIncludingUnassignedUniNode.java | 5 +- .../streams/DefaultMoveStreamFactory.java | 30 +------ .../streams/dataset/DataStreamFactory.java | 11 --- .../dataset/DatasetSessionFactory.java | 1 - .../uni/AbstractForEachDataStream.java | 20 ++--- .../uni/ForEachFromSolutionDataStream.java | 52 ------------ .../uni/ForEachIncludingPinnedDataStream.java | 9 -- .../BavetConstraintStreamScoreDirector.java | 2 - 12 files changed, 21 insertions(+), 256 deletions(-) delete mode 100644 core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/ForEachFromSolutionUniNode.java delete mode 100644 core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachFromSolutionDataStream.java diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/AbstractSession.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/AbstractSession.java index edf5b1f289..b58df160d9 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/AbstractSession.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/AbstractSession.java @@ -2,36 +2,24 @@ import java.util.IdentityHashMap; import java.util.Map; -import java.util.stream.Stream; -import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.impl.bavet.uni.AbstractForEachUniNode; import ai.timefold.solver.core.impl.bavet.uni.AbstractForEachUniNode.LifecycleOperation; -import ai.timefold.solver.core.impl.score.director.SessionContext; public abstract class AbstractSession implements AutoCloseable { private final NodeNetwork nodeNetwork; - private final Map, AbstractForEachUniNode.InitializableForEachNode[]> initializeEffectiveClassToNodeArrayMap; private final Map, AbstractForEachUniNode[]> insertEffectiveClassToNodeArrayMap; private final Map, AbstractForEachUniNode[]> updateEffectiveClassToNodeArrayMap; private final Map, AbstractForEachUniNode[]> retractEffectiveClassToNodeArrayMap; protected AbstractSession(NodeNetwork nodeNetwork) { this.nodeNetwork = nodeNetwork; - this.initializeEffectiveClassToNodeArrayMap = new IdentityHashMap<>(nodeNetwork.forEachNodeCount()); this.insertEffectiveClassToNodeArrayMap = new IdentityHashMap<>(nodeNetwork.forEachNodeCount()); this.updateEffectiveClassToNodeArrayMap = new IdentityHashMap<>(nodeNetwork.forEachNodeCount()); this.retractEffectiveClassToNodeArrayMap = new IdentityHashMap<>(nodeNetwork.forEachNodeCount()); } - @SuppressWarnings({ "unchecked", "rawtypes" }) - public final void initialize(SessionContext context) { - for (var node : findInitializableNodes()) { - node.initialize(context); - } - } - public final void insert(Object fact) { var factClass = fact.getClass(); for (var node : findNodes(factClass, LifecycleOperation.INSERT)) { @@ -57,29 +45,6 @@ private AbstractForEachUniNode[] findNodes(Class factClass, Lifecycle return nodeArray; } - @SuppressWarnings("unchecked") - private AbstractForEachUniNode.InitializableForEachNode[] findInitializableNodes() { - // There will only be one solution class in the problem. - // Therefore we do not need to know what it is, and using the annotation class will serve as a unique key. - var factClass = PlanningSolution.class; - var effectiveClassToNodeArrayMap = initializeEffectiveClassToNodeArrayMap; - // Map.computeIfAbsent() would have created lambdas on the hot path, this will not. - var nodeArray = effectiveClassToNodeArrayMap.get(factClass); - if (nodeArray == null) { - nodeArray = nodeNetwork.getForEachNodes(factClass) - .flatMap(node -> { - if (node instanceof AbstractForEachUniNode.InitializableForEachNode initializableForEachNode) { - return Stream.of(initializableForEachNode); - } else { - return Stream.empty(); - } - }) - .toArray(AbstractForEachUniNode.InitializableForEachNode[]::new); - effectiveClassToNodeArrayMap.put(factClass, nodeArray); - } - return nodeArray; - } - public final void update(Object fact) { var factClass = fact.getClass(); for (var node : findNodes(factClass, LifecycleOperation.UPDATE)) { @@ -100,11 +65,6 @@ public void settle() { @Override public final void close() { - for (var node : findInitializableNodes()) { - // Initializable nodes get a supply manager, fair to assume they will be demanding supplies. - // Give them the opportunity to cancel those demands. - node.close(); - } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractNodeBuildHelper.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractNodeBuildHelper.java index eb2dd0e85e..4bb364e006 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractNodeBuildHelper.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractNodeBuildHelper.java @@ -1,12 +1,5 @@ package ai.timefold.solver.core.impl.bavet.common; -import ai.timefold.solver.core.impl.bavet.NodeNetwork; -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.uni.AbstractForEachUniNode; - import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -18,6 +11,13 @@ import java.util.function.Consumer; import java.util.function.UnaryOperator; +import ai.timefold.solver.core.impl.bavet.NodeNetwork; +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.uni.AbstractForEachUniNode; + public abstract class AbstractNodeBuildHelper { private final Set activeStreamSet; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractForEachUniNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractForEachUniNode.java index 116e705e22..93a6fc1023 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractForEachUniNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractForEachUniNode.java @@ -9,7 +9,6 @@ import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; -import ai.timefold.solver.core.impl.score.director.SessionContext; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -136,13 +135,4 @@ public enum LifecycleOperation { RETRACT } - public interface InitializableForEachNode extends AutoCloseable { - - void initialize(SessionContext context); - - @Override - void close(); // Drop the checked exception. - - } - } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/ForEachFromSolutionUniNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/ForEachFromSolutionUniNode.java deleted file mode 100644 index 5888b0294a..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/ForEachFromSolutionUniNode.java +++ /dev/null @@ -1,83 +0,0 @@ -package ai.timefold.solver.core.impl.bavet.uni; - -import java.util.Objects; - -import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; -import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; -import ai.timefold.solver.core.impl.domain.valuerange.descriptor.ValueRangeDescriptor; -import ai.timefold.solver.core.impl.score.director.SessionContext; - -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - -/** - * Node that reads a property from a planning solution. - * Since anything directly on a solution is only allowed to change with a new working solution, - * this node has the following properties: - * - *
    - *
  • Requires initialization when setting new working solution. - * Inserts at any other time are not allowed.
  • - *
  • Does not allow retracts. Items can not be removed.
  • - *
  • Updates should still be possible, since the values may be planning entities.
  • - *
- * - * @param - * @param - */ -@NullMarked -public final class ForEachFromSolutionUniNode - extends ForEachIncludingUnassignedUniNode - implements AbstractForEachUniNode.InitializableForEachNode { - - private final ValueRangeDescriptor valueRangeDescriptor; - - private boolean isInitialized = false; - - @SuppressWarnings("unchecked") - public ForEachFromSolutionUniNode(ValueRangeDescriptor valueRangeDescriptor, - TupleLifecycle> nextNodesTupleLifecycle, int outputStoreSize) { - super((Class) valueRangeDescriptor.getVariableDescriptor().getVariablePropertyType(), nextNodesTupleLifecycle, - outputStoreSize); - this.valueRangeDescriptor = Objects.requireNonNull(valueRangeDescriptor); - } - - @Override - public void initialize(SessionContext context) { - if (this.isInitialized) { // Failsafe. - throw new IllegalStateException("Impossible state: initialize() has already been called on %s." - .formatted(this)); - } else { - this.isInitialized = true; - var valueRange = context. getValueRange(valueRangeDescriptor); - var valueIterator = valueRange.createOriginalIterator(); - while (valueIterator.hasNext()) { - var value = valueIterator.next(); - super.insert(value); - } - } - } - - @Override - public void insert(@Nullable A a) { - throw new UnsupportedOperationException("Impossible state: direct insert is not supported on %s." - .formatted(this)); - } - - @Override - public void retract(@Nullable A a) { - throw new UnsupportedOperationException("Impossible state: direct retract is not supported on %s." - .formatted(this)); - } - - @Override - public boolean supports(LifecycleOperation lifecycleOperation) { - return lifecycleOperation == LifecycleOperation.UPDATE; - } - - @Override - public void close() { - // No need to do anything; initialization doesn't perform anything that'd need cleanup. - } - -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/ForEachIncludingUnassignedUniNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/ForEachIncludingUnassignedUniNode.java index 7d9879d822..a078a195ca 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/ForEachIncludingUnassignedUniNode.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/ForEachIncludingUnassignedUniNode.java @@ -7,9 +7,8 @@ import org.jspecify.annotations.Nullable; @NullMarked -public sealed class ForEachIncludingUnassignedUniNode - extends AbstractForEachUniNode - permits ForEachFromSolutionUniNode { +public final class ForEachIncludingUnassignedUniNode + extends AbstractForEachUniNode { public ForEachIncludingUnassignedUniNode(Class forEachClass, TupleLifecycle> nextNodesTupleLifecycle, int outputStoreSize) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamFactory.java index 1315be1004..eeb92db7f1 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamFactory.java @@ -1,10 +1,7 @@ package ai.timefold.solver.core.impl.move.streams; import ai.timefold.solver.core.config.solver.EnvironmentMode; -import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultPlanningListVariableMetaModel; -import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultPlanningVariableMetaModel; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; -import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor; import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.DatasetSessionFactory; import ai.timefold.solver.core.impl.move.streams.dataset.bi.AbstractBiDataStream; @@ -72,35 +69,14 @@ public UniDataStream enumerateIncludingPinned(Class sourceC public BiDataStream enumerateEntityValuePairs( GenuineVariableMetaModel variableMetaModel, UniDataStream entityDataStream) { - var variableDescriptor = getVariableDescriptor(variableMetaModel); - var valueRangeDescriptor = variableDescriptor.getValueRangeDescriptor(); var includeNull = variableMetaModel instanceof PlanningVariableMetaModel planningVariableMetaModel ? planningVariableMetaModel.allowsUnassigned() : variableMetaModel instanceof PlanningListVariableMetaModel planningListVariableMetaModel && planningListVariableMetaModel.allowsUnassignedValues(); - if (valueRangeDescriptor.canExtractValueRangeFromSolution()) { - // No need for filtering the value range; all values from solution are valid. - var stream = dataStreamFactory.forEachFromSolution(variableMetaModel, includeNull); - return entityDataStream.join(stream); - } else { - var stream = dataStreamFactory.forEachExcludingPinned(variableMetaModel.type(), includeNull); - return entityDataStream.join(stream, DataJoiners. filtering( - (solutionView, entity, value) -> solutionView.isValueInRange(variableMetaModel, entity, value))); - } - } - - private static GenuineVariableDescriptor - getVariableDescriptor(GenuineVariableMetaModel variableMetaModel) { - if (variableMetaModel instanceof DefaultPlanningVariableMetaModel planningVariableMetaModel) { - return planningVariableMetaModel.variableDescriptor(); - } else if (variableMetaModel instanceof DefaultPlanningListVariableMetaModel planningListVariableMetaModel) { - return planningListVariableMetaModel.variableDescriptor(); - } else { - throw new IllegalStateException( - "Impossible state: variable metamodel (%s) represents neither basic not list variable." - .formatted(variableMetaModel.getClass().getSimpleName())); - } + var stream = dataStreamFactory.forEachExcludingPinned(variableMetaModel.type(), includeNull); + return entityDataStream.join(stream, DataJoiners. filtering( + (solutionView, entity, value) -> solutionView.isValueInRange(variableMetaModel, entity, value))); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/DataStreamFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/DataStreamFactory.java index 200c4255bc..1b9e32c594 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/DataStreamFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/DataStreamFactory.java @@ -9,18 +9,15 @@ import java.util.stream.Stream; import ai.timefold.solver.core.config.solver.EnvironmentMode; -import ai.timefold.solver.core.impl.domain.solution.descriptor.InnerGenuineVariableMetaModel; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataset; import ai.timefold.solver.core.impl.move.streams.dataset.common.TerminalDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.uni.AbstractUniDataStream; -import ai.timefold.solver.core.impl.move.streams.dataset.uni.ForEachFromSolutionDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.uni.ForEachIncludingPinnedDataStream; import ai.timefold.solver.core.impl.move.streams.maybeapi.DataJoiners; import ai.timefold.solver.core.impl.move.streams.maybeapi.stream.UniDataStream; import ai.timefold.solver.core.impl.score.director.SessionContext; -import ai.timefold.solver.core.preview.api.domain.metamodel.GenuineVariableMetaModel; import org.jspecify.annotations.NullMarked; @@ -70,14 +67,6 @@ public UniDataStream forEachExcludingPinned(Class sourceCla return share((AbstractUniDataStream) stream); } - @SuppressWarnings("unchecked") - public UniDataStream forEachFromSolution(GenuineVariableMetaModel variableMetaModel, - boolean includeNull) { - var variableDescriptor = ((InnerGenuineVariableMetaModel) variableMetaModel).variableDescriptor(); - return share(new ForEachFromSolutionDataStream<>(this, variableDescriptor.getValueRangeDescriptor(), - includeNull)); - } - public void assertValidForEachType(Class fromType) { var problemFactOrEntityClassSet = solutionDescriptor.getProblemFactOrEntityClassSet(); /* diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/DatasetSessionFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/DatasetSessionFactory.java index b837a13636..33f8f92b0b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/DatasetSessionFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/DatasetSessionFactory.java @@ -33,7 +33,6 @@ public DatasetSession buildSession(SessionContext context) for (var datasetInstance : buildHelper.getDatasetInstanceList()) { session.registerDatasetInstance(datasetInstance.getParent(), datasetInstance); } - session.initialize(context); return session; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractForEachDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractForEachDataStream.java index 795c73dfca..b04678e3b8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractForEachDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractForEachDataStream.java @@ -1,24 +1,25 @@ package ai.timefold.solver.core.impl.move.streams.dataset.uni; +import static ai.timefold.solver.core.impl.bavet.uni.AbstractForEachUniNode.LifecycleOperation; + +import java.util.Objects; +import java.util.Set; + import ai.timefold.solver.core.impl.bavet.common.TupleSource; import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; -import ai.timefold.solver.core.impl.bavet.uni.AbstractForEachUniNode; +import ai.timefold.solver.core.impl.bavet.uni.ForEachIncludingUnassignedUniNode; import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import ai.timefold.solver.core.impl.move.streams.dataset.common.AbstractDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; -import org.jspecify.annotations.NullMarked; -import java.util.Objects; -import java.util.Set; - -import static ai.timefold.solver.core.impl.bavet.uni.AbstractForEachUniNode.LifecycleOperation; +import org.jspecify.annotations.NullMarked; @NullMarked abstract sealed class AbstractForEachDataStream extends AbstractUniDataStream implements TupleSource - permits ForEachIncludingPinnedDataStream, ForEachFromSolutionDataStream { + permits ForEachIncludingPinnedDataStream { protected final Class forEachClass; final boolean shouldIncludeNull; @@ -39,16 +40,13 @@ public final void collectActiveDataStreams(Set> da public final void buildNode(DataNodeBuildHelper buildHelper) { TupleLifecycle> tupleLifecycle = buildHelper.getAggregatedTupleLifecycle(childStreamList); var outputStoreSize = buildHelper.extractTupleStoreSize(this); - var node = createNodeInstance(tupleLifecycle, outputStoreSize); + var node = new ForEachIncludingUnassignedUniNode<>(forEachClass, tupleLifecycle, outputStoreSize); if (shouldIncludeNull && node.supports(LifecycleOperation.INSERT)) { node.insert(null); } buildHelper.addNode(node, this, null); } - protected abstract AbstractForEachUniNode createNodeInstance(TupleLifecycle> tupleLifecycle, - int outputStoreSize); - @Override public abstract boolean equals(Object o); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachFromSolutionDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachFromSolutionDataStream.java deleted file mode 100644 index 2bf679ab63..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachFromSolutionDataStream.java +++ /dev/null @@ -1,52 +0,0 @@ -package ai.timefold.solver.core.impl.move.streams.dataset.uni; - -import java.util.Objects; - -import ai.timefold.solver.core.impl.bavet.common.TupleSource; -import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; -import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; -import ai.timefold.solver.core.impl.bavet.uni.AbstractForEachUniNode; -import ai.timefold.solver.core.impl.bavet.uni.ForEachFromSolutionUniNode; -import ai.timefold.solver.core.impl.domain.valuerange.descriptor.ValueRangeDescriptor; -import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; - -import org.jspecify.annotations.NullMarked; - -@NullMarked -public final class ForEachFromSolutionDataStream - extends AbstractForEachDataStream - implements TupleSource { - - private final ValueRangeDescriptor valueRangeDescriptor; - - public ForEachFromSolutionDataStream(DataStreamFactory dataStreamFactory, - ValueRangeDescriptor valueRangeDescriptor, boolean includeNull) { - super(dataStreamFactory, (Class) valueRangeDescriptor.getVariableDescriptor().getVariablePropertyType(), - includeNull); - this.valueRangeDescriptor = Objects.requireNonNull(valueRangeDescriptor); - } - - @Override - protected AbstractForEachUniNode createNodeInstance(TupleLifecycle> tupleLifecycle, int outputStoreSize) { - return new ForEachFromSolutionUniNode<>(valueRangeDescriptor, tupleLifecycle, outputStoreSize); - } - - @Override - public boolean equals(Object o) { - return o instanceof ForEachFromSolutionDataStream that && - Objects.equals(shouldIncludeNull, that.shouldIncludeNull) && - Objects.equals(forEachClass, that.forEachClass) && - Objects.equals(valueRangeDescriptor, that.valueRangeDescriptor); - } - - @Override - public int hashCode() { - return Objects.hash(shouldIncludeNull, forEachClass, valueRangeDescriptor); - } - - @Override - public String toString() { - return "ForEachFromSolution(" + valueRangeDescriptor + ") with " + childStreamList.size() + " children"; - } - -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachIncludingPinnedDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachIncludingPinnedDataStream.java index 642518c948..de66d7e164 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachIncludingPinnedDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/ForEachIncludingPinnedDataStream.java @@ -3,10 +3,6 @@ import java.util.Objects; import ai.timefold.solver.core.impl.bavet.common.TupleSource; -import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle; -import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; -import ai.timefold.solver.core.impl.bavet.uni.AbstractForEachUniNode; -import ai.timefold.solver.core.impl.bavet.uni.ForEachIncludingUnassignedUniNode; import ai.timefold.solver.core.impl.move.streams.dataset.DataStreamFactory; import org.jspecify.annotations.NullMarked; @@ -21,11 +17,6 @@ public ForEachIncludingPinnedDataStream(DataStreamFactory dataStreamF super(dataStreamFactory, forEachClass, includeNull); } - @Override - protected AbstractForEachUniNode createNodeInstance(TupleLifecycle> tupleLifecycle, int outputStoreSize) { - return new ForEachIncludingUnassignedUniNode<>(forEachClass, tupleLifecycle, outputStoreSize); - } - @Override public boolean equals(Object o) { return o instanceof ForEachIncludingPinnedDataStream that && diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirector.java index 016b844b97..7e5af64bf0 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirector.java @@ -13,7 +13,6 @@ import ai.timefold.solver.core.impl.domain.variable.descriptor.VariableDescriptor; import ai.timefold.solver.core.impl.score.director.AbstractScoreDirector; import ai.timefold.solver.core.impl.score.director.InnerScore; -import ai.timefold.solver.core.impl.score.director.SessionContext; import ai.timefold.solver.core.impl.score.stream.bavet.BavetConstraintSession; import org.jspecify.annotations.NullMarked; @@ -56,7 +55,6 @@ public void clearShadowVariablesListenerQueue() { @Override public void setWorkingSolution(Solution_ workingSolution) { session = scoreDirectorFactory.newSession(workingSolution, constraintMatchPolicy, derived); - session.initialize(new SessionContext<>(this)); super.setWorkingSolution(workingSolution, session::insert); } From f7813b36f3f8e4e2ec367310ae1337cca179857c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Thu, 7 Aug 2025 10:18:29 +0200 Subject: [PATCH 17/18] Remove more unnecessary code --- .../core/impl/bavet/AbstractSession.java | 6 +- .../move/MoveStreamsBasedMoveRepository.java | 1 - .../streams/DefaultMoveStreamSession.java | 6 +- .../BavetConstraintStreamScoreDirector.java | 1 - .../streams/dataset/UniDatasetStreamTest.java | 380 +++++++++--------- 5 files changed, 186 insertions(+), 208 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/AbstractSession.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/AbstractSession.java index b58df160d9..38b65835ae 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/AbstractSession.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/AbstractSession.java @@ -6,7 +6,7 @@ import ai.timefold.solver.core.impl.bavet.uni.AbstractForEachUniNode; import ai.timefold.solver.core.impl.bavet.uni.AbstractForEachUniNode.LifecycleOperation; -public abstract class AbstractSession implements AutoCloseable { +public abstract class AbstractSession { private final NodeNetwork nodeNetwork; private final Map, AbstractForEachUniNode[]> insertEffectiveClassToNodeArrayMap; @@ -63,8 +63,4 @@ public void settle() { nodeNetwork.settle(); } - @Override - public final void close() { - } - } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/MoveStreamsBasedMoveRepository.java b/core/src/main/java/ai/timefold/solver/core/impl/move/MoveStreamsBasedMoveRepository.java index e37cee8369..77b52fd00c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/MoveStreamsBasedMoveRepository.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/MoveStreamsBasedMoveRepository.java @@ -89,7 +89,6 @@ public void stepEnded(AbstractStepScope stepScope) { @Override public void phaseEnded(AbstractPhaseScope phaseScope) { if (moveStreamSession != null) { - moveStreamSession.close(); moveStreamSession = null; } phaseScope.getScoreDirector().setMoveRepository(null); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamSession.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamSession.java index 2b668ff9e4..321ac4d6cb 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamSession.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamSession.java @@ -14,7 +14,7 @@ @NullMarked public final class DefaultMoveStreamSession - implements MoveStreamSession, AutoCloseable { + implements MoveStreamSession { private final DatasetSession datasetSession; private final SolutionView solutionView; @@ -52,8 +52,4 @@ public SolutionView getSolutionView() { return solutionView; } - @Override - public void close() { - datasetSession.close(); - } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirector.java index 7e5af64bf0..48e931d238 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirector.java @@ -98,7 +98,6 @@ public boolean requiresFlushing() { public void close() { super.close(); if (session != null) { - session.close(); session = null; } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/move/streams/dataset/UniDatasetStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/move/streams/dataset/UniDatasetStreamTest.java index b8d81068c5..3504d050ec 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/move/streams/dataset/UniDatasetStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/move/streams/dataset/UniDatasetStreamTest.java @@ -30,28 +30,27 @@ void forEachBasicVariable() { .createDataset(); var solution = TestdataSolution.generateSolution(2, 2); - try (var datasetSession = UniDatasetStreamTest.createSession(dataStreamFactory, solution)) { - var uniDatasetInstance = datasetSession.getInstance(uniDataset); - - var entity1 = solution.getEntityList().get(0); - var entity2 = solution.getEntityList().get(1); - - assertThat(uniDatasetInstance.iterator()) - .toIterable() - .map(t -> t.factA) - .containsExactly(entity1, entity2); - - // Make incremental changes. - var entity3 = new TestdataEntity("entity3", solution.getValueList().get(0)); - datasetSession.insert(entity3); - datasetSession.retract(entity2); - datasetSession.settle(); - - assertThat(uniDatasetInstance.iterator()) - .toIterable() - .map(t -> t.factA) - .containsExactly(entity1, entity3); - } + var datasetSession = UniDatasetStreamTest.createSession(dataStreamFactory, solution); + var uniDatasetInstance = datasetSession.getInstance(uniDataset); + + var entity1 = solution.getEntityList().get(0); + var entity2 = solution.getEntityList().get(1); + + assertThat(uniDatasetInstance.iterator()) + .toIterable() + .map(t -> t.factA) + .containsExactly(entity1, entity2); + + // Make incremental changes. + var entity3 = new TestdataEntity("entity3", solution.getValueList().get(0)); + datasetSession.insert(entity3); + datasetSession.retract(entity2); + datasetSession.settle(); + + assertThat(uniDatasetInstance.iterator()) + .toIterable() + .map(t -> t.factA) + .containsExactly(entity1, entity3); } @Test @@ -63,28 +62,27 @@ void forEachBasicVariableIncludingNull() { .createDataset(); var solution = TestdataSolution.generateSolution(2, 2); - try (var datasetSession = UniDatasetStreamTest.createSession(dataStreamFactory, solution)) { - var uniDatasetInstance = datasetSession.getInstance(uniDataset); - - var entity1 = solution.getEntityList().get(0); - var entity2 = solution.getEntityList().get(1); - - assertThat(uniDatasetInstance.iterator()) - .toIterable() - .map(t -> t.factA) - .containsExactly(null, entity1, entity2); - - // Make incremental changes. - var entity3 = new TestdataEntity("entity3", solution.getValueList().get(0)); - datasetSession.insert(entity3); - datasetSession.retract(entity2); - datasetSession.settle(); - - assertThat(uniDatasetInstance.iterator()) - .toIterable() - .map(t -> t.factA) - .containsExactly(null, entity1, entity3); - } + var datasetSession = UniDatasetStreamTest.createSession(dataStreamFactory, solution); + var uniDatasetInstance = datasetSession.getInstance(uniDataset); + + var entity1 = solution.getEntityList().get(0); + var entity2 = solution.getEntityList().get(1); + + assertThat(uniDatasetInstance.iterator()) + .toIterable() + .map(t -> t.factA) + .containsExactly(null, entity1, entity2); + + // Make incremental changes. + var entity3 = new TestdataEntity("entity3", solution.getValueList().get(0)); + datasetSession.insert(entity3); + datasetSession.retract(entity2); + datasetSession.settle(); + + assertThat(uniDatasetInstance.iterator()) + .toIterable() + .map(t -> t.factA) + .containsExactly(null, entity1, entity3); } @Test @@ -96,28 +94,27 @@ void forEachListVariable() { .createDataset(); var solution = TestdataListSolution.generateInitializedSolution(2, 2); - try (var datasetSession = createSession(dataStreamFactory, solution)) { - var uniDatasetInstance = datasetSession.getInstance(uniDataset); - - var entity1 = solution.getEntityList().get(0); - var entity2 = solution.getEntityList().get(1); - - assertThat(uniDatasetInstance.iterator()) - .toIterable() - .map(t -> t.factA) - .containsExactly(entity1, entity2); - - // Make incremental changes. - var entity3 = new TestdataListEntity("entity3"); - datasetSession.insert(entity3); - datasetSession.retract(entity2); - datasetSession.settle(); - - assertThat(uniDatasetInstance.iterator()) - .toIterable() - .map(t -> t.factA) - .containsExactly(entity1, entity3); - } + var datasetSession = createSession(dataStreamFactory, solution); + var uniDatasetInstance = datasetSession.getInstance(uniDataset); + + var entity1 = solution.getEntityList().get(0); + var entity2 = solution.getEntityList().get(1); + + assertThat(uniDatasetInstance.iterator()) + .toIterable() + .map(t -> t.factA) + .containsExactly(entity1, entity2); + + // Make incremental changes. + var entity3 = new TestdataListEntity("entity3"); + datasetSession.insert(entity3); + datasetSession.retract(entity2); + datasetSession.settle(); + + assertThat(uniDatasetInstance.iterator()) + .toIterable() + .map(t -> t.factA) + .containsExactly(entity1, entity3); } @Test @@ -129,28 +126,27 @@ void forEachListVariableIncludingNull() { .createDataset(); var solution = TestdataListSolution.generateInitializedSolution(2, 2); - try (var datasetSession = createSession(dataStreamFactory, solution)) { - var uniDatasetInstance = datasetSession.getInstance(uniDataset); - - var entity1 = solution.getEntityList().get(0); - var entity2 = solution.getEntityList().get(1); - - assertThat(uniDatasetInstance.iterator()) - .toIterable() - .map(t -> t.factA) - .containsExactly(null, entity1, entity2); - - // Make incremental changes. - var entity3 = new TestdataListEntity("entity3"); - datasetSession.insert(entity3); - datasetSession.retract(entity2); - datasetSession.settle(); - - assertThat(uniDatasetInstance.iterator()) - .toIterable() - .map(t -> t.factA) - .containsExactly(null, entity1, entity3); - } + var datasetSession = createSession(dataStreamFactory, solution); + var uniDatasetInstance = datasetSession.getInstance(uniDataset); + + var entity1 = solution.getEntityList().get(0); + var entity2 = solution.getEntityList().get(1); + + assertThat(uniDatasetInstance.iterator()) + .toIterable() + .map(t -> t.factA) + .containsExactly(null, entity1, entity2); + + // Make incremental changes. + var entity3 = new TestdataListEntity("entity3"); + datasetSession.insert(entity3); + datasetSession.retract(entity2); + datasetSession.settle(); + + assertThat(uniDatasetInstance.iterator()) + .toIterable() + .map(t -> t.factA) + .containsExactly(null, entity1, entity3); } private static DatasetSession createSession(DataStreamFactory dataStreamFactory, @@ -191,26 +187,25 @@ void forEachListVariableIncludingPinned() { unpinnedEntity.setPinned(false); unpinnedEntity.setPlanningPinToIndex(0); - try (var datasetSession = createSession(dataStreamFactory, solution)) { - var uniDatasetInstance = datasetSession.getInstance(uniDataset); - - assertThat(uniDatasetInstance.iterator()) - .toIterable() - .map(t -> t.factA) - .containsExactly(fullyPinnedEntity, partiallyPinnedEntity, unpinnedEntity); - - // Make incremental changes. - var entity4 = new TestdataPinnedWithIndexListEntity("entity4"); - entity4.setPinned(true); - datasetSession.insert(entity4); - datasetSession.retract(partiallyPinnedEntity); - datasetSession.settle(); - - assertThat(uniDatasetInstance.iterator()) - .toIterable() - .map(t -> t.factA) - .containsExactly(fullyPinnedEntity, unpinnedEntity, entity4); - } + var datasetSession = createSession(dataStreamFactory, solution); + var uniDatasetInstance = datasetSession.getInstance(uniDataset); + + assertThat(uniDatasetInstance.iterator()) + .toIterable() + .map(t -> t.factA) + .containsExactly(fullyPinnedEntity, partiallyPinnedEntity, unpinnedEntity); + + // Make incremental changes. + var entity4 = new TestdataPinnedWithIndexListEntity("entity4"); + entity4.setPinned(true); + datasetSession.insert(entity4); + datasetSession.retract(partiallyPinnedEntity); + datasetSession.settle(); + + assertThat(uniDatasetInstance.iterator()) + .toIterable() + .map(t -> t.factA) + .containsExactly(fullyPinnedEntity, unpinnedEntity, entity4); } @Test @@ -235,26 +230,25 @@ void forEachListVariableIncludingPinnedAndNull() { unpinnedEntity.setPinned(false); unpinnedEntity.setPlanningPinToIndex(0); - try (var datasetSession = createSession(dataStreamFactory, solution)) { - var uniDatasetInstance = datasetSession.getInstance(uniDataset); - - assertThat(uniDatasetInstance.iterator()) - .toIterable() - .map(t -> t.factA) - .containsExactly(null, fullyPinnedEntity, partiallyPinnedEntity, unpinnedEntity); - - // Make incremental changes. - var entity4 = new TestdataPinnedWithIndexListEntity("entity4"); - entity4.setPinned(true); - datasetSession.insert(entity4); - datasetSession.retract(partiallyPinnedEntity); - datasetSession.settle(); - - assertThat(uniDatasetInstance.iterator()) - .toIterable() - .map(t -> t.factA) - .containsExactly(null, fullyPinnedEntity, unpinnedEntity, entity4); - } + var datasetSession = createSession(dataStreamFactory, solution); + var uniDatasetInstance = datasetSession.getInstance(uniDataset); + + assertThat(uniDatasetInstance.iterator()) + .toIterable() + .map(t -> t.factA) + .containsExactly(null, fullyPinnedEntity, partiallyPinnedEntity, unpinnedEntity); + + // Make incremental changes. + var entity4 = new TestdataPinnedWithIndexListEntity("entity4"); + entity4.setPinned(true); + datasetSession.insert(entity4); + datasetSession.retract(partiallyPinnedEntity); + datasetSession.settle(); + + assertThat(uniDatasetInstance.iterator()) + .toIterable() + .map(t -> t.factA) + .containsExactly(null, fullyPinnedEntity, unpinnedEntity, entity4); } @Test @@ -280,26 +274,25 @@ void forEachListVariableExcludingPinned() { // Entities with planningPin true wi unpinnedEntity.setPinned(false); unpinnedEntity.setPlanningPinToIndex(0); - try (var datasetSession = createSession(dataStreamFactory, solution)) { - var uniDatasetInstance = datasetSession.getInstance(uniDataset); - - assertThat(uniDatasetInstance.iterator()) - .toIterable() - .map(t -> t.factA) - .containsExactly(partiallyPinnedEntity, unpinnedEntity); - - // Make incremental changes. - var entity4 = new TestdataPinnedWithIndexListEntity("entity4"); - entity4.setPinned(true); - datasetSession.insert(entity4); - datasetSession.retract(partiallyPinnedEntity); - datasetSession.settle(); - - assertThat(uniDatasetInstance.iterator()) - .toIterable() - .map(t -> t.factA) - .containsExactly(unpinnedEntity); - } + var datasetSession = createSession(dataStreamFactory, solution); + var uniDatasetInstance = datasetSession.getInstance(uniDataset); + + assertThat(uniDatasetInstance.iterator()) + .toIterable() + .map(t -> t.factA) + .containsExactly(partiallyPinnedEntity, unpinnedEntity); + + // Make incremental changes. + var entity4 = new TestdataPinnedWithIndexListEntity("entity4"); + entity4.setPinned(true); + datasetSession.insert(entity4); + datasetSession.retract(partiallyPinnedEntity); + datasetSession.settle(); + + assertThat(uniDatasetInstance.iterator()) + .toIterable() + .map(t -> t.factA) + .containsExactly(unpinnedEntity); } @Test @@ -325,26 +318,25 @@ void forEachListVariableExcludingPinnedIncludingNull() { // Entities with planni unpinnedEntity.setPinned(false); unpinnedEntity.setPlanningPinToIndex(0); - try (var datasetSession = createSession(dataStreamFactory, solution)) { - var uniDatasetInstance = datasetSession.getInstance(uniDataset); - - assertThat(uniDatasetInstance.iterator()) - .toIterable() - .map(t -> t.factA) - .containsExactly(null, partiallyPinnedEntity, unpinnedEntity); - - // Make incremental changes. - var entity4 = new TestdataPinnedWithIndexListEntity("entity4"); - entity4.setPinned(true); - datasetSession.insert(entity4); - datasetSession.retract(partiallyPinnedEntity); - datasetSession.settle(); - - assertThat(uniDatasetInstance.iterator()) - .toIterable() - .map(t -> t.factA) - .containsExactly(null, unpinnedEntity); - } + var datasetSession = createSession(dataStreamFactory, solution); + var uniDatasetInstance = datasetSession.getInstance(uniDataset); + + assertThat(uniDatasetInstance.iterator()) + .toIterable() + .map(t -> t.factA) + .containsExactly(null, partiallyPinnedEntity, unpinnedEntity); + + // Make incremental changes. + var entity4 = new TestdataPinnedWithIndexListEntity("entity4"); + entity4.setPinned(true); + datasetSession.insert(entity4); + datasetSession.retract(partiallyPinnedEntity); + datasetSession.settle(); + + assertThat(uniDatasetInstance.iterator()) + .toIterable() + .map(t -> t.factA) + .containsExactly(null, unpinnedEntity); } @Test @@ -379,14 +371,13 @@ void forEachListVariableIncludingPinnedValues() { // Properly set shadow variables based on the changes above. solution.getEntityList().forEach(TestdataPinnedWithIndexListEntity::setUpShadowVariables); - try (var datasetSession = createSession(dataStreamFactory, solution)) { - var uniDatasetInstance = datasetSession.getInstance(uniDataset); + var datasetSession = createSession(dataStreamFactory, solution); + var uniDatasetInstance = datasetSession.getInstance(uniDataset); - assertThat(uniDatasetInstance.iterator()) - .toIterable() - .map(t -> t.factA) - .containsExactly(value1, value2, value3, value4, unassignedValue); - } + assertThat(uniDatasetInstance.iterator()) + .toIterable() + .map(t -> t.factA) + .containsExactly(value1, value2, value3, value4, unassignedValue); } @Test @@ -421,14 +412,13 @@ void forEachListVariableIncludingPinnedValuesAndNull() { // Properly set shadow variables based on the changes above. solution.getEntityList().forEach(TestdataPinnedWithIndexListEntity::setUpShadowVariables); - try (var datasetSession = createSession(dataStreamFactory, solution)) { - var uniDatasetInstance = datasetSession.getInstance(uniDataset); + var datasetSession = createSession(dataStreamFactory, solution); + var uniDatasetInstance = datasetSession.getInstance(uniDataset); - assertThat(uniDatasetInstance.iterator()) - .toIterable() - .map(t -> t.factA) - .containsExactly(null, value1, value2, value3, value4, unassignedValue); - } + assertThat(uniDatasetInstance.iterator()) + .toIterable() + .map(t -> t.factA) + .containsExactly(null, value1, value2, value3, value4, unassignedValue); } @Test @@ -466,14 +456,13 @@ void forEachListVariableExcludingPinnedValues() { // Properly set shadow variables based on the changes above. solution.getEntityList().forEach(TestdataPinnedWithIndexListEntity::setUpShadowVariables); - try (var datasetSession = createSession(dataStreamFactory, solution)) { - var uniDatasetInstance = datasetSession.getInstance(uniDataset); + var datasetSession = createSession(dataStreamFactory, solution); + var uniDatasetInstance = datasetSession.getInstance(uniDataset); - assertThat(uniDatasetInstance.iterator()) - .toIterable() - .map(t -> t.factA) - .containsExactly(value2, value3, value4); - } + assertThat(uniDatasetInstance.iterator()) + .toIterable() + .map(t -> t.factA) + .containsExactly(value2, value3, value4); } @Test @@ -511,14 +500,13 @@ void forEachListVariableExcludingPinnedValuesIncludingNull() { // Properly set shadow variables based on the changes above. solution.getEntityList().forEach(TestdataPinnedWithIndexListEntity::setUpShadowVariables); - try (var datasetSession = createSession(dataStreamFactory, solution)) { - var uniDatasetInstance = datasetSession.getInstance(uniDataset); + var datasetSession = createSession(dataStreamFactory, solution); + var uniDatasetInstance = datasetSession.getInstance(uniDataset); - assertThat(uniDatasetInstance.iterator()) - .toIterable() - .map(t -> t.factA) - .containsExactly(null, value2, value3, value4); - } + assertThat(uniDatasetInstance.iterator()) + .toIterable() + .map(t -> t.factA) + .containsExactly(null, value2, value3, value4); } } \ No newline at end of file From c9dd898a0cd04ede67b8b8ac3bf5d5363a565c4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Petrovick=C3=BD?= Date: Mon, 11 Aug 2025 09:41:25 +0200 Subject: [PATCH 18/18] Address review comments --- .../streams/dataset/bi/BiMapBiDataStream.java | 4 +- .../dataset/bi/UniMapBiDataStream.java | 10 +- .../dataset/common/AbstractDataStream.java | 9 +- .../dataset/uni/AbstractUniDataStream.java | 5 +- .../dataset/uni/BiMapUniDataStream.java | 4 +- .../dataset/uni/UniMapUniDataStream.java | 4 +- .../maybeapi/generic/move/ListChangeMove.java | 14 +-- .../provider/ListChangeMoveProvider.java | 103 +++++++++++------- .../maybeapi/stream/UniDataStream.java | 8 +- 9 files changed, 93 insertions(+), 68 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiMapBiDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiMapBiDataStream.java index c802eb4b65..07cd840e8a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiMapBiDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiMapBiDataStream.java @@ -19,9 +19,9 @@ final class BiMapBiDataStream private final BiDataMapper mappingFunctionB; private @Nullable AftBridgeBiDataStream aftStream; - public BiMapBiDataStream(DataStreamFactory constraintFactory, AbstractBiDataStream parent, + public BiMapBiDataStream(DataStreamFactory dataStreamFactory, AbstractBiDataStream parent, BiDataMapper mappingFunctionA, BiDataMapper mappingFunctionB) { - super(constraintFactory, parent); + super(dataStreamFactory, parent); this.mappingFunctionA = mappingFunctionA; this.mappingFunctionB = mappingFunctionB; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/UniMapBiDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/UniMapBiDataStream.java index c0ae6a2efc..6ca4ae629a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/UniMapBiDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/UniMapBiDataStream.java @@ -13,15 +13,15 @@ import org.jspecify.annotations.Nullable; @NullMarked -final class UniMapBiDataStream +final class UniMapBiDataStream extends AbstractUniDataStream { private final BiDataMapper mappingFunction; private @Nullable AftBridgeUniDataStream aftStream; - public UniMapBiDataStream(DataStreamFactory constraintFactory, AbstractBiDataStream parent, + public UniMapBiDataStream(DataStreamFactory dataStreamFactory, AbstractBiDataStream parent, BiDataMapper mappingFunction) { - super(constraintFactory, parent); + super(dataStreamFactory, parent); this.mappingFunction = mappingFunction; } @@ -51,7 +51,7 @@ public boolean equals(Object object) { return true; if (object == null || getClass() != object.getClass()) return false; - UniMapBiDataStream that = (UniMapBiDataStream) object; + UniMapBiDataStream that = (UniMapBiDataStream) object; return Objects.equals(parent, that.parent) && Objects.equals(mappingFunction, that.mappingFunction); } @@ -63,7 +63,7 @@ public int hashCode() { @Override public String toString() { - return "BiMap()"; + return "UniMap()"; } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDataStream.java index 2378b65ef2..4451a183eb 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/AbstractDataStream.java @@ -42,12 +42,13 @@ protected boolean guaranteesDistinct() { // Node creation // ************************************************************************ - public void collectActiveDataStreams(Set> constraintStreamSet) { + public void collectActiveDataStreams(Set> dataStreamSet) { if (parent == null) { // Maybe a join/ifExists/forEach forgot to override this? - throw new IllegalStateException("Impossible state: the stream (" + this + ") does not have a parent."); + throw new IllegalStateException("Impossible state: the stream (%s) does not have a parent." + .formatted(this)); } - parent.collectActiveDataStreams(constraintStreamSet); - constraintStreamSet.add(this); + parent.collectActiveDataStreams(dataStreamSet); + dataStreamSet.add(this); } /** diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractUniDataStream.java index 2ddf62a680..430fda1502 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractUniDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractUniDataStream.java @@ -5,7 +5,6 @@ import java.util.Objects; import java.util.function.Function; -import ai.timefold.solver.core.api.score.stream.uni.UniConstraintStream; import ai.timefold.solver.core.impl.bavet.common.GroupNodeConstructor; import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple; import ai.timefold.solver.core.impl.bavet.uni.Group1Mapping0CollectorUniNode; @@ -100,13 +99,13 @@ private UniDataStream ifExistsOrNot(boolean shouldExist, UniDa } /** - * Convert the {@link UniConstraintStream} to a different {@link UniConstraintStream}, + * Convert the {@link UniDataStream} to a different {@link UniDataStream}, * containing the set of tuples resulting from applying the group key mapping function * on all tuples of the original stream. * Neither tuple of the new stream {@link Objects#equals(Object, Object)} any other. * * @param groupKeyMapping mapping function to convert each element in the stream to a different element - * @param the type of a fact in the destination {@link UniConstraintStream}'s tuple; + * @param the type of a fact in the destination {@link UniDataStream}'s tuple; * must honor {@link Object#hashCode() the general contract of hashCode}. */ protected AbstractUniDataStream groupBy(Function groupKeyMapping) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/BiMapUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/BiMapUniDataStream.java index 497549acec..a56a3d9871 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/BiMapUniDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/BiMapUniDataStream.java @@ -20,9 +20,9 @@ final class BiMapUniDataStream private final UniDataMapper mappingFunctionB; private @Nullable AftBridgeBiDataStream aftStream; - public BiMapUniDataStream(DataStreamFactory constraintFactory, AbstractUniDataStream parent, + public BiMapUniDataStream(DataStreamFactory dataStreamFactory, AbstractUniDataStream parent, UniDataMapper mappingFunctionA, UniDataMapper mappingFunctionB) { - super(constraintFactory, parent); + super(dataStreamFactory, parent); this.mappingFunctionA = mappingFunctionA; this.mappingFunctionB = mappingFunctionB; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniMapUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniMapUniDataStream.java index f3b78dca30..9eb97a6ff2 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniMapUniDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniMapUniDataStream.java @@ -18,9 +18,9 @@ final class UniMapUniDataStream private final UniDataMapper mappingFunction; private @Nullable AftBridgeUniDataStream aftStream; - public UniMapUniDataStream(DataStreamFactory constraintFactory, AbstractUniDataStream parent, + public UniMapUniDataStream(DataStreamFactory dataStreamFactory, AbstractUniDataStream parent, UniDataMapper mappingFunction) { - super(constraintFactory, parent); + super(dataStreamFactory, parent); this.mappingFunction = mappingFunction; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListChangeMove.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListChangeMove.java index 43eb178563..5b72d6a6e5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListChangeMove.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListChangeMove.java @@ -174,14 +174,12 @@ public int getDestinationIndex() { @Override public boolean equals(Object o) { - if (this == o) - return true; - if (!(o instanceof ListChangeMove that)) - return false; - return sourceIndex == that.sourceIndex && destinationIndex == that.destinationIndex - && Objects.equals(variableMetaModel, that.variableMetaModel) - && Objects.equals(sourceEntity, that.sourceEntity) - && Objects.equals(destinationEntity, that.destinationEntity); + return o instanceof ListChangeMove other + && Objects.equals(variableMetaModel, other.variableMetaModel) + && Objects.equals(sourceEntity, other.sourceEntity) + && sourceIndex == other.sourceIndex + && Objects.equals(destinationEntity, other.destinationEntity) + && destinationIndex == other.destinationIndex; } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java index 3a908128d0..ba3103c5ce 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java @@ -14,16 +14,35 @@ import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningListVariableMetaModel; import ai.timefold.solver.core.preview.api.domain.metamodel.PositionInList; import ai.timefold.solver.core.preview.api.domain.metamodel.UnassignedElement; +import ai.timefold.solver.core.preview.api.move.SolutionView; import org.jspecify.annotations.NullMarked; +/** + * For each unassigned value, creates a move to assign it to some position of some list variable. + * For each assigned value that is not pinned, creates: + * + *
    + *
  • A move to unassign it.
  • + *
  • A move to reassign it to another position if assigned.
  • + *
+ * + * To assign or reassign a value, creates: + * + *
    + *
  • A move for every unpinned value in every entity's list variable to assign the value before that position.
  • + *
  • A move for every entity to assign it to the last position in the list variable.
  • + *
+ * + * This is a generic move provider that works with any list variable; + * user-defined change move providers needn't be this complex, as they understand the specifics of the domain. + */ @NullMarked public class ListChangeMoveProvider implements MoveProvider { private final PlanningListVariableMetaModel variableMetaModel; private final BiDataFilter isValueInListFilter; - private final BiDataFilter validChangeFilter; public ListChangeMoveProvider(PlanningListVariableMetaModel variableMetaModel) { this.variableMetaModel = Objects.requireNonNull(variableMetaModel); @@ -36,51 +55,23 @@ public ListChangeMoveProvider(PlanningListVariableMetaModel { - var currentPosition = solutionView.getPositionOf(variableMetaModel, value); - if (currentPosition.equals(targetPosition)) { // No change needed. - return false; - } - if (currentPosition instanceof UnassignedElement) { - // Only assign the value if the target entity will accept it. - var targetPositionInList = targetPosition.ensureAssigned(); - return solutionView.isValueInRange(variableMetaModel, targetPositionInList.entity(), value); - } else { - if (!(targetPosition instanceof PositionInList targetPositionInList)) { // Unassigning a value. - return true; - } - var currentPositionInList = currentPosition.ensureAssigned(); - if (currentPositionInList.entity() == targetPositionInList.entity()) { - var valueCount = solutionView.countValues(variableMetaModel, currentPositionInList.entity()); - if (valueCount == 1) { - return false; // The value is already in the list, and it is the only one. - } else if (targetPositionInList.index() == valueCount) { - // The value is already in the list, and we are trying to move it past the end of the list. - return false; - } else { // Same list, same position; ignore. - return currentPositionInList.index() != targetPositionInList.index(); - } - } else { // We can move freely between entities, assuming the target entity accepts the value. - return solutionView.isValueInRange(variableMetaModel, targetPositionInList.entity(), value); - } - } - }; } @Override public MoveProducer apply(MoveStreamFactory moveStreamFactory) { - // For each unassigned value, we need to create a move to assign it to some position of some list variable. - // For each assigned value that is not pinned, we need to create: - // - A move to unassign it. - // - A move to reassign it to another position if assigned. - // To assign or reassign a value, we need to create: - // - A move for every unpinned value in every entity's list variable to assign the value before that position. - // - A move for every entity to assign it to the last position in the list variable. + // Stream with unpinned entities; + // includes null if the variable allows unassigned values. var unpinnedEntities = moveStreamFactory.enumerate(variableMetaModel.entity().type(), variableMetaModel.allowsUnassignedValues()); + // Stream with unpinned values, which are assigned to any list variable; + // always includes null so that we can later create a position at the end of the list, + // i.e. with no value after it. var unpinnedValues = moveStreamFactory.enumerate(variableMetaModel.type(), true) .filter((solutionView, value) -> value == null || solutionView.getPositionOf(variableMetaModel, value) instanceof PositionInList); + // Joins the two previous streams to create pairs of (entity, value), + // eliminating values which do not match that entity's value range. + // It maps these pairs to expected target positions in that entity's list variable. var entityValuePairs = unpinnedEntities.join(unpinnedValues, DataJoiners.filtering(isValueInListFilter)) .map((solutionView, entity, value) -> { if (entity == null) { // Null entity means we need to unassign the value. @@ -94,8 +85,12 @@ public MoveProducer apply(MoveStreamFactory moveStreamFact } }) .distinct(); + // Finally the stream of these positions is joined with the stream of all existing values, + // filtering out those which would not result in a valid move. var dataStream = moveStreamFactory.enumerate(variableMetaModel.type(), false) - .join(entityValuePairs, DataJoiners.filtering(validChangeFilter)); + .join(entityValuePairs, DataJoiners.filtering(this::isValidChange)); + // When picking from this stream, we decide what kind of move we need to create, + // based on whether the value is assigned or unassigned. return moveStreamFactory.pick(dataStream) .asMove((solutionView, value, targetPosition) -> { var currentPosition = solutionView.getPositionOf(variableMetaModel, Objects.requireNonNull(value)); @@ -115,4 +110,36 @@ public MoveProducer apply(MoveStreamFactory moveStreamFact }); } + private boolean isValidChange(SolutionView solutionView, Value_ value, ElementPosition targetPosition) { + var currentPosition = solutionView.getPositionOf(variableMetaModel, value); + if (currentPosition.equals(targetPosition)) { // No change needed. + return false; + } + + if (currentPosition instanceof UnassignedElement) { // Only assign the value if the target entity will accept it. + var targetPositionInList = targetPosition.ensureAssigned(); + return solutionView.isValueInRange(variableMetaModel, targetPositionInList.entity(), value); + } + + if (!(targetPosition instanceof PositionInList targetPositionInList)) { // Unassigning a value. + return true; + } + + var currentPositionInList = currentPosition.ensureAssigned(); + if (currentPositionInList.entity() == targetPositionInList.entity()) { // The value is already in the list. + + var valueCount = solutionView.countValues(variableMetaModel, currentPositionInList.entity()); + if (valueCount == 1) { // The value is the only value in the list; no change. + return false; + } else if (targetPositionInList.index() == valueCount) { // Trying to move the value past the end of the list. + return false; + } else { // Same list, same position; ignore. + return currentPositionInList.index() != targetPositionInList.index(); + } + } + + // We can move freely between entities, assuming the target entity accepts the value. + return solutionView.isValueInRange(variableMetaModel, targetPositionInList.entity(), value); + } + } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/UniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/UniDataStream.java index 216e453f9a..7135e22ac5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/UniDataStream.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/stream/UniDataStream.java @@ -142,7 +142,7 @@ default UniDataStream ifNotExistsOther(Class otherClass, BiData *
  • Bijectivity. * No two input tuples should map to the same output tuple, * or to tuples that are {@link Object#equals(Object) equal}. - * Not following this recommendation creates a constraint stream with duplicate tuples, + * Not following this recommendation creates a data stream with duplicate tuples, * and may force you to use {@link #distinct()} later, which comes at a performance cost.
  • *
  • Immutable data carriers. * The objects returned by the mapping function should be identified by their contents and nothing else. @@ -153,13 +153,13 @@ default UniDataStream ifNotExistsOther(Class otherClass, BiData * * *

    - * Simple example: assuming a constraint stream of tuples of {@code Person}s + * Simple example: assuming a data stream of tuples of {@code Person}s * {@code [Ann(age = 20), Beth(age = 25), Cathy(age = 30)]}, * calling {@code map(Person::getAge)} on such stream will produce a stream of {@link Integer}s * {@code [20, 25, 30]}, * *

    - * Example with a non-bijective mapping function: assuming a constraint stream of tuples of {@code Person}s + * Example with a non-bijective mapping function: assuming a data stream of tuples of {@code Person}s * {@code [Ann(age = 20), Beth(age = 25), Cathy(age = 30), David(age = 30), Eric(age = 20)]}, * calling {@code map(Person::getAge)} on such stream will produce a stream of {@link Integer}s * {@code [20, 25, 30, 30, 20]}. @@ -189,7 +189,7 @@ BiDataStream map(UniDataMapp * (No two tuples will {@link Object#equals(Object) equal}.) * *

    - * By default, tuples going through a constraint stream are distinct. + * By default, tuples going through a data stream are distinct. * However, operations such as {@link #map(UniDataMapper)} may create a stream which breaks that promise. * By calling this method on such a stream, * duplicate copies of the same tuple will be omitted at a performance cost.