Skip to content

Commit c6c7211

Browse files
chore: unassign if solution is inconsistent (#2443)
1 parent 7f0c1ac commit c6c7211

12 files changed

Lines changed: 198 additions & 10 deletions

File tree

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,14 @@ public boolean updateShadowVariables() {
419419
dirty = false;
420420
}
421421

422+
public List<Object> getInconsistentEntities() {
423+
if (shadowVariableSession == null) {
424+
throw new IllegalStateException(
425+
"Impossible state: The shadowVariableSession is null. A solution without shadow variables cannot be inconsistent.");
426+
}
427+
return shadowVariableSession.getInconsistentEntities();
428+
}
429+
422430
/**
423431
* Triggers all cascading update shadow variable user-logic.
424432
*/

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/DefaultShadowVariableSession.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package ai.timefold.solver.core.impl.domain.variable.declarative;
22

3+
import java.util.List;
4+
35
import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor;
46
import ai.timefold.solver.core.impl.domain.variable.descriptor.VariableDescriptor;
57
import ai.timefold.solver.core.impl.domain.variable.supply.Supply;
@@ -50,4 +52,8 @@ public void afterListVariableChanged(ListVariableDescriptor<Solution_> variableD
5052
public boolean updateVariables() {
5153
return graph.updateChanged();
5254
}
55+
56+
public List<Object> getInconsistentEntities() {
57+
return graph.getInconsistentEntities();
58+
}
5359
}

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/DefaultVariableReferenceGraph.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import java.util.List;
77
import java.util.function.IntFunction;
88

9+
import ai.timefold.solver.core.impl.util.LinkedIdentityHashSet;
10+
911
import org.jspecify.annotations.NonNull;
1012

1113
final class DefaultVariableReferenceGraph<Solution_> extends AbstractVariableReferenceGraph<Solution_, BitSet> {
@@ -71,4 +73,20 @@ public void setUnknownInconsistencyValues() {
7173
graph.commitChanges(changeTracker);
7274
affectedEntitiesUpdater.setUnknownInconsistencyValues();
7375
}
76+
77+
@Override
78+
public List<Object> getInconsistentEntities() {
79+
var out = new LinkedIdentityHashSet<>();
80+
var graphTrackingInconsistentEntities = new DefaultTopologicalOrderGraph(this.nodeTopologicalOrders.length);
81+
graph.forEachEdge(graphTrackingInconsistentEntities::addEdge);
82+
graphTrackingInconsistentEntities.commitChanges(new BitSet());
83+
var loopedComponentList = graphTrackingInconsistentEntities.getLoopedComponentList();
84+
for (var loopedComponent : loopedComponentList) {
85+
for (var nodeId : loopedComponent) {
86+
var node = this.nodeList.get(nodeId);
87+
out.add(node.entity());
88+
}
89+
}
90+
return new ArrayList<>(out);
91+
}
7492
}

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/EmptyVariableReferenceGraph.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package ai.timefold.solver.core.impl.domain.variable.declarative;
22

3+
import java.util.Collections;
4+
import java.util.List;
5+
36
import ai.timefold.solver.core.preview.api.domain.metamodel.VariableMetaModel;
47

58
final class EmptyVariableReferenceGraph implements VariableReferenceGraph {
@@ -22,6 +25,11 @@ public void afterVariableChanged(VariableMetaModel<?, ?, ?> variableReference, O
2225
// No need to do anything.
2326
}
2427

28+
@Override
29+
public List<Object> getInconsistentEntities() {
30+
return Collections.emptyList();
31+
}
32+
2533
@Override
2634
public String toString() {
2735
return "{}";

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/FixedVariableReferenceGraph.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package ai.timefold.solver.core.impl.domain.variable.declarative;
22

33
import java.util.BitSet;
4+
import java.util.Collections;
5+
import java.util.List;
46
import java.util.PriorityQueue;
57
import java.util.Spliterators;
68
import java.util.function.IntFunction;
@@ -106,4 +108,9 @@ boolean innerUpdateChanged() {
106108
isChanged.clear();
107109
return true;
108110
}
111+
112+
@Override
113+
public List<Object> getInconsistentEntities() {
114+
return Collections.emptyList();
115+
}
109116
}

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/SingleDirectionalParentVariableReferenceGraph.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.util.ArrayList;
44
import java.util.Arrays;
5+
import java.util.Collections;
56
import java.util.Comparator;
67
import java.util.HashSet;
78
import java.util.IdentityHashMap;
@@ -136,4 +137,9 @@ public void afterVariableChanged(VariableMetaModel<?, ?, ?> variableReference, O
136137
}
137138
}
138139

140+
@Override
141+
public List<Object> getInconsistentEntities() {
142+
return Collections.emptyList();
143+
}
144+
139145
}

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/declarative/VariableReferenceGraph.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,5 @@ default void afterListVariableChanged(VariableMetaModel<?, ?, ?> variableReferen
7070
// Most graphs do not have edges that depend on a list variable's contents.
7171
}
7272

73+
List<Object> getInconsistentEntities();
7374
}

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,59 @@ protected void afterSetWorkingSolution() {
343343
// Do nothing
344344
}
345345

346+
public void unassignInconsistentEntities() {
347+
var inconsistentEntities = variableListenerSupport.getInconsistentEntities();
348+
if (listVariableStateSupply != null) {
349+
var listVariableDescriptor = listVariableStateSupply.getSourceVariableDescriptor();
350+
var listElementClass = listVariableStateSupply.getSourceVariableDescriptor().getElementType();
351+
for (var inconsistentEntity : inconsistentEntities) {
352+
if (listElementClass.isInstance(inconsistentEntity)) {
353+
var inverse = Objects.requireNonNull(listVariableStateSupply.getInverseSingleton(inconsistentEntity));
354+
int index = Objects.requireNonNull(listVariableStateSupply.getIndex(inconsistentEntity));
355+
356+
if (listVariableDescriptor.isElementPinned(Objects.requireNonNull(workingSolution), inverse, index)) {
357+
throw new IllegalStateException("""
358+
Entity (%s) is pinned while involved in a dependency loop.
359+
This creates an unresolvable inconsistency.""".formatted(inconsistentEntity));
360+
}
361+
362+
beforeListVariableElementUnassigned(listVariableDescriptor, inconsistentEntity);
363+
beforeListVariableChanged(listVariableDescriptor, inverse, index, index + 1);
364+
listVariableDescriptor.removeElement(inverse, index);
365+
afterListVariableChanged(listVariableDescriptor, inverse, index, index);
366+
afterListVariableElementUnassigned(listVariableDescriptor, inconsistentEntity);
367+
triggerVariableListeners();
368+
}
369+
// Unassign any normal @PlanningVariable on the entity too
370+
unassignPlainEntity(inconsistentEntity);
371+
}
372+
} else {
373+
for (var inconsistentEntity : inconsistentEntities) {
374+
unassignPlainEntity(inconsistentEntity);
375+
}
376+
}
377+
}
378+
379+
private void unassignPlainEntity(Object inconsistentEntity) {
380+
var entityDescriptor = getSolutionDescriptor().findEntityDescriptor(inconsistentEntity.getClass());
381+
if (entityDescriptor == null) {
382+
throw new IllegalStateException(
383+
"Impossible state: Object (%s) is not an entity but is inconsistent".formatted(inconsistentEntity));
384+
}
385+
if (entityDescriptor.isGenuine()
386+
&& !entityDescriptor.isMovable(Objects.requireNonNull(workingSolution), inconsistentEntity)) {
387+
throw new IllegalStateException("""
388+
Entity (%s) is pinned while involved in a dependency loop.
389+
This creates an unresolvable inconsistency.""".formatted(inconsistentEntity));
390+
}
391+
for (var genuineVariableDescriptor : entityDescriptor.getGenuineVariableDescriptorList()) {
392+
beforeVariableChanged(genuineVariableDescriptor, inconsistentEntity);
393+
genuineVariableDescriptor.setValue(inconsistentEntity, null);
394+
afterVariableChanged(genuineVariableDescriptor, inconsistentEntity);
395+
}
396+
triggerVariableListeners();
397+
}
398+
346399
@Override
347400
public void setMoveRepository(@Nullable MoveRepository<Solution_> moveRepository) {
348401
if (this.moveRepository == moveRepository) { // Prevent double initialization

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ default InnerScore<Score_> executeTemporaryMove(Move<Solution_> move, boolean as
169169

170170
boolean ignoreInconsistentSolutions();
171171

172+
void unassignInconsistentEntities();
173+
172174
/**
173175
* @return never null
174176
*/
@@ -372,5 +374,4 @@ default void afterEntityRemoved(Object entity) {
372374
void beforeProblemFactRemoved(Object problemFact);
373375

374376
void afterProblemFactRemoved(Object problemFact);
375-
376377
}

core/src/main/java/ai/timefold/solver/core/impl/solver/recaller/BestSolutionRecaller.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,17 @@
1111
import ai.timefold.solver.core.impl.solver.event.SolverEventSupport;
1212
import ai.timefold.solver.core.impl.solver.scope.SolverScope;
1313

14+
import org.slf4j.Logger;
15+
import org.slf4j.LoggerFactory;
16+
1417
/**
1518
* Remembers the {@link PlanningSolution best solution} that a {@link Solver} encounters.
1619
*
1720
* @param <Solution_> the solution type, the class with the {@link PlanningSolution} annotation
1821
*/
1922
public class BestSolutionRecaller<Solution_> extends PhaseLifecycleListenerAdapter<Solution_> {
2023

24+
private static final Logger log = LoggerFactory.getLogger(BestSolutionRecaller.class);
2125
protected boolean assertInitialScoreFromScratch = false;
2226
protected boolean assertShadowVariablesAreNotStale = false;
2327
protected boolean assertBestScoreIsUnmodified = false;
@@ -44,16 +48,21 @@ public void setSolverEventSupport(SolverEventSupport<Solution_> solverEventSuppo
4448
// Worker methods
4549
// ************************************************************************
4650

47-
@SuppressWarnings({ "unchecked", "rawtypes" })
4851
@Override
52+
@SuppressWarnings("unchecked")
4953
public void solvingStarted(SolverScope<Solution_> solverScope) {
5054
// Starting bestSolution is already set by Solver.solve(Solution)
5155
var scoreDirector = solverScope.getScoreDirector();
56+
@SuppressWarnings("rawtypes")
5257
InnerScore innerScore = scoreDirector.calculateScore();
5358
if (innerScore.isInvalid()) {
54-
throw new IllegalStateException(
55-
"The initial solution passed to the solver (%s) is invalid because it has dependency loops."
56-
.formatted(solverScope.getWorkingSolution()));
59+
log.warn("The initial solution passed to the solver is inconsistent. Unassigning involved entities.");
60+
scoreDirector.unassignInconsistentEntities();
61+
innerScore = scoreDirector.calculateScore();
62+
if (innerScore.isInvalid()) {
63+
throw new IllegalStateException(
64+
"The initial solution passed to the solver is inconsistent even after unassigning involved entities.");
65+
}
5766
}
5867
var score = innerScore.raw();
5968
solverScope.setBestScore(innerScore);

0 commit comments

Comments
 (0)