Skip to content

Commit 7ec00b1

Browse files
authored
chore: move streams value range on entity for basic vars (#1707)
Also addresses issues with composite ranges which could possibly duplicate elements.
1 parent 75863ea commit 7ec00b1

File tree

118 files changed

+2652
-1103
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

118 files changed

+2652
-1103
lines changed

core/src/main/java/ai/timefold/solver/core/api/domain/solution/PlanningEntityCollectionProperty.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
import java.lang.annotation.Retention;
88
import java.lang.annotation.Target;
99
import java.util.Collection;
10+
import java.util.LinkedHashSet;
11+
import java.util.List;
12+
import java.util.SortedSet;
1013

1114
import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
1215
import ai.timefold.solver.core.api.score.director.ScoreDirector;
@@ -16,6 +19,9 @@
1619
* <p>
1720
* Every element in the planning entity collection should have the {@link PlanningEntity} annotation.
1821
* Every element in the planning entity collection will be added to the {@link ScoreDirector}.
22+
* <p>
23+
* For solver reproducibility, the collection must have a deterministic, stable iteration order.
24+
* It is recommended to use a {@link List}, {@link LinkedHashSet} or {@link SortedSet}.
1925
*/
2026
@Target({ METHOD, FIELD })
2127
@Retention(RUNTIME)

core/src/main/java/ai/timefold/solver/core/api/domain/solution/ProblemFactCollectionProperty.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
import java.lang.annotation.Retention;
88
import java.lang.annotation.Target;
99
import java.util.Collection;
10+
import java.util.LinkedHashSet;
11+
import java.util.List;
12+
import java.util.SortedSet;
1013

1114
import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
1215
import ai.timefold.solver.core.api.score.stream.ConstraintFactory;
@@ -21,7 +24,10 @@
2124
* <p>
2225
* Do not annotate {@link PlanningEntity planning entities} as problem facts:
2326
* they are automatically available as facts for {@link ConstraintFactory#forEach(Class)}.
24-
*
27+
* <p>
28+
* For solver reproducibility, the collection must have a deterministic, stable iteration order.
29+
* It is recommended to use a {@link List}, {@link LinkedHashSet} or {@link SortedSet}.
30+
*
2531
* @see ProblemFactProperty
2632
*/
2733
@Target({ METHOD, FIELD })

core/src/main/java/ai/timefold/solver/core/api/domain/valuerange/CountableValueRange.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,20 @@
44

55
import ai.timefold.solver.core.api.domain.variable.PlanningVariable;
66

7-
import org.jspecify.annotations.NonNull;
7+
import org.jspecify.annotations.NullMarked;
88
import org.jspecify.annotations.Nullable;
99

1010
/**
1111
* A {@link ValueRange} that is ending. Therefore, it has a discrete (as in non-continuous) range.
12-
*
12+
* <p>
13+
* Don't implement this interface directly.
14+
* If you can't use a collection to store the values,
15+
* use {@link ValueRangeFactory} to get an instance of a {@link CountableValueRange}.
16+
*
1317
* @see ValueRangeFactory
1418
* @see ValueRange
1519
*/
20+
@NullMarked
1621
public interface CountableValueRange<T> extends ValueRange<T> {
1722

1823
/**
@@ -36,7 +41,6 @@ public interface CountableValueRange<T> extends ValueRange<T> {
3641
/**
3742
* Select the elements in original (natural) order.
3843
*/
39-
@NonNull
4044
Iterator<T> createOriginalIterator();
4145

4246
}

core/src/main/java/ai/timefold/solver/core/api/domain/valuerange/ValueRange.java

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,30 @@
66
import java.util.Random;
77
import java.util.Set;
88

9+
import ai.timefold.solver.core.api.domain.variable.PlanningListVariable;
910
import ai.timefold.solver.core.api.domain.variable.PlanningVariable;
1011

11-
import org.jspecify.annotations.NonNull;
12+
import org.jspecify.annotations.NullMarked;
1213
import org.jspecify.annotations.Nullable;
1314

1415
/**
15-
* A ValueRange is a set of a values for a {@link PlanningVariable}.
16+
* A ValueRange is a set of a values for a {@link PlanningVariable} or {@link PlanningListVariable}.
1617
* These values might be stored in memory as a {@link Collection} (usually a {@link List} or {@link Set}),
1718
* but if the values are numbers, they can also be stored in memory by their bounds
1819
* to use less memory and provide more opportunities.
1920
* <p>
20-
* ValueRange is stateful.
21+
* ValueRange is stateless, and its contents must not depend on any planning variables.
2122
* Implementations must be immutable.
2223
* <p>
23-
* Prefer using {@link CountableValueRange}.
24-
* In a future version of Timefold Solver, uncountable value ranges will not be allowed,
25-
* and certain recently introduced features already do not support them.
24+
* Don't implement this interface directly.
25+
* If you can't use a collection to store the values,
26+
* use {@link ValueRangeFactory} to get an instance of a {@link CountableValueRange}.
2627
*
27-
* @see ValueRangeFactory
2828
* @see CountableValueRange
29+
* @see ValueRangeProvider
30+
* @see ValueRangeFactory
2931
*/
32+
@NullMarked
3033
public interface ValueRange<T> {
3134

3235
/**
@@ -50,7 +53,6 @@ public interface ValueRange<T> {
5053
* @param workingRandom the {@link Random} to use when any random number is needed,
5154
* so runs are reproducible.
5255
*/
53-
@NonNull
54-
Iterator<T> createRandomIterator(@NonNull Random workingRandom);
56+
Iterator<T> createRandomIterator(Random workingRandom);
5557

5658
}

core/src/main/java/ai/timefold/solver/core/api/domain/valuerange/ValueRangeProvider.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,50 @@
77
import java.lang.annotation.Retention;
88
import java.lang.annotation.Target;
99
import java.util.Collection;
10+
import java.util.LinkedHashSet;
11+
import java.util.List;
12+
import java.util.SortedSet;
1013

14+
import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
15+
import ai.timefold.solver.core.api.domain.variable.PlanningListVariable;
1116
import ai.timefold.solver.core.api.domain.variable.PlanningVariable;
1217
import ai.timefold.solver.core.api.solver.SolverFactory;
18+
import ai.timefold.solver.core.api.solver.change.ProblemChange;
1319

1420
import org.jspecify.annotations.NonNull;
1521

1622
/**
1723
* Provides the planning values that can be used for a {@link PlanningVariable}.
24+
*
1825
* <p>
1926
* This is specified on a getter of a java bean property (or directly on a field)
2027
* which returns a {@link Collection} or {@link ValueRange}.
2128
* A {@link Collection} is implicitly converted to a {@link ValueRange}.
29+
* For solver reproducibility, the collection must have a deterministic, stable iteration order.
30+
* It is recommended to use a {@link List}, {@link LinkedHashSet} or {@link SortedSet}.
31+
*
32+
* <p>
33+
* Value ranges are not allowed to contain {@code null} values.
34+
* When {@link PlanningVariable#allowsUnassigned()} or {@link PlanningListVariable#allowsUnassignedValues()} is true,
35+
* the solver will handle {@code null} values on its own.
36+
*
37+
* <p>
38+
* Value ranges are not allowed to contain multiple copies of the same object,
39+
* as defined by {@code ==}.
40+
* It is recommended that the value range never contains two objects
41+
* that are equal according to {@link Object#equals(Object)},
42+
* but this is not enforced to not depend on user-defined {@link Object#equals(Object)} implementations.
43+
* Having duplicates in a value range can lead to unexpected behavior,
44+
* and skews selection probabilities in random selection algorithms.
45+
*
46+
* <p>
47+
* Value ranges are not allowed to change during solving.
48+
* This is especially important for value ranges defined on {@link PlanningEntity}-annotated classes;
49+
* these must never depend on any of that entity's variables, or on any other entity's variables.
50+
* If you need to change a value range defined on an entity,
51+
* trigger a {@link ProblemChange} for that entity or restart the solver with an updated planning solution.
52+
* If you need to change a value range defined on a planning solution,
53+
* restart the solver with a new planning solution.
2254
*/
2355
@Target({ METHOD, FIELD })
2456
@Retention(RUNTIME)

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import ai.timefold.solver.core.impl.domain.variable.supply.SupplyManager;
1313

1414
import org.jspecify.annotations.NullMarked;
15+
import org.jspecify.annotations.Nullable;
1516

1617
/**
1718
* Filtering nodes are expensive.
@@ -38,7 +39,7 @@ protected AbstractForEachUniNode(Class<A> forEachClass, TupleLifecycle<UniTuple<
3839
this.propagationQueue = new StaticPropagationQueue<>(nextNodesTupleLifecycle);
3940
}
4041

41-
public void insert(A a) {
42+
public void insert(@Nullable A a) {
4243
var tuple = new UniTuple<>(a, outputStoreSize);
4344
var old = tupleMap.put(a, tuple);
4445
if (old != null) {
@@ -48,9 +49,9 @@ public void insert(A a) {
4849
propagationQueue.insert(tuple);
4950
}
5051

51-
public abstract void update(A a);
52+
public abstract void update(@Nullable A a);
5253

53-
protected final void updateExisting(A a, UniTuple<A> tuple) {
54+
protected final void updateExisting(@Nullable A a, UniTuple<A> tuple) {
5455
var state = tuple.state;
5556
if (state.isDirty()) {
5657
if (state == TupleState.DYING || state == TupleState.ABORTING) {
@@ -63,7 +64,7 @@ protected final void updateExisting(A a, UniTuple<A> tuple) {
6364
}
6465
}
6566

66-
public void retract(A a) {
67+
public void retract(@Nullable A a) {
6768
var tuple = tupleMap.remove(a);
6869
if (tuple == null) {
6970
throw new IllegalStateException("The fact (%s) was never inserted, so it cannot retract."
@@ -72,7 +73,7 @@ public void retract(A a) {
7273
retractExisting(a, tuple);
7374
}
7475

75-
protected void retractExisting(A a, UniTuple<A> tuple) {
76+
protected void retractExisting(@Nullable A a, UniTuple<A> tuple) {
7677
var state = tuple.state;
7778
if (state.isDirty()) {
7879
if (state == TupleState.DYING || state == TupleState.ABORTING) {

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ public final class ForEachExcludingPinnedUniNode<Solution_, A>
3131
*
3232
* @param entityMetaModel Expects a pinnable entity.
3333
* Every other option should have already been exluded and passed to a different kind of node.
34-
* @param nextNodesTupleLifecycle
35-
* @param outputStoreSize
3634
*/
3735
public ForEachExcludingPinnedUniNode(PlanningEntityMetaModel<Solution_, A> entityMetaModel,
3836
TupleLifecycle<UniTuple<A>> nextNodesTupleLifecycle, int outputStoreSize) {
@@ -57,15 +55,15 @@ private Predicate<A> buildFilter(Solution_ workingSolution, SupplyManager supply
5755
}
5856

5957
@Override
60-
public void insert(A a) {
58+
public void insert(@Nullable A a) {
6159
if (!filter.test(a)) { // Skip inserting the tuple as it does not pass the filter.
6260
return;
6361
}
6462
super.insert(a);
6563
}
6664

6765
@Override
68-
public void update(A a) {
66+
public void update(@Nullable A a) {
6967
var tuple = tupleMap.get(a);
7068
if (tuple == null) { // The tuple was never inserted because it did not pass the filter.
7169
insert(a);
@@ -77,7 +75,7 @@ public void update(A a) {
7775
}
7876

7977
@Override
80-
public void retract(A a) {
78+
public void retract(@Nullable A a) {
8179
var tuple = tupleMap.remove(a);
8280
if (tuple == null) { // The tuple was never inserted because it did not pass the filter.
8381
return;

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple;
88

99
import org.jspecify.annotations.NullMarked;
10+
import org.jspecify.annotations.Nullable;
1011

1112
@NullMarked
1213
public final class ForEachExcludingUnassignedUniNode<A>
@@ -21,15 +22,15 @@ public ForEachExcludingUnassignedUniNode(Class<A> forEachClass, Predicate<A> fil
2122
}
2223

2324
@Override
24-
public void insert(A a) {
25+
public void insert(@Nullable A a) {
2526
if (!filter.test(a)) { // Skip inserting the tuple as it does not pass the filter.
2627
return;
2728
}
2829
super.insert(a);
2930
}
3031

3132
@Override
32-
public void update(A a) {
33+
public void update(@Nullable A a) {
3334
var tuple = tupleMap.get(a);
3435
if (tuple == null) { // The tuple was never inserted because it did not pass the filter.
3536
insert(a);
@@ -41,7 +42,7 @@ public void update(A a) {
4142
}
4243

4344
@Override
44-
public void retract(A a) {
45+
public void retract(@Nullable A a) {
4546
var tuple = tupleMap.remove(a);
4647
if (tuple == null) { // The tuple was never inserted because it did not pass the filter.
4748
return;

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

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
package ai.timefold.solver.core.impl.bavet.uni;
22

3+
import java.util.Objects;
4+
35
import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle;
46
import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple;
7+
import ai.timefold.solver.core.impl.domain.valuerange.descriptor.ValueRangeDescriptor;
58
import ai.timefold.solver.core.impl.domain.variable.supply.SupplyManager;
6-
import ai.timefold.solver.core.impl.move.streams.FromSolutionValueCollectingFunction;
9+
import ai.timefold.solver.core.impl.score.director.ValueRangeManager;
710

811
import org.jspecify.annotations.NullMarked;
12+
import org.jspecify.annotations.Nullable;
913

1014
/**
1115
* Node that reads a property from a planning solution.
@@ -27,14 +31,19 @@ public final class ForEachFromSolutionUniNode<Solution_, A>
2731
extends ForEachIncludingUnassignedUniNode<A>
2832
implements AbstractForEachUniNode.InitializableForEachNode<Solution_> {
2933

30-
private final FromSolutionValueCollectingFunction<Solution_, A> valueCollectingFunction;
34+
private final ValueRangeManager<Solution_> valueRangeManager;
35+
private final ValueRangeDescriptor<Solution_> valueRangeDescriptor;
3136

3237
private boolean isInitialized = false;
3338

34-
public ForEachFromSolutionUniNode(FromSolutionValueCollectingFunction<Solution_, A> valueCollectingFunction,
35-
TupleLifecycle<UniTuple<A>> nextNodesTupleLifecycle, int outputStoreSize) {
36-
super(valueCollectingFunction.declaredClass(), nextNodesTupleLifecycle, outputStoreSize);
37-
this.valueCollectingFunction = valueCollectingFunction;
39+
@SuppressWarnings("unchecked")
40+
public ForEachFromSolutionUniNode(ValueRangeManager<Solution_> valueRangeManager,
41+
ValueRangeDescriptor<Solution_> valueRangeDescriptor, TupleLifecycle<UniTuple<A>> nextNodesTupleLifecycle,
42+
int outputStoreSize) {
43+
super((Class<A>) valueRangeDescriptor.getVariableDescriptor().getVariablePropertyType(), nextNodesTupleLifecycle,
44+
outputStoreSize);
45+
this.valueRangeManager = Objects.requireNonNull(valueRangeManager);
46+
this.valueRangeDescriptor = Objects.requireNonNull(valueRangeDescriptor);
3847
}
3948

4049
@Override
@@ -44,7 +53,7 @@ public void initialize(Solution_ workingSolution, SupplyManager supplyManager) {
4453
.formatted(this));
4554
} else {
4655
this.isInitialized = true;
47-
var valueRange = valueCollectingFunction.apply(workingSolution);
56+
var valueRange = valueRangeManager.<A> getFromSolution(valueRangeDescriptor, workingSolution);
4857
var valueIterator = valueRange.createOriginalIterator();
4958
while (valueIterator.hasNext()) {
5059
var value = valueIterator.next();
@@ -54,13 +63,13 @@ public void initialize(Solution_ workingSolution, SupplyManager supplyManager) {
5463
}
5564

5665
@Override
57-
public void insert(A a) {
66+
public void insert(@Nullable A a) {
5867
throw new UnsupportedOperationException("Impossible state: direct insert is not supported on %s."
5968
.formatted(this));
6069
}
6170

6271
@Override
63-
public void retract(A a) {
72+
public void retract(@Nullable A a) {
6473
throw new UnsupportedOperationException("Impossible state: direct retract is not supported on %s."
6574
.formatted(this));
6675
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple;
55

66
import org.jspecify.annotations.NullMarked;
7+
import org.jspecify.annotations.Nullable;
78

89
@NullMarked
910
public sealed class ForEachIncludingUnassignedUniNode<A>
@@ -16,7 +17,7 @@ public ForEachIncludingUnassignedUniNode(Class<A> forEachClass, TupleLifecycle<U
1617
}
1718

1819
@Override
19-
public void update(A a) {
20+
public void update(@Nullable A a) {
2021
var tuple = tupleMap.get(a);
2122
if (tuple == null) {
2223
throw new IllegalStateException("The fact (%s) was never inserted, so it cannot update."

0 commit comments

Comments
 (0)