Skip to content

Commit ce80c11

Browse files
chore: unassign on inconsistent graph
1 parent 855d722 commit ce80c11

11 files changed

Lines changed: 132 additions & 10 deletions

File tree

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.VariableDescriptor;
46
import ai.timefold.solver.core.impl.domain.variable.supply.Supply;
57
import ai.timefold.solver.core.preview.api.domain.metamodel.VariableMetaModel;
@@ -37,4 +39,8 @@ public void afterVariableChanged(VariableMetaModel<Solution_, ?, ?> variableMeta
3739
public boolean updateVariables() {
3840
return graph.updateChanged();
3941
}
42+
43+
public List<Object> getInconsistentEntities() {
44+
return graph.getInconsistentEntities();
45+
}
4046
}

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: 3 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.preview.api.domain.metamodel.VariableMetaModel;
46

57
public sealed interface VariableReferenceGraph
@@ -34,4 +36,5 @@ public sealed interface VariableReferenceGraph
3436
*/
3537
void afterVariableChanged(VariableMetaModel<?, ?, ?> variableReference, Object entity);
3638

39+
List<Object> getInconsistentEntities();
3740
}

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/listener/support/VariableListenerSupport.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,14 @@ public boolean triggerVariableListenersInNotificationQueues() {
374374
return true;
375375
}
376376

377+
public List<Object> getInconsistentEntities() {
378+
if (shadowVariableSession == null) {
379+
throw new IllegalStateException(
380+
"The shadowVariableSession is null. A solution without shadow variables cannot be inconsistent.");
381+
}
382+
return shadowVariableSession.getInconsistentEntities();
383+
}
384+
377385
/**
378386
* Triggers all cascading update shadow variable user-logic.
379387
*/

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,45 @@ protected void afterSetWorkingSolution() {
344344
// Do nothing
345345
}
346346

347+
public void unassignInconsistentEntities() {
348+
var inconsistentEntities = variableListenerSupport.getInconsistentEntities();
349+
if (listVariableStateSupply != null) {
350+
var listVariableDescriptor = listVariableStateSupply.getSourceVariableDescriptor();
351+
var listElementClass = listVariableStateSupply.getSourceVariableDescriptor().getElementType();
352+
for (var inconsistentEntity : inconsistentEntities) {
353+
if (listElementClass.isInstance(inconsistentEntity)) {
354+
var inverse = Objects.requireNonNull(listVariableStateSupply.getInverseSingleton(inconsistentEntity));
355+
int index = Objects.requireNonNull(listVariableStateSupply.getIndex(inconsistentEntity));
356+
beforeListVariableElementUnassigned(listVariableDescriptor, inconsistentEntity);
357+
beforeListVariableChanged(listVariableDescriptor, inverse, index, index + 1);
358+
listVariableDescriptor.removeElement(inverse, index);
359+
afterListVariableChanged(listVariableDescriptor, inverse, index, index);
360+
afterListVariableElementUnassigned(listVariableDescriptor, inconsistentEntity);
361+
triggerVariableListeners();
362+
}
363+
// Unassign any normal @PlanningVariable on the entity too
364+
unassignPlainEntity(inconsistentEntity);
365+
}
366+
} else {
367+
for (var inconsistentEntity : inconsistentEntities) {
368+
unassignPlainEntity(inconsistentEntity);
369+
}
370+
}
371+
}
372+
373+
private void unassignPlainEntity(Object inconsistentEntity) {
374+
var entityDescriptor = getSolutionDescriptor().findEntityDescriptor(inconsistentEntity.getClass());
375+
if (entityDescriptor == null) {
376+
throw new IllegalStateException("Object (%s) is not an entity but is inconsistent".formatted(inconsistentEntity));
377+
}
378+
for (var genuineVariableDescriptor : entityDescriptor.getGenuineVariableDescriptorList()) {
379+
beforeVariableChanged(genuineVariableDescriptor, inconsistentEntity);
380+
genuineVariableDescriptor.setValue(inconsistentEntity, null);
381+
afterVariableChanged(genuineVariableDescriptor, inconsistentEntity);
382+
}
383+
triggerVariableListeners();
384+
}
385+
347386
@Override
348387
public void setMoveRepository(@Nullable MoveRepository<Solution_> moveRepository) {
349388
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
@@ -170,6 +170,8 @@ default InnerScore<Score_> executeTemporaryMove(Move<Solution_> move, boolean as
170170

171171
boolean ignoreInconsistentSolutions();
172172

173+
void unassignInconsistentEntities();
174+
173175
/**
174176
* @return never null
175177
*/
@@ -363,5 +365,4 @@ default void afterEntityRemoved(Object entity) {
363365
void beforeProblemFactRemoved(Object problemFact);
364366

365367
void afterProblemFactRemoved(Object problemFact);
366-
367368
}

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

Lines changed: 12 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,20 @@ 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("The initial solution passed to the solver is inconsistent even after unassigning involved entities.");
64+
}
5765
}
5866
var score = innerScore.raw();
5967
solverScope.setBestScore(innerScore);

0 commit comments

Comments
 (0)