diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/FilteringEntityValueRangeSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/FilteringEntityValueRangeSelector.java index de0b7bf296..537f470ad1 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/FilteringEntityValueRangeSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/FilteringEntityValueRangeSelector.java @@ -62,7 +62,7 @@ public void phaseStarted(AbstractPhaseScope phaseScope) { super.phaseStarted(phaseScope); this.entitiesSize = childEntitySelector.getEntityDescriptor().extractEntities(phaseScope.getWorkingSolution()).size(); this.reachableValues = phaseScope.getScoreDirector().getValueRangeManager() - .getReachableValeMatrix(childEntitySelector.getEntityDescriptor().getGenuineListVariableDescriptor()); + .getReachableValues(phaseScope.getScoreDirector().getSolutionDescriptor().getListVariableDescriptor()); this.childEntitySelector.phaseStarted(phaseScope); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FilteringValueRangeSelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FilteringValueRangeSelector.java index 3c00299c77..0b2f1e5da7 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FilteringValueRangeSelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/decorator/FilteringValueRangeSelector.java @@ -76,7 +76,7 @@ public void phaseStarted(AbstractPhaseScope phaseScope) { this.nonReplayingValueSelector.phaseStarted(phaseScope); this.replayingValueSelector.phaseStarted(phaseScope); this.reachableValues = phaseScope.getScoreDirector().getValueRangeManager() - .getReachableValeMatrix(listVariableStateSupply.getSourceVariableDescriptor()); + .getReachableValues(listVariableStateSupply.getSourceVariableDescriptor()); valuesSize = reachableValues.getSize(); } @@ -129,20 +129,31 @@ public Iterator iterator(Object entity) { @Override public Iterator iterator() { if (randomSelection) { - return new RandomFilteringValueRangeIterator(replayingValueSelector.iterator(), listVariableStateSupply, - reachableValues, workingRandom, (int) getSize(), checkSourceAndDestination, true); + // If the nonReplayingValueSelector does not have any additional configuration, + // we can bypass it and only use reachable values, + // which helps optimize the number of evaluations. + // However, if the nonReplayingValueSelector includes custom configurations, + // such as filtering, + // we will first evaluate its values and then filter out those that are not reachable. + if (nonReplayingValueSelector instanceof IterableFromEntityPropertyValueSelector) { + return new OptimizedRandomFilteringValueRangeIterator(replayingValueSelector.iterator(), + listVariableStateSupply, + reachableValues, workingRandom, (int) getSize(), checkSourceAndDestination); + } else { + return new RandomFilteringValueRangeIterator(replayingValueSelector.iterator(), + nonReplayingValueSelector.iterator(), listVariableStateSupply, reachableValues, (int) getSize(), + checkSourceAndDestination); + } } else { return new OriginalFilteringValueRangeIterator(replayingValueSelector.iterator(), - nonReplayingValueSelector.iterator(), listVariableStateSupply, reachableValues, checkSourceAndDestination, - false); + nonReplayingValueSelector.iterator(), listVariableStateSupply, reachableValues, checkSourceAndDestination); } } @Override public Iterator endingIterator(Object entity) { return new OriginalFilteringValueRangeIterator(replayingValueSelector.iterator(), - nonReplayingValueSelector.iterator(), listVariableStateSupply, reachableValues, checkSourceAndDestination, - false); + nonReplayingValueSelector.iterator(), listVariableStateSupply, reachableValues, checkSourceAndDestination); } @Override @@ -255,17 +266,13 @@ boolean isValueOrEntityReachable(Object destinationValue) { } } - private class OriginalFilteringValueRangeIterator extends AbstractFilteringValueRangeIterator { + private abstract class AbstractUpcomingValueRangeIterator extends AbstractFilteringValueRangeIterator { // The value iterator that only replays the current selected value - private final Iterator replayingValueIterator; - // The value iterator returns all possible values based on its settings. - // However, - // it may include invalid values that need to be filtered out. - // This iterator must be used to ensure that all positions are included in the CH phase. - // This does not apply to the LS phase. - private final Iterator valueIterator; + final Iterator replayingValueIterator; + // The value iterator returns all possible values based on the outer selector settings. + final Iterator valueIterator; - private OriginalFilteringValueRangeIterator(Iterator replayingValueIterator, Iterator valueIterator, + private AbstractUpcomingValueRangeIterator(Iterator replayingValueIterator, Iterator valueIterator, ListVariableStateSupply listVariableStateSupply, ReachableValues reachableValues, boolean checkSourceAndDestination, boolean useValueList) { super(listVariableStateSupply, reachableValues, checkSourceAndDestination, useValueList); @@ -273,7 +280,7 @@ private OriginalFilteringValueRangeIterator(Iterator replayingValueItera this.valueIterator = valueIterator; } - private void initialize() { + void initialize() { if (initialized) { return; } @@ -288,6 +295,16 @@ private void initialize() { noData(); } } + } + + private class OriginalFilteringValueRangeIterator extends AbstractUpcomingValueRangeIterator { + + private OriginalFilteringValueRangeIterator(Iterator replayingValueIterator, Iterator valueIterator, + ListVariableStateSupply listVariableStateSupply, ReachableValues reachableValues, + boolean checkSourceAndDestination) { + super(replayingValueIterator, valueIterator, listVariableStateSupply, reachableValues, checkSourceAndDestination, + false); + } @Override protected Object createUpcomingSelection() { @@ -306,16 +323,51 @@ protected Object createUpcomingSelection() { } } - private class RandomFilteringValueRangeIterator extends AbstractFilteringValueRangeIterator { + private class RandomFilteringValueRangeIterator extends AbstractUpcomingValueRangeIterator { + private final int maxBailoutSize; + + private RandomFilteringValueRangeIterator(Iterator replayingValueIterator, Iterator valueIterator, + ListVariableStateSupply listVariableStateSupply, ReachableValues reachableValues, + int maxBailoutSize, boolean checkSourceAndDestination) { + super(replayingValueIterator, valueIterator, listVariableStateSupply, reachableValues, checkSourceAndDestination, + false); + this.maxBailoutSize = maxBailoutSize; + } + + @Override + protected Object createUpcomingSelection() { + initialize(); + if (!hasData) { + return noUpcomingSelection(); + } + Object next; + var bailoutSize = maxBailoutSize; + do { + if (bailoutSize <= 0 || !valueIterator.hasNext()) { + return noUpcomingSelection(); + } + bailoutSize--; + next = valueIterator.next(); + } while (!isValueOrEntityReachable(next)); + return next; + } + } + + /** + * The optimized iterator only traverses reachable values from the current selection. + * Unlike {@link RandomFilteringValueRangeIterator}, + * it does not use an outer iterator to filter out non-reachable values. + */ + private class OptimizedRandomFilteringValueRangeIterator extends AbstractFilteringValueRangeIterator { private final Iterator replayingValueIterator; private final Random workingRandom; private final int maxBailoutSize; - private RandomFilteringValueRangeIterator(Iterator replayingValueIterator, + private OptimizedRandomFilteringValueRangeIterator(Iterator replayingValueIterator, ListVariableStateSupply listVariableStateSupply, ReachableValues reachableValues, - Random workingRandom, int maxBailoutSize, boolean checkSourceAndDestination, boolean useValueList) { - super(listVariableStateSupply, reachableValues, checkSourceAndDestination, useValueList); + Random workingRandom, int maxBailoutSize, boolean checkSourceAndDestination) { + super(listVariableStateSupply, reachableValues, checkSourceAndDestination, true); this.replayingValueIterator = replayingValueIterator; this.workingRandom = workingRandom; this.maxBailoutSize = maxBailoutSize; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeManager.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeManager.java index 594ebb3902..90b3a71c48 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeManager.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/ValueRangeManager.java @@ -59,8 +59,8 @@ public final class ValueRangeManager { private final Map, CountableValueRange> fromSolutionMap = new IdentityHashMap<>(); private final Map, CountableValueRange>> fromEntityMap = new IdentityHashMap<>(); - private @Nullable ReachableValues reachableValues = null; + private @Nullable Solution_ cachedWorkingSolution = null; private @Nullable SolutionInitializationStatistics cachedInitializationStatistics = null; private @Nullable ProblemSizeStatistics cachedProblemSizeStatistics = null; @@ -414,24 +414,22 @@ public long countOnEntity(ValueRangeDescriptor valueRangeDescriptor, .getSize(); } - public ReachableValues getReachableValeMatrix(ListVariableDescriptor listVariableDescriptor) { + public ReachableValues getReachableValues(ListVariableDescriptor listVariableDescriptor) { if (reachableValues == null) { if (cachedWorkingSolution == null) { throw new IllegalStateException( - "Impossible state: the matrix %s requested before the working solution is known." - .formatted(ReachableValues.class.getSimpleName())); + "Impossible state: value reachability requested before the working solution is known."); } var entityDescriptor = listVariableDescriptor.getEntityDescriptor(); - var valueRangeDescriptor = listVariableDescriptor.getValueRangeDescriptor(); var entityList = entityDescriptor.extractEntities(cachedWorkingSolution); - var allValues = getFromSolution(valueRangeDescriptor); + var allValues = getFromSolution(listVariableDescriptor.getValueRangeDescriptor()); var valuesSize = allValues.getSize(); if (valuesSize > Integer.MAX_VALUE) { throw new IllegalStateException( "The matrix %s cannot be built for the entity %s (%s) because value range has a size (%d) which is higher than Integer.MAX_VALUE." .formatted(ReachableValues.class.getSimpleName(), entityDescriptor.getEntityClass().getSimpleName(), - valueRangeDescriptor.getVariableDescriptor().getVariableName(), valuesSize)); + listVariableDescriptor.getVariableName(), valuesSize)); } // list of entities reachable for a value var entityMatrix = new IdentityHashMap>((int) valuesSize); @@ -439,7 +437,7 @@ public ReachableValues getReachableValeMatrix(ListVariableDescriptor var valueMatrix = new IdentityHashMap>((int) valuesSize); for (var entity : entityList) { var valuesIterator = allValues.createOriginalIterator(); - var range = getFromEntity(valueRangeDescriptor, entity); + var range = getFromEntity(listVariableDescriptor.getValueRangeDescriptor(), entity); while (valuesIterator.hasNext()) { var value = valuesIterator.next(); if (range.contains(value)) { diff --git a/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java index 8b17c05a6d..c96c176330 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java @@ -365,7 +365,8 @@ void solveWithEntityValueRangeBasicVariable() { @Test void solveWithEntityValueRangeListVariable() { var solverConfig = PlannerTestUtils - .buildSolverConfig(TestdataListEntityProvidingSolution.class, TestdataListEntityProvidingEntity.class) + .buildSolverConfig(TestdataListEntityProvidingSolution.class, TestdataListEntityProvidingEntity.class, + TestdataListEntityProvidingValue.class) .withEasyScoreCalculatorClass(TestdataListEntityProvidingScoreCalculator.class) .withPhases(new ConstructionHeuristicPhaseConfig()); @@ -381,8 +382,10 @@ void solveWithEntityValueRangeListVariable() { var bestSolution = PlannerTestUtils.solve(solverConfig, solution, true); assertThat(bestSolution).isNotNull(); // Only one entity should provide the value list and assign the values. - assertThat(bestSolution.getEntityList().get(0).getValueList()).hasSameElementsAs(List.of(value1, value2)); - assertThat(bestSolution.getEntityList().get(1).getValueList()).hasSameElementsAs(List.of(value3)); + assertThat(bestSolution.getEntityList().get(0).getValueList().stream().map(TestdataListEntityProvidingValue::getCode)) + .hasSameElementsAs(List.of("v1", "v2")); + assertThat(bestSolution.getEntityList().get(1).getValueList().stream().map(TestdataListEntityProvidingValue::getCode)) + .hasSameElementsAs(List.of("v3")); } @Test diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/ReachableMatrixTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/ReachableMatrixTest.java index c2a5061a5b..4d902f0318 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/ReachableMatrixTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/ReachableMatrixTest.java @@ -32,7 +32,7 @@ void testReachableEntities() { var solutionDescriptor = scoreDirector.getSolutionDescriptor(); var entityDescriptor = solutionDescriptor.findEntityDescriptor(TestdataListEntityProvidingEntity.class); var reachableValues = scoreDirector.getValueRangeManager() - .getReachableValeMatrix(entityDescriptor.getGenuineListVariableDescriptor()); + .getReachableValues(entityDescriptor.getGenuineListVariableDescriptor()); assertThat(reachableValues.extractEntities(v1)).containsExactlyInAnyOrder(a); assertThat(reachableValues.extractEntities(v2)).containsExactlyInAnyOrder(a, b); @@ -60,7 +60,7 @@ void testReachableValues() { var solutionDescriptor = scoreDirector.getSolutionDescriptor(); var entityDescriptor = solutionDescriptor.findEntityDescriptor(TestdataListEntityProvidingEntity.class); var reachableValues = scoreDirector.getValueRangeManager() - .getReachableValeMatrix(entityDescriptor.getGenuineListVariableDescriptor()); + .getReachableValues(entityDescriptor.getGenuineListVariableDescriptor()); assertThat(reachableValues.extractValues(v1)).containsExactlyInAnyOrder(v2, v3); assertThat(reachableValues.extractValues(v2)).containsExactlyInAnyOrder(v1, v3); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelectorTest.java index ba5753b45a..d60870cc4c 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/list/ElementDestinationSelectorTest.java @@ -9,6 +9,7 @@ import static ai.timefold.solver.core.testdomain.list.TestdataListUtils.getPinnedAllowsUnassignedvaluesListVariableDescriptor; import static ai.timefold.solver.core.testdomain.list.TestdataListUtils.getPinnedListVariableDescriptor; import static ai.timefold.solver.core.testdomain.list.TestdataListUtils.mockEntitySelector; +import static ai.timefold.solver.core.testdomain.list.TestdataListUtils.mockIterableFromEntityPropertyValueSelector; import static ai.timefold.solver.core.testdomain.list.TestdataListUtils.mockIterableValueSelector; import static ai.timefold.solver.core.testutil.PlannerAssert.assertAllCodesOfIterableSelector; import static ai.timefold.solver.core.testutil.PlannerAssert.assertAllCodesOfIterator; @@ -175,11 +176,11 @@ void randomWithEntityValueRange() { // 1 - pick random value in ElementPositionRandomIterator and return the first unpinned position // 1 - remaining call var valueSelector = mockIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v3); + var filteringValueRangeSelector = mockIterableFromEntityPropertyValueSelector(valueSelector, true); var replayinValueSelector = mockIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v3); checkEntityValueRange(new FilteringEntityValueRangeSelector<>(mockEntitySelector(a, b, c), valueSelector, true), - new FilteringValueRangeSelector<>(valueSelector, replayinValueSelector, true, false), scoreDirector, - new TestRandom(1, 1, 1), - "C[0]"); + new FilteringValueRangeSelector<>(filteringValueRangeSelector, replayinValueSelector, true, false), + scoreDirector, new TestRandom(1, 1, 1), "C[0]"); // select A for V1 and random pos A[2] // Random values @@ -188,12 +189,13 @@ void randomWithEntityValueRange() { // 0 - pick random position, only v2 is reachable // 0 - remaining call valueSelector = mockIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v1); + filteringValueRangeSelector = mockIterableFromEntityPropertyValueSelector(valueSelector, true); replayinValueSelector = mockIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v1); // Cause the value iterator return no value at the second call doReturn(List.of(v1).iterator(), Collections.emptyIterator()).when(valueSelector).iterator(); checkEntityValueRange(new FilteringEntityValueRangeSelector<>(mockEntitySelector(a, b, c), valueSelector, true), - new FilteringValueRangeSelector<>(valueSelector, replayinValueSelector, true, false), scoreDirector, - new TestRandom(0, 3, 0, 0), "A[2]"); + new FilteringValueRangeSelector<>(filteringValueRangeSelector, replayinValueSelector, true, false), + scoreDirector, new TestRandom(0, 3, 0, 0), "A[2]"); // select B for V1 and random pos B[1] // 1 - pick entity B in RandomFilteringValueRangeIterator @@ -201,12 +203,13 @@ void randomWithEntityValueRange() { // 1 - pick random position, v1 and v3 are reachable // 0 - remaining call valueSelector = mockIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v2, v2, v2, v2, v2); // simulate five positions + filteringValueRangeSelector = mockIterableFromEntityPropertyValueSelector(valueSelector, true); replayinValueSelector = mockIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v2); // Cause the value iterator return no value at the second call doReturn(List.of(v2).iterator(), Collections.emptyIterator()).when(valueSelector).iterator(); checkEntityValueRange(new FilteringEntityValueRangeSelector<>(mockEntitySelector(a, b, c), valueSelector, true), - new FilteringValueRangeSelector<>(valueSelector, replayinValueSelector, true, false), scoreDirector, - new TestRandom(1, 3, 1, 0), "B[1]"); + new FilteringValueRangeSelector<>(filteringValueRangeSelector, replayinValueSelector, true, false), + scoreDirector, new TestRandom(1, 3, 1, 0), "B[1]"); // select C for V5 and first unpinned pos C[1] // 0 - pick entity C in RandomFilteringValueRangeIterator @@ -214,12 +217,13 @@ void randomWithEntityValueRange() { // 1 - pick random position, v3 and v4 are reachable // 0 - remaining call valueSelector = mockIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v5, v5, v5, v5, v5); // simulate five positions + filteringValueRangeSelector = mockIterableFromEntityPropertyValueSelector(valueSelector, true); replayinValueSelector = mockIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v5); // Cause the value iterator return no value at the second call doReturn(List.of(v5).iterator(), Collections.emptyIterator()).when(valueSelector).iterator(); checkEntityValueRange(new FilteringEntityValueRangeSelector<>(mockEntitySelector(a, b, c), valueSelector, true), - new FilteringValueRangeSelector<>(valueSelector, replayinValueSelector, true, false), scoreDirector, - new TestRandom(0, 3, 1, 0), "C[1]"); + new FilteringValueRangeSelector<>(filteringValueRangeSelector, replayinValueSelector, true, false), + scoreDirector, new TestRandom(0, 3, 1, 0), "C[1]"); } private void checkEntityValueRange(FilteringEntityValueRangeSelector entitySelector, diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListChangeMoveSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListChangeMoveSelectorTest.java index c3e0ed0e75..d37b13762c 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListChangeMoveSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListChangeMoveSelectorTest.java @@ -25,6 +25,8 @@ import java.util.List; import java.util.Random; +import ai.timefold.solver.core.api.score.director.ScoreDirector; +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionFilter; import ai.timefold.solver.core.preview.api.domain.metamodel.ElementPosition; import ai.timefold.solver.core.testdomain.TestdataValue; import ai.timefold.solver.core.testdomain.list.TestdataListEntity; @@ -415,6 +417,40 @@ void randomWithEntityValueRange() { "3 {B[0]->B[0]}"); } + @Test + void randomWithEntityValueRangeAndFiltering() { + var v1 = new TestdataListEntityProvidingValue("1"); + var v2 = new TestdataListEntityProvidingValue("2"); + var v3 = new TestdataListEntityProvidingValue("3"); + var a = new TestdataListEntityProvidingEntity("A", List.of(v1, v2), List.of(v2, v1)); + var b = new TestdataListEntityProvidingEntity("B", List.of(v2, v3), List.of(v3)); + var solution = new TestdataListEntityProvidingSolution(); + solution.setEntityList(List.of(a, b)); + + var scoreDirector = mockScoreDirector(TestdataListEntityProvidingSolution.buildSolutionDescriptor()); + scoreDirector.setWorkingSolution(solution); + + var mimicRecordingValueSelector = getMimicRecordingIterableValueSelector( + getEntityRangeListVariableDescriptor(scoreDirector).getValueRangeDescriptor(), true); + var solutionDescriptor = scoreDirector.getSolutionDescriptor(); + var entityDescriptor = solutionDescriptor.findEntityDescriptor(TestdataListEntityProvidingEntity.class); + var destinationSelector = getEntityValueRangeDestinationSelector(mimicRecordingValueSelector, solutionDescriptor, + entityDescriptor, IgnoreBValueSelectionFilter.class, true); + var moveSelector = new ListChangeMoveSelector<>(mimicRecordingValueSelector, destinationSelector, true); + + var solverScope = solvingStarted(moveSelector, scoreDirector, new Random(0)); + phaseStarted(solverScope, moveSelector); + + // IgnoreBValueSelectionFilter is applied to the value selector used by the destination selector, + // and that causes the B destination to become an invalid destination + assertCodesOfNeverEndingMoveSelector(moveSelector, + "1 {A[1]->A[1]}", + "3 {B[0]->A[1]}", + "1 {A[1]->A[1]}", + "1 {A[1]->A[1]}", + "1 {A[1]->A[0]}"); + } + @Test void randomWithPinning() { var v1 = new TestdataPinnedWithIndexListValue("1"); @@ -576,4 +612,18 @@ void randomAllowsUnassignedValuesWithEntityValueRange() { "3 {B[0]->B[0]}", "3 {B[0]->B[0]}"); } + + public static class IgnoreBValueSelectionFilter + implements SelectionFilter { + + public IgnoreBValueSelectionFilter() { + // Required for solver initialization + } + + @Override + public boolean accept(ScoreDirector scoreDirector, + TestdataListEntityProvidingValue selection) { + return !selection.getEntity().getCode().equals("B"); + } + } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListSwapMoveSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListSwapMoveSelectorTest.java index eb7b8006e5..74cbafa85d 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListSwapMoveSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListSwapMoveSelectorTest.java @@ -100,19 +100,20 @@ void originalWithEntityValueRange() { var mimicRecordingValueSelector = getMimicRecordingIterableValueSelector( getEntityRangeListVariableDescriptor(scoreDirector).getValueRangeDescriptor(), false); - var filteringValueRangeSelector = getFilteringValueRangeSelector(mimicRecordingValueSelector, false, true); + var filteringValueRangeSelector = + getFilteringValueRangeSelector(mimicRecordingValueSelector, mimicRecordingValueSelector, false, true, false); var moveSelector = new ListSwapMoveSelector<>(mimicRecordingValueSelector, filteringValueRangeSelector, false); var solverScope = solvingStarted(moveSelector, scoreDirector); phaseStarted(solverScope, moveSelector); - // Not testing size; filtering selector doesn't and can't report correct size unless iterating over all values. assertAllCodesOfMoveSelectorWithoutSize(moveSelector, - "1 {A[1]} <-> 2 {A[0]}", - "2 {A[0]} <-> 1 {A[1]}", - "2 {A[0]} <-> 3 {B[0]}", - "3 {B[0]} <-> 2 {A[0]}"); + "1 {A[1]} <-> 2 {A[0]}", // A is the only valid entity for v1 + "2 {A[0]} <-> 1 {A[1]}", // A and B accepts v2 and v1 is reachable by v2 + "2 {A[0]} <-> 3 {B[0]}", // A and B accepts v2 and v3 is reachable by v2 + "3 {B[0]} <-> 2 {A[0]}" // A and B accepts v3 and v2 is reachable by v3 + ); } @Test @@ -178,14 +179,14 @@ void originalWithPinningAndEntityValueRange() { var mimicRecordingValueSelector = getMimicRecordingIterableValueSelector( getPinnedEntityRangeListVariableDescriptor(scoreDirector).getValueRangeDescriptor(), false); - var filteringValueRangeSelector = getFilteringValueRangeSelector(mimicRecordingValueSelector, false, true); + var filteringValueRangeSelector = + getFilteringValueRangeSelector(mimicRecordingValueSelector, mimicRecordingValueSelector, false, true, false); var moveSelector = new ListSwapMoveSelector<>(mimicRecordingValueSelector, filteringValueRangeSelector, false); var solverScope = solvingStarted(moveSelector, scoreDirector); phaseStarted(solverScope, moveSelector); - // Not testing size; filtering selector doesn't and can't report correct size unless iterating over all values. assertAllCodesOfMoveSelectorWithoutSize(moveSelector, "1 {A[1]} <-> 3 {C[0]}", "3 {C[0]} <-> 1 {A[1]}"); @@ -253,14 +254,14 @@ void originalAllowsUnassignedValuesWithEntityValueRange() { var mimicRecordingValueSelector = getMimicRecordingIterableValueSelector( getAllowsUnassignedvaluesEntityRangeListVariableDescriptor(scoreDirector).getValueRangeDescriptor(), false); - var filteringValueRangeSelector = getFilteringValueRangeSelector(mimicRecordingValueSelector, false, true); + var filteringValueRangeSelector = + getFilteringValueRangeSelector(mimicRecordingValueSelector, mimicRecordingValueSelector, false, true, false); var moveSelector = new ListSwapMoveSelector<>(mimicRecordingValueSelector, filteringValueRangeSelector, false); var solverScope = solvingStarted(moveSelector, scoreDirector); phaseStarted(solverScope, moveSelector); - // Not testing size; filtering selector doesn't and can't report correct size unless iterating over all values. assertAllCodesOfMoveSelectorWithoutSize(moveSelector, "1 {A[1]} <-> 2 {A[0]}", "2 {A[0]} <-> 1 {A[1]}"); @@ -319,14 +320,17 @@ void randomWithEntityValueRange() { var mimicRecordingValueSelector = getMimicRecordingIterableValueSelector( getEntityRangeListVariableDescriptor(scoreDirector), v2, v1, v3, v1, v3, v2, v1, v3, v1, v3); - var filteringValueRangeSelector = getFilteringValueRangeSelector(mimicRecordingValueSelector, true, true); + var iterableValueRangeSelector = mockIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v2, v1, + v3, v1, v3, v2, v1, v3, v1, v3); + + var filteringValueRangeSelector = + getFilteringValueRangeSelector(mimicRecordingValueSelector, iterableValueRangeSelector, true, true, true); var moveSelector = new ListSwapMoveSelector<>(mimicRecordingValueSelector, filteringValueRangeSelector, true); var solverScope = solvingStarted(moveSelector, scoreDirector, new Random(0)); phaseStarted(solverScope, moveSelector); - // Not testing size; filtering selector doesn't and can't report correct size unless iterating over all values. assertCodesOfNeverEndingMoveSelector(moveSelector, "2 {A[0]} <-> 3 {B[0]}", "1 {A[1]} <-> 2 {A[0]}", @@ -335,6 +339,65 @@ void randomWithEntityValueRange() { "3 {B[0]} <-> 2 {A[0]}"); } + @Test + void randomWithEntityValueRangeAndFiltering() { + var v1 = new TestdataListEntityProvidingValue("1"); + var v2 = new TestdataListEntityProvidingValue("2"); + var v3 = new TestdataListEntityProvidingValue("3"); + var a = new TestdataListEntityProvidingEntity("A", List.of(v1, v2, v3), List.of(v2, v1)); + var b = new TestdataListEntityProvidingEntity("B", List.of(v2, v3), List.of(v3)); + var solution = new TestdataListEntityProvidingSolution(); + solution.setEntityList(List.of(a, b)); + + var scoreDirector = mockScoreDirector(TestdataListEntityProvidingSolution.buildSolutionDescriptor()); + scoreDirector.setWorkingSolution(solution); + + // This test validates a path in the FilteringValueRangeSelector that does not use the OptimizedRandomFilteringValueRangeIterator + { + // The mimic recorder selector returns v2 + var mimicRecordingValueSelector = + getMimicRecordingIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v2, v2); + + // The nonReplaying selector returns only v3 + var iterableValueRangeSelector = + mockIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v3, v3, v3, v3); + + // Since OptimizedRandomFilteringValueRangeIterator is not used, the values from iterableValueRangeSelector must be accounted + var filteringValueRangeSelector = + getFilteringValueRangeSelector(mimicRecordingValueSelector, iterableValueRangeSelector, true, true, false); + + var moveSelector = new ListSwapMoveSelector<>(mimicRecordingValueSelector, filteringValueRangeSelector, true); + + var solverScope = solvingStarted(moveSelector, scoreDirector, new Random(0)); + phaseStarted(solverScope, moveSelector); + + // Generate one move for v2 + assertCodesOfNeverEndingMoveSelector(moveSelector, + "2 {A[0]} <-> 3 {B[0]}"); + } + { + // The mimic recorder selector returns v1 + var mimicRecordingValueSelector = + getMimicRecordingIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v1); + + // The value selector will return only v1 and nonReplaying selector will return the value v3, which is assigned to B + // Selecting v1 will result in no valid destination because B does not accept v1 + var iterableValueRangeSelector = + mockIterableValueSelector(getEntityRangeListVariableDescriptor(scoreDirector), v3, v3, v3, v3); + + // Since OptimizedRandomFilteringValueRangeIterator is not used, the values from iterableValueRangeSelector must be accounted + var filteringValueRangeSelector = + getFilteringValueRangeSelector(mimicRecordingValueSelector, iterableValueRangeSelector, true, true, false); + + var moveSelector = new ListSwapMoveSelector<>(mimicRecordingValueSelector, filteringValueRangeSelector, true); + + var solverScope = solvingStarted(moveSelector, scoreDirector, new Random(0)); + phaseStarted(solverScope, moveSelector); + + assertCodesOfNeverEndingMoveSelector(moveSelector); + } + } + @Test void randomWithPinning() { var v1 = new TestdataPinnedWithIndexListValue("1"); @@ -393,7 +456,8 @@ void randomWithPinningAndEntityValueRange() { var mimicRecordingValueSelector = getMimicRecordingIterableValueSelector( getPinnedEntityRangeListVariableDescriptor(scoreDirector), v3, v1, v4, v1); - var filteringValueRangeSelector = getFilteringValueRangeSelector(mimicRecordingValueSelector, true, true); + var filteringValueRangeSelector = + getFilteringValueRangeSelector(mimicRecordingValueSelector, mimicRecordingValueSelector, true, true, true); var moveSelector = new ListSwapMoveSelector<>(mimicRecordingValueSelector, filteringValueRangeSelector, true); @@ -463,7 +527,8 @@ void randomAllowsUnassignedValuesWithEntityValueRange() { var mimicRecordingValueSelector = getMimicRecordingIterableValueSelector( getAllowsUnassignedvaluesEntityRangeListVariableDescriptor(scoreDirector), v3, v1, v2, v4, v1); - var filteringValueRangeSelector = getFilteringValueRangeSelector(mimicRecordingValueSelector, true, true); + var filteringValueRangeSelector = + getFilteringValueRangeSelector(mimicRecordingValueSelector, mimicRecordingValueSelector, true, true, true); var moveSelector = new ListSwapMoveSelector<>(mimicRecordingValueSelector, filteringValueRangeSelector, true); diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/TestdataObjectDistanceMeter.java b/core/src/test/java/ai/timefold/solver/core/testdomain/TestdataObjectDistanceMeter.java new file mode 100644 index 0000000000..0383d5dc56 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/TestdataObjectDistanceMeter.java @@ -0,0 +1,19 @@ +package ai.timefold.solver.core.testdomain; + +import ai.timefold.solver.core.impl.heuristic.selector.common.nearby.NearbyDistanceMeter; + +public class TestdataObjectDistanceMeter implements NearbyDistanceMeter { + + @Override + public double getNearbyDistance(T origin, TestdataObject destination) { + return Math.abs(coordinate(destination) - coordinate(origin)); + } + + static int coordinate(TestdataObject o) { + try { + return Integer.parseInt(o.getCode()); + } catch (NumberFormatException e) { + return 0; + } + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListUtils.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListUtils.java index 1b75f4be1d..44a0130445 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListUtils.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListUtils.java @@ -20,6 +20,7 @@ import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; import ai.timefold.solver.core.impl.heuristic.selector.SelectorTestUtils; +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionFilter; import ai.timefold.solver.core.impl.heuristic.selector.entity.EntitySelector; import ai.timefold.solver.core.impl.heuristic.selector.list.DestinationSelector; import ai.timefold.solver.core.impl.heuristic.selector.list.DestinationSelectorFactory; @@ -30,6 +31,7 @@ import ai.timefold.solver.core.impl.heuristic.selector.value.mimic.MimicRecordingValueSelector; import ai.timefold.solver.core.impl.heuristic.selector.value.mimic.MimicReplayingValueSelector; import ai.timefold.solver.core.impl.score.director.InnerScoreDirector; +import ai.timefold.solver.core.impl.solver.ClassInstanceCache; import ai.timefold.solver.core.preview.api.domain.metamodel.ElementPosition; import ai.timefold.solver.core.preview.api.domain.metamodel.PositionInList; import ai.timefold.solver.core.testdomain.TestdataValue; @@ -70,6 +72,11 @@ public static EntitySelector mockEntitySelector(TestdataLi return SelectorTestUtils.mockEntitySelector(TestdataListEntity.buildEntityDescriptor(), (Object[]) entities); } + public static EntitySelector + mockEntitySelector(EntityDescriptor entityDescriptor, Entity_... entities) { + return SelectorTestUtils.mockEntitySelector(entityDescriptor, entities); + } + public static EntitySelector mockEntitySelector(TestdataListEntityProvidingEntity... entities) { return SelectorTestUtils.mockEntitySelector( @@ -99,6 +106,16 @@ public static IterableValueSelector mockIterableValueSele return SelectorTestUtils.mockIterableValueSelector(listVariableDescriptor, values); } + public static IterableFromEntityPropertyValueSelector + mockIterableFromEntityPropertyValueSelector(IterableValueSelector childMoveSelector, + boolean randomSelection) { + var fromEntityValueSelector = mock(FromEntityPropertyValueSelector.class); + doReturn(childMoveSelector.iterator()).when(fromEntityValueSelector).iterator(any()); + doReturn(childMoveSelector.getVariableDescriptor()).when(fromEntityValueSelector).getVariableDescriptor(); + doReturn(childMoveSelector.isCountable()).when(fromEntityValueSelector).isCountable(); + return new IterableFromEntityPropertyValueSelector(fromEntityValueSelector, randomSelection); + } + public static IterableValueSelector mockNeverEndingIterableValueSelector( ListVariableDescriptor listVariableDescriptor, TestdataListValue... values) { var valueSelector = mockIterableValueSelector( @@ -225,14 +242,25 @@ public static DestinationSelector mockDestinationSelector return destinationSelector; } - public static ListVariableDescriptor getListVariableDescriptor( - InnerScoreDirector scoreDirector) { - return (ListVariableDescriptor) scoreDirector + public static ListVariableDescriptor getListVariableDescriptor( + InnerScoreDirector scoreDirector) { + return (ListVariableDescriptor) scoreDirector .getSolutionDescriptor() - .getEntityDescriptorStrict(TestdataListEntity.class) + .getGenuineEntityDescriptors() + .iterator() + .next() .getGenuineVariableDescriptor("valueList"); } + public static EntityDescriptor getEntityDescriptor( + InnerScoreDirector scoreDirector) { + return scoreDirector + .getSolutionDescriptor() + .getGenuineEntityDescriptors() + .iterator() + .next(); + } + public static ListVariableDescriptor getEntityRangeListVariableDescriptor( InnerScoreDirector scoreDirector) { return (ListVariableDescriptor) scoreDirector @@ -305,10 +333,19 @@ public static ListVariableDescriptor getPin public static FilteringValueRangeSelector getFilteringValueRangeSelector(MimicRecordingValueSelector mimicRecordingValueSelector, - boolean randomSelection, boolean assertBothSides) { + IterableValueSelector nonReplaying, + boolean randomSelection, boolean assertBothSides, boolean generateIterableFromEntityProperty) { var replayingValueSelector = new MimicReplayingValueSelector<>(mimicRecordingValueSelector); - return new FilteringValueRangeSelector<>(mimicRecordingValueSelector, replayingValueSelector, randomSelection, - assertBothSides); + if (generateIterableFromEntityProperty) { + var iterableEntityPropertyValueSelector = + mockIterableFromEntityPropertyValueSelector(nonReplaying, randomSelection); + // Ensure OptimizedRandomFilteringValueRangeIterator is created for random iterators + return new FilteringValueRangeSelector<>(iterableEntityPropertyValueSelector, replayingValueSelector, + randomSelection, assertBothSides); + } else { + return new FilteringValueRangeSelector<>(nonReplaying, replayingValueSelector, randomSelection, + assertBothSides); + } } public static DestinationSelector getEntityValueRangeDestinationSelector( @@ -326,6 +363,24 @@ public static DestinationSelector getEntityValueRangeDestinationSelect .buildDestinationSelector(configPolicy, SelectionCacheType.JUST_IN_TIME, randomSelection, true, "any"); } + public static DestinationSelector getEntityValueRangeDestinationSelector( + MimicRecordingValueSelector innerMimicRecordingValueSelector, SolutionDescriptor solutionDescriptor, + EntityDescriptor entityDescriptor, Class selectionFilterClass, + boolean randomSelection) { + var destinationSelectorConfig = new DestinationSelectorConfig(); + destinationSelectorConfig.setEntitySelectorConfig(new EntitySelectorConfig() + .withEntityClass(entityDescriptor.getEntityClass())); + destinationSelectorConfig.setValueSelectorConfig(new ValueSelectorConfig() + .withFilterClass(selectionFilterClass) + .withVariableName(entityDescriptor.getGenuineListVariableDescriptor().getVariableName())); + var configPolicy = mock(HeuristicConfigPolicy.class); + doReturn(solutionDescriptor).when(configPolicy).getSolutionDescriptor(); + doReturn(innerMimicRecordingValueSelector).when(configPolicy).getValueMimicRecorder(any()); + doReturn(ClassInstanceCache.create()).when(configPolicy).getClassInstanceCache(); + return DestinationSelectorFactory. create(destinationSelectorConfig) + .buildDestinationSelector(configPolicy, SelectionCacheType.JUST_IN_TIME, randomSelection, true, "any"); + } + private static Iterator cyclicIterator(List elements) { if (elements.isEmpty()) { return Collections.emptyIterator(); diff --git a/core/src/test/java/ai/timefold/solver/core/testutil/PlannerAssert.java b/core/src/test/java/ai/timefold/solver/core/testutil/PlannerAssert.java index eb8e6fb7e3..49362c6fe6 100644 --- a/core/src/test/java/ai/timefold/solver/core/testutil/PlannerAssert.java +++ b/core/src/test/java/ai/timefold/solver/core/testutil/PlannerAssert.java @@ -274,9 +274,11 @@ public static void assertAllCodesOfIterableSelector(IterableSelector selec public static void assertCodesOfNeverEndingIterableSelector(IterableSelector selector, long size, String... codes) { Iterator iterator = selector.iterator(); assertCodesOfNeverEndingIterator(iterator, codes); - assertThat(iterator).hasNext(); - assertThat(selector.isCountable()).isTrue(); - assertThat(selector.isNeverEnding()).isTrue(); + if (codes.length > 0) { + assertThat(iterator).hasNext(); + assertThat(selector.isCountable()).isTrue(); + assertThat(selector.isNeverEnding()).isTrue(); + } if (size != DO_NOT_ASSERT_SIZE) { assertThat(selector.getSize()).isEqualTo(size); }