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..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 @@ -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 { +public abstract class AbstractSession { 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)) { @@ -98,13 +63,4 @@ public void settle() { nodeNetwork.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/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/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 0229c1284f..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 @@ -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/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/director/MoveDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/move/director/MoveDirector.java index 04ccf2efe9..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 @@ -185,6 +185,12 @@ 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/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 4e1414ec6d..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,13 +1,11 @@ package ai.timefold.solver.core.impl.move.streams; -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.config.solver.EnvironmentMode; 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.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; @@ -28,14 +26,14 @@ 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); } public DefaultMoveStreamSession createSession(SessionContext context) { var session = datasetSessionFactory.buildSession(context); - return new DefaultMoveStreamSession<>(session, context.workingSolution()); + return new DefaultMoveStreamSession<>(session, context.solutionView()); } @Override @@ -71,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/DefaultMoveStreamSession.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/DefaultMoveStreamSession.java index 132c976fdc..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 @@ -2,25 +2,26 @@ 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.dataset.bi.BiDataset; +import ai.timefold.solver.core.impl.move.streams.dataset.bi.BiDatasetInstance; +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; @NullMarked public final class DefaultMoveStreamSession - implements MoveStreamSession, AutoCloseable { + implements MoveStreamSession { 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,12 +48,8 @@ public void settle() { datasetSession.settle(); } - public Solution_ getWorkingSolution() { - return workingSolution; + public SolutionView getSolutionView() { + return solutionView; } - @Override - public void close() { - datasetSession.close(); - } } 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 42362d5b92..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 @@ -8,11 +8,12 @@ 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.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; @@ -42,7 +43,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 +52,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 +79,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..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 @@ -9,11 +9,12 @@ 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.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; +import ai.timefold.solver.core.preview.api.move.SolutionView; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -49,7 +50,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 +63,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 +71,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 +102,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/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/AbstractBiDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractBiDataStream.java deleted file mode 100644 index fcb49755c8..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/AbstractBiDataStream.java +++ /dev/null @@ -1,32 +0,0 @@ -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.stream.BiDataStream; - -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - -@NullMarked -public abstract class AbstractBiDataStream extends AbstractDataStream - implements BiDataStream { - - protected AbstractBiDataStream(DataStreamFactory dataStreamFactory) { - super(dataStreamFactory, null); - } - - protected AbstractBiDataStream(DataStreamFactory dataStreamFactory, - @Nullable AbstractDataStream parent) { - super(dataStreamFactory, parent); - } - - @Override - public final BiDataStream filter(BiDataFilter filter) { - return shareAndAddChild(new FilterBiDataStream<>(dataStreamFactory, this, filter)); - } - - 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/DataStreamFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/DataStreamFactory.java index a70f79b977..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 @@ -8,13 +8,16 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import ai.timefold.solver.core.impl.domain.solution.descriptor.InnerGenuineVariableMetaModel; +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.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.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; @@ -22,10 +25,12 @@ 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) { @@ -62,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(); /* @@ -126,6 +123,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/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..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 @@ -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; @@ -32,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/ForEachFromSolutionDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/ForEachFromSolutionDataStream.java deleted file mode 100644 index aa2aaeb166..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/ForEachFromSolutionDataStream.java +++ /dev/null @@ -1,50 +0,0 @@ -package ai.timefold.solver.core.impl.move.streams.dataset; - -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 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 getNode(TupleLifecycle> tupleLifecycle, int outputStoreSize) { - return new ForEachFromSolutionUniNode<>(valueRangeDescriptor, tupleLifecycle, outputStoreSize); - } - - @Override - public boolean equals(Object o) { - return o instanceof ForEachFromSolutionDataStream that && - Objects.equals(forEachClass, that.forEachClass) && - Objects.equals(valueRangeDescriptor, that.valueRangeDescriptor); - } - - @Override - public int hashCode() { - return Objects.hash(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/bi/AbstractBiDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/AbstractBiDataStream.java new file mode 100644 index 0000000000..04edf5f270 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/AbstractBiDataStream.java @@ -0,0 +1,78 @@ +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; +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; +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; + +@NullMarked +public abstract class AbstractBiDataStream extends AbstractDataStream + implements BiDataStream { + + protected AbstractBiDataStream(DataStreamFactory dataStreamFactory) { + super(dataStreamFactory, null); + } + + protected AbstractBiDataStream(DataStreamFactory dataStreamFactory, + @Nullable AbstractDataStream parent) { + super(dataStreamFactory, parent); + } + + @Override + public final BiDataStream filter(BiDataFilter filter) { + 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)); + return dataStreamFactory.share(new AftBridgeUniDataStream<>(dataStreamFactory, stream), stream::setAftBridge); + } + + @Override + 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); + } + + @Override + public AbstractBiDataStream distinct() { + if (guaranteesDistinct()) { + return this; // Already distinct, no need to create a new stream. + } + return groupBy(ConstantLambdaUtils.biPickFirst(), ConstantLambdaUtils.biPickSecond()); + } + + 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/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..72a3667dbe 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,6 +1,8 @@ -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; 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 88% 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..2f7c2e9dfd 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; @@ -33,6 +35,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/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..9a06430107 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiGroupBiDataStream.java @@ -0,0 +1,58 @@ +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; + +@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/bi/BiMapBiDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiMapBiDataStream.java new file mode 100644 index 0000000000..07cd840e8a --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/BiMapBiDataStream.java @@ -0,0 +1,72 @@ +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; + +@NullMarked +final class BiMapBiDataStream + extends AbstractBiDataStream { + + private final BiDataMapper mappingFunctionA; + private final BiDataMapper mappingFunctionB; + private @Nullable AftBridgeBiDataStream aftStream; + + public BiMapBiDataStream(DataStreamFactory dataStreamFactory, AbstractBiDataStream parent, + BiDataMapper mappingFunctionA, BiDataMapper mappingFunctionB) { + super(dataStreamFactory, 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/FilterBiDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/FilterBiDataStream.java similarity index 92% 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..2cc285e295 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; 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 95% 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..a1997fcaf1 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; 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/bi/UniMapBiDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/UniMapBiDataStream.java new file mode 100644 index 0000000000..6ca4ae629a --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/bi/UniMapBiDataStream.java @@ -0,0 +1,69 @@ +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; + +@NullMarked +final class UniMapBiDataStream + extends AbstractUniDataStream { + + private final BiDataMapper mappingFunction; + private @Nullable AftBridgeUniDataStream aftStream; + + public UniMapBiDataStream(DataStreamFactory dataStreamFactory, AbstractBiDataStream parent, + BiDataMapper mappingFunction) { + super(dataStreamFactory, 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; + UniMapBiDataStream that = (UniMapBiDataStream) 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 "UniMap()"; + } + +} 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 79% 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 8e902cc071..4451a183eb 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,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.ArrayList; import java.util.List; @@ -6,7 +6,7 @@ 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; @@ -29,16 +29,26 @@ public final > Stream_ shareAndAdd return dataStreamFactory.share(stream, childStreamList::add); } + protected boolean guaranteesDistinct() { + 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; + } + } + // ************************************************************************ // 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/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..99fb5a8bfc 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,9 +1,10 @@ -package ai.timefold.solver.core.impl.move.streams.dataset; +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; 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 85% 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..28d9744760 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; @@ -25,11 +25,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/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 new file mode 100644 index 0000000000..343624b86c --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/common/bridge/AftBridgeBiDataStream.java @@ -0,0 +1,42 @@ +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.common.DataNodeBuildHelper; + +import org.jspecify.annotations.NullMarked; + +@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..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 @@ -3,10 +3,10 @@ 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.AbstractDataStream; import ai.timefold.solver.core.impl.move.streams.dataset.common.DataNodeBuildHelper; +import ai.timefold.solver.core.impl.move.streams.dataset.uni.AbstractUniDataStream; import org.jspecify.annotations.NullMarked; @@ -31,7 +31,7 @@ public boolean equals(Object o) { @Override public int hashCode() { - return parent.hashCode(); + return Objects.requireNonNull(parent).hashCode(); } @Override 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..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.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.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/AbstractForEachDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractForEachDataStream.java similarity index 77% 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..b04678e3b8 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; @@ -8,7 +8,9 @@ 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; @@ -17,10 +19,10 @@ abstract sealed class AbstractForEachDataStream extends AbstractUniDataStream implements TupleSource - permits ForEachIncludingPinnedDataStream, ForEachFromSolutionDataStream { + permits ForEachIncludingPinnedDataStream { protected final Class forEachClass; - private final boolean shouldIncludeNull; + final boolean shouldIncludeNull; protected AbstractForEachDataStream(DataStreamFactory dataStreamFactory, Class forEachClass, boolean includeNull) { @@ -38,15 +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 = getNode(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 getNode(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/AbstractUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/AbstractUniDataStream.java similarity index 51% 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 e8188c6ddf..430fda1502 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,13 +1,27 @@ -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.common.GroupNodeConstructor.oneKeyGroupBy; + +import java.util.Objects; +import java.util.function.Function; + +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; +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; 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; +import ai.timefold.solver.core.impl.util.ConstantLambdaUtils; -import org.jspecify.annotations.NonNull; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -30,8 +44,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); @@ -46,8 +59,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); } @@ -86,6 +98,52 @@ private UniDataStream ifExistsOrNot(boolean shouldExist, UniDa joinerComber.mergedJoiner(), joinerComber.mergedFiltering()), childStreamList::add); } + /** + * 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 UniDataStream}'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)); + return dataStreamFactory.share(new AftBridgeUniDataStream<>(dataStreamFactory, stream), stream::setAftBridge); + } + + @Override + public BiDataStream map(UniDataMapper mappingA, + UniDataMapper mappingB) { + var stream = shareAndAddChild(new BiMapUniDataStream<>(dataStreamFactory, this, mappingA, mappingB)); + return dataStreamFactory.share(new AftBridgeBiDataStream<>(dataStreamFactory, stream), stream::setAftBridge); + } + + @Override + public AbstractUniDataStream distinct() { + if (guaranteesDistinct()) { + return this; // Already distinct, no need to create a new stream. + } + return groupBy(ConstantLambdaUtils.identity()); + } + 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/dataset/uni/BiMapUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/BiMapUniDataStream.java new file mode 100644 index 0000000000..a56a3d9871 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/BiMapUniDataStream.java @@ -0,0 +1,73 @@ +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; + +@NullMarked +final class BiMapUniDataStream + extends AbstractBiDataStream { + + private final UniDataMapper mappingFunctionA; + private final UniDataMapper mappingFunctionB; + private @Nullable AftBridgeBiDataStream aftStream; + + public BiMapUniDataStream(DataStreamFactory dataStreamFactory, AbstractUniDataStream parent, + UniDataMapper mappingFunctionA, UniDataMapper mappingFunctionB) { + super(dataStreamFactory, 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; + BiMapUniDataStream that = (BiMapUniDataStream) 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/FilterUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/FilterUniDataStream.java similarity index 92% 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..5c03988605 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,9 +1,10 @@ -package ai.timefold.solver.core.impl.move.streams.dataset; +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; 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 59% 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..de66d7e164 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,12 +1,9 @@ -package ai.timefold.solver.core.impl.move.streams.dataset; +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; @@ -20,20 +17,16 @@ public ForEachIncludingPinnedDataStream(DataStreamFactory dataStreamF super(dataStreamFactory, forEachClass, includeNull); } - @Override - protected AbstractForEachUniNode getNode(TupleLifecycle> tupleLifecycle, int outputStoreSize) { - return new ForEachIncludingUnassignedUniNode<>(forEachClass, tupleLifecycle, outputStoreSize); - } - @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/dataset/IfExistsUniDataStream.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/IfExistsUniDataStream.java similarity index 96% 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..f9a93b8fb9 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; 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..11225e0cf4 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,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.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; 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 78% 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..b0aa6205ba 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; @@ -25,6 +27,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/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..f68580ac0e --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniGroupUniDataStream.java @@ -0,0 +1,66 @@ +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; + +@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/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 new file mode 100644 index 0000000000..9eb97a6ff2 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/dataset/uni/UniMapUniDataStream.java @@ -0,0 +1,79 @@ +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; + +@NullMarked +final class UniMapUniDataStream + extends AbstractUniDataStream { + + private final UniDataMapper mappingFunction; + private @Nullable AftBridgeUniDataStream aftStream; + + public UniMapUniDataStream(DataStreamFactory dataStreamFactory, AbstractUniDataStream parent, + UniDataMapper mappingFunction) { + super(dataStreamFactory, 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/maybeapi/BiDataMapper.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/BiDataMapper.java new file mode 100644 index 0000000000..7d8ddc1346 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/BiDataMapper.java @@ -0,0 +1,30 @@ +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; + +/** + * 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..81358cad1c --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/UniDataMapper.java @@ -0,0 +1,29 @@ +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; + +/** + * 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/ListAssignMove.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ListAssignMove.java index 599583df70..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 @@ -10,8 +10,9 @@ 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; +@NullMarked public final class ListAssignMove extends AbstractMove { private final PlanningListVariableMetaModel variableMetaModel; @@ -31,23 +32,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 +57,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 faad305891..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 @@ -12,7 +12,8 @@ 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; /** * Moves an element of a {@link PlanningListVariable list variable}. The moved element is identified @@ -23,6 +24,7 @@ * * @param the solution type, the class with the {@link PlanningSolution} annotation */ +@NullMarked public final class ListChangeMove extends AbstractMove { private final PlanningListVariableMetaModel variableMetaModel; @@ -31,7 +33,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]} @@ -119,7 +121,7 @@ private Value_ getMovedValue() { // ************************************************************************ @Override - public void execute(@NonNull MutableSolutionView solutionView) { + public void execute(MutableSolutionView solutionView) { if (sourceEntity == destinationEntity) { planningValue = solutionView.moveValueInList(variableMetaModel, sourceEntity, sourceIndex, destinationIndex); } else { @@ -129,14 +131,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 { @@ -145,8 +147,8 @@ public void execute(@NonNull MutableSolutionView solutionView) { } @Override - public @NonNull Collection extractPlanningValues() { - return Collections.singleton(planningValue); + public Collection extractPlanningValues() { + return Collections.singleton(getMovedValue()); } @Override @@ -154,15 +156,30 @@ 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) - 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 @@ -171,7 +188,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 de52cc3194..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 @@ -11,8 +11,9 @@ 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; +@NullMarked public final class ListUnassignMove extends AbstractMove { private final PlanningListVariableMetaModel variableMetaModel; @@ -32,23 +33,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); } @@ -57,6 +58,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) @@ -73,7 +82,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/ChangeMoveProvider.java b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ChangeMoveProvider.java index fab7691ba4..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 @@ -9,7 +9,6 @@ import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningVariableMetaModel; import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; @NullMarked public class ChangeMoveProvider @@ -25,12 +24,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..ba3103c5ce --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/provider/ListChangeMoveProvider.java @@ -0,0 +1,145 @@ +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; +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.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; + + public ListChangeMoveProvider(PlanningListVariableMetaModel variableMetaModel) { + this.variableMetaModel = Objects.requireNonNull(variableMetaModel); + this.isValueInListFilter = (solution, entity, value) -> { + if (entity == null || value == null) { + // Necessary for the null to survive until the later stage, + // 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); + }; + } + + @Override + public MoveProducer apply(MoveStreamFactory moveStreamFactory) { + // 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. + 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); + } else { // This will trigger assignment of the value immediately before this value. + return solutionView.getPositionOf(variableMetaModel, value); + } + }) + .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(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)); + 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()); + }); + } + + 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/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..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 @@ -1,11 +1,16 @@ 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/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)); } 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..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 @@ -9,6 +9,7 @@ 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; @@ -123,4 +124,76 @@ 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 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 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]}. + * + *

+ * 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 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. + */ + UniDataStream distinct(); + } 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..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 @@ -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); } @@ -100,7 +98,6 @@ public boolean requiresFlushing() { public void close() { super.close(); if (session != null) { - session.close(); session = null; } } 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 5d1f25449d..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 @@ -131,6 +131,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 +149,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/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..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 @@ -43,6 +43,18 @@ 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. * 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..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 @@ -365,7 +365,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 +382,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 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..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 @@ -11,6 +11,7 @@ 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; @@ -92,10 +93,9 @@ 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); + 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 a5d60aaf87..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 @@ -5,6 +5,8 @@ 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; import ai.timefold.solver.core.testdomain.TestdataEntity; @@ -21,130 +23,130 @@ 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(); 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 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(); 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 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(); 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 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(); 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, @@ -165,7 +167,8 @@ 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)) @@ -184,31 +187,31 @@ 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 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)) @@ -227,31 +230,31 @@ 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 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)) @@ -271,31 +274,31 @@ 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 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)) @@ -315,31 +318,31 @@ 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 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)) @@ -368,19 +371,19 @@ 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 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)) @@ -409,20 +412,19 @@ 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 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)) @@ -454,20 +456,19 @@ 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 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)) @@ -499,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 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..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 @@ -6,6 +6,9 @@ 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; 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; @@ -16,34 +19,30 @@ 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; +@NullMarked class ChangeMoveProviderTest { @Test - void fromSolutionBasicVariable() { + 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); @@ -52,9 +51,9 @@ void fromSolutionBasicVariable() { 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); + var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); var moveProvider = new ChangeMoveProvider<>(variableMetaModel); var moveProducer = moveProvider.apply(moveStreamFactory); var moveStreamSession = createSession(moveStreamFactory, scoreDirector); @@ -101,12 +100,11 @@ void fromSolutionBasicVariable() { } @Test - void fromSolutionBasicVariableIncompleteValueRange() { + 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); @@ -120,9 +118,9 @@ void fromSolutionBasicVariableIncompleteValueRange() { 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); + var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); var moveProvider = new ChangeMoveProvider<>(variableMetaModel); var moveProducer = moveProvider.apply(moveStreamFactory); var moveStreamSession = createSession(moveStreamFactory, scoreDirector); @@ -169,20 +167,19 @@ void fromSolutionBasicVariableIncompleteValueRange() { } @Test - void fromEntityBasicVariable() { + 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); + var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); var moveProvider = new ChangeMoveProvider<>(variableMetaModel); var moveProducer = moveProvider.apply(moveStreamFactory); var moveStreamSession = createSession(moveStreamFactory, scoreDirector); @@ -210,21 +207,20 @@ void fromEntityBasicVariable() { } @Test - void fromEntityBasicVariableAllowsUnassigned() { + 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); + var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); var moveProvider = new ChangeMoveProvider<>(variableMetaModel); var moveProducer = moveProvider.apply(moveStreamFactory); var moveStreamSession = createSession(moveStreamFactory, scoreDirector); @@ -272,20 +268,20 @@ void fromEntityBasicVariableAllowsUnassigned() { } @Test - void fromSolutionBasicVariableAllowsUnassigned() { + 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); + var moveStreamFactory = new DefaultMoveStreamFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT); var moveProvider = new ChangeMoveProvider<>(variableMetaModel); var moveProducer = moveProvider.apply(moveStreamFactory); var moveStreamSession = createSession(moveStreamFactory, scoreDirector); @@ -361,7 +357,9 @@ void fromSolutionBasicVariableAllowsUnassigned() { } 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); @@ -379,4 +377,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 new file mode 100644 index 0000000000..f4006cbe7d --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/impl/move/streams/maybeapi/provider/ListChangeMoveProviderTest.java @@ -0,0 +1,425 @@ +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; +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.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.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.unassignedvar.TestdataListUnassignedEntityProvidingEntity; +import ai.timefold.solver.core.testdomain.list.valuerange.unassignedvar.TestdataListUnassignedEntityProvidingSolution; + +import org.jspecify.annotations.NullMarked; +import org.junit.jupiter.api.Test; + +@NullMarked +class ListChangeMoveProviderTest { + + @Test + void fromSolution() { + var solutionDescriptor = TestdataListSolution.buildSolutionDescriptor(); + var variableMetaModel = solutionDescriptor.getMetaModel() + .entity(TestdataListEntity.class) + .planningListVariable(); + + var solution = TestdataListSolution.generateUninitializedSolution(2, 2); + var e1 = solution.getEntityList().get(0); + var e2 = solution.getEntityList().get(1); + var unassignedValue = solution.getValueList().get(0); + var initiallyAssignedValue = solution.getValueList().get(1); + e2.getValueList().add(initiallyAssignedValue); + solution.getEntityList().forEach(TestdataListEntity::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(4); + + var moveList = StreamSupport.stream(moveIterable.spliterator(), false) + .toList(); + assertThat(moveList).hasSize(4); + + // 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 move1 = getListAssignMove(moveList, 0); + assertSoftly(softly -> { + softly.assertThat(move1.getDestinationEntity()).isEqualTo(e1); + softly.assertThat(move1.getDestinationIndex()).isEqualTo(0); + softly.assertThat(move1.extractPlanningEntities()) + .containsExactly(e1); + softly.assertThat(move1.extractPlanningValues()) + .containsExactly(unassignedValue); + }); + + var move2 = getListAssignMove(moveList, 1); + assertSoftly(softly -> { + softly.assertThat(move2.getDestinationEntity()).isEqualTo(e2); + softly.assertThat(move2.getDestinationIndex()).isEqualTo(1); + softly.assertThat(move2.extractPlanningEntities()) + .containsExactly(e2); + softly.assertThat(move2.extractPlanningValues()) + .containsExactly(unassignedValue); + }); + + var move3 = getListAssignMove(moveList, 2); + assertSoftly(softly -> { + softly.assertThat(move3.getDestinationEntity()).isEqualTo(e2); + softly.assertThat(move3.getDestinationIndex()).isEqualTo(0); + softly.assertThat(move3.extractPlanningEntities()) + .containsExactly(e2); + softly.assertThat(move3.extractPlanningValues()) + .containsExactly(unassignedValue); + }); + + var move4 = getListChangeMove(moveList, 3); + assertSoftly(softly -> { + 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(move4.extractPlanningValues()) + .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, 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 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(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(v3); + }); + + 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(v3); + }); + } + + @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, + Solution_ solution) { + var constraintProvider = new TestingConstraintProvider( + solutionDescriptor.getListVariableDescriptor().getEntityDescriptor().getEntityClass()); + var scoreDirectorFactory = + new BavetConstraintStreamScoreDirectorFactory<>(solutionDescriptor, constraintProvider, + EnvironmentMode.TRACKED_FULL_ASSERT); + var scoreDirector = scoreDirectorFactory.buildScoreDirector(); + scoreDirector.setWorkingSolution(solution); + return scoreDirector; + } + + // 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"); + } + + } + + 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 dba6a02ccf..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 @@ -24,6 +24,7 @@ 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; @@ -81,6 +82,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 +92,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; @@ -151,6 +152,7 @@ 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; @@ -193,12 +195,37 @@ 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 solveWithMoveStreamsListVar() { + 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)); + + // 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, true); + assertThat(solution).isNotNull(); + } + @Test void solveWithMoveStreamsNotEnabled() { var solverConfig = new SolverConfig() // Preview feature not enabled. @@ -2367,14 +2394,16 @@ 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(); + return List.of(new ChangeMoveProvider<>(variableMetamodel)); } + } /** @@ -2397,4 +2426,38 @@ 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(); + 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()); + } + + } + } 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..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 @@ -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)); @@ -73,4 +73,5 @@ public void removeValue(TestdataListValue value) { .filter(v -> !Objects.equals(v, value)) .toList(); } + } 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 7f177e1984..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 @@ -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..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 @@ -13,7 +13,7 @@ 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; 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;