Skip to content

Commit 75863ea

Browse files
authored
chore: prepare for list var value range on entity
This pull request introduces the ability to define value ranges for list variables in the entities. The proposed approach in the CH phase involves creating a new descriptor and selector that can read all values from the entities, serving as a single source of truth. This unique list of elements is required for the queued value placer to function correctly. Additionally, a new cache tier for value range elements is implemented to enable faster access to value ranges values. For the LS phase, the changes follow the same strategy used for basic variables, generating valid and invalid moves through the respective move generators. However, the logic for list moves in isMoveDoable has been updated to ensure that moves are valid when using entity value ranges. This approach makes the logic simpler and more performant in some cases, as generating only valid moves would require more complex changes than just ensuring it is doable. The list var-related code is hidden behind fail fasts. It may still be refactored before getting enabled, pending performance regression tests under various conditions.
1 parent 61ba827 commit 75863ea

File tree

202 files changed

+5141
-1300
lines changed

Some content is hidden

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

202 files changed

+5141
-1300
lines changed

core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/NearbyUtil.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ private static ValueSelectorConfig configureSecondaryValueSelector(ValueSelector
162162
.withValueSelectorConfig(valueConfig);
163163
}
164164

165-
private static String addRandomSuffix(String name, Random random) {
165+
public static String addRandomSuffix(String name, Random random) {
166166
var value = new StringBuilder(name);
167167
value.append("-");
168168
random.ints(97, 122) // ['a', 'z']

core/src/main/java/ai/timefold/solver/core/config/util/ConfigUtils.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package ai.timefold.solver.core.config.util;
22

33
import static ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessorFactory.MemberAccessorType.FIELD_OR_READ_METHOD;
4+
import static ai.timefold.solver.core.impl.domain.solution.cloner.DeepCloningUtils.IMMUTABLE_CLASSES;
45

56
import java.lang.annotation.Annotation;
67
import java.lang.reflect.AnnotatedElement;
@@ -449,6 +450,17 @@ Maybe the member (%s) should return a parameterized %s."""
449450
memberName, type, memberName, type.getSimpleName())));
450451
}
451452

453+
/**
454+
* @param type the class type
455+
* @return true if it is immutable; otherwise false
456+
*/
457+
public static boolean isGenericTypeImmutable(Class<?> type) {
458+
if (type == null) {
459+
return false;
460+
}
461+
return type.isRecord() || IMMUTABLE_CLASSES.contains(type);
462+
}
463+
452464
public static Optional<Class<?>> extractGenericTypeParameter(@NonNull String parentClassConcept,
453465
@NonNull Class<?> parentClass, @NonNull Class<?> type, @NonNull Type genericType,
454466
@Nullable Class<? extends Annotation> annotationClass, @NonNull String memberName) {

core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhase.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ public void solve(SolverScope<Solution_> solverScope) {
6464
// (When it exhausts all values, it will start over from the beginning.)
6565
// To prevent that, we need to limit the number of steps to the number of unassigned values.
6666
var workingSolution = phaseScope.getWorkingSolution();
67-
maxStepCount = solutionDescriptor.getListVariableDescriptor().countUnassigned(workingSolution);
67+
maxStepCount = solutionDescriptor.getListVariableDescriptor().countUnassigned(workingSolution,
68+
solverScope.getValueRangeManager());
6869
}
6970

7071
TerminationStatus earlyTerminationStatus = null;

core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/AbstractEntityPlacerFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ protected ChangeMoveSelectorConfig buildChangeMoveSelectorConfig(HeuristicConfig
2828
.withVariableName(variableDescriptor.getVariableName());
2929
if (ValueSelectorConfig.hasSorter(configPolicy.getValueSorterManner(), variableDescriptor)) {
3030
changeValueSelectorConfig = changeValueSelectorConfig
31-
.withCacheType(variableDescriptor.isValueRangeEntityIndependent() ? PHASE : STEP)
31+
.withCacheType(variableDescriptor.canExtractValueRangeFromSolution() ? PHASE : STEP)
3232
.withSelectionOrder(SelectionOrder.SORTED)
3333
.withSorterManner(configPolicy.getValueSorterManner());
3434
}

core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/QueuedValuePlacer.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@
99
import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector;
1010
import ai.timefold.solver.core.impl.heuristic.selector.move.factory.MoveIteratorFactory;
1111
import ai.timefold.solver.core.impl.heuristic.selector.move.generic.list.ListChangeMoveSelector;
12-
import ai.timefold.solver.core.impl.heuristic.selector.value.EntityIndependentValueSelector;
13-
import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.EntityIndependentFilteringValueSelector;
12+
import ai.timefold.solver.core.impl.heuristic.selector.value.IterableValueSelector;
1413
import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.FilteringValueSelector;
14+
import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.IterableFilteringValueSelector;
1515

1616
public class QueuedValuePlacer<Solution_> extends AbstractEntityPlacer<Solution_> implements EntityPlacer<Solution_> {
1717

18-
protected final EntityIndependentValueSelector<Solution_> valueSelector;
18+
protected final IterableValueSelector<Solution_> valueSelector;
1919
protected final MoveSelector<Solution_> moveSelector;
2020

2121
public QueuedValuePlacer(EntityPlacerFactory<Solution_> factory, HeuristicConfigPolicy<Solution_> configPolicy,
22-
EntityIndependentValueSelector<Solution_> valueSelector, MoveSelector<Solution_> moveSelector) {
22+
IterableValueSelector<Solution_> valueSelector, MoveSelector<Solution_> moveSelector) {
2323
super(factory, configPolicy);
2424
this.valueSelector = valueSelector;
2525
this.moveSelector = moveSelector;
@@ -67,7 +67,7 @@ protected Placement<Solution_> createUpcomingSelection() {
6767
@Override
6868
public EntityPlacer<Solution_> rebuildWithFilter(SelectionFilter<Solution_, Object> filter) {
6969
return new QueuedValuePlacer<>(factory, configPolicy,
70-
(EntityIndependentFilteringValueSelector<Solution_>) FilteringValueSelector.of(valueSelector, filter),
70+
(IterableFilteringValueSelector<Solution_>) FilteringValueSelector.of(valueSelector, filter),
7171
moveSelector);
7272
}
7373

core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/placer/QueuedValuePlacerFactory.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,17 @@
1515
import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy;
1616
import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector;
1717
import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelectorFactory;
18-
import ai.timefold.solver.core.impl.heuristic.selector.value.EntityIndependentValueSelector;
18+
import ai.timefold.solver.core.impl.heuristic.selector.value.IterableValueSelector;
1919
import ai.timefold.solver.core.impl.heuristic.selector.value.ValueSelector;
2020
import ai.timefold.solver.core.impl.heuristic.selector.value.ValueSelectorFactory;
2121

2222
public class QueuedValuePlacerFactory<Solution_>
2323
extends AbstractEntityPlacerFactory<Solution_, QueuedValuePlacerConfig> {
2424

2525
public static QueuedValuePlacerConfig unfoldNew(MoveSelectorConfig templateMoveSelectorConfig) {
26-
throw new UnsupportedOperationException("The <constructionHeuristic> contains a moveSelector ("
27-
+ templateMoveSelectorConfig + ") and the <queuedValuePlacer> does not support unfolding those yet.");
26+
throw new UnsupportedOperationException(
27+
"The <constructionHeuristic> contains a moveSelector (%s) and the <queuedValuePlacer> does not support unfolding those yet."
28+
.formatted(templateMoveSelectorConfig));
2829
}
2930

3031
public QueuedValuePlacerFactory(QueuedValuePlacerConfig placerConfig) {
@@ -48,15 +49,14 @@ public QueuedValuePlacer<Solution_> buildEntityPlacer(HeuristicConfigPolicy<Solu
4849

4950
MoveSelector<Solution_> moveSelector = MoveSelectorFactory.<Solution_> create(moveSelectorConfig_)
5051
.buildMoveSelector(configPolicy, SelectionCacheType.JUST_IN_TIME, SelectionOrder.ORIGINAL, false);
51-
if (!(valueSelector instanceof EntityIndependentValueSelector)) {
52-
throw new IllegalArgumentException("The queuedValuePlacer (" + this
53-
+ ") needs to be based on an "
54-
+ EntityIndependentValueSelector.class.getSimpleName() + " (" + valueSelector + ")."
55-
+ " Check your @" + ValueRangeProvider.class.getSimpleName() + " annotations.");
52+
if (!(valueSelector instanceof IterableValueSelector<Solution_> iterableValueSelector)) {
53+
throw new IllegalArgumentException(
54+
"The queuedValuePlacer (%s) needs to be based on an %s (%s). Check your @%s annotations.".formatted(this,
55+
IterableValueSelector.class.getSimpleName(), valueSelector,
56+
ValueRangeProvider.class.getSimpleName()));
5657

5758
}
58-
return new QueuedValuePlacer<>(this, configPolicy, (EntityIndependentValueSelector<Solution_>) valueSelector,
59-
moveSelector);
59+
return new QueuedValuePlacer<>(this, configPolicy, iterableValueSelector, moveSelector);
6060
}
6161

6262
private ValueSelectorConfig buildValueSelectorConfig(HeuristicConfigPolicy<Solution_> configPolicy,

core/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@
2727
import ai.timefold.solver.core.api.domain.entity.PlanningPin;
2828
import ai.timefold.solver.core.api.domain.entity.PlanningPinToIndex;
2929
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
30-
import ai.timefold.solver.core.api.domain.valuerange.CountableValueRange;
31-
import ai.timefold.solver.core.api.domain.valuerange.ValueRange;
3230
import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider;
3331
import ai.timefold.solver.core.api.domain.variable.AnchorShadowVariable;
3432
import ai.timefold.solver.core.api.domain.variable.CascadingUpdateShadowVariable;
@@ -67,6 +65,7 @@
6765
import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.ComparatorSelectionSorter;
6866
import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter;
6967
import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.WeightFactorySelectionSorter;
68+
import ai.timefold.solver.core.impl.score.director.ValueRangeManager;
7069
import ai.timefold.solver.core.impl.util.CollectionUtils;
7170
import ai.timefold.solver.core.impl.util.MutableInt;
7271
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningEntityMetaModel;
@@ -859,18 +858,31 @@ public PlanningPinToIndexReader getEffectivePlanningPinToIndexReader() {
859858
return effectivePlanningPinToIndexReader;
860859
}
861860

862-
public long getMaximumValueCount(Solution_ solution, Object entity) {
861+
public long getMaximumValueCount(Solution_ solution, Object entity, ValueRangeManager<Solution_> valueRangeManager) {
863862
var maximumValueCount = 0L;
864863
for (var variableDescriptor : effectiveGenuineVariableDescriptorList) {
865-
maximumValueCount = Math.max(maximumValueCount, variableDescriptor.getValueRangeSize(solution, entity));
864+
if (variableDescriptor.canExtractValueRangeFromSolution()) {
865+
maximumValueCount = Math.max(maximumValueCount,
866+
valueRangeManager.countOnSolution(variableDescriptor.getValueRangeDescriptor(),
867+
solution));
868+
} else {
869+
maximumValueCount = Math.max(maximumValueCount,
870+
valueRangeManager.countOnEntity(variableDescriptor.getValueRangeDescriptor(),
871+
entity));
872+
873+
}
866874
}
867875
return maximumValueCount;
868876

869877
}
870878

871-
public void processProblemScale(Solution_ solution, Object entity, ProblemScaleTracker tracker) {
879+
public void processProblemScale(Solution_ solution, Object entity, ProblemScaleTracker tracker,
880+
ValueRangeManager<Solution_> valueRangeManager) {
872881
for (var variableDescriptor : effectiveGenuineVariableDescriptorList) {
873-
var valueCount = variableDescriptor.getValueRangeSize(solution, entity);
882+
var valueCount = variableDescriptor.canExtractValueRangeFromSolution()
883+
? valueRangeManager.countOnSolution(variableDescriptor.getValueRangeDescriptor(),
884+
solution)
885+
: valueRangeManager.countOnEntity(variableDescriptor.getValueRangeDescriptor(), entity);
874886
// TODO: When minimum Java supported is 21, this can be replaced with a sealed interface switch
875887
if (variableDescriptor instanceof BasicVariableDescriptor<Solution_> basicVariableDescriptor) {
876888
if (basicVariableDescriptor.isChained()) {
@@ -880,34 +892,34 @@ public void processProblemScale(Solution_ solution, Object entity, ProblemScaleT
880892
tracker.addPinnedListValueCount(1);
881893
}
882894
// Anchors are entities
883-
var valueRange = variableDescriptor.getValueRangeDescriptor().extractValueRange(solution, entity);
884-
if (valueRange instanceof CountableValueRange<?> countableValueRange) {
885-
var valueIterator = countableValueRange.createOriginalIterator();
886-
while (valueIterator.hasNext()) {
887-
var value = valueIterator.next();
888-
if (variableDescriptor.isValuePotentialAnchor(value)) {
889-
if (tracker.isAnchorVisited(value)) {
890-
continue;
891-
}
892-
// Assumes anchors are not pinned
893-
tracker.incrementListEntityCount(true);
895+
var valueRange = variableDescriptor.canExtractValueRangeFromSolution()
896+
? valueRangeManager.getFromSolution(variableDescriptor.getValueRangeDescriptor(),
897+
solution)
898+
: valueRangeManager.getFromEntity(variableDescriptor.getValueRangeDescriptor(),
899+
entity);
900+
var valueIterator = valueRange.createOriginalIterator();
901+
while (valueIterator.hasNext()) {
902+
var value = valueIterator.next();
903+
if (variableDescriptor.isValuePotentialAnchor(value)) {
904+
if (tracker.isAnchorVisited(value)) {
905+
continue;
894906
}
907+
// Assumes anchors are not pinned
908+
tracker.incrementListEntityCount(true);
895909
}
896-
} else {
897-
throw new IllegalStateException("""
898-
The value range (%s) for variable (%s) is not countable.
899-
Verify that a @%s does not return a %s when it can return %s or %s.
900-
""".formatted(valueRange, variableDescriptor.getSimpleEntityAndVariableName(),
901-
ValueRangeProvider.class.getSimpleName(), ValueRange.class.getSimpleName(),
902-
CountableValueRange.class.getSimpleName(), Collection.class.getSimpleName()));
903910
}
904911
} else {
905912
if (isMovable(solution, entity)) {
906913
tracker.addBasicProblemScale(valueCount);
907914
}
908915
}
909916
} else if (variableDescriptor instanceof ListVariableDescriptor<Solution_> listVariableDescriptor) {
910-
tracker.setListTotalValueCount((int) listVariableDescriptor.getValueRangeSize(solution, entity));
917+
var size = variableDescriptor.canExtractValueRangeFromSolution()
918+
? valueRangeManager.countOnSolution(listVariableDescriptor.getValueRangeDescriptor(),
919+
solution)
920+
: valueRangeManager.countOnEntity(listVariableDescriptor.getValueRangeDescriptor(),
921+
entity);
922+
tracker.setListTotalValueCount((int) size);
911923
if (isMovable(solution, entity)) {
912924
tracker.incrementListEntityCount(true);
913925
tracker.addPinnedListValueCount(listVariableDescriptor.getFirstUnpinnedIndex(entity));

core/src/main/java/ai/timefold/solver/core/impl/domain/solution/cloner/DeepCloningUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
public final class DeepCloningUtils {
4040

4141
// Instances of these JDK classes will never be deep-cloned.
42-
private static final Set<Class<?>> IMMUTABLE_CLASSES = Set.of(
42+
public static final Set<Class<?>> IMMUTABLE_CLASSES = Set.of(
4343
// Numbers
4444
Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class, BigInteger.class, BigDecimal.class,
4545
// Optional

0 commit comments

Comments
 (0)