Skip to content

Commit f212a67

Browse files
committed
feat: support value range sorting in ValueSelectorFactory
1 parent 00f95c0 commit f212a67

File tree

6 files changed

+112
-62
lines changed

6 files changed

+112
-62
lines changed

core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/FromEntityPropertyValueSelector.java

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import ai.timefold.solver.core.impl.domain.valuerange.descriptor.ValueRangeDescriptor;
99
import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor;
1010
import ai.timefold.solver.core.impl.heuristic.selector.AbstractDemandEnabledSelector;
11+
import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter;
1112
import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope;
1213
import ai.timefold.solver.core.impl.score.director.InnerScoreDirector;
1314
import ai.timefold.solver.core.impl.solver.scope.SolverScope;
@@ -22,13 +23,16 @@ public final class FromEntityPropertyValueSelector<Solution_>
2223
implements ValueSelector<Solution_> {
2324

2425
private final ValueRangeDescriptor<Solution_> valueRangeDescriptor;
26+
private final SelectionSorter<Solution_, Object> selectionSorter;
2527
private final boolean randomSelection;
2628

2729
private CountableValueRange<Object> countableValueRange;
2830
private InnerScoreDirector<Solution_, ?> scoreDirector;
2931

30-
public FromEntityPropertyValueSelector(ValueRangeDescriptor<Solution_> valueRangeDescriptor, boolean randomSelection) {
32+
public FromEntityPropertyValueSelector(ValueRangeDescriptor<Solution_> valueRangeDescriptor,
33+
SelectionSorter<Solution_, Object> selectionSorter, boolean randomSelection) {
3134
this.valueRangeDescriptor = valueRangeDescriptor;
35+
this.selectionSorter = selectionSorter;
3236
this.randomSelection = randomSelection;
3337
}
3438

@@ -51,7 +55,7 @@ public void solvingEnded(SolverScope<Solution_> solverScope) {
5155
@Override
5256
public void phaseStarted(AbstractPhaseScope<Solution_> phaseScope) {
5357
super.phaseStarted(phaseScope);
54-
this.countableValueRange = scoreDirector.getValueRangeManager().getFromSolution(valueRangeDescriptor);
58+
this.countableValueRange = scoreDirector.getValueRangeManager().getFromSolution(valueRangeDescriptor, selectionSorter);
5559
}
5660

5761
@Override
@@ -63,6 +67,9 @@ public void phaseEnded(AbstractPhaseScope<Solution_> phaseScope) {
6367
// ************************************************************************
6468
// Worker methods
6569
// ************************************************************************
70+
public SelectionSorter<Solution_, Object> getSelectionSorter() {
71+
return selectionSorter;
72+
}
6673

6774
@Override
6875
public GenuineVariableDescriptor<Solution_> getVariableDescriptor() {
@@ -92,7 +99,7 @@ public long getSize(Object entity) {
9299

93100
@Override
94101
public Iterator<Object> iterator(Object entity) {
95-
var valueRange = scoreDirector.getValueRangeManager().getFromEntity(valueRangeDescriptor, entity);
102+
var valueRange = scoreDirector.getValueRangeManager().getFromEntity(valueRangeDescriptor, entity, selectionSorter);
96103
if (!randomSelection) {
97104
return valueRange.createOriginalIterator();
98105
} else {
@@ -107,24 +114,22 @@ public Iterator<Object> endingIterator(Object entity) {
107114
// This logic aligns with the requirements for Nearby in the enterprise repository
108115
return countableValueRange.createOriginalIterator();
109116
} else {
110-
var valueRange = scoreDirector.getValueRangeManager().getFromEntity(valueRangeDescriptor, entity);
117+
var valueRange = scoreDirector.getValueRangeManager().getFromEntity(valueRangeDescriptor, entity, selectionSorter);
111118
return valueRange.createOriginalIterator();
112119
}
113120
}
114121

115122
@Override
116123
public boolean equals(Object o) {
117-
if (this == o)
118-
return true;
119-
if (o == null || getClass() != o.getClass())
124+
if (!(o instanceof FromEntityPropertyValueSelector<?> that))
120125
return false;
121-
FromEntityPropertyValueSelector<?> that = (FromEntityPropertyValueSelector<?>) o;
122-
return randomSelection == that.randomSelection && Objects.equals(valueRangeDescriptor, that.valueRangeDescriptor);
126+
return randomSelection == that.randomSelection && Objects.equals(valueRangeDescriptor, that.valueRangeDescriptor)
127+
&& Objects.equals(selectionSorter, that.selectionSorter);
123128
}
124129

125130
@Override
126131
public int hashCode() {
127-
return Objects.hash(valueRangeDescriptor, randomSelection);
132+
return Objects.hash(valueRangeDescriptor, selectionSorter, randomSelection);
128133
}
129134

130135
@Override

core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/IterableFromSolutionPropertyValueSelector.java

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import ai.timefold.solver.core.impl.domain.valuerange.descriptor.ValueRangeDescriptor;
1111
import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor;
1212
import ai.timefold.solver.core.impl.heuristic.selector.AbstractDemandEnabledSelector;
13+
import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter;
1314
import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope;
1415
import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope;
1516

@@ -21,6 +22,7 @@ public final class IterableFromSolutionPropertyValueSelector<Solution_>
2122
implements IterableValueSelector<Solution_> {
2223

2324
private final ValueRangeDescriptor<Solution_> valueRangeDescriptor;
25+
private final SelectionSorter<Solution_, Object> sorter;
2426
private final SelectionCacheType minimumCacheType;
2527
private final boolean randomSelection;
2628
private final boolean valueRangeMightContainEntity;
@@ -30,8 +32,9 @@ public final class IterableFromSolutionPropertyValueSelector<Solution_>
3032
private boolean cachedEntityListIsDirty = false;
3133

3234
public IterableFromSolutionPropertyValueSelector(ValueRangeDescriptor<Solution_> valueRangeDescriptor,
33-
SelectionCacheType minimumCacheType, boolean randomSelection) {
35+
SelectionSorter<Solution_, Object> sorter, SelectionCacheType minimumCacheType, boolean randomSelection) {
3436
this.valueRangeDescriptor = valueRangeDescriptor;
37+
this.sorter = sorter;
3538
this.minimumCacheType = minimumCacheType;
3639
this.randomSelection = randomSelection;
3740
valueRangeMightContainEntity = valueRangeDescriptor.mightContainEntity();
@@ -57,7 +60,7 @@ public void phaseStarted(AbstractPhaseScope<Solution_> phaseScope) {
5760
super.phaseStarted(phaseScope);
5861
var scoreDirector = phaseScope.getScoreDirector();
5962
cachedValueRange = scoreDirector.getValueRangeManager().getFromSolution(valueRangeDescriptor,
60-
scoreDirector.getWorkingSolution());
63+
scoreDirector.getWorkingSolution(), sorter);
6164
if (valueRangeMightContainEntity) {
6265
cachedEntityListRevision = scoreDirector.getWorkingEntityListRevision();
6366
cachedEntityListIsDirty = false;
@@ -74,7 +77,7 @@ public void stepStarted(AbstractStepScope<Solution_> stepScope) {
7477
cachedEntityListIsDirty = true;
7578
} else {
7679
cachedValueRange = scoreDirector.getValueRangeManager().getFromSolution(valueRangeDescriptor,
77-
scoreDirector.getWorkingSolution());
80+
scoreDirector.getWorkingSolution(), sorter);
7881
cachedEntityListRevision = scoreDirector.getWorkingEntityListRevision();
7982
}
8083
}
@@ -153,19 +156,16 @@ private void checkCachedEntityListIsDirty() {
153156
}
154157

155158
@Override
156-
public boolean equals(Object other) {
157-
if (this == other)
158-
return true;
159-
if (other == null || getClass() != other.getClass())
159+
public boolean equals(Object o) {
160+
if (!(o instanceof IterableFromSolutionPropertyValueSelector<?> that))
160161
return false;
161-
var that = (IterableFromSolutionPropertyValueSelector<?>) other;
162-
return randomSelection == that.randomSelection &&
163-
Objects.equals(valueRangeDescriptor, that.valueRangeDescriptor) && minimumCacheType == that.minimumCacheType;
162+
return randomSelection == that.randomSelection && Objects.equals(valueRangeDescriptor, that.valueRangeDescriptor)
163+
&& Objects.equals(sorter, that.sorter) && minimumCacheType == that.minimumCacheType;
164164
}
165165

166166
@Override
167167
public int hashCode() {
168-
return Objects.hash(valueRangeDescriptor, minimumCacheType, randomSelection);
168+
return Objects.hash(valueRangeDescriptor, sorter, minimumCacheType, randomSelection);
169169
}
170170

171171
@Override

core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java

Lines changed: 59 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,19 @@ public ValueSelector<Solution_> buildValueSelector(HeuristicConfigPolicy<Solutio
122122

123123
// baseValueSelector and lower should be SelectionOrder.ORIGINAL if they are going to get cached completely
124124
var randomSelection = determineBaseRandomSelection(variableDescriptor, resolvedCacheType, resolvedSelectionOrder);
125-
var valueSelector =
126-
buildBaseValueSelector(variableDescriptor, SelectionCacheType.max(minimumCacheType, resolvedCacheType),
127-
randomSelection);
128125
var instanceCache = configPolicy.getClassInstanceCache();
126+
/*
127+
* When a filtering value range or nearby are used,
128+
* we opt to sort the data using a sorting node selector instead of sorting at the value range level.
129+
* This choice is required
130+
* because the FilteringValueRangeSelector or the related Nearby selector does not respect the sort order
131+
* provided by the child selectors.
132+
*/
133+
var canSortAtValueRangeLevel = entityValueRangeRecorderId == null && nearbySelectionConfig == null;
134+
var sorter =
135+
canSortAtValueRangeLevel ? determineSorter(variableDescriptor, resolvedSelectionOrder, instanceCache) : null;
136+
var valueSelector = buildBaseValueSelector(variableDescriptor, sorter,
137+
SelectionCacheType.max(minimumCacheType, resolvedCacheType), randomSelection);
129138
if (nearbySelectionConfig != null) {
130139
// TODO Static filtering (such as movableEntitySelectionFilter) should affect nearbySelection too
131140
valueSelector = applyNearbySelection(configPolicy, entityDescriptor, minimumCacheType,
@@ -141,7 +150,9 @@ public ValueSelector<Solution_> buildValueSelector(HeuristicConfigPolicy<Solutio
141150
}
142151
valueSelector = applyFiltering(valueSelector, instanceCache);
143152
valueSelector = applyInitializedChainedValueFilter(configPolicy, variableDescriptor, valueSelector);
144-
valueSelector = applySorting(resolvedCacheType, resolvedSelectionOrder, valueSelector, instanceCache);
153+
if (!canSortAtValueRangeLevel) {
154+
valueSelector = applySorting(resolvedCacheType, resolvedSelectionOrder, valueSelector, instanceCache);
155+
}
145156
valueSelector = applyProbability(resolvedCacheType, resolvedSelectionOrder, valueSelector, instanceCache);
146157
valueSelector = applyShuffling(resolvedCacheType, resolvedSelectionOrder, valueSelector);
147158
valueSelector = applyCaching(resolvedCacheType, resolvedSelectionOrder, valueSelector);
@@ -260,8 +271,45 @@ private static Class<? extends ComparatorFactory> determineComparatorFactoryClas
260271
}
261272
}
262273

274+
private SelectionSorter<Solution_, Object> determineSorter(GenuineVariableDescriptor<Solution_> variableDescriptor,
275+
SelectionOrder resolvedSelectionOrder, ClassInstanceCache instanceCache) {
276+
SelectionSorter<Solution_, Object> sorter = null;
277+
if (resolvedSelectionOrder == ai.timefold.solver.core.config.heuristic.selector.common.SelectionOrder.SORTED) {
278+
var sorterManner = config.getSorterManner();
279+
var comparatorClass = determineComparatorClass(config);
280+
var comparatorFactoryClass = determineComparatorFactoryClass(config);
281+
if (sorterManner != null) {
282+
if (!ValueSelectorConfig.hasSorter(sorterManner, variableDescriptor)) {
283+
return null;
284+
}
285+
sorter = ValueSelectorConfig.determineSorter(sorterManner, variableDescriptor);
286+
} else if (comparatorClass != null) {
287+
Comparator<Object> sorterComparator =
288+
instanceCache.newInstance(config, determineComparatorPropertyName(config), comparatorClass);
289+
sorter = new ComparatorSelectionSorter<>(sorterComparator,
290+
SelectionSorterOrder.resolve(config.getSorterOrder()));
291+
} else if (comparatorFactoryClass != null) {
292+
var comparatorFactory = instanceCache.newInstance(config, determineComparatorFactoryPropertyName(config),
293+
comparatorFactoryClass);
294+
sorter = new ComparatorFactorySelectionSorter<>(comparatorFactory,
295+
SelectionSorterOrder.resolve(config.getSorterOrder()));
296+
} else if (config.getSorterClass() != null) {
297+
sorter = instanceCache.newInstance(config, "sorterClass", config.getSorterClass());
298+
} else {
299+
throw new IllegalArgumentException("""
300+
The valueSelectorConfig (%s) with resolvedSelectionOrder (%s) needs \
301+
a sorterManner (%s) or a %s (%s) or a %s (%s) \
302+
or a sorterClass (%s)."""
303+
.formatted(config, resolvedSelectionOrder, sorterManner, determineComparatorPropertyName(config),
304+
comparatorClass, determineComparatorFactoryPropertyName(config), comparatorFactoryClass,
305+
config.getSorterClass()));
306+
}
307+
}
308+
return sorter;
309+
}
310+
263311
private ValueSelector<Solution_> buildBaseValueSelector(GenuineVariableDescriptor<Solution_> variableDescriptor,
264-
SelectionCacheType minimumCacheType, boolean randomSelection) {
312+
SelectionSorter<Solution_, Object> sorter, SelectionCacheType minimumCacheType, boolean randomSelection) {
265313
var valueRangeDescriptor = variableDescriptor.getValueRangeDescriptor();
266314
// TODO minimumCacheType SOLVER is only a problem if the valueRange includes entities or custom weird cloning
267315
if (minimumCacheType == SelectionCacheType.SOLVER) {
@@ -271,11 +319,13 @@ private ValueSelector<Solution_> buildBaseValueSelector(GenuineVariableDescripto
271319
+ ") is not yet supported. Please use " + SelectionCacheType.PHASE + " instead.");
272320
}
273321
if (valueRangeDescriptor.canExtractValueRangeFromSolution()) {
274-
return new IterableFromSolutionPropertyValueSelector<>(valueRangeDescriptor, minimumCacheType, randomSelection);
322+
return new IterableFromSolutionPropertyValueSelector<>(valueRangeDescriptor, sorter, minimumCacheType,
323+
randomSelection);
275324
} else {
276325
// TODO Do not allow PHASE cache on FromEntityPropertyValueSelector, except if the moveSelector is PHASE cached too.
277-
var fromEntityPropertySelector = new FromEntityPropertyValueSelector<>(valueRangeDescriptor, randomSelection);
278-
return new IterableFromEntityPropertyValueSelector<>(fromEntityPropertySelector, randomSelection);
326+
var fromEntityPropertySelector =
327+
new FromEntityPropertyValueSelector<>(valueRangeDescriptor, sorter, randomSelection);
328+
return new IterableFromEntityPropertyValueSelector<>(fromEntityPropertySelector, minimumCacheType, randomSelection);
279329
}
280330
}
281331

@@ -372,37 +422,7 @@ private static void assertNotSorterClassAnd(ValueSelectorConfig config, String p
372422
protected ValueSelector<Solution_> applySorting(SelectionCacheType resolvedCacheType, SelectionOrder resolvedSelectionOrder,
373423
ValueSelector<Solution_> valueSelector, ClassInstanceCache instanceCache) {
374424
if (resolvedSelectionOrder == SelectionOrder.SORTED) {
375-
SelectionSorter<Solution_, Object> sorter;
376-
var sorterManner = config.getSorterManner();
377-
var comparatorClass = determineComparatorClass(config);
378-
var comparatorFactoryClass = determineComparatorFactoryClass(config);
379-
if (sorterManner != null) {
380-
var variableDescriptor = valueSelector.getVariableDescriptor();
381-
if (!ValueSelectorConfig.hasSorter(sorterManner, variableDescriptor)) {
382-
return valueSelector;
383-
}
384-
sorter = ValueSelectorConfig.determineSorter(sorterManner, variableDescriptor);
385-
} else if (comparatorClass != null) {
386-
Comparator<Object> sorterComparator =
387-
instanceCache.newInstance(config, determineComparatorPropertyName(config), comparatorClass);
388-
sorter = new ComparatorSelectionSorter<>(sorterComparator,
389-
SelectionSorterOrder.resolve(config.getSorterOrder()));
390-
} else if (comparatorFactoryClass != null) {
391-
var comparatorFactory = instanceCache.newInstance(config, determineComparatorFactoryPropertyName(config),
392-
comparatorFactoryClass);
393-
sorter = new ComparatorFactorySelectionSorter<>(comparatorFactory,
394-
SelectionSorterOrder.resolve(config.getSorterOrder()));
395-
} else if (config.getSorterClass() != null) {
396-
sorter = instanceCache.newInstance(config, "sorterClass", config.getSorterClass());
397-
} else {
398-
throw new IllegalArgumentException("""
399-
The valueSelectorConfig (%s) with resolvedSelectionOrder (%s) needs \
400-
a sorterManner (%s) or a %s (%s) or a %s (%s) \
401-
or a sorterClass (%s)."""
402-
.formatted(config, resolvedSelectionOrder, sorterManner, determineComparatorPropertyName(config),
403-
comparatorClass, determineComparatorFactoryPropertyName(config), comparatorFactoryClass,
404-
config.getSorterClass()));
405-
}
425+
var sorter = determineSorter(valueSelector.getVariableDescriptor(), resolvedSelectionOrder, instanceCache);
406426
if (!valueSelector.getVariableDescriptor().canExtractValueRangeFromSolution()
407427
&& resolvedCacheType == SelectionCacheType.STEP) {
408428
valueSelector = new FromEntitySortingValueSelector<>(valueSelector, resolvedCacheType, sorter);

0 commit comments

Comments
 (0)