Skip to content

Commit fdeb06a

Browse files
authored
fix: forbid pinning uninitialized entities when unassigned not allowed (#821)
1 parent a649452 commit fdeb06a

File tree

13 files changed

+725
-468
lines changed

13 files changed

+725
-468
lines changed

core/src/main/java/ai/timefold/solver/core/api/domain/variable/PlanningListVariable.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,6 @@
3838
* the <em>order</em> in which customers are visited and tasks are being worked on matters. Also, each customer
3939
* must be visited <em>once</em> and each task must be completed by <em>exactly one</em> employee.
4040
*
41-
* <p>
42-
* <strong>Overconstrained planning is currently not supported for list variables.</strong>
43-
*
4441
* @see PlanningPin
4542
* @see PlanningPinToIndex
4643
*/

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/ListVariableDescriptor.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,6 @@ public Class<?> getElementType() {
101101

102102
public int countUnassigned(Solution_ solution) {
103103
var valueCount = new MutableLong(getValueRangeSize(solution, null));
104-
var entityDescriptor = getEntityDescriptor();
105104
var solutionDescriptor = entityDescriptor.getSolutionDescriptor();
106105
solutionDescriptor.visitEntitiesByEntityClass(solution,
107106
entityDescriptor.getEntityClass(), entity -> {
@@ -113,11 +112,12 @@ public int countUnassigned(Solution_ solution) {
113112
}
114113

115114
public InverseRelationShadowVariableDescriptor<Solution_> getInverseRelationShadowVariableDescriptor() {
116-
var entityDescriptor = getEntityDescriptor().getSolutionDescriptor().findEntityDescriptor(getElementType());
117-
if (entityDescriptor == null) {
115+
var inverseRelationEntityDescriptor =
116+
getEntityDescriptor().getSolutionDescriptor().findEntityDescriptor(getElementType());
117+
if (inverseRelationEntityDescriptor == null) {
118118
return null;
119119
}
120-
var applicableShadowDescriptors = entityDescriptor.getShadowVariableDescriptors()
120+
var applicableShadowDescriptors = inverseRelationEntityDescriptor.getShadowVariableDescriptors()
121121
.stream()
122122
.filter(f -> f instanceof InverseRelationShadowVariableDescriptor<Solution_> inverseRelationShadowVariableDescriptor
123123
&& Objects.equals(inverseRelationShadowVariableDescriptor.getSourceVariableDescriptorList().get(0),
@@ -131,7 +131,7 @@ public InverseRelationShadowVariableDescriptor<Solution_> getInverseRelationShad
131131
"""
132132
Instances of entityClass (%s) may be used in list variable (%s), but the class has more than one @%s-annotated field (%s).
133133
Remove the annotations from all but one field."""
134-
.formatted(entityDescriptor.getEntityClass().getCanonicalName(),
134+
.formatted(inverseRelationEntityDescriptor.getEntityClass().getCanonicalName(),
135135
getSimpleEntityAndVariableName(),
136136
InverseRelationShadowVariable.class.getSimpleName(),
137137
applicableShadowDescriptors.stream()
@@ -150,7 +150,8 @@ Instances of entityClass (%s) may be used in list variable (%s), but the class h
150150
public List<Object> getValue(Object entity) {
151151
Object value = super.getValue(entity);
152152
if (value == null) {
153-
throw new IllegalStateException("The planning list variable (" + this + ") of entity (" + entity + ") is null.");
153+
throw new IllegalStateException("The planning list variable (%s) of entity (%s) is null."
154+
.formatted(this, entity));
154155
}
155156
return (List<Object>) value;
156157
}

core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,8 @@ public void setWorkingSolution(Solution_ workingSolution) {
223223
solutionDescriptor.visitAllProblemFacts(workingSolution, visitor);
224224
}
225225
// This visits all the entities, applying the visitor if non-null.
226+
Consumer<Object> entityValidator = entity -> scoreDirectorFactory.validateEntity(this, entity);
227+
visitor = visitor == null ? entityValidator : visitor.andThen(entityValidator);
226228
var initializationStatistics = solutionDescriptor.computeInitializationStatistics(workingSolution, visitor);
227229
setWorkingEntityListDirty();
228230

core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirectorFactory.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
44
import ai.timefold.solver.core.api.score.Score;
5+
import ai.timefold.solver.core.api.score.director.ScoreDirector;
56
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;
7+
import ai.timefold.solver.core.impl.domain.variable.descriptor.BasicVariableDescriptor;
8+
import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor;
69
import ai.timefold.solver.core.impl.score.definition.ScoreDefinition;
710
import ai.timefold.solver.core.impl.score.trend.InitializingScoreTrend;
811

@@ -21,7 +24,8 @@ public abstract class AbstractScoreDirectorFactory<Solution_, Score_ extends Sco
2124

2225
protected final transient Logger logger = LoggerFactory.getLogger(getClass());
2326

24-
protected SolutionDescriptor<Solution_> solutionDescriptor;
27+
protected final SolutionDescriptor<Solution_> solutionDescriptor;
28+
protected final ListVariableDescriptor<Solution_> listVariableDescriptor;
2529

2630
protected InitializingScoreTrend initializingScoreTrend;
2731

@@ -32,6 +36,7 @@ public abstract class AbstractScoreDirectorFactory<Solution_, Score_ extends Sco
3236

3337
public AbstractScoreDirectorFactory(SolutionDescriptor<Solution_> solutionDescriptor) {
3438
this.solutionDescriptor = solutionDescriptor;
39+
this.listVariableDescriptor = solutionDescriptor.getListVariableDescriptor();
3540
}
3641

3742
@Override
@@ -108,4 +113,25 @@ public void assertScoreFromScratch(Solution_ solution) {
108113
}
109114
}
110115

116+
public void validateEntity(ScoreDirector<Solution_> scoreDirector, Object entity) {
117+
if (listVariableDescriptor == null) { // Only basic variables.
118+
var entityDescriptor = solutionDescriptor.findEntityDescriptorOrFail(entity.getClass());
119+
if (entityDescriptor.isMovable(scoreDirector, entity)) {
120+
return;
121+
}
122+
for (var variableDescriptor : entityDescriptor.getGenuineVariableDescriptorList()) {
123+
var basicVariableDescriptor = (BasicVariableDescriptor<Solution_>) variableDescriptor;
124+
if (basicVariableDescriptor.allowsUnassigned()) {
125+
continue;
126+
}
127+
var value = basicVariableDescriptor.getValue(entity);
128+
if (value == null) {
129+
throw new IllegalStateException(
130+
"The entity (%s) has a variable (%s) pinned to null, even though unassigned values are not allowed."
131+
.formatted(entity, basicVariableDescriptor.getVariableName()));
132+
}
133+
}
134+
}
135+
}
136+
111137
}

0 commit comments

Comments
 (0)