diff --git a/core/src/main/java/ai/timefold/solver/core/api/domain/solution/PlanningEntityCollectionProperty.java b/core/src/main/java/ai/timefold/solver/core/api/domain/solution/PlanningEntityCollectionProperty.java index 632e937d311..2fb7294fda4 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/domain/solution/PlanningEntityCollectionProperty.java +++ b/core/src/main/java/ai/timefold/solver/core/api/domain/solution/PlanningEntityCollectionProperty.java @@ -7,6 +7,9 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.SortedSet; import ai.timefold.solver.core.api.domain.entity.PlanningEntity; import ai.timefold.solver.core.api.score.director.ScoreDirector; @@ -16,6 +19,9 @@ *
* Every element in the planning entity collection should have the {@link PlanningEntity} annotation. * Every element in the planning entity collection will be added to the {@link ScoreDirector}. + *
+ * For solver reproducibility, the collection must have a deterministic, stable iteration order. + * It is recommended to use a {@link List}, {@link LinkedHashSet} or {@link SortedSet}. */ @Target({ METHOD, FIELD }) @Retention(RUNTIME) diff --git a/core/src/main/java/ai/timefold/solver/core/api/domain/solution/ProblemFactCollectionProperty.java b/core/src/main/java/ai/timefold/solver/core/api/domain/solution/ProblemFactCollectionProperty.java index 50c1ced5a35..589dbe160d5 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/domain/solution/ProblemFactCollectionProperty.java +++ b/core/src/main/java/ai/timefold/solver/core/api/domain/solution/ProblemFactCollectionProperty.java @@ -7,6 +7,9 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.SortedSet; import ai.timefold.solver.core.api.domain.entity.PlanningEntity; import ai.timefold.solver.core.api.score.stream.ConstraintFactory; @@ -21,6 +24,9 @@ *
* Do not annotate {@link PlanningEntity planning entities} as problem facts: * they are automatically available as facts for {@link ConstraintFactory#forEach(Class)}. + *
+ * For solver reproducibility, the collection must have a deterministic, stable iteration order. + * It is recommended to use a {@link List}, {@link LinkedHashSet} or {@link SortedSet}. * * @see ProblemFactProperty */ diff --git a/core/src/main/java/ai/timefold/solver/core/api/domain/valuerange/ValueRangeProvider.java b/core/src/main/java/ai/timefold/solver/core/api/domain/valuerange/ValueRangeProvider.java index 4ab61e1a629..6d75f595b49 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/domain/valuerange/ValueRangeProvider.java +++ b/core/src/main/java/ai/timefold/solver/core/api/domain/valuerange/ValueRangeProvider.java @@ -7,18 +7,50 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.SortedSet; +import ai.timefold.solver.core.api.domain.entity.PlanningEntity; +import ai.timefold.solver.core.api.domain.variable.PlanningListVariable; import ai.timefold.solver.core.api.domain.variable.PlanningVariable; import ai.timefold.solver.core.api.solver.SolverFactory; +import ai.timefold.solver.core.api.solver.change.ProblemChange; import org.jspecify.annotations.NonNull; /** * Provides the planning values that can be used for a {@link PlanningVariable}. + * *
* This is specified on a getter of a java bean property (or directly on a field) * which returns a {@link Collection} or {@link ValueRange}. * A {@link Collection} is implicitly converted to a {@link ValueRange}. + * For solver reproducibility, the collection must have a deterministic, stable iteration order. + * It is recommended to use a {@link List}, {@link LinkedHashSet} or {@link SortedSet}. + * + *
+ * Value ranges are not allowed to contain {@code null} values. + * The solver will automatically add a null to any range + * when {@link PlanningVariable#allowsUnassigned()} or {@link PlanningListVariable#allowsUnassignedValues()} is true. + * + *
+ * Value ranges are not allowed to contain multiple copies of the same object, + * as defined by {@code ==}. + * It is recommended that the value range never contains two objects + * that are equal according to {@link Object#equals(Object)}, + * but this is not enforced to not depend on user-defined {@link Object#equals(Object)} implementations. + * Having duplicates in a value range can lead to unexpected behavior, + * and skews selection probabilities in random selection algorithms. + * + *
+ * Value ranges are not allowed to change during solving.
+ * This is especially important for value ranges defined on {@link PlanningEntity}-annotated classes;
+ * these must never depend on any of that entity's variables, or on any other entity's variables.
+ * If you need to change a value range defined on an entity,
+ * trigger a {@link ProblemChange} for that entity or restart the solver with an updated planning solution.
+ * If you need to change a value range defined on a planning solution,
+ * restart the solver with a new planning solution.
*/
@Target({ METHOD, FIELD })
@Retention(RUNTIME)
diff --git a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractForEachUniNode.java b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractForEachUniNode.java
index 46e02bded61..1b2ef60ef4f 100644
--- a/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractForEachUniNode.java
+++ b/core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractForEachUniNode.java
@@ -12,6 +12,7 @@
import ai.timefold.solver.core.impl.domain.variable.supply.SupplyManager;
import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
/**
* Filtering nodes are expensive.
@@ -38,7 +39,7 @@ protected AbstractForEachUniNode(Class forEachClass, TupleLifecycle
- *
- *
- * @param