Skip to content

Commit 9094c30

Browse files
committed
feat: improve the logic for random iterators of entity range selectors
1 parent 1029045 commit 9094c30

File tree

7 files changed

+294
-48
lines changed

7 files changed

+294
-48
lines changed

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

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -129,20 +129,31 @@ public Iterator<Object> iterator(Object entity) {
129129
@Override
130130
public Iterator<Object> iterator() {
131131
if (randomSelection) {
132-
return new RandomFilteringValueRangeIterator(replayingValueSelector.iterator(), listVariableStateSupply,
133-
reachableValues, workingRandom, (int) getSize(), checkSourceAndDestination, true);
132+
// If the nonReplayingValueSelector does not have any additional configuration,
133+
// we can bypass it and only use reachable values,
134+
// which helps optimize the number of evaluations.
135+
// However, if the nonReplayingValueSelector includes custom configurations,
136+
// such as filtering,
137+
// we will first evaluate its values and then filter out those that are not reachable.
138+
if (nonReplayingValueSelector instanceof IterableFromEntityPropertyValueSelector<Solution_>) {
139+
return new OptimizedRandomFilteringValueRangeIterator(replayingValueSelector.iterator(),
140+
listVariableStateSupply,
141+
reachableValues, workingRandom, (int) getSize(), checkSourceAndDestination);
142+
} else {
143+
return new RandomFilteringValueRangeIterator(replayingValueSelector.iterator(),
144+
nonReplayingValueSelector.iterator(), listVariableStateSupply, reachableValues, (int) getSize(),
145+
checkSourceAndDestination);
146+
}
134147
} else {
135148
return new OriginalFilteringValueRangeIterator(replayingValueSelector.iterator(),
136-
nonReplayingValueSelector.iterator(), listVariableStateSupply, reachableValues, checkSourceAndDestination,
137-
false);
149+
nonReplayingValueSelector.iterator(), listVariableStateSupply, reachableValues, checkSourceAndDestination);
138150
}
139151
}
140152

141153
@Override
142154
public Iterator<Object> endingIterator(Object entity) {
143155
return new OriginalFilteringValueRangeIterator(replayingValueSelector.iterator(),
144-
nonReplayingValueSelector.iterator(), listVariableStateSupply, reachableValues, checkSourceAndDestination,
145-
false);
156+
nonReplayingValueSelector.iterator(), listVariableStateSupply, reachableValues, checkSourceAndDestination);
146157
}
147158

148159
@Override
@@ -258,7 +269,7 @@ boolean isValueOrEntityReachable(Object destinationValue) {
258269
private class OriginalFilteringValueRangeIterator extends AbstractFilteringValueRangeIterator {
259270
// The value iterator that only replays the current selected value
260271
private final Iterator<Object> replayingValueIterator;
261-
// The value iterator returns all possible values based on its settings.
272+
// The value iterator returns all possible values based on the outer selector settings.
262273
// However,
263274
// it may include invalid values that need to be filtered out.
264275
// This iterator must be used to ensure that all positions are included in the CH phase.
@@ -267,8 +278,8 @@ private class OriginalFilteringValueRangeIterator extends AbstractFilteringValue
267278

268279
private OriginalFilteringValueRangeIterator(Iterator<Object> replayingValueIterator, Iterator<Object> valueIterator,
269280
ListVariableStateSupply<Solution_> listVariableStateSupply, ReachableValues reachableValues,
270-
boolean checkSourceAndDestination, boolean useValueList) {
271-
super(listVariableStateSupply, reachableValues, checkSourceAndDestination, useValueList);
281+
boolean checkSourceAndDestination) {
282+
super(listVariableStateSupply, reachableValues, checkSourceAndDestination, false);
272283
this.replayingValueIterator = replayingValueIterator;
273284
this.valueIterator = valueIterator;
274285
}
@@ -307,15 +318,71 @@ protected Object createUpcomingSelection() {
307318
}
308319

309320
private class RandomFilteringValueRangeIterator extends AbstractFilteringValueRangeIterator {
321+
// The value iterator that only replays the current selected value
322+
private final Iterator<Object> replayingValueIterator;
323+
// The value iterator returns all possible values based on the outer selector settings.
324+
private final Iterator<Object> valueIterator;
325+
private final int maxBailoutSize;
326+
327+
private RandomFilteringValueRangeIterator(Iterator<Object> replayingValueIterator, Iterator<Object> valueIterator,
328+
ListVariableStateSupply<Solution_> listVariableStateSupply, ReachableValues reachableValues,
329+
int maxBailoutSize, boolean checkSourceAndDestination) {
330+
super(listVariableStateSupply, reachableValues, checkSourceAndDestination, false);
331+
this.replayingValueIterator = replayingValueIterator;
332+
this.valueIterator = valueIterator;
333+
this.maxBailoutSize = maxBailoutSize;
334+
}
335+
336+
private void initialize() {
337+
if (initialized) {
338+
return;
339+
}
340+
if (replayingValueIterator.hasNext()) {
341+
var upcomingValue = replayingValueIterator.next();
342+
if (!valueIterator.hasNext()) {
343+
noData();
344+
} else {
345+
loadValues(Objects.requireNonNull(upcomingValue));
346+
}
347+
} else {
348+
noData();
349+
}
350+
}
351+
352+
@Override
353+
protected Object createUpcomingSelection() {
354+
initialize();
355+
if (!hasData) {
356+
return noUpcomingSelection();
357+
}
358+
Object next;
359+
var bailoutSize = maxBailoutSize;
360+
do {
361+
if (bailoutSize <= 0 || !valueIterator.hasNext()) {
362+
return noUpcomingSelection();
363+
}
364+
bailoutSize--;
365+
next = valueIterator.next();
366+
} while (!isValueOrEntityReachable(next));
367+
return next;
368+
}
369+
}
370+
371+
/**
372+
* The optimized iterator only traverses reachable values from the current selection.
373+
* Unlike {@link RandomFilteringValueRangeIterator},
374+
* it does not use an outer iterator to filter out non-reachable values.
375+
*/
376+
private class OptimizedRandomFilteringValueRangeIterator extends AbstractFilteringValueRangeIterator {
310377

311378
private final Iterator<Object> replayingValueIterator;
312379
private final Random workingRandom;
313380
private final int maxBailoutSize;
314381

315-
private RandomFilteringValueRangeIterator(Iterator<Object> replayingValueIterator,
382+
private OptimizedRandomFilteringValueRangeIterator(Iterator<Object> replayingValueIterator,
316383
ListVariableStateSupply<Solution_> listVariableStateSupply, ReachableValues reachableValues,
317-
Random workingRandom, int maxBailoutSize, boolean checkSourceAndDestination, boolean useValueList) {
318-
super(listVariableStateSupply, reachableValues, checkSourceAndDestination, useValueList);
384+
Random workingRandom, int maxBailoutSize, boolean checkSourceAndDestination) {
385+
super(listVariableStateSupply, reachableValues, checkSourceAndDestination, true);
319386
this.replayingValueIterator = replayingValueIterator;
320387
this.workingRandom = workingRandom;
321388
this.maxBailoutSize = maxBailoutSize;

core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,8 @@ void solveWithEntityValueRangeBasicVariable() {
365365
@Test
366366
void solveWithEntityValueRangeListVariable() {
367367
var solverConfig = PlannerTestUtils
368-
.buildSolverConfig(TestdataListEntityProvidingSolution.class, TestdataListEntityProvidingEntity.class)
368+
.buildSolverConfig(TestdataListEntityProvidingSolution.class, TestdataListEntityProvidingEntity.class,
369+
TestdataListEntityProvidingValue.class)
369370
.withEasyScoreCalculatorClass(TestdataListEntityProvidingScoreCalculator.class)
370371
.withPhases(new ConstructionHeuristicPhaseConfig());
371372

@@ -381,8 +382,10 @@ void solveWithEntityValueRangeListVariable() {
381382
var bestSolution = PlannerTestUtils.solve(solverConfig, solution, true);
382383
assertThat(bestSolution).isNotNull();
383384
// Only one entity should provide the value list and assign the values.
384-
assertThat(bestSolution.getEntityList().get(0).getValueList()).hasSameElementsAs(List.of(value1, value2));
385-
assertThat(bestSolution.getEntityList().get(1).getValueList()).hasSameElementsAs(List.of(value3));
385+
assertThat(bestSolution.getEntityList().get(0).getValueList().stream().map(TestdataListEntityProvidingValue::getCode))
386+
.hasSameElementsAs(List.of("v1", "v2"));
387+
assertThat(bestSolution.getEntityList().get(1).getValueList().stream().map(TestdataListEntityProvidingValue::getCode))
388+
.hasSameElementsAs(List.of("v3"));
386389
}
387390

388391
@Test

core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelectorTest.java

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import static ai.timefold.solver.core.testdomain.list.TestdataListUtils.getPinnedAllowsUnassignedvaluesListVariableDescriptor;
1010
import static ai.timefold.solver.core.testdomain.list.TestdataListUtils.getPinnedListVariableDescriptor;
1111
import static ai.timefold.solver.core.testdomain.list.TestdataListUtils.mockEntitySelector;
12+
import static ai.timefold.solver.core.testdomain.list.TestdataListUtils.mockIterableFromEntityPropertyValueSelector;
1213
import static ai.timefold.solver.core.testdomain.list.TestdataListUtils.mockIterableValueSelector;
1314
import static ai.timefold.solver.core.testutil.PlannerAssert.assertAllCodesOfIterableSelector;
1415
import static ai.timefold.solver.core.testutil.PlannerAssert.assertAllCodesOfIterator;
@@ -175,11 +176,11 @@ void randomWithEntityValueRange() {
175176
// 1 - pick random value in ElementPositionRandomIterator and return the first unpinned position
176177
// 1 - remaining call
177178
var valueSelector = mockIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v3);
179+
var filteringValueRangeSelector = mockIterableFromEntityPropertyValueSelector(valueSelector, true);
178180
var replayinValueSelector = mockIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v3);
179181
checkEntityValueRange(new FilteringEntityValueRangeSelector<>(mockEntitySelector(a, b, c), valueSelector, true),
180-
new FilteringValueRangeSelector<>(valueSelector, replayinValueSelector, true, false), scoreDirector,
181-
new TestRandom(1, 1, 1),
182-
"C[0]");
182+
new FilteringValueRangeSelector<>(filteringValueRangeSelector, replayinValueSelector, true, false),
183+
scoreDirector, new TestRandom(1, 1, 1), "C[0]");
183184

184185
// select A for V1 and random pos A[2]
185186
// Random values
@@ -188,38 +189,41 @@ void randomWithEntityValueRange() {
188189
// 0 - pick random position, only v2 is reachable
189190
// 0 - remaining call
190191
valueSelector = mockIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v1);
192+
filteringValueRangeSelector = mockIterableFromEntityPropertyValueSelector(valueSelector, true);
191193
replayinValueSelector = mockIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v1);
192194
// Cause the value iterator return no value at the second call
193195
doReturn(List.of(v1).iterator(), Collections.emptyIterator()).when(valueSelector).iterator();
194196
checkEntityValueRange(new FilteringEntityValueRangeSelector<>(mockEntitySelector(a, b, c), valueSelector, true),
195-
new FilteringValueRangeSelector<>(valueSelector, replayinValueSelector, true, false), scoreDirector,
196-
new TestRandom(0, 3, 0, 0), "A[2]");
197+
new FilteringValueRangeSelector<>(filteringValueRangeSelector, replayinValueSelector, true, false),
198+
scoreDirector, new TestRandom(0, 3, 0, 0), "A[2]");
197199

198200
// select B for V1 and random pos B[1]
199201
// 1 - pick entity B in RandomFilteringValueRangeIterator
200202
// 3 - pick a random value in ElementPositionRandomIterator and force generating a random position
201203
// 1 - pick random position, v1 and v3 are reachable
202204
// 0 - remaining call
203205
valueSelector = mockIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v2, v2, v2, v2, v2); // simulate five positions
206+
filteringValueRangeSelector = mockIterableFromEntityPropertyValueSelector(valueSelector, true);
204207
replayinValueSelector = mockIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v2);
205208
// Cause the value iterator return no value at the second call
206209
doReturn(List.of(v2).iterator(), Collections.emptyIterator()).when(valueSelector).iterator();
207210
checkEntityValueRange(new FilteringEntityValueRangeSelector<>(mockEntitySelector(a, b, c), valueSelector, true),
208-
new FilteringValueRangeSelector<>(valueSelector, replayinValueSelector, true, false), scoreDirector,
209-
new TestRandom(1, 3, 1, 0), "B[1]");
211+
new FilteringValueRangeSelector<>(filteringValueRangeSelector, replayinValueSelector, true, false),
212+
scoreDirector, new TestRandom(1, 3, 1, 0), "B[1]");
210213

211214
// select C for V5 and first unpinned pos C[1]
212215
// 0 - pick entity C in RandomFilteringValueRangeIterator
213216
// 3 - pick random value in ElementPositionRandomIterator and force generating a random position
214217
// 1 - pick random position, v3 and v4 are reachable
215218
// 0 - remaining call
216219
valueSelector = mockIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v5, v5, v5, v5, v5); // simulate five positions
220+
filteringValueRangeSelector = mockIterableFromEntityPropertyValueSelector(valueSelector, true);
217221
replayinValueSelector = mockIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v5);
218222
// Cause the value iterator return no value at the second call
219223
doReturn(List.of(v5).iterator(), Collections.emptyIterator()).when(valueSelector).iterator();
220224
checkEntityValueRange(new FilteringEntityValueRangeSelector<>(mockEntitySelector(a, b, c), valueSelector, true),
221-
new FilteringValueRangeSelector<>(valueSelector, replayinValueSelector, true, false), scoreDirector,
222-
new TestRandom(0, 3, 1, 0), "C[1]");
225+
new FilteringValueRangeSelector<>(filteringValueRangeSelector, replayinValueSelector, true, false),
226+
scoreDirector, new TestRandom(0, 3, 1, 0), "C[1]");
223227
}
224228

225229
private void checkEntityValueRange(FilteringEntityValueRangeSelector<TestdataListEntityProvidingSolution> entitySelector,

core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListChangeMoveSelectorTest.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import java.util.List;
2626
import java.util.Random;
2727

28+
import ai.timefold.solver.core.api.score.director.ScoreDirector;
29+
import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionFilter;
2830
import ai.timefold.solver.core.preview.api.domain.metamodel.ElementPosition;
2931
import ai.timefold.solver.core.testdomain.TestdataValue;
3032
import ai.timefold.solver.core.testdomain.list.TestdataListEntity;
@@ -415,6 +417,40 @@ void randomWithEntityValueRange() {
415417
"3 {B[0]->B[0]}");
416418
}
417419

420+
@Test
421+
void randomWithEntityValueRangeAndFiltering() {
422+
var v1 = new TestdataListEntityProvidingValue("1");
423+
var v2 = new TestdataListEntityProvidingValue("2");
424+
var v3 = new TestdataListEntityProvidingValue("3");
425+
var a = new TestdataListEntityProvidingEntity("A", List.of(v1, v2), List.of(v2, v1));
426+
var b = new TestdataListEntityProvidingEntity("B", List.of(v2, v3), List.of(v3));
427+
var solution = new TestdataListEntityProvidingSolution();
428+
solution.setEntityList(List.of(a, b));
429+
430+
var scoreDirector = mockScoreDirector(TestdataListEntityProvidingSolution.buildSolutionDescriptor());
431+
scoreDirector.setWorkingSolution(solution);
432+
433+
var mimicRecordingValueSelector = getMimicRecordingIterableValueSelector(
434+
getEntityRangeListVariableDescriptor(scoreDirector).getValueRangeDescriptor(), true);
435+
var solutionDescriptor = scoreDirector.getSolutionDescriptor();
436+
var entityDescriptor = solutionDescriptor.findEntityDescriptor(TestdataListEntityProvidingEntity.class);
437+
var destinationSelector = getEntityValueRangeDestinationSelector(mimicRecordingValueSelector, solutionDescriptor,
438+
entityDescriptor, IgnoreBValueSelectionFilter.class, true);
439+
var moveSelector = new ListChangeMoveSelector<>(mimicRecordingValueSelector, destinationSelector, true);
440+
441+
var solverScope = solvingStarted(moveSelector, scoreDirector, new Random(0));
442+
phaseStarted(solverScope, moveSelector);
443+
444+
// IgnoreBValueSelectionFilter is applied to the value selector used by the destination selector,
445+
// and that causes the B destination to become an invalid destination
446+
assertCodesOfNeverEndingMoveSelector(moveSelector,
447+
"1 {A[1]->A[1]}",
448+
"3 {B[0]->A[1]}",
449+
"1 {A[1]->A[1]}",
450+
"1 {A[1]->A[1]}",
451+
"1 {A[1]->A[0]}");
452+
}
453+
418454
@Test
419455
void randomWithPinning() {
420456
var v1 = new TestdataPinnedWithIndexListValue("1");
@@ -576,4 +612,18 @@ void randomAllowsUnassignedValuesWithEntityValueRange() {
576612
"3 {B[0]->B[0]}",
577613
"3 {B[0]->B[0]}");
578614
}
615+
616+
public static class IgnoreBValueSelectionFilter
617+
implements SelectionFilter<TestdataListEntityProvidingSolution, TestdataListEntityProvidingValue> {
618+
619+
public IgnoreBValueSelectionFilter() {
620+
// Required for solver initialization
621+
}
622+
623+
@Override
624+
public boolean accept(ScoreDirector<TestdataListEntityProvidingSolution> scoreDirector,
625+
TestdataListEntityProvidingValue selection) {
626+
return !selection.getEntity().getCode().equals("B");
627+
}
628+
}
579629
}

0 commit comments

Comments
 (0)