Skip to content

Commit eb92383

Browse files
authored
feat: enable support for mixed models and R&R moves with basic variables (#1651)
The proposed approach adds two new properties: `entitySelector` and `variableName`. The `entitySelector` property allows the configuration of the entity when the model has multiple entities, and the second property defines the variable to be used when there are multiple basic variables.
1 parent b2e3eed commit eb92383

File tree

7 files changed

+162
-12
lines changed

7 files changed

+162
-12
lines changed

benchmark/src/main/resources/benchmark.xsd

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1641,6 +1641,12 @@
16411641

16421642

16431643
<xs:element minOccurs="0" name="maximumRuinedPercentage" type="xs:double"/>
1644+
1645+
1646+
<xs:element minOccurs="0" name="entitySelector" type="tns:entitySelectorConfig"/>
1647+
1648+
1649+
<xs:element minOccurs="0" name="variableName" type="xs:string"/>
16441650

16451651

16461652
</xs:sequence>

core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/generic/RuinRecreateMoveSelectorConfig.java

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import java.util.function.Consumer;
44

5+
import jakarta.xml.bind.annotation.XmlElement;
56
import jakarta.xml.bind.annotation.XmlType;
67

8+
import ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig;
79
import ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig;
810
import ai.timefold.solver.core.config.util.ConfigUtils;
911

@@ -14,7 +16,9 @@
1416
"minimumRuinedCount",
1517
"maximumRuinedCount",
1618
"minimumRuinedPercentage",
17-
"maximumRuinedPercentage"
19+
"maximumRuinedPercentage",
20+
"entitySelectorConfig",
21+
"variableName"
1822
})
1923
public class RuinRecreateMoveSelectorConfig extends MoveSelectorConfig<RuinRecreateMoveSelectorConfig> {
2024

@@ -30,6 +34,10 @@ public class RuinRecreateMoveSelectorConfig extends MoveSelectorConfig<RuinRecre
3034
protected Double minimumRuinedPercentage = null;
3135
protected Double maximumRuinedPercentage = null;
3236

37+
@XmlElement(name = "entitySelector")
38+
protected EntitySelectorConfig entitySelectorConfig = null;
39+
protected String variableName = null;
40+
3341
// **************************
3442
// Getters/Setters
3543
// **************************
@@ -81,11 +89,38 @@ public void setMaximumRuinedPercentage(@Nullable Double maximumRuinedPercentage)
8189
this.maximumRuinedPercentage = maximumRuinedPercentage;
8290
}
8391

92+
public EntitySelectorConfig getEntitySelectorConfig() {
93+
return entitySelectorConfig;
94+
}
95+
96+
public void setEntitySelectorConfig(EntitySelectorConfig entitySelectorConfig) {
97+
this.entitySelectorConfig = entitySelectorConfig;
98+
}
99+
100+
public String getVariableName() {
101+
return variableName;
102+
}
103+
104+
public void setVariableName(String variableName) {
105+
this.variableName = variableName;
106+
}
107+
84108
public @NonNull RuinRecreateMoveSelectorConfig withMaximumRuinedPercentage(@NonNull Double maximumRuinedPercentage) {
85109
this.maximumRuinedPercentage = maximumRuinedPercentage;
86110
return this;
87111
}
88112

113+
public @NonNull RuinRecreateMoveSelectorConfig
114+
withEntitySelectorConfig(@NonNull EntitySelectorConfig entitySelectorConfig) {
115+
this.setEntitySelectorConfig(entitySelectorConfig);
116+
return this;
117+
}
118+
119+
public @NonNull RuinRecreateMoveSelectorConfig withVariableName(@NonNull String variableName) {
120+
this.setVariableName(variableName);
121+
return this;
122+
}
123+
89124
// **************************
90125
// Interface methods
91126
// **************************
@@ -102,7 +137,9 @@ public boolean hasNearbySelectionConfig() {
102137

103138
@Override
104139
public void visitReferencedClasses(@NonNull Consumer<Class<?>> classVisitor) {
105-
// No referenced classes.
140+
if (entitySelectorConfig != null) {
141+
entitySelectorConfig.visitReferencedClasses(classVisitor);
142+
}
106143
}
107144

108145
@Override
@@ -116,6 +153,9 @@ public void visitReferencedClasses(@NonNull Consumer<Class<?>> classVisitor) {
116153
ConfigUtils.inheritOverwritableProperty(minimumRuinedPercentage, inheritedConfig.getMinimumRuinedPercentage());
117154
maximumRuinedPercentage =
118155
ConfigUtils.inheritOverwritableProperty(maximumRuinedPercentage, inheritedConfig.getMaximumRuinedPercentage());
156+
entitySelectorConfig = ConfigUtils.inheritConfig(entitySelectorConfig, inheritedConfig.getEntitySelectorConfig());
157+
variableName =
158+
ConfigUtils.inheritOverwritableProperty(variableName, inheritedConfig.getVariableName());
119159
return this;
120160
}
121161

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import java.util.Set;
55

66
import ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicPhaseConfig;
7+
import ai.timefold.solver.core.config.constructionheuristic.placer.QueuedEntityPlacerConfig;
8+
import ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig;
79
import ai.timefold.solver.core.config.solver.termination.TerminationConfig;
810
import ai.timefold.solver.core.impl.constructionheuristic.DefaultConstructionHeuristicPhase;
911
import ai.timefold.solver.core.impl.constructionheuristic.DefaultConstructionHeuristicPhase.DefaultConstructionHeuristicPhaseBuilder;
@@ -19,8 +21,11 @@ public final class RuinRecreateConstructionHeuristicPhaseBuilder<Solution_>
1921
extends DefaultConstructionHeuristicPhaseBuilder<Solution_> {
2022

2123
public static <Solution_> RuinRecreateConstructionHeuristicPhaseBuilder<Solution_>
22-
create(HeuristicConfigPolicy<Solution_> solverConfigPolicy) {
23-
var constructionHeuristicConfig = new ConstructionHeuristicPhaseConfig();
24+
create(HeuristicConfigPolicy<Solution_> solverConfigPolicy, EntitySelectorConfig entitySelectorConfig) {
25+
var queuedEntityPlacerConfig = new QueuedEntityPlacerConfig()
26+
.withEntitySelectorConfig(entitySelectorConfig);
27+
var constructionHeuristicConfig = new ConstructionHeuristicPhaseConfig()
28+
.withEntityPlacerConfig(queuedEntityPlacerConfig);
2429
return create(solverConfigPolicy, constructionHeuristicConfig);
2530
}
2631

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

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import ai.timefold.solver.core.config.heuristic.selector.common.SelectionOrder;
55
import ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig;
66
import ai.timefold.solver.core.config.heuristic.selector.move.generic.RuinRecreateMoveSelectorConfig;
7+
import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor;
78
import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy;
89
import ai.timefold.solver.core.impl.heuristic.selector.entity.EntitySelectorFactory;
910
import ai.timefold.solver.core.impl.heuristic.selector.move.AbstractMoveSelectorFactory;
@@ -25,18 +26,39 @@ protected MoveSelector<Solution_> buildBaseMoveSelector(HeuristicConfigPolicy<So
2526
CountSupplier minimumSelectedSupplier = ruinRecreateMoveSelectorConfig::determineMinimumRuinedCount;
2627
CountSupplier maximumSelectedSupplier = ruinRecreateMoveSelectorConfig::determineMaximumRuinedCount;
2728

28-
var entitySelector = EntitySelectorFactory.<Solution_> create(new EntitySelectorConfig())
29+
var entitySelectorConfig = config.getEntitySelectorConfig();
30+
if (entitySelectorConfig == null) {
31+
entitySelectorConfig = new EntitySelectorConfig();
32+
}
33+
var ruinRecreateEntitySelector = EntitySelectorFactory.<Solution_> create(entitySelectorConfig)
2934
.buildEntitySelector(configPolicy, minimumCacheType,
3035
SelectionOrder.fromRandomSelectionBoolean(true));
31-
var genuineVariableDescriptorList = entitySelector.getEntityDescriptor().getGenuineVariableDescriptorList();
32-
if (genuineVariableDescriptorList.size() != 1) {
36+
var genuineVariableDescriptorList =
37+
ruinRecreateEntitySelector.getEntityDescriptor().getGenuineBasicVariableDescriptorList();
38+
if (genuineVariableDescriptorList.size() != 1 && config.getVariableName() == null) {
3339
throw new UnsupportedOperationException(
34-
"Ruin and Recreate move selector currently only supports 1 planning variable.");
40+
"""
41+
The entity class %s contains several variables (%s), and it cannot be deduced automatically.
42+
Maybe set the property variableName."""
43+
.formatted(
44+
ruinRecreateEntitySelector.getEntityDescriptor().getEntityClass().getName(),
45+
genuineVariableDescriptorList.stream().map(GenuineVariableDescriptor::getVariableName)
46+
.toList()));
3547
}
3648
var variableDescriptor = genuineVariableDescriptorList.get(0);
37-
38-
var constructionHeuristicPhaseBuilder = RuinRecreateConstructionHeuristicPhaseBuilder.create(configPolicy);
39-
return new RuinRecreateMoveSelector<>(entitySelector, variableDescriptor, constructionHeuristicPhaseBuilder,
49+
if (genuineVariableDescriptorList.size() > 1) {
50+
variableDescriptor = genuineVariableDescriptorList.stream()
51+
.filter(v -> v.getVariableName().equals(config.getVariableName())).findFirst().orElse(null);
52+
}
53+
if (variableDescriptor == null) {
54+
throw new UnsupportedOperationException("The entity class %s has no variable named %s."
55+
.formatted(ruinRecreateEntitySelector.getEntityDescriptor().getEntityClass(), config.getVariableName()));
56+
}
57+
var nestedEntitySelectorConfig =
58+
getDefaultEntitySelectorConfigForEntity(configPolicy, ruinRecreateEntitySelector.getEntityDescriptor());
59+
var constructionHeuristicPhaseBuilder =
60+
RuinRecreateConstructionHeuristicPhaseBuilder.create(configPolicy, nestedEntitySelectorConfig);
61+
return new RuinRecreateMoveSelector<>(ruinRecreateEntitySelector, variableDescriptor, constructionHeuristicPhaseBuilder,
4062
minimumSelectedSupplier, maximumSelectedSupplier);
4163
}
4264
}

core/src/main/resources/solver.xsd

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,6 +900,10 @@
900900
<xs:element minOccurs="0" name="minimumRuinedPercentage" type="xs:double"/>
901901

902902
<xs:element minOccurs="0" name="maximumRuinedPercentage" type="xs:double"/>
903+
904+
<xs:element minOccurs="0" name="entitySelector" type="tns:entitySelectorConfig"/>
905+
906+
<xs:element minOccurs="0" name="variableName" type="xs:string"/>
903907

904908
</xs:sequence>
905909

core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1956,6 +1956,8 @@ private static List<MoveSelectorConfig> generateMovesForMixedModel() {
19561956
// Pilar swap - basic
19571957
allMoveSelectionConfigList.add(new PillarSwapMoveSelectorConfig().withPillarSelectorConfig(
19581958
new PillarSelectorConfig().withEntitySelectorConfig(pillarChangeEntitySelectorConfig)));
1959+
// R&R - basic
1960+
allMoveSelectionConfigList.add(new RuinRecreateMoveSelectorConfig().withVariableName("basicValue"));
19591961
// Change - list
19601962
allMoveSelectionConfigList.add(new ListChangeMoveSelectorConfig());
19611963
// Swap - list
@@ -2019,6 +2021,11 @@ private static List<MoveSelectorConfig> generateMovesForMultiEntityMixedModel()
20192021
// Pilar swap - basic
20202022
allMoveSelectionConfigList.add(new PillarSwapMoveSelectorConfig().withPillarSelectorConfig(
20212023
new PillarSelectorConfig().withEntitySelectorConfig(pillarChangeEntitySelectorConfig)));
2024+
// R&R - basic
2025+
allMoveSelectionConfigList.add(new RuinRecreateMoveSelectorConfig()
2026+
.withEntitySelectorConfig(
2027+
new EntitySelectorConfig().withEntityClass(TestdataMixedMultiEntitySecondEntity.class))
2028+
.withVariableName("basicValue"));
20222029
// Change - list
20232030
allMoveSelectionConfigList.add(new ListChangeMoveSelectorConfig());
20242031
// Swap - list
@@ -2074,6 +2081,60 @@ void solveMultiEntityMoveConfigMixedModel(MoveSelectorConfig moveSelectionConfig
20742081
}
20752082
}
20762083

2084+
@Test
2085+
void failRuinRecreateWithMultiVar() {
2086+
// Solver config
2087+
var localSearchConfig = new LocalSearchPhaseConfig().withMoveSelectorConfig(new RuinRecreateMoveSelectorConfig());
2088+
var solverConfig = PlannerTestUtils.buildSolverConfig(
2089+
TestdataMixedSolution.class, TestdataMixedEntity.class, TestdataMixedValue.class,
2090+
TestdataMixedOtherValue.class)
2091+
.withPreviewFeature(DECLARATIVE_SHADOW_VARIABLES)
2092+
.withPhases(localSearchConfig)
2093+
.withEasyScoreCalculatorClass(TestdataMixedEasyScoreCalculator.class);
2094+
var problem = TestdataMixedSolution.generateUninitializedSolution(2, 2, 2);
2095+
assertThatCode(() -> PlannerTestUtils.solve(solverConfig, problem))
2096+
.hasMessageContaining("The entity class")
2097+
.hasMessageContaining("TestdataMixedEntity")
2098+
.hasMessageContaining("contains several variables")
2099+
.hasMessageContaining("it cannot be deduced automatically.")
2100+
.hasMessageContaining("Maybe set the property variableName");
2101+
}
2102+
2103+
@Test
2104+
void failRuinRecreateWithBadVar() {
2105+
// Solver config
2106+
var moveSelectionConfig = new RuinRecreateMoveSelectorConfig()
2107+
.withVariableName("badVariable");
2108+
var localSearchConfig = new LocalSearchPhaseConfig().withMoveSelectorConfig(moveSelectionConfig);
2109+
var solverConfig = PlannerTestUtils.buildSolverConfig(
2110+
TestdataMixedSolution.class, TestdataMixedEntity.class, TestdataMixedValue.class,
2111+
TestdataMixedOtherValue.class)
2112+
.withPreviewFeature(DECLARATIVE_SHADOW_VARIABLES)
2113+
.withPhases(localSearchConfig)
2114+
.withEasyScoreCalculatorClass(TestdataMixedEasyScoreCalculator.class);
2115+
var problem = TestdataMixedSolution.generateUninitializedSolution(2, 2, 2);
2116+
assertThatCode(() -> PlannerTestUtils.solve(solverConfig, problem))
2117+
.hasMessageContaining("The entity class")
2118+
.hasMessageContaining("TestdataMixedEntity")
2119+
.hasMessageContaining("has no variable named badVariable");
2120+
}
2121+
2122+
@Test
2123+
void failRuinRecreateWithMultiEntityMultiVar() {
2124+
// Solver config
2125+
var localSearchConfig = new LocalSearchPhaseConfig().withMoveSelectorConfig(new RuinRecreateMoveSelectorConfig());
2126+
var solverConfig = PlannerTestUtils
2127+
.buildSolverConfig(TestdataMixedMultiEntitySolution.class, TestdataMixedMultiEntityFirstEntity.class,
2128+
TestdataMixedMultiEntitySecondEntity.class)
2129+
.withPreviewFeature(DECLARATIVE_SHADOW_VARIABLES)
2130+
.withPhases(localSearchConfig)
2131+
.withEasyScoreCalculatorClass(TestdataMixedEntityEasyScoreCalculator.class);
2132+
var problem = TestdataMixedMultiEntitySolution.generateUninitializedSolution(2, 2, 2);
2133+
assertThatCode(() -> PlannerTestUtils.solve(solverConfig, problem))
2134+
.hasMessageContaining("has no entityClass configured and because there are multiple in the entityClassSet")
2135+
.hasMessageContaining("it cannot be deduced automatically");
2136+
}
2137+
20772138
public static final class MinimizeUnusedEntitiesEasyScoreCalculator
20782139
implements EasyScoreCalculator<Object, SimpleScore> {
20792140

docs/src/modules/ROOT/pages/optimization-algorithms/move-selector-reference.adoc

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,14 +373,26 @@ Advanced configuration:
373373
----
374374
<ruinRecreateMoveSelector>
375375
<minimumRuinedCount>5</minimumRuinedCount>
376-
<maximumRuinedCount>40</maximumRuinedCount>
376+
<maximumRuinedCount>20</maximumRuinedCount>
377+
<entitySelector>
378+
<entityClass>...Lecture</entityClass>
379+
...
380+
</entitySelector>
381+
<variableName>room</variableName>
377382
</ruinRecreateMoveSelector>
378383
----
379384

380385
The `minimumRuinedCount` and `maximumRuinedCount` properties limit the number of entities that are unassigned.
381386
The default values are `5` and `20` respectively, but for large datasets,
382387
it may prove beneficial to increase these values.
383388

389+
The `entitySelector` property specifies which entity should be selected, allowing its values to be ruined and recreated.
390+
In a model with multiple entities, this property is required,
391+
or the solver will fail because it cannot automatically deduce the entity.
392+
393+
When there are multiple basic variables defined in the model, the solver cannot automatically select one of them.
394+
The property `variableName` allows you to specify which variable will be used by the move generator.
395+
384396
[NOTE]
385397
====
386398
`RuinRecreateMove` doesn’t support customizing the construction heuristic that it runs.

0 commit comments

Comments
 (0)