diff --git a/benchmark/src/main/resources/benchmark.xsd b/benchmark/src/main/resources/benchmark.xsd
index 86ede21c93..2a608ffee8 100644
--- a/benchmark/src/main/resources/benchmark.xsd
+++ b/benchmark/src/main/resources/benchmark.xsd
@@ -1178,6 +1178,12 @@
+
+
+
+
+
+
@@ -2027,6 +2033,12 @@
+
+
+
+
+
+
@@ -2066,6 +2078,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -2381,6 +2453,12 @@
+
+
+
+
+
+
diff --git a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/composite/CartesianProductMoveSelectorConfig.java b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/composite/CartesianProductMoveSelectorConfig.java
index a7b4b978c0..c0e3ced88d 100644
--- a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/composite/CartesianProductMoveSelectorConfig.java
+++ b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/composite/CartesianProductMoveSelectorConfig.java
@@ -12,6 +12,7 @@
import ai.timefold.solver.core.config.heuristic.selector.move.factory.MoveIteratorFactoryConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.factory.MoveListFactoryConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.generic.ChangeMoveSelectorConfig;
+import ai.timefold.solver.core.config.heuristic.selector.move.generic.MultistageMoveSelectorConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.generic.PillarChangeMoveSelectorConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.generic.PillarSwapMoveSelectorConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.generic.RuinRecreateMoveSelectorConfig;
@@ -20,6 +21,7 @@
import ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.SubChainSwapMoveSelectorConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.TailChainSwapMoveSelectorConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListChangeMoveSelectorConfig;
+import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListMultistageMoveSelectorConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListRuinRecreateMoveSelectorConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListSwapMoveSelectorConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.SubListChangeMoveSelectorConfig;
@@ -54,6 +56,10 @@ public class CartesianProductMoveSelectorConfig extends MoveSelectorConfig {
+ public static final String XML_ELEMENT_NAME = "multistageMoveSelector";
+
+ protected Class> stageProviderClass;
+
+ protected Class> entityClass = null;
+ protected String variableName = null;
+
+ // **************************
+ // Getters/Setters
+ // **************************
+
+ public Class> getStageProviderClass() {
+ return stageProviderClass;
+ }
+
+ public void setStageProviderClass(
+ Class> stageProviderClass) {
+ this.stageProviderClass = stageProviderClass;
+ }
+
+ public Class> getEntityClass() {
+ return entityClass;
+ }
+
+ public void setEntityClass(Class> entityClass) {
+ this.entityClass = entityClass;
+ }
+
+ public String getVariableName() {
+ return variableName;
+ }
+
+ public void setVariableName(String variableName) {
+ this.variableName = variableName;
+ }
+
+ // **************************
+ // With methods
+ // **************************
+
+ public @NonNull MultistageMoveSelectorConfig withStageProviderClass(
+ @NonNull Class> stageProviderClass) {
+ this.setStageProviderClass(stageProviderClass);
+ return this;
+ }
+
+ public @NonNull MultistageMoveSelectorConfig withEntityClass(@NonNull Class> entityClass) {
+ this.setEntityClass(entityClass);
+ return this;
+ }
+
+ public @NonNull MultistageMoveSelectorConfig withVariableName(@NonNull String variableName) {
+ this.setVariableName(variableName);
+ return this;
+ }
+
+ // **************************
+ // Interface methods
+ // **************************
+
+ @Override
+ public boolean hasNearbySelectionConfig() {
+ return false;
+ }
+
+ @Override
+ public @NonNull MultistageMoveSelectorConfig copyConfig() {
+ return new MultistageMoveSelectorConfig().inherit(this);
+ }
+
+ @Override
+ public void visitReferencedClasses(@NonNull Consumer> classVisitor) {
+ classVisitor.accept(stageProviderClass);
+ }
+
+ @Override
+ public @NonNull MultistageMoveSelectorConfig
+ inherit(@NonNull MultistageMoveSelectorConfig inheritedConfig) {
+ super.inherit(inheritedConfig);
+ stageProviderClass =
+ ConfigUtils.inheritOverwritableProperty(stageProviderClass,
+ inheritedConfig.getStageProviderClass());
+ entityClass =
+ ConfigUtils.inheritOverwritableProperty(entityClass, inheritedConfig.getEntityClass());
+ variableName =
+ ConfigUtils.inheritOverwritableProperty(variableName, inheritedConfig.getVariableName());
+ return this;
+ }
+}
diff --git a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/ListMultistageMoveSelectorConfig.java b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/ListMultistageMoveSelectorConfig.java
new file mode 100644
index 0000000000..9f641ea0b2
--- /dev/null
+++ b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/ListMultistageMoveSelectorConfig.java
@@ -0,0 +1,71 @@
+package ai.timefold.solver.core.config.heuristic.selector.move.generic.list;
+
+import java.util.function.Consumer;
+
+import jakarta.xml.bind.annotation.XmlType;
+
+import ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig;
+import ai.timefold.solver.core.config.util.ConfigUtils;
+
+import org.jspecify.annotations.NonNull;
+
+@XmlType(propOrder = {
+ "stageProviderClass"
+})
+public class ListMultistageMoveSelectorConfig extends MoveSelectorConfig {
+ public static final String XML_ELEMENT_NAME = "listMultistageMoveSelector";
+
+ protected Class> stageProviderClass;
+
+ // **************************
+ // Getters/Setters
+ // **************************
+
+ public Class> getStageProviderClass() {
+ return stageProviderClass;
+ }
+
+ public void setStageProviderClass(
+ Class> stageProviderClass) {
+ this.stageProviderClass = stageProviderClass;
+ }
+
+ // **************************
+ // With methods
+ // **************************
+
+ public @NonNull ListMultistageMoveSelectorConfig withStageProviderClass(
+ @NonNull Class> stageProviderClass) {
+ this.setStageProviderClass(stageProviderClass);
+ return this;
+ }
+
+ // **************************
+ // Interface methods
+ // **************************
+
+ @Override
+ public boolean hasNearbySelectionConfig() {
+ return false;
+ }
+
+ @Override
+ public @NonNull ListMultistageMoveSelectorConfig copyConfig() {
+ return new ListMultistageMoveSelectorConfig().inherit(this);
+ }
+
+ @Override
+ public void visitReferencedClasses(@NonNull Consumer> classVisitor) {
+ classVisitor.accept(stageProviderClass);
+ }
+
+ @Override
+ public @NonNull ListMultistageMoveSelectorConfig
+ inherit(@NonNull ListMultistageMoveSelectorConfig inheritedConfig) {
+ super.inherit(inheritedConfig);
+ stageProviderClass =
+ ConfigUtils.inheritOverwritableProperty(stageProviderClass,
+ inheritedConfig.getStageProviderClass());
+ return this;
+ }
+}
diff --git a/core/src/main/java/ai/timefold/solver/core/config/localsearch/LocalSearchPhaseConfig.java b/core/src/main/java/ai/timefold/solver/core/config/localsearch/LocalSearchPhaseConfig.java
index 959cad57a9..70855a3f21 100644
--- a/core/src/main/java/ai/timefold/solver/core/config/localsearch/LocalSearchPhaseConfig.java
+++ b/core/src/main/java/ai/timefold/solver/core/config/localsearch/LocalSearchPhaseConfig.java
@@ -12,6 +12,7 @@
import ai.timefold.solver.core.config.heuristic.selector.move.factory.MoveIteratorFactoryConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.factory.MoveListFactoryConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.generic.ChangeMoveSelectorConfig;
+import ai.timefold.solver.core.config.heuristic.selector.move.generic.MultistageMoveSelectorConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.generic.PillarChangeMoveSelectorConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.generic.PillarSwapMoveSelectorConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.generic.RuinRecreateMoveSelectorConfig;
@@ -20,6 +21,7 @@
import ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.SubChainSwapMoveSelectorConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.TailChainSwapMoveSelectorConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListChangeMoveSelectorConfig;
+import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListMultistageMoveSelectorConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListRuinRecreateMoveSelectorConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListSwapMoveSelectorConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.SubListChangeMoveSelectorConfig;
@@ -65,6 +67,10 @@ public class LocalSearchPhaseConfig extends PhaseConfig
type = RuinRecreateMoveSelectorConfig.class),
@XmlElement(name = ListRuinRecreateMoveSelectorConfig.XML_ELEMENT_NAME,
type = ListRuinRecreateMoveSelectorConfig.class),
+ @XmlElement(name = MultistageMoveSelectorConfig.XML_ELEMENT_NAME,
+ type = MultistageMoveSelectorConfig.class),
+ @XmlElement(name = ListMultistageMoveSelectorConfig.XML_ELEMENT_NAME,
+ type = ListMultistageMoveSelectorConfig.class),
@XmlElement(name = SubChainChangeMoveSelectorConfig.XML_ELEMENT_NAME,
type = SubChainChangeMoveSelectorConfig.class),
@XmlElement(name = SubChainSwapMoveSelectorConfig.XML_ELEMENT_NAME,
diff --git a/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java b/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java
index 13489dc0c5..927f24e01c 100644
--- a/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java
+++ b/core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java
@@ -14,6 +14,8 @@
import ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig;
import ai.timefold.solver.core.config.heuristic.selector.list.DestinationSelectorConfig;
import ai.timefold.solver.core.config.heuristic.selector.list.SubListSelectorConfig;
+import ai.timefold.solver.core.config.heuristic.selector.move.generic.MultistageMoveSelectorConfig;
+import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListMultistageMoveSelectorConfig;
import ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig;
import ai.timefold.solver.core.config.partitionedsearch.PartitionedSearchPhaseConfig;
import ai.timefold.solver.core.config.solver.EnvironmentMode;
@@ -27,6 +29,7 @@
import ai.timefold.solver.core.impl.heuristic.selector.list.ElementDestinationSelector;
import ai.timefold.solver.core.impl.heuristic.selector.list.RandomSubListSelector;
import ai.timefold.solver.core.impl.heuristic.selector.list.SubListSelector;
+import ai.timefold.solver.core.impl.heuristic.selector.move.AbstractMoveSelectorFactory;
import ai.timefold.solver.core.impl.heuristic.selector.value.ValueSelector;
import ai.timefold.solver.core.impl.localsearch.decider.LocalSearchDecider;
import ai.timefold.solver.core.impl.localsearch.decider.acceptor.Acceptor;
@@ -141,12 +144,22 @@ DestinationSelector applyNearbySelection(DestinationSelec
HeuristicConfigPolicy configPolicy, SelectionCacheType minimumCacheType,
SelectionOrder resolvedSelectionOrder, ElementDestinationSelector destinationSelector);
+ AbstractMoveSelectorFactory
+ buildBasicMultistageMoveSelectorFactory(
+ MultistageMoveSelectorConfig moveSelectorConfig);
+
+ AbstractMoveSelectorFactory
+ buildListMultistageMoveSelectorFactory(
+ ListMultistageMoveSelectorConfig moveSelectorConfig);
+
enum Feature {
MULTITHREADED_SOLVING("Multi-threaded solving", "remove moveThreadCount from solver configuration"),
PARTITIONED_SEARCH("Partitioned search", "remove partitioned search phase from solver configuration"),
NEARBY_SELECTION("Nearby selection", "remove nearby selection from solver configuration"),
AUTOMATIC_NODE_SHARING("Automatic node sharing",
- "remove automatic node sharing from solver configuration");
+ "remove automatic node sharing from solver configuration"),
+ MULTISTAGE_MOVE("Multistage move selector",
+ "remove multistageMoveSelector and/or listMultistageMoveSelector from the solver configuration");
private final String name;
private final String workaround;
diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/BasicVariableDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/BasicVariableDescriptor.java
index 920cbdc56a..a1cfb4f0aa 100644
--- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/BasicVariableDescriptor.java
+++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/BasicVariableDescriptor.java
@@ -10,6 +10,7 @@
import ai.timefold.solver.core.impl.domain.policy.DescriptorPolicy;
import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionFilter;
import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.MovableChainedTrailingValueFilter;
+import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningVariableMetaModel;
public final class BasicVariableDescriptor extends GenuineVariableDescriptor {
@@ -185,6 +186,12 @@ public SelectionFilter getMovableChainedTrailingValueFilter()
return movableChainedTrailingValueFilter;
}
+ @Override
+ @SuppressWarnings("unchecked")
+ public PlanningVariableMetaModel getVariableMetaModel() {
+ return (PlanningVariableMetaModel) super.getVariableMetaModel();
+ }
+
private record SortingProperties(String comparatorPropertyName, Class extends Comparator> comparatorClass,
String comparatorFactoryPropertyName, Class extends ComparatorFactory> comparatorFactoryClass) {
diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/ListVariableDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/ListVariableDescriptor.java
index 4d0e7f5f49..753c717352 100644
--- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/ListVariableDescriptor.java
+++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/ListVariableDescriptor.java
@@ -16,6 +16,7 @@
import ai.timefold.solver.core.impl.domain.variable.inverserelation.InverseRelationShadowVariableDescriptor;
import ai.timefold.solver.core.impl.move.director.MoveDirector;
import ai.timefold.solver.core.impl.neighborhood.maybeapi.stream.enumerating.function.BiEnumeratingPredicate;
+import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningListVariableMetaModel;
public final class ListVariableDescriptor extends GenuineVariableDescriptor {
@@ -194,4 +195,10 @@ public int getFirstUnpinnedIndex(Object entity) {
}
}
+ @Override
+ @SuppressWarnings("unchecked")
+ public PlanningListVariableMetaModel getVariableMetaModel() {
+ return (PlanningListVariableMetaModel) super.getVariableMetaModel();
+ }
+
}
diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/VariableDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/VariableDescriptor.java
index b60f60b098..fe2d2b0d98 100644
--- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/VariableDescriptor.java
+++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/VariableDescriptor.java
@@ -131,12 +131,13 @@ public final boolean isGenuineAndUninitialized(Object entity) {
&& !genuineVariableDescriptor.isInitialized(entity);
}
- public VariableMetaModel getVariableMetaModel() {
+ @SuppressWarnings("unchecked")
+ public VariableMetaModel getVariableMetaModel() {
if (cachedMetamodel != null) {
- return cachedMetamodel;
+ return (VariableMetaModel) cachedMetamodel;
}
cachedMetamodel = entityDescriptor.getEntityMetaModel()
.variable(variableName);
- return cachedMetamodel;
+ return (VariableMetaModel) cachedMetamodel;
}
}
diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/MoveSelectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/MoveSelectorFactory.java
index a9e53722fc..4800ed2290 100644
--- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/MoveSelectorFactory.java
+++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/MoveSelectorFactory.java
@@ -8,6 +8,7 @@
import ai.timefold.solver.core.config.heuristic.selector.move.factory.MoveIteratorFactoryConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.factory.MoveListFactoryConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.generic.ChangeMoveSelectorConfig;
+import ai.timefold.solver.core.config.heuristic.selector.move.generic.MultistageMoveSelectorConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.generic.PillarChangeMoveSelectorConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.generic.PillarSwapMoveSelectorConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.generic.RuinRecreateMoveSelectorConfig;
@@ -17,11 +18,13 @@
import ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.SubChainSwapMoveSelectorConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.TailChainSwapMoveSelectorConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListChangeMoveSelectorConfig;
+import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListMultistageMoveSelectorConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListRuinRecreateMoveSelectorConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListSwapMoveSelectorConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.SubListChangeMoveSelectorConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.SubListSwapMoveSelectorConfig;
import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.kopt.KOptListMoveSelectorConfig;
+import ai.timefold.solver.core.enterprise.TimefoldSolverEnterpriseService;
import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy;
import ai.timefold.solver.core.impl.heuristic.selector.move.composite.CartesianProductMoveSelectorFactory;
import ai.timefold.solver.core.impl.heuristic.selector.move.composite.UnionMoveSelectorFactory;
@@ -84,6 +87,15 @@ public interface MoveSelectorFactory {
return new UnionMoveSelectorFactory<>(unionMoveSelectorConfig);
} else if (moveSelectorConfig instanceof CartesianProductMoveSelectorConfig cartesianProductMoveSelectorConfig) {
return new CartesianProductMoveSelectorFactory<>(cartesianProductMoveSelectorConfig);
+ } else if (moveSelectorConfig instanceof MultistageMoveSelectorConfig advancedRuinRecreateMoveSelectorConfig) {
+ var enterpriseService = TimefoldSolverEnterpriseService
+ .loadOrFail(TimefoldSolverEnterpriseService.Feature.MULTISTAGE_MOVE);
+ return enterpriseService.buildBasicMultistageMoveSelectorFactory(advancedRuinRecreateMoveSelectorConfig);
+ } else if (moveSelectorConfig instanceof ListMultistageMoveSelectorConfig advancedListRuinRecreateMoveSelectorConfig) {
+ var enterpriseService = TimefoldSolverEnterpriseService
+ .loadOrFail(TimefoldSolverEnterpriseService.Feature.MULTISTAGE_MOVE);
+ return enterpriseService
+ .buildListMultistageMoveSelectorFactory(advancedListRuinRecreateMoveSelectorConfig);
} else {
throw new IllegalArgumentException(String.format("Unknown %s type: (%s).",
MoveSelectorConfig.class.getSimpleName(), moveSelectorConfig.getClass().getName()));
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 a5b0aee668..beb959c42f 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
@@ -271,7 +271,8 @@ public boolean isPinned(EntityDescriptor entityDescriptor, @
return !entityDescriptor.isMovable(backingScoreDirector.getWorkingSolution(), entity);
}
- protected static ElementPosition getPositionOf(InnerScoreDirector scoreDirector,
+ protected static ElementPosition getPositionOf(
+ InnerScoreDirector scoreDirector,
PlanningListVariableMetaModel listVariableDescriptor, Value_ value) {
return scoreDirector.getListVariableStateSupply(extractVariableDescriptor(listVariableDescriptor))
.getElementPosition(value);
diff --git a/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/Moves.java b/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/Moves.java
index 243e8f01c9..d1887e71de 100644
--- a/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/Moves.java
+++ b/core/src/main/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/Moves.java
@@ -61,12 +61,14 @@ public static Move assign(
}
public static Move unassign(
- PlanningListVariableMetaModel variableMetaModel, PositionInList targetPosition) {
- return unassign(targetPosition.entity(), variableMetaModel, targetPosition.index());
+ PlanningListVariableMetaModel variableMetaModel,
+ PositionInList targetPosition) {
+ return unassign(variableMetaModel, targetPosition.entity(), targetPosition.index());
}
- public static Move unassign(Entity_ entity,
- PlanningListVariableMetaModel variableMetaModel, int index) {
+ public static Move unassign(
+ PlanningListVariableMetaModel variableMetaModel, Entity_ entity,
+ int index) {
return new ListUnassignMove<>(variableMetaModel, entity, index);
}
diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java
index 992053f57d..ed8e297ff2 100644
--- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java
+++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java
@@ -147,7 +147,8 @@ public VariableDescriptorCache getVariableDescriptorCache() {
}
@Override
- public ListVariableStateSupply
+ @SuppressWarnings("unchecked")
+ public ListVariableStateSupply
getListVariableStateSupply(ListVariableDescriptor variableDescriptor) {
var originalListVariableDescriptor = getSolutionDescriptor().getListVariableDescriptor();
if (variableDescriptor != originalListVariableDescriptor) {
@@ -155,7 +156,7 @@ public VariableDescriptorCache getVariableDescriptorCache() {
"The variableDescriptor (%s) is not the same as the solution's variableDescriptor (%s)."
.formatted(variableDescriptor, originalListVariableDescriptor));
}
- return Objects.requireNonNull(listVariableStateSupply);
+ return Objects.requireNonNull((ListVariableStateSupply) listVariableStateSupply);
}
@Override
diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java
index 738f4f1a2c..163b91d9ae 100644
--- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java
+++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java
@@ -269,7 +269,7 @@ default Solution_ cloneWorkingSolution() {
ValueRangeManager getValueRangeManager();
- ListVariableStateSupply
+ ListVariableStateSupply
getListVariableStateSupply(ListVariableDescriptor variableDescriptor);
InnerScoreDirector createChildThreadScoreDirector(ChildThreadType childThreadType);
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 07385d5e14..b9d6934695 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
@@ -76,7 +76,8 @@ Value_ getValueAtIndex(PlanningListVariableMetaModel ElementPosition getPositionOf(PlanningListVariableMetaModel variableMetaModel,
+ ElementPosition getPositionOf(
+ PlanningListVariableMetaModel variableMetaModel,
Value_ value);
/**
diff --git a/core/src/main/resources/solver.xsd b/core/src/main/resources/solver.xsd
index 8c6e02fc1c..5614d5884f 100644
--- a/core/src/main/resources/solver.xsd
+++ b/core/src/main/resources/solver.xsd
@@ -591,6 +591,10 @@
+
+
+
+
@@ -1157,6 +1161,10 @@
+
+
+
+
@@ -1183,6 +1191,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1393,6 +1447,10 @@
+
+
+
+
diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListUtils.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListUtils.java
index 99df7e80ef..0b519d418e 100644
--- a/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListUtils.java
+++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListUtils.java
@@ -232,7 +232,8 @@ public static DestinationSelector mockNeverEndingDestinat
return destinationSelector;
}
- public static DestinationSelector mockDestinationSelector(ElementPosition... locationsInList) {
+ public static DestinationSelector
+ mockDestinationSelector(ElementPosition... locationsInList) {
DestinationSelector destinationSelector = mock(DestinationSelector.class);
var refList = Arrays.asList(locationsInList);
when(destinationSelector.isCountable()).thenReturn(true);
diff --git a/docs/src/modules/ROOT/pages/enterprise-edition/enterprise-edition.adoc b/docs/src/modules/ROOT/pages/enterprise-edition/enterprise-edition.adoc
index 90c0904b69..dbe7398824 100644
--- a/docs/src/modules/ROOT/pages/enterprise-edition/enterprise-edition.adoc
+++ b/docs/src/modules/ROOT/pages/enterprise-edition/enterprise-edition.adoc
@@ -1030,3 +1030,23 @@ If you are using the `ThrottlingBestSolutionEventConsumer` for intermediate best
together with a final best solution consumer,
both these consumers will receive the final best solution.
====
+
+[#multistageMoves]
+=== Multistage Moves
+
+[NOTE]
+====
+This feature is a commercial feature of Timefold Solver Enterprise Edition.
+It is not available in the Community Edition.
+====
+
+Multistage moves are moves composed of one or more stages, where each stage selects a single `Move` to execute.
+
+Each stage has access to either a `BasicVariableMoveEvaluator` or a `ListVariableMoveEvaluator`, which allows the stage to evaluate moves without executing them.
+
+Stages are selected from either a `BasicVariableStageProvider` or a `ListVariableStageProvider`, which is initialized from the working solution at phase start.
+
+Multistage moves are configured from either a xref:optimization-algorithms/move-selector-reference.adoc#multistageMoveSelector[MultistageMoveSelectorConfig]
+or a xref:optimization-algorithms/move-selector-reference.adoc#listMultistageMoveSelector[ListMultistageMoveSelectorConfig].
+
+Multistage moves are useful for creating specialized ruin-and-recreate moves where the valid values that won't violate hard constraints can be determined in advance.
diff --git a/docs/src/modules/ROOT/pages/optimization-algorithms/move-selector-reference.adoc b/docs/src/modules/ROOT/pages/optimization-algorithms/move-selector-reference.adoc
index 8ce7191e46..0a41a7228e 100644
--- a/docs/src/modules/ROOT/pages/optimization-algorithms/move-selector-reference.adoc
+++ b/docs/src/modules/ROOT/pages/optimization-algorithms/move-selector-reference.adoc
@@ -425,6 +425,58 @@ to control the frequency of this move:
The above configuration will run the `RuinRecreateMove` once for every 100 fine-grained moves.
As always, benchmarking is recommended to find the optimal value for your use case.
+[#multistageMoveSelector]
+=== `MultistageMoveSelector`
+
+[NOTE]
+====
+This feature is a commercial feature of Timefold Solver Enterprise Edition.
+It is not available in the Community Edition.
+====
+
+The `multistageMoveSelector` selects a multistage move to execute from a `BasicVariableStageProvider`.
+
+The `BasicVariableStageProvider` is initialized from the working solution at phase start and when selected supplies a list of `BasicVariableCustomStage`, which each select a move and are executed in order.
+
+Each `BasicVariableCustomStage` has access to a `BasicVariableMoveEvaluator` which allows the stage to evaluate moves without executing them.
+
+Configuration:
+
+[source,xml,options="nowrap"]
+----
+
+ ...MyStageProvider
+ ...Shift
+ employee
+
+----
+
+[source,java,options="nowrap"]
+----
+public class MyStageProvider implements BasicVariableStageProvider {
+ List shiftList;
+ Map> employeesBySkillMap;
+
+ @Override
+ public void initialize(Schedule schedule) {
+ this.shiftList = schedule.getShifts();
+ this.employeesBySkillMap = new HashMap<>();
+
+ for (var employee : schedule.getEmployees()) {
+ this.employeesBySkillMap.computeIfAbsent(employee.getSkill(), ignored -> new ArrayList<>()).add(employee);
+ }
+ }
+
+ @Override
+ public List>
+ createStages(Random random) {
+ var shift = shiftList.get(random.nextInt(shiftList.size()));
+ var employeesWithSkill = employeesBySkillMap.get(shift.getRequiredSkill());
+ return List.of(
+ BasicVariableCustomStage.bestFit(shift, employeesWithSkill));
+ }
+}
+----
[#listMoveSelectors]
== Move selectors for list variables
@@ -652,6 +704,56 @@ to control the frequency of this move:
The above configuration will run the `ListRuinRecreateMove` once for every 100 fine-grained moves.
As always, benchmarking is recommended to find the optimal value for your use case.
+[#listMultistageMoveSelector]
+=== `ListMultistageMoveSelector`
+
+[NOTE]
+====
+This feature is a commercial feature of Timefold Solver Enterprise Edition.
+It is not available in the Community Edition.
+====
+
+The `listMultistageMoveSelector` selects a multistage move to execute from a `ListVariableStageProvider`.
+
+The `ListVariableStageProvider` is initialized from the working solution at phase start and when selected supplies a list of `ListVariableCustomStage`, which each select a move and are executed in order.
+
+Each `ListVariableCustomStage` has access to a `ListVariableMoveEvaluator` which allows the stage to evaluate moves without executing them.
+
+Configuration:
+
+[source,xml,options="nowrap"]
+----
+
+ ...MyStageProvider
+
+----
+
+[source,java,options="nowrap"]
+----
+public class MyStageProvider implements ListVariableStageProvider {
+ List visitList;
+ Map> vehiclesBySkillMap;
+
+ @Override
+ public void initialize(RoutePlan routePlan) {
+ this.visitList = routePlan.getVisits();
+ this.vehiclesBySkillMap = new HashMap<>();
+
+ for (var vehicle : routePlan.getVehicles()) {
+ this.vehiclesBySkillMap.computeIfAbsent(vehicle.getSkill(), ignored -> new ArrayList<>()).add(vehicle);
+ }
+ }
+
+ @Override
+ public List>
+ createStages(Random random) {
+ var visit = visitList.get(random.nextInt(visitList.size()));
+ var vehiclesWithSkill = vehiclesBySkillMap.get(visit.getRequiredSkill());
+ return List.of(
+ ListVariableCustomStage.bestFit(visit, vehiclesWithSkill));
+ }
+}
+----
[#chainMoveSelectors]
== Move selectors for chained variables