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 comparatorClass, String comparatorFactoryPropertyName, Class 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