Skip to content

Commit 2d3a8cf

Browse files
committed
feat: enable mixed models for remaining moves
1 parent d0da2b4 commit 2d3a8cf

46 files changed

Lines changed: 360 additions & 343 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseFactory.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ public static EntityPlacerConfig buildListVariableQueuedValuePlacerConfig(Heuris
201201
// Finally, QueuedValuePlacer uses the recording ValueSelector and a ListChangeMoveSelector.
202202
// The ListChangeMoveSelector's replaying ValueSelector mimics the QueuedValuePlacer's recording ValueSelector.
203203
return new QueuedValuePlacerConfig()
204+
.withEntityClass(variableDescriptor.getEntityDescriptor().getEntityClass())
204205
.withValueSelectorConfig(mimicRecordingValueSelectorConfig)
205206
.withMoveSelectorConfig(listChangeMoveSelectorConfig);
206207
}

core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/SubListSelectorFactory.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,15 @@ public SubListSelector<Solution_> buildSubListSelector(HeuristicConfigPolicy<Sol
4848
+ ") which is not supported. SubListSelector only supports random selection order.");
4949
}
5050

51-
EntityIndependentValueSelector<Solution_> valueSelector = buildEntityIndependentValueSelector(configPolicy,
52-
entitySelector.getEntityDescriptor(), minimumCacheType, inheritedSelectionOrder);
51+
var valueSelector = buildEntityIndependentValueSelector(configPolicy, entitySelector.getEntityDescriptor(),
52+
minimumCacheType, inheritedSelectionOrder);
5353

54-
int minimumSubListSize = Objects.requireNonNullElse(config.getMinimumSubListSize(), DEFAULT_MINIMUM_SUB_LIST_SIZE);
55-
int maximumSubListSize = Objects.requireNonNullElse(config.getMaximumSubListSize(), DEFAULT_MAXIMUM_SUB_LIST_SIZE);
56-
RandomSubListSelector<Solution_> baseSubListSelector =
54+
var minimumSubListSize = Objects.requireNonNullElse(config.getMinimumSubListSize(), DEFAULT_MINIMUM_SUB_LIST_SIZE);
55+
var maximumSubListSize = Objects.requireNonNullElse(config.getMaximumSubListSize(), DEFAULT_MAXIMUM_SUB_LIST_SIZE);
56+
var baseSubListSelector =
5757
new RandomSubListSelector<>(entitySelector, valueSelector, minimumSubListSize, maximumSubListSize);
5858

59-
SubListSelector<Solution_> subListSelector =
59+
var subListSelector =
6060
applyNearbySelection(configPolicy, minimumCacheType, inheritedSelectionOrder, baseSubListSelector);
6161

6262
subListSelector = applyMimicRecording(configPolicy, subListSelector);

core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/PillarSwapMoveSelectorFactory.java

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import ai.timefold.solver.core.config.heuristic.selector.common.SelectionOrder;
99
import ai.timefold.solver.core.config.heuristic.selector.entity.pillar.PillarSelectorConfig;
1010
import ai.timefold.solver.core.config.heuristic.selector.move.generic.PillarSwapMoveSelectorConfig;
11+
import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor;
1112
import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor;
1213
import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy;
1314
import ai.timefold.solver.core.impl.heuristic.selector.entity.pillar.PillarSelector;
@@ -25,27 +26,57 @@ public PillarSwapMoveSelectorFactory(PillarSwapMoveSelectorConfig moveSelectorCo
2526
@Override
2627
protected MoveSelector<Solution_> buildBaseMoveSelector(HeuristicConfigPolicy<Solution_> configPolicy,
2728
SelectionCacheType minimumCacheType, boolean randomSelection) {
28-
PillarSelectorConfig leftPillarSelectorConfig =
29+
var leftPillarSelectorConfig =
2930
Objects.requireNonNullElseGet(config.getPillarSelectorConfig(), PillarSelectorConfig::new);
30-
PillarSelectorConfig rightPillarSelectorConfig =
31+
EntityDescriptor<Solution_> leftEntityDescriptor = null;
32+
if (leftPillarSelectorConfig.getEntitySelectorConfig() != null
33+
&& leftPillarSelectorConfig.getEntitySelectorConfig().getEntityClass() != null) {
34+
leftEntityDescriptor = configPolicy.getSolutionDescriptor()
35+
.findEntityDescriptor(leftPillarSelectorConfig.getEntitySelectorConfig().getEntityClass());
36+
}
37+
var leftVariableNameIncludeList = config.getVariableNameIncludeList();
38+
if (leftVariableNameIncludeList == null && leftEntityDescriptor != null
39+
&& leftEntityDescriptor.hasAnyGenuineBasicVariables() && leftEntityDescriptor.hasAnyGenuineListVariables()) {
40+
// Mixed models filter out the list variable
41+
leftVariableNameIncludeList = leftEntityDescriptor.getGenuineBasicVariableDescriptorList().stream()
42+
.map(GenuineVariableDescriptor::getVariableName)
43+
.toList();
44+
}
45+
var rightPillarSelectorConfig =
3146
Objects.requireNonNullElse(config.getSecondaryPillarSelectorConfig(), leftPillarSelectorConfig);
32-
PillarSelector<Solution_> leftPillarSelector =
33-
buildPillarSelector(leftPillarSelectorConfig, configPolicy, minimumCacheType, randomSelection);
34-
PillarSelector<Solution_> rightPillarSelector =
35-
buildPillarSelector(rightPillarSelectorConfig, configPolicy, minimumCacheType, randomSelection);
47+
EntityDescriptor<Solution_> rightEntityDescriptor = null;
48+
if (rightPillarSelectorConfig.getEntitySelectorConfig() != null
49+
&& rightPillarSelectorConfig.getEntitySelectorConfig().getEntityClass() != null) {
50+
rightEntityDescriptor = configPolicy.getSolutionDescriptor()
51+
.findEntityDescriptor(rightPillarSelectorConfig.getEntitySelectorConfig().getEntityClass());
52+
}
53+
var rightVariableNameIncludeList = config.getVariableNameIncludeList();
54+
if (rightVariableNameIncludeList == null && rightEntityDescriptor != null
55+
&& rightEntityDescriptor.hasAnyGenuineBasicVariables() && rightEntityDescriptor.hasAnyGenuineListVariables()) {
56+
// Mixed models filter out the list variable
57+
rightVariableNameIncludeList = rightEntityDescriptor.getGenuineBasicVariableDescriptorList().stream()
58+
.map(GenuineVariableDescriptor::getVariableName)
59+
.toList();
60+
}
61+
var leftPillarSelector =
62+
buildPillarSelector(leftPillarSelectorConfig, configPolicy, leftVariableNameIncludeList, minimumCacheType,
63+
randomSelection);
64+
var rightPillarSelector =
65+
buildPillarSelector(rightPillarSelectorConfig, configPolicy, rightVariableNameIncludeList, minimumCacheType,
66+
randomSelection);
3667

37-
List<GenuineVariableDescriptor<Solution_>> variableDescriptorList =
38-
deduceVariableDescriptorList(leftPillarSelector.getEntityDescriptor(), config.getVariableNameIncludeList());
68+
var variableDescriptorList =
69+
deduceVariableDescriptorList(leftPillarSelector.getEntityDescriptor(), leftVariableNameIncludeList);
3970
return new PillarSwapMoveSelector<>(leftPillarSelector, rightPillarSelector, variableDescriptorList, randomSelection);
4071
}
4172

4273
private PillarSelector<Solution_> buildPillarSelector(PillarSelectorConfig pillarSelectorConfig,
43-
HeuristicConfigPolicy<Solution_> configPolicy, SelectionCacheType minimumCacheType, boolean randomSelection) {
74+
HeuristicConfigPolicy<Solution_> configPolicy, List<String> variableNameIncludeList,
75+
SelectionCacheType minimumCacheType, boolean randomSelection) {
4476
return PillarSelectorFactory.<Solution_> create(pillarSelectorConfig)
4577
.buildPillarSelector(configPolicy, config.getSubPillarType(),
4678
(Class<? extends Comparator<Object>>) config.getSubPillarSequenceComparatorClass(), minimumCacheType,
47-
SelectionOrder.fromRandomSelectionBoolean(randomSelection),
48-
config.getVariableNameIncludeList());
79+
SelectionOrder.fromRandomSelectionBoolean(randomSelection), variableNameIncludeList);
4980
}
5081

5182
}

core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/SubListChangeMoveSelectorFactory.java

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,18 @@ protected MoveSelectorConfig<?> buildUnfoldedMoveSelectorConfig(HeuristicConfigP
6767
if (onlyEntityDescriptor != null) {
6868
entityDescriptors = Collections.singletonList(onlyEntityDescriptor);
6969
} else {
70-
entityDescriptors = configPolicy.getSolutionDescriptor().getGenuineEntityDescriptors();
70+
// We select a single entity since there is only one descriptor that includes a list variable
71+
var onlyEntityDescriptorWithListVariable =
72+
getTheOnlyEntityDescriptorWithListVariable(configPolicy.getSolutionDescriptor());
73+
entityDescriptors = new ArrayList<>();
74+
if (onlyEntityDescriptorWithListVariable != null) {
75+
entityDescriptors.add(onlyEntityDescriptorWithListVariable);
76+
}
7177
}
72-
if (entityDescriptors.size() > 1) {
73-
throw new IllegalArgumentException("""
74-
The subListChangeMoveSelector (%s) cannot unfold when there are multiple entities (%s).
75-
Please use one subListChangeMoveSelector per each planning list variable."""
76-
.formatted(config, entityDescriptors));
78+
if (entityDescriptors.isEmpty()) {
79+
throw new IllegalArgumentException(
80+
"The subListChangeMoveSelector (%s) cannot unfold because there are no planning list variables."
81+
.formatted(config));
7782
}
7883
var entityDescriptor = entityDescriptors.iterator().next();
7984

@@ -117,11 +122,6 @@ The subListChangeMoveSelector (%s) cannot unfold when there are multiple entitie
117122
.map(variableDescriptor -> ((ListVariableDescriptor<Solution_>) variableDescriptor))
118123
.toList());
119124
}
120-
if (variableDescriptorList.isEmpty()) {
121-
throw new IllegalArgumentException(
122-
"The subListChangeMoveSelector (%s) cannot unfold because there are no planning list variables."
123-
.formatted(config));
124-
}
125125
if (variableDescriptorList.size() > 1) {
126126
throw new IllegalArgumentException(
127127
"The subListChangeMoveSelector (%s) cannot unfold because there are multiple planning list variables."

core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/SubListSwapMoveSelectorFactory.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@
88
import ai.timefold.solver.core.config.heuristic.selector.list.SubListSelectorConfig;
99
import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.SubListSwapMoveSelectorConfig;
1010
import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy;
11-
import ai.timefold.solver.core.impl.heuristic.selector.entity.EntitySelector;
1211
import ai.timefold.solver.core.impl.heuristic.selector.entity.EntitySelectorFactory;
13-
import ai.timefold.solver.core.impl.heuristic.selector.list.SubListSelector;
1412
import ai.timefold.solver.core.impl.heuristic.selector.list.SubListSelectorFactory;
1513
import ai.timefold.solver.core.impl.heuristic.selector.move.AbstractMoveSelectorFactory;
1614
import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector;
@@ -30,15 +28,17 @@ protected MoveSelector<Solution_> buildBaseMoveSelector(HeuristicConfigPolicy<So
3028
+ ") only supports random selection order.");
3129
}
3230

33-
SelectionOrder selectionOrder = SelectionOrder.fromRandomSelectionBoolean(randomSelection);
31+
var selectionOrder = SelectionOrder.fromRandomSelectionBoolean(randomSelection);
3432

35-
EntitySelector<Solution_> entitySelector = EntitySelectorFactory
36-
.<Solution_> create(new EntitySelectorConfig())
33+
var onlyEntityDescriptor = getTheOnlyEntityDescriptorWithListVariable(configPolicy.getSolutionDescriptor());
34+
// We defined the entity class since there is a single entity descriptor that includes a list variable
35+
var entitySelector = EntitySelectorFactory
36+
.<Solution_> create(new EntitySelectorConfig().withEntityClass(onlyEntityDescriptor.getEntityClass()))
3737
.buildEntitySelector(configPolicy, minimumCacheType, selectionOrder);
3838

39-
SubListSelectorConfig subListSelectorConfig =
39+
var subListSelectorConfig =
4040
Objects.requireNonNullElseGet(config.getSubListSelectorConfig(), SubListSelectorConfig::new);
41-
SubListSelectorConfig secondarySubListSelectorConfig =
41+
var secondarySubListSelectorConfig =
4242
Objects.requireNonNullElse(config.getSecondarySubListSelectorConfig(), subListSelectorConfig);
4343

4444
// minimum -> subListSelector
@@ -63,14 +63,14 @@ protected MoveSelector<Solution_> buildBaseMoveSelector(HeuristicConfigPolicy<So
6363
SubListSwapMoveSelectorConfig::getMaximumSubListSize,
6464
"secondarySubListSelector", secondarySubListSelectorConfig);
6565
}
66-
SubListSelector<Solution_> leftSubListSelector = SubListSelectorFactory
66+
var leftSubListSelector = SubListSelectorFactory
6767
.<Solution_> create(subListSelectorConfig)
6868
.buildSubListSelector(configPolicy, entitySelector, minimumCacheType, selectionOrder);
69-
SubListSelector<Solution_> rightSubListSelector = SubListSelectorFactory
69+
var rightSubListSelector = SubListSelectorFactory
7070
.<Solution_> create(secondarySubListSelectorConfig)
7171
.buildSubListSelector(configPolicy, entitySelector, minimumCacheType, selectionOrder);
7272

73-
boolean selectReversingMoveToo = Objects.requireNonNullElse(config.getSelectReversingMoveToo(), true);
73+
var selectReversingMoveToo = Objects.requireNonNullElse(config.getSelectReversingMoveToo(), true);
7474

7575
return new RandomSubListSwapMoveSelector<>(leftSubListSelector, rightSubListSelector, selectReversingMoveToo);
7676
}

core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ruin/ListRuinRecreateMoveSelectorFactory.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,14 @@ protected MoveSelector<Solution_> buildBaseMoveSelector(HeuristicConfigPolicy<So
3030
CountSupplier minimumSelectedSupplier = ruinMoveSelectorConfig::determineMinimumRuinedCount;
3131
CountSupplier maximumSelectedSupplier = ruinMoveSelectorConfig::determineMaximumRuinedCount;
3232

33-
this.getTheOnlyEntityDescriptor(configPolicy.getSolutionDescriptor());
33+
this.getTheOnlyEntityDescriptorWithListVariable(configPolicy.getSolutionDescriptor());
3434

3535
var listVariableDescriptor = configPolicy.getSolutionDescriptor().getListVariableDescriptor();
3636
var entityDescriptor = listVariableDescriptor.getEntityDescriptor();
3737
var valueSelector =
38-
(EntityIndependentValueSelector<Solution_>) ValueSelectorFactory.<Solution_> create(new ValueSelectorConfig())
38+
(EntityIndependentValueSelector<Solution_>) ValueSelectorFactory
39+
.<Solution_> create(
40+
new ValueSelectorConfig().withVariableName(listVariableDescriptor.getVariableName()))
3941
.buildValueSelector(configPolicy, entityDescriptor, minimumCacheType, SelectionOrder.RANDOM,
4042
false, ValueSelectorFactory.ListValueFilteringType.ACCEPT_ASSIGNED);
4143
var entityPlacerConfig = DefaultConstructionHeuristicPhaseFactory.buildListVariableQueuedValuePlacerConfig(configPolicy,

core/src/test/java/ai/timefold/solver/core/api/solver/SolutionManagerTest.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@
3131
import ai.timefold.solver.core.testdomain.list.shadowhistory.TestdataListSolutionWithShadowHistory;
3232
import ai.timefold.solver.core.testdomain.list.shadowhistory.TestdataListValueWithShadowHistory;
3333
import ai.timefold.solver.core.testdomain.list.shadowhistory.TestdataListWithShadowHistoryIncrementalScoreCalculator;
34-
import ai.timefold.solver.core.testdomain.multivar.basic.TestdataMultiVarEntity;
35-
import ai.timefold.solver.core.testdomain.multivar.basic.TestdataMultiVarSolution;
36-
import ai.timefold.solver.core.testdomain.multivar.basic.TestdataMultivarIncrementalScoreCalculator;
37-
import ai.timefold.solver.core.testdomain.multivar.basic.TestdataOtherValue;
34+
import ai.timefold.solver.core.testdomain.multivar.TestdataMultiVarEntity;
35+
import ai.timefold.solver.core.testdomain.multivar.TestdataMultiVarSolution;
36+
import ai.timefold.solver.core.testdomain.multivar.TestdataMultivarIncrementalScoreCalculator;
37+
import ai.timefold.solver.core.testdomain.multivar.TestdataOtherValue;
3838
import ai.timefold.solver.core.testdomain.shadow.TestdataShadowedEntity;
3939
import ai.timefold.solver.core.testdomain.shadow.TestdataShadowedIncrementalScoreCalculator;
4040
import ai.timefold.solver.core.testdomain.shadow.TestdataShadowedSolution;

core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import ai.timefold.solver.core.config.constructionheuristic.placer.QueuedValuePlacerConfig;
2323
import ai.timefold.solver.core.config.solver.monitoring.MonitoringConfig;
2424
import ai.timefold.solver.core.config.solver.monitoring.SolverMetric;
25-
import ai.timefold.solver.core.config.solver.termination.TerminationConfig;
2625
import ai.timefold.solver.core.impl.phase.event.PhaseLifecycleListenerAdapter;
2726
import ai.timefold.solver.core.impl.solver.DefaultSolver;
2827
import ai.timefold.solver.core.impl.solver.scope.SolverScope;
@@ -36,10 +35,6 @@
3635
import ai.timefold.solver.core.testdomain.list.unassignedvar.TestdataAllowsUnassignedValuesListEntity;
3736
import ai.timefold.solver.core.testdomain.list.unassignedvar.TestdataAllowsUnassignedValuesListSolution;
3837
import ai.timefold.solver.core.testdomain.list.unassignedvar.TestdataAllowsUnassignedValuesListValue;
39-
import ai.timefold.solver.core.testdomain.multivar.list.multientity.TestdataListMultiEntityFirstEntity;
40-
import ai.timefold.solver.core.testdomain.multivar.list.multientity.TestdataListMultiEntitySecondEntity;
41-
import ai.timefold.solver.core.testdomain.multivar.list.multientity.TestdataListMultiEntitySolution;
42-
import ai.timefold.solver.core.testdomain.multivar.list.singleentity.TestdataListMultiVarEasyScoreCalculator;
4338
import ai.timefold.solver.core.testdomain.pinned.TestdataPinnedEntity;
4439
import ai.timefold.solver.core.testdomain.pinned.TestdataPinnedSolution;
4540
import ai.timefold.solver.core.testdomain.pinned.unassignedvar.TestdataPinnedAllowsUnassignedEntity;
@@ -394,19 +389,4 @@ void failWithPooledEntityPlacers() {
394389
"The Construction Heuristic configuration (ConstructionHeuristicPhaseConfig) does not support multiple configurations when using the pooled placer configuration PooledEntityPlacerConfig.");
395390
}
396391

397-
@Test
398-
void failMultiEntityWithListAndBasicVariables() {
399-
var solverConfig = PlannerTestUtils.buildSolverConfig(
400-
TestdataListMultiEntitySolution.class, TestdataListMultiEntityFirstEntity.class,
401-
TestdataListMultiEntitySecondEntity.class)
402-
.withPhases(new ConstructionHeuristicPhaseConfig()
403-
.withTerminationConfig(new TerminationConfig().withStepCountLimit(16)))
404-
.withEasyScoreCalculatorClass(TestdataListMultiVarEasyScoreCalculator.class);
405-
406-
var problem = TestdataListMultiEntitySolution.generateUninitializedSolution(2, 2, 2);
407-
assertThatCode(() -> PlannerTestUtils.solve(solverConfig, problem))
408-
.hasMessageContaining("has no entityClass configured and because there are multiple in the entityClassSet")
409-
.hasMessageContaining("it cannot be deduced automatically");
410-
}
411-
412392
}

core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/placer/entity/QueuedEntityPlacerFactoryTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@
2727
import ai.timefold.solver.core.impl.solver.scope.SolverScope;
2828
import ai.timefold.solver.core.testdomain.TestdataValue;
2929
import ai.timefold.solver.core.testdomain.difficultyweight.TestdataDifficultyWeightSolution;
30-
import ai.timefold.solver.core.testdomain.multivar.basic.TestdataMultiVarEntity;
31-
import ai.timefold.solver.core.testdomain.multivar.basic.TestdataMultiVarSolution;
30+
import ai.timefold.solver.core.testdomain.multivar.TestdataMultiVarEntity;
31+
import ai.timefold.solver.core.testdomain.multivar.TestdataMultiVarSolution;
3232

3333
import org.junit.jupiter.api.Test;
3434

core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/placer/entity/QueuedEntityPlacerTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333
import ai.timefold.solver.core.testdomain.TestdataEntity;
3434
import ai.timefold.solver.core.testdomain.TestdataSolution;
3535
import ai.timefold.solver.core.testdomain.TestdataValue;
36-
import ai.timefold.solver.core.testdomain.multivar.basic.TestdataMultiVarEntity;
37-
import ai.timefold.solver.core.testdomain.multivar.basic.TestdataMultiVarSolution;
36+
import ai.timefold.solver.core.testdomain.multivar.TestdataMultiVarEntity;
37+
import ai.timefold.solver.core.testdomain.multivar.TestdataMultiVarSolution;
3838

3939
import org.junit.jupiter.api.Test;
4040

0 commit comments

Comments
 (0)