Skip to content

Commit 7f0c1ac

Browse files
perf: add ability to ignore inconsistent solutions (#2396)
1 parent 058c2e6 commit 7f0c1ac

41 files changed

Lines changed: 591 additions & 72 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.

core/src/main/java/ai/timefold/solver/core/enterprise/TimefoldSolverEnterpriseService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ static <T> T loadOrDefault(Function<TimefoldSolverEnterpriseService, T> builder,
178178
}
179179
}
180180

181-
TopologicalOrderGraph buildTopologyGraph(int size);
181+
TopologicalOrderGraph buildTopologyGraph(int size, boolean ignoreInconsistentSolutions);
182182

183183
/**
184184
* Will create new classes that apply node-sharing to the given {@link ConstraintProvider}.

core/src/main/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptor.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
import ai.timefold.solver.core.impl.domain.solution.cloner.gizmo.GizmoSolutionCloner;
5858
import ai.timefold.solver.core.impl.domain.solution.cloner.gizmo.GizmoSolutionClonerFactory;
5959
import ai.timefold.solver.core.impl.domain.variable.declarative.DeclarativeShadowVariableDescriptor;
60+
import ai.timefold.solver.core.impl.domain.variable.declarative.ShadowVariablesInconsistentVariableDescriptor;
6061
import ai.timefold.solver.core.impl.domain.variable.descriptor.BasicVariableDescriptor;
6162
import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor;
6263
import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor;
@@ -986,6 +987,13 @@ public List<DeclarativeShadowVariableDescriptor<Solution_>> getDeclarativeShadow
986987
return declarativeShadowVariableDescriptorList;
987988
}
988989

990+
public boolean hasAnyShadowVariablesInconsistentMember() {
991+
return entityDescriptorMap.values().stream()
992+
.flatMap(entityDescriptor -> entityDescriptor.getShadowVariableDescriptors().stream())
993+
.anyMatch(
994+
shadowVariableDescriptor -> shadowVariableDescriptor instanceof ShadowVariablesInconsistentVariableDescriptor<Solution_>);
995+
}
996+
989997
public Stream<Object> extractAllEntitiesStream(Solution_ solution) {
990998
var stream = Stream.empty();
991999
for (var memberAccessor : entityMemberAccessorMap.values()) {
@@ -1051,5 +1059,4 @@ public <Score_ extends Score<Score_>> void setScore(Solution_ solution, Score_ s
10511059
public String toString() {
10521060
return "%s(%s)".formatted(getClass().getSimpleName(), solutionClass.getName());
10531061
}
1054-
10551062
}

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ public static <Solution_> ShadowVariableSupport<Solution_> create(InnerScoreDire
7171
private final List<ListVariableChangeHandler<Solution_>> listVariableChangeHandlerList;
7272

7373
private boolean dirty = false;
74+
private boolean updateSuccessful = true;
7475
@Nullable
7576
private DefaultShadowVariableSession<Solution_> shadowVariableSession = null;
7677
private ConsistencyTracker<Solution_> consistencyTracker = new ConsistencyTracker<>();
@@ -392,12 +393,12 @@ public void afterListVariableChanged(ListVariableDescriptor<Solution_> variableD
392393
return scoreDirector;
393394
}
394395

395-
public void updateShadowVariables() {
396+
public boolean updateShadowVariables() {
396397
if (!dirty) {
397398
// Shortcut in case the trigger is called multiple times in a row,
398399
// without any notifications inbetween.
399400
// This is better than trying to ensure that the situation never ever occurs.
400-
return;
401+
return updateSuccessful;
401402
}
402403
if (listVariableDescriptor != null) {
403404
// If there is no cascade, skip the whole thing.
@@ -409,7 +410,11 @@ public void updateShadowVariables() {
409410
listVariableChangeList.clear();
410411
}
411412
if (shadowVariableSession != null) {
412-
shadowVariableSession.updateVariables();
413+
if (!shadowVariableSession.updateVariables()) {
414+
updateSuccessful = false;
415+
notificationQueuesAreEmpty = true;
416+
return false;
417+
}
413418
}
414419
dirty = false;
415420
}
@@ -490,9 +495,9 @@ private void cascadeUnassignedValues(
490495
*
491496
* @param workingSolution working solution
492497
*/
493-
public void forceUpdateAllShadowVariables(Solution_ workingSolution) {
498+
public boolean forceUpdateAllShadowVariables(Solution_ workingSolution) {
494499
scoreDirector.getSolutionDescriptor().visitAllEntities(workingSolution, this::simulateGenuineVariableChange);
495-
updateShadowVariables();
500+
return updateShadowVariables();
496501
}
497502

498503
/**

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public void updateShadowVariables(Class<Solution_> solutionClass, Object... enti
7373
SolutionDescriptor.buildSolutionDescriptor(Objects.requireNonNull(solutionClass),
7474
entityClassList.toArray(new Class<?>[0]));
7575
// No solution, we trigger all supported events manually
76-
var session = InternalShadowVariableSession.build(solutionDescriptor, entities);
76+
var session = InternalShadowVariableSession.build(solutionDescriptor, false, entities);
7777
// Update all built-in shadow variables
7878
var listVariableDescriptor = solutionDescriptor.getListVariableDescriptor();
7979
if (listVariableDescriptor == null) {
@@ -90,12 +90,14 @@ private record InternalShadowVariableSession<Solution_>(SolutionDescriptor<Solut
9090

9191
public static <Solution_> InternalShadowVariableSession<Solution_> build(
9292
SolutionDescriptor<Solution_> solutionDescriptor,
93+
boolean ignoreInconsistentSolutions,
9394
Object... entities) {
9495
return new InternalShadowVariableSession<>(solutionDescriptor,
9596
DefaultShadowVariableSessionFactory.buildGraph(
9697
new DefaultShadowVariableSessionFactory.GraphDescriptor<>(solutionDescriptor,
9798
ChangedVariableNotifier.empty(), entities)
98-
.assertingNoReferencedMissingEntities()));
99+
.assertingNoReferencedMissingEntities(),
100+
ignoreInconsistentSolutions));
99101
}
100102

101103
/**
@@ -298,7 +300,7 @@ public void setWorkingSolutionWithoutUpdatingShadows(Solution_ workingSolution)
298300
}
299301

300302
@Override
301-
public InnerScore<Score_> calculateScore() {
303+
public InnerScore<Score_> innerCalculateScore() {
302304
throw new UnsupportedOperationException();
303305
}
304306

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public abstract sealed class AbstractVariableReferenceGraph<Solution_, ChangeTra
8686
* and {@link #afterVariableChanged(VariableMetaModel, Object)}
8787
* can short circuit.
8888
*/
89-
abstract void innerUpdateChanged();
89+
abstract boolean innerUpdateChanged();
9090

9191
/**
9292
* Called when any non-declarative source variable for the
@@ -97,10 +97,11 @@ public abstract sealed class AbstractVariableReferenceGraph<Solution_, ChangeTra
9797
abstract void markChanged(GraphNode<Solution_> changed);
9898

9999
@Override
100-
public final void updateChanged() {
100+
public final boolean updateChanged() {
101101
isUpdating = true;
102-
innerUpdateChanged();
102+
var success = innerUpdateChanged();
103103
isUpdating = false;
104+
return success;
104105
}
105106

106107
private BaseTopologicalOrderGraph.NodeTopologicalOrder[] buildNodeTopologicalOrderArray(BaseTopologicalOrderGraph graph,

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

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,15 @@ final class AffectedEntitiesUpdater<Solution_>
2121
// Internal state; expensive to create, therefore we reuse.
2222
private final LoopedTracker loopedTracker;
2323
private final BitSet visited;
24+
private final boolean ignoreInconsistentSolutions;
2425
private final PriorityQueue<BaseTopologicalOrderGraph.NodeTopologicalOrder> changeQueue;
26+
private boolean consistencyProcessed;
2527

2628
AffectedEntitiesUpdater(BaseTopologicalOrderGraph graph, List<GraphNode<Solution_>> nodeList,
2729
BaseTopologicalOrderGraph.NodeTopologicalOrder[] nodeTopologicalOrders,
2830
Function<Object, List<GraphNode<Solution_>>> entityToContainingNode,
29-
int entityCount, ChangedVariableNotifier<Solution_> changedVariableNotifier) {
31+
int entityCount, ChangedVariableNotifier<Solution_> changedVariableNotifier,
32+
boolean ignoreInconsistentSolutions) {
3033
this.graph = graph;
3134
this.nodeList = nodeList;
3235
this.nodeTopologicalOrders = nodeTopologicalOrders;
@@ -36,6 +39,8 @@ final class AffectedEntitiesUpdater<Solution_>
3639
createNodeToEntityNodes(entityCount, nodeList, entityToContainingNode));
3740
this.visited = new BitSet(instanceCount);
3841
this.changeQueue = new PriorityQueue<>(instanceCount);
42+
this.ignoreInconsistentSolutions = ignoreInconsistentSolutions;
43+
this.consistencyProcessed = false;
3944
}
4045

4146
static <Solution_> int[][] createNodeToEntityNodes(int entityCount,
@@ -98,6 +103,7 @@ public void accept(BitSet changed) {
98103

99104
// Prepare for the next time updateChanged() is called.
100105
// No need to clear changeQueue, as that already finishes empty.
106+
consistencyProcessed = true;
101107
loopedTracker.clear();
102108
visited.clear();
103109
}
@@ -129,19 +135,22 @@ private boolean updateEntityShadowVariables(GraphNode<Solution_> entityVariable,
129135

130136
// Do not need to update anyChanged here; the graph already marked
131137
// all nodes whose looped status changed for us
132-
var groupEntities = shadowVariableReferences.get(0).groupEntities();
133-
var groupEntityIds = entityVariable.groupEntityIds();
134138

135-
if (groupEntities != null) {
136-
for (var i = 0; i < groupEntityIds.length; i++) {
137-
var groupEntity = groupEntities[i];
138-
var groupEntityId = groupEntityIds[i];
139+
if (!ignoreInconsistentSolutions || !consistencyProcessed) {
140+
var groupEntities = shadowVariableReferences.get(0).groupEntities();
141+
var groupEntityIds = entityVariable.groupEntityIds();
142+
143+
if (groupEntities != null) {
144+
for (var i = 0; i < groupEntityIds.length; i++) {
145+
var groupEntity = groupEntities[i];
146+
var groupEntityId = groupEntityIds[i];
147+
anyChanged |=
148+
updateLoopedStatusOfEntity(groupEntity, groupEntityId, entityConsistencyState);
149+
}
150+
} else {
139151
anyChanged |=
140-
updateLoopedStatusOfEntity(groupEntity, groupEntityId, entityConsistencyState);
152+
updateLoopedStatusOfEntity(entity, entityVariable.entityId(), entityConsistencyState);
141153
}
142-
} else {
143-
anyChanged |=
144-
updateLoopedStatusOfEntity(entity, entityVariable.entityId(), entityConsistencyState);
145154
}
146155

147156
for (var shadowVariableReference : shadowVariableReferences) {

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ private ConsistencyTracker(boolean isFrozen) {
2323
}
2424

2525
public static <Solution_> ConsistencyTracker<Solution_> frozen(SolutionDescriptor<Solution_> solutionDescriptor,
26+
boolean ignoreInconsistentSolutions,
2627
Object[] entityOrFacts) {
2728
var out = new ConsistencyTracker<Solution_>(true);
28-
out.setUnknownConsistencyFromEntityShadowVariablesInconsistent(solutionDescriptor, entityOrFacts);
29+
out.setUnknownConsistencyFromEntityShadowVariablesInconsistent(solutionDescriptor, ignoreInconsistentSolutions,
30+
entityOrFacts);
2931
return out;
3032
}
3133

@@ -46,6 +48,7 @@ public static <Solution_> ConsistencyTracker<Solution_> frozen(SolutionDescripto
4648
* (regardless of its actual consistency in the graph).
4749
*/
4850
void setUnknownConsistencyFromEntityShadowVariablesInconsistent(SolutionDescriptor<Solution_> solutionDescriptor,
51+
boolean ignoreInconsistentSolutions,
4952
Object[] entityOrFacts) { // Not private so DefaultVariableReferenceGraph javadoc can reference it.
5053
var entities = Arrays.stream(entityOrFacts)
5154
.filter(maybeEntity -> solutionDescriptor.hasEntityDescriptor(maybeEntity.getClass()))
@@ -60,7 +63,8 @@ void setUnknownConsistencyFromEntityShadowVariablesInconsistent(SolutionDescript
6063
new DefaultShadowVariableSessionFactory.GraphDescriptor<>(solutionDescriptor,
6164
ChangedVariableNotifier.empty(), entities)
6265
.withConsistencyTracker(this)
63-
.assertingNoReferencedMissingEntities());
66+
.assertingNoReferencedMissingEntities(),
67+
ignoreInconsistentSolutions);
6468

6569
// Graph will either be DefaultVariableReferenceGraph or EmptyVariableReferenceGraph
6670
// If it is empty, we don't need to do anything.
@@ -71,7 +75,7 @@ void setUnknownConsistencyFromEntityShadowVariablesInconsistent(SolutionDescript
7175

7276
/**
7377
* If true, consistency and shadow variables are frozen and should not be updated.
74-
* ConstraintVerifier creates a frozen instance via {@link #frozen(SolutionDescriptor, Object[])}.
78+
* ConstraintVerifier creates a frozen instance via {@link #frozen(SolutionDescriptor, boolean, Object[])}.
7579
*
7680
* @return true if consistency and shadow variables are frozen and should not be updated
7781
*/

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public void afterListVariableChanged(ListVariableDescriptor<Solution_> variableD
4747
variableDescriptor.getValue(entity), fromIndex, toIndex);
4848
}
4949

50-
public void updateVariables() {
51-
graph.updateChanged();
50+
public boolean updateVariables() {
51+
return graph.updateChanged();
5252
}
5353
}

0 commit comments

Comments
 (0)