From 10290453c5ad0339e7619acfd336fb53a500ca75 Mon Sep 17 00:00:00 2001 From: fred Date: Wed, 6 Aug 2025 08:05:52 -0300 Subject: [PATCH 1/7] chore: fetch reachable values by range descriptor --- .../decorator/FilteringEntityValueRangeSelector.java | 3 ++- .../value/decorator/FilteringValueRangeSelector.java | 2 +- .../core/impl/score/director/ValueRangeManager.java | 12 +++++++----- .../selector/common/ReachableMatrixTest.java | 4 ++-- 4 files changed, 12 insertions(+), 9 deletions(-) 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..656b46b423 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,8 @@ 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()); + .getReachableValueMatrix( + childEntitySelector.getEntityDescriptor().getGenuineListVariableDescriptor().getValueRangeDescriptor()); 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..cf440343fc 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()); + .getReachableValueMatrix(listVariableStateSupply.getSourceVariableDescriptor().getValueRangeDescriptor()); valuesSize = reachableValues.getSize(); } 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..1828aae1e8 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,9 @@ public final class ValueRangeManager { private final Map, CountableValueRange> fromSolutionMap = new IdentityHashMap<>(); private final Map, CountableValueRange>> fromEntityMap = new IdentityHashMap<>(); + private final Map, ReachableValues> fromReachableValuesMap = + new IdentityHashMap<>(); - private @Nullable ReachableValues reachableValues = null; private @Nullable Solution_ cachedWorkingSolution = null; private @Nullable SolutionInitializationStatistics cachedInitializationStatistics = null; private @Nullable ProblemSizeStatistics cachedProblemSizeStatistics = null; @@ -414,15 +415,15 @@ public long countOnEntity(ValueRangeDescriptor valueRangeDescriptor, .getSize(); } - public ReachableValues getReachableValeMatrix(ListVariableDescriptor listVariableDescriptor) { + public ReachableValues getReachableValueMatrix(ValueRangeDescriptor valueRangeDescriptor) { + var reachableValues = fromReachableValuesMap.get(valueRangeDescriptor); 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())); } - var entityDescriptor = listVariableDescriptor.getEntityDescriptor(); - var valueRangeDescriptor = listVariableDescriptor.getValueRangeDescriptor(); + var entityDescriptor = valueRangeDescriptor.getVariableDescriptor().getEntityDescriptor(); var entityList = entityDescriptor.extractEntities(cachedWorkingSolution); var allValues = getFromSolution(valueRangeDescriptor); var valuesSize = allValues.getSize(); @@ -449,6 +450,7 @@ public ReachableValues getReachableValeMatrix(ListVariableDescriptor } } reachableValues = new ReachableValues(entityMatrix, valueMatrix); + fromReachableValuesMap.put(valueRangeDescriptor, reachableValues); } return reachableValues; } @@ -482,7 +484,7 @@ private static void updateValueMap(Map> valueMatrix, Countab public void reset(@Nullable Solution_ workingSolution) { fromSolutionMap.clear(); fromEntityMap.clear(); - reachableValues = null; + fromReachableValuesMap.clear(); // We only update the cached solution if it is not null; null means to only reset the maps. if (workingSolution != null) { cachedWorkingSolution = workingSolution; 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..2aabb397cf 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()); + .getReachableValueMatrix(entityDescriptor.getGenuineListVariableDescriptor().getValueRangeDescriptor()); 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()); + .getReachableValueMatrix(entityDescriptor.getGenuineListVariableDescriptor().getValueRangeDescriptor()); assertThat(reachableValues.extractValues(v1)).containsExactlyInAnyOrder(v2, v3); assertThat(reachableValues.extractValues(v2)).containsExactlyInAnyOrder(v1, v3); From 9094c3052f3b0c7895de135ab80c72376cb837e9 Mon Sep 17 00:00:00 2001 From: fred Date: Fri, 8 Aug 2025 14:50:24 -0300 Subject: [PATCH 2/7] feat: improve the logic for random iterators of entity range selectors --- .../FilteringValueRangeSelector.java | 91 +++++++++++++++--- ...DefaultConstructionHeuristicPhaseTest.java | 9 +- .../list/ElementDestinationSelectorTest.java | 22 +++-- .../list/ListChangeMoveSelectorTest.java | 50 ++++++++++ .../list/ListSwapMoveSelectorTest.java | 93 ++++++++++++++++--- .../testdomain/list/TestdataListUtils.java | 69 ++++++++++++-- .../solver/core/testutil/PlannerAssert.java | 8 +- 7 files changed, 294 insertions(+), 48 deletions(-) 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 cf440343fc..99ce460055 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 @@ -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 @@ -258,7 +269,7 @@ boolean isValueOrEntityReachable(Object destinationValue) { private class OriginalFilteringValueRangeIterator 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. + // The value iterator returns all possible values based on the outer selector 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. @@ -267,8 +278,8 @@ private class OriginalFilteringValueRangeIterator extends AbstractFilteringValue private OriginalFilteringValueRangeIterator(Iterator replayingValueIterator, Iterator valueIterator, ListVariableStateSupply listVariableStateSupply, ReachableValues reachableValues, - boolean checkSourceAndDestination, boolean useValueList) { - super(listVariableStateSupply, reachableValues, checkSourceAndDestination, useValueList); + boolean checkSourceAndDestination) { + super(listVariableStateSupply, reachableValues, checkSourceAndDestination, false); this.replayingValueIterator = replayingValueIterator; this.valueIterator = valueIterator; } @@ -307,15 +318,71 @@ protected Object createUpcomingSelection() { } private class RandomFilteringValueRangeIterator 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 the outer selector settings. + private final Iterator valueIterator; + private final int maxBailoutSize; + + private RandomFilteringValueRangeIterator(Iterator replayingValueIterator, Iterator valueIterator, + ListVariableStateSupply listVariableStateSupply, ReachableValues reachableValues, + int maxBailoutSize, boolean checkSourceAndDestination) { + super(listVariableStateSupply, reachableValues, checkSourceAndDestination, false); + this.replayingValueIterator = replayingValueIterator; + this.valueIterator = valueIterator; + this.maxBailoutSize = maxBailoutSize; + } + + private void initialize() { + if (initialized) { + return; + } + if (replayingValueIterator.hasNext()) { + var upcomingValue = replayingValueIterator.next(); + if (!valueIterator.hasNext()) { + noData(); + } else { + loadValues(Objects.requireNonNull(upcomingValue)); + } + } else { + noData(); + } + } + + @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/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/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/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); } From af8e56702fb54419a2ac83c29bd761bb82a24556 Mon Sep 17 00:00:00 2001 From: fred Date: Fri, 8 Aug 2025 20:17:29 -0300 Subject: [PATCH 3/7] chore: minor adjustments --- .../TestdataObjectDistanceMeter.java | 19 +++++++++++++++++++ .../testdomain/list/TestdataListUtils.java | 11 +++++++++++ 2 files changed, 30 insertions(+) create mode 100644 core/src/test/java/ai/timefold/solver/core/testdomain/TestdataObjectDistanceMeter.java 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 44a0130445..94fe9ac81f 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 @@ -17,6 +17,7 @@ import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.domain.valuerange.descriptor.ValueRangeDescriptor; +import ai.timefold.solver.core.impl.domain.variable.descriptor.BasicVariableDescriptor; 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; @@ -252,6 +253,16 @@ public static ListVariableDescriptor getListVariableDescr .getGenuineVariableDescriptor("valueList"); } + public static BasicVariableDescriptor getBasicVariableDescriptor( + InnerScoreDirector scoreDirector) { + return (BasicVariableDescriptor) scoreDirector + .getSolutionDescriptor() + .getGenuineEntityDescriptors() + .iterator() + .next() + .getGenuineVariableDescriptor("value"); + } + public static EntityDescriptor getEntityDescriptor( InnerScoreDirector scoreDirector) { return scoreDirector From b3077cc3290f80251c224c5e3f9d50ee19ccf0a3 Mon Sep 17 00:00:00 2001 From: fred Date: Wed, 19 Feb 2025 09:11:31 -0300 Subject: [PATCH 4/7] feat: add quarkus random seed --- .../TimefoldProcessorOverridePropertiesAtRuntimeTest.java | 6 +++++- .../java/ai/timefold/solver/quarkus/TimefoldRecorder.java | 2 ++ .../timefold/solver/quarkus/config/SolverRuntimeConfig.java | 5 +++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorOverridePropertiesAtRuntimeTest.java b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorOverridePropertiesAtRuntimeTest.java index 5dd7f1061b..af33fd9ca2 100644 --- a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorOverridePropertiesAtRuntimeTest.java +++ b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorOverridePropertiesAtRuntimeTest.java @@ -62,6 +62,7 @@ private static String getRequiredProperty(String name) { private static Map getRuntimeProperties() { Map out = new HashMap<>(); out.put("quarkus.timefold.solver.termination.best-score-limit", "7"); + out.put("quarkus.timefold.solver.random-seed", "123"); out.put("quarkus.timefold.solver.move-thread-count", "3"); out.put("quarkus.timefold.solver-manager.parallel-solver-count", "10"); out.put("quarkus.timefold.solver.termination.diminished-returns.enabled", "true"); @@ -90,11 +91,13 @@ public String getSolverConfig() { termination.diminished-returns.minimum-improvement-ratio=%s termination.bestScoreLimit=%s moveThreadCount=%s + randomSeed=%d """ .formatted(diminishedReturnsConfig.getSlidingWindowDuration().toHours(), diminishedReturnsConfig.getMinimumImprovementRatio(), solverConfig.getTerminationConfig().getBestScoreLimit(), - solverConfig.getMoveThreadCount()); + solverConfig.getMoveThreadCount(), + 123); } @GET @@ -120,6 +123,7 @@ void solverConfigPropertiesShouldBeOverwritten() throws IOException { assertEquals("0.5", solverConfigProperties.get("termination.diminished-returns.minimum-improvement-ratio")); assertEquals("7", solverConfigProperties.get("termination.bestScoreLimit")); assertEquals("3", solverConfigProperties.get("moveThreadCount")); + assertEquals("123", solverConfigProperties.get("randomSeed")); } @Test diff --git a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/TimefoldRecorder.java b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/TimefoldRecorder.java index ed760c051e..44eb66a61f 100644 --- a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/TimefoldRecorder.java +++ b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/TimefoldRecorder.java @@ -136,6 +136,8 @@ public static void updateSolverConfigWithRuntimeProperties(SolverConfig solverCo .ifPresent(solverConfig::setDaemon); maybeSolverRuntimeConfig.flatMap(SolverRuntimeConfig::moveThreadCount) .ifPresent(solverConfig::setMoveThreadCount); + maybeSolverRuntimeConfig.flatMap(SolverRuntimeConfig::randomSeed) + .ifPresent(solverConfig::setRandomSeed); maybeSolverRuntimeConfig.flatMap(config -> config.termination().diminishedReturns()) .ifPresent(diminishedReturnsConfig -> setDiminishedReturns(solverConfig, diminishedReturnsConfig)); } diff --git a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/config/SolverRuntimeConfig.java b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/config/SolverRuntimeConfig.java index 671520a238..8507f1f371 100644 --- a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/config/SolverRuntimeConfig.java +++ b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/config/SolverRuntimeConfig.java @@ -43,4 +43,9 @@ public interface SolverRuntimeConfig { * Configuration properties that overwrite {@link TerminationConfig}. */ TerminationRuntimeConfig termination(); + + /** + * Configuration of the random seed. + */ + Optional randomSeed(); } From 3e3f6e29495d502973772e5a0bfb16872ba526d8 Mon Sep 17 00:00:00 2001 From: fred Date: Mon, 11 Aug 2025 13:41:46 -0300 Subject: [PATCH 5/7] chore: address comments --- .../FilteringEntityValueRangeSelector.java | 3 +-- .../FilteringValueRangeSelector.java | 2 +- .../score/director/ValueRangeManager.java | 20 ++++++++----------- .../selector/common/ReachableMatrixTest.java | 4 ++-- .../testdomain/list/TestdataListUtils.java | 11 ---------- 5 files changed, 12 insertions(+), 28 deletions(-) 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 656b46b423..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,8 +62,7 @@ public void phaseStarted(AbstractPhaseScope phaseScope) { super.phaseStarted(phaseScope); this.entitiesSize = childEntitySelector.getEntityDescriptor().extractEntities(phaseScope.getWorkingSolution()).size(); this.reachableValues = phaseScope.getScoreDirector().getValueRangeManager() - .getReachableValueMatrix( - childEntitySelector.getEntityDescriptor().getGenuineListVariableDescriptor().getValueRangeDescriptor()); + .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 99ce460055..b81fbff9aa 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() - .getReachableValueMatrix(listVariableStateSupply.getSourceVariableDescriptor().getValueRangeDescriptor()); + .getReachableValues(listVariableStateSupply.getSourceVariableDescriptor()); valuesSize = reachableValues.getSize(); } 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 1828aae1e8..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,7 @@ public final class ValueRangeManager { private final Map, CountableValueRange> fromSolutionMap = new IdentityHashMap<>(); private final Map, CountableValueRange>> fromEntityMap = new IdentityHashMap<>(); - private final Map, ReachableValues> fromReachableValuesMap = - new IdentityHashMap<>(); + private @Nullable ReachableValues reachableValues = null; private @Nullable Solution_ cachedWorkingSolution = null; private @Nullable SolutionInitializationStatistics cachedInitializationStatistics = null; @@ -415,24 +414,22 @@ public long countOnEntity(ValueRangeDescriptor valueRangeDescriptor, .getSize(); } - public ReachableValues getReachableValueMatrix(ValueRangeDescriptor valueRangeDescriptor) { - var reachableValues = fromReachableValuesMap.get(valueRangeDescriptor); + 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 = valueRangeDescriptor.getVariableDescriptor().getEntityDescriptor(); + var entityDescriptor = listVariableDescriptor.getEntityDescriptor(); 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); @@ -440,7 +437,7 @@ public ReachableValues getReachableValueMatrix(ValueRangeDescriptor v 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)) { @@ -450,7 +447,6 @@ public ReachableValues getReachableValueMatrix(ValueRangeDescriptor v } } reachableValues = new ReachableValues(entityMatrix, valueMatrix); - fromReachableValuesMap.put(valueRangeDescriptor, reachableValues); } return reachableValues; } @@ -484,7 +480,7 @@ private static void updateValueMap(Map> valueMatrix, Countab public void reset(@Nullable Solution_ workingSolution) { fromSolutionMap.clear(); fromEntityMap.clear(); - fromReachableValuesMap.clear(); + reachableValues = null; // We only update the cached solution if it is not null; null means to only reset the maps. if (workingSolution != null) { cachedWorkingSolution = workingSolution; 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 2aabb397cf..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() - .getReachableValueMatrix(entityDescriptor.getGenuineListVariableDescriptor().getValueRangeDescriptor()); + .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() - .getReachableValueMatrix(entityDescriptor.getGenuineListVariableDescriptor().getValueRangeDescriptor()); + .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/testdomain/list/TestdataListUtils.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/TestdataListUtils.java index 94fe9ac81f..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 @@ -17,7 +17,6 @@ import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.domain.valuerange.descriptor.ValueRangeDescriptor; -import ai.timefold.solver.core.impl.domain.variable.descriptor.BasicVariableDescriptor; 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; @@ -253,16 +252,6 @@ public static ListVariableDescriptor getListVariableDescr .getGenuineVariableDescriptor("valueList"); } - public static BasicVariableDescriptor getBasicVariableDescriptor( - InnerScoreDirector scoreDirector) { - return (BasicVariableDescriptor) scoreDirector - .getSolutionDescriptor() - .getGenuineEntityDescriptors() - .iterator() - .next() - .getGenuineVariableDescriptor("value"); - } - public static EntityDescriptor getEntityDescriptor( InnerScoreDirector scoreDirector) { return scoreDirector From 73083a39472c948941f5f281c9f82ab4cdbb56f7 Mon Sep 17 00:00:00 2001 From: fred Date: Mon, 11 Aug 2025 13:50:21 -0300 Subject: [PATCH 6/7] Revert "feat: add quarkus random seed" This reverts commit b3077cc3290f80251c224c5e3f9d50ee19ccf0a3. --- .../TimefoldProcessorOverridePropertiesAtRuntimeTest.java | 6 +----- .../java/ai/timefold/solver/quarkus/TimefoldRecorder.java | 2 -- .../timefold/solver/quarkus/config/SolverRuntimeConfig.java | 5 ----- 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorOverridePropertiesAtRuntimeTest.java b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorOverridePropertiesAtRuntimeTest.java index af33fd9ca2..5dd7f1061b 100644 --- a/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorOverridePropertiesAtRuntimeTest.java +++ b/quarkus-integration/quarkus/deployment/src/test/java/ai/timefold/solver/quarkus/TimefoldProcessorOverridePropertiesAtRuntimeTest.java @@ -62,7 +62,6 @@ private static String getRequiredProperty(String name) { private static Map getRuntimeProperties() { Map out = new HashMap<>(); out.put("quarkus.timefold.solver.termination.best-score-limit", "7"); - out.put("quarkus.timefold.solver.random-seed", "123"); out.put("quarkus.timefold.solver.move-thread-count", "3"); out.put("quarkus.timefold.solver-manager.parallel-solver-count", "10"); out.put("quarkus.timefold.solver.termination.diminished-returns.enabled", "true"); @@ -91,13 +90,11 @@ public String getSolverConfig() { termination.diminished-returns.minimum-improvement-ratio=%s termination.bestScoreLimit=%s moveThreadCount=%s - randomSeed=%d """ .formatted(diminishedReturnsConfig.getSlidingWindowDuration().toHours(), diminishedReturnsConfig.getMinimumImprovementRatio(), solverConfig.getTerminationConfig().getBestScoreLimit(), - solverConfig.getMoveThreadCount(), - 123); + solverConfig.getMoveThreadCount()); } @GET @@ -123,7 +120,6 @@ void solverConfigPropertiesShouldBeOverwritten() throws IOException { assertEquals("0.5", solverConfigProperties.get("termination.diminished-returns.minimum-improvement-ratio")); assertEquals("7", solverConfigProperties.get("termination.bestScoreLimit")); assertEquals("3", solverConfigProperties.get("moveThreadCount")); - assertEquals("123", solverConfigProperties.get("randomSeed")); } @Test diff --git a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/TimefoldRecorder.java b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/TimefoldRecorder.java index 44eb66a61f..ed760c051e 100644 --- a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/TimefoldRecorder.java +++ b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/TimefoldRecorder.java @@ -136,8 +136,6 @@ public static void updateSolverConfigWithRuntimeProperties(SolverConfig solverCo .ifPresent(solverConfig::setDaemon); maybeSolverRuntimeConfig.flatMap(SolverRuntimeConfig::moveThreadCount) .ifPresent(solverConfig::setMoveThreadCount); - maybeSolverRuntimeConfig.flatMap(SolverRuntimeConfig::randomSeed) - .ifPresent(solverConfig::setRandomSeed); maybeSolverRuntimeConfig.flatMap(config -> config.termination().diminishedReturns()) .ifPresent(diminishedReturnsConfig -> setDiminishedReturns(solverConfig, diminishedReturnsConfig)); } diff --git a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/config/SolverRuntimeConfig.java b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/config/SolverRuntimeConfig.java index 8507f1f371..671520a238 100644 --- a/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/config/SolverRuntimeConfig.java +++ b/quarkus-integration/quarkus/runtime/src/main/java/ai/timefold/solver/quarkus/config/SolverRuntimeConfig.java @@ -43,9 +43,4 @@ public interface SolverRuntimeConfig { * Configuration properties that overwrite {@link TerminationConfig}. */ TerminationRuntimeConfig termination(); - - /** - * Configuration of the random seed. - */ - Optional randomSeed(); } From 8acf712cc8d7ec62e14ac94052aecbf00e5bc57d Mon Sep 17 00:00:00 2001 From: fred Date: Mon, 11 Aug 2025 15:20:07 -0300 Subject: [PATCH 7/7] chore: address sonar --- .../FilteringValueRangeSelector.java | 55 +++++++------------ 1 file changed, 20 insertions(+), 35 deletions(-) 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 b81fbff9aa..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 @@ -266,25 +266,21 @@ 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; + final Iterator replayingValueIterator; // The value iterator returns all possible values based on the outer selector 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 valueIterator; - private OriginalFilteringValueRangeIterator(Iterator replayingValueIterator, Iterator valueIterator, + private AbstractUpcomingValueRangeIterator(Iterator replayingValueIterator, Iterator valueIterator, ListVariableStateSupply listVariableStateSupply, ReachableValues reachableValues, - boolean checkSourceAndDestination) { - super(listVariableStateSupply, reachableValues, checkSourceAndDestination, false); + boolean checkSourceAndDestination, boolean useValueList) { + super(listVariableStateSupply, reachableValues, checkSourceAndDestination, useValueList); this.replayingValueIterator = replayingValueIterator; this.valueIterator = valueIterator; } - private void initialize() { + void initialize() { if (initialized) { return; } @@ -299,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() { @@ -317,38 +323,17 @@ protected Object createUpcomingSelection() { } } - private class RandomFilteringValueRangeIterator 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 the outer selector settings. - private final Iterator valueIterator; + private class RandomFilteringValueRangeIterator extends AbstractUpcomingValueRangeIterator { private final int maxBailoutSize; private RandomFilteringValueRangeIterator(Iterator replayingValueIterator, Iterator valueIterator, ListVariableStateSupply listVariableStateSupply, ReachableValues reachableValues, int maxBailoutSize, boolean checkSourceAndDestination) { - super(listVariableStateSupply, reachableValues, checkSourceAndDestination, false); - this.replayingValueIterator = replayingValueIterator; - this.valueIterator = valueIterator; + super(replayingValueIterator, valueIterator, listVariableStateSupply, reachableValues, checkSourceAndDestination, + false); this.maxBailoutSize = maxBailoutSize; } - private void initialize() { - if (initialized) { - return; - } - if (replayingValueIterator.hasNext()) { - var upcomingValue = replayingValueIterator.next(); - if (!valueIterator.hasNext()) { - noData(); - } else { - loadValues(Objects.requireNonNull(upcomingValue)); - } - } else { - noData(); - } - } - @Override protected Object createUpcomingSelection() { initialize();