Skip to content

Commit 8f43e8e

Browse files
authored
feat: enable Basic var and List var to coexist for the construction heuristic
This PR allows for the construction of an initial solution using both basic and list variables within the same model. The proposal does not introduce any new configurations for the construction heuristic; rather, it modifies the entity placer definition to accept a list instead of just a single element. There are some limitations concerning the definition of the entity placer. Therefore, the current behavior for a single placer configuration will remain unchanged. However, a new option has been added to define a placer for values (list variable) and a placer for entities (basic variable). By default, the solving process combines the default strategies from both placers. For each list variable placement, we then test all possible placements for the basic variables and select the best combination. It is important to note that the list variables impose some restrictions on the CH settings, limiting it to only defining QueuedValuePlacer. The proposed approach retains these limitations, and the mixed model can only be configured as shown in the previous snippet.
1 parent 8fa8b30 commit 8f43e8e

73 files changed

Lines changed: 2331 additions & 116 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.

benchmark/src/main/resources/benchmark.xsd

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -674,7 +674,7 @@
674674
<xs:element minOccurs="0" name="valueSorterManner" type="tns:valueSorterManner"/>
675675

676676

677-
<xs:choice minOccurs="0">
677+
<xs:choice maxOccurs="unbounded" minOccurs="0">
678678

679679

680680
<xs:element name="queuedEntityPlacer" type="tns:queuedEntityPlacerConfig"/>

core/src/build/revapi-differences.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,23 @@
331331
"new": "class ai.timefold.solver.core.api.score.buildin.simplelong.SimpleLongScore",
332332
"annotation": "@org.jspecify.annotations.NullMarked",
333333
"justification": "@NonNull replaced by @NullMarked"
334+
},
335+
{
336+
"ignore": true,
337+
"code": "java.field.removed",
338+
"old": "field ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicPhaseConfig.entityPlacerConfig",
339+
"justification": "New CH configuration with multiple placers"
340+
},
341+
{
342+
"ignore": true,
343+
"code": "java.annotation.attributeValueChanged",
344+
"old": "class ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicPhaseConfig",
345+
"new": "class ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicPhaseConfig",
346+
"annotationType": "jakarta.xml.bind.annotation.XmlType",
347+
"attribute": "propOrder",
348+
"oldValue": "{\"constructionHeuristicType\", \"entitySorterManner\", \"valueSorterManner\", \"entityPlacerConfig\", \"moveSelectorConfigList\", \"foragerConfig\"}",
349+
"newValue": "{\"constructionHeuristicType\", \"entitySorterManner\", \"valueSorterManner\", \"entityPlacerConfigList\", \"moveSelectorConfigList\", \"foragerConfig\"}",
350+
"justification": "New CH configuration with multiple placers"
334351
}
335352
]
336353
}

core/src/main/java/ai/timefold/solver/core/config/constructionheuristic/ConstructionHeuristicPhaseConfig.java

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package ai.timefold.solver.core.config.constructionheuristic;
22

3+
import java.util.Arrays;
34
import java.util.List;
45
import java.util.function.Consumer;
56

@@ -36,7 +37,7 @@
3637
"constructionHeuristicType",
3738
"entitySorterManner",
3839
"valueSorterManner",
39-
"entityPlacerConfig",
40+
"entityPlacerConfigList",
4041
"moveSelectorConfigList",
4142
"foragerConfig"
4243
})
@@ -56,9 +57,9 @@ public class ConstructionHeuristicPhaseConfig extends PhaseConfig<ConstructionHe
5657
@XmlElement(name = "queuedValuePlacer", type = QueuedValuePlacerConfig.class),
5758
@XmlElement(name = "pooledEntityPlacer", type = PooledEntityPlacerConfig.class)
5859
})
59-
protected EntityPlacerConfig entityPlacerConfig = null;
60+
protected List<EntityPlacerConfig> entityPlacerConfigList = null;
6061

61-
/** Simpler alternative for {@link #entityPlacerConfig}. */
62+
/** Simpler alternative for {@link #entityPlacerConfigList}. */
6263
@XmlElements({
6364
@XmlElement(name = CartesianProductMoveSelectorConfig.XML_ELEMENT_NAME,
6465
type = CartesianProductMoveSelectorConfig.class),
@@ -110,12 +111,35 @@ public void setValueSorterManner(@Nullable ValueSorterManner valueSorterManner)
110111
this.valueSorterManner = valueSorterManner;
111112
}
112113

113-
public @Nullable EntityPlacerConfig getEntityPlacerConfig() {
114-
return entityPlacerConfig;
114+
public List<EntityPlacerConfig> getEntityPlacerConfigList() {
115+
return entityPlacerConfigList;
116+
}
117+
118+
public void setEntityPlacerConfigList(List<EntityPlacerConfig> entityPlacerConfigList) {
119+
this.entityPlacerConfigList = entityPlacerConfigList;
115120
}
116121

117-
public void setEntityPlacerConfig(@Nullable EntityPlacerConfig entityPlacerConfig) {
118-
this.entityPlacerConfig = entityPlacerConfig;
122+
/**
123+
* @deprecated Use {@link #setEntityPlacerConfigList(List)}} instead.
124+
*/
125+
@Deprecated(forRemoval = true, since = "1.22.0")
126+
public void setEntityPlacerConfig(EntityPlacerConfig entityPlacerConfig) {
127+
setEntityPlacerConfigList(List.of(entityPlacerConfig));
128+
}
129+
130+
/**
131+
* @deprecated Use {@link #getEntityPlacerConfigList()} instead.
132+
*/
133+
@Deprecated(forRemoval = true, since = "1.22.0")
134+
public @Nullable EntityPlacerConfig getEntityPlacerConfig() {
135+
if (entityPlacerConfigList == null || entityPlacerConfigList.isEmpty()) {
136+
return null;
137+
}
138+
if (entityPlacerConfigList.size() > 1) {
139+
throw new IllegalStateException(
140+
"Returning a single entity placer configuration is not possible. Maybe use getEntityPlacerConfigList instead.");
141+
}
142+
return entityPlacerConfigList.get(0);
119143
}
120144

121145
public @Nullable List<@NonNull MoveSelectorConfig> getMoveSelectorConfigList() {
@@ -154,8 +178,19 @@ public void setForagerConfig(@Nullable ConstructionHeuristicForagerConfig forage
154178
return this;
155179
}
156180

157-
public @NonNull ConstructionHeuristicPhaseConfig withEntityPlacerConfig(@NonNull EntityPlacerConfig<?> entityPlacerConfig) {
158-
this.entityPlacerConfig = entityPlacerConfig;
181+
public @NonNull ConstructionHeuristicPhaseConfig
182+
withEntityPlacerConfigList(@NonNull EntityPlacerConfig<?>... entityPlacerConfig) {
183+
setEntityPlacerConfigList(Arrays.asList(entityPlacerConfig));
184+
return this;
185+
}
186+
187+
/**
188+
* @deprecated use {@link #withEntityPlacerConfigList(EntityPlacerConfig[])} instead.
189+
*/
190+
@Deprecated(forRemoval = true, since = "1.22.0")
191+
public @NonNull ConstructionHeuristicPhaseConfig
192+
withEntityPlacerConfig(@NonNull EntityPlacerConfig entityPlacerConfig) {
193+
setEntityPlacerConfigList(List.of(entityPlacerConfig));
159194
return this;
160195
}
161196

@@ -180,8 +215,8 @@ public void setForagerConfig(@Nullable ConstructionHeuristicForagerConfig forage
180215
inheritedConfig.getEntitySorterManner());
181216
valueSorterManner = ConfigUtils.inheritOverwritableProperty(valueSorterManner,
182217
inheritedConfig.getValueSorterManner());
183-
setEntityPlacerConfig(ConfigUtils.inheritOverwritableProperty(
184-
getEntityPlacerConfig(), inheritedConfig.getEntityPlacerConfig()));
218+
entityPlacerConfigList = ConfigUtils.inheritMergeableListConfig(
219+
entityPlacerConfigList, inheritedConfig.getEntityPlacerConfigList());
185220
moveSelectorConfigList = ConfigUtils.inheritMergeableListConfig(
186221
moveSelectorConfigList, inheritedConfig.getMoveSelectorConfigList());
187222
foragerConfig = ConfigUtils.inheritConfig(foragerConfig, inheritedConfig.getForagerConfig());
@@ -198,8 +233,8 @@ public void visitReferencedClasses(@NonNull Consumer<Class<?>> classVisitor) {
198233
if (terminationConfig != null) {
199234
terminationConfig.visitReferencedClasses(classVisitor);
200235
}
201-
if (entityPlacerConfig != null) {
202-
entityPlacerConfig.visitReferencedClasses(classVisitor);
236+
if (entityPlacerConfigList != null) {
237+
entityPlacerConfigList.forEach(entityPlacerConfig -> entityPlacerConfig.visitReferencedClasses(classVisitor));
203238
}
204239
if (moveSelectorConfigList != null) {
205240
moveSelectorConfigList.forEach(ms -> ms.visitReferencedClasses(classVisitor));

core/src/main/java/ai/timefold/solver/core/config/constructionheuristic/placer/QueuedEntityPlacerConfig.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
})
3333
public class QueuedEntityPlacerConfig extends EntityPlacerConfig<QueuedEntityPlacerConfig> {
3434

35+
public static final String XML_ELEMENT_NAME = "queuedEntityPlacer";
36+
3537
@XmlElement(name = "entitySelector")
3638
protected EntitySelectorConfig entitySelectorConfig = null;
3739

core/src/main/java/ai/timefold/solver/core/config/constructionheuristic/placer/QueuedValuePlacerConfig.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
})
3232
public class QueuedValuePlacerConfig extends EntityPlacerConfig<QueuedValuePlacerConfig> {
3333

34+
public static final String XML_ELEMENT_NAME = "queuedValuePlacer";
35+
3436
protected Class<?> entityClass = null;
3537

3638
@XmlElement(name = "valueSelector")

core/src/main/java/ai/timefold/solver/core/impl/AbstractFromConfigFactory.java

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import java.util.Collection;
44
import java.util.List;
55
import java.util.Objects;
6-
import java.util.stream.Collectors;
76

87
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
98
import ai.timefold.solver.core.config.AbstractConfig;
@@ -19,7 +18,7 @@ public abstract class AbstractFromConfigFactory<Solution_, Config_ extends Abstr
1918

2019
protected final Config_ config;
2120

22-
public AbstractFromConfigFactory(Config_ config) {
21+
protected AbstractFromConfigFactory(Config_ config) {
2322
this.config = config;
2423
}
2524

@@ -55,23 +54,36 @@ private EntityDescriptor<Solution_> getEntityDescriptorForClass(SolutionDescript
5554
Class<?> entityClass) {
5655
EntityDescriptor<Solution_> entityDescriptor = solutionDescriptor.getEntityDescriptorStrict(entityClass);
5756
if (entityDescriptor == null) {
58-
throw new IllegalArgumentException("The config (" + config
59-
+ ") has an entityClass (" + entityClass + ") that is not a known planning entity.\n"
60-
+ "Check your solver configuration. If that class (" + entityClass.getSimpleName()
61-
+ ") is not in the entityClassSet (" + solutionDescriptor.getEntityClassSet()
62-
+ "), check your @" + PlanningSolution.class.getSimpleName()
63-
+ " implementation's annotated methods too.");
57+
throw new IllegalArgumentException(
58+
"""
59+
The config (%s) has an entityClass (%s) that is not a known planning entity.
60+
Check your solver configuration. If that class (%s) is not in the entityClassSet (%s), check your @%s implementation's annotated methods too."""
61+
.formatted(config, entityClass, entityClass.getSimpleName(), solutionDescriptor.getEntityClassSet(),
62+
PlanningSolution.class.getSimpleName()));
6463
}
6564
return entityDescriptor;
6665
}
6766

6867
protected EntityDescriptor<Solution_> getTheOnlyEntityDescriptor(SolutionDescriptor<Solution_> solutionDescriptor) {
6968
Collection<EntityDescriptor<Solution_>> entityDescriptors = solutionDescriptor.getGenuineEntityDescriptors();
7069
if (entityDescriptors.size() != 1) {
71-
throw new IllegalArgumentException("The config (" + config
72-
+ ") has no entityClass configured and because there are multiple in the entityClassSet ("
73-
+ solutionDescriptor.getEntityClassSet()
74-
+ "), it cannot be deduced automatically.");
70+
throw new IllegalArgumentException(
71+
"The config (%s) has no entityClass configured and because there are multiple in the entityClassSet (%s), it cannot be deduced automatically."
72+
.formatted(config, solutionDescriptor.getEntityClassSet()));
73+
}
74+
return entityDescriptors.iterator().next();
75+
}
76+
77+
protected EntityDescriptor<Solution_>
78+
getTheOnlyEntityDescriptorWithBasicVariables(SolutionDescriptor<Solution_> solutionDescriptor) {
79+
Collection<EntityDescriptor<Solution_>> entityDescriptors = solutionDescriptor.getGenuineEntityDescriptors()
80+
.stream()
81+
.filter(EntityDescriptor::hasAnyGenuineBasicVariables)
82+
.toList();
83+
if (entityDescriptors.size() != 1) {
84+
throw new IllegalArgumentException(
85+
"The config (%s) has no entityClass configured and because there are multiple in the entityClassSet (%s) defining basic variables, it cannot be deduced automatically."
86+
.formatted(config, solutionDescriptor.getEntityClassSet()));
7587
}
7688
return entityDescriptors.iterator().next();
7789
}
@@ -87,11 +99,11 @@ protected GenuineVariableDescriptor<Solution_> getVariableDescriptorForName(Enti
8799
String variableName) {
88100
GenuineVariableDescriptor<Solution_> variableDescriptor = entityDescriptor.getGenuineVariableDescriptor(variableName);
89101
if (variableDescriptor == null) {
90-
throw new IllegalArgumentException("The config (" + config
91-
+ ") has a variableName (" + variableName
92-
+ ") which is not a valid planning variable on entityClass ("
93-
+ entityDescriptor.getEntityClass() + ").\n"
94-
+ entityDescriptor.buildInvalidVariableNameExceptionMessage(variableName));
102+
throw new IllegalArgumentException(
103+
"""
104+
The config (%s) has a variableName (%s) which is not a valid planning variable on entityClass (%s).
105+
%s""".formatted(config, variableName, entityDescriptor.getEntityClass(),
106+
entityDescriptor.buildInvalidVariableNameExceptionMessage(variableName)));
95107
}
96108
return variableDescriptor;
97109
}
@@ -100,11 +112,10 @@ protected GenuineVariableDescriptor<Solution_> getTheOnlyVariableDescriptor(Enti
100112
List<GenuineVariableDescriptor<Solution_>> variableDescriptorList =
101113
entityDescriptor.getGenuineVariableDescriptorList();
102114
if (variableDescriptorList.size() != 1) {
103-
throw new IllegalArgumentException("The config (" + config
104-
+ ") has no configured variableName for entityClass (" + entityDescriptor.getEntityClass()
105-
+ ") and because there are multiple variableNames ("
106-
+ entityDescriptor.getGenuineVariableNameSet()
107-
+ "), it cannot be deduced automatically.");
115+
throw new IllegalArgumentException(
116+
"The config (%s) has no configured variableName for entityClass (%s) and because there are multiple variableNames (%s), it cannot be deduced automatically."
117+
.formatted(config, entityDescriptor.getEntityClass(),
118+
entityDescriptor.getGenuineVariableNameSet()));
108119
}
109120
return variableDescriptorList.iterator().next();
110121
}
@@ -122,10 +133,10 @@ protected List<GenuineVariableDescriptor<Solution_>> deduceVariableDescriptorLis
122133
.map(variableNameInclude -> variableDescriptorList.stream()
123134
.filter(variableDescriptor -> variableDescriptor.getVariableName().equals(variableNameInclude))
124135
.findFirst()
125-
.orElseThrow(() -> new IllegalArgumentException("The config (" + config
126-
+ ") has a variableNameInclude (" + variableNameInclude
127-
+ ") which does not exist in the entity (" + entityDescriptor.getEntityClass()
128-
+ ")'s variableDescriptorList (" + variableDescriptorList + ").")))
129-
.collect(Collectors.toList());
136+
.orElseThrow(() -> new IllegalArgumentException(
137+
"The config (%s) has a variableNameInclude (%s) which does not exist in the entity (%s)'s variableDescriptorList (%s)."
138+
.formatted(config, variableNameInclude, entityDescriptor.getEntityClass(),
139+
variableDescriptorList))))
140+
.toList();
130141
}
131142
}

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,14 @@ public void solve(SolverScope<Solution_> solverScope) {
5757
phaseStarted(phaseScope);
5858

5959
var solutionDescriptor = solverScope.getSolutionDescriptor();
60-
var listVariableDescriptor = solutionDescriptor.getListVariableDescriptor();
61-
var hasListVariable = listVariableDescriptor != null;
60+
var hasListVariable = solutionDescriptor.hasListVariable();
6261
var maxStepCount = -1;
6362
if (hasListVariable) {
6463
// In case of list variable with support for unassigned values, the placer will iterate indefinitely.
6564
// (When it exhausts all values, it will start over from the beginning.)
6665
// To prevent that, we need to limit the number of steps to the number of unassigned values.
6766
var workingSolution = phaseScope.getWorkingSolution();
68-
maxStepCount = listVariableDescriptor.countUnassigned(workingSolution);
67+
maxStepCount = solutionDescriptor.getListVariableDescriptor().countUnassigned(workingSolution);
6968
}
7069

7170
TerminationStatus earlyTerminationStatus = null;

0 commit comments

Comments
 (0)