Skip to content

Commit 70a6eff

Browse files
fix: update left indicted set even if its key not changed for indexed if exists, aggregate support for precompute, make BiPrecomputeTest Indictment aware
1 parent 4dfea10 commit 70a6eff

10 files changed

Lines changed: 132 additions & 54 deletions

File tree

core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/PrecomputeBiNode.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ public PrecomputeBiNode(Supplier<BavetPrecomputeBuildHelper<BiTuple<A, B>>> prec
2323

2424
@Override
2525
protected BiTuple<A, B> remapTuple(BiTuple<A, B> tuple) {
26-
var out = BiTuple.of(tuple.getA(), tuple.getB(), outputStoreSize);
27-
out.setIndictmentSource(tuple.getIndictmentSource());
28-
return out;
26+
return BiTuple.of(tuple.getA(), tuple.getB(), outputStoreSize);
2927
}
3028
}

core/src/main/java/ai/timefold/solver/core/impl/bavet/common/AbstractIndexedIfExistsNode.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,12 @@ public final void updateLeft(LeftTuple_ leftTuple) {
140140
// The indexers contain counters in the DEAD state, to track the rightCount.
141141
if (!isFiltering) {
142142
updateUnchangedCounterLeft(counter);
143+
if (leftTuple.getIndictmentSource() != IndictmentSource.DISABLED) {
144+
IndictmentSource.clearSupport(getId(), leftTuple);
145+
forEachRightFromLeft(leftTuple, newCompositeKey, rightTuple -> {
146+
IndictmentSource.addSupport(getId(), leftTuple, rightTuple);
147+
});
148+
}
143149
} else {
144150
// Call filtering for the leftTuple and rightTuple combinations again
145151
clearLeftTrackerList(leftTuple);
Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package ai.timefold.solver.core.impl.bavet.common;
22

3+
import java.util.ArrayList;
34
import java.util.IdentityHashMap;
5+
import java.util.LinkedHashMap;
46
import java.util.List;
57
import java.util.function.UnaryOperator;
68

79
import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple;
10+
import ai.timefold.solver.core.impl.bavet.common.tuple.indictment.IndictmentSource;
811

912
import org.jspecify.annotations.NullMarked;
1013

@@ -13,6 +16,18 @@ public record TupleRecorder<Tuple_ extends Tuple>(List<Tuple_> recordedTupleList
1316
UnaryOperator<Tuple_> mapper,
1417
IdentityHashMap<Tuple_, Tuple_> inputTupleToOutputTuple) {
1518
public void recordTuple(Tuple_ tuple) {
16-
recordedTupleList.add(inputTupleToOutputTuple.computeIfAbsent(tuple, mapper));
19+
var outTuple = inputTupleToOutputTuple.computeIfAbsent(tuple, mapper);
20+
if (tuple.getIndictmentSource() != IndictmentSource.DISABLED) {
21+
if (outTuple.getIndictmentSource() == IndictmentSource.DISABLED) {
22+
outTuple.setIndictmentSource(
23+
new IndictmentSource.AggregateIndictmentSource(new ArrayList<>(), new LinkedHashMap<>()));
24+
}
25+
// Precompute uses an independent node network, so we need to aggregate its supports to not interfere with the
26+
// outer node network support
27+
var aggregateIndictmentSource = (IndictmentSource.AggregateIndictmentSource) outTuple.getIndictmentSource();
28+
tuple.getIndictmentSource()
29+
.visitAllSources(source -> aggregateIndictmentSource.sourceList().add(IndictmentSource.of(source)));
30+
}
31+
recordedTupleList.add(outTuple);
1732
}
1833
}

core/src/main/java/ai/timefold/solver/core/impl/bavet/common/tuple/indictment/IndictmentSource.java

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,21 @@
1212

1313
import ai.timefold.solver.core.impl.bavet.common.tuple.Tuple;
1414

15+
import org.jspecify.annotations.NullMarked;
16+
import org.jspecify.annotations.Nullable;
17+
18+
@NullMarked
1519
public sealed interface IndictmentSource {
1620
IndictmentSource DISABLED = new DisabledIndictmentSource();
1721

18-
void visitSources(Set<IndictmentSource> visited, long[] involvedNodeIds, Consumer<Object> sourceConsumer);
22+
void visitSources(Set<IndictmentSource> visited, long @Nullable [] involvedNodeIds, Consumer<Object> sourceConsumer);
1923

2024
Map<Long, Set<IndictmentSource>> support();
2125

26+
default void visitAllSources(Consumer<Object> sourceConsumer) {
27+
visitSources(new HashSet<>(), null, sourceConsumer);
28+
}
29+
2230
default void visitSources(long[] involvedNodeIds, Consumer<Object> sourceConsumer) {
2331
visitSources(new HashSet<>(), involvedNodeIds, sourceConsumer);
2432
}
@@ -28,13 +36,22 @@ default Set<IndictmentSource> getSupportForNodeId(long nodeId) {
2836
}
2937

3038
static boolean checkIfAlreadyVisitedAndVisitSupport(IndictmentSource self, Set<IndictmentSource> visited,
31-
long[] involvedNodeIds, Consumer<Object> sourceConsumer) {
39+
long @Nullable [] involvedNodeIds, Consumer<Object> sourceConsumer) {
3240
if (!visited.add(self)) {
3341
return true;
3442
}
35-
for (var nodeId : involvedNodeIds) {
36-
for (var indictmentSource : self.support().getOrDefault(nodeId, Collections.emptySet())) {
37-
indictmentSource.visitSources(visited, involvedNodeIds, sourceConsumer);
43+
44+
if (involvedNodeIds == null) {
45+
for (var indictmentSourceSet : self.support().values()) {
46+
for (var indictmentSource : indictmentSourceSet) {
47+
indictmentSource.visitSources(visited, null, sourceConsumer);
48+
}
49+
}
50+
} else {
51+
for (var nodeId : involvedNodeIds) {
52+
for (var indictmentSource : self.support().getOrDefault(nodeId, Collections.emptySet())) {
53+
indictmentSource.visitSources(visited, involvedNodeIds, sourceConsumer);
54+
}
3855
}
3956
}
4057
return false;
@@ -102,7 +119,8 @@ static void removeSupport(long nodeId, Tuple carry, Tuple support) {
102119

103120
record DisabledIndictmentSource() implements IndictmentSource {
104121
@Override
105-
public void visitSources(Set<IndictmentSource> visited, long[] involvedNodeIds, Consumer<Object> sourceConsumer) {
122+
public void visitSources(Set<IndictmentSource> visited, long @Nullable [] involvedNodeIds,
123+
Consumer<Object> sourceConsumer) {
106124
throw new UnsupportedOperationException("Impossible state: indictments are disabled.");
107125
}
108126

@@ -114,7 +132,8 @@ public Map<Long, Set<IndictmentSource>> support() {
114132

115133
record RootIndictmentSource(Object source, Map<Long, Set<IndictmentSource>> support) implements IndictmentSource {
116134
@Override
117-
public void visitSources(Set<IndictmentSource> visited, long[] involvedNodeIds, Consumer<Object> sourceConsumer) {
135+
public void visitSources(Set<IndictmentSource> visited, long @Nullable [] involvedNodeIds,
136+
Consumer<Object> sourceConsumer) {
118137
if (checkIfAlreadyVisitedAndVisitSupport(this, visited, involvedNodeIds, sourceConsumer)) {
119138
return;
120139
}
@@ -135,7 +154,8 @@ public int hashCode() {
135154
record JoinedIndictmentSource(IndictmentSource left, IndictmentSource right,
136155
Map<Long, Set<IndictmentSource>> support) implements IndictmentSource {
137156
@Override
138-
public void visitSources(Set<IndictmentSource> visited, long[] involvedNodeIds, Consumer<Object> sourceConsumer) {
157+
public void visitSources(Set<IndictmentSource> visited, long @Nullable [] involvedNodeIds,
158+
Consumer<Object> sourceConsumer) {
139159
if (checkIfAlreadyVisitedAndVisitSupport(this, visited, involvedNodeIds, sourceConsumer)) {
140160
return;
141161
}
@@ -157,7 +177,8 @@ public int hashCode() {
157177
record AggregateIndictmentSource(List<IndictmentSource> sourceList,
158178
Map<Long, Set<IndictmentSource>> support) implements IndictmentSource {
159179
@Override
160-
public void visitSources(Set<IndictmentSource> visited, long[] involvedNodeIds, Consumer<Object> sourceConsumer) {
180+
public void visitSources(Set<IndictmentSource> visited, long @Nullable [] involvedNodeIds,
181+
Consumer<Object> sourceConsumer) {
161182
if (checkIfAlreadyVisitedAndVisitSupport(this, visited, involvedNodeIds, sourceConsumer)) {
162183
return;
163184
}

core/src/main/java/ai/timefold/solver/core/impl/bavet/quad/PrecomputeQuadNode.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ public PrecomputeQuadNode(Supplier<BavetPrecomputeBuildHelper<QuadTuple<A, B, C,
2323

2424
@Override
2525
protected QuadTuple<A, B, C, D> remapTuple(QuadTuple<A, B, C, D> tuple) {
26-
var out = QuadTuple.of(tuple.getA(), tuple.getB(), tuple.getC(), tuple.getD(), outputStoreSize);
27-
out.setIndictmentSource(tuple.getIndictmentSource());
28-
return out;
26+
return QuadTuple.of(tuple.getA(), tuple.getB(), tuple.getC(), tuple.getD(), outputStoreSize);
2927
}
3028
}

core/src/main/java/ai/timefold/solver/core/impl/bavet/tri/PrecomputeTriNode.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ public PrecomputeTriNode(Supplier<BavetPrecomputeBuildHelper<TriTuple<A, B, C>>>
2323

2424
@Override
2525
protected TriTuple<A, B, C> remapTuple(TriTuple<A, B, C> tuple) {
26-
var out = TriTuple.of(tuple.getA(), tuple.getB(), tuple.getC(), outputStoreSize);
27-
out.setIndictmentSource(tuple.getIndictmentSource());
28-
return out;
26+
return TriTuple.of(tuple.getA(), tuple.getB(), tuple.getC(), outputStoreSize);
2927
}
3028
}

core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/PrecomputeUniNode.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,7 @@ public PrecomputeUniNode(Supplier<BavetPrecomputeBuildHelper<UniTuple<A>>> preco
2323

2424
@Override
2525
protected UniTuple<A> remapTuple(UniTuple<A> tuple) {
26-
var out = UniTuple.of(tuple.getA(), outputStoreSize);
27-
out.setIndictmentSource(tuple.getIndictmentSource());
28-
return out;
26+
return UniTuple.of(tuple.getA(), outputStoreSize);
2927
}
3028

3129
}

core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamPrecomputeTest.java

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package ai.timefold.solver.core.impl.score.stream.common.bi;
22

3+
import java.util.ArrayList;
4+
import java.util.Arrays;
35
import java.util.List;
46
import java.util.function.Function;
57

@@ -11,7 +13,6 @@
1113
import ai.timefold.solver.core.impl.score.stream.common.AbstractConstraintStreamTest;
1214
import ai.timefold.solver.core.impl.score.stream.common.ConstraintStreamImplSupport;
1315
import ai.timefold.solver.core.impl.score.stream.common.ConstraintStreamPrecomputeTest;
14-
import ai.timefold.solver.core.impl.util.Pair;
1516
import ai.timefold.solver.core.testdomain.score.lavish.TestdataLavishEntity;
1617
import ai.timefold.solver.core.testdomain.score.lavish.TestdataLavishEntityGroup;
1718
import ai.timefold.solver.core.testdomain.score.lavish.TestdataLavishSolution;
@@ -354,9 +355,27 @@ public void filter_1_changed_forEachUnfilteredUniquePair() {
354355
assertMatch(entity2, entity3));
355356
}
356357

358+
record Expected<A, B>(A a, B b, Object... indicted) {
359+
Expected<A, B> addIndicted(Object indictedObject) {
360+
for (var object : indicted) {
361+
if (object == indictedObject) {
362+
return this;
363+
}
364+
}
365+
var newIndictments = Arrays.copyOf(indicted, indicted.length + 1);
366+
newIndictments[indicted.length] = indictedObject;
367+
return new Expected<>(a, b, newIndictments);
368+
}
369+
}
370+
371+
<A, B> Expected<A, B> expect(A a, B b, Object... indicted) {
372+
return new Expected<>(a, b, indicted);
373+
}
374+
357375
private <A, B> void assertPrecompute(TestdataLavishSolution solution,
358-
List<Pair<A, B>> expectedValues,
376+
List<Expected<A, B>> expectedValues,
359377
Function<PrecomputeFactory, BiConstraintStream<A, B>> entityStreamSupplier) {
378+
expectedValues = new ArrayList<>(expectedValues);
360379
var scoreDirector =
361380
buildScoreDirector(factory -> factory.precompute(entityStreamSupplier)
362381
.ifExists(TestdataLavishEntity.class)
@@ -371,11 +390,16 @@ private <A, B> void assertPrecompute(TestdataLavishSolution solution,
371390
scoreDirector.beforeVariableChanged(entity, "value");
372391
entity.setValue(solution.getFirstValue());
373392
scoreDirector.afterVariableChanged(entity, "value");
393+
var listIterator = expectedValues.listIterator();
394+
while (listIterator.hasNext()) {
395+
var expected = listIterator.next();
396+
listIterator.set(expected.addIndicted(entity));
397+
}
374398
}
375399

376400
assertScore(scoreDirector, expectedValues.stream()
377-
.map(pair -> new Object[] { pair.key(), pair.value() })
378-
.map(AbstractConstraintStreamTest::assertMatch)
401+
.map(expected -> assertMatch(expected.a, expected.b)
402+
.withIndictedObjects(expected.indicted))
379403
.toArray(AssertableMatch[]::new));
380404
}
381405

@@ -384,15 +408,19 @@ private <A, B> void assertPrecompute(TestdataLavishSolution solution,
384408
public void ifExists() {
385409
var solution = TestdataLavishSolution.generateEmptySolution();
386410
var entityWithoutGroup = new TestdataLavishEntity();
411+
entityWithoutGroup.setCode("A");
387412
var entityWithGroup = new TestdataLavishEntity();
413+
entityWithGroup.setCode("B");
388414
var entityGroup = new TestdataLavishEntityGroup();
415+
entityGroup.setCode("C");
389416
entityWithGroup.setEntityGroup(entityGroup);
390417
solution.getEntityList().addAll(List.of(entityWithoutGroup, entityWithGroup));
391418
solution.getEntityGroupList().add(entityGroup);
392419
var value = new TestdataLavishValue();
420+
value.setCode("D");
393421
solution.getValueList().add(value);
394422

395-
assertPrecompute(solution, List.of(new Pair<>(entityWithGroup, value)),
423+
assertPrecompute(solution, List.of(expect(entityWithGroup, value, entityWithGroup, value, entityGroup)),
396424
pf -> pf.forEachUnfiltered(TestdataLavishEntity.class)
397425
.join(TestdataLavishValue.class)
398426
.ifExists(TestdataLavishEntityGroup.class, Joiners.equal(
@@ -413,7 +441,7 @@ public void ifNotExists() {
413441
var value = new TestdataLavishValue();
414442
solution.getValueList().add(value);
415443

416-
assertPrecompute(solution, List.of(new Pair<>(entityWithoutGroup, value)),
444+
assertPrecompute(solution, List.of(expect(entityWithoutGroup, value, entityWithoutGroup, value)),
417445
pf -> pf.forEachUnfiltered(TestdataLavishEntity.class)
418446
.join(TestdataLavishValue.class)
419447
.ifNotExists(TestdataLavishEntityGroup.class, Joiners.equal(
@@ -434,7 +462,7 @@ public void groupBy() {
434462
var value = new TestdataLavishValue();
435463
solution.getValueList().add(value);
436464

437-
assertPrecompute(solution, List.of(new Pair<>(entityGroup, 1L)),
465+
assertPrecompute(solution, List.of(expect(entityGroup, 1L)),
438466
pf -> pf.forEachUnfiltered(TestdataLavishEntity.class)
439467
.filter(entity -> entity.getEntityGroup() != null)
440468
.groupBy(TestdataLavishEntity::getEntityGroup, ConstraintCollectors.count()));
@@ -453,8 +481,8 @@ public void flatten() {
453481
var value = new TestdataLavishValue();
454482
solution.getValueList().add(value);
455483

456-
assertPrecompute(solution, List.of(new Pair<>(entityWithoutGroup, entityWithoutGroup),
457-
new Pair<>(entityWithGroup, entityWithoutGroup)),
484+
assertPrecompute(solution, List.of(expect(entityWithoutGroup, entityWithoutGroup),
485+
expect(entityWithGroup, entityWithoutGroup)),
458486
pf -> pf.forEachUnfiltered(TestdataLavishEntity.class)
459487
.flatten(List::of));
460488
}
@@ -475,8 +503,8 @@ record ValueHolder(int value) {
475503
var value = new TestdataLavishValue();
476504
solution.getValueList().add(value);
477505

478-
assertPrecompute(solution, List.of(new Pair<>(entity1, new ValueHolder(entity1.getIntegerProperty())),
479-
new Pair<>(entity2, new ValueHolder(entity2.getIntegerProperty()))),
506+
assertPrecompute(solution, List.of(expect(entity1, new ValueHolder(entity1.getIntegerProperty())),
507+
expect(entity2, new ValueHolder(entity2.getIntegerProperty()))),
480508
pf -> pf.forEachUnfiltered(TestdataLavishEntity.class)
481509
.flatten(entity -> List.of(new ValueHolder(entity.getIntegerProperty()))));
482510
}
@@ -494,8 +522,8 @@ public void flattenLast() {
494522
var value = new TestdataLavishValue();
495523
solution.getValueList().add(value);
496524

497-
assertPrecompute(solution, List.of(new Pair<>(entityWithoutGroup, value),
498-
new Pair<>(entityWithGroup, value)),
525+
assertPrecompute(solution, List.of(expect(entityWithoutGroup, value, entityWithoutGroup, value),
526+
expect(entityWithGroup, value, entityWithGroup, value)),
499527
pf -> pf.forEachUnfiltered(TestdataLavishEntity.class)
500528
.groupBy(ConstraintCollectors.toList())
501529
.flattenLast(entityList -> entityList)
@@ -518,8 +546,8 @@ record ValueHolder(int value) {
518546
var value = new TestdataLavishValue();
519547
solution.getValueList().add(value);
520548

521-
assertPrecompute(solution, List.of(new Pair<>(new ValueHolder(1), value),
522-
new Pair<>(new ValueHolder(2), value)),
549+
assertPrecompute(solution, List.of(expect(new ValueHolder(1), value, entity1, value),
550+
expect(new ValueHolder(2), value, entity2, value)),
523551
pf -> pf.forEachUnfiltered(TestdataLavishEntity.class)
524552
.groupBy(ConstraintCollectors.toList())
525553
.flattenLast(entityList -> entityList
@@ -544,8 +572,8 @@ public void map() {
544572
var value = new TestdataLavishValue();
545573
solution.getValueList().add(value);
546574

547-
assertPrecompute(solution, List.of(new Pair<>(entityGroup, value),
548-
new Pair<>(entityGroup, value)),
575+
assertPrecompute(solution, List.of(expect(entityGroup, value, entityWithGroup1, value),
576+
expect(entityGroup, value, entityWithGroup2, value)),
549577
pf -> pf.forEachUnfiltered(TestdataLavishEntity.class)
550578
.join(TestdataLavishValue.class)
551579
.filter((entity, joinedValue) -> entity.getEntityGroup() != null)
@@ -558,15 +586,21 @@ public void map() {
558586
public void concat() {
559587
var solution = TestdataLavishSolution.generateEmptySolution();
560588
var entityWithoutGroup = new TestdataLavishEntity();
589+
entityWithoutGroup.setCode("A");
561590
var entityWithGroup = new TestdataLavishEntity();
591+
entityWithGroup.setCode("B");
562592
var entityGroup = new TestdataLavishEntityGroup();
593+
entityGroup.setCode("C");
563594
entityWithGroup.setEntityGroup(entityGroup);
564595
solution.getEntityList().addAll(List.of(entityWithoutGroup, entityWithGroup));
565596
solution.getEntityGroupList().add(entityGroup);
566597
var value = new TestdataLavishValue();
598+
value.setCode("D");
567599
solution.getValueList().add(value);
568600

569-
assertPrecompute(solution, List.of(new Pair<>(entityWithoutGroup, value), new Pair<>(entityWithGroup, value)),
601+
assertPrecompute(solution, List.of(
602+
expect(entityWithoutGroup, value, entityWithoutGroup, value),
603+
expect(entityWithGroup, value, entityWithGroup, value)),
570604
pf -> pf.forEachUnfiltered(TestdataLavishEntity.class)
571605
.join(TestdataLavishValue.class)
572606
.filter((entity, joinedValue) -> entity.getEntityGroup() == null)
@@ -590,7 +624,7 @@ public void distinct() {
590624
var value = new TestdataLavishValue();
591625
solution.getValueList().add(value);
592626

593-
assertPrecompute(solution, List.of(new Pair<>(entityGroup, value)),
627+
assertPrecompute(solution, List.of(expect(entityGroup, value, entityWithGroup1, entityWithGroup2, value)),
594628
pf -> pf.forEachUnfiltered(TestdataLavishEntity.class)
595629
.join(TestdataLavishValue.class)
596630
.filter((entity, joinedValue) -> entity.getEntityGroup() != null)
@@ -615,9 +649,9 @@ public void complement() {
615649
solution.getValueList().add(value);
616650

617651
assertPrecompute(solution, List.of(
618-
new Pair<>(entityWithGroup1, value),
619-
new Pair<>(entityWithGroup2, value),
620-
new Pair<>(entityWithoutGroup, null)),
652+
expect(entityWithGroup1, value, entityWithGroup1, value),
653+
expect(entityWithGroup2, value, entityWithGroup2, value),
654+
expect(entityWithoutGroup, null, entityWithoutGroup)),
621655
pf -> pf.forEachUnfiltered(TestdataLavishEntity.class)
622656
.join(TestdataLavishValue.class)
623657
.filter((entity, joinedValue) -> entity.getEntityGroup() != null)

0 commit comments

Comments
 (0)