Skip to content

Commit afe99f5

Browse files
zepfredtriceo
authored andcommitted
fix: enable ListIterator support on FilteringEntityByEntitySelector and FilteringEntityByValueSelector (#1821)
(cherry picked from commit df4a081)
1 parent 2eb59ef commit afe99f5

File tree

5 files changed

+287
-4
lines changed

5 files changed

+287
-4
lines changed

core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/FilteringEntityByEntitySelector.java

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor;
1111
import ai.timefold.solver.core.impl.domain.variable.descriptor.BasicVariableDescriptor;
1212
import ai.timefold.solver.core.impl.heuristic.selector.AbstractDemandEnabledSelector;
13+
import ai.timefold.solver.core.impl.heuristic.selector.common.iterator.UpcomingSelectionListIterator;
1314
import ai.timefold.solver.core.impl.heuristic.selector.entity.EntitySelector;
1415
import ai.timefold.solver.core.impl.heuristic.selector.entity.EntitySelectorFactory;
1516
import ai.timefold.solver.core.impl.heuristic.selector.move.generic.SwapMoveSelector;
@@ -183,12 +184,14 @@ public Iterator<Object> iterator() {
183184

184185
@Override
185186
public ListIterator<Object> listIterator() {
186-
throw new UnsupportedOperationException();
187+
return new OriginalFilteringValueRangeListIterator<>(this::selectReplayedEntity, childEntitySelector.listIterator(),
188+
basicVariableDescriptors, valueRangeManager);
187189
}
188190

189191
@Override
190192
public ListIterator<Object> listIterator(int index) {
191-
throw new UnsupportedOperationException();
193+
return new OriginalFilteringValueRangeListIterator<>(this::selectReplayedEntity,
194+
childEntitySelector.listIterator(index), basicVariableDescriptors, valueRangeManager);
192195
}
193196

194197
@Override
@@ -318,6 +321,91 @@ public Object next() {
318321

319322
}
320323

324+
private static class OriginalFilteringValueRangeListIterator<Solution_> extends UpcomingSelectionListIterator<Object> {
325+
326+
private final Supplier<Object> upcomingEntitySupplier;
327+
private final BasicVariableDescriptor<Solution_>[] basicVariableDescriptors;
328+
private final ListIterator<Object> entityIterator;
329+
private final ValueRangeManager<Solution_> valueRangeManager;
330+
private Object replayedEntity;
331+
332+
private OriginalFilteringValueRangeListIterator(Supplier<Object> upcomingEntitySupplier,
333+
ListIterator<Object> entityIterator, BasicVariableDescriptor<Solution_>[] basicVariableDescriptors,
334+
ValueRangeManager<Solution_> valueRangeManager) {
335+
this.upcomingEntitySupplier = upcomingEntitySupplier;
336+
this.entityIterator = entityIterator;
337+
this.basicVariableDescriptors = basicVariableDescriptors;
338+
this.valueRangeManager = valueRangeManager;
339+
}
340+
341+
void checkReplayedEntity() {
342+
var newReplayedEntity = upcomingEntitySupplier.get();
343+
if (newReplayedEntity != replayedEntity) {
344+
replayedEntity = newReplayedEntity;
345+
}
346+
}
347+
348+
@Override
349+
public boolean hasNext() {
350+
checkReplayedEntity();
351+
return super.hasNext();
352+
}
353+
354+
@Override
355+
public boolean hasPrevious() {
356+
checkReplayedEntity();
357+
return super.hasPrevious();
358+
}
359+
360+
@Override
361+
protected Object createUpcomingSelection() {
362+
if (!entityIterator.hasNext()) {
363+
return noUpcomingSelection();
364+
}
365+
while (entityIterator.hasNext()) {
366+
var otherEntity = entityIterator.next();
367+
if (isReachable(replayedEntity, otherEntity)) {
368+
return otherEntity;
369+
}
370+
}
371+
return noUpcomingSelection();
372+
}
373+
374+
@Override
375+
protected Object createPreviousSelection() {
376+
if (!entityIterator.hasPrevious()) {
377+
return noUpcomingSelection();
378+
}
379+
while (entityIterator.hasPrevious()) {
380+
var otherEntity = entityIterator.previous();
381+
if (isReachable(replayedEntity, otherEntity)) {
382+
return otherEntity;
383+
}
384+
}
385+
return noUpcomingSelection();
386+
}
387+
388+
boolean isReachable(Object entity, Object otherEntity) {
389+
if (entity == otherEntity) {
390+
return false;
391+
}
392+
for (var basicVariableDescriptor : basicVariableDescriptors) {
393+
if (!isReachable(entity, basicVariableDescriptor.getValue(entity), otherEntity,
394+
basicVariableDescriptor.getValue(otherEntity), basicVariableDescriptor, valueRangeManager)) {
395+
return false;
396+
}
397+
}
398+
return true;
399+
}
400+
401+
private boolean isReachable(Object entity, Object value, Object otherEntity, Object otherValue,
402+
BasicVariableDescriptor<Solution_> variableDescriptor, ValueRangeManager<Solution_> valueRangeManager) {
403+
return valueRangeManager.getFromEntity(variableDescriptor.getValueRangeDescriptor(), entity).contains(otherValue)
404+
&& valueRangeManager.getFromEntity(variableDescriptor.getValueRangeDescriptor(), otherEntity)
405+
.contains(value);
406+
}
407+
}
408+
321409
private static class RandomFilteringValueRangeIterator<Solution_>
322410
extends AbstractFilteringValueRangeIterator<Solution_> {
323411

core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/FilteringEntityByValueSelector.java

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import ai.timefold.solver.core.impl.heuristic.selector.AbstractDemandEnabledSelector;
1616
import ai.timefold.solver.core.impl.heuristic.selector.common.ReachableValues;
1717
import ai.timefold.solver.core.impl.heuristic.selector.common.iterator.UpcomingSelectionIterator;
18+
import ai.timefold.solver.core.impl.heuristic.selector.common.iterator.UpcomingSelectionListIterator;
1819
import ai.timefold.solver.core.impl.heuristic.selector.entity.EntitySelector;
1920
import ai.timefold.solver.core.impl.heuristic.selector.move.generic.list.ListChangeMoveSelectorFactory;
2021
import ai.timefold.solver.core.impl.heuristic.selector.value.IterableValueSelector;
@@ -161,12 +162,14 @@ public Iterator<Object> iterator() {
161162

162163
@Override
163164
public ListIterator<Object> listIterator() {
164-
throw new UnsupportedOperationException();
165+
return new OriginalFilteringValueRangeListIterator(this::selectReplayedValue, childEntitySelector.listIterator(),
166+
reachableValues);
165167
}
166168

167169
@Override
168170
public ListIterator<Object> listIterator(int index) {
169-
throw new UnsupportedOperationException();
171+
return new OriginalFilteringValueRangeListIterator(this::selectReplayedValue, childEntitySelector.listIterator(index),
172+
reachableValues);
170173
}
171174

172175
@Override
@@ -215,6 +218,68 @@ protected Object createUpcomingSelection() {
215218
}
216219
}
217220

221+
private static class OriginalFilteringValueRangeListIterator extends UpcomingSelectionListIterator<Object> {
222+
223+
private final Supplier<Object> upcomingValueSupplier;
224+
private final ListIterator<Object> entityIterator;
225+
private final ReachableValues reachableValues;
226+
private Object replayedValue;
227+
228+
private OriginalFilteringValueRangeListIterator(Supplier<Object> upcomingValueSupplier,
229+
ListIterator<Object> entityIterator, ReachableValues reachableValues) {
230+
this.upcomingValueSupplier = upcomingValueSupplier;
231+
this.entityIterator = entityIterator;
232+
this.reachableValues = reachableValues;
233+
}
234+
235+
void checkReplayedValue() {
236+
var newReplayedValue = upcomingValueSupplier.get();
237+
if (newReplayedValue != replayedValue) {
238+
replayedValue = newReplayedValue;
239+
}
240+
}
241+
242+
@Override
243+
public boolean hasNext() {
244+
checkReplayedValue();
245+
return super.hasNext();
246+
}
247+
248+
@Override
249+
public boolean hasPrevious() {
250+
checkReplayedValue();
251+
return super.hasPrevious();
252+
}
253+
254+
@Override
255+
protected Object createUpcomingSelection() {
256+
if (!entityIterator.hasNext()) {
257+
return noUpcomingSelection();
258+
}
259+
while (entityIterator.hasNext()) {
260+
var otherEntity = entityIterator.next();
261+
if (reachableValues.isEntityReachable(replayedValue, otherEntity)) {
262+
return otherEntity;
263+
}
264+
}
265+
return noUpcomingSelection();
266+
}
267+
268+
@Override
269+
protected Object createPreviousSelection() {
270+
if (!entityIterator.hasPrevious()) {
271+
return noUpcomingSelection();
272+
}
273+
while (entityIterator.hasPrevious()) {
274+
var otherEntity = entityIterator.previous();
275+
if (reachableValues.isEntityReachable(replayedValue, otherEntity)) {
276+
return otherEntity;
277+
}
278+
}
279+
return noUpcomingSelection();
280+
}
281+
}
282+
218283
private static class RandomFilteringValueRangeIterator implements Iterator<Object> {
219284

220285
private final Supplier<Object> upcomingValueSupplier;

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

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static ai.timefold.solver.core.testutil.PlannerAssert.assertEmptyNeverEndingIterableSelector;
2020
import static ai.timefold.solver.core.testutil.PlannerAssert.verifyPhaseLifecycle;
2121
import static ai.timefold.solver.core.testutil.PlannerTestUtils.mockScoreDirector;
22+
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
2223
import static org.mockito.Mockito.doReturn;
2324

2425
import java.util.Collections;
@@ -145,6 +146,74 @@ void random() {
145146
random.assertIntBoundJustRequested((int) destinationSize);
146147
}
147148

149+
@Test
150+
void originalWithEntityValueRange() {
151+
var v1 = new TestdataListEntityProvidingValue("V1");
152+
var v2 = new TestdataListEntityProvidingValue("V2");
153+
var v3 = new TestdataListEntityProvidingValue("V3");
154+
var v4 = new TestdataListEntityProvidingValue("V4");
155+
var v5 = new TestdataListEntityProvidingValue("V5");
156+
var a = new TestdataListEntityProvidingEntity("A", List.of(v1, v2), List.of(v1, v2));
157+
var b = new TestdataListEntityProvidingEntity("B", List.of(v2, v3), List.of(v3));
158+
var c = new TestdataListEntityProvidingEntity("C", List.of(v3, v4, v5), List.of(v4, v5));
159+
var solution = new TestdataListEntityProvidingSolution();
160+
solution.setEntityList(List.of(a, b, c));
161+
162+
var scoreDirector = mockScoreDirector(TestdataListEntityProvidingSolution.buildSolutionDescriptor());
163+
scoreDirector.setWorkingSolution(solution);
164+
165+
// V1 is only reachable by A
166+
var valueSelector = mockIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v1);
167+
var selector = new FilteringEntityByValueSelector<>(mockEntitySelector(a, b, c), valueSelector, false);
168+
var solverScope = solvingStarted(selector, scoreDirector);
169+
phaseStarted(solverScope, selector);
170+
assertAllCodesOfIterator(selector.listIterator(), "A");
171+
172+
// V2 is reachable by A and B
173+
valueSelector = mockIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v2);
174+
selector = new FilteringEntityByValueSelector<>(mockEntitySelector(a, b, c), valueSelector, false);
175+
solverScope = solvingStarted(selector, scoreDirector);
176+
phaseStarted(solverScope, selector);
177+
assertAllCodesOfIterator(selector.listIterator(), "A", "B");
178+
179+
// V3 is reachable by B and C
180+
valueSelector = mockIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v3);
181+
selector = new FilteringEntityByValueSelector<>(mockEntitySelector(a, b, c), valueSelector, false);
182+
solverScope = solvingStarted(selector, scoreDirector);
183+
phaseStarted(solverScope, selector);
184+
assertAllCodesOfIterator(selector.listIterator(), "B", "C");
185+
186+
// V4 is only reachable by C
187+
valueSelector = mockIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v4);
188+
selector = new FilteringEntityByValueSelector<>(mockEntitySelector(a, b, c), valueSelector, false);
189+
solverScope = solvingStarted(selector, scoreDirector);
190+
phaseStarted(solverScope, selector);
191+
assertAllCodesOfIterator(selector.listIterator(), "C");
192+
193+
// V5 is only reachable by C
194+
valueSelector = mockIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v5);
195+
selector = new FilteringEntityByValueSelector<>(mockEntitySelector(a, b, c), valueSelector, false);
196+
solverScope = solvingStarted(selector, scoreDirector);
197+
phaseStarted(solverScope, selector);
198+
assertAllCodesOfIterator(selector.listIterator(), "C");
199+
200+
// Getting the previous element
201+
valueSelector = mockIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v3);
202+
selector = new FilteringEntityByValueSelector<>(mockEntitySelector(a, b, c), valueSelector, false);
203+
solverScope = solvingStarted(selector, scoreDirector);
204+
phaseStarted(solverScope, selector);
205+
var listIterator = selector.listIterator();
206+
assertThat(listIterator.hasNext()).isTrue();
207+
assertThat(listIterator.next()).isSameAs(b);
208+
assertThat(listIterator.hasNext()).isTrue();
209+
assertThat(listIterator.next()).isSameAs(c);
210+
assertThat(listIterator.hasNext()).isFalse();
211+
assertThat(listIterator.hasPrevious()).isTrue();
212+
assertThat(listIterator.previous()).isSameAs(c);
213+
assertThat(listIterator.hasPrevious()).isTrue();
214+
assertThat(listIterator.previous()).isSameAs(b);
215+
}
216+
148217
@Test
149218
void randomWithEntityValueRange() {
150219
var v1 = new TestdataListEntityProvidingValue("V1");

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,50 @@ void originalLeftUnequalsRight() {
229229
verifyPhaseLifecycle(rightEntitySelector, 1, 2, 5);
230230
}
231231

232+
@Test
233+
void originalLeftUnequalsRightWithEntityRange() {
234+
var v1 = new TestdataValue("1");
235+
var v2 = new TestdataValue("2");
236+
var v3 = new TestdataValue("3");
237+
var v4 = new TestdataValue("4");
238+
var e1 = new TestdataAllowsUnassignedEntityProvidingEntity("A", List.of(v1, v4));
239+
var e2 = new TestdataAllowsUnassignedEntityProvidingEntity("B", List.of(v2, v3));
240+
var e3 = new TestdataAllowsUnassignedEntityProvidingEntity("C", List.of(v1, v4));
241+
var solution = new TestdataAllowsUnassignedEntityProvidingSolution("s1");
242+
solution.setEntityList(List.of(e1, e2, e3));
243+
244+
var scoreDirector = mockScoreDirector(TestdataAllowsUnassignedEntityProvidingSolution.buildSolutionDescriptor());
245+
scoreDirector.setWorkingSolution(solution);
246+
247+
var leftEntitySelector =
248+
new FromSolutionEntitySelector<>(getEntityDescriptor(scoreDirector), SelectionCacheType.JUST_IN_TIME, false);
249+
var entityMimicRecorder = new MimicRecordingEntitySelector<>(leftEntitySelector);
250+
251+
var replayingEntitySelector = new MimicReplayingEntitySelector<>(entityMimicRecorder);
252+
var rightEntitySelector =
253+
new FilteringEntityByEntitySelector<>(leftEntitySelector, replayingEntitySelector, false);
254+
255+
// It is impossible for the left and right selectors to be equal
256+
// when using the entity-range filtering node FilteringEntityByEntitySelector
257+
var moveSelector = new SwapMoveSelector<>(entityMimicRecorder, rightEntitySelector,
258+
leftEntitySelector.getEntityDescriptor().getGenuineVariableDescriptorList(), false);
259+
260+
var solverScope = solvingStarted(moveSelector, scoreDirector);
261+
phaseStarted(moveSelector, solverScope);
262+
263+
scoreDirector.setWorkingSolution(solution);
264+
var expectedSize = (long) solution.getEntityList().size() * solution.getEntityList().size();
265+
266+
// The moves are duplicated because the left and right selectors are not equal,
267+
// and listIterator(index) is not called in such cases.
268+
assertAllCodesOfMoveSelector(moveSelector, expectedSize, "A<->B",
269+
"A<->C",
270+
"B<->A",
271+
"B<->C",
272+
"C<->A",
273+
"C<->B");
274+
}
275+
232276
@Test
233277
void emptyRightOriginalLeftUnequalsRight() {
234278
EntityDescriptor entityDescriptor = TestdataEntity.buildEntityDescriptor();

core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig;
4848
import ai.timefold.solver.core.config.heuristic.selector.entity.EntitySorterManner;
4949
import ai.timefold.solver.core.config.heuristic.selector.entity.pillar.PillarSelectorConfig;
50+
import ai.timefold.solver.core.config.heuristic.selector.list.DestinationSelectorConfig;
5051
import ai.timefold.solver.core.config.heuristic.selector.list.SubListSelectorConfig;
5152
import ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig;
5253
import ai.timefold.solver.core.config.heuristic.selector.move.composite.UnionMoveSelectorConfig;
@@ -2193,6 +2194,10 @@ private static List<MoveSelectorConfig> generateMovesForBasicVar() {
21932194
allMoveSelectionConfigList.add(new ChangeMoveSelectorConfig().withEntitySelectorConfig(entitySelectorConfig));
21942195
// Swap - basic
21952196
allMoveSelectionConfigList.add(new SwapMoveSelectorConfig().withEntitySelectorConfig(entitySelectorConfig));
2197+
// Swap - basic - original sort order
2198+
allMoveSelectionConfigList.add(new SwapMoveSelectorConfig()
2199+
.withSelectionOrder(SelectionOrder.ORIGINAL)
2200+
.withEntitySelectorConfig(entitySelectorConfig));
21962201
// Pillar change - basic
21972202
var pillarChangeMoveSelectorConfig = new PillarChangeMoveSelectorConfig();
21982203
var pillarChangeValueSelectorConfig = new ValueSelectorConfig().withVariableName("value");
@@ -2275,8 +2280,20 @@ private static List<MoveSelectorConfig> generateMovesForListVarEntityRangeModel(
22752280
.withVariableName("valueList");
22762281
// Change - list
22772282
allMoveSelectionConfigList.add(new ListChangeMoveSelectorConfig().withValueSelectorConfig(valueSelectorConfig));
2283+
// Change - list - original sort order
2284+
allMoveSelectionConfigList.add(
2285+
new ListChangeMoveSelectorConfig().withValueSelectorConfig(valueSelectorConfig)
2286+
.withSelectionOrder(SelectionOrder.ORIGINAL)
2287+
.withDestinationSelectorConfig(new DestinationSelectorConfig()
2288+
.withEntitySelectorConfig(
2289+
new EntitySelectorConfig().withSelectionOrder(SelectionOrder.ORIGINAL))
2290+
.withValueSelectorConfig(
2291+
new ValueSelectorConfig().withSelectionOrder(SelectionOrder.ORIGINAL))));
22782292
// Swap - list
22792293
allMoveSelectionConfigList.add(new ListSwapMoveSelectorConfig().withValueSelectorConfig(valueSelectorConfig));
2294+
// Swap - list - original sort order
2295+
allMoveSelectionConfigList.add(new ListSwapMoveSelectorConfig().withSelectionOrder(SelectionOrder.ORIGINAL)
2296+
.withValueSelectorConfig(valueSelectorConfig.copyConfig().withSelectionOrder(SelectionOrder.ORIGINAL)));
22802297
// Sublist change - list
22812298
allMoveSelectionConfigList.add(new SubListChangeMoveSelectorConfig()
22822299
.withSubListSelectorConfig(new SubListSelectorConfig().withValueSelectorConfig(valueSelectorConfig)));

0 commit comments

Comments
 (0)