From e14f7b1229e14386568da843d13a087673560a65 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 4 Nov 2025 08:59:54 -0500 Subject: [PATCH 1/9] wip: R&R --- benchmark/src/main/resources/benchmark.xsd | 93 ++++++++++++ .../CartesianProductMoveSelectorConfig.java | 6 + .../composite/UnionMoveSelectorConfig.java | 6 + ...dvancedRuinRecreateMoveSelectorConfig.java | 124 +++++++++++++++ ...cedListRuinRecreateMoveSelectorConfig.java | 141 ++++++++++++++++++ .../localsearch/LocalSearchPhaseConfig.java | 6 + .../TimefoldSolverEnterpriseService.java | 15 +- .../selector/move/MoveSelectorFactory.java | 12 ++ .../api/move/ruin/BasicRecreatedElement.java | 6 + .../move/ruin/BasicRuinAndRecreatePicker.java | 10 ++ .../api/move/ruin/BasicRuinAndRecreator.java | 10 ++ .../api/move/ruin/BasicRuinedElement.java | 5 + .../ruin/BasicRuinedOrRecreatedElement.java | 5 + .../api/move/ruin/ListRecreatedElement.java | 6 + .../move/ruin/ListRuinAndRecreatePicker.java | 10 ++ .../api/move/ruin/ListRuinAndRecreator.java | 11 ++ .../api/move/ruin/ListRuinedElement.java | 5 + .../ruin/ListRuinedOrRecreatedElement.java | 5 + .../api/move/ruin/RecreateEvaluator.java | 9 ++ .../api/move/ruin/RecreateMetadata.java | 17 +++ core/src/main/resources/solver.xsd | 62 ++++++++ 21 files changed, 563 insertions(+), 1 deletion(-) create mode 100644 core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/AdvancedRuinRecreateMoveSelectorConfig.java create mode 100644 core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/AdvancedListRuinRecreateMoveSelectorConfig.java create mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRecreatedElement.java create mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinAndRecreatePicker.java create mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinAndRecreator.java create mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinedElement.java create mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinedOrRecreatedElement.java create mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRecreatedElement.java create mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinAndRecreatePicker.java create mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinAndRecreator.java create mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinedElement.java create mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinedOrRecreatedElement.java create mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/RecreateEvaluator.java create mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/RecreateMetadata.java diff --git a/benchmark/src/main/resources/benchmark.xsd b/benchmark/src/main/resources/benchmark.xsd index 86ede21c93..28c1f20e91 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,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2381,6 +2468,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..69a4482d57 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 @@ -11,6 +11,7 @@ import ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig; 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.AdvancedRuinRecreateMoveSelectorConfig; import ai.timefold.solver.core.config.heuristic.selector.move.generic.ChangeMoveSelectorConfig; import ai.timefold.solver.core.config.heuristic.selector.move.generic.PillarChangeMoveSelectorConfig; import ai.timefold.solver.core.config.heuristic.selector.move.generic.PillarSwapMoveSelectorConfig; @@ -19,6 +20,7 @@ import ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.SubChainChangeMoveSelectorConfig; 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.AdvancedListRuinRecreateMoveSelectorConfig; import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListChangeMoveSelectorConfig; import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListRuinRecreateMoveSelectorConfig; import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListSwapMoveSelectorConfig; @@ -54,6 +56,10 @@ public class CartesianProductMoveSelectorConfig extends MoveSelectorConfig { + public static final String XML_ELEMENT_NAME = "advancedRuinRecreateMoveSelector"; + + protected Class> ruinAndRecreatePickerClass; + protected Class> recreatorClass; + + protected Class entityClass = null; + protected String variableName = null; + + // ************************** + // Getters/Setters + // ************************** + + public Class> getRuinAndRecreatePickerClass() { + return ruinAndRecreatePickerClass; + } + + public void setRuinAndRecreatePickerClass( + Class> ruinAndRecreatePickerClass) { + this.ruinAndRecreatePickerClass = ruinAndRecreatePickerClass; + } + + public @NonNull AdvancedRuinRecreateMoveSelectorConfig withRuinAndRecreatePickerClass( + @NonNull Class> ruinAndRecreatePickerClass) { + this.setRuinAndRecreatePickerClass(ruinAndRecreatePickerClass); + return this; + } + + public Class> getRecreatorClass() { + return recreatorClass; + } + + public void setRecreatorClass( + Class> recreatorClass) { + this.recreatorClass = recreatorClass; + } + + public @NonNull AdvancedRuinRecreateMoveSelectorConfig + withRecreatorClass(@NonNull Class> recreatorClass) { + this.setRecreatorClass(recreatorClass); + return this; + } + + public Class getEntityClass() { + return entityClass; + } + + public void setEntityClass(Class entityClass) { + this.entityClass = entityClass; + } + + public @NonNull AdvancedRuinRecreateMoveSelectorConfig withEntityClass(@NonNull Class entityClass) { + this.setEntityClass(entityClass); + return this; + } + + public String getVariableName() { + return variableName; + } + + public void setVariableName(String variableName) { + this.variableName = variableName; + } + + public @NonNull AdvancedRuinRecreateMoveSelectorConfig withVariableName(@NonNull String variableName) { + this.setVariableName(variableName); + return this; + } + + // ************************** + // Interface methods + // ************************** + + @Override + public boolean hasNearbySelectionConfig() { + return false; + } + + @Override + public @NonNull AdvancedRuinRecreateMoveSelectorConfig copyConfig() { + return new AdvancedRuinRecreateMoveSelectorConfig().inherit(this); + } + + @Override + public void visitReferencedClasses(@NonNull Consumer> classVisitor) { + classVisitor.accept(ruinAndRecreatePickerClass); + classVisitor.accept(recreatorClass); + } + + @Override + public @NonNull AdvancedRuinRecreateMoveSelectorConfig + inherit(@NonNull AdvancedRuinRecreateMoveSelectorConfig inheritedConfig) { + super.inherit(inheritedConfig); + ruinAndRecreatePickerClass = + ConfigUtils.inheritOverwritableProperty(ruinAndRecreatePickerClass, + inheritedConfig.getRuinAndRecreatePickerClass()); + recreatorClass = + ConfigUtils.inheritOverwritableProperty(recreatorClass, inheritedConfig.getRecreatorClass()); + 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/AdvancedListRuinRecreateMoveSelectorConfig.java b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/AdvancedListRuinRecreateMoveSelectorConfig.java new file mode 100644 index 0000000000..354130c98c --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/AdvancedListRuinRecreateMoveSelectorConfig.java @@ -0,0 +1,141 @@ +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 ai.timefold.solver.core.preview.api.move.ruin.ListRuinAndRecreatePicker; +import ai.timefold.solver.core.preview.api.move.ruin.ListRuinAndRecreator; + +import org.jspecify.annotations.NonNull; + +@XmlType(propOrder = { + "ruinAndRecreatePickerClass", + "recreatorClass", + "entityClass", + "valueClass", + "variableName" +}) +public class AdvancedListRuinRecreateMoveSelectorConfig extends MoveSelectorConfig { + public static final String XML_ELEMENT_NAME = "advancedListRuinRecreateMoveSelector"; + + protected Class> ruinAndRecreatePickerClass; + protected Class> recreatorClass; + + protected Class entityClass = null; + protected Class valueClass = null; + protected String variableName = null; + + // ************************** + // Getters/Setters + // ************************** + + public Class> getRuinAndRecreatePickerClass() { + return ruinAndRecreatePickerClass; + } + + public void setRuinAndRecreatePickerClass( + Class> ruinAndRecreatePickerClass) { + this.ruinAndRecreatePickerClass = ruinAndRecreatePickerClass; + } + + public @NonNull AdvancedListRuinRecreateMoveSelectorConfig withRuinAndRecreatePickerClass( + @NonNull Class> ruinAndRecreatePickerClass) { + this.setRuinAndRecreatePickerClass(ruinAndRecreatePickerClass); + return this; + } + + public Class> getRecreatorClass() { + return recreatorClass; + } + + public void setRecreatorClass( + Class> recreatorClass) { + this.recreatorClass = recreatorClass; + } + + public @NonNull AdvancedListRuinRecreateMoveSelectorConfig + withRecreatorClass(@NonNull Class> recreatorClass) { + this.setRecreatorClass(recreatorClass); + return this; + } + + public Class getEntityClass() { + return entityClass; + } + + public void setEntityClass(Class entityClass) { + this.entityClass = entityClass; + } + + public @NonNull AdvancedListRuinRecreateMoveSelectorConfig withEntityClass(@NonNull Class entityClass) { + this.setEntityClass(entityClass); + return this; + } + + public Class getValueClass() { + return valueClass; + } + + public void setValueClass(Class valueClass) { + this.valueClass = valueClass; + } + + public @NonNull AdvancedListRuinRecreateMoveSelectorConfig withValueClass(@NonNull Class valueClass) { + this.setValueClass(valueClass); + return this; + } + + public String getVariableName() { + return variableName; + } + + public void setVariableName(String variableName) { + this.variableName = variableName; + } + + public @NonNull AdvancedListRuinRecreateMoveSelectorConfig withVariableName(@NonNull String variableName) { + this.setVariableName(variableName); + return this; + } + + // ************************** + // Interface methods + // ************************** + + @Override + public boolean hasNearbySelectionConfig() { + return false; + } + + @Override + public @NonNull AdvancedListRuinRecreateMoveSelectorConfig copyConfig() { + return new AdvancedListRuinRecreateMoveSelectorConfig().inherit(this); + } + + @Override + public void visitReferencedClasses(@NonNull Consumer> classVisitor) { + classVisitor.accept(ruinAndRecreatePickerClass); + classVisitor.accept(recreatorClass); + } + + @Override + public @NonNull AdvancedListRuinRecreateMoveSelectorConfig + inherit(@NonNull AdvancedListRuinRecreateMoveSelectorConfig inheritedConfig) { + super.inherit(inheritedConfig); + ruinAndRecreatePickerClass = + ConfigUtils.inheritOverwritableProperty(ruinAndRecreatePickerClass, + inheritedConfig.getRuinAndRecreatePickerClass()); + recreatorClass = + ConfigUtils.inheritOverwritableProperty(recreatorClass, inheritedConfig.getRecreatorClass()); + entityClass = + ConfigUtils.inheritOverwritableProperty(entityClass, inheritedConfig.getEntityClass()); + valueClass = + ConfigUtils.inheritOverwritableProperty(valueClass, inheritedConfig.getValueClass()); + variableName = + ConfigUtils.inheritOverwritableProperty(variableName, inheritedConfig.getVariableName()); + 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..ddc49ff1c0 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 @@ -11,6 +11,7 @@ import ai.timefold.solver.core.config.heuristic.selector.move.composite.UnionMoveSelectorConfig; 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.AdvancedRuinRecreateMoveSelectorConfig; import ai.timefold.solver.core.config.heuristic.selector.move.generic.ChangeMoveSelectorConfig; import ai.timefold.solver.core.config.heuristic.selector.move.generic.PillarChangeMoveSelectorConfig; import ai.timefold.solver.core.config.heuristic.selector.move.generic.PillarSwapMoveSelectorConfig; @@ -19,6 +20,7 @@ import ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.SubChainChangeMoveSelectorConfig; 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.AdvancedListRuinRecreateMoveSelectorConfig; import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListChangeMoveSelectorConfig; import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListRuinRecreateMoveSelectorConfig; import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListSwapMoveSelectorConfig; @@ -65,6 +67,10 @@ public class LocalSearchPhaseConfig extends PhaseConfig type = RuinRecreateMoveSelectorConfig.class), @XmlElement(name = ListRuinRecreateMoveSelectorConfig.XML_ELEMENT_NAME, type = ListRuinRecreateMoveSelectorConfig.class), + @XmlElement(name = AdvancedRuinRecreateMoveSelectorConfig.XML_ELEMENT_NAME, + type = AdvancedRuinRecreateMoveSelectorConfig.class), + @XmlElement(name = AdvancedListRuinRecreateMoveSelectorConfig.XML_ELEMENT_NAME, + type = AdvancedListRuinRecreateMoveSelectorConfig.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..7e3ec66c6e 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.AdvancedRuinRecreateMoveSelectorConfig; +import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.AdvancedListRuinRecreateMoveSelectorConfig; 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 + buildBasicAdvancedRuinRecreateMoveSelectorFactory( + AdvancedRuinRecreateMoveSelectorConfig moveSelectorConfig); + + AbstractMoveSelectorFactory + buildListAdvancedRuinRecreateMoveSelectorFactory( + AdvancedListRuinRecreateMoveSelectorConfig 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"), + ADVANCED_RUIN_AND_RECREATE("Advanced ruin and recreate", + "remove advancedRuinRecreateMoveSelector and/or advancedListRuinRecreateMoveSelector from the solver configuration"); private final String name; private final String workaround; 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..665ecb79eb 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 @@ -7,6 +7,7 @@ import ai.timefold.solver.core.config.heuristic.selector.move.composite.UnionMoveSelectorConfig; 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.AdvancedRuinRecreateMoveSelectorConfig; import ai.timefold.solver.core.config.heuristic.selector.move.generic.ChangeMoveSelectorConfig; import ai.timefold.solver.core.config.heuristic.selector.move.generic.PillarChangeMoveSelectorConfig; import ai.timefold.solver.core.config.heuristic.selector.move.generic.PillarSwapMoveSelectorConfig; @@ -16,12 +17,14 @@ import ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.SubChainChangeMoveSelectorConfig; 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.AdvancedListRuinRecreateMoveSelectorConfig; import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListChangeMoveSelectorConfig; 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 AdvancedRuinRecreateMoveSelectorConfig advancedRuinRecreateMoveSelectorConfig) { + var enterpriseService = TimefoldSolverEnterpriseService + .loadOrFail(TimefoldSolverEnterpriseService.Feature.ADVANCED_RUIN_AND_RECREATE); + return enterpriseService.buildBasicAdvancedRuinRecreateMoveSelectorFactory(advancedRuinRecreateMoveSelectorConfig); + } else if (moveSelectorConfig instanceof AdvancedListRuinRecreateMoveSelectorConfig advancedListRuinRecreateMoveSelectorConfig) { + var enterpriseService = TimefoldSolverEnterpriseService + .loadOrFail(TimefoldSolverEnterpriseService.Feature.ADVANCED_RUIN_AND_RECREATE); + return enterpriseService + .buildListAdvancedRuinRecreateMoveSelectorFactory(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/preview/api/move/ruin/BasicRecreatedElement.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRecreatedElement.java new file mode 100644 index 0000000000..95a215c483 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRecreatedElement.java @@ -0,0 +1,6 @@ +package ai.timefold.solver.core.preview.api.move.ruin; + +public record BasicRecreatedElement( + Entity_ recreatedEntity, + Metadata_ possibleValueMetadata) implements BasicRuinedOrRecreatedElement { +} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinAndRecreatePicker.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinAndRecreatePicker.java new file mode 100644 index 0000000000..ecd00df034 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinAndRecreatePicker.java @@ -0,0 +1,10 @@ +package ai.timefold.solver.core.preview.api.move.ruin; + +import java.util.List; +import java.util.Random; + +public interface BasicRuinAndRecreatePicker { + void initialize(Solution_ solution); + + List> pickValues(Random random); +} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinAndRecreator.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinAndRecreator.java new file mode 100644 index 0000000000..7a2f4cd484 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinAndRecreator.java @@ -0,0 +1,10 @@ +package ai.timefold.solver.core.preview.api.move.ruin; + +import ai.timefold.solver.core.api.score.Score; + +import org.jspecify.annotations.NonNull; + +public interface BasicRuinAndRecreator> { + Value_ recreate(RecreateEvaluator evaluator, + Entity_ recreatedEntity, Metadata_ valueMetadata); +} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinedElement.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinedElement.java new file mode 100644 index 0000000000..b6a9cf183a --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinedElement.java @@ -0,0 +1,5 @@ +package ai.timefold.solver.core.preview.api.move.ruin; + +public record BasicRuinedElement( + Entity_ ruinedEntity) implements BasicRuinedOrRecreatedElement { +} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinedOrRecreatedElement.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinedOrRecreatedElement.java new file mode 100644 index 0000000000..d997f79991 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinedOrRecreatedElement.java @@ -0,0 +1,5 @@ +package ai.timefold.solver.core.preview.api.move.ruin; + +public sealed interface BasicRuinedOrRecreatedElement + permits BasicRecreatedElement, BasicRuinedElement { +} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRecreatedElement.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRecreatedElement.java new file mode 100644 index 0000000000..cf4e71e3ab --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRecreatedElement.java @@ -0,0 +1,6 @@ +package ai.timefold.solver.core.preview.api.move.ruin; + +public record ListRecreatedElement( + Value_ recreatedValue, + Metadata_ possibleLocationMetadata) implements ListRuinedOrRecreatedElement { +} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinAndRecreatePicker.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinAndRecreatePicker.java new file mode 100644 index 0000000000..5b07b2595f --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinAndRecreatePicker.java @@ -0,0 +1,10 @@ +package ai.timefold.solver.core.preview.api.move.ruin; + +import java.util.List; +import java.util.Random; + +public interface ListRuinAndRecreatePicker { + void initialize(Solution_ solution); + + List> pickValues(Random random); +} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinAndRecreator.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinAndRecreator.java new file mode 100644 index 0000000000..3b328d42f5 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinAndRecreator.java @@ -0,0 +1,11 @@ +package ai.timefold.solver.core.preview.api.move.ruin; + +import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.preview.api.domain.metamodel.ElementPosition; + +import org.jspecify.annotations.NonNull; + +public interface ListRuinAndRecreator> { + ElementPosition recreate(RecreateEvaluator evaluator, Value_ recreatedValue, + Metadata_ locationMetadata); +} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinedElement.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinedElement.java new file mode 100644 index 0000000000..f904b29144 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinedElement.java @@ -0,0 +1,5 @@ +package ai.timefold.solver.core.preview.api.move.ruin; + +public record ListRuinedElement( + Value_ ruinedValue) implements ListRuinedOrRecreatedElement { +} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinedOrRecreatedElement.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinedOrRecreatedElement.java new file mode 100644 index 0000000000..2436065d7c --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinedOrRecreatedElement.java @@ -0,0 +1,5 @@ +package ai.timefold.solver.core.preview.api.move.ruin; + +public sealed interface ListRuinedOrRecreatedElement + permits ListRecreatedElement, ListRuinedElement { +} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/RecreateEvaluator.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/RecreateEvaluator.java new file mode 100644 index 0000000000..e8d9742fe4 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/RecreateEvaluator.java @@ -0,0 +1,9 @@ +package ai.timefold.solver.core.preview.api.move.ruin; + +import ai.timefold.solver.core.api.score.Score; + +import org.jspecify.annotations.NonNull; + +public interface RecreateEvaluator> { + Score_ evaluate(Value_ newValue); +} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/RecreateMetadata.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/RecreateMetadata.java new file mode 100644 index 0000000000..833c335028 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/RecreateMetadata.java @@ -0,0 +1,17 @@ +package ai.timefold.solver.core.preview.api.move.ruin; + +import ai.timefold.solver.core.preview.api.move.Rebaser; + +public interface RecreateMetadata { + default RecreateMetadata rebase(Rebaser rebaser) { + throw new UnsupportedOperationException(); + } + + default Iterable entities() { + throw new UnsupportedOperationException(); + } + + default Iterable values() { + throw new UnsupportedOperationException(); + } +} diff --git a/core/src/main/resources/solver.xsd b/core/src/main/resources/solver.xsd index 8c6e02fc1c..ffcad5e13f 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,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1393,6 +1451,10 @@ + + + + From 329d26db3dbeb7cb3132690f2ec5a8a528fbce81 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Thu, 20 Nov 2025 14:36:06 -0500 Subject: [PATCH 2/9] feat: allow assigning multiple entities at once in an assignment action --- benchmark/src/main/resources/benchmark.xsd | 6 -- ...dvancedRuinRecreateMoveSelectorConfig.java | 21 ----- ...cedListRuinRecreateMoveSelectorConfig.java | 21 ----- .../api/move/ruin/BasicAssignmentAction.java | 65 ++++++++++++++ .../move/ruin/BasicAssignmentEvaluator.java | 13 +++ .../api/move/ruin/BasicRecreatedElement.java | 6 -- .../move/ruin/BasicRuinAndRecreatePicker.java | 8 +- .../api/move/ruin/BasicRuinAndRecreator.java | 10 --- .../api/move/ruin/BasicRuinedElement.java | 5 -- .../ruin/BasicRuinedOrRecreatedElement.java | 5 -- .../api/move/ruin/ListAssignmentAction.java | 88 +++++++++++++++++++ .../move/ruin/ListAssignmentEvaluator.java | 15 ++++ .../api/move/ruin/ListRecreatedElement.java | 6 -- .../move/ruin/ListRuinAndRecreatePicker.java | 8 +- .../api/move/ruin/ListRuinAndRecreator.java | 11 --- .../api/move/ruin/ListRuinedElement.java | 5 -- .../ruin/ListRuinedOrRecreatedElement.java | 5 -- .../api/move/ruin/RecreateEvaluator.java | 9 -- .../api/move/ruin/RecreateMetadata.java | 17 ---- core/src/main/resources/solver.xsd | 4 - 20 files changed, 193 insertions(+), 135 deletions(-) create mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicAssignmentAction.java create mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicAssignmentEvaluator.java delete mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRecreatedElement.java delete mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinAndRecreator.java delete mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinedElement.java delete mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinedOrRecreatedElement.java create mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListAssignmentAction.java create mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListAssignmentEvaluator.java delete mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRecreatedElement.java delete mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinAndRecreator.java delete mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinedElement.java delete mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinedOrRecreatedElement.java delete mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/RecreateEvaluator.java delete mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/RecreateMetadata.java diff --git a/benchmark/src/main/resources/benchmark.xsd b/benchmark/src/main/resources/benchmark.xsd index 28c1f20e91..71c5ca3bb4 100644 --- a/benchmark/src/main/resources/benchmark.xsd +++ b/benchmark/src/main/resources/benchmark.xsd @@ -2093,9 +2093,6 @@ - - - @@ -2129,9 +2126,6 @@ - - - diff --git a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/AdvancedRuinRecreateMoveSelectorConfig.java b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/AdvancedRuinRecreateMoveSelectorConfig.java index 8ccb1a987d..3f8b42ffd5 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/AdvancedRuinRecreateMoveSelectorConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/AdvancedRuinRecreateMoveSelectorConfig.java @@ -7,13 +7,11 @@ import ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig; import ai.timefold.solver.core.config.util.ConfigUtils; import ai.timefold.solver.core.preview.api.move.ruin.BasicRuinAndRecreatePicker; -import ai.timefold.solver.core.preview.api.move.ruin.BasicRuinAndRecreator; import org.jspecify.annotations.NonNull; @XmlType(propOrder = { "ruinAndRecreatePickerClass", - "recreatorClass", "entityClass", "variableName" }) @@ -21,7 +19,6 @@ public class AdvancedRuinRecreateMoveSelectorConfig extends MoveSelectorConfig> ruinAndRecreatePickerClass; - protected Class> recreatorClass; protected Class entityClass = null; protected String variableName = null; @@ -45,21 +42,6 @@ public void setRuinAndRecreatePickerClass( return this; } - public Class> getRecreatorClass() { - return recreatorClass; - } - - public void setRecreatorClass( - Class> recreatorClass) { - this.recreatorClass = recreatorClass; - } - - public @NonNull AdvancedRuinRecreateMoveSelectorConfig - withRecreatorClass(@NonNull Class> recreatorClass) { - this.setRecreatorClass(recreatorClass); - return this; - } - public Class getEntityClass() { return entityClass; } @@ -103,7 +85,6 @@ public boolean hasNearbySelectionConfig() { @Override public void visitReferencedClasses(@NonNull Consumer> classVisitor) { classVisitor.accept(ruinAndRecreatePickerClass); - classVisitor.accept(recreatorClass); } @Override @@ -113,8 +94,6 @@ public void visitReferencedClasses(@NonNull Consumer> classVisitor) { ruinAndRecreatePickerClass = ConfigUtils.inheritOverwritableProperty(ruinAndRecreatePickerClass, inheritedConfig.getRuinAndRecreatePickerClass()); - recreatorClass = - ConfigUtils.inheritOverwritableProperty(recreatorClass, inheritedConfig.getRecreatorClass()); entityClass = ConfigUtils.inheritOverwritableProperty(entityClass, inheritedConfig.getEntityClass()); variableName = diff --git a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/AdvancedListRuinRecreateMoveSelectorConfig.java b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/AdvancedListRuinRecreateMoveSelectorConfig.java index 354130c98c..8f05c24ca1 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/AdvancedListRuinRecreateMoveSelectorConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/AdvancedListRuinRecreateMoveSelectorConfig.java @@ -7,13 +7,11 @@ import ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig; import ai.timefold.solver.core.config.util.ConfigUtils; import ai.timefold.solver.core.preview.api.move.ruin.ListRuinAndRecreatePicker; -import ai.timefold.solver.core.preview.api.move.ruin.ListRuinAndRecreator; import org.jspecify.annotations.NonNull; @XmlType(propOrder = { "ruinAndRecreatePickerClass", - "recreatorClass", "entityClass", "valueClass", "variableName" @@ -22,7 +20,6 @@ public class AdvancedListRuinRecreateMoveSelectorConfig extends MoveSelectorConf public static final String XML_ELEMENT_NAME = "advancedListRuinRecreateMoveSelector"; protected Class> ruinAndRecreatePickerClass; - protected Class> recreatorClass; protected Class entityClass = null; protected Class valueClass = null; @@ -47,21 +44,6 @@ public void setRuinAndRecreatePickerClass( return this; } - public Class> getRecreatorClass() { - return recreatorClass; - } - - public void setRecreatorClass( - Class> recreatorClass) { - this.recreatorClass = recreatorClass; - } - - public @NonNull AdvancedListRuinRecreateMoveSelectorConfig - withRecreatorClass(@NonNull Class> recreatorClass) { - this.setRecreatorClass(recreatorClass); - return this; - } - public Class getEntityClass() { return entityClass; } @@ -118,7 +100,6 @@ public boolean hasNearbySelectionConfig() { @Override public void visitReferencedClasses(@NonNull Consumer> classVisitor) { classVisitor.accept(ruinAndRecreatePickerClass); - classVisitor.accept(recreatorClass); } @Override @@ -128,8 +109,6 @@ public void visitReferencedClasses(@NonNull Consumer> classVisitor) { ruinAndRecreatePickerClass = ConfigUtils.inheritOverwritableProperty(ruinAndRecreatePickerClass, inheritedConfig.getRuinAndRecreatePickerClass()); - recreatorClass = - ConfigUtils.inheritOverwritableProperty(recreatorClass, inheritedConfig.getRecreatorClass()); entityClass = ConfigUtils.inheritOverwritableProperty(entityClass, inheritedConfig.getEntityClass()); valueClass = diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicAssignmentAction.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicAssignmentAction.java new file mode 100644 index 0000000000..fe22cfb8f6 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicAssignmentAction.java @@ -0,0 +1,65 @@ +package ai.timefold.solver.core.preview.api.move.ruin; + +import java.util.stream.StreamSupport; + +import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.preview.api.move.Rebaser; + +import org.jspecify.annotations.NonNull; + +public interface BasicAssignmentAction> { + void assignValues(BasicAssignmentEvaluator evaluator); + + default BasicAssignmentAction rebase(Rebaser rebaser) { + throw new UnsupportedOperationException("Rebase is not supported. Implement rebase to support multithreaded solving."); + } + + static > BasicAssignmentAction + unassign(Entity_ entity) { + return new BasicAssignmentAction<>() { + @Override + public void assignValues(BasicAssignmentEvaluator evaluator) { + evaluator.unassign(entity); + } + + @Override + public BasicAssignmentAction rebase(Rebaser rebaser) { + return BasicAssignmentAction.unassign(rebaser.rebase(entity)); + } + }; + } + + static > BasicAssignmentAction + bestFit(Entity_ entity, Iterable values) { + return new BasicAssignmentAction<>() { + @Override + public void assignValues(BasicAssignmentEvaluator evaluator) { + var iterator = values.iterator(); + if (!iterator.hasNext()) { + throw new IllegalArgumentException( + "Cannot perform a best-fit assignment on entity (%s) since the value iterable (%s) is empty." + .formatted(entity, values)); + } + var bestValue = iterator.next(); + evaluator.assign(entity, bestValue); + var bestScore = evaluator.score(); + while (iterator.hasNext()) { + var candidateValue = iterator.next(); + evaluator.assign(entity, candidateValue); + var score = evaluator.score(); + if (score.compareTo(bestScore) > 0) { + bestScore = score; + bestValue = candidateValue; + } + } + evaluator.assign(entity, bestValue); + } + + @Override + public BasicAssignmentAction rebase(Rebaser rebaser) { + return BasicAssignmentAction.bestFit(rebaser.rebase(entity), + StreamSupport.stream(values.spliterator(), false).map(rebaser::rebase).toList()); + } + }; + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicAssignmentEvaluator.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicAssignmentEvaluator.java new file mode 100644 index 0000000000..bdd09ea740 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicAssignmentEvaluator.java @@ -0,0 +1,13 @@ +package ai.timefold.solver.core.preview.api.move.ruin; + +import ai.timefold.solver.core.api.score.Score; + +import org.jspecify.annotations.NonNull; + +public interface BasicAssignmentEvaluator> { + void assign(Entity_ entity, Value_ value); + + void unassign(Entity_ entity); + + Score_ score(); +} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRecreatedElement.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRecreatedElement.java deleted file mode 100644 index 95a215c483..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRecreatedElement.java +++ /dev/null @@ -1,6 +0,0 @@ -package ai.timefold.solver.core.preview.api.move.ruin; - -public record BasicRecreatedElement( - Entity_ recreatedEntity, - Metadata_ possibleValueMetadata) implements BasicRuinedOrRecreatedElement { -} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinAndRecreatePicker.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinAndRecreatePicker.java index ecd00df034..a94cc962b3 100644 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinAndRecreatePicker.java +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinAndRecreatePicker.java @@ -3,8 +3,12 @@ import java.util.List; import java.util.Random; -public interface BasicRuinAndRecreatePicker { +import ai.timefold.solver.core.api.score.Score; + +import org.jspecify.annotations.NonNull; + +public interface BasicRuinAndRecreatePicker> { void initialize(Solution_ solution); - List> pickValues(Random random); + List> pickAssignments(Random random); } diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinAndRecreator.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinAndRecreator.java deleted file mode 100644 index 7a2f4cd484..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinAndRecreator.java +++ /dev/null @@ -1,10 +0,0 @@ -package ai.timefold.solver.core.preview.api.move.ruin; - -import ai.timefold.solver.core.api.score.Score; - -import org.jspecify.annotations.NonNull; - -public interface BasicRuinAndRecreator> { - Value_ recreate(RecreateEvaluator evaluator, - Entity_ recreatedEntity, Metadata_ valueMetadata); -} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinedElement.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinedElement.java deleted file mode 100644 index b6a9cf183a..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinedElement.java +++ /dev/null @@ -1,5 +0,0 @@ -package ai.timefold.solver.core.preview.api.move.ruin; - -public record BasicRuinedElement( - Entity_ ruinedEntity) implements BasicRuinedOrRecreatedElement { -} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinedOrRecreatedElement.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinedOrRecreatedElement.java deleted file mode 100644 index d997f79991..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinedOrRecreatedElement.java +++ /dev/null @@ -1,5 +0,0 @@ -package ai.timefold.solver.core.preview.api.move.ruin; - -public sealed interface BasicRuinedOrRecreatedElement - permits BasicRecreatedElement, BasicRuinedElement { -} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListAssignmentAction.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListAssignmentAction.java new file mode 100644 index 0000000000..c684a76c65 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListAssignmentAction.java @@ -0,0 +1,88 @@ +package ai.timefold.solver.core.preview.api.move.ruin; + +import java.util.stream.StreamSupport; + +import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.preview.api.move.Rebaser; + +import org.jspecify.annotations.NonNull; + +public interface ListAssignmentAction> { + void assignValues(ListAssignmentEvaluator evaluator); + + default ListAssignmentAction rebase(Rebaser rebaser) { + throw new UnsupportedOperationException("Rebase is not supported. Implement rebase to support multithreaded solving."); + } + + static > ListAssignmentAction + unassign(Value_ value) { + return new ListAssignmentAction<>() { + @Override + public void assignValues(ListAssignmentEvaluator evaluator) { + evaluator.unassign(value); + } + + @Override + public ListAssignmentAction rebase(Rebaser rebaser) { + return unassign(rebaser.rebase(value)); + } + }; + } + + static > ListAssignmentAction + bestFit(Value_ value, Iterable entities) { + return new ListAssignmentAction<>() { + @Override + public void assignValues(ListAssignmentEvaluator evaluator) { + var iterator = entities.iterator(); + if (!iterator.hasNext()) { + throw new IllegalArgumentException( + "Cannot perform a best-fit assignment on value (%s) since the entities iterable (%s) is empty." + .formatted(value, entities)); + } + + // ensure value is unassigned initially to simplify list position calculations + // (so we can ignore the case that the value is already present in entity) + evaluator.unassign(value); + + var bestEntity = iterator.next(); + var bestIndex = 0; + + var listSize = evaluator.listSize(bestEntity); + evaluator.assign(bestEntity, 0, value); + var bestScore = evaluator.score(); + + for (var i = 1; i <= listSize; i++) { + evaluator.assign(bestEntity, i, value); + var score = evaluator.score(); + if (score.compareTo(bestScore) > 0) { + bestScore = score; + bestIndex = i; + } + } + + while (iterator.hasNext()) { + var candidateEntity = iterator.next(); + listSize = evaluator.listSize(candidateEntity); + for (var i = 0; i <= listSize; i++) { + evaluator.assign(candidateEntity, i, value); + var score = evaluator.score(); + if (score.compareTo(bestScore) > 0) { + bestScore = score; + bestEntity = candidateEntity; + bestIndex = i; + } + } + } + evaluator.assign(bestEntity, bestIndex, value); + } + + @Override + public ListAssignmentAction rebase(Rebaser rebaser) { + return bestFit(rebaser.rebase(value), + StreamSupport.stream(entities.spliterator(), false) + .map(rebaser::rebase).toList()); + } + }; + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListAssignmentEvaluator.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListAssignmentEvaluator.java new file mode 100644 index 0000000000..41f6a1ae1a --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListAssignmentEvaluator.java @@ -0,0 +1,15 @@ +package ai.timefold.solver.core.preview.api.move.ruin; + +import ai.timefold.solver.core.api.score.Score; + +import org.jspecify.annotations.NonNull; + +public interface ListAssignmentEvaluator> { + void assign(Entity_ entity, int index, Value_ value); + + void unassign(Value_ value); + + int listSize(Entity_ entity); + + Score_ score(); +} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRecreatedElement.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRecreatedElement.java deleted file mode 100644 index cf4e71e3ab..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRecreatedElement.java +++ /dev/null @@ -1,6 +0,0 @@ -package ai.timefold.solver.core.preview.api.move.ruin; - -public record ListRecreatedElement( - Value_ recreatedValue, - Metadata_ possibleLocationMetadata) implements ListRuinedOrRecreatedElement { -} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinAndRecreatePicker.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinAndRecreatePicker.java index 5b07b2595f..08c74a6252 100644 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinAndRecreatePicker.java +++ b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinAndRecreatePicker.java @@ -3,8 +3,12 @@ import java.util.List; import java.util.Random; -public interface ListRuinAndRecreatePicker { +import ai.timefold.solver.core.api.score.Score; + +import org.jspecify.annotations.NonNull; + +public interface ListRuinAndRecreatePicker> { void initialize(Solution_ solution); - List> pickValues(Random random); + List> pickAssignments(Random random); } diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinAndRecreator.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinAndRecreator.java deleted file mode 100644 index 3b328d42f5..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinAndRecreator.java +++ /dev/null @@ -1,11 +0,0 @@ -package ai.timefold.solver.core.preview.api.move.ruin; - -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.preview.api.domain.metamodel.ElementPosition; - -import org.jspecify.annotations.NonNull; - -public interface ListRuinAndRecreator> { - ElementPosition recreate(RecreateEvaluator evaluator, Value_ recreatedValue, - Metadata_ locationMetadata); -} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinedElement.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinedElement.java deleted file mode 100644 index f904b29144..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinedElement.java +++ /dev/null @@ -1,5 +0,0 @@ -package ai.timefold.solver.core.preview.api.move.ruin; - -public record ListRuinedElement( - Value_ ruinedValue) implements ListRuinedOrRecreatedElement { -} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinedOrRecreatedElement.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinedOrRecreatedElement.java deleted file mode 100644 index 2436065d7c..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinedOrRecreatedElement.java +++ /dev/null @@ -1,5 +0,0 @@ -package ai.timefold.solver.core.preview.api.move.ruin; - -public sealed interface ListRuinedOrRecreatedElement - permits ListRecreatedElement, ListRuinedElement { -} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/RecreateEvaluator.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/RecreateEvaluator.java deleted file mode 100644 index e8d9742fe4..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/RecreateEvaluator.java +++ /dev/null @@ -1,9 +0,0 @@ -package ai.timefold.solver.core.preview.api.move.ruin; - -import ai.timefold.solver.core.api.score.Score; - -import org.jspecify.annotations.NonNull; - -public interface RecreateEvaluator> { - Score_ evaluate(Value_ newValue); -} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/RecreateMetadata.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/RecreateMetadata.java deleted file mode 100644 index 833c335028..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/RecreateMetadata.java +++ /dev/null @@ -1,17 +0,0 @@ -package ai.timefold.solver.core.preview.api.move.ruin; - -import ai.timefold.solver.core.preview.api.move.Rebaser; - -public interface RecreateMetadata { - default RecreateMetadata rebase(Rebaser rebaser) { - throw new UnsupportedOperationException(); - } - - default Iterable entities() { - throw new UnsupportedOperationException(); - } - - default Iterable values() { - throw new UnsupportedOperationException(); - } -} diff --git a/core/src/main/resources/solver.xsd b/core/src/main/resources/solver.xsd index ffcad5e13f..5614d5884f 100644 --- a/core/src/main/resources/solver.xsd +++ b/core/src/main/resources/solver.xsd @@ -1201,8 +1201,6 @@ - - @@ -1225,8 +1223,6 @@ - - From e26dae8bf6768430e135753f690a6c01015979d2 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 9 Dec 2025 10:51:17 -0500 Subject: [PATCH 3/9] chore: make advanced R&R use neighboorhood API --- ...dvancedRuinRecreateMoveSelectorConfig.java | 9 +- ...cedListRuinRecreateMoveSelectorConfig.java | 9 +- .../director/DefaultMutableSolutionView.java | 258 ++++++++++++++++++ .../move/director/EphemeralMoveDirector.java | 2 +- .../core/impl/move/director/MoveDirector.java | 242 +--------------- .../VariableChangeRecordingScoreDirector.java | 7 + .../VariableDescriptorAwareScoreDirector.java | 4 + .../api/move/ruin/BasicAssignmentAction.java | 65 ----- .../move/ruin/BasicAssignmentEvaluator.java | 13 - .../move/ruin/BasicRuinAndRecreatePicker.java | 14 - .../api/move/ruin/ListAssignmentAction.java | 88 ------ .../move/ruin/ListAssignmentEvaluator.java | 15 - .../move/ruin/ListRuinAndRecreatePicker.java | 14 - 13 files changed, 285 insertions(+), 455 deletions(-) create mode 100644 core/src/main/java/ai/timefold/solver/core/impl/move/director/DefaultMutableSolutionView.java delete mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicAssignmentAction.java delete mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicAssignmentEvaluator.java delete mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinAndRecreatePicker.java delete mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListAssignmentAction.java delete mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListAssignmentEvaluator.java delete mode 100644 core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinAndRecreatePicker.java diff --git a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/AdvancedRuinRecreateMoveSelectorConfig.java b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/AdvancedRuinRecreateMoveSelectorConfig.java index 3f8b42ffd5..b1c4547cec 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/AdvancedRuinRecreateMoveSelectorConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/AdvancedRuinRecreateMoveSelectorConfig.java @@ -6,7 +6,6 @@ import ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig; import ai.timefold.solver.core.config.util.ConfigUtils; -import ai.timefold.solver.core.preview.api.move.ruin.BasicRuinAndRecreatePicker; import org.jspecify.annotations.NonNull; @@ -18,7 +17,7 @@ public class AdvancedRuinRecreateMoveSelectorConfig extends MoveSelectorConfig { public static final String XML_ELEMENT_NAME = "advancedRuinRecreateMoveSelector"; - protected Class> ruinAndRecreatePickerClass; + protected Class ruinAndRecreatePickerClass; protected Class entityClass = null; protected String variableName = null; @@ -27,17 +26,17 @@ public class AdvancedRuinRecreateMoveSelectorConfig extends MoveSelectorConfig> getRuinAndRecreatePickerClass() { + public Class getRuinAndRecreatePickerClass() { return ruinAndRecreatePickerClass; } public void setRuinAndRecreatePickerClass( - Class> ruinAndRecreatePickerClass) { + Class ruinAndRecreatePickerClass) { this.ruinAndRecreatePickerClass = ruinAndRecreatePickerClass; } public @NonNull AdvancedRuinRecreateMoveSelectorConfig withRuinAndRecreatePickerClass( - @NonNull Class> ruinAndRecreatePickerClass) { + @NonNull Class ruinAndRecreatePickerClass) { this.setRuinAndRecreatePickerClass(ruinAndRecreatePickerClass); return this; } diff --git a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/AdvancedListRuinRecreateMoveSelectorConfig.java b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/AdvancedListRuinRecreateMoveSelectorConfig.java index 8f05c24ca1..44260fa052 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/AdvancedListRuinRecreateMoveSelectorConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/AdvancedListRuinRecreateMoveSelectorConfig.java @@ -6,7 +6,6 @@ import ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig; import ai.timefold.solver.core.config.util.ConfigUtils; -import ai.timefold.solver.core.preview.api.move.ruin.ListRuinAndRecreatePicker; import org.jspecify.annotations.NonNull; @@ -19,7 +18,7 @@ public class AdvancedListRuinRecreateMoveSelectorConfig extends MoveSelectorConfig { public static final String XML_ELEMENT_NAME = "advancedListRuinRecreateMoveSelector"; - protected Class> ruinAndRecreatePickerClass; + protected Class ruinAndRecreatePickerClass; protected Class entityClass = null; protected Class valueClass = null; @@ -29,17 +28,17 @@ public class AdvancedListRuinRecreateMoveSelectorConfig extends MoveSelectorConf // Getters/Setters // ************************** - public Class> getRuinAndRecreatePickerClass() { + public Class getRuinAndRecreatePickerClass() { return ruinAndRecreatePickerClass; } public void setRuinAndRecreatePickerClass( - Class> ruinAndRecreatePickerClass) { + Class ruinAndRecreatePickerClass) { this.ruinAndRecreatePickerClass = ruinAndRecreatePickerClass; } public @NonNull AdvancedListRuinRecreateMoveSelectorConfig withRuinAndRecreatePickerClass( - @NonNull Class> ruinAndRecreatePickerClass) { + @NonNull Class ruinAndRecreatePickerClass) { this.setRuinAndRecreatePickerClass(ruinAndRecreatePickerClass); return this; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/director/DefaultMutableSolutionView.java b/core/src/main/java/ai/timefold/solver/core/impl/move/director/DefaultMutableSolutionView.java new file mode 100644 index 0000000000..f7f5cef146 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/director/DefaultMutableSolutionView.java @@ -0,0 +1,258 @@ +package ai.timefold.solver.core.impl.move.director; + +import java.util.Objects; + +import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; +import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultPlanningListVariableMetaModel; +import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultPlanningVariableMetaModel; +import ai.timefold.solver.core.impl.domain.solution.descriptor.InnerGenuineVariableMetaModel; +import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply; +import ai.timefold.solver.core.impl.domain.variable.descriptor.BasicVariableDescriptor; +import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; +import ai.timefold.solver.core.impl.move.InnerMutableSolutionView; +import ai.timefold.solver.core.impl.score.director.VariableDescriptorAwareScoreDirector; +import ai.timefold.solver.core.preview.api.domain.metamodel.ElementPosition; +import ai.timefold.solver.core.preview.api.domain.metamodel.GenuineVariableMetaModel; +import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningListVariableMetaModel; +import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningVariableMetaModel; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +public sealed class DefaultMutableSolutionView implements InnerMutableSolutionView + permits MoveDirector { + protected final VariableDescriptorAwareScoreDirector externalScoreDirector; + + public DefaultMutableSolutionView(VariableDescriptorAwareScoreDirector externalScoreDirector) { + this.externalScoreDirector = externalScoreDirector; + } + + @Override + public final void assignValue(PlanningListVariableMetaModel variableMetaModel, + Value_ planningValue, Entity_ destinationEntity, int destinationIndex) { + var variableDescriptor = + ((DefaultPlanningListVariableMetaModel) variableMetaModel).variableDescriptor(); + externalScoreDirector.beforeListVariableElementAssigned(variableDescriptor, planningValue); + externalScoreDirector.beforeListVariableChanged(variableDescriptor, destinationEntity, destinationIndex, + destinationIndex); + variableDescriptor.addElement(destinationEntity, destinationIndex, planningValue); + externalScoreDirector.afterListVariableChanged(variableDescriptor, destinationEntity, destinationIndex, + destinationIndex + 1); + externalScoreDirector.afterListVariableElementAssigned(variableDescriptor, planningValue); + } + + @Override + public void unassignValue(PlanningListVariableMetaModel variableMetaModel, + Value_ value) { + var locationInList = getPositionOf(variableMetaModel, value) + .ensureAssigned(() -> """ + The value (%s) is not assigned to a list variable. + This may indicate score corruption or a problem with the move's implementation.""" + .formatted(value)); + unassignValue(variableMetaModel, value, locationInList.entity(), locationInList.index()); + } + + @Override + public Value_ unassignValue(PlanningListVariableMetaModel variableMetaModel, + Entity_ entity, int index) { + var value = getValueAtIndex(variableMetaModel, entity, index); + unassignValue(variableMetaModel, value, entity, index); + return value; + } + + private void unassignValue( + PlanningListVariableMetaModel variableMetaModel, Value_ movedValue, Entity_ entity, + int index) { + var variableDescriptor = + ((DefaultPlanningListVariableMetaModel) variableMetaModel).variableDescriptor(); + externalScoreDirector.beforeListVariableElementUnassigned(variableDescriptor, movedValue); + externalScoreDirector.beforeListVariableChanged(variableDescriptor, entity, index, index + 1); + variableDescriptor.getValue(entity).remove(index); + externalScoreDirector.afterListVariableChanged(variableDescriptor, entity, index, index); + externalScoreDirector.afterListVariableElementUnassigned(variableDescriptor, movedValue); + } + + public final void changeVariable(PlanningVariableMetaModel variableMetaModel, + Entity_ entity, @Nullable Value_ newValue) { + var variableDescriptor = extractVariableDescriptor(variableMetaModel); + externalScoreDirector.beforeVariableChanged(variableDescriptor, entity); + variableDescriptor.setValue(entity, newValue); + externalScoreDirector.afterVariableChanged(variableDescriptor, entity); + } + + @SuppressWarnings("unchecked") + public final @Nullable Value_ moveValueBetweenLists( + PlanningListVariableMetaModel variableMetaModel, Entity_ sourceEntity, int sourceIndex, + Entity_ destinationEntity, int destinationIndex) { + if (sourceEntity == destinationEntity) { + return moveValueInList(variableMetaModel, sourceEntity, sourceIndex, destinationIndex); + } + var variableDescriptor = extractVariableDescriptor(variableMetaModel); + + externalScoreDirector.beforeListVariableChanged(variableDescriptor, sourceEntity, sourceIndex, sourceIndex + 1); + var element = (Value_) variableDescriptor.removeElement(sourceEntity, sourceIndex); + externalScoreDirector.afterListVariableChanged(variableDescriptor, sourceEntity, sourceIndex, sourceIndex); + + externalScoreDirector.beforeListVariableChanged(variableDescriptor, destinationEntity, destinationIndex, + destinationIndex); + variableDescriptor.addElement(destinationEntity, destinationIndex, element); + externalScoreDirector.afterListVariableChanged(variableDescriptor, destinationEntity, destinationIndex, + destinationIndex + 1); + + return element; + } + + @SuppressWarnings("unchecked") + @Override + public final @Nullable Value_ moveValueInList( + PlanningListVariableMetaModel variableMetaModel, Entity_ sourceEntity, int sourceIndex, + int destinationIndex) { + if (sourceIndex == destinationIndex) { + return null; + } + var variableDescriptor = extractVariableDescriptor(variableMetaModel); + var fromIndex = Math.min(sourceIndex, destinationIndex); + var toIndex = Math.max(sourceIndex, destinationIndex) + 1; + + externalScoreDirector.beforeListVariableChanged(variableDescriptor, sourceEntity, fromIndex, toIndex); + Value_ element = (Value_) variableDescriptor.removeElement(sourceEntity, sourceIndex); + variableDescriptor.addElement(sourceEntity, destinationIndex, element); + externalScoreDirector.afterListVariableChanged(variableDescriptor, sourceEntity, fromIndex, toIndex); + + return element; + } + + @Override + public void swapValuesBetweenLists( + PlanningListVariableMetaModel variableMetaModel, Entity_ leftEntity, int leftIndex, + Entity_ rightEntity, int rightIndex) { + if (leftEntity == rightEntity) { + swapValuesInList(variableMetaModel, leftEntity, leftIndex, rightIndex); + } else { + var variableDescriptor = extractVariableDescriptor(variableMetaModel); + externalScoreDirector.beforeListVariableChanged(variableDescriptor, leftEntity, leftIndex, leftIndex + 1); + externalScoreDirector.beforeListVariableChanged(variableDescriptor, rightEntity, rightIndex, rightIndex + 1); + var oldLeftElement = variableDescriptor.setElement(leftEntity, leftIndex, + variableDescriptor.getElement(rightEntity, rightIndex)); + variableDescriptor.setElement(rightEntity, rightIndex, oldLeftElement); + externalScoreDirector.afterListVariableChanged(variableDescriptor, leftEntity, leftIndex, leftIndex + 1); + externalScoreDirector.afterListVariableChanged(variableDescriptor, rightEntity, rightIndex, rightIndex + 1); + } + } + + @Override + public void swapValuesInList(PlanningListVariableMetaModel variableMetaModel, + Entity_ entity, int leftIndex, int rightIndex) { + if (leftIndex == rightIndex) { + return; + } + + var variableDescriptor = extractVariableDescriptor(variableMetaModel); + var fromIndex = Math.min(leftIndex, rightIndex); + var toIndex = Math.max(leftIndex, rightIndex) + 1; + + externalScoreDirector.beforeListVariableChanged(variableDescriptor, entity, fromIndex, toIndex); + var oldLeftElement = + variableDescriptor.setElement(entity, leftIndex, variableDescriptor.getElement(entity, rightIndex)); + variableDescriptor.setElement(entity, rightIndex, oldLeftElement); + externalScoreDirector.afterListVariableChanged(variableDescriptor, entity, fromIndex, toIndex); + } + + @Override + public boolean isValueInRange(GenuineVariableMetaModel variableMetaModel, + @Nullable Entity_ entity, @Nullable Value_ value) { + var innerGenuineVariableMetaModel = (InnerGenuineVariableMetaModel) variableMetaModel; + var valueRangeDescriptor = innerGenuineVariableMetaModel.variableDescriptor() + .getValueRangeDescriptor(); + if (valueRangeDescriptor.canExtractValueRangeFromSolution()) { + return externalScoreDirector.getValueRangeManager() + .getFromSolution(valueRangeDescriptor) + .contains(value); + } else { + return externalScoreDirector.getValueRangeManager() + .getFromEntity(valueRangeDescriptor, Objects.requireNonNull(entity)) + .contains(value); + } + } + + @Override + public final Value_ getValue(PlanningVariableMetaModel variableMetaModel, + Entity_ entity) { + return extractVariableDescriptor(variableMetaModel).getValue(entity); + } + + @Override + public int countValues(PlanningListVariableMetaModel variableMetaModel, + Entity_ entity) { + return extractVariableDescriptor(variableMetaModel).getValue(entity).size(); + } + + @SuppressWarnings("unchecked") + @Override + public final Value_ getValueAtIndex( + PlanningListVariableMetaModel variableMetaModel, Entity_ entity, int index) { + return (Value_) extractVariableDescriptor(variableMetaModel).getValue(entity).get(index); + } + + @Override + public ElementPosition + getPositionOf(PlanningListVariableMetaModel variableMetaModel, Value_ value) { + return getPositionOf(externalScoreDirector, variableMetaModel, value); + } + + @Override + public boolean isPinned(PlanningVariableMetaModel variableMetaModel, + @Nullable Entity_ entity) { + return isPinned(extractVariableDescriptor(variableMetaModel).getEntityDescriptor(), entity); + } + + public boolean isPinned(EntityDescriptor entityDescriptor, @Nullable Value_ entity) { + if (entity == null) { + return false; // Null is never pinned. + } + return !entityDescriptor.isMovable(externalScoreDirector.getWorkingSolution(), entity); + } + + protected static ElementPosition getPositionOf( + VariableDescriptorAwareScoreDirector scoreDirector, + PlanningListVariableMetaModel listVariableMetaModel, Value_ value) { + var listVariableDescriptor = extractVariableDescriptor(listVariableMetaModel); + return scoreDirector.getListVariableStateSupply(listVariableDescriptor).getElementPosition(value); + } + + @Override + public boolean isPinned(PlanningListVariableMetaModel variableMetaModel, + @Nullable Value_ value) { + return isPinned(extractVariableDescriptor(variableMetaModel), value); + } + + public boolean isPinned(ListVariableDescriptor listVariableDescriptor, @Nullable Value_ value) { + if (value == null) { + return false; // Null is never pinned. + } + return getListVariableStateSupply(listVariableDescriptor).isPinned(value); + } + + @SuppressWarnings("unchecked") + public final ListVariableStateSupply + getListVariableStateSupply(ListVariableDescriptor listVariableDescriptor) { + return (ListVariableStateSupply) externalScoreDirector + .getListVariableStateSupply(listVariableDescriptor); + } + + private static BasicVariableDescriptor + extractVariableDescriptor(PlanningVariableMetaModel variableMetaModel) { + return ((DefaultPlanningVariableMetaModel) variableMetaModel).variableDescriptor(); + } + + private static ListVariableDescriptor + extractVariableDescriptor(PlanningListVariableMetaModel variableMetaModel) { + return ((DefaultPlanningListVariableMetaModel) variableMetaModel).variableDescriptor(); + } + + @Override + public VariableDescriptorAwareScoreDirector getScoreDirector() { + return externalScoreDirector; + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/director/EphemeralMoveDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/move/director/EphemeralMoveDirector.java index 96b2873018..351f622c51 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/director/EphemeralMoveDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/director/EphemeralMoveDirector.java @@ -24,7 +24,7 @@ final class EphemeralMoveDirector> extends MoveDirector { EphemeralMoveDirector(InnerScoreDirector scoreDirector) { - super(scoreDirector); + super(new VariableChangeRecordingScoreDirector<>(scoreDirector), scoreDirector); } Move createUndoMove() { 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..705d739fd6 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 @@ -4,21 +4,11 @@ import java.util.function.BiFunction; import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; -import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultPlanningListVariableMetaModel; -import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultPlanningVariableMetaModel; -import ai.timefold.solver.core.impl.domain.solution.descriptor.InnerGenuineVariableMetaModel; -import ai.timefold.solver.core.impl.domain.variable.descriptor.BasicVariableDescriptor; -import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.heuristic.move.MoveAdapters; import ai.timefold.solver.core.impl.move.InnerMutableSolutionView; import ai.timefold.solver.core.impl.score.director.InnerScore; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.score.director.VariableDescriptorAwareScoreDirector; -import ai.timefold.solver.core.preview.api.domain.metamodel.ElementPosition; -import ai.timefold.solver.core.preview.api.domain.metamodel.GenuineVariableMetaModel; -import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningListVariableMetaModel; -import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningVariableMetaModel; import ai.timefold.solver.core.preview.api.move.Move; import ai.timefold.solver.core.preview.api.move.Rebaser; @@ -27,173 +17,22 @@ @NullMarked public sealed class MoveDirector> + extends DefaultMutableSolutionView implements InnerMutableSolutionView, Rebaser permits EphemeralMoveDirector { - protected final VariableDescriptorAwareScoreDirector externalScoreDirector; private final InnerScoreDirector backingScoreDirector; public MoveDirector(InnerScoreDirector scoreDirector) { + super(scoreDirector); this.backingScoreDirector = Objects.requireNonNull(scoreDirector); - if (EphemeralMoveDirector.class.isAssignableFrom(getClass())) { - // Ephemeral move director records operations for a later undo, - // and the external director is no longer an instance of InnerScoreDirector. - // However, some pieces of code need methods from InnerScoreDirector, - // in which case we turn to the backing score director. - // This is only safe for operations that do not need to be undone, such as calculateScore(). - // Operations which need undo must go through the external score director, which is recording in this case. - this.externalScoreDirector = new VariableChangeRecordingScoreDirector<>(scoreDirector, false); - } else { - this.externalScoreDirector = scoreDirector; - } } - @Override - public final void assignValue(PlanningListVariableMetaModel variableMetaModel, - Value_ planningValue, Entity_ destinationEntity, int destinationIndex) { - var variableDescriptor = - ((DefaultPlanningListVariableMetaModel) variableMetaModel).variableDescriptor(); - externalScoreDirector.beforeListVariableElementAssigned(variableDescriptor, planningValue); - externalScoreDirector.beforeListVariableChanged(variableDescriptor, destinationEntity, destinationIndex, - destinationIndex); - variableDescriptor.addElement(destinationEntity, destinationIndex, planningValue); - externalScoreDirector.afterListVariableChanged(variableDescriptor, destinationEntity, destinationIndex, - destinationIndex + 1); - externalScoreDirector.afterListVariableElementAssigned(variableDescriptor, planningValue); - } - - @Override - public void unassignValue(PlanningListVariableMetaModel variableMetaModel, - Value_ value) { - var locationInList = getPositionOf(variableMetaModel, value) - .ensureAssigned(() -> """ - The value (%s) is not assigned to a list variable. - This may indicate score corruption or a problem with the move's implementation.""" - .formatted(value)); - unassignValue(variableMetaModel, value, locationInList.entity(), locationInList.index()); - } - - @Override - public Value_ unassignValue(PlanningListVariableMetaModel variableMetaModel, - Entity_ entity, int index) { - var value = getValueAtIndex(variableMetaModel, entity, index); - unassignValue(variableMetaModel, value, entity, index); - return value; - } - - private void unassignValue( - PlanningListVariableMetaModel variableMetaModel, Value_ movedValue, Entity_ entity, - int index) { - var variableDescriptor = - ((DefaultPlanningListVariableMetaModel) variableMetaModel).variableDescriptor(); - externalScoreDirector.beforeListVariableElementUnassigned(variableDescriptor, movedValue); - externalScoreDirector.beforeListVariableChanged(variableDescriptor, entity, index, index + 1); - variableDescriptor.getValue(entity).remove(index); - externalScoreDirector.afterListVariableChanged(variableDescriptor, entity, index, index); - externalScoreDirector.afterListVariableElementUnassigned(variableDescriptor, movedValue); - } - - public final void changeVariable(PlanningVariableMetaModel variableMetaModel, - Entity_ entity, @Nullable Value_ newValue) { - var variableDescriptor = extractVariableDescriptor(variableMetaModel); - externalScoreDirector.beforeVariableChanged(variableDescriptor, entity); - variableDescriptor.setValue(entity, newValue); - externalScoreDirector.afterVariableChanged(variableDescriptor, entity); - } - - @SuppressWarnings("unchecked") - public final @Nullable Value_ moveValueBetweenLists( - PlanningListVariableMetaModel variableMetaModel, Entity_ sourceEntity, int sourceIndex, - Entity_ destinationEntity, int destinationIndex) { - if (sourceEntity == destinationEntity) { - return moveValueInList(variableMetaModel, sourceEntity, sourceIndex, destinationIndex); - } - var variableDescriptor = extractVariableDescriptor(variableMetaModel); - - externalScoreDirector.beforeListVariableChanged(variableDescriptor, sourceEntity, sourceIndex, sourceIndex + 1); - var element = (Value_) variableDescriptor.removeElement(sourceEntity, sourceIndex); - externalScoreDirector.afterListVariableChanged(variableDescriptor, sourceEntity, sourceIndex, sourceIndex); - - externalScoreDirector.beforeListVariableChanged(variableDescriptor, destinationEntity, destinationIndex, - destinationIndex); - variableDescriptor.addElement(destinationEntity, destinationIndex, element); - externalScoreDirector.afterListVariableChanged(variableDescriptor, destinationEntity, destinationIndex, - destinationIndex + 1); - - return element; - } - - @SuppressWarnings("unchecked") - @Override - public final @Nullable Value_ moveValueInList( - PlanningListVariableMetaModel variableMetaModel, Entity_ sourceEntity, int sourceIndex, - int destinationIndex) { - if (sourceIndex == destinationIndex) { - return null; - } - var variableDescriptor = extractVariableDescriptor(variableMetaModel); - var fromIndex = Math.min(sourceIndex, destinationIndex); - var toIndex = Math.max(sourceIndex, destinationIndex) + 1; - - externalScoreDirector.beforeListVariableChanged(variableDescriptor, sourceEntity, fromIndex, toIndex); - Value_ element = (Value_) variableDescriptor.removeElement(sourceEntity, sourceIndex); - variableDescriptor.addElement(sourceEntity, destinationIndex, element); - externalScoreDirector.afterListVariableChanged(variableDescriptor, sourceEntity, fromIndex, toIndex); - - return element; - } - - @Override - public void swapValuesBetweenLists( - PlanningListVariableMetaModel variableMetaModel, Entity_ leftEntity, int leftIndex, - Entity_ rightEntity, int rightIndex) { - if (leftEntity == rightEntity) { - swapValuesInList(variableMetaModel, leftEntity, leftIndex, rightIndex); - } else { - var variableDescriptor = extractVariableDescriptor(variableMetaModel); - externalScoreDirector.beforeListVariableChanged(variableDescriptor, leftEntity, leftIndex, leftIndex + 1); - externalScoreDirector.beforeListVariableChanged(variableDescriptor, rightEntity, rightIndex, rightIndex + 1); - var oldLeftElement = variableDescriptor.setElement(leftEntity, leftIndex, - variableDescriptor.getElement(rightEntity, rightIndex)); - variableDescriptor.setElement(rightEntity, rightIndex, oldLeftElement); - externalScoreDirector.afterListVariableChanged(variableDescriptor, leftEntity, leftIndex, leftIndex + 1); - externalScoreDirector.afterListVariableChanged(variableDescriptor, rightEntity, rightIndex, rightIndex + 1); - } - } - - @Override - public void swapValuesInList(PlanningListVariableMetaModel variableMetaModel, - Entity_ entity, int leftIndex, int rightIndex) { - if (leftIndex == rightIndex) { - return; - } - - var variableDescriptor = extractVariableDescriptor(variableMetaModel); - var fromIndex = Math.min(leftIndex, rightIndex); - var toIndex = Math.max(leftIndex, rightIndex) + 1; - - externalScoreDirector.beforeListVariableChanged(variableDescriptor, entity, fromIndex, toIndex); - var oldLeftElement = - variableDescriptor.setElement(entity, leftIndex, variableDescriptor.getElement(entity, rightIndex)); - variableDescriptor.setElement(entity, rightIndex, oldLeftElement); - externalScoreDirector.afterListVariableChanged(variableDescriptor, entity, fromIndex, toIndex); - } - - @Override - public boolean isValueInRange(GenuineVariableMetaModel variableMetaModel, - @Nullable Entity_ entity, @Nullable Value_ value) { - var innerGenuineVariableMetaModel = (InnerGenuineVariableMetaModel) variableMetaModel; - var valueRangeDescriptor = innerGenuineVariableMetaModel.variableDescriptor() - .getValueRangeDescriptor(); - if (valueRangeDescriptor.canExtractValueRangeFromSolution()) { - return backingScoreDirector.getValueRangeManager() - .getFromSolution(valueRangeDescriptor) - .contains(value); - } else { - return backingScoreDirector.getValueRangeManager() - .getFromEntity(valueRangeDescriptor, Objects.requireNonNull(entity)) - .contains(value); - } + protected MoveDirector( + VariableDescriptorAwareScoreDirector externalScoreDirector, + InnerScoreDirector backingScoreDirector) { + super(externalScoreDirector); + this.backingScoreDirector = Objects.requireNonNull(backingScoreDirector); } /** @@ -233,78 +72,11 @@ public final Result_ executeTemporary(ai.timefold.solver.core.impl.heu return executeTemporary(MoveAdapters.toNewMove(move), postprocessor); } - @Override - public final Value_ getValue(PlanningVariableMetaModel variableMetaModel, - Entity_ entity) { - return extractVariableDescriptor(variableMetaModel).getValue(entity); - } - - @Override - public int countValues(PlanningListVariableMetaModel variableMetaModel, - Entity_ entity) { - return extractVariableDescriptor(variableMetaModel).getValue(entity).size(); - } - - @SuppressWarnings("unchecked") - @Override - public final Value_ getValueAtIndex( - PlanningListVariableMetaModel variableMetaModel, Entity_ entity, int index) { - return (Value_) extractVariableDescriptor(variableMetaModel).getValue(entity).get(index); - } - - @Override - public ElementPosition - getPositionOf(PlanningListVariableMetaModel variableMetaModel, Value_ value) { - return getPositionOf(backingScoreDirector, variableMetaModel, value); - } - - @Override - public boolean isPinned(PlanningVariableMetaModel variableMetaModel, - @Nullable Entity_ entity) { - return isPinned(extractVariableDescriptor(variableMetaModel).getEntityDescriptor(), entity); - } - - public boolean isPinned(EntityDescriptor entityDescriptor, @Nullable Value_ entity) { - if (entity == null) { - return false; // Null is never pinned. - } - return !entityDescriptor.isMovable(backingScoreDirector.getWorkingSolution(), entity); - } - - protected static ElementPosition getPositionOf(InnerScoreDirector scoreDirector, - PlanningListVariableMetaModel listVariableDescriptor, Value_ value) { - return scoreDirector.getListVariableStateSupply(extractVariableDescriptor(listVariableDescriptor)) - .getElementPosition(value); - } - - @Override - public boolean isPinned(PlanningListVariableMetaModel variableMetaModel, - @Nullable Value_ value) { - return isPinned(extractVariableDescriptor(variableMetaModel), value); - } - - public boolean isPinned(ListVariableDescriptor listVariableDescriptor, @Nullable Value_ value) { - if (value == null) { - return false; // Null is never pinned. - } - return backingScoreDirector.getListVariableStateSupply(listVariableDescriptor).isPinned(value); - } - @Override public final @Nullable T rebase(@Nullable T problemFactOrPlanningEntity) { return externalScoreDirector.lookUpWorkingObject(problemFactOrPlanningEntity); } - private static BasicVariableDescriptor - extractVariableDescriptor(PlanningVariableMetaModel variableMetaModel) { - return ((DefaultPlanningVariableMetaModel) variableMetaModel).variableDescriptor(); - } - - private static ListVariableDescriptor - extractVariableDescriptor(PlanningListVariableMetaModel variableMetaModel) { - return ((DefaultPlanningListVariableMetaModel) variableMetaModel).variableDescriptor(); - } - /** * Moves that are to be undone later need to be run with the instance returned by this method. * To undo the move, remember to call {@link EphemeralMoveDirector#close()}. diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/director/VariableChangeRecordingScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/move/director/VariableChangeRecordingScoreDirector.java index bd132cb403..11af73c9c1 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/director/VariableChangeRecordingScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/director/VariableChangeRecordingScoreDirector.java @@ -9,6 +9,7 @@ import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.director.ScoreDirector; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.VariableDescriptor; import ai.timefold.solver.core.impl.heuristic.move.AbstractMove; @@ -179,6 +180,12 @@ public ValueRangeManager getValueRangeManager() { return getBacking().getValueRangeManager(); } + @Override + public ListVariableStateSupply + getListVariableStateSupply(ListVariableDescriptor listVariableDescriptor) { + return backingScoreDirector.getListVariableStateSupply(listVariableDescriptor); + } + /** * Returns the score director to which events are delegated. */ diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/VariableDescriptorAwareScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/VariableDescriptorAwareScoreDirector.java index ce1401616d..7d5b472e59 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/VariableDescriptorAwareScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/VariableDescriptorAwareScoreDirector.java @@ -2,6 +2,7 @@ import ai.timefold.solver.core.api.score.director.ScoreDirector; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.VariableDescriptor; @@ -10,6 +11,9 @@ public interface VariableDescriptorAwareScoreDirector SolutionDescriptor getSolutionDescriptor(); + ListVariableStateSupply + getListVariableStateSupply(ListVariableDescriptor listVariableDescriptor); + // ************************************************************************ // Basic variable // ************************************************************************ diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicAssignmentAction.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicAssignmentAction.java deleted file mode 100644 index fe22cfb8f6..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicAssignmentAction.java +++ /dev/null @@ -1,65 +0,0 @@ -package ai.timefold.solver.core.preview.api.move.ruin; - -import java.util.stream.StreamSupport; - -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.preview.api.move.Rebaser; - -import org.jspecify.annotations.NonNull; - -public interface BasicAssignmentAction> { - void assignValues(BasicAssignmentEvaluator evaluator); - - default BasicAssignmentAction rebase(Rebaser rebaser) { - throw new UnsupportedOperationException("Rebase is not supported. Implement rebase to support multithreaded solving."); - } - - static > BasicAssignmentAction - unassign(Entity_ entity) { - return new BasicAssignmentAction<>() { - @Override - public void assignValues(BasicAssignmentEvaluator evaluator) { - evaluator.unassign(entity); - } - - @Override - public BasicAssignmentAction rebase(Rebaser rebaser) { - return BasicAssignmentAction.unassign(rebaser.rebase(entity)); - } - }; - } - - static > BasicAssignmentAction - bestFit(Entity_ entity, Iterable values) { - return new BasicAssignmentAction<>() { - @Override - public void assignValues(BasicAssignmentEvaluator evaluator) { - var iterator = values.iterator(); - if (!iterator.hasNext()) { - throw new IllegalArgumentException( - "Cannot perform a best-fit assignment on entity (%s) since the value iterable (%s) is empty." - .formatted(entity, values)); - } - var bestValue = iterator.next(); - evaluator.assign(entity, bestValue); - var bestScore = evaluator.score(); - while (iterator.hasNext()) { - var candidateValue = iterator.next(); - evaluator.assign(entity, candidateValue); - var score = evaluator.score(); - if (score.compareTo(bestScore) > 0) { - bestScore = score; - bestValue = candidateValue; - } - } - evaluator.assign(entity, bestValue); - } - - @Override - public BasicAssignmentAction rebase(Rebaser rebaser) { - return BasicAssignmentAction.bestFit(rebaser.rebase(entity), - StreamSupport.stream(values.spliterator(), false).map(rebaser::rebase).toList()); - } - }; - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicAssignmentEvaluator.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicAssignmentEvaluator.java deleted file mode 100644 index bdd09ea740..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicAssignmentEvaluator.java +++ /dev/null @@ -1,13 +0,0 @@ -package ai.timefold.solver.core.preview.api.move.ruin; - -import ai.timefold.solver.core.api.score.Score; - -import org.jspecify.annotations.NonNull; - -public interface BasicAssignmentEvaluator> { - void assign(Entity_ entity, Value_ value); - - void unassign(Entity_ entity); - - Score_ score(); -} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinAndRecreatePicker.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinAndRecreatePicker.java deleted file mode 100644 index a94cc962b3..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/BasicRuinAndRecreatePicker.java +++ /dev/null @@ -1,14 +0,0 @@ -package ai.timefold.solver.core.preview.api.move.ruin; - -import java.util.List; -import java.util.Random; - -import ai.timefold.solver.core.api.score.Score; - -import org.jspecify.annotations.NonNull; - -public interface BasicRuinAndRecreatePicker> { - void initialize(Solution_ solution); - - List> pickAssignments(Random random); -} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListAssignmentAction.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListAssignmentAction.java deleted file mode 100644 index c684a76c65..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListAssignmentAction.java +++ /dev/null @@ -1,88 +0,0 @@ -package ai.timefold.solver.core.preview.api.move.ruin; - -import java.util.stream.StreamSupport; - -import ai.timefold.solver.core.api.score.Score; -import ai.timefold.solver.core.preview.api.move.Rebaser; - -import org.jspecify.annotations.NonNull; - -public interface ListAssignmentAction> { - void assignValues(ListAssignmentEvaluator evaluator); - - default ListAssignmentAction rebase(Rebaser rebaser) { - throw new UnsupportedOperationException("Rebase is not supported. Implement rebase to support multithreaded solving."); - } - - static > ListAssignmentAction - unassign(Value_ value) { - return new ListAssignmentAction<>() { - @Override - public void assignValues(ListAssignmentEvaluator evaluator) { - evaluator.unassign(value); - } - - @Override - public ListAssignmentAction rebase(Rebaser rebaser) { - return unassign(rebaser.rebase(value)); - } - }; - } - - static > ListAssignmentAction - bestFit(Value_ value, Iterable entities) { - return new ListAssignmentAction<>() { - @Override - public void assignValues(ListAssignmentEvaluator evaluator) { - var iterator = entities.iterator(); - if (!iterator.hasNext()) { - throw new IllegalArgumentException( - "Cannot perform a best-fit assignment on value (%s) since the entities iterable (%s) is empty." - .formatted(value, entities)); - } - - // ensure value is unassigned initially to simplify list position calculations - // (so we can ignore the case that the value is already present in entity) - evaluator.unassign(value); - - var bestEntity = iterator.next(); - var bestIndex = 0; - - var listSize = evaluator.listSize(bestEntity); - evaluator.assign(bestEntity, 0, value); - var bestScore = evaluator.score(); - - for (var i = 1; i <= listSize; i++) { - evaluator.assign(bestEntity, i, value); - var score = evaluator.score(); - if (score.compareTo(bestScore) > 0) { - bestScore = score; - bestIndex = i; - } - } - - while (iterator.hasNext()) { - var candidateEntity = iterator.next(); - listSize = evaluator.listSize(candidateEntity); - for (var i = 0; i <= listSize; i++) { - evaluator.assign(candidateEntity, i, value); - var score = evaluator.score(); - if (score.compareTo(bestScore) > 0) { - bestScore = score; - bestEntity = candidateEntity; - bestIndex = i; - } - } - } - evaluator.assign(bestEntity, bestIndex, value); - } - - @Override - public ListAssignmentAction rebase(Rebaser rebaser) { - return bestFit(rebaser.rebase(value), - StreamSupport.stream(entities.spliterator(), false) - .map(rebaser::rebase).toList()); - } - }; - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListAssignmentEvaluator.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListAssignmentEvaluator.java deleted file mode 100644 index 41f6a1ae1a..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListAssignmentEvaluator.java +++ /dev/null @@ -1,15 +0,0 @@ -package ai.timefold.solver.core.preview.api.move.ruin; - -import ai.timefold.solver.core.api.score.Score; - -import org.jspecify.annotations.NonNull; - -public interface ListAssignmentEvaluator> { - void assign(Entity_ entity, int index, Value_ value); - - void unassign(Value_ value); - - int listSize(Entity_ entity); - - Score_ score(); -} diff --git a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinAndRecreatePicker.java b/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinAndRecreatePicker.java deleted file mode 100644 index 08c74a6252..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/preview/api/move/ruin/ListRuinAndRecreatePicker.java +++ /dev/null @@ -1,14 +0,0 @@ -package ai.timefold.solver.core.preview.api.move.ruin; - -import java.util.List; -import java.util.Random; - -import ai.timefold.solver.core.api.score.Score; - -import org.jspecify.annotations.NonNull; - -public interface ListRuinAndRecreatePicker> { - void initialize(Solution_ solution); - - List> pickAssignments(Random random); -} From e615e4bc53bd81f6886b5c3ee388c4a352bfcdc0 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Tue, 9 Dec 2025 12:59:44 -0500 Subject: [PATCH 4/9] chore: remove uneccessary changes --- .../director/DefaultMutableSolutionView.java | 258 ------------------ .../move/director/EphemeralMoveDirector.java | 2 +- .../core/impl/move/director/MoveDirector.java | 242 +++++++++++++++- .../VariableChangeRecordingScoreDirector.java | 7 - .../VariableDescriptorAwareScoreDirector.java | 4 - 5 files changed, 236 insertions(+), 277 deletions(-) delete mode 100644 core/src/main/java/ai/timefold/solver/core/impl/move/director/DefaultMutableSolutionView.java diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/director/DefaultMutableSolutionView.java b/core/src/main/java/ai/timefold/solver/core/impl/move/director/DefaultMutableSolutionView.java deleted file mode 100644 index f7f5cef146..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/director/DefaultMutableSolutionView.java +++ /dev/null @@ -1,258 +0,0 @@ -package ai.timefold.solver.core.impl.move.director; - -import java.util.Objects; - -import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; -import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultPlanningListVariableMetaModel; -import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultPlanningVariableMetaModel; -import ai.timefold.solver.core.impl.domain.solution.descriptor.InnerGenuineVariableMetaModel; -import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply; -import ai.timefold.solver.core.impl.domain.variable.descriptor.BasicVariableDescriptor; -import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; -import ai.timefold.solver.core.impl.move.InnerMutableSolutionView; -import ai.timefold.solver.core.impl.score.director.VariableDescriptorAwareScoreDirector; -import ai.timefold.solver.core.preview.api.domain.metamodel.ElementPosition; -import ai.timefold.solver.core.preview.api.domain.metamodel.GenuineVariableMetaModel; -import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningListVariableMetaModel; -import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningVariableMetaModel; - -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - -@NullMarked -public sealed class DefaultMutableSolutionView implements InnerMutableSolutionView - permits MoveDirector { - protected final VariableDescriptorAwareScoreDirector externalScoreDirector; - - public DefaultMutableSolutionView(VariableDescriptorAwareScoreDirector externalScoreDirector) { - this.externalScoreDirector = externalScoreDirector; - } - - @Override - public final void assignValue(PlanningListVariableMetaModel variableMetaModel, - Value_ planningValue, Entity_ destinationEntity, int destinationIndex) { - var variableDescriptor = - ((DefaultPlanningListVariableMetaModel) variableMetaModel).variableDescriptor(); - externalScoreDirector.beforeListVariableElementAssigned(variableDescriptor, planningValue); - externalScoreDirector.beforeListVariableChanged(variableDescriptor, destinationEntity, destinationIndex, - destinationIndex); - variableDescriptor.addElement(destinationEntity, destinationIndex, planningValue); - externalScoreDirector.afterListVariableChanged(variableDescriptor, destinationEntity, destinationIndex, - destinationIndex + 1); - externalScoreDirector.afterListVariableElementAssigned(variableDescriptor, planningValue); - } - - @Override - public void unassignValue(PlanningListVariableMetaModel variableMetaModel, - Value_ value) { - var locationInList = getPositionOf(variableMetaModel, value) - .ensureAssigned(() -> """ - The value (%s) is not assigned to a list variable. - This may indicate score corruption or a problem with the move's implementation.""" - .formatted(value)); - unassignValue(variableMetaModel, value, locationInList.entity(), locationInList.index()); - } - - @Override - public Value_ unassignValue(PlanningListVariableMetaModel variableMetaModel, - Entity_ entity, int index) { - var value = getValueAtIndex(variableMetaModel, entity, index); - unassignValue(variableMetaModel, value, entity, index); - return value; - } - - private void unassignValue( - PlanningListVariableMetaModel variableMetaModel, Value_ movedValue, Entity_ entity, - int index) { - var variableDescriptor = - ((DefaultPlanningListVariableMetaModel) variableMetaModel).variableDescriptor(); - externalScoreDirector.beforeListVariableElementUnassigned(variableDescriptor, movedValue); - externalScoreDirector.beforeListVariableChanged(variableDescriptor, entity, index, index + 1); - variableDescriptor.getValue(entity).remove(index); - externalScoreDirector.afterListVariableChanged(variableDescriptor, entity, index, index); - externalScoreDirector.afterListVariableElementUnassigned(variableDescriptor, movedValue); - } - - public final void changeVariable(PlanningVariableMetaModel variableMetaModel, - Entity_ entity, @Nullable Value_ newValue) { - var variableDescriptor = extractVariableDescriptor(variableMetaModel); - externalScoreDirector.beforeVariableChanged(variableDescriptor, entity); - variableDescriptor.setValue(entity, newValue); - externalScoreDirector.afterVariableChanged(variableDescriptor, entity); - } - - @SuppressWarnings("unchecked") - public final @Nullable Value_ moveValueBetweenLists( - PlanningListVariableMetaModel variableMetaModel, Entity_ sourceEntity, int sourceIndex, - Entity_ destinationEntity, int destinationIndex) { - if (sourceEntity == destinationEntity) { - return moveValueInList(variableMetaModel, sourceEntity, sourceIndex, destinationIndex); - } - var variableDescriptor = extractVariableDescriptor(variableMetaModel); - - externalScoreDirector.beforeListVariableChanged(variableDescriptor, sourceEntity, sourceIndex, sourceIndex + 1); - var element = (Value_) variableDescriptor.removeElement(sourceEntity, sourceIndex); - externalScoreDirector.afterListVariableChanged(variableDescriptor, sourceEntity, sourceIndex, sourceIndex); - - externalScoreDirector.beforeListVariableChanged(variableDescriptor, destinationEntity, destinationIndex, - destinationIndex); - variableDescriptor.addElement(destinationEntity, destinationIndex, element); - externalScoreDirector.afterListVariableChanged(variableDescriptor, destinationEntity, destinationIndex, - destinationIndex + 1); - - return element; - } - - @SuppressWarnings("unchecked") - @Override - public final @Nullable Value_ moveValueInList( - PlanningListVariableMetaModel variableMetaModel, Entity_ sourceEntity, int sourceIndex, - int destinationIndex) { - if (sourceIndex == destinationIndex) { - return null; - } - var variableDescriptor = extractVariableDescriptor(variableMetaModel); - var fromIndex = Math.min(sourceIndex, destinationIndex); - var toIndex = Math.max(sourceIndex, destinationIndex) + 1; - - externalScoreDirector.beforeListVariableChanged(variableDescriptor, sourceEntity, fromIndex, toIndex); - Value_ element = (Value_) variableDescriptor.removeElement(sourceEntity, sourceIndex); - variableDescriptor.addElement(sourceEntity, destinationIndex, element); - externalScoreDirector.afterListVariableChanged(variableDescriptor, sourceEntity, fromIndex, toIndex); - - return element; - } - - @Override - public void swapValuesBetweenLists( - PlanningListVariableMetaModel variableMetaModel, Entity_ leftEntity, int leftIndex, - Entity_ rightEntity, int rightIndex) { - if (leftEntity == rightEntity) { - swapValuesInList(variableMetaModel, leftEntity, leftIndex, rightIndex); - } else { - var variableDescriptor = extractVariableDescriptor(variableMetaModel); - externalScoreDirector.beforeListVariableChanged(variableDescriptor, leftEntity, leftIndex, leftIndex + 1); - externalScoreDirector.beforeListVariableChanged(variableDescriptor, rightEntity, rightIndex, rightIndex + 1); - var oldLeftElement = variableDescriptor.setElement(leftEntity, leftIndex, - variableDescriptor.getElement(rightEntity, rightIndex)); - variableDescriptor.setElement(rightEntity, rightIndex, oldLeftElement); - externalScoreDirector.afterListVariableChanged(variableDescriptor, leftEntity, leftIndex, leftIndex + 1); - externalScoreDirector.afterListVariableChanged(variableDescriptor, rightEntity, rightIndex, rightIndex + 1); - } - } - - @Override - public void swapValuesInList(PlanningListVariableMetaModel variableMetaModel, - Entity_ entity, int leftIndex, int rightIndex) { - if (leftIndex == rightIndex) { - return; - } - - var variableDescriptor = extractVariableDescriptor(variableMetaModel); - var fromIndex = Math.min(leftIndex, rightIndex); - var toIndex = Math.max(leftIndex, rightIndex) + 1; - - externalScoreDirector.beforeListVariableChanged(variableDescriptor, entity, fromIndex, toIndex); - var oldLeftElement = - variableDescriptor.setElement(entity, leftIndex, variableDescriptor.getElement(entity, rightIndex)); - variableDescriptor.setElement(entity, rightIndex, oldLeftElement); - externalScoreDirector.afterListVariableChanged(variableDescriptor, entity, fromIndex, toIndex); - } - - @Override - public boolean isValueInRange(GenuineVariableMetaModel variableMetaModel, - @Nullable Entity_ entity, @Nullable Value_ value) { - var innerGenuineVariableMetaModel = (InnerGenuineVariableMetaModel) variableMetaModel; - var valueRangeDescriptor = innerGenuineVariableMetaModel.variableDescriptor() - .getValueRangeDescriptor(); - if (valueRangeDescriptor.canExtractValueRangeFromSolution()) { - return externalScoreDirector.getValueRangeManager() - .getFromSolution(valueRangeDescriptor) - .contains(value); - } else { - return externalScoreDirector.getValueRangeManager() - .getFromEntity(valueRangeDescriptor, Objects.requireNonNull(entity)) - .contains(value); - } - } - - @Override - public final Value_ getValue(PlanningVariableMetaModel variableMetaModel, - Entity_ entity) { - return extractVariableDescriptor(variableMetaModel).getValue(entity); - } - - @Override - public int countValues(PlanningListVariableMetaModel variableMetaModel, - Entity_ entity) { - return extractVariableDescriptor(variableMetaModel).getValue(entity).size(); - } - - @SuppressWarnings("unchecked") - @Override - public final Value_ getValueAtIndex( - PlanningListVariableMetaModel variableMetaModel, Entity_ entity, int index) { - return (Value_) extractVariableDescriptor(variableMetaModel).getValue(entity).get(index); - } - - @Override - public ElementPosition - getPositionOf(PlanningListVariableMetaModel variableMetaModel, Value_ value) { - return getPositionOf(externalScoreDirector, variableMetaModel, value); - } - - @Override - public boolean isPinned(PlanningVariableMetaModel variableMetaModel, - @Nullable Entity_ entity) { - return isPinned(extractVariableDescriptor(variableMetaModel).getEntityDescriptor(), entity); - } - - public boolean isPinned(EntityDescriptor entityDescriptor, @Nullable Value_ entity) { - if (entity == null) { - return false; // Null is never pinned. - } - return !entityDescriptor.isMovable(externalScoreDirector.getWorkingSolution(), entity); - } - - protected static ElementPosition getPositionOf( - VariableDescriptorAwareScoreDirector scoreDirector, - PlanningListVariableMetaModel listVariableMetaModel, Value_ value) { - var listVariableDescriptor = extractVariableDescriptor(listVariableMetaModel); - return scoreDirector.getListVariableStateSupply(listVariableDescriptor).getElementPosition(value); - } - - @Override - public boolean isPinned(PlanningListVariableMetaModel variableMetaModel, - @Nullable Value_ value) { - return isPinned(extractVariableDescriptor(variableMetaModel), value); - } - - public boolean isPinned(ListVariableDescriptor listVariableDescriptor, @Nullable Value_ value) { - if (value == null) { - return false; // Null is never pinned. - } - return getListVariableStateSupply(listVariableDescriptor).isPinned(value); - } - - @SuppressWarnings("unchecked") - public final ListVariableStateSupply - getListVariableStateSupply(ListVariableDescriptor listVariableDescriptor) { - return (ListVariableStateSupply) externalScoreDirector - .getListVariableStateSupply(listVariableDescriptor); - } - - private static BasicVariableDescriptor - extractVariableDescriptor(PlanningVariableMetaModel variableMetaModel) { - return ((DefaultPlanningVariableMetaModel) variableMetaModel).variableDescriptor(); - } - - private static ListVariableDescriptor - extractVariableDescriptor(PlanningListVariableMetaModel variableMetaModel) { - return ((DefaultPlanningListVariableMetaModel) variableMetaModel).variableDescriptor(); - } - - @Override - public VariableDescriptorAwareScoreDirector getScoreDirector() { - return externalScoreDirector; - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/director/EphemeralMoveDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/move/director/EphemeralMoveDirector.java index 351f622c51..96b2873018 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/director/EphemeralMoveDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/director/EphemeralMoveDirector.java @@ -24,7 +24,7 @@ final class EphemeralMoveDirector> extends MoveDirector { EphemeralMoveDirector(InnerScoreDirector scoreDirector) { - super(new VariableChangeRecordingScoreDirector<>(scoreDirector), scoreDirector); + super(scoreDirector); } Move createUndoMove() { 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 705d739fd6..a5b0aee668 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 @@ -4,11 +4,21 @@ import java.util.function.BiFunction; import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; +import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultPlanningListVariableMetaModel; +import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultPlanningVariableMetaModel; +import ai.timefold.solver.core.impl.domain.solution.descriptor.InnerGenuineVariableMetaModel; +import ai.timefold.solver.core.impl.domain.variable.descriptor.BasicVariableDescriptor; +import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.heuristic.move.MoveAdapters; import ai.timefold.solver.core.impl.move.InnerMutableSolutionView; import ai.timefold.solver.core.impl.score.director.InnerScore; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.impl.score.director.VariableDescriptorAwareScoreDirector; +import ai.timefold.solver.core.preview.api.domain.metamodel.ElementPosition; +import ai.timefold.solver.core.preview.api.domain.metamodel.GenuineVariableMetaModel; +import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningListVariableMetaModel; +import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningVariableMetaModel; import ai.timefold.solver.core.preview.api.move.Move; import ai.timefold.solver.core.preview.api.move.Rebaser; @@ -17,22 +27,173 @@ @NullMarked public sealed class MoveDirector> - extends DefaultMutableSolutionView implements InnerMutableSolutionView, Rebaser permits EphemeralMoveDirector { + protected final VariableDescriptorAwareScoreDirector externalScoreDirector; private final InnerScoreDirector backingScoreDirector; public MoveDirector(InnerScoreDirector scoreDirector) { - super(scoreDirector); this.backingScoreDirector = Objects.requireNonNull(scoreDirector); + if (EphemeralMoveDirector.class.isAssignableFrom(getClass())) { + // Ephemeral move director records operations for a later undo, + // and the external director is no longer an instance of InnerScoreDirector. + // However, some pieces of code need methods from InnerScoreDirector, + // in which case we turn to the backing score director. + // This is only safe for operations that do not need to be undone, such as calculateScore(). + // Operations which need undo must go through the external score director, which is recording in this case. + this.externalScoreDirector = new VariableChangeRecordingScoreDirector<>(scoreDirector, false); + } else { + this.externalScoreDirector = scoreDirector; + } } - protected MoveDirector( - VariableDescriptorAwareScoreDirector externalScoreDirector, - InnerScoreDirector backingScoreDirector) { - super(externalScoreDirector); - this.backingScoreDirector = Objects.requireNonNull(backingScoreDirector); + @Override + public final void assignValue(PlanningListVariableMetaModel variableMetaModel, + Value_ planningValue, Entity_ destinationEntity, int destinationIndex) { + var variableDescriptor = + ((DefaultPlanningListVariableMetaModel) variableMetaModel).variableDescriptor(); + externalScoreDirector.beforeListVariableElementAssigned(variableDescriptor, planningValue); + externalScoreDirector.beforeListVariableChanged(variableDescriptor, destinationEntity, destinationIndex, + destinationIndex); + variableDescriptor.addElement(destinationEntity, destinationIndex, planningValue); + externalScoreDirector.afterListVariableChanged(variableDescriptor, destinationEntity, destinationIndex, + destinationIndex + 1); + externalScoreDirector.afterListVariableElementAssigned(variableDescriptor, planningValue); + } + + @Override + public void unassignValue(PlanningListVariableMetaModel variableMetaModel, + Value_ value) { + var locationInList = getPositionOf(variableMetaModel, value) + .ensureAssigned(() -> """ + The value (%s) is not assigned to a list variable. + This may indicate score corruption or a problem with the move's implementation.""" + .formatted(value)); + unassignValue(variableMetaModel, value, locationInList.entity(), locationInList.index()); + } + + @Override + public Value_ unassignValue(PlanningListVariableMetaModel variableMetaModel, + Entity_ entity, int index) { + var value = getValueAtIndex(variableMetaModel, entity, index); + unassignValue(variableMetaModel, value, entity, index); + return value; + } + + private void unassignValue( + PlanningListVariableMetaModel variableMetaModel, Value_ movedValue, Entity_ entity, + int index) { + var variableDescriptor = + ((DefaultPlanningListVariableMetaModel) variableMetaModel).variableDescriptor(); + externalScoreDirector.beforeListVariableElementUnassigned(variableDescriptor, movedValue); + externalScoreDirector.beforeListVariableChanged(variableDescriptor, entity, index, index + 1); + variableDescriptor.getValue(entity).remove(index); + externalScoreDirector.afterListVariableChanged(variableDescriptor, entity, index, index); + externalScoreDirector.afterListVariableElementUnassigned(variableDescriptor, movedValue); + } + + public final void changeVariable(PlanningVariableMetaModel variableMetaModel, + Entity_ entity, @Nullable Value_ newValue) { + var variableDescriptor = extractVariableDescriptor(variableMetaModel); + externalScoreDirector.beforeVariableChanged(variableDescriptor, entity); + variableDescriptor.setValue(entity, newValue); + externalScoreDirector.afterVariableChanged(variableDescriptor, entity); + } + + @SuppressWarnings("unchecked") + public final @Nullable Value_ moveValueBetweenLists( + PlanningListVariableMetaModel variableMetaModel, Entity_ sourceEntity, int sourceIndex, + Entity_ destinationEntity, int destinationIndex) { + if (sourceEntity == destinationEntity) { + return moveValueInList(variableMetaModel, sourceEntity, sourceIndex, destinationIndex); + } + var variableDescriptor = extractVariableDescriptor(variableMetaModel); + + externalScoreDirector.beforeListVariableChanged(variableDescriptor, sourceEntity, sourceIndex, sourceIndex + 1); + var element = (Value_) variableDescriptor.removeElement(sourceEntity, sourceIndex); + externalScoreDirector.afterListVariableChanged(variableDescriptor, sourceEntity, sourceIndex, sourceIndex); + + externalScoreDirector.beforeListVariableChanged(variableDescriptor, destinationEntity, destinationIndex, + destinationIndex); + variableDescriptor.addElement(destinationEntity, destinationIndex, element); + externalScoreDirector.afterListVariableChanged(variableDescriptor, destinationEntity, destinationIndex, + destinationIndex + 1); + + return element; + } + + @SuppressWarnings("unchecked") + @Override + public final @Nullable Value_ moveValueInList( + PlanningListVariableMetaModel variableMetaModel, Entity_ sourceEntity, int sourceIndex, + int destinationIndex) { + if (sourceIndex == destinationIndex) { + return null; + } + var variableDescriptor = extractVariableDescriptor(variableMetaModel); + var fromIndex = Math.min(sourceIndex, destinationIndex); + var toIndex = Math.max(sourceIndex, destinationIndex) + 1; + + externalScoreDirector.beforeListVariableChanged(variableDescriptor, sourceEntity, fromIndex, toIndex); + Value_ element = (Value_) variableDescriptor.removeElement(sourceEntity, sourceIndex); + variableDescriptor.addElement(sourceEntity, destinationIndex, element); + externalScoreDirector.afterListVariableChanged(variableDescriptor, sourceEntity, fromIndex, toIndex); + + return element; + } + + @Override + public void swapValuesBetweenLists( + PlanningListVariableMetaModel variableMetaModel, Entity_ leftEntity, int leftIndex, + Entity_ rightEntity, int rightIndex) { + if (leftEntity == rightEntity) { + swapValuesInList(variableMetaModel, leftEntity, leftIndex, rightIndex); + } else { + var variableDescriptor = extractVariableDescriptor(variableMetaModel); + externalScoreDirector.beforeListVariableChanged(variableDescriptor, leftEntity, leftIndex, leftIndex + 1); + externalScoreDirector.beforeListVariableChanged(variableDescriptor, rightEntity, rightIndex, rightIndex + 1); + var oldLeftElement = variableDescriptor.setElement(leftEntity, leftIndex, + variableDescriptor.getElement(rightEntity, rightIndex)); + variableDescriptor.setElement(rightEntity, rightIndex, oldLeftElement); + externalScoreDirector.afterListVariableChanged(variableDescriptor, leftEntity, leftIndex, leftIndex + 1); + externalScoreDirector.afterListVariableChanged(variableDescriptor, rightEntity, rightIndex, rightIndex + 1); + } + } + + @Override + public void swapValuesInList(PlanningListVariableMetaModel variableMetaModel, + Entity_ entity, int leftIndex, int rightIndex) { + if (leftIndex == rightIndex) { + return; + } + + var variableDescriptor = extractVariableDescriptor(variableMetaModel); + var fromIndex = Math.min(leftIndex, rightIndex); + var toIndex = Math.max(leftIndex, rightIndex) + 1; + + externalScoreDirector.beforeListVariableChanged(variableDescriptor, entity, fromIndex, toIndex); + var oldLeftElement = + variableDescriptor.setElement(entity, leftIndex, variableDescriptor.getElement(entity, rightIndex)); + variableDescriptor.setElement(entity, rightIndex, oldLeftElement); + externalScoreDirector.afterListVariableChanged(variableDescriptor, entity, fromIndex, toIndex); + } + + @Override + public boolean isValueInRange(GenuineVariableMetaModel variableMetaModel, + @Nullable Entity_ entity, @Nullable Value_ value) { + var innerGenuineVariableMetaModel = (InnerGenuineVariableMetaModel) variableMetaModel; + var valueRangeDescriptor = innerGenuineVariableMetaModel.variableDescriptor() + .getValueRangeDescriptor(); + if (valueRangeDescriptor.canExtractValueRangeFromSolution()) { + return backingScoreDirector.getValueRangeManager() + .getFromSolution(valueRangeDescriptor) + .contains(value); + } else { + return backingScoreDirector.getValueRangeManager() + .getFromEntity(valueRangeDescriptor, Objects.requireNonNull(entity)) + .contains(value); + } } /** @@ -72,11 +233,78 @@ public final Result_ executeTemporary(ai.timefold.solver.core.impl.heu return executeTemporary(MoveAdapters.toNewMove(move), postprocessor); } + @Override + public final Value_ getValue(PlanningVariableMetaModel variableMetaModel, + Entity_ entity) { + return extractVariableDescriptor(variableMetaModel).getValue(entity); + } + + @Override + public int countValues(PlanningListVariableMetaModel variableMetaModel, + Entity_ entity) { + return extractVariableDescriptor(variableMetaModel).getValue(entity).size(); + } + + @SuppressWarnings("unchecked") + @Override + public final Value_ getValueAtIndex( + PlanningListVariableMetaModel variableMetaModel, Entity_ entity, int index) { + return (Value_) extractVariableDescriptor(variableMetaModel).getValue(entity).get(index); + } + + @Override + public ElementPosition + getPositionOf(PlanningListVariableMetaModel variableMetaModel, Value_ value) { + return getPositionOf(backingScoreDirector, variableMetaModel, value); + } + + @Override + public boolean isPinned(PlanningVariableMetaModel variableMetaModel, + @Nullable Entity_ entity) { + return isPinned(extractVariableDescriptor(variableMetaModel).getEntityDescriptor(), entity); + } + + public boolean isPinned(EntityDescriptor entityDescriptor, @Nullable Value_ entity) { + if (entity == null) { + return false; // Null is never pinned. + } + return !entityDescriptor.isMovable(backingScoreDirector.getWorkingSolution(), entity); + } + + protected static ElementPosition getPositionOf(InnerScoreDirector scoreDirector, + PlanningListVariableMetaModel listVariableDescriptor, Value_ value) { + return scoreDirector.getListVariableStateSupply(extractVariableDescriptor(listVariableDescriptor)) + .getElementPosition(value); + } + + @Override + public boolean isPinned(PlanningListVariableMetaModel variableMetaModel, + @Nullable Value_ value) { + return isPinned(extractVariableDescriptor(variableMetaModel), value); + } + + public boolean isPinned(ListVariableDescriptor listVariableDescriptor, @Nullable Value_ value) { + if (value == null) { + return false; // Null is never pinned. + } + return backingScoreDirector.getListVariableStateSupply(listVariableDescriptor).isPinned(value); + } + @Override public final @Nullable T rebase(@Nullable T problemFactOrPlanningEntity) { return externalScoreDirector.lookUpWorkingObject(problemFactOrPlanningEntity); } + private static BasicVariableDescriptor + extractVariableDescriptor(PlanningVariableMetaModel variableMetaModel) { + return ((DefaultPlanningVariableMetaModel) variableMetaModel).variableDescriptor(); + } + + private static ListVariableDescriptor + extractVariableDescriptor(PlanningListVariableMetaModel variableMetaModel) { + return ((DefaultPlanningListVariableMetaModel) variableMetaModel).variableDescriptor(); + } + /** * Moves that are to be undone later need to be run with the instance returned by this method. * To undo the move, remember to call {@link EphemeralMoveDirector#close()}. diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/director/VariableChangeRecordingScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/move/director/VariableChangeRecordingScoreDirector.java index 11af73c9c1..bd132cb403 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/director/VariableChangeRecordingScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/director/VariableChangeRecordingScoreDirector.java @@ -9,7 +9,6 @@ import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.director.ScoreDirector; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; -import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.VariableDescriptor; import ai.timefold.solver.core.impl.heuristic.move.AbstractMove; @@ -180,12 +179,6 @@ public ValueRangeManager getValueRangeManager() { return getBacking().getValueRangeManager(); } - @Override - public ListVariableStateSupply - getListVariableStateSupply(ListVariableDescriptor listVariableDescriptor) { - return backingScoreDirector.getListVariableStateSupply(listVariableDescriptor); - } - /** * Returns the score director to which events are delegated. */ diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/VariableDescriptorAwareScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/VariableDescriptorAwareScoreDirector.java index 7d5b472e59..ce1401616d 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/VariableDescriptorAwareScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/VariableDescriptorAwareScoreDirector.java @@ -2,7 +2,6 @@ import ai.timefold.solver.core.api.score.director.ScoreDirector; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; -import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.VariableDescriptor; @@ -11,9 +10,6 @@ public interface VariableDescriptorAwareScoreDirector SolutionDescriptor getSolutionDescriptor(); - ListVariableStateSupply - getListVariableStateSupply(ListVariableDescriptor listVariableDescriptor); - // ************************************************************************ // Basic variable // ************************************************************************ From 1998f11082cc18bc202a39d5b31b7b8dd693b575 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Thu, 11 Dec 2025 16:12:21 -0500 Subject: [PATCH 5/9] chore: review comments --- benchmark/src/main/resources/benchmark.xsd | 29 ++--- .../CartesianProductMoveSelectorConfig.java | 12 +- .../composite/UnionMoveSelectorConfig.java | 12 +- ...java => MultiStageMoveSelectorConfig.java} | 44 +++---- ...cedListRuinRecreateMoveSelectorConfig.java | 119 ------------------ .../ListMultiStageMoveSelectorConfig.java | 67 ++++++++++ .../localsearch/LocalSearchPhaseConfig.java | 12 +- .../TimefoldSolverEnterpriseService.java | 16 +-- .../selector/move/MoveSelectorFactory.java | 12 +- .../core/impl/move/director/MoveDirector.java | 3 +- .../neighborhood/maybeapi/move/Moves.java | 10 +- .../score/director/AbstractScoreDirector.java | 5 +- .../score/director/InnerScoreDirector.java | 2 +- .../core/preview/api/move/SolutionView.java | 3 +- .../testdomain/list/TestdataListUtils.java | 3 +- 15 files changed, 147 insertions(+), 202 deletions(-) rename core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/{AdvancedRuinRecreateMoveSelectorConfig.java => MultiStageMoveSelectorConfig.java} (53%) delete mode 100644 core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/AdvancedListRuinRecreateMoveSelectorConfig.java create mode 100644 core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/ListMultiStageMoveSelectorConfig.java diff --git a/benchmark/src/main/resources/benchmark.xsd b/benchmark/src/main/resources/benchmark.xsd index 71c5ca3bb4..4f3c3bc1b4 100644 --- a/benchmark/src/main/resources/benchmark.xsd +++ b/benchmark/src/main/resources/benchmark.xsd @@ -1178,10 +1178,10 @@ - + - + @@ -2033,10 +2033,10 @@ - + - + @@ -2078,7 +2078,7 @@ - + @@ -2090,7 +2090,7 @@ - + @@ -2111,7 +2111,7 @@ - + @@ -2123,16 +2123,7 @@ - - - - - - - - - - + @@ -2462,10 +2453,10 @@ - + - + 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 69a4482d57..e145d18c5e 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 @@ -11,8 +11,8 @@ import ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig; 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.AdvancedRuinRecreateMoveSelectorConfig; 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,8 +20,8 @@ import ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.SubChainChangeMoveSelectorConfig; 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.AdvancedListRuinRecreateMoveSelectorConfig; 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; @@ -56,10 +56,10 @@ public class CartesianProductMoveSelectorConfig extends MoveSelectorConfig { - public static final String XML_ELEMENT_NAME = "advancedRuinRecreateMoveSelector"; +public class MultiStageMoveSelectorConfig extends MoveSelectorConfig { + public static final String XML_ELEMENT_NAME = "multiStageMoveSelector"; - protected Class ruinAndRecreatePickerClass; + protected Class stageProviderClass; protected Class entityClass = null; protected String variableName = null; @@ -26,18 +26,18 @@ public class AdvancedRuinRecreateMoveSelectorConfig extends MoveSelectorConfig getRuinAndRecreatePickerClass() { - return ruinAndRecreatePickerClass; + public Class getStageProviderClass() { + return stageProviderClass; } - public void setRuinAndRecreatePickerClass( - Class ruinAndRecreatePickerClass) { - this.ruinAndRecreatePickerClass = ruinAndRecreatePickerClass; + public void setStageProviderClass( + Class stageProviderClass) { + this.stageProviderClass = stageProviderClass; } - public @NonNull AdvancedRuinRecreateMoveSelectorConfig withRuinAndRecreatePickerClass( - @NonNull Class ruinAndRecreatePickerClass) { - this.setRuinAndRecreatePickerClass(ruinAndRecreatePickerClass); + public @NonNull MultiStageMoveSelectorConfig withStageProviderClass( + @NonNull Class stageProviderClass) { + this.setStageProviderClass(stageProviderClass); return this; } @@ -49,7 +49,7 @@ public void setEntityClass(Class entityClass) { this.entityClass = entityClass; } - public @NonNull AdvancedRuinRecreateMoveSelectorConfig withEntityClass(@NonNull Class entityClass) { + public @NonNull MultiStageMoveSelectorConfig withEntityClass(@NonNull Class entityClass) { this.setEntityClass(entityClass); return this; } @@ -62,7 +62,7 @@ public void setVariableName(String variableName) { this.variableName = variableName; } - public @NonNull AdvancedRuinRecreateMoveSelectorConfig withVariableName(@NonNull String variableName) { + public @NonNull MultiStageMoveSelectorConfig withVariableName(@NonNull String variableName) { this.setVariableName(variableName); return this; } @@ -77,22 +77,22 @@ public boolean hasNearbySelectionConfig() { } @Override - public @NonNull AdvancedRuinRecreateMoveSelectorConfig copyConfig() { - return new AdvancedRuinRecreateMoveSelectorConfig().inherit(this); + public @NonNull MultiStageMoveSelectorConfig copyConfig() { + return new MultiStageMoveSelectorConfig().inherit(this); } @Override public void visitReferencedClasses(@NonNull Consumer> classVisitor) { - classVisitor.accept(ruinAndRecreatePickerClass); + classVisitor.accept(stageProviderClass); } @Override - public @NonNull AdvancedRuinRecreateMoveSelectorConfig - inherit(@NonNull AdvancedRuinRecreateMoveSelectorConfig inheritedConfig) { + public @NonNull MultiStageMoveSelectorConfig + inherit(@NonNull MultiStageMoveSelectorConfig inheritedConfig) { super.inherit(inheritedConfig); - ruinAndRecreatePickerClass = - ConfigUtils.inheritOverwritableProperty(ruinAndRecreatePickerClass, - inheritedConfig.getRuinAndRecreatePickerClass()); + stageProviderClass = + ConfigUtils.inheritOverwritableProperty(stageProviderClass, + inheritedConfig.getStageProviderClass()); entityClass = ConfigUtils.inheritOverwritableProperty(entityClass, inheritedConfig.getEntityClass()); variableName = diff --git a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/AdvancedListRuinRecreateMoveSelectorConfig.java b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/AdvancedListRuinRecreateMoveSelectorConfig.java deleted file mode 100644 index 44260fa052..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/AdvancedListRuinRecreateMoveSelectorConfig.java +++ /dev/null @@ -1,119 +0,0 @@ -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 = { - "ruinAndRecreatePickerClass", - "entityClass", - "valueClass", - "variableName" -}) -public class AdvancedListRuinRecreateMoveSelectorConfig extends MoveSelectorConfig { - public static final String XML_ELEMENT_NAME = "advancedListRuinRecreateMoveSelector"; - - protected Class ruinAndRecreatePickerClass; - - protected Class entityClass = null; - protected Class valueClass = null; - protected String variableName = null; - - // ************************** - // Getters/Setters - // ************************** - - public Class getRuinAndRecreatePickerClass() { - return ruinAndRecreatePickerClass; - } - - public void setRuinAndRecreatePickerClass( - Class ruinAndRecreatePickerClass) { - this.ruinAndRecreatePickerClass = ruinAndRecreatePickerClass; - } - - public @NonNull AdvancedListRuinRecreateMoveSelectorConfig withRuinAndRecreatePickerClass( - @NonNull Class ruinAndRecreatePickerClass) { - this.setRuinAndRecreatePickerClass(ruinAndRecreatePickerClass); - return this; - } - - public Class getEntityClass() { - return entityClass; - } - - public void setEntityClass(Class entityClass) { - this.entityClass = entityClass; - } - - public @NonNull AdvancedListRuinRecreateMoveSelectorConfig withEntityClass(@NonNull Class entityClass) { - this.setEntityClass(entityClass); - return this; - } - - public Class getValueClass() { - return valueClass; - } - - public void setValueClass(Class valueClass) { - this.valueClass = valueClass; - } - - public @NonNull AdvancedListRuinRecreateMoveSelectorConfig withValueClass(@NonNull Class valueClass) { - this.setValueClass(valueClass); - return this; - } - - public String getVariableName() { - return variableName; - } - - public void setVariableName(String variableName) { - this.variableName = variableName; - } - - public @NonNull AdvancedListRuinRecreateMoveSelectorConfig withVariableName(@NonNull String variableName) { - this.setVariableName(variableName); - return this; - } - - // ************************** - // Interface methods - // ************************** - - @Override - public boolean hasNearbySelectionConfig() { - return false; - } - - @Override - public @NonNull AdvancedListRuinRecreateMoveSelectorConfig copyConfig() { - return new AdvancedListRuinRecreateMoveSelectorConfig().inherit(this); - } - - @Override - public void visitReferencedClasses(@NonNull Consumer> classVisitor) { - classVisitor.accept(ruinAndRecreatePickerClass); - } - - @Override - public @NonNull AdvancedListRuinRecreateMoveSelectorConfig - inherit(@NonNull AdvancedListRuinRecreateMoveSelectorConfig inheritedConfig) { - super.inherit(inheritedConfig); - ruinAndRecreatePickerClass = - ConfigUtils.inheritOverwritableProperty(ruinAndRecreatePickerClass, - inheritedConfig.getRuinAndRecreatePickerClass()); - entityClass = - ConfigUtils.inheritOverwritableProperty(entityClass, inheritedConfig.getEntityClass()); - valueClass = - ConfigUtils.inheritOverwritableProperty(valueClass, inheritedConfig.getValueClass()); - 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..e8e3db25bf --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/ListMultiStageMoveSelectorConfig.java @@ -0,0 +1,67 @@ +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; + } + + 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 ddc49ff1c0..810dd7eee4 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 @@ -11,8 +11,8 @@ import ai.timefold.solver.core.config.heuristic.selector.move.composite.UnionMoveSelectorConfig; 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.AdvancedRuinRecreateMoveSelectorConfig; 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,8 +20,8 @@ import ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.SubChainChangeMoveSelectorConfig; 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.AdvancedListRuinRecreateMoveSelectorConfig; 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; @@ -67,10 +67,10 @@ public class LocalSearchPhaseConfig extends PhaseConfig type = RuinRecreateMoveSelectorConfig.class), @XmlElement(name = ListRuinRecreateMoveSelectorConfig.XML_ELEMENT_NAME, type = ListRuinRecreateMoveSelectorConfig.class), - @XmlElement(name = AdvancedRuinRecreateMoveSelectorConfig.XML_ELEMENT_NAME, - type = AdvancedRuinRecreateMoveSelectorConfig.class), - @XmlElement(name = AdvancedListRuinRecreateMoveSelectorConfig.XML_ELEMENT_NAME, - type = AdvancedListRuinRecreateMoveSelectorConfig.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 7e3ec66c6e..cb83a00e06 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,8 +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.AdvancedRuinRecreateMoveSelectorConfig; -import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.AdvancedListRuinRecreateMoveSelectorConfig; +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; @@ -144,13 +144,13 @@ DestinationSelector applyNearbySelection(DestinationSelec HeuristicConfigPolicy configPolicy, SelectionCacheType minimumCacheType, SelectionOrder resolvedSelectionOrder, ElementDestinationSelector destinationSelector); - AbstractMoveSelectorFactory - buildBasicAdvancedRuinRecreateMoveSelectorFactory( - AdvancedRuinRecreateMoveSelectorConfig moveSelectorConfig); + AbstractMoveSelectorFactory + buildBasicMultiStageMoveSelectorFactory( + MultiStageMoveSelectorConfig moveSelectorConfig); - AbstractMoveSelectorFactory - buildListAdvancedRuinRecreateMoveSelectorFactory( - AdvancedListRuinRecreateMoveSelectorConfig moveSelectorConfig); + AbstractMoveSelectorFactory + buildListMultistageMoveSelectorFactory( + ListMultiStageMoveSelectorConfig moveSelectorConfig); enum Feature { MULTITHREADED_SOLVING("Multi-threaded solving", "remove moveThreadCount from solver configuration"), 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 665ecb79eb..d12d0cae11 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 @@ -7,8 +7,8 @@ import ai.timefold.solver.core.config.heuristic.selector.move.composite.UnionMoveSelectorConfig; 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.AdvancedRuinRecreateMoveSelectorConfig; 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,8 +17,8 @@ import ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.SubChainChangeMoveSelectorConfig; 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.AdvancedListRuinRecreateMoveSelectorConfig; 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; @@ -87,15 +87,15 @@ public interface MoveSelectorFactory { return new UnionMoveSelectorFactory<>(unionMoveSelectorConfig); } else if (moveSelectorConfig instanceof CartesianProductMoveSelectorConfig cartesianProductMoveSelectorConfig) { return new CartesianProductMoveSelectorFactory<>(cartesianProductMoveSelectorConfig); - } else if (moveSelectorConfig instanceof AdvancedRuinRecreateMoveSelectorConfig advancedRuinRecreateMoveSelectorConfig) { + } else if (moveSelectorConfig instanceof MultiStageMoveSelectorConfig advancedRuinRecreateMoveSelectorConfig) { var enterpriseService = TimefoldSolverEnterpriseService .loadOrFail(TimefoldSolverEnterpriseService.Feature.ADVANCED_RUIN_AND_RECREATE); - return enterpriseService.buildBasicAdvancedRuinRecreateMoveSelectorFactory(advancedRuinRecreateMoveSelectorConfig); - } else if (moveSelectorConfig instanceof AdvancedListRuinRecreateMoveSelectorConfig advancedListRuinRecreateMoveSelectorConfig) { + return enterpriseService.buildBasicMultiStageMoveSelectorFactory(advancedRuinRecreateMoveSelectorConfig); + } else if (moveSelectorConfig instanceof ListMultiStageMoveSelectorConfig advancedListRuinRecreateMoveSelectorConfig) { var enterpriseService = TimefoldSolverEnterpriseService .loadOrFail(TimefoldSolverEnterpriseService.Feature.ADVANCED_RUIN_AND_RECREATE); return enterpriseService - .buildListAdvancedRuinRecreateMoveSelectorFactory(advancedListRuinRecreateMoveSelectorConfig); + .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/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); From 65790772372333cec5c971b546eb83546294fca8 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Thu, 11 Dec 2025 16:35:27 -0500 Subject: [PATCH 6/9] chore: better descriptor return signatures --- .../variable/descriptor/BasicVariableDescriptor.java | 7 +++++++ .../domain/variable/descriptor/ListVariableDescriptor.java | 7 +++++++ .../domain/variable/descriptor/VariableDescriptor.java | 7 ++++--- 3 files changed, 18 insertions(+), 3 deletions(-) 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; } } From 969834cb553ba5ae12cb7a63718ace5a83275451 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Fri, 12 Dec 2025 16:31:53 -0500 Subject: [PATCH 7/9] chore: add docs --- benchmark/src/main/resources/benchmark.xsd | 16 +-- .../CartesianProductMoveSelectorConfig.java | 12 +-- .../composite/UnionMoveSelectorConfig.java | 12 +-- ...java => MultistageMoveSelectorConfig.java} | 18 ++-- ... => ListMultistageMoveSelectorConfig.java} | 14 +-- .../localsearch/LocalSearchPhaseConfig.java | 12 +-- .../TimefoldSolverEnterpriseService.java | 14 +-- .../selector/move/MoveSelectorFactory.java | 10 +- .../enterprise-edition.adoc | 20 ++++ .../move-selector-reference.adoc | 102 ++++++++++++++++++ 10 files changed, 176 insertions(+), 54 deletions(-) rename core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/{MultiStageMoveSelectorConfig.java => MultistageMoveSelectorConfig.java} (80%) rename core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/{ListMultiStageMoveSelectorConfig.java => ListMultistageMoveSelectorConfig.java} (76%) diff --git a/benchmark/src/main/resources/benchmark.xsd b/benchmark/src/main/resources/benchmark.xsd index 4f3c3bc1b4..2a608ffee8 100644 --- a/benchmark/src/main/resources/benchmark.xsd +++ b/benchmark/src/main/resources/benchmark.xsd @@ -1178,10 +1178,10 @@ - + - + @@ -2033,10 +2033,10 @@ - + - + @@ -2078,7 +2078,7 @@ - + @@ -2111,7 +2111,7 @@ - + @@ -2453,10 +2453,10 @@ - + - + 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 e145d18c5e..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,7 +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.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; @@ -21,7 +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.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; @@ -56,10 +56,10 @@ public class CartesianProductMoveSelectorConfig extends MoveSelectorConfig { - public static final String XML_ELEMENT_NAME = "multiStageMoveSelector"; +public class MultistageMoveSelectorConfig extends MoveSelectorConfig { + public static final String XML_ELEMENT_NAME = "multistageMoveSelector"; protected Class stageProviderClass; @@ -35,7 +35,7 @@ public void setStageProviderClass( this.stageProviderClass = stageProviderClass; } - public @NonNull MultiStageMoveSelectorConfig withStageProviderClass( + public @NonNull MultistageMoveSelectorConfig withStageProviderClass( @NonNull Class stageProviderClass) { this.setStageProviderClass(stageProviderClass); return this; @@ -49,7 +49,7 @@ public void setEntityClass(Class entityClass) { this.entityClass = entityClass; } - public @NonNull MultiStageMoveSelectorConfig withEntityClass(@NonNull Class entityClass) { + public @NonNull MultistageMoveSelectorConfig withEntityClass(@NonNull Class entityClass) { this.setEntityClass(entityClass); return this; } @@ -62,7 +62,7 @@ public void setVariableName(String variableName) { this.variableName = variableName; } - public @NonNull MultiStageMoveSelectorConfig withVariableName(@NonNull String variableName) { + public @NonNull MultistageMoveSelectorConfig withVariableName(@NonNull String variableName) { this.setVariableName(variableName); return this; } @@ -77,8 +77,8 @@ public boolean hasNearbySelectionConfig() { } @Override - public @NonNull MultiStageMoveSelectorConfig copyConfig() { - return new MultiStageMoveSelectorConfig().inherit(this); + public @NonNull MultistageMoveSelectorConfig copyConfig() { + return new MultistageMoveSelectorConfig().inherit(this); } @Override @@ -87,8 +87,8 @@ public void visitReferencedClasses(@NonNull Consumer> classVisitor) { } @Override - public @NonNull MultiStageMoveSelectorConfig - inherit(@NonNull MultiStageMoveSelectorConfig inheritedConfig) { + public @NonNull MultistageMoveSelectorConfig + inherit(@NonNull MultistageMoveSelectorConfig inheritedConfig) { super.inherit(inheritedConfig); stageProviderClass = ConfigUtils.inheritOverwritableProperty(stageProviderClass, 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 similarity index 76% rename from core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/ListMultiStageMoveSelectorConfig.java rename to core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/ListMultistageMoveSelectorConfig.java index e8e3db25bf..49bf24e59f 100644 --- 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 @@ -12,8 +12,8 @@ @XmlType(propOrder = { "stageProviderClass" }) -public class ListMultiStageMoveSelectorConfig extends MoveSelectorConfig { - public static final String XML_ELEMENT_NAME = "listMultiStageMoveSelector"; +public class ListMultistageMoveSelectorConfig extends MoveSelectorConfig { + public static final String XML_ELEMENT_NAME = "listMultistageMoveSelector"; protected Class stageProviderClass; @@ -30,7 +30,7 @@ public void setStageProviderClass( this.stageProviderClass = stageProviderClass; } - public @NonNull ListMultiStageMoveSelectorConfig withStageProviderClass( + public @NonNull ListMultistageMoveSelectorConfig withStageProviderClass( @NonNull Class stageProviderClass) { this.setStageProviderClass(stageProviderClass); return this; @@ -46,8 +46,8 @@ public boolean hasNearbySelectionConfig() { } @Override - public @NonNull ListMultiStageMoveSelectorConfig copyConfig() { - return new ListMultiStageMoveSelectorConfig().inherit(this); + public @NonNull ListMultistageMoveSelectorConfig copyConfig() { + return new ListMultistageMoveSelectorConfig().inherit(this); } @Override @@ -56,8 +56,8 @@ public void visitReferencedClasses(@NonNull Consumer> classVisitor) { } @Override - public @NonNull ListMultiStageMoveSelectorConfig - inherit(@NonNull ListMultiStageMoveSelectorConfig inheritedConfig) { + public @NonNull ListMultistageMoveSelectorConfig + inherit(@NonNull ListMultistageMoveSelectorConfig inheritedConfig) { super.inherit(inheritedConfig); stageProviderClass = ConfigUtils.inheritOverwritableProperty(stageProviderClass, 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 810dd7eee4..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,7 +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.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; @@ -21,7 +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.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; @@ -67,10 +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 = 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 cb83a00e06..a2b7f679d9 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,8 +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.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; @@ -144,13 +144,13 @@ DestinationSelector applyNearbySelection(DestinationSelec HeuristicConfigPolicy configPolicy, SelectionCacheType minimumCacheType, SelectionOrder resolvedSelectionOrder, ElementDestinationSelector destinationSelector); - AbstractMoveSelectorFactory - buildBasicMultiStageMoveSelectorFactory( - MultiStageMoveSelectorConfig moveSelectorConfig); + AbstractMoveSelectorFactory + buildBasicMultistageMoveSelectorFactory( + MultistageMoveSelectorConfig moveSelectorConfig); - AbstractMoveSelectorFactory + AbstractMoveSelectorFactory buildListMultistageMoveSelectorFactory( - ListMultiStageMoveSelectorConfig moveSelectorConfig); + ListMultistageMoveSelectorConfig moveSelectorConfig); enum Feature { MULTITHREADED_SOLVING("Multi-threaded solving", "remove moveThreadCount from solver configuration"), 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 d12d0cae11..99ad62d7f6 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,7 +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.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; @@ -18,7 +18,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.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; @@ -87,11 +87,11 @@ public interface MoveSelectorFactory { return new UnionMoveSelectorFactory<>(unionMoveSelectorConfig); } else if (moveSelectorConfig instanceof CartesianProductMoveSelectorConfig cartesianProductMoveSelectorConfig) { return new CartesianProductMoveSelectorFactory<>(cartesianProductMoveSelectorConfig); - } else if (moveSelectorConfig instanceof MultiStageMoveSelectorConfig advancedRuinRecreateMoveSelectorConfig) { + } else if (moveSelectorConfig instanceof MultistageMoveSelectorConfig advancedRuinRecreateMoveSelectorConfig) { var enterpriseService = TimefoldSolverEnterpriseService .loadOrFail(TimefoldSolverEnterpriseService.Feature.ADVANCED_RUIN_AND_RECREATE); - return enterpriseService.buildBasicMultiStageMoveSelectorFactory(advancedRuinRecreateMoveSelectorConfig); - } else if (moveSelectorConfig instanceof ListMultiStageMoveSelectorConfig advancedListRuinRecreateMoveSelectorConfig) { + return enterpriseService.buildBasicMultistageMoveSelectorFactory(advancedRuinRecreateMoveSelectorConfig); + } else if (moveSelectorConfig instanceof ListMultistageMoveSelectorConfig advancedListRuinRecreateMoveSelectorConfig) { var enterpriseService = TimefoldSolverEnterpriseService .loadOrFail(TimefoldSolverEnterpriseService.Feature.ADVANCED_RUIN_AND_RECREATE); return enterpriseService 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 From e138452be4917644ea4e30ab1fd353b115b78607 Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Fri, 12 Dec 2025 16:40:39 -0500 Subject: [PATCH 8/9] chore: review comments --- .../core/enterprise/TimefoldSolverEnterpriseService.java | 4 ++-- .../impl/heuristic/selector/move/MoveSelectorFactory.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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 a2b7f679d9..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 @@ -158,8 +158,8 @@ enum Feature { NEARBY_SELECTION("Nearby selection", "remove nearby selection from solver configuration"), AUTOMATIC_NODE_SHARING("Automatic node sharing", "remove automatic node sharing from solver configuration"), - ADVANCED_RUIN_AND_RECREATE("Advanced ruin and recreate", - "remove advancedRuinRecreateMoveSelector and/or advancedListRuinRecreateMoveSelector from the 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/heuristic/selector/move/MoveSelectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/MoveSelectorFactory.java index 99ad62d7f6..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 @@ -89,11 +89,11 @@ public interface MoveSelectorFactory { return new CartesianProductMoveSelectorFactory<>(cartesianProductMoveSelectorConfig); } else if (moveSelectorConfig instanceof MultistageMoveSelectorConfig advancedRuinRecreateMoveSelectorConfig) { var enterpriseService = TimefoldSolverEnterpriseService - .loadOrFail(TimefoldSolverEnterpriseService.Feature.ADVANCED_RUIN_AND_RECREATE); + .loadOrFail(TimefoldSolverEnterpriseService.Feature.MULTISTAGE_MOVE); return enterpriseService.buildBasicMultistageMoveSelectorFactory(advancedRuinRecreateMoveSelectorConfig); } else if (moveSelectorConfig instanceof ListMultistageMoveSelectorConfig advancedListRuinRecreateMoveSelectorConfig) { var enterpriseService = TimefoldSolverEnterpriseService - .loadOrFail(TimefoldSolverEnterpriseService.Feature.ADVANCED_RUIN_AND_RECREATE); + .loadOrFail(TimefoldSolverEnterpriseService.Feature.MULTISTAGE_MOVE); return enterpriseService .buildListMultistageMoveSelectorFactory(advancedListRuinRecreateMoveSelectorConfig); } else { From 1bcc11a4477acac55e7e5c325c3a8195060a089b Mon Sep 17 00:00:00 2001 From: Christopher Chianelli Date: Fri, 12 Dec 2025 16:50:56 -0500 Subject: [PATCH 9/9] chore: review comments --- .../generic/MultistageMoveSelectorConfig.java | 26 +++++++++++-------- .../ListMultistageMoveSelectorConfig.java | 4 +++ 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/MultistageMoveSelectorConfig.java b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/MultistageMoveSelectorConfig.java index 9f3bb1c117..f9442c8b01 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/MultistageMoveSelectorConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/MultistageMoveSelectorConfig.java @@ -35,12 +35,6 @@ public void setStageProviderClass( this.stageProviderClass = stageProviderClass; } - public @NonNull MultistageMoveSelectorConfig withStageProviderClass( - @NonNull Class stageProviderClass) { - this.setStageProviderClass(stageProviderClass); - return this; - } - public Class getEntityClass() { return entityClass; } @@ -49,11 +43,6 @@ public void setEntityClass(Class entityClass) { this.entityClass = entityClass; } - public @NonNull MultistageMoveSelectorConfig withEntityClass(@NonNull Class entityClass) { - this.setEntityClass(entityClass); - return this; - } - public String getVariableName() { return variableName; } @@ -62,6 +51,21 @@ 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; 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 index 49bf24e59f..9f641ea0b2 100644 --- 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 @@ -30,6 +30,10 @@ public void setStageProviderClass( this.stageProviderClass = stageProviderClass; } + // ************************** + // With methods + // ************************** + public @NonNull ListMultistageMoveSelectorConfig withStageProviderClass( @NonNull Class stageProviderClass) { this.setStageProviderClass(stageProviderClass);