diff --git a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/NearbyAutoConfigurationEnabled.java b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/NearbyAutoConfigurationEnabled.java index 52ffcbafc8e..e67d4b127eb 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/NearbyAutoConfigurationEnabled.java +++ b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/NearbyAutoConfigurationEnabled.java @@ -11,6 +11,11 @@ */ public interface NearbyAutoConfigurationEnabled> { + /** + * @return true if it can enable the nearby setting for the given move configuration; otherwise, it returns false. + */ + boolean canEnableNearbyInMixedModels(); + /** * @return new instance with the Nearby Selection settings properly configured */ diff --git a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/composite/UnionMoveSelectorConfig.java b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/composite/UnionMoveSelectorConfig.java index c11dfa6632c..d17619266ed 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/composite/UnionMoveSelectorConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/composite/UnionMoveSelectorConfig.java @@ -206,6 +206,11 @@ public boolean hasNearbySelectionConfig() { && moveSelectorConfigList.stream().anyMatch(MoveSelectorConfig::hasNearbySelectionConfig); } + @Override + public boolean canEnableNearbyInMixedModels() { + return false; + } + @Override public String toString() { return getClass().getSimpleName() + "(" + moveSelectorConfigList + ")"; diff --git a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/ChangeMoveSelectorConfig.java b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/ChangeMoveSelectorConfig.java index d9b6b081346..7b11e95aa93 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/ChangeMoveSelectorConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/ChangeMoveSelectorConfig.java @@ -104,6 +104,11 @@ public boolean hasNearbySelectionConfig() { || (valueSelectorConfig != null && valueSelectorConfig.hasNearbySelectionConfig()); } + @Override + public boolean canEnableNearbyInMixedModels() { + return false; + } + @Override public String toString() { return getClass().getSimpleName() + "(" + entitySelectorConfig + ", " + valueSelectorConfig + ")"; diff --git a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/SwapMoveSelectorConfig.java b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/SwapMoveSelectorConfig.java index 7dcd43fc836..bcdad12baf8 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/SwapMoveSelectorConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/SwapMoveSelectorConfig.java @@ -127,6 +127,11 @@ public boolean hasNearbySelectionConfig() { || (secondaryEntitySelectorConfig != null && secondaryEntitySelectorConfig.hasNearbySelectionConfig()); } + @Override + public boolean canEnableNearbyInMixedModels() { + return false; + } + @Override public String toString() { return getClass().getSimpleName() + "(" + entitySelectorConfig diff --git a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/chained/TailChainSwapMoveSelectorConfig.java b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/chained/TailChainSwapMoveSelectorConfig.java index f4dc04f6f16..a0c71dd100b 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/chained/TailChainSwapMoveSelectorConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/chained/TailChainSwapMoveSelectorConfig.java @@ -108,6 +108,11 @@ public boolean hasNearbySelectionConfig() { || (valueSelectorConfig != null && valueSelectorConfig.hasNearbySelectionConfig()); } + @Override + public boolean canEnableNearbyInMixedModels() { + return false; + } + @Override public String toString() { return getClass().getSimpleName() + "(" + entitySelectorConfig + ", " + valueSelectorConfig + ")"; diff --git a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/ListChangeMoveSelectorConfig.java b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/ListChangeMoveSelectorConfig.java index 8eca5d5436c..55c0b2c5102 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/ListChangeMoveSelectorConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/ListChangeMoveSelectorConfig.java @@ -106,6 +106,11 @@ public boolean hasNearbySelectionConfig() { || (destinationSelectorConfig != null && destinationSelectorConfig.hasNearbySelectionConfig()); } + @Override + public boolean canEnableNearbyInMixedModels() { + return true; + } + @Override public String toString() { return getClass().getSimpleName() + "(" + valueSelectorConfig + ", " + destinationSelectorConfig + ")"; diff --git a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/ListSwapMoveSelectorConfig.java b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/ListSwapMoveSelectorConfig.java index c3848196450..f763b8da3b6 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/ListSwapMoveSelectorConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/ListSwapMoveSelectorConfig.java @@ -104,6 +104,11 @@ public boolean hasNearbySelectionConfig() { || (secondaryValueSelectorConfig != null && secondaryValueSelectorConfig.hasNearbySelectionConfig()); } + @Override + public boolean canEnableNearbyInMixedModels() { + return true; + } + @Override public String toString() { return getClass().getSimpleName() + "(" + valueSelectorConfig diff --git a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/kopt/KOptListMoveSelectorConfig.java b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/kopt/KOptListMoveSelectorConfig.java index 7b6f809ecca..620bccd2eda 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/kopt/KOptListMoveSelectorConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/list/kopt/KOptListMoveSelectorConfig.java @@ -138,6 +138,11 @@ public boolean hasNearbySelectionConfig() { || (valueSelectorConfig != null && valueSelectorConfig.hasNearbySelectionConfig()); } + @Override + public boolean canEnableNearbyInMixedModels() { + return true; + } + @Override public String toString() { return getClass().getSimpleName() + "()"; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptor.java index 37cdbc08433..67d6f42376c 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptor.java @@ -166,10 +166,6 @@ public static SolutionDescriptor buildSolutionDescriptor( solutionDescriptor.constraintWeightSupplier.initialize(solutionDescriptor, descriptorPolicy.getMemberAccessorFactory(), descriptorPolicy.getDomainAccessType()); } - // Temporally disabling the mixed model - if (solutionDescriptor.hasBothBasicAndListVariables()) { - throw new IllegalStateException("Combining list variable and basic variables is currently not supported."); - } return solutionDescriptor; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/HeuristicConfigPolicy.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/HeuristicConfigPolicy.java index 07534bca2a6..ab86367cfde 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/HeuristicConfigPolicy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/HeuristicConfigPolicy.java @@ -161,6 +161,12 @@ public HeuristicConfigPolicy createPhaseConfigPolicy() { return cloneBuilder().build(); } + public HeuristicConfigPolicy copyConfigPolicyWithoutNearbySetting() { + return cloneBuilder() + .withNearbyDistanceMeterClass(null) + .build(); + } + public HeuristicConfigPolicy createChildThreadConfigPolicy(ChildThreadType childThreadType) { return cloneBuilder() .withLogIndentation(logIndentation + " ") diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/SubListSelectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/SubListSelectorFactory.java index 13dabfbf431..3a1844f7345 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/SubListSelectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/SubListSelectorFactory.java @@ -113,8 +113,16 @@ private SubListSelector applyNearbySelection(HeuristicConfigPolicy buildEntityIndependentValueSelector( HeuristicConfigPolicy configPolicy, EntityDescriptor entityDescriptor, SelectionCacheType minimumCacheType, SelectionOrder inheritedSelectionOrder) { - ValueSelectorConfig valueSelectorConfig = - Objects.requireNonNullElseGet(config.getValueSelectorConfig(), ValueSelectorConfig::new); + ValueSelectorConfig valueSelectorConfig = config != null ? config.getValueSelectorConfig() : null; + if (valueSelectorConfig == null) { + valueSelectorConfig = new ValueSelectorConfig(); + } + // Mixed models require that the variable name be set + if (configPolicy.getSolutionDescriptor().hasBothBasicAndListVariables() + && valueSelectorConfig.getVariableName() == null) { + var variableName = entityDescriptor.getGenuineListVariableDescriptor().getVariableName(); + valueSelectorConfig.setVariableName(variableName); + } ValueSelector valueSelector = ValueSelectorFactory . create(valueSelectorConfig) .buildValueSelector(configPolicy, entityDescriptor, minimumCacheType, inheritedSelectionOrder); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/composite/UnionMoveSelectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/composite/UnionMoveSelectorFactory.java index 58ed734d1cf..f6dad4d0096 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/composite/UnionMoveSelectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/composite/UnionMoveSelectorFactory.java @@ -26,6 +26,7 @@ protected MoveSelector buildBaseMoveSelector(HeuristicConfigPolicy(config.getMoveSelectorList()); if (configPolicy.getNearbyDistanceMeterClass() != null) { + var isMixedModel = configPolicy.getSolutionDescriptor().hasBothBasicAndListVariables(); for (var selectorConfig : config.getMoveSelectorList()) { if (selectorConfig instanceof NearbyAutoConfigurationEnabled nearbySelectorConfig) { if (selectorConfig.hasNearbySelectionConfig()) { @@ -38,7 +39,12 @@ The selector configuration (%s) already includes the Nearby Selection setting, m // We delay the autoconfiguration to the deepest UnionMoveSelectorConfig node in the tree // to avoid duplicating configuration // when there are nested unionMoveSelector configurations - if (selectorConfig instanceof UnionMoveSelectorConfig) { + var isUnionMoveSelectorConfig = selectorConfig instanceof UnionMoveSelectorConfig; + // When using a mixed model, we do not enable nearby for basic variables, + // as it applies only to list or chained variables. + // Chained variables are forbidden in mixed models. + var isNearbyDisabled = isMixedModel && !nearbySelectorConfig.canEnableNearbyInMixedModels(); + if (isUnionMoveSelectorConfig || isNearbyDisabled) { continue; } // Add a new configuration with Nearby Selection enabled diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/PhaseFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/PhaseFactory.java index 5eec41ea40e..d223d71a477 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/PhaseFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/PhaseFactory.java @@ -4,6 +4,7 @@ import java.util.List; import ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicPhaseConfig; +import ai.timefold.solver.core.config.constructionheuristic.placer.QueuedEntityPlacerConfig; import ai.timefold.solver.core.config.exhaustivesearch.ExhaustiveSearchPhaseConfig; import ai.timefold.solver.core.config.localsearch.LocalSearchPhaseConfig; import ai.timefold.solver.core.config.partitionedsearch.PartitionedSearchPhaseConfig; @@ -56,15 +57,28 @@ static List> buildPhases(List phaseCon + "without a configured termination (" + previousPhaseConfig + ")."); } } + var isConstructionPhase = ConstructionHeuristicPhaseConfig.class.isAssignableFrom(phaseConfig.getClass()); + // We currently do not support nearby functionality for CH. + // Additionally, mixed models must not include any nearby settings for basic variables, + // as this may cause failures in certain cases, + // such as when defining multiple variables with a Cartesian product. + var entityPlacerConfig = + isConstructionPhase ? ((ConstructionHeuristicPhaseConfig) phaseConfig).getEntityPlacerConfig() : null; + var disableNearbySetting = + configPolicy.getNearbyDistanceMeterClass() != null && entityPlacerConfig != null + && QueuedEntityPlacerConfig.class.isAssignableFrom(entityPlacerConfig.getClass()) + && configPolicy.getSolutionDescriptor().hasBothBasicAndListVariables(); + var updatedConfigPolicy = + disableNearbySetting ? configPolicy.copyConfigPolicyWithoutNearbySetting() : configPolicy; // The initialization phase can only be applied to construction heuristics or custom phases - var isConstructionOrCustomPhase = ConstructionHeuristicPhaseConfig.class.isAssignableFrom(phaseConfig.getClass()) - || CustomPhaseConfig.class.isAssignableFrom(phaseConfig.getClass()); + var isConstructionOrCustomPhase = + isConstructionPhase || CustomPhaseConfig.class.isAssignableFrom(phaseConfig.getClass()); // The next phase must be a local search var isNextPhaseLocalSearch = phaseIndex + 1 < phaseConfigList.size() && LocalSearchPhaseConfig.class.isAssignableFrom(phaseConfigList.get(phaseIndex + 1).getClass()); PhaseFactory phaseFactory = PhaseFactory.create(phaseConfig); var phase = phaseFactory.buildPhase(phaseIndex, - !isPhaseSelected && isConstructionOrCustomPhase && isNextPhaseLocalSearch, configPolicy, + !isPhaseSelected && isConstructionOrCustomPhase && isNextPhaseLocalSearch, updatedConfigPolicy, bestSolutionRecaller, termination); // Ensure only one initialization phase is set if (!isPhaseSelected && isConstructionOrCustomPhase && isNextPhaseLocalSearch) { diff --git a/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java index 2ca4bbacced..f904fb90460 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java @@ -47,7 +47,6 @@ import ai.timefold.solver.core.testutil.AbstractMeterTest; import ai.timefold.solver.core.testutil.PlannerTestUtils; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import io.micrometer.core.instrument.Metrics; @@ -349,7 +348,6 @@ void constructionHeuristicAllocateToValueFromQueue() { .filter(e -> e.getValue() == null)).isEmpty(); } - @Disabled("The mixed model is currently unavailable for general use") @Test void failMixedModelDefaultConfiguration() { var solverConfig = PlannerTestUtils diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/composite/UnionMoveSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/composite/UnionMoveSelectorTest.java index 6877ad48ef9..1976ae42cf2 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/composite/UnionMoveSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/composite/UnionMoveSelectorTest.java @@ -2,6 +2,7 @@ import static ai.timefold.solver.core.testutil.PlannerAssert.assertAllCodesOfMoveSelector; import static ai.timefold.solver.core.testutil.PlannerAssert.verifyPhaseLifecycle; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -11,6 +12,7 @@ import java.util.Map; import java.util.Random; +import ai.timefold.solver.core.config.heuristic.selector.move.composite.UnionMoveSelectorConfig; import ai.timefold.solver.core.impl.heuristic.move.DummyMove; import ai.timefold.solver.core.impl.heuristic.selector.SelectorTestUtils; import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector; @@ -211,4 +213,9 @@ void emptyBiasedRandomSelection() { verifyPhaseLifecycle(childMoveSelectorList.get(1), 1, 1, 1); } + @Test + void testEnableNearbyMixedModel() { + var moveSelectorConfig = new UnionMoveSelectorConfig(); + assertThat(moveSelectorConfig.canEnableNearbyInMixedModels()).isFalse(); + } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/ChangeMoveTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/ChangeMoveTest.java index 10158a9ceeb..de016c22af6 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/ChangeMoveTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/ChangeMoveTest.java @@ -9,6 +9,7 @@ import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.score.director.ScoreDirector; +import ai.timefold.solver.core.config.heuristic.selector.move.generic.ChangeMoveSelectorConfig; import ai.timefold.solver.core.impl.score.director.easy.EasyScoreDirectorFactory; import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataSolution; @@ -168,4 +169,9 @@ void toStringTestMultiVar() { assertThat(new ChangeMove<>(variableDescriptor, c, v3)).hasToString("c {v4 -> v3}"); } + @Test + void testEnableNearbyMixedModel() { + var moveSelectorConfig = new ChangeMoveSelectorConfig(); + assertThat(moveSelectorConfig.canEnableNearbyInMixedModels()).isFalse(); + } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/SwapMoveTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/SwapMoveTest.java index 30355be02db..6ba7e232953 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/SwapMoveTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/SwapMoveTest.java @@ -10,6 +10,7 @@ import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.score.director.ScoreDirector; +import ai.timefold.solver.core.config.heuristic.selector.move.generic.SwapMoveSelectorConfig; import ai.timefold.solver.core.impl.score.director.easy.EasyScoreDirectorFactory; import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataSolution; @@ -259,4 +260,9 @@ void toStringTestMultiVar() { assertThat(new SwapMove<>(variableDescriptorList, c, b)).hasToString("c {v2, v4, w2} <-> b {v1, v3, w1}"); } + @Test + void testEnableNearbyMixedModel() { + var moveSelectorConfig = new SwapMoveSelectorConfig(); + assertThat(moveSelectorConfig.canEnableNearbyInMixedModels()).isFalse(); + } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/chained/TailChainSwapMoveTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/chained/TailChainSwapMoveTest.java index 6880091ba25..d10beb50447 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/chained/TailChainSwapMoveTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/chained/TailChainSwapMoveTest.java @@ -8,6 +8,7 @@ import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.score.director.ScoreDirector; +import ai.timefold.solver.core.config.heuristic.selector.move.generic.chained.TailChainSwapMoveSelectorConfig; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.domain.variable.anchor.AnchorVariableDemand; import ai.timefold.solver.core.impl.domain.variable.anchor.AnchorVariableSupply; @@ -292,4 +293,10 @@ void extractPlanningEntitiesWithRightEntityNull() { move.doMoveOnGenuineVariables(innerScoreDirector); assertThat(move.getPlanningEntities()).doesNotContainNull(); } + + @Test + void testEnableNearbyMixedModel() { + var moveSelectorConfig = new TailChainSwapMoveSelectorConfig(); + assertThat(moveSelectorConfig.canEnableNearbyInMixedModels()).isFalse(); + } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListChangeMoveTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListChangeMoveTest.java index 71ae5280ce1..725bf9c76cd 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListChangeMoveTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListChangeMoveTest.java @@ -12,6 +12,7 @@ import java.util.stream.Stream; import ai.timefold.solver.core.api.score.director.ScoreDirector; +import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListChangeMoveSelectorConfig; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; import ai.timefold.solver.core.testdomain.TestdataObject; @@ -188,4 +189,10 @@ void toStringTest() { assertThat(new ListChangeMove<>(variableDescriptor, e1, 1, e1, 0)).hasToString("2 {e1[1] -> e1[0]}"); assertThat(new ListChangeMove<>(variableDescriptor, e1, 0, e2, 1)).hasToString("1 {e1[0] -> e2[1]}"); } + + @Test + void testEnableNearbyMixedModel() { + var moveSelectorConfig = new ListChangeMoveSelectorConfig(); + assertThat(moveSelectorConfig.canEnableNearbyInMixedModels()).isTrue(); + } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListSwapMoveTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListSwapMoveTest.java index 2441d8a1805..b2a4c80819f 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListSwapMoveTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListSwapMoveTest.java @@ -7,6 +7,7 @@ import static org.mockito.Mockito.verify; import ai.timefold.solver.core.api.score.director.ScoreDirector; +import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListSwapMoveSelectorConfig; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.move.director.MoveDirector; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; @@ -137,4 +138,10 @@ void toStringTest() { assertThat(new ListSwapMove<>(variableDescriptor, e1, 0, e1, 1)).hasToString("1 {e1[0]} <-> 2 {e1[1]}"); assertThat(new ListSwapMove<>(variableDescriptor, e1, 1, e2, 0)).hasToString("2 {e1[1]} <-> 3 {e2[0]}"); } + + @Test + void testEnableNearbyMixedModel() { + var moveSelectorConfig = new ListSwapMoveSelectorConfig(); + assertThat(moveSelectorConfig.canEnableNearbyInMixedModels()).isTrue(); + } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/SubListChangeMoveSelectorFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/SubListChangeMoveSelectorFactoryTest.java index 809d0efa443..7f034b035f5 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/SubListChangeMoveSelectorFactoryTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/SubListChangeMoveSelectorFactoryTest.java @@ -20,7 +20,6 @@ import ai.timefold.solver.core.testdomain.list.TestdataListSolution; import ai.timefold.solver.core.testdomain.mixed.multientity.TestdataMixedMultiEntitySolution; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -44,7 +43,6 @@ void buildMoveSelector() { assertThat(selector.isSelectReversingMoveToo()).isTrue(); } - @Disabled("The mixed model is currently unavailable for general use") @Test void buildMoveSelectorMultiEntity() { var config = new SubListChangeMoveSelectorConfig(); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/SubListSwapMoveSelectorFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/SubListSwapMoveSelectorFactoryTest.java index b388c6d70f1..12422e928d1 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/SubListSwapMoveSelectorFactoryTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/SubListSwapMoveSelectorFactoryTest.java @@ -16,7 +16,6 @@ import ai.timefold.solver.core.testdomain.list.TestdataListSolution; import ai.timefold.solver.core.testdomain.mixed.multientity.TestdataMixedMultiEntitySolution; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -41,7 +40,6 @@ void buildBaseMoveSelector() { assertThat(selector.isSelectReversingMoveToo()).isTrue(); } - @Disabled("The mixed model is currently unavailable for general use") @Test void buildMoveSelectorMultiEntity() { var config = new SubListSwapMoveSelectorConfig(); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptListMoveTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptListMoveTest.java index 22cee8a8e85..4e7c6468ade 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptListMoveTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptListMoveTest.java @@ -11,6 +11,7 @@ import java.util.List; import java.util.function.Function; +import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.kopt.KOptListMoveSelectorConfig; import ai.timefold.solver.core.impl.domain.variable.ListVariableStateDemand; import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; @@ -548,6 +549,12 @@ void testIsFeasible() { assertThat(kOptListMove.isMoveDoable(scoreDirector)).isFalse(); } + @Test + void testEnableNearbyMixedModel() { + var moveSelectorConfig = new KOptListMoveSelectorConfig(); + assertThat(moveSelectorConfig.canEnableNearbyInMixedModels()).isTrue(); + } + /** * Create a sequential or non-sequential k-opt from the supplied pairs of undirected removed and added edges. * diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java index 3e38ae69574..a199116cc21 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java @@ -146,7 +146,6 @@ import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.extension.ExtendWith; @@ -1657,19 +1656,6 @@ private static List generateMovesForMultiEntity() { return allMoveSelectionConfigList; } - @Test - void failMixedModel() { - var solverConfig = PlannerTestUtils - .buildSolverConfig(TestdataMixedSolution.class, TestdataMixedEntity.class, TestdataMixedValue.class, - TestdataMixedOtherValue.class) - .withPreviewFeature(DECLARATIVE_SHADOW_VARIABLES); - - assertThatCode(() -> PlannerTestUtils.solve(solverConfig, new TestdataSolution("s1"))) - .hasMessageContaining( - "Combining list variable and basic variables is currently not supported"); - } - - @Disabled("The mixed model is currently unavailable for general use") @Test void solveMixedModel() { // Same size for both list and basic variables @@ -1681,7 +1667,6 @@ void solveMixedModel() { executeSolveMixedModel(3, 2, 2); } - @Disabled("The mixed model is currently unavailable for general use") @Test void solveMultiEntityMixedModel() { // Same size for both list and basic variables @@ -1764,7 +1749,6 @@ void executeSolveMultiEntityMixedModel(int entitySize, int valueSize, int otherV } } - @Disabled("The mixed model is currently unavailable for general use") @Test void solveMixedModelCustomMove() { var solverConfig = PlannerTestUtils.buildSolverConfig( @@ -1789,7 +1773,6 @@ void solveMixedModelCustomMove() { .isEmpty(); } - @Disabled("The mixed model is currently unavailable for general use") @Test void solveMixedModelCustomPhase() { var solverConfig = PlannerTestUtils.buildSolverConfig( @@ -1822,7 +1805,6 @@ private static List> getSortMannerLi return sortMannerList; } - @Disabled("The mixed model is currently unavailable for general use") @ParameterizedTest @MethodSource("getSortMannerList") void solveMixedModelWithSortManner(Pair sorterManner) { @@ -1856,7 +1838,6 @@ void solveMixedModelWithSortManner(Pair s .isEmpty(); } - @Disabled("The mixed model is currently unavailable for general use") @Test void solvePinnedMixedModel() { // We don't enable the LS because we want to ensure the pinned entity remains uninitialized @@ -1880,7 +1861,6 @@ void solvePinnedMixedModel() { assertThat(solution.getEntityList().get(0).getValueList()).isEmpty(); } - @Disabled("The mixed model is currently unavailable for general use") @Test void solveUnassignedMixedModel() { var solverConfig = PlannerTestUtils.buildSolverConfig( @@ -1907,7 +1887,6 @@ void solveUnassignedMixedModel() { .hasSize(2); } - @Disabled("The mixed model is currently unavailable for general use") @Test void solvePinnedAndUnassignedMixedModel() { var solverConfig = PlannerTestUtils.buildSolverConfig( @@ -1995,7 +1974,6 @@ private static List generateMovesForMixedModel() { return allMoveSelectionConfigList; } - @Disabled("The mixed model is currently unavailable for general use") @ParameterizedTest @MethodSource("generateMovesForMixedModel") void solveMoveConfigMixedModel(MoveSelectorConfig moveSelectionConfig) { @@ -2058,7 +2036,6 @@ private static List generateMovesForMultiEntityMixedModel() return allMoveSelectionConfigList; } - @Disabled("The mixed model is currently unavailable for general use") @ParameterizedTest @MethodSource("generateMovesForMultiEntityMixedModel") void solveMultiEntityMoveConfigMixedModel(MoveSelectorConfig moveSelectionConfig) { diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/mixed/multientity/TestdataMixedMultiEntityFirstValue.java b/core/src/test/java/ai/timefold/solver/core/testdomain/mixed/multientity/TestdataMixedMultiEntityFirstValue.java index abc089f9380..bf8cbdccbb4 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/mixed/multientity/TestdataMixedMultiEntityFirstValue.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/mixed/multientity/TestdataMixedMultiEntityFirstValue.java @@ -1,9 +1,7 @@ package ai.timefold.solver.core.testdomain.mixed.multientity; -import ai.timefold.solver.core.api.domain.entity.PlanningEntity; import ai.timefold.solver.core.testdomain.TestdataObject; -@PlanningEntity public class TestdataMixedMultiEntityFirstValue extends TestdataObject { public TestdataMixedMultiEntityFirstValue() { diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/mixed/multientity/TestdataMixedMultiEntitySecondValue.java b/core/src/test/java/ai/timefold/solver/core/testdomain/mixed/multientity/TestdataMixedMultiEntitySecondValue.java index cda51205e40..8ce38b9b329 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/mixed/multientity/TestdataMixedMultiEntitySecondValue.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/mixed/multientity/TestdataMixedMultiEntitySecondValue.java @@ -1,9 +1,7 @@ package ai.timefold.solver.core.testdomain.mixed.multientity; -import ai.timefold.solver.core.api.domain.entity.PlanningEntity; import ai.timefold.solver.core.testdomain.TestdataObject; -@PlanningEntity public class TestdataMixedMultiEntitySecondValue extends TestdataObject { private int strength; diff --git a/docs/src/modules/ROOT/pages/optimization-algorithms/construction-heuristics.adoc b/docs/src/modules/ROOT/pages/optimization-algorithms/construction-heuristics.adoc index d7ae4236a2c..79dbda1baff 100644 --- a/docs/src/modules/ROOT/pages/optimization-algorithms/construction-heuristics.adoc +++ b/docs/src/modules/ROOT/pages/optimization-algorithms/construction-heuristics.adoc @@ -702,6 +702,79 @@ is supported too. For scaling out, see <>. +[#mixedModelConstructionHeuristics] +== Mixed modeling and construction heuristics + +The default behavior of the construction heuristic for xref:using-timefold-solver/modeling-planning-problems.adoc#mixedModels[mixed models] is +to use strategies based on the allocation of xref:optimization-algorithms/construction-heuristics.adoc#allocateEntityFromQueue[entities] and xref:optimization-algorithms/construction-heuristics.adoc#allocateToValueFromQueue[values] +for solving all related variables. + +=== Algorithm description + +The allocations work like this: + +. Put all entities in a queue. +. Assign the first entity (from that queue) to the best value. +. Repeat until all entities are assigned. +. Put all values in a round-robin queue. +. Assign the best entity to the first value (from that queue). +. Repeat until all entities are assigned. + +[#mixedModelConfiguration] +=== Configuration + +Simple configuration: + +[source,xml,options="nowrap"] +---- + + + + + + + + + + + +---- + +Advanced configuration for a single entity class with a list variable and a single basic variable: + +[source,xml,options="nowrap"] +---- + + + + + PHASE + SORTED + DECREASING_DIFFICULTY + + + + + PHASE + SORTED + INCREASING_STRENGTH + + + + + + + + + + PHASE + SORTED + INCREASING_STRENGTH + + + +---- + [#scalingConstructionHeuristics] == Scaling construction heuristics diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc b/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc index 7b7e91fbe2e..52e1942c974 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc +++ b/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc @@ -1848,7 +1848,7 @@ if you need any of the following planning techniques: - <> or <>, - xref:optimization-algorithms/exhaustive-search.adoc#exhaustiveSearch[exhaustive search], - xref:enterprise-edition/enterprise-edition.adoc#partitionedSearch[partitioned search], -- coexistence with another list or basic planning variable. +- coexistence with another list variable. ==== For example, the vehicle routing problem can be modeled as follows: @@ -2501,6 +2501,94 @@ Two value range providers are usually combined: Since ``Domicile`` and ``Visit`` both implement ``Standstill``, an <> on ``Standstill`` will combine both value ranges. ==== +[#mixedModels] +== Mixed models + +If both xref:using-timefold-solver/modeling-planning-problems.adoc#planningVariable[basic] and xref:using-timefold-solver/modeling-planning-problems#planningListVariable[list] variables are included in the model, +it is referred to as a *mixed model*. +This model eliminates the need for xref:using-timefold-solver/modeling-planning-problems#chainedPlanningVariable[chained] variables +when mixed variable types are required. + +The following planning entity defines two genuine variables: + +[tabs] +==== +Java:: ++ +[source,java,options="nowrap"] +---- +@PlanningEntity +public class Line { + @PlanningVariable + private Operator operator; + + @PlanningListVariable + private List jobs; + + ... +} +---- + +Python:: ++ +[source,python,options="nowrap"] +---- +@planning_entity +@dataclass +class Line: + operator: Annotated[Operator | None, PlanningVariable] + jobs: Annotated[list[Job], PlanningListVariable] = field(default_factory=list) + ... +---- +==== + +The mixed models also allow for the creation of multiple planning entities, each defining its own variables: + +[tabs] +==== +Java:: ++ +[source,java,options="nowrap"] +---- +@PlanningEntity +public class Line { + @PlanningListVariable + private List jobs; + + ... +} + +@PlanningEntity +public class LineOperation { + @PlanningVariable + private Operator operator; + + ... +} +---- + +Python:: ++ +[source,python,options="nowrap"] +---- +@planning_entity +@dataclass +class Line: + jobs: Annotated[list[Job], PlanningListVariable] = field(default_factory=list) + ... + +@planning_entity +@dataclass +class LineOperation: + operator: Annotated[Operator | None, PlanningVariable] + ... +---- +==== + +[WARNING] +==== +Creating mixed models with chained and list variables is prohibited, and only one list variable can be defined. +==== [#planningProblemAndPlanningSolution] == Planning problem and planning solution