Skip to content

Commit 2297c26

Browse files
perf: add ability to ignore inconsistent solutions (#2396)
1 parent 0220241 commit 2297c26

41 files changed

Lines changed: 604 additions & 79 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
@@ -151,7 +151,7 @@ static <T> T loadOrDefault(Function<TimefoldSolverEnterpriseService, T> builder,
151151
}
152152
}
153153

154-
TopologicalOrderGraph buildTopologyGraph(int size);
154+
TopologicalOrderGraph buildTopologyGraph(int size, boolean ignoreInconsistentSolutions);
155155

156156
/**
157157
* 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
@@ -59,6 +59,7 @@
5959
import ai.timefold.solver.core.impl.domain.solution.cloner.gizmo.GizmoSolutionCloner;
6060
import ai.timefold.solver.core.impl.domain.solution.cloner.gizmo.GizmoSolutionClonerFactory;
6161
import ai.timefold.solver.core.impl.domain.variable.declarative.DeclarativeShadowVariableDescriptor;
62+
import ai.timefold.solver.core.impl.domain.variable.declarative.ShadowVariablesInconsistentVariableDescriptor;
6263
import ai.timefold.solver.core.impl.domain.variable.descriptor.BasicVariableDescriptor;
6364
import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor;
6465
import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor;
@@ -1037,6 +1038,13 @@ public List<DeclarativeShadowVariableDescriptor<Solution_>> getDeclarativeShadow
10371038
return declarativeShadowVariableDescriptorList;
10381039
}
10391040

1041+
public boolean hasAnyShadowVariablesInconsistentMember() {
1042+
return entityDescriptorMap.values().stream()
1043+
.flatMap(entityDescriptor -> entityDescriptor.getShadowVariableDescriptors().stream())
1044+
.anyMatch(
1045+
shadowVariableDescriptor -> shadowVariableDescriptor instanceof ShadowVariablesInconsistentVariableDescriptor<Solution_>);
1046+
}
1047+
10401048
public Stream<Object> extractAllEntitiesStream(Solution_ solution) {
10411049
var stream = Stream.empty();
10421050
for (var memberAccessor : entityMemberAccessorMap.values()) {
@@ -1102,5 +1110,4 @@ public <Score_ extends Score<Score_>> void setScore(Solution_ solution, Score_ s
11021110
public String toString() {
11031111
return "%s(%s)".formatted(getClass().getSimpleName(), solutionClass.getName());
11041112
}
1105-
11061113
}

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
@@ -106,7 +106,7 @@ public void updateShadowVariables(Class<Solution_> solutionClass, Object... enti
106106
.formatted(missingShadowVariableTypeList));
107107
}
108108
// No solution, we trigger all supported events manually
109-
var session = InternalShadowVariableSession.build(solutionDescriptor, entities);
109+
var session = InternalShadowVariableSession.build(solutionDescriptor, false, entities);
110110
// Update all built-in shadow variables
111111
var listVariableDescriptor = solutionDescriptor.getListVariableDescriptor();
112112
if (listVariableDescriptor == null) {
@@ -123,12 +123,14 @@ private record InternalShadowVariableSession<Solution_>(SolutionDescriptor<Solut
123123

124124
public static <Solution_> InternalShadowVariableSession<Solution_> build(
125125
SolutionDescriptor<Solution_> solutionDescriptor,
126+
boolean ignoreInconsistentSolutions,
126127
Object... entities) {
127128
return new InternalShadowVariableSession<>(solutionDescriptor,
128129
DefaultShadowVariableSessionFactory.buildGraph(
129130
new DefaultShadowVariableSessionFactory.GraphDescriptor<>(solutionDescriptor,
130131
ChangedVariableNotifier.empty(), entities)
131-
.assertingNoReferencedMissingEntities()));
132+
.assertingNoReferencedMissingEntities(),
133+
ignoreInconsistentSolutions));
132134
}
133135

134136
/**
@@ -331,7 +333,7 @@ public void setWorkingSolutionWithoutUpdatingShadows(Solution_ workingSolution)
331333
}
332334

333335
@Override
334-
public InnerScore<Score_> calculateScore() {
336+
public InnerScore<Score_> innerCalculateScore() {
335337
throw new UnsupportedOperationException();
336338
}
337339

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
@@ -79,7 +79,7 @@ public abstract sealed class AbstractVariableReferenceGraph<Solution_, ChangeTra
7979
* and {@link #afterVariableChanged(VariableMetaModel, Object)}
8080
* can short circuit.
8181
*/
82-
abstract void innerUpdateChanged();
82+
abstract boolean innerUpdateChanged();
8383

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

9292
@Override
93-
public final void updateChanged() {
93+
public final boolean updateChanged() {
9494
isUpdating = true;
95-
innerUpdateChanged();
95+
var success = innerUpdateChanged();
9696
isUpdating = false;
97+
return success;
9798
}
9899

99100
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
@@ -34,7 +34,7 @@ public void afterVariableChanged(VariableMetaModel<Solution_, ?, ?> variableMeta
3434
entity);
3535
}
3636

37-
public void updateVariables() {
38-
graph.updateChanged();
37+
public boolean updateVariables() {
38+
return graph.updateChanged();
3939
}
4040
}

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

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -162,29 +162,31 @@ public ChangedVariableNotifier<Solution_> changedVariableNotifier() {
162162
}
163163
}
164164

165-
public static <Solution_> VariableReferenceGraph buildGraph(GraphDescriptor<Solution_> graphDescriptor) {
165+
public static <Solution_> VariableReferenceGraph buildGraph(GraphDescriptor<Solution_> graphDescriptor,
166+
boolean ignoreInconsistentSolutions) {
166167
var graphStructureAndDirection = GraphStructure.determineGraphStructure(graphDescriptor.solutionDescriptor(),
167168
graphDescriptor.entities());
168169
LOGGER.trace("Shadow variable graph structure: {}", graphStructureAndDirection);
169-
return buildGraphForStructureAndDirection(graphStructureAndDirection, graphDescriptor);
170+
return buildGraphForStructureAndDirection(graphStructureAndDirection, graphDescriptor, ignoreInconsistentSolutions);
170171
}
171172

172173
static <Solution_> VariableReferenceGraph buildGraphForStructureAndDirection(
173-
GraphStructure.GraphStructureAndDirection graphStructureAndDirection, GraphDescriptor<Solution_> graphDescriptor) {
174+
GraphStructure.GraphStructureAndDirection graphStructureAndDirection, GraphDescriptor<Solution_> graphDescriptor,
175+
boolean ignoreInconsistentSolutions) {
174176
return switch (graphStructureAndDirection.structure()) {
175177
case EMPTY -> EmptyVariableReferenceGraph.INSTANCE;
176178
case SINGLE_DIRECTIONAL_PARENT -> {
177179
var scoreDirector =
178180
graphDescriptor.variableReferenceGraphBuilder().changedVariableNotifier.innerScoreDirector();
179181
if (scoreDirector == null) {
180-
yield buildArbitraryGraph(graphDescriptor);
182+
yield buildArbitraryGraph(graphDescriptor, ignoreInconsistentSolutions);
181183
}
182184
yield buildSingleDirectionalParentGraph(graphDescriptor, graphStructureAndDirection);
183185
}
184186
case ARBITRARY_SINGLE_ENTITY_AT_MOST_ONE_DIRECTIONAL_PARENT_TYPE ->
185-
buildArbitrarySingleEntityGraph(graphDescriptor);
187+
buildArbitrarySingleEntityGraph(graphDescriptor, ignoreInconsistentSolutions);
186188
case NO_DYNAMIC_EDGES, ARBITRARY ->
187-
buildArbitraryGraph(graphDescriptor);
189+
buildArbitraryGraph(graphDescriptor, ignoreInconsistentSolutions);
188190
};
189191
}
190192

@@ -280,7 +282,8 @@ yield new TopologicalSorter(listStateSupply::getPreviousElement,
280282
};
281283
}
282284

283-
private static <Solution_> VariableReferenceGraph buildArbitraryGraph(GraphDescriptor<Solution_> graphDescriptor) {
285+
private static <Solution_> VariableReferenceGraph buildArbitraryGraph(GraphDescriptor<Solution_> graphDescriptor,
286+
boolean ignoreInconsistentSolutions) {
284287
var declarativeShadowVariableDescriptors =
285288
graphDescriptor.solutionDescriptor().getDeclarativeShadowVariableDescriptors();
286289
var variableIdToUpdater = EntityVariableUpdaterLookup.<Solution_> entityIndependentLookup();
@@ -294,13 +297,14 @@ private static <Solution_> VariableReferenceGraph buildArbitraryGraph(GraphDescr
294297
graphDescriptor,
295298
declarativeShadowVariableDescriptors, variableIdToUpdater);
296299
return buildVariableReferenceGraph(graphDescriptor, declarativeShadowVariableDescriptors,
297-
declarativeShadowVariableToAliasMap);
300+
declarativeShadowVariableToAliasMap, ignoreInconsistentSolutions);
298301
}
299302

300303
private static <Solution_> VariableReferenceGraph buildVariableReferenceGraph(
301304
GraphDescriptor<Solution_> graphDescriptor,
302305
List<DeclarativeShadowVariableDescriptor<Solution_>> declarativeShadowVariableDescriptors,
303-
Map<VariableMetaModel<?, ?, ?>, Set<VariableSourceReference>> declarativeShadowVariableToAliasMap) {
306+
Map<VariableMetaModel<?, ?, ?>, Set<VariableSourceReference>> declarativeShadowVariableToAliasMap,
307+
boolean ignoreInconsistentSolutions) {
304308
// Create variable processors for each declarative shadow variable descriptor
305309
for (var declarativeShadowVariable : declarativeShadowVariableDescriptors) {
306310
var fromVariableId = declarativeShadowVariable.getVariableMetaModel();
@@ -315,7 +319,8 @@ private static <Solution_> VariableReferenceGraph buildVariableReferenceGraph(
315319
// Create the fixed edges in the graph
316320
createFixedVariableRelationEdges(graphDescriptor.variableReferenceGraphBuilder(), graphDescriptor.entities(),
317321
declarativeShadowVariableDescriptors);
318-
return graphDescriptor.variableReferenceGraphBuilder().build(graphDescriptor.graphCreator());
322+
return graphDescriptor.variableReferenceGraphBuilder().build(graphDescriptor.graphCreator(),
323+
ignoreInconsistentSolutions);
319324
}
320325

321326
private record GroupVariableUpdaterInfo<Solution_>(
@@ -443,7 +448,7 @@ public List<VariableUpdaterInfo<Solution_>> getUpdatersForEntityVariable(Object
443448
}
444449

445450
private static <Solution_> VariableReferenceGraph buildArbitrarySingleEntityGraph(
446-
GraphDescriptor<Solution_> graphDescriptor) {
451+
GraphDescriptor<Solution_> graphDescriptor, boolean ignoreInconsistentSolutions) {
447452
var declarativeShadowVariableDescriptors =
448453
graphDescriptor.solutionDescriptor().getDeclarativeShadowVariableDescriptors();
449454
// Use a dependent lookup; if an entity does not use groups, then all variables can share the same node.
@@ -475,7 +480,7 @@ private static <Solution_> VariableReferenceGraph buildArbitrarySingleEntityGrap
475480
(entity, declarativeShadowVariable, variableId) -> variableIdToGroupedUpdater.get(variableId)
476481
.getUpdatersForEntityVariable(entity, declarativeShadowVariable));
477482
return buildVariableReferenceGraph(graphDescriptor, declarativeShadowVariableDescriptors,
478-
declarativeShadowVariableToAliasMap);
483+
declarativeShadowVariableToAliasMap, ignoreInconsistentSolutions);
479484
}
480485

481486
private static <Solution_> Map<VariableMetaModel<?, ?, ?>, Set<VariableSourceReference>> createGraphNodes(
@@ -691,18 +696,21 @@ private static <Solution_> void createFixedVariableRelationEdges(
691696
}
692697

693698
public DefaultShadowVariableSession<Solution_> forSolution(ConsistencyTracker<Solution_> consistencyTracker,
699+
boolean ignoreInconsistentSolutions,
694700
Solution_ solution) {
695701
var entities = new ArrayList<>();
696702
solutionDescriptor.visitAllEntities(solution, entities::add);
697-
return forEntities(consistencyTracker, entities.toArray());
703+
return forEntities(consistencyTracker, ignoreInconsistentSolutions, entities.toArray());
698704
}
699705

700706
public DefaultShadowVariableSession<Solution_> forEntities(ConsistencyTracker<Solution_> consistencyTracker,
707+
boolean ignoreInconsistentSolutions,
701708
Object... entities) {
702709
var graph = buildGraph(
703710
new GraphDescriptor<>(solutionDescriptor, ChangedVariableNotifier.of(scoreDirector), entities)
704711
.withConsistencyTracker(consistencyTracker)
705-
.withGraphCreator(graphCreator));
712+
.withGraphCreator(graphCreator),
713+
ignoreInconsistentSolutions);
706714
return new DefaultShadowVariableSession<>(graph);
707715
}
708716
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ public int getTopologicalOrder(int node) {
117117
}
118118

119119
@Override
120-
public void commitChanges(BitSet changed) {
120+
public boolean commitChanges(BitSet changed) {
121121
var index = new MutableInt(1);
122122
var stackIndex = new MutableInt(0);
123123
var size = forwardEdges.length;
@@ -126,6 +126,7 @@ public void commitChanges(BitSet changed) {
126126
var lowMap = new int[size];
127127
var onStackSet = new boolean[size];
128128
var components = new ArrayList<BitSet>();
129+
var anyLooped = false;
129130
componentMap.clear();
130131

131132
for (var node = 0; node < size; node++) {
@@ -139,6 +140,7 @@ public void commitChanges(BitSet changed) {
139140
var component = components.get(i);
140141
var componentSize = component.cardinality();
141142
var isComponentLooped = componentSize != 1;
143+
anyLooped |= isComponentLooped;
142144
var componentNodes = new ArrayList<Integer>(componentSize);
143145
for (var node = component.nextSetBit(0); node >= 0; node = component.nextSetBit(node + 1)) {
144146
nodeIdToTopologicalOrderMap[node] = ordIndex;
@@ -159,6 +161,7 @@ public void commitChanges(BitSet changed) {
159161
}
160162
}
161163
}
164+
return anyLooped;
162165
}
163166

164167
private void strongConnect(int node, MutableInt index, MutableInt stackIndex, int[] stack,

0 commit comments

Comments
 (0)