Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -16,6 +19,9 @@
* <p>
* 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}.
* <p>
* 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,6 +24,9 @@
* <p>
* Do not annotate {@link PlanningEntity planning entities} as problem facts:
* they are automatically available as facts for {@link ConstraintFactory#forEach(Class)}.
* <p>
* 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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
*
* <p>
* 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}.
*
* <p>
* 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.
*
* <p>
* 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.
*
* <p>
* 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -38,7 +39,7 @@ protected AbstractForEachUniNode(Class<A> forEachClass, TupleLifecycle<UniTuple<
this.propagationQueue = new StaticPropagationQueue<>(nextNodesTupleLifecycle);
}

public void insert(A a) {
public void insert(@Nullable A a) {
var tuple = new UniTuple<>(a, outputStoreSize);
var old = tupleMap.put(a, tuple);
if (old != null) {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
import org.jspecify.annotations.NullMarked;

@NullMarked
public sealed class ForEachIncludingUnassignedUniNode<A>
extends AbstractForEachUniNode<A>
permits ForEachFromSolutionUniNode {
public final class ForEachIncludingUnassignedUniNode<A>
extends AbstractForEachUniNode<A> {

public ForEachIncludingUnassignedUniNode(Class<A> forEachClass, TupleLifecycle<UniTuple<A>> nextNodesTupleLifecycle,
int outputStoreSize) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public record DefaultPlanningListVariableMetaModel<Solution_, Entity_, Value_>(
ListVariableDescriptor<Solution_> variableDescriptor)
implements
PlanningListVariableMetaModel<Solution_, Entity_, Value_>,
InnerVariableMetaModel<Solution_> {
InnerGenuineVariableMetaModel<Solution_> {

@SuppressWarnings("unchecked")
@Override
Expand All @@ -27,6 +27,11 @@ public String name() {
return variableDescriptor.getVariableName();
}

@Override
public boolean hasValueRangeOnEntity() {
return !variableDescriptor.isValueRangeEntityIndependent();
}

@Override
public boolean allowsUnassignedValues() {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public record DefaultPlanningVariableMetaModel<Solution_, Entity_, Value_>(
BasicVariableDescriptor<Solution_> variableDescriptor)
implements
PlanningVariableMetaModel<Solution_, Entity_, Value_>,
InnerVariableMetaModel<Solution_> {
InnerGenuineVariableMetaModel<Solution_> {

@SuppressWarnings("unchecked")
@Override
Expand All @@ -27,6 +27,11 @@ public String name() {
return variableDescriptor.getVariableName();
}

@Override
public boolean hasValueRangeOnEntity() {
return !variableDescriptor.isValueRangeEntityIndependent();
}

@Override
public boolean allowsUnassigned() {
return variableDescriptor.allowsUnassigned();
Expand Down Expand Up @@ -58,4 +63,5 @@ public String toString() {
return "Genuine Variable '%s %s.%s' (allowsUnassigned: %b, isChained: %b)"
.formatted(type(), entity.getClass().getSimpleName(), name(), allowsUnassigned(), isChained());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package ai.timefold.solver.core.impl.domain.solution.descriptor;

import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor;

import org.jspecify.annotations.NullMarked;

@NullMarked
public sealed interface InnerGenuineVariableMetaModel<Solution_>
extends InnerVariableMetaModel<Solution_>
permits DefaultPlanningVariableMetaModel, DefaultPlanningListVariableMetaModel {

@Override
GenuineVariableDescriptor<Solution_> variableDescriptor();

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

@NullMarked
public sealed interface InnerVariableMetaModel<Solution_>
permits DefaultPlanningVariableMetaModel, DefaultPlanningListVariableMetaModel, DefaultShadowVariableMetaModel {
permits InnerGenuineVariableMetaModel, DefaultShadowVariableMetaModel {

VariableDescriptor<Solution_> variableDescriptor();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
package ai.timefold.solver.core.impl.domain.valuerange.buildin.collection;

import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.Set;

import ai.timefold.solver.core.impl.domain.valuerange.AbstractCountableValueRange;
import ai.timefold.solver.core.impl.heuristic.selector.common.iterator.CachedListRandomIterator;
import ai.timefold.solver.core.impl.util.CollectionUtils;

import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

@NullMarked
public final class ListValueRange<T> extends AbstractCountableValueRange<T> {

private static final int LIST_SIZE_LOOKUP_LIMIT = 10; // Selected arbitrarily.

private final List<T> list;
private @Nullable Set<T> lookupSet; // Initialized lazily for large lists.

public ListValueRange(List<T> list) {
this.list = list;
Expand All @@ -25,14 +34,26 @@ public long getSize() {
@Override
public T get(long index) {
if (index > Integer.MAX_VALUE) {
throw new IndexOutOfBoundsException("The index (" + index + ") must fit in an int.");
throw new IndexOutOfBoundsException("The index (%d) must fit in an int."
.formatted(index));
}
return list.get((int) index);
}

@Override
public boolean contains(T value) {
return list.contains(value);
public boolean contains(@Nullable T value) {
if (list.size() > LIST_SIZE_LOOKUP_LIMIT) {
if (lookupSet == null) {
lookupSet = Collections.newSetFromMap(CollectionUtils.newIdentityHashMap(list.size()));
lookupSet.addAll(list);
if (lookupSet.size() != list.size()) {
throw new IllegalStateException("The value range contains duplicate values: " + list);
}
}
return lookupSet.contains(value);
} else { // For small lists, sequential scanning is not a performance issue.
return list.contains(value);
}
}

@Override
Expand All @@ -46,9 +67,8 @@ public boolean contains(T value) {
}

@Override
public String toString() {
// Formatting: interval (mathematics) ISO 31-11
return list.isEmpty() ? "[]" : "[" + list.get(0) + "-" + list.get(list.size() - 1) + "]";
public String toString() { // Formatting: interval (mathematics) ISO 31-11
return list.isEmpty() ? "[]" : "[%s-%s]".formatted(list.get(0), list.get(list.size() - 1));
}

}
Loading
Loading