From f2731364d70533a3ba59e0f05e98ef09fda0564e Mon Sep 17 00:00:00 2001 From: fred Date: Tue, 3 Feb 2026 09:52:25 -0300 Subject: [PATCH 01/13] chore: one-time fail fast check of value ranges --- .../score/director/AbstractScoreDirector.java | 67 ++++++++++ .../score/director/InnerScoreDirector.java | 7 + .../core/impl/solver/scope/SolverScope.java | 4 + .../core/impl/solver/DefaultSolverTest.java | 123 ++++++++++++++++++ 4 files changed, 201 insertions(+) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java index 4ca56f985e..aba067af69 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java @@ -8,7 +8,10 @@ import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.Spliterator; +import java.util.Spliterators; import java.util.function.Consumer; +import java.util.stream.StreamSupport; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.domain.solution.cloner.SolutionCloner; @@ -26,6 +29,7 @@ import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.domain.variable.InnerVariableListener; import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply; +import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.VariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.listener.support.VariableListenerSupport; @@ -816,6 +820,69 @@ that could cause the scoreDifference (%s).""".formatted(scoreDifference, beforeM } } + @Override + public void assertValueRangeForSolution(Solution_ workingSolution) { + var solutionDescriptor = getSolutionDescriptor(); + var allEntityDescriptorList = solutionDescriptor.getEntityDescriptors(); + for (var entityDescriptor : allEntityDescriptorList) { + var basicVariableDescriptorList = entityDescriptor.getGenuineBasicVariableDescriptorList(); + var listVariableDescriptor = entityDescriptor.getGenuineListVariableDescriptor(); + var allEntities = entityDescriptor.extractEntities(workingSolution); + for (var entity : allEntities) { + assertBasicVariables(this, basicVariableDescriptorList, entity); + assertListVariable(this, listVariableDescriptor, entity); + } + } + } + + private static void assertBasicVariables(InnerScoreDirector scoreDirector, + List> basicVariableDescriptorList, Object entity) { + if (basicVariableDescriptorList == null || basicVariableDescriptorList.isEmpty()) { + return; + } + for (var variableDescriptor : basicVariableDescriptorList) { + var value = variableDescriptor.getValue(entity); + var valueRange = scoreDirector.getValueRangeManager() + .getFromEntity(variableDescriptor.getValueRangeDescriptor(), entity); + // We use the lookup search instead of rebasing in order to return the expected error message + if (value != null && !valueRange + .contains(Objects.requireNonNullElse(scoreDirector.lookUpWorkingObjectOrReturnNull(value), value))) { + throw new IllegalStateException( + "The value (%s) from the planning variable (%s) has been assigned to the entity (%s), but it is outside of the related value range %s." + .formatted(value, variableDescriptor.getVariableName(), entity, + StreamSupport.stream(Spliterators.spliterator(valueRange.createOriginalIterator(), + valueRange.getSize(), Spliterator.SIZED), false) + .toList())); + } + } + + } + + private static void assertListVariable(InnerScoreDirector scoreDirector, + ListVariableDescriptor variableDescriptor, Object entity) { + if (variableDescriptor == null) { + return; + } + var valueList = variableDescriptor.getValue(entity); + if (valueList.isEmpty()) { + return; + } + var valueRange = scoreDirector.getValueRangeManager() + .getFromEntity(variableDescriptor.getValueRangeDescriptor(), entity); + for (var value : valueList) { + // We use the lookup search instead of rebasing in order to return the expected error message + if (value != null && !valueRange + .contains(Objects.requireNonNullElse(scoreDirector.lookUpWorkingObjectOrReturnNull(value), value))) { + throw new IllegalStateException( + "The value (%s) from the planning variable (%s) has been assigned to the entity (%s), but it is outside of the related value range %s." + .formatted(value, variableDescriptor.getVariableName(), entity, + StreamSupport.stream(Spliterators.spliterator(valueRange.createOriginalIterator(), + valueRange.getSize(), Spliterator.SIZED), false) + .toList())); + } + } + } + public SolutionTracker.SolutionCorruptionResult getSolutionCorruptionAfterUndo(Move move, InnerScore undoInnerScore) { var trackingWorkingSolution = solutionTracker != null; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java index ea90596bac..0cd489a751 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java @@ -370,6 +370,13 @@ default Solution_ cloneWorkingSolution() { void assertExpectedUndoMoveScore(Move move, InnerScore beforeMoveScore, SolverLifecyclePoint executionPoint); + /** + * Asserts if any assigned planning values are included in the solution range or any entity value range. + * + * @param workingSolution the solution to be evaluated + */ + void assertValueRangeForSolution(Solution_ workingSolution); + /** * Needs to be called after use because some implementations need to clean up their resources. */ diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java index fe8be8f24a..0b831f9b08 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java @@ -343,6 +343,10 @@ public void setInitialSolution(Solution_ initialSolution) { // Set the best solution to the solution with shadow variable updated. setBestSolution(scoreDirector.cloneSolution(scoreDirector.getWorkingSolution())); + + // One-time check of value ranges + // to ensure assigned planning values are included in the solution range or any entity value range + scoreDirector.assertValueRangeForSolution(initialSolution); } public SolverScope createChildThreadSolverScope(ChildThreadType childThreadType) { diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java index 37563ee918..eee3802648 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java @@ -101,12 +101,17 @@ import ai.timefold.solver.core.testdomain.list.unassignedvar.TestdataAllowsUnassignedValuesListEntity; import ai.timefold.solver.core.testdomain.list.unassignedvar.TestdataAllowsUnassignedValuesListSolution; import ai.timefold.solver.core.testdomain.list.unassignedvar.TestdataAllowsUnassignedValuesListValue; +import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingEntity; +import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingSolution; +import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingValue; import ai.timefold.solver.core.testdomain.list.valuerange.unassignedvar.TestdataListUnassignedEntityProvidingEntity; import ai.timefold.solver.core.testdomain.list.valuerange.unassignedvar.TestdataListUnassignedEntityProvidingScoreCalculator; import ai.timefold.solver.core.testdomain.list.valuerange.unassignedvar.TestdataListUnassignedEntityProvidingSolution; import ai.timefold.solver.core.testdomain.mixed.multientity.TestdataMixedEntityEasyScoreCalculator; import ai.timefold.solver.core.testdomain.mixed.multientity.TestdataMixedMultiEntityFirstEntity; +import ai.timefold.solver.core.testdomain.mixed.multientity.TestdataMixedMultiEntityFirstValue; import ai.timefold.solver.core.testdomain.mixed.multientity.TestdataMixedMultiEntitySecondEntity; +import ai.timefold.solver.core.testdomain.mixed.multientity.TestdataMixedMultiEntitySecondValue; import ai.timefold.solver.core.testdomain.mixed.multientity.TestdataMixedMultiEntitySolution; import ai.timefold.solver.core.testdomain.mixed.singleentity.MixedCustomMoveIteratorFactory; import ai.timefold.solver.core.testdomain.mixed.singleentity.MixedCustomPhaseCommand; @@ -136,6 +141,10 @@ import ai.timefold.solver.core.testdomain.sort.comparator.OneValuePerEntityComparatorEasyScoreCalculator; import ai.timefold.solver.core.testdomain.sort.comparator.TestdataComparatorSortableEntity; import ai.timefold.solver.core.testdomain.sort.comparator.TestdataComparatorSortableSolution; +import ai.timefold.solver.core.testdomain.valuerange.entityproviding.TestdataEntityProvidingEntity; +import ai.timefold.solver.core.testdomain.valuerange.entityproviding.TestdataEntityProvidingSolution; +import ai.timefold.solver.core.testdomain.valuerange.entityproviding.multivar.TestdataAllowsUnassignedMultiVarEntityProvidingEntity; +import ai.timefold.solver.core.testdomain.valuerange.entityproviding.multivar.TestdataAllowsUnassignedMultiVarEntityProvidingSolution; import ai.timefold.solver.core.testdomain.valuerange.entityproviding.unassignedvar.TestdataAllowsUnassignedEntityProvidingEntity; import ai.timefold.solver.core.testdomain.valuerange.entityproviding.unassignedvar.TestdataAllowsUnassignedEntityProvidingScoreCalculator; import ai.timefold.solver.core.testdomain.valuerange.entityproviding.unassignedvar.TestdataAllowsUnassignedEntityProvidingSolution; @@ -1960,6 +1969,120 @@ void failRuinRecreateWithMultiEntityMultiVar() { .hasMessageContaining("it cannot be deduced automatically"); } + @Test + void failBasicVariableInvalidValueRange() { + // Solver config + var solverConfig = + PlannerTestUtils.buildSolverConfig(TestdataEntityProvidingSolution.class, TestdataEntityProvidingEntity.class) + .withEasyScoreCalculatorClass(DummySimpleScoreEasyScoreCalculator.class); + + var problem = new TestdataEntityProvidingSolution(); + var v1 = new TestdataValue("1"); + var v2 = new TestdataValue("2"); + var v3 = new TestdataValue("3"); + // The entity has an assigned value v3 that is not included in the entity value ranges + var e1 = new TestdataEntityProvidingEntity("e1", List.of(v1, v2), v3); + var e2 = new TestdataEntityProvidingEntity("e2", List.of(v1, v2), v1); + problem.setEntityList(new ArrayList<>(Arrays.asList(e1, e2))); + + assertThatCode(() -> PlannerTestUtils.solve(solverConfig, problem)) + .hasMessageContaining( + "The value (3) from the planning variable (value) has been assigned to the entity (e1), but it is outside of the related value range [1, 2]"); + } + + @Test + void failMultipleBasicVariableInvalidValueRange() { + // Solver config + var solverConfig = + PlannerTestUtils + .buildSolverConfig(TestdataAllowsUnassignedMultiVarEntityProvidingSolution.class, + TestdataAllowsUnassignedMultiVarEntityProvidingEntity.class) + .withEasyScoreCalculatorClass(DummySimpleScoreEasyScoreCalculator.class); + + var problem = new TestdataAllowsUnassignedMultiVarEntityProvidingSolution(); + var v1 = new TestdataValue("1"); + var v2 = new TestdataValue("2"); + var v3 = new TestdataValue("3"); + // The entity has been assigned a value v3 for the second value range, + // which is not included in the entity's value ranges + var e1 = new TestdataAllowsUnassignedMultiVarEntityProvidingEntity("e1", List.of(v1, v2, v3), v3, List.of(v1, v2), v3, + v3); + var e2 = new TestdataAllowsUnassignedMultiVarEntityProvidingEntity("e2", List.of(v1, v2, v3), v1, List.of(v1, v2, v3), + v1, v1); + problem.setEntityList(new ArrayList<>(Arrays.asList(e1, e2))); + + assertThatCode(() -> PlannerTestUtils.solve(solverConfig, problem)) + .hasMessageContaining( + "The value (3) from the planning variable (secondValue) has been assigned to the entity (e1), but it is outside of the related value range [null, 1, 2]"); + } + + @Test + void failListVariableInvalidValueRange() { + // Solver config + var solverConfig = PlannerTestUtils.buildSolverConfig( + TestdataListEntityProvidingSolution.class, TestdataListEntityProvidingEntity.class, + TestdataListEntityProvidingValue.class) + .withEasyScoreCalculatorClass(DummySimpleScoreEasyScoreCalculator.class); + + var problem = new TestdataListEntityProvidingSolution(); + var v1 = new TestdataListEntityProvidingValue("1"); + var v2 = new TestdataListEntityProvidingValue("2"); + var v3 = new TestdataListEntityProvidingValue("3"); + // The entity has an assigned value v3 that is not included in the entity value ranges + var e1 = new TestdataListEntityProvidingEntity("e1", List.of(v1, v2), List.of(v3)); + var e2 = new TestdataListEntityProvidingEntity("e2", List.of(v1, v2), List.of(v1)); + problem.setEntityList(new ArrayList<>(Arrays.asList(e1, e2))); + + assertThatCode(() -> PlannerTestUtils.solve(solverConfig, problem)) + .hasMessageContaining( + "The value (3) from the planning variable (valueList) has been assigned to the entity (e1), but it is outside of the related value range [1, 2]"); + } + + @Test + void failMixedModelInvalidValueRange() { + // Solver config + var solverConfig = PlannerTestUtils.buildSolverConfig( + TestdataMixedMultiEntitySolution.class, TestdataMixedMultiEntityFirstEntity.class, + TestdataMixedMultiEntitySecondEntity.class) + .withEasyScoreCalculatorClass(DummySimpleScoreEasyScoreCalculator.class); + + var problem = new TestdataMixedMultiEntitySolution(); + var v1a = new TestdataMixedMultiEntityFirstValue("1"); + var v2a = new TestdataMixedMultiEntityFirstValue("2"); + var v3a = new TestdataMixedMultiEntityFirstValue("3"); + var v1b = new TestdataMixedMultiEntitySecondValue("1", 1); + var v2b = new TestdataMixedMultiEntitySecondValue("2", 1); + var v3b = new TestdataMixedMultiEntitySecondValue("3", 1); + + // 1 - Invalid basic variable + var e1a = new TestdataMixedMultiEntityFirstEntity("e1", 1); + var e1b = new TestdataMixedMultiEntitySecondEntity("e1"); + e1b.setBasicValue(v1b); + var e2b = new TestdataMixedMultiEntitySecondEntity("e2"); + // Invalid assigned value + problem.setEntityList(List.of(e1a)); + problem.setOtherEntityList(List.of(e1b, e2b)); + problem.setValueList(List.of(v1a, v2a, v3a)); + problem.setOtherValueList(List.of(v2b, v3b)); + + assertThatCode(() -> PlannerTestUtils.solve(solverConfig, problem)) + .hasMessageContaining( + "The value (1) from the planning variable (basicValue) has been assigned to the entity (e1), but it is outside of the related value range [2, 3]"); + e1b.setBasicValue(null); + + // 2 - Invalid list variable + // Invalid assigned value + e1a.getValueList().add(v1a); + problem.setEntityList(List.of(e1a)); + problem.setOtherEntityList(List.of(e1b, e2b)); + problem.setValueList(List.of(v2a, v3a)); + problem.setOtherValueList(List.of(v2b, v3b)); + + assertThatCode(() -> PlannerTestUtils.solve(solverConfig, problem)) + .hasMessageContaining( + "The value (1) from the planning variable (valueList) has been assigned to the entity (e1), but it is outside of the related value range [2, 3]"); + } + public static final class MinimizeUnusedEntitiesEasyScoreCalculator implements EasyScoreCalculator { From c4d649a88c94a727e9d564aa00237d87e459620d Mon Sep 17 00:00:00 2001 From: fred Date: Tue, 3 Feb 2026 11:53:32 -0300 Subject: [PATCH 02/13] chore: add assertion of the value range to solver phases --- .../impl/phase/custom/DefaultCustomPhase.java | 8 ++ .../score/director/AbstractScoreDirector.java | 9 ++- .../core/impl/solver/DefaultSolverTest.java | 81 +++++++++++++++++++ 3 files changed, 94 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultCustomPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultCustomPhase.java index 09669ca1cb..550095d19f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultCustomPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultCustomPhase.java @@ -29,9 +29,12 @@ public final class DefaultCustomPhase private final List> customPhaseCommandList; private TerminationStatus terminationStatus = TerminationStatus.NOT_TERMINATED; + private final boolean assertValueRange; + private DefaultCustomPhase(DefaultCustomPhaseBuilder builder) { super(builder); this.customPhaseCommandList = builder.customPhaseCommandList; + this.assertValueRange = builder.assertValueRange; } @Override @@ -87,6 +90,9 @@ private void doStep(CustomStepScope stepScope, PhaseCommand phaseTermination.isPhaseTerminated(stepScope.getPhaseScope())); + if (assertValueRange) { + scoreDirector.assertValueRangeForSolution(scoreDirector.getWorkingSolution()); + } calculateWorkingStepScore(stepScope, customPhaseCommand); var solver = stepScope.getPhaseScope().getSolverScope().getSolver(); solver.getBestSolutionRecaller().processWorkingSolutionDuringStep(stepScope); @@ -124,6 +130,7 @@ public static final class DefaultCustomPhaseBuilder extends AbstractPossiblyInitializingPhaseBuilder { private final List> customPhaseCommandList; + private boolean assertValueRange = false; public DefaultCustomPhaseBuilder(int phaseIndex, boolean lastInitializingPhase, String logIndentation, PhaseTermination phaseTermination, List> customPhaseCommandList) { @@ -134,6 +141,7 @@ public DefaultCustomPhaseBuilder(int phaseIndex, boolean lastInitializingPhase, @Override public DefaultCustomPhaseBuilder enableAssertions(EnvironmentMode environmentMode) { super.enableAssertions(environmentMode); + this.assertValueRange = environmentMode.isFullyAsserted(); return this; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java index aba067af69..3a0a291060 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java @@ -352,6 +352,7 @@ public InnerScore executeTemporaryMove(Move move, @Nullable C solutionTracker.setAfterMoveSolution(workingSolution); } if (assertMoveScoreFromScratch) { + assertValueRangeForSolution(workingSolution); assertWorkingScoreFromScratch(score, move); } if (consumer != null) { @@ -829,13 +830,13 @@ public void assertValueRangeForSolution(Solution_ workingSolution) { var listVariableDescriptor = entityDescriptor.getGenuineListVariableDescriptor(); var allEntities = entityDescriptor.extractEntities(workingSolution); for (var entity : allEntities) { - assertBasicVariables(this, basicVariableDescriptorList, entity); - assertListVariable(this, listVariableDescriptor, entity); + assertValueRangeForBasicVariables(this, basicVariableDescriptorList, entity); + assertValueRangeForListVariable(this, listVariableDescriptor, entity); } } } - private static void assertBasicVariables(InnerScoreDirector scoreDirector, + private static void assertValueRangeForBasicVariables(InnerScoreDirector scoreDirector, List> basicVariableDescriptorList, Object entity) { if (basicVariableDescriptorList == null || basicVariableDescriptorList.isEmpty()) { return; @@ -858,7 +859,7 @@ private static void assertBasicVariables(InnerScoreDirector void assertListVariable(InnerScoreDirector scoreDirector, + private static void assertValueRangeForListVariable(InnerScoreDirector scoreDirector, ListVariableDescriptor variableDescriptor, Object entity) { if (variableDescriptor == null) { return; diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java index eee3802648..6d758fa87f 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java @@ -12,6 +12,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Random; @@ -70,6 +71,8 @@ import ai.timefold.solver.core.config.solver.PreviewFeature; import ai.timefold.solver.core.config.solver.SolverConfig; import ai.timefold.solver.core.config.solver.termination.TerminationConfig; +import ai.timefold.solver.core.impl.heuristic.move.AbstractMove; +import ai.timefold.solver.core.impl.heuristic.selector.move.factory.MoveIteratorFactory; import ai.timefold.solver.core.impl.score.DummySimpleScoreEasyScoreCalculator; import ai.timefold.solver.core.impl.score.constraint.DefaultConstraintMatchTotal; import ai.timefold.solver.core.impl.score.constraint.DefaultIndictment; @@ -2083,6 +2086,37 @@ void failMixedModelInvalidValueRange() { "The value (1) from the planning variable (valueList) has been assigned to the entity (e1), but it is outside of the related value range [2, 3]"); } + @Test + void failLocalSearchValueRangeAssertion() { + var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataListSolution.class, TestdataListEntity.class, + TestdataListValue.class); + solverConfig.setEnvironmentMode(EnvironmentMode.FULL_ASSERT); + var localSearchPhaseConfig = new LocalSearchPhaseConfig(); + localSearchPhaseConfig.setMoveSelectorConfig( + new MoveIteratorFactoryConfig().withMoveIteratorFactoryClass(InvalidMoveListFactory.class)); + solverConfig.setPhaseConfigList(List.of(new ConstructionHeuristicPhaseConfig(), localSearchPhaseConfig)); + + var problem = TestdataListSolution.generateUninitializedSolution(2, 2); + assertThatCode(() -> PlannerTestUtils.solve(solverConfig, problem)) + .hasMessageContaining( + "The value (bad value) from the planning variable (valueList) has been assigned to the entity (Generated Entity 0), but it is outside of the related value range [Generated Value 0, Generated Value 1]"); + } + + @Test + void failCustomPhaseValueRangeAssertion() { + var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataListSolution.class, TestdataListEntity.class, + TestdataListValue.class); + solverConfig.setEnvironmentMode(EnvironmentMode.FULL_ASSERT); + var customPhaseConfig = new CustomPhaseConfig() + .withCustomPhaseCommands(new InvalidCustomPhaseCommand()); + solverConfig.setPhaseConfigList(List.of(new ConstructionHeuristicPhaseConfig(), customPhaseConfig)); + + var problem = TestdataListSolution.generateUninitializedSolution(2, 2); + assertThatCode(() -> PlannerTestUtils.solve(solverConfig, problem)) + .hasMessageContaining( + "The value (bad value) from the planning variable (valueList) has been assigned to the entity (Generated Entity 0), but it is outside of the related value range [Generated Value 0, Generated Value 1]"); + } + public static final class MinimizeUnusedEntitiesEasyScoreCalculator implements EasyScoreCalculator { @@ -2332,4 +2366,51 @@ public static final class TestingMixedEasyScoreCalculator } + + public static final class InvalidCustomPhaseCommand implements PhaseCommand { + + @Override + public void changeWorkingSolution(ScoreDirector scoreDirector, BooleanSupplier isPhaseTerminated) { + var entity = scoreDirector.getWorkingSolution().getEntityList().get(0); + scoreDirector.beforeListVariableChanged(entity, "valueList", 0, 0); + entity.getValueList().add(new TestdataListValue("bad value")); + scoreDirector.afterListVariableChanged(entity, "valueList", 0, entity.getValueList().size()); + } + } + + public static final class InvalidMoveListFactory implements MoveIteratorFactory { + @Override + public long getSize(ScoreDirector scoreDirector) { + return 1; + } + + @Override + public Iterator + createOriginalMoveIterator(ScoreDirector scoreDirector) { + return List.of(new InvalidMove()).iterator(); + } + + @Override + public Iterator createRandomMoveIterator( + ScoreDirector scoreDirector, + Random workingRandom) { + return createOriginalMoveIterator(scoreDirector); + } + } + + public static class InvalidMove extends AbstractMove { + + @Override + protected void doMoveOnGenuineVariables(ScoreDirector scoreDirector) { + var entity = scoreDirector.getWorkingSolution().getEntityList().get(0); + scoreDirector.beforeListVariableChanged(entity, "valueList", 0, 0); + entity.getValueList().add(new TestdataListValue("bad value")); + scoreDirector.afterListVariableChanged(entity, "valueList", 0, entity.getValueList().size()); + } + + @Override + public boolean isMoveDoable(ScoreDirector scoreDirector) { + return true; + } + } } From 954990d5962e405383e229e727fb15108053db6b Mon Sep 17 00:00:00 2001 From: fred Date: Tue, 3 Feb 2026 14:33:50 -0300 Subject: [PATCH 03/13] chore: improve assertion logic --- .../impl/phase/custom/DefaultCustomPhase.java | 8 --- .../score/director/AbstractScoreDirector.java | 67 +++++++++++++------ .../score/director/InnerScoreDirector.java | 7 -- .../impl/solver/DefaultSolverFactory.java | 1 + .../core/impl/solver/scope/SolverScope.java | 4 -- .../core/impl/solver/DefaultSolverTest.java | 6 +- 6 files changed, 51 insertions(+), 42 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultCustomPhase.java b/core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultCustomPhase.java index 550095d19f..09669ca1cb 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultCustomPhase.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/phase/custom/DefaultCustomPhase.java @@ -29,12 +29,9 @@ public final class DefaultCustomPhase private final List> customPhaseCommandList; private TerminationStatus terminationStatus = TerminationStatus.NOT_TERMINATED; - private final boolean assertValueRange; - private DefaultCustomPhase(DefaultCustomPhaseBuilder builder) { super(builder); this.customPhaseCommandList = builder.customPhaseCommandList; - this.assertValueRange = builder.assertValueRange; } @Override @@ -90,9 +87,6 @@ private void doStep(CustomStepScope stepScope, PhaseCommand phaseTermination.isPhaseTerminated(stepScope.getPhaseScope())); - if (assertValueRange) { - scoreDirector.assertValueRangeForSolution(scoreDirector.getWorkingSolution()); - } calculateWorkingStepScore(stepScope, customPhaseCommand); var solver = stepScope.getPhaseScope().getSolverScope().getSolver(); solver.getBestSolutionRecaller().processWorkingSolutionDuringStep(stepScope); @@ -130,7 +124,6 @@ public static final class DefaultCustomPhaseBuilder extends AbstractPossiblyInitializingPhaseBuilder { private final List> customPhaseCommandList; - private boolean assertValueRange = false; public DefaultCustomPhaseBuilder(int phaseIndex, boolean lastInitializingPhase, String logIndentation, PhaseTermination phaseTermination, List> customPhaseCommandList) { @@ -141,7 +134,6 @@ public DefaultCustomPhaseBuilder(int phaseIndex, boolean lastInitializingPhase, @Override public DefaultCustomPhaseBuilder enableAssertions(EnvironmentMode environmentMode) { super.enableAssertions(environmentMode); - this.assertValueRange = environmentMode.isFullyAsserted(); return this; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java index 3a0a291060..68b0973393 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java @@ -104,6 +104,8 @@ public abstract class AbstractScoreDirector moveRepository; protected AbstractScoreDirector(AbstractScoreDirectorBuilder builder) { @@ -129,6 +131,7 @@ protected AbstractScoreDirector(AbstractScoreDirectorBuilder executeTemporaryMove(Move move, @Nullable C solutionTracker.setAfterMoveSolution(workingSolution); } if (assertMoveScoreFromScratch) { - assertValueRangeForSolution(workingSolution); assertWorkingScoreFromScratch(score, move); } if (consumer != null) { @@ -509,6 +515,9 @@ public void afterVariableChanged(VariableDescriptor variableDescripto } variableListenerSupport.afterVariableChanged(variableDescriptor, entity); neighborhoodsElementUpdateNotifier.accept(entity); + if (isStepAssertOrMore) { + assertValueRangeForEntity(entity); + } } @Override @@ -559,6 +568,9 @@ public void afterListVariableChanged(ListVariableDescriptor variableD int toIndex) { variableListenerSupport.afterListVariableChanged(variableDescriptor, entity, fromIndex, toIndex); neighborhoodsElementUpdateNotifier.accept(entity); + if (isStepAssertOrMore) { + assertValueRangeForEntity(entity); + } } public void beforeEntityRemoved(EntityDescriptor entityDescriptor, Object entity) { @@ -821,33 +833,36 @@ that could cause the scoreDifference (%s).""".formatted(scoreDifference, beforeM } } - @Override - public void assertValueRangeForSolution(Solution_ workingSolution) { - var solutionDescriptor = getSolutionDescriptor(); - var allEntityDescriptorList = solutionDescriptor.getEntityDescriptors(); - for (var entityDescriptor : allEntityDescriptorList) { - var basicVariableDescriptorList = entityDescriptor.getGenuineBasicVariableDescriptorList(); - var listVariableDescriptor = entityDescriptor.getGenuineListVariableDescriptor(); - var allEntities = entityDescriptor.extractEntities(workingSolution); - for (var entity : allEntities) { - assertValueRangeForBasicVariables(this, basicVariableDescriptorList, entity); - assertValueRangeForListVariable(this, listVariableDescriptor, entity); - } + private void assertValueRangeForEntity(Object entity) { + var entityDescriptor = getSolutionDescriptor().findEntityDescriptor(entity.getClass()); + if (entityDescriptor == null) { + // It may be called for a shadow entity + return; } + var basicVariableDescriptorList = entityDescriptor.getGenuineBasicVariableDescriptorList(); + var listVariableDescriptor = entityDescriptor.getGenuineListVariableDescriptor(); + assertValueRangeForBasicVariables(this, basicVariableDescriptorList, entity, lookUpEnabled); + assertValueRangeForListVariable(this, listVariableDescriptor, entity, lookUpEnabled); } private static void assertValueRangeForBasicVariables(InnerScoreDirector scoreDirector, - List> basicVariableDescriptorList, Object entity) { + List> basicVariableDescriptorList, Object entity, boolean lookUpEnabled) { if (basicVariableDescriptorList == null || basicVariableDescriptorList.isEmpty()) { return; } for (var variableDescriptor : basicVariableDescriptorList) { var value = variableDescriptor.getValue(entity); + if (value == null) { + return; + } var valueRange = scoreDirector.getValueRangeManager() .getFromEntity(variableDescriptor.getValueRangeDescriptor(), entity); // We use the lookup search instead of rebasing in order to return the expected error message - if (value != null && !valueRange - .contains(Objects.requireNonNullElse(scoreDirector.lookUpWorkingObjectOrReturnNull(value), value))) { + var rebasedValue = lookUpEnabled ? scoreDirector.lookUpWorkingObjectOrReturnNull(value) : value; + if (rebasedValue == null) { + rebasedValue = value; + } + if (!valueRange.contains(rebasedValue)) { throw new IllegalStateException( "The value (%s) from the planning variable (%s) has been assigned to the entity (%s), but it is outside of the related value range %s." .formatted(value, variableDescriptor.getVariableName(), entity, @@ -856,11 +871,10 @@ private static void assertValueRangeForBasicVariables(InnerScoreDire .toList())); } } - } private static void assertValueRangeForListVariable(InnerScoreDirector scoreDirector, - ListVariableDescriptor variableDescriptor, Object entity) { + ListVariableDescriptor variableDescriptor, Object entity, boolean lookUpEnabled) { if (variableDescriptor == null) { return; } @@ -871,9 +885,15 @@ private static void assertValueRangeForListVariable(InnerScoreDirect var valueRange = scoreDirector.getValueRangeManager() .getFromEntity(variableDescriptor.getValueRangeDescriptor(), entity); for (var value : valueList) { + if (value == null) { + continue; + } // We use the lookup search instead of rebasing in order to return the expected error message - if (value != null && !valueRange - .contains(Objects.requireNonNullElse(scoreDirector.lookUpWorkingObjectOrReturnNull(value), value))) { + var rebasedValue = lookUpEnabled ? scoreDirector.lookUpWorkingObjectOrReturnNull(value) : value; + if (rebasedValue == null) { + rebasedValue = value; + } + if (!valueRange.contains(rebasedValue)) { throw new IllegalStateException( "The value (%s) from the planning variable (%s) has been assigned to the entity (%s), but it is outside of the related value range %s." .formatted(value, variableDescriptor.getVariableName(), entity, @@ -1075,6 +1095,7 @@ public abstract static class AbstractScoreDirectorBuilder build(); /** diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java index 0cd489a751..ea90596bac 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/InnerScoreDirector.java @@ -370,13 +370,6 @@ default Solution_ cloneWorkingSolution() { void assertExpectedUndoMoveScore(Move move, InnerScore beforeMoveScore, SolverLifecyclePoint executionPoint); - /** - * Asserts if any assigned planning values are included in the solution range or any entity value range. - * - * @param workingSolution the solution to be evaluated - */ - void assertValueRangeForSolution(Solution_ workingSolution); - /** * Needs to be called after use because some implementations need to clean up their resources. */ diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java index 97badc7880..bdaa4d2213 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java @@ -119,6 +119,7 @@ public > ScoreDirectorFactory ge .withLookUpEnabled(true) .withConstraintMatchPolicy( constraintMatchEnabled ? ConstraintMatchPolicy.ENABLED : ConstraintMatchPolicy.DISABLED) + .withEnvironmentMode(environmentMode) .build(); solverScope.setScoreDirector(castScoreDirector); solverScope.setProblemChangeDirector(new DefaultProblemChangeDirector<>(castScoreDirector)); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java index 0b831f9b08..fe8be8f24a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/scope/SolverScope.java @@ -343,10 +343,6 @@ public void setInitialSolution(Solution_ initialSolution) { // Set the best solution to the solution with shadow variable updated. setBestSolution(scoreDirector.cloneSolution(scoreDirector.getWorkingSolution())); - - // One-time check of value ranges - // to ensure assigned planning values are included in the solution range or any entity value range - scoreDirector.assertValueRangeForSolution(initialSolution); } public SolverScope createChildThreadSolverScope(ChildThreadType childThreadType) { diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java index 6d758fa87f..fd0376b2e5 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java @@ -2106,7 +2106,7 @@ void failLocalSearchValueRangeAssertion() { void failCustomPhaseValueRangeAssertion() { var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataListSolution.class, TestdataListEntity.class, TestdataListValue.class); - solverConfig.setEnvironmentMode(EnvironmentMode.FULL_ASSERT); + solverConfig.setEnvironmentMode(EnvironmentMode.STEP_ASSERT); var customPhaseConfig = new CustomPhaseConfig() .withCustomPhaseCommands(new InvalidCustomPhaseCommand()); solverConfig.setPhaseConfigList(List.of(new ConstructionHeuristicPhaseConfig(), customPhaseConfig)); @@ -2366,11 +2366,11 @@ public static final class TestingMixedEasyScoreCalculator } - public static final class InvalidCustomPhaseCommand implements PhaseCommand { @Override - public void changeWorkingSolution(ScoreDirector scoreDirector, BooleanSupplier isPhaseTerminated) { + public void changeWorkingSolution(ScoreDirector scoreDirector, + BooleanSupplier isPhaseTerminated) { var entity = scoreDirector.getWorkingSolution().getEntityList().get(0); scoreDirector.beforeListVariableChanged(entity, "valueList", 0, 0); entity.getValueList().add(new TestdataListValue("bad value")); From 6b9e95f10160c46d1ecd07c771d5145004dc380d Mon Sep 17 00:00:00 2001 From: fred Date: Tue, 3 Feb 2026 15:13:33 -0300 Subject: [PATCH 04/13] chore: improve assertion logic second part --- .../impl/domain/lookup/LookUpManager.java | 5 ++++ .../impl/domain/lookup/LookUpStrategy.java | 3 +++ .../domain/lookup/NoneLookUpStrategy.java | 4 +++ .../score/director/AbstractScoreDirector.java | 27 ++++++++++--------- .../TestdataCorrectlyClonedSolution.java | 6 +++-- 5 files changed, 30 insertions(+), 15 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/lookup/LookUpManager.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/lookup/LookUpManager.java index 0997f6b907..8f95e3ea58 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/lookup/LookUpManager.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/lookup/LookUpManager.java @@ -70,4 +70,9 @@ public E lookUpWorkingObjectOrReturnNull(E externalObject) { return lookUpStrategy.lookUpWorkingObjectIfExists(idToWorkingObjectMap, externalObject); } + public boolean isLookUpEnabled(E externalObject) { + LookUpStrategy lookUpStrategy = lookUpStrategyResolver.determineLookUpStrategy(externalObject); + return lookUpStrategy.isLookUpEnabled(); + } + } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/lookup/LookUpStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/lookup/LookUpStrategy.java index 3915fce051..1a79ae07f5 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/lookup/LookUpStrategy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/lookup/LookUpStrategy.java @@ -13,4 +13,7 @@ public sealed interface LookUpStrategy E lookUpWorkingObjectIfExists(Map idToWorkingObjectMap, E externalObject); + default boolean isLookUpEnabled() { + return true; + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/lookup/NoneLookUpStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/lookup/NoneLookUpStrategy.java index a3b0b92470..8d66cfa521 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/lookup/NoneLookUpStrategy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/lookup/NoneLookUpStrategy.java @@ -38,4 +38,8 @@ public E lookUpWorkingObjectIfExists(Map idToWorkingObjectMa + LookUpStrategyType.class.getSimpleName() + " (not recommended)."); } + @Override + public boolean isLookUpEnabled() { + return false; + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java index 68b0973393..b98e957160 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java @@ -841,24 +841,25 @@ private void assertValueRangeForEntity(Object entity) { } var basicVariableDescriptorList = entityDescriptor.getGenuineBasicVariableDescriptorList(); var listVariableDescriptor = entityDescriptor.getGenuineListVariableDescriptor(); - assertValueRangeForBasicVariables(this, basicVariableDescriptorList, entity, lookUpEnabled); - assertValueRangeForListVariable(this, listVariableDescriptor, entity, lookUpEnabled); + assertValueRangeForBasicVariables(this, lookUpManager, basicVariableDescriptorList, entity); + assertValueRangeForListVariable(this, lookUpManager, listVariableDescriptor, entity); } private static void assertValueRangeForBasicVariables(InnerScoreDirector scoreDirector, - List> basicVariableDescriptorList, Object entity, boolean lookUpEnabled) { - if (basicVariableDescriptorList == null || basicVariableDescriptorList.isEmpty()) { + LookUpManager lookUpManager, List> basicVariableDescriptorList, + Object entity) { + if (basicVariableDescriptorList == null || basicVariableDescriptorList.isEmpty() || lookUpManager == null) { return; } for (var variableDescriptor : basicVariableDescriptorList) { var value = variableDescriptor.getValue(entity); - if (value == null) { - return; + if (value == null || !lookUpManager.isLookUpEnabled(value)) { + continue; } var valueRange = scoreDirector.getValueRangeManager() .getFromEntity(variableDescriptor.getValueRangeDescriptor(), entity); - // We use the lookup search instead of rebasing in order to return the expected error message - var rebasedValue = lookUpEnabled ? scoreDirector.lookUpWorkingObjectOrReturnNull(value) : value; + // We use the lookup search instead of rebasing in order to control the expected error message + var rebasedValue = scoreDirector.lookUpWorkingObjectOrReturnNull(value); if (rebasedValue == null) { rebasedValue = value; } @@ -874,22 +875,22 @@ private static void assertValueRangeForBasicVariables(InnerScoreDire } private static void assertValueRangeForListVariable(InnerScoreDirector scoreDirector, - ListVariableDescriptor variableDescriptor, Object entity, boolean lookUpEnabled) { + LookUpManager lookUpManager, ListVariableDescriptor variableDescriptor, Object entity) { if (variableDescriptor == null) { return; } var valueList = variableDescriptor.getValue(entity); - if (valueList.isEmpty()) { + if (valueList.isEmpty() || lookUpManager == null) { return; } var valueRange = scoreDirector.getValueRangeManager() .getFromEntity(variableDescriptor.getValueRangeDescriptor(), entity); for (var value : valueList) { - if (value == null) { + if (value == null || !lookUpManager.isLookUpEnabled(value)) { continue; } - // We use the lookup search instead of rebasing in order to return the expected error message - var rebasedValue = lookUpEnabled ? scoreDirector.lookUpWorkingObjectOrReturnNull(value) : value; + // We use the lookup search instead of rebasing in order to control the expected error message + var rebasedValue = scoreDirector.lookUpWorkingObjectOrReturnNull(value); if (rebasedValue == null) { rebasedValue = value; } diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/clone/customcloner/TestdataCorrectlyClonedSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/clone/customcloner/TestdataCorrectlyClonedSolution.java index 548c5d7c59..ca84660a6d 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/clone/customcloner/TestdataCorrectlyClonedSolution.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/clone/customcloner/TestdataCorrectlyClonedSolution.java @@ -1,6 +1,6 @@ package ai.timefold.solver.core.testdomain.clone.customcloner; -import java.util.Arrays; +import java.util.ArrayList; import java.util.List; import ai.timefold.solver.core.api.domain.solution.PlanningEntityProperty; @@ -24,11 +24,13 @@ public class TestdataCorrectlyClonedSolution implements SolutionCloner staticList = List.of(new TestdataValue("1"), new TestdataValue("2")); + @ValueRangeProvider(id = "valueRange") @ProblemFactCollectionProperty public List valueRange() { // two values needed to allow for at least one doable move, otherwise the second step ends in an infinite loop - return Arrays.asList(new TestdataValue("1"), new TestdataValue("2")); + return new ArrayList<>(staticList); } @Override From 05fa4c944b891f91fc86cfec804a5caba0c56381 Mon Sep 17 00:00:00 2001 From: fred Date: Tue, 3 Feb 2026 15:17:16 -0300 Subject: [PATCH 05/13] chore: address comments --- .../ai/timefold/solver/core/impl/solver/DefaultSolverTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java index fd0376b2e5..b75999786c 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java @@ -2398,7 +2398,7 @@ public Iterator createRandomMoveIterator( } } - public static class InvalidMove extends AbstractMove { + public static final class InvalidMove extends AbstractMove { @Override protected void doMoveOnGenuineVariables(ScoreDirector scoreDirector) { From 19adc8eaffd1b15d3333150dc58b0a3845728f9c Mon Sep 17 00:00:00 2001 From: fred Date: Tue, 3 Feb 2026 15:22:34 -0300 Subject: [PATCH 06/13] chore: address comments --- .../solver/core/impl/score/director/AbstractScoreDirector.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java index b98e957160..004e284811 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java @@ -836,7 +836,7 @@ that could cause the scoreDifference (%s).""".formatted(scoreDifference, beforeM private void assertValueRangeForEntity(Object entity) { var entityDescriptor = getSolutionDescriptor().findEntityDescriptor(entity.getClass()); if (entityDescriptor == null) { - // It may be called for a shadow entity + // It may be called for a shadow entity return; } var basicVariableDescriptorList = entityDescriptor.getGenuineBasicVariableDescriptorList(); From 0f3662f6db9b8de910557d228573143a2740b3fa Mon Sep 17 00:00:00 2001 From: fred Date: Tue, 3 Feb 2026 17:15:58 -0300 Subject: [PATCH 07/13] chore: address comments --- .../impl/domain/lookup/LookUpManager.java | 5 -- .../impl/domain/lookup/LookUpStrategy.java | 3 - .../domain/lookup/NoneLookUpStrategy.java | 4 -- .../score/director/AbstractScoreDirector.java | 57 ++++++++----------- 4 files changed, 24 insertions(+), 45 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/lookup/LookUpManager.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/lookup/LookUpManager.java index 8f95e3ea58..0997f6b907 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/lookup/LookUpManager.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/lookup/LookUpManager.java @@ -70,9 +70,4 @@ public E lookUpWorkingObjectOrReturnNull(E externalObject) { return lookUpStrategy.lookUpWorkingObjectIfExists(idToWorkingObjectMap, externalObject); } - public boolean isLookUpEnabled(E externalObject) { - LookUpStrategy lookUpStrategy = lookUpStrategyResolver.determineLookUpStrategy(externalObject); - return lookUpStrategy.isLookUpEnabled(); - } - } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/lookup/LookUpStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/lookup/LookUpStrategy.java index 1a79ae07f5..3915fce051 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/lookup/LookUpStrategy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/lookup/LookUpStrategy.java @@ -13,7 +13,4 @@ public sealed interface LookUpStrategy E lookUpWorkingObjectIfExists(Map idToWorkingObjectMap, E externalObject); - default boolean isLookUpEnabled() { - return true; - } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/lookup/NoneLookUpStrategy.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/lookup/NoneLookUpStrategy.java index 8d66cfa521..a3b0b92470 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/lookup/NoneLookUpStrategy.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/lookup/NoneLookUpStrategy.java @@ -38,8 +38,4 @@ public E lookUpWorkingObjectIfExists(Map idToWorkingObjectMa + LookUpStrategyType.class.getSimpleName() + " (not recommended)."); } - @Override - public boolean isLookUpEnabled() { - return false; - } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java index 004e284811..f9ca68cada 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java @@ -8,10 +8,7 @@ import java.util.List; import java.util.Objects; import java.util.Set; -import java.util.Spliterator; -import java.util.Spliterators; import java.util.function.Consumer; -import java.util.stream.StreamSupport; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.domain.solution.cloner.SolutionCloner; @@ -29,7 +26,7 @@ import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.domain.variable.InnerVariableListener; import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply; -import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor; +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.domain.variable.descriptor.VariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.listener.support.VariableListenerSupport; @@ -839,68 +836,62 @@ private void assertValueRangeForEntity(Object entity) { // It may be called for a shadow entity return; } - var basicVariableDescriptorList = entityDescriptor.getGenuineBasicVariableDescriptorList(); + var basicVariableDescriptorList = entityDescriptor.getGenuineBasicVariableDescriptorList().stream() + .map(v -> (BasicVariableDescriptor) v).toList(); var listVariableDescriptor = entityDescriptor.getGenuineListVariableDescriptor(); - assertValueRangeForBasicVariables(this, lookUpManager, basicVariableDescriptorList, entity); - assertValueRangeForListVariable(this, lookUpManager, listVariableDescriptor, entity); + assertValueRangeForBasicVariables(this, basicVariableDescriptorList, entity); + assertValueRangeForListVariable(this, listVariableDescriptor, entity); } private static void assertValueRangeForBasicVariables(InnerScoreDirector scoreDirector, - LookUpManager lookUpManager, List> basicVariableDescriptorList, + List> basicVariableDescriptorList, Object entity) { - if (basicVariableDescriptorList == null || basicVariableDescriptorList.isEmpty() || lookUpManager == null) { + if (basicVariableDescriptorList == null || basicVariableDescriptorList.isEmpty()) { return; } for (var variableDescriptor : basicVariableDescriptorList) { var value = variableDescriptor.getValue(entity); - if (value == null || !lookUpManager.isLookUpEnabled(value)) { + if (value == null) { + // Chained is not supported continue; } var valueRange = scoreDirector.getValueRangeManager() .getFromEntity(variableDescriptor.getValueRangeDescriptor(), entity); - // We use the lookup search instead of rebasing in order to control the expected error message - var rebasedValue = scoreDirector.lookUpWorkingObjectOrReturnNull(value); - if (rebasedValue == null) { - rebasedValue = value; - } - if (!valueRange.contains(rebasedValue)) { + if (!valueRange.contains(value)) { + if (variableDescriptor.isChained()) { + // We also check the entity list + var allEntities = + variableDescriptor.getEntityDescriptor().extractEntities(scoreDirector.getWorkingSolution()); + if (allEntities.contains(value)) { + return; + } + } throw new IllegalStateException( "The value (%s) from the planning variable (%s) has been assigned to the entity (%s), but it is outside of the related value range %s." - .formatted(value, variableDescriptor.getVariableName(), entity, - StreamSupport.stream(Spliterators.spliterator(valueRange.createOriginalIterator(), - valueRange.getSize(), Spliterator.SIZED), false) - .toList())); + .formatted(value, variableDescriptor.getVariableName(), entity, valueRange)); } } } private static void assertValueRangeForListVariable(InnerScoreDirector scoreDirector, - LookUpManager lookUpManager, ListVariableDescriptor variableDescriptor, Object entity) { + ListVariableDescriptor variableDescriptor, Object entity) { if (variableDescriptor == null) { return; } var valueList = variableDescriptor.getValue(entity); - if (valueList.isEmpty() || lookUpManager == null) { + if (valueList.isEmpty()) { return; } var valueRange = scoreDirector.getValueRangeManager() .getFromEntity(variableDescriptor.getValueRangeDescriptor(), entity); for (var value : valueList) { - if (value == null || !lookUpManager.isLookUpEnabled(value)) { + if (value == null) { continue; } - // We use the lookup search instead of rebasing in order to control the expected error message - var rebasedValue = scoreDirector.lookUpWorkingObjectOrReturnNull(value); - if (rebasedValue == null) { - rebasedValue = value; - } - if (!valueRange.contains(rebasedValue)) { + if (!valueRange.contains(value)) { throw new IllegalStateException( "The value (%s) from the planning variable (%s) has been assigned to the entity (%s), but it is outside of the related value range %s." - .formatted(value, variableDescriptor.getVariableName(), entity, - StreamSupport.stream(Spliterators.spliterator(valueRange.createOriginalIterator(), - valueRange.getSize(), Spliterator.SIZED), false) - .toList())); + .formatted(value, variableDescriptor.getVariableName(), entity, valueRange)); } } } From 6403ad21fe7272350dd9233453738448e7487f93 Mon Sep 17 00:00:00 2001 From: fred Date: Tue, 3 Feb 2026 18:19:55 -0300 Subject: [PATCH 08/13] chore: fix tests --- .../list/ElementDestinationSelectorTest.java | 4 +-- .../bi/AbstractBiConstraintStreamTest.java | 25 ++++++++++++++- .../AbstractQuadConstraintStreamTest.java | 31 ++++++++++++++++--- .../tri/AbstractTriConstraintStreamTest.java | 23 ++++++++++++++ .../uni/AbstractUniConstraintStreamTest.java | 24 ++++++++++++++ .../core/impl/solver/DefaultSolverTest.java | 14 ++++----- .../TestdataEntityProvidingSolution.java | 6 +++- ...lowsUnassignedEntityProvidingSolution.java | 6 +++- 8 files changed, 117 insertions(+), 16 deletions(-) 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 6f7be8ae3e..33206e0e5d 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 @@ -313,7 +313,7 @@ void randomPartiallyPinnedAndUnassigned() { var v3 = new TestdataPinnedUnassignedValuesListValue("3"); var v4 = new TestdataPinnedUnassignedValuesListValue("4"); var v5 = new TestdataPinnedUnassignedValuesListValue("5"); - var v6 = new TestdataPinnedUnassignedValuesListValue("5"); + var v6 = new TestdataPinnedUnassignedValuesListValue("6"); var unassignedValue = new TestdataPinnedUnassignedValuesListValue("7"); var a = new TestdataPinnedUnassignedValuesListEntity("A", v1, v2); var b = new TestdataPinnedUnassignedValuesListEntity("B"); @@ -325,7 +325,7 @@ void randomPartiallyPinnedAndUnassigned() { var solution = new TestdataPinnedUnassignedValuesListSolution(); solution.setEntityList(List.of(a, b, c, d)); - solution.setValueList(List.of(v1, v2, v3, v3, v4, v5, unassignedValue)); + solution.setValueList(List.of(v1, v2, v3, v3, v4, v5, v6, unassignedValue)); SolutionManager.updateShadowVariables(solution); var random = new TestRandom( diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java index 288dd56881..b543846c7a 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/bi/AbstractBiConstraintStreamTest.java @@ -1295,7 +1295,7 @@ public void groupBy_0Mapping1Collector() { InnerScoreDirector scoreDirector = buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .groupBy(countBi()) - .penalize(SimpleScore.ONE, (count) -> count) + .penalize(SimpleScore.ONE, count -> count) .asConstraint(TEST_CONSTRAINT_NAME)); // From scratch @@ -2076,6 +2076,8 @@ public void concatUniWithoutValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -2120,6 +2122,8 @@ public void concatAndDistinctUniWithoutValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -2165,6 +2169,8 @@ public void concatBiWithoutValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -2211,6 +2217,8 @@ public void concatBiWithValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -2257,6 +2265,8 @@ public void concatAndDistinctBiWithoutValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -2304,6 +2314,8 @@ public void concatAndDistinctBiWithValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -2349,6 +2361,8 @@ public void concatTriWithoutValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -2397,6 +2411,8 @@ public void concatAndDistinctTriWithoutValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -2446,6 +2462,8 @@ public void concatQuadWithoutValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -2496,6 +2514,8 @@ public void concatAndDistinctQuadWithoutValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -2547,6 +2567,8 @@ public void concatAfterGroupBy() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -2602,6 +2624,7 @@ public void complement() { var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); var value1 = solution.getFirstValue(); var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + solution.getValueList().add(value2); var entity1 = solution.getFirstEntity(); var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamTest.java index 9525a0635f..1ecb3d8d0b 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/quad/AbstractQuadConstraintStreamTest.java @@ -1278,7 +1278,7 @@ public void flattenLastWithDuplicates() { buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, filtering((a, b, c) -> a != c && b != c)) .join(TestdataLavishEntity.class, filtering((a, b, c, d) -> a != d && b != d)) - .flattenLast((d) -> asList(group1, group1, group2)) + .flattenLast(d -> asList(group1, group1, group2)) .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); @@ -1316,7 +1316,7 @@ public void flattenLastWithoutDuplicates() { buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, filtering((a, b, c) -> a != c && b != c)) .join(TestdataLavishEntity.class, filtering((a, b, c, d) -> a != d && b != d)) - .flattenLast((d) -> asList(group1, group2)) + .flattenLast(d -> asList(group1, group2)) .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); @@ -1351,7 +1351,7 @@ public void flattenLastAndDistinctWithDuplicates() { buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, filtering((a, b, c) -> a != c && b != c)) .join(TestdataLavishEntity.class, filtering((a, b, c, d) -> a != d && b != d)) - .flattenLast((d) -> asList(group1, group1, group2)) + .flattenLast(d -> asList(group1, group1, group2)) .distinct() .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); @@ -1387,7 +1387,7 @@ public void flattenLastAndDistinctWithoutDuplicates() { buildScoreDirector(factory -> factory.forEachUniquePair(TestdataLavishEntity.class) .join(TestdataLavishEntity.class, filtering((a, b, c) -> a != c && b != c)) .join(TestdataLavishEntity.class, filtering((a, b, c, d) -> a != d && b != d)) - .flattenLast((d) -> asList(group1, group2)) + .flattenLast(d -> asList(group1, group2)) .distinct() .penalize(SimpleScore.ONE) .asConstraint(TEST_CONSTRAINT_NAME)); @@ -1416,6 +1416,8 @@ public void concatUniWithoutValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -1464,6 +1466,8 @@ public void concatAndDistinctUniWithoutValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -1513,6 +1517,8 @@ public void concatBiWithoutValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -1563,6 +1569,8 @@ public void concatAndDistinctBiWithoutValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -1614,6 +1622,8 @@ public void concatTriWithoutValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -1666,6 +1676,8 @@ public void concatAndDistinctTriWithoutValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -1719,6 +1731,8 @@ public void concatQuadWithoutValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -1773,6 +1787,8 @@ public void concatQuadWithValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -1827,6 +1843,8 @@ public void concatAndDistinctQuadWithoutValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -1882,6 +1900,8 @@ public void concatAndDistinctQuadWithValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -1935,6 +1955,8 @@ public void concatAfterGroupBy() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -1997,6 +2019,7 @@ public void complement() { var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); var value1 = solution.getFirstValue(); var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + solution.getValueList().add(value2); var entity1 = solution.getFirstEntity(); var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java index 50d6c3070e..eece9f2814 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/tri/AbstractTriConstraintStreamTest.java @@ -1771,6 +1771,8 @@ public void concatUniWithoutValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -1817,6 +1819,8 @@ public void concatAndDistinctUniWithoutValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -1864,6 +1868,8 @@ public void concatBiWithoutValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -1912,6 +1918,8 @@ public void concatAndDistinctBiWithoutValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -1961,6 +1969,8 @@ public void concatTriWithoutValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -2011,6 +2021,8 @@ public void concatTriWithValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -2061,6 +2073,8 @@ public void concatAndDistinctTriWithoutValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -2112,6 +2126,8 @@ public void concatAndDistinctTriWithValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -2161,6 +2177,8 @@ public void concatQuadWithoutValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -2213,6 +2231,8 @@ public void concatAndDistinctQuadWithoutValueDuplicates() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -2266,6 +2286,8 @@ public void concatAfterGroupBy() { TestdataLavishValue value1 = solution.getFirstValue(); TestdataLavishValue value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); TestdataLavishValue value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); TestdataLavishEntity entity1 = solution.getFirstEntity(); TestdataLavishEntity entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -2325,6 +2347,7 @@ public void complement() { var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); var value1 = solution.getFirstValue(); var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + solution.getValueList().add(value2); var entity1 = solution.getFirstEntity(); var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java index 55ee9f2dfb..b4beaef8d5 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/stream/common/uni/AbstractUniConstraintStreamTest.java @@ -240,6 +240,7 @@ public void join_1Equal() { var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); var value1 = solution.getFirstValue(); var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + solution.getValueList().add(value2); var entity1 = solution.getFirstEntity(); var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -2592,6 +2593,8 @@ public void concatUniWithoutValueDuplicates() { var value1 = solution.getFirstValue(); var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); var entity1 = solution.getFirstEntity(); var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -2634,6 +2637,8 @@ public void concatUniWithValueDuplicates() { var value1 = solution.getFirstValue(); var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); var entity1 = solution.getFirstEntity(); var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -2676,6 +2681,8 @@ public void concatAndDistinctUniWithoutValueDuplicates() { var value1 = solution.getFirstValue(); var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); var entity1 = solution.getFirstEntity(); var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -2719,6 +2726,8 @@ public void concatAndDistinctUniWithValueDuplicates() { var value1 = solution.getFirstValue(); var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); var entity1 = solution.getFirstEntity(); var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -2760,6 +2769,8 @@ public void concatBiWithoutValueDuplicates() { var value1 = solution.getFirstValue(); var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); var entity1 = solution.getFirstEntity(); var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -2804,6 +2815,8 @@ public void concatAndDistinctBiWithoutValueDuplicates() { var value1 = solution.getFirstValue(); var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); var entity1 = solution.getFirstEntity(); var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -2849,6 +2862,8 @@ public void concatTriWithoutValueDuplicates() { var value1 = solution.getFirstValue(); var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); var entity1 = solution.getFirstEntity(); var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -2895,6 +2910,8 @@ public void concatAndDistinctTriWithoutValueDuplicates() { var value1 = solution.getFirstValue(); var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); var entity1 = solution.getFirstEntity(); var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -2942,6 +2959,8 @@ public void concatQuadWithoutValueDuplicates() { var value1 = solution.getFirstValue(); var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); var entity1 = solution.getFirstEntity(); var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -2990,6 +3009,8 @@ public void concatAndDistinctQuadWithoutValueDuplicates() { var value1 = solution.getFirstValue(); var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); var entity1 = solution.getFirstEntity(); var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -3039,6 +3060,8 @@ public void concatAfterGroupBy() { var value1 = solution.getFirstValue(); var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); var value3 = new TestdataLavishValue("MyValue 3", solution.getFirstValueGroup()); + solution.getValueList().add(value2); + solution.getValueList().add(value3); var entity1 = solution.getFirstEntity(); var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); @@ -3089,6 +3112,7 @@ public void complement() { var solution = TestdataLavishSolution.generateSolution(2, 5, 1, 1); var value1 = solution.getFirstValue(); var value2 = new TestdataLavishValue("MyValue 2", solution.getFirstValueGroup()); + solution.getValueList().add(value2); var entity1 = solution.getFirstEntity(); var entity2 = new TestdataLavishEntity("MyEntity 2", solution.getFirstEntityGroup(), value2); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java index b75999786c..311fb9e7d0 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java @@ -1990,7 +1990,7 @@ void failBasicVariableInvalidValueRange() { assertThatCode(() -> PlannerTestUtils.solve(solverConfig, problem)) .hasMessageContaining( - "The value (3) from the planning variable (value) has been assigned to the entity (e1), but it is outside of the related value range [1, 2]"); + "The value (3) from the planning variable (value) has been assigned to the entity (e1), but it is outside of the related value range [1-2]"); } @Test @@ -2016,7 +2016,7 @@ void failMultipleBasicVariableInvalidValueRange() { assertThatCode(() -> PlannerTestUtils.solve(solverConfig, problem)) .hasMessageContaining( - "The value (3) from the planning variable (secondValue) has been assigned to the entity (e1), but it is outside of the related value range [null, 1, 2]"); + "The value (3) from the planning variable (secondValue) has been assigned to the entity (e1), but it is outside of the related value range [null]∪[1-2]"); } @Test @@ -2038,7 +2038,7 @@ void failListVariableInvalidValueRange() { assertThatCode(() -> PlannerTestUtils.solve(solverConfig, problem)) .hasMessageContaining( - "The value (3) from the planning variable (valueList) has been assigned to the entity (e1), but it is outside of the related value range [1, 2]"); + "The value (3) from the planning variable (valueList) has been assigned to the entity (e1), but it is outside of the related value range [1-2]"); } @Test @@ -2070,7 +2070,7 @@ void failMixedModelInvalidValueRange() { assertThatCode(() -> PlannerTestUtils.solve(solverConfig, problem)) .hasMessageContaining( - "The value (1) from the planning variable (basicValue) has been assigned to the entity (e1), but it is outside of the related value range [2, 3]"); + "The value (1) from the planning variable (basicValue) has been assigned to the entity (e1), but it is outside of the related value range [2-3]"); e1b.setBasicValue(null); // 2 - Invalid list variable @@ -2083,7 +2083,7 @@ void failMixedModelInvalidValueRange() { assertThatCode(() -> PlannerTestUtils.solve(solverConfig, problem)) .hasMessageContaining( - "The value (1) from the planning variable (valueList) has been assigned to the entity (e1), but it is outside of the related value range [2, 3]"); + "The value (1) from the planning variable (valueList) has been assigned to the entity (e1), but it is outside of the related value range [2-3]"); } @Test @@ -2099,7 +2099,7 @@ void failLocalSearchValueRangeAssertion() { var problem = TestdataListSolution.generateUninitializedSolution(2, 2); assertThatCode(() -> PlannerTestUtils.solve(solverConfig, problem)) .hasMessageContaining( - "The value (bad value) from the planning variable (valueList) has been assigned to the entity (Generated Entity 0), but it is outside of the related value range [Generated Value 0, Generated Value 1]"); + "The value (bad value) from the planning variable (valueList) has been assigned to the entity (Generated Entity 0), but it is outside of the related value range [Generated Value 0-Generated Value 1]"); } @Test @@ -2114,7 +2114,7 @@ void failCustomPhaseValueRangeAssertion() { var problem = TestdataListSolution.generateUninitializedSolution(2, 2); assertThatCode(() -> PlannerTestUtils.solve(solverConfig, problem)) .hasMessageContaining( - "The value (bad value) from the planning variable (valueList) has been assigned to the entity (Generated Entity 0), but it is outside of the related value range [Generated Value 0, Generated Value 1]"); + "The value (bad value) from the planning variable (valueList) has been assigned to the entity (Generated Entity 0), but it is outside of the related value range [Generated Value 0-Generated Value 1]"); } public static final class MinimizeUnusedEntitiesEasyScoreCalculator diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/entityproviding/TestdataEntityProvidingSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/entityproviding/TestdataEntityProvidingSolution.java index f4dda05a60..93f2234e9e 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/entityproviding/TestdataEntityProvidingSolution.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/entityproviding/TestdataEntityProvidingSolution.java @@ -71,7 +71,11 @@ private static TestdataEntityProvidingSolution generateSolution(int valueListSiz } } var entity = new TestdataEntityProvidingEntity("Generated Entity " + i, valueRange); - entity.setValue(initialized ? valueList.get(i % valueListSize) : null); + var value = initialized ? valueList.get(i % valueListSize) : null; + entity.setValue(value); + if (value != null && !entity.getValueRange().contains(value)) { + entity.getValueRange().add(value); + } entityList.add(entity); } solution.setEntityList(entityList); diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/entityproviding/unassignedvar/TestdataAllowsUnassignedEntityProvidingSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/entityproviding/unassignedvar/TestdataAllowsUnassignedEntityProvidingSolution.java index c009c306c7..190b9cbbf8 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/entityproviding/unassignedvar/TestdataAllowsUnassignedEntityProvidingSolution.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/entityproviding/unassignedvar/TestdataAllowsUnassignedEntityProvidingSolution.java @@ -72,7 +72,11 @@ private static TestdataAllowsUnassignedEntityProvidingSolution generateSolution( } } var entity = new TestdataAllowsUnassignedEntityProvidingEntity("Generated Entity " + i, valueRange); - entity.setValue(initialized ? valueList.get(i % valueListSize) : null); + var value = initialized ? valueList.get(i % valueListSize) : null; + entity.setValue(value); + if (value != null && !entity.getValueRange().contains(value)) { + entity.getValueRange().add(value); + } entityList.add(entity); } solution.setEntityList(entityList); From db9b5a98a7aace372d0107b09e8d6f856bf3b1b6 Mon Sep 17 00:00:00 2001 From: fred Date: Tue, 3 Feb 2026 18:56:37 -0300 Subject: [PATCH 09/13] chore: address comments --- .../score/director/AbstractScoreDirector.java | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java index f9ca68cada..3def8724b7 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java @@ -513,7 +513,7 @@ public void afterVariableChanged(VariableDescriptor variableDescripto variableListenerSupport.afterVariableChanged(variableDescriptor, entity); neighborhoodsElementUpdateNotifier.accept(entity); if (isStepAssertOrMore) { - assertValueRangeForEntity(entity); + assertValueRangeForBasicVariables(entity); } } @@ -566,7 +566,8 @@ public void afterListVariableChanged(ListVariableDescriptor variableD variableListenerSupport.afterListVariableChanged(variableDescriptor, entity, fromIndex, toIndex); neighborhoodsElementUpdateNotifier.accept(entity); if (isStepAssertOrMore) { - assertValueRangeForEntity(entity); + var valueList = variableDescriptor.getValue(entity).subList(fromIndex, toIndex); + assertValueRangeForListVariable(entity, valueList); } } @@ -831,6 +832,18 @@ that could cause the scoreDifference (%s).""".formatted(scoreDifference, beforeM } private void assertValueRangeForEntity(Object entity) { + assertValueRangeForBasicVariables(entity); + var listVariableDescriptor = getSolutionDescriptor().getListVariableDescriptor(); + if (listVariableDescriptor != null) { + if (!listVariableDescriptor.getEntityDescriptor().matchesEntity(entity)) { + return; + } + var valueList = listVariableDescriptor.getValue(entity); + assertValueRangeForListVariable(entity, valueList); + } + } + + private void assertValueRangeForBasicVariables(Object entity) { var entityDescriptor = getSolutionDescriptor().findEntityDescriptor(entity.getClass()); if (entityDescriptor == null) { // It may be called for a shadow entity @@ -838,14 +851,21 @@ private void assertValueRangeForEntity(Object entity) { } var basicVariableDescriptorList = entityDescriptor.getGenuineBasicVariableDescriptorList().stream() .map(v -> (BasicVariableDescriptor) v).toList(); - var listVariableDescriptor = entityDescriptor.getGenuineListVariableDescriptor(); assertValueRangeForBasicVariables(this, basicVariableDescriptorList, entity); - assertValueRangeForListVariable(this, listVariableDescriptor, entity); + } + + private void assertValueRangeForListVariable(Object entity, List valueList) { + var entityDescriptor = getSolutionDescriptor().findEntityDescriptor(entity.getClass()); + if (entityDescriptor == null) { + // It may be called for a shadow entity + return; + } + var listVariableDescriptor = entityDescriptor.getGenuineListVariableDescriptor(); + assertValueRangeForListVariable(this, listVariableDescriptor, entity, valueList); } private static void assertValueRangeForBasicVariables(InnerScoreDirector scoreDirector, - List> basicVariableDescriptorList, - Object entity) { + List> basicVariableDescriptorList, Object entity) { if (basicVariableDescriptorList == null || basicVariableDescriptorList.isEmpty()) { return; } @@ -874,11 +894,7 @@ private static void assertValueRangeForBasicVariables(InnerScoreDire } private static void assertValueRangeForListVariable(InnerScoreDirector scoreDirector, - ListVariableDescriptor variableDescriptor, Object entity) { - if (variableDescriptor == null) { - return; - } - var valueList = variableDescriptor.getValue(entity); + ListVariableDescriptor variableDescriptor, Object entity, List valueList) { if (valueList.isEmpty()) { return; } From 859bf12206cf2e5574182d3290b6b374171d388e Mon Sep 17 00:00:00 2001 From: fred Date: Tue, 3 Feb 2026 20:31:05 -0300 Subject: [PATCH 10/13] chore: address comments --- .../solver/core/impl/score/director/AbstractScoreDirector.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java index 3def8724b7..5d054fa559 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java @@ -872,7 +872,6 @@ private static void assertValueRangeForBasicVariables(InnerScoreDire for (var variableDescriptor : basicVariableDescriptorList) { var value = variableDescriptor.getValue(entity); if (value == null) { - // Chained is not supported continue; } var valueRange = scoreDirector.getValueRangeManager() From f2a6b92656e9afe288cd3717015dd8dac52afd3c Mon Sep 17 00:00:00 2001 From: fred Date: Wed, 4 Feb 2026 11:03:19 -0300 Subject: [PATCH 11/13] chore: address comments --- .../domain/variable/ShadowVariableUpdateHelper.java | 8 +++++--- .../solver/core/impl/move/DefaultMoveRunner.java | 6 ++++-- .../impl/move/MoveRunnerScoreDirectorFactory.java | 5 +++-- .../impl/score/director/AbstractScoreDirector.java | 10 ++-------- .../score/director/AbstractScoreDirectorFactory.java | 4 +++- .../score/director/ScoreDirectorFactoryFactory.java | 4 ++-- .../score/director/easy/EasyScoreDirectorFactory.java | 10 ++++++---- .../incremental/IncrementalScoreDirectorFactory.java | 11 +++++++---- .../BavetConstraintStreamScoreDirectorFactory.java | 2 +- .../AbstractConstraintStreamScoreDirectorFactory.java | 6 ++++-- .../solver/core/impl/solver/DefaultSolverFactory.java | 1 - .../selector/move/generic/ChangeMoveTest.java | 3 ++- .../selector/move/generic/PillarChangeMoveTest.java | 3 ++- .../selector/move/generic/PillarSwapMoveTest.java | 3 ++- .../heuristic/selector/move/generic/SwapMoveTest.java | 3 ++- .../solver/core/impl/move/MoveDirectorTest.java | 6 ++++-- .../director/easy/EasyScoreDirectorFactoryTest.java | 3 ++- .../score/director/easy/EasyScoreDirectorTest.java | 3 ++- .../stream/enumerating/UniEnumeratingStreamTest.java | 5 +++-- .../solver/core/testutil/PlannerTestUtils.java | 4 +++- 20 files changed, 59 insertions(+), 41 deletions(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ShadowVariableUpdateHelper.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ShadowVariableUpdateHelper.java index cf8ee184c2..661b407bdd 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ShadowVariableUpdateHelper.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/ShadowVariableUpdateHelper.java @@ -25,6 +25,7 @@ import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.constraint.ConstraintMatchTotal; import ai.timefold.solver.core.api.score.constraint.Indictment; +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.domain.entity.descriptor.EntityDescriptor; import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultShadowVariableMetaModel; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; @@ -338,8 +339,8 @@ private List> fetchBasicDescriptors(EntityDes private static class InternalScoreDirectorFactory> extends AbstractScoreDirectorFactory> { - public InternalScoreDirectorFactory(SolutionDescriptor solutionDescriptor) { - super(solutionDescriptor); + public InternalScoreDirectorFactory(SolutionDescriptor solutionDescriptor, EnvironmentMode environmentMode) { + super(solutionDescriptor, environmentMode); } @Override @@ -387,7 +388,8 @@ public static final class Builder> AbstractScoreDirectorBuilder, InternalScoreDirector.Builder> { public Builder(SolutionDescriptor solutionDescriptor) { - super(new InternalScoreDirectorFactory<>(solutionDescriptor)); + // We use PHASE_ASSERT by default + super(new InternalScoreDirectorFactory<>(solutionDescriptor, EnvironmentMode.PHASE_ASSERT)); withConstraintMatchPolicy(DISABLED); withLookUpEnabled(false); withExpectShadowVariablesInCorrectState(false); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/DefaultMoveRunner.java b/core/src/main/java/ai/timefold/solver/core/impl/move/DefaultMoveRunner.java index fb7928d24f..15ab0b24b3 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/DefaultMoveRunner.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/DefaultMoveRunner.java @@ -2,6 +2,7 @@ import java.util.Objects; +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultPlanningSolutionMetaModel; import ai.timefold.solver.core.impl.score.director.AbstractScoreDirectorFactory; import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningSolutionMetaModel; @@ -16,9 +17,10 @@ public final class DefaultMoveRunner implements MoveRunner private final AbstractScoreDirectorFactory scoreDirectorFactory; public DefaultMoveRunner(PlanningSolutionMetaModel solutionMetaModel) { + // We use PHASE_ASSERT by default this(new MoveRunnerScoreDirectorFactory<>( - ((DefaultPlanningSolutionMetaModel) Objects.requireNonNull(solutionMetaModel)) - .solutionDescriptor())); + ((DefaultPlanningSolutionMetaModel) Objects.requireNonNull(solutionMetaModel)).solutionDescriptor(), + EnvironmentMode.PHASE_ASSERT)); } private DefaultMoveRunner(AbstractScoreDirectorFactory scoreDirectorFactory) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/move/MoveRunnerScoreDirectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/move/MoveRunnerScoreDirectorFactory.java index 222e38cd9a..3b5c72c3b3 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/move/MoveRunnerScoreDirectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/move/MoveRunnerScoreDirectorFactory.java @@ -1,6 +1,7 @@ package ai.timefold.solver.core.impl.move; import ai.timefold.solver.core.api.score.Score; +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.score.director.AbstractScoreDirector; import ai.timefold.solver.core.impl.score.director.AbstractScoreDirectorFactory; @@ -11,8 +12,8 @@ final class MoveRunnerScoreDirectorFactory> extends AbstractScoreDirectorFactory> { - public MoveRunnerScoreDirectorFactory(SolutionDescriptor solutionDescriptor) { - super(solutionDescriptor); + public MoveRunnerScoreDirectorFactory(SolutionDescriptor solutionDescriptor, EnvironmentMode environmentMode) { + super(solutionDescriptor, environmentMode); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java index 5d054fa559..210d343c02 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java @@ -128,7 +128,8 @@ protected AbstractScoreDirector(AbstractScoreDirectorBuilder build(); /** diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirectorFactory.java index f2b9bc7d15..e5a9a4e66b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirectorFactory.java @@ -27,6 +27,7 @@ public abstract class AbstractScoreDirectorFactory solutionDescriptor; + protected final EnvironmentMode environmentMode; protected final ListVariableDescriptor listVariableDescriptor; protected InitializingScoreTrend initializingScoreTrend; @@ -36,8 +37,9 @@ public abstract class AbstractScoreDirectorFactory solutionDescriptor) { + public AbstractScoreDirectorFactory(SolutionDescriptor solutionDescriptor, EnvironmentMode environmentMode) { this.solutionDescriptor = solutionDescriptor; + this.environmentMode = environmentMode; this.listVariableDescriptor = solutionDescriptor.getListVariableDescriptor(); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactory.java index 657c7cabbe..26a1783617 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/ScoreDirectorFactoryFactory.java @@ -68,9 +68,9 @@ DRL constraints requested via scoreDrlList (%s), but this is no longer supported // At this point, we are guaranteed to have at most one score director factory selected. if (config.getEasyScoreCalculatorClass() != null) { - return EasyScoreDirectorFactory.buildScoreDirectorFactory(solutionDescriptor, config); + return EasyScoreDirectorFactory.buildScoreDirectorFactory(solutionDescriptor, config, environmentMode); } else if (config.getIncrementalScoreCalculatorClass() != null) { - return IncrementalScoreDirectorFactory.buildScoreDirectorFactory(solutionDescriptor, config); + return IncrementalScoreDirectorFactory.buildScoreDirectorFactory(solutionDescriptor, config, environmentMode); } else if (config.getConstraintProviderClass() != null) { return BavetConstraintStreamScoreDirectorFactory.buildScoreDirectorFactory(solutionDescriptor, config, environmentMode); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorFactory.java index ffd0d03fab..e95953a721 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorFactory.java @@ -4,6 +4,7 @@ import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; import ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig; +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.config.util.ConfigUtils; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.score.director.AbstractScoreDirector; @@ -22,7 +23,8 @@ public final class EasyScoreDirectorFactory> { public static > EasyScoreDirectorFactory - buildScoreDirectorFactory(SolutionDescriptor solutionDescriptor, ScoreDirectorFactoryConfig config) { + buildScoreDirectorFactory(SolutionDescriptor solutionDescriptor, ScoreDirectorFactoryConfig config, + EnvironmentMode environmentMode) { var easyScoreCalculatorClass = config.getEasyScoreCalculatorClass(); if (easyScoreCalculatorClass == null || !EasyScoreCalculator.class.isAssignableFrom(easyScoreCalculatorClass)) { throw new IllegalArgumentException( @@ -33,14 +35,14 @@ public final class EasyScoreDirectorFactory(solutionDescriptor, easyScoreCalculator); + return new EasyScoreDirectorFactory<>(solutionDescriptor, easyScoreCalculator, environmentMode); } private final EasyScoreCalculator easyScoreCalculator; public EasyScoreDirectorFactory(SolutionDescriptor solutionDescriptor, - EasyScoreCalculator easyScoreCalculator) { - super(solutionDescriptor); + EasyScoreCalculator easyScoreCalculator, EnvironmentMode environmentMode) { + super(solutionDescriptor, environmentMode); this.easyScoreCalculator = easyScoreCalculator; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorFactory.java index 16b946376d..6793844bbd 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/incremental/IncrementalScoreDirectorFactory.java @@ -7,6 +7,7 @@ import ai.timefold.solver.core.api.score.calculator.ConstraintMatchAwareIncrementalScoreCalculator; import ai.timefold.solver.core.api.score.calculator.IncrementalScoreCalculator; import ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig; +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.config.util.ConfigUtils; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.score.director.AbstractScoreDirectorFactory; @@ -24,7 +25,8 @@ public final class IncrementalScoreDirectorFactory> { public static > IncrementalScoreDirectorFactory - buildScoreDirectorFactory(SolutionDescriptor solutionDescriptor, ScoreDirectorFactoryConfig config) { + buildScoreDirectorFactory(SolutionDescriptor solutionDescriptor, ScoreDirectorFactoryConfig config, + EnvironmentMode environmentMode) { if (!IncrementalScoreCalculator.class.isAssignableFrom(config.getIncrementalScoreCalculatorClass())) { throw new IllegalArgumentException( "The incrementalScoreCalculatorClass (%s) does not implement %s." @@ -37,14 +39,15 @@ public final class IncrementalScoreDirectorFactory> incrementalScoreCalculatorSupplier; public IncrementalScoreDirectorFactory(SolutionDescriptor solutionDescriptor, - Supplier> incrementalScoreCalculatorSupplier) { - super(solutionDescriptor); + Supplier> incrementalScoreCalculatorSupplier, + EnvironmentMode environmentMode) { + super(solutionDescriptor, environmentMode); this.incrementalScoreCalculatorSupplier = incrementalScoreCalculatorSupplier; } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirectorFactory.java index 10de34dcd9..df7939d398 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/stream/BavetConstraintStreamScoreDirectorFactory.java @@ -66,7 +66,7 @@ public BavetConstraintStreamScoreDirectorFactory(SolutionDescriptor s public BavetConstraintStreamScoreDirectorFactory(SolutionDescriptor solutionDescriptor, ConstraintProvider constraintProvider, EnvironmentMode environmentMode, boolean profilingEnabled) { - super(solutionDescriptor); + super(solutionDescriptor, environmentMode); var constraintFactory = new BavetConstraintFactory<>(solutionDescriptor, environmentMode); constraintMetaModel = DefaultConstraintMetaModel.of(constraintFactory.buildConstraints(constraintProvider)); constraintSessionFactory = diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamScoreDirectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamScoreDirectorFactory.java index 7f3bceb99b..6429a7a805 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamScoreDirectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/stream/common/AbstractConstraintStreamScoreDirectorFactory.java @@ -3,6 +3,7 @@ import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.score.Score; import ai.timefold.solver.core.api.score.stream.ConstraintMetaModel; +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; import ai.timefold.solver.core.impl.score.director.AbstractScoreDirectorFactory; import ai.timefold.solver.core.impl.score.director.ScoreDirectorFactory; @@ -18,8 +19,9 @@ public abstract class AbstractConstraintStreamScoreDirectorFactory, Factory_ extends AbstractConstraintStreamScoreDirectorFactory> extends AbstractScoreDirectorFactory { - protected AbstractConstraintStreamScoreDirectorFactory(SolutionDescriptor solutionDescriptor) { - super(solutionDescriptor); + protected AbstractConstraintStreamScoreDirectorFactory(SolutionDescriptor solutionDescriptor, + EnvironmentMode environmentMode) { + super(solutionDescriptor, environmentMode); } /** diff --git a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java index bdaa4d2213..97badc7880 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java @@ -119,7 +119,6 @@ public > ScoreDirectorFactory ge .withLookUpEnabled(true) .withConstraintMatchPolicy( constraintMatchEnabled ? ConstraintMatchPolicy.ENABLED : ConstraintMatchPolicy.DISABLED) - .withEnvironmentMode(environmentMode) .build(); solverScope.setScoreDirector(castScoreDirector); solverScope.setProblemChangeDirector(new DefaultProblemChangeDirector<>(castScoreDirector)); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/ChangeMoveTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/ChangeMoveTest.java index a0bc22e86e..2eca9b7e3a 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/ChangeMoveTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/ChangeMoveTest.java @@ -10,6 +10,7 @@ import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.score.director.ScoreDirector; import ai.timefold.solver.core.config.heuristic.selector.move.generic.ChangeMoveSelectorConfig; +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.score.director.easy.EasyScoreDirectorFactory; import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataSolution; @@ -56,7 +57,7 @@ void doMove() { var scoreDirectorFactory = new EasyScoreDirectorFactory<>(TestdataAllowsUnassignedEntityProvidingSolution.buildSolutionDescriptor(), - solution -> SimpleScore.ZERO); + solution -> SimpleScore.ZERO, EnvironmentMode.PHASE_ASSERT); var scoreDirector = scoreDirectorFactory.buildScoreDirector(); var variableDescriptor = TestdataAllowsUnassignedEntityProvidingEntity.buildVariableDescriptorForValue(); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/PillarChangeMoveTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/PillarChangeMoveTest.java index 13f1b4caa5..59a04955ad 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/PillarChangeMoveTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/PillarChangeMoveTest.java @@ -12,6 +12,7 @@ import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.score.director.ScoreDirector; +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.score.director.ValueRangeManager; import ai.timefold.solver.core.impl.score.director.VariableDescriptorAwareScoreDirector; import ai.timefold.solver.core.impl.score.director.easy.EasyScoreDirectorFactory; @@ -80,7 +81,7 @@ void doMove() { var scoreDirectorFactory = new EasyScoreDirectorFactory<>(TestdataAllowsUnassignedEntityProvidingSolution.buildSolutionDescriptor(), - solution -> SimpleScore.ZERO); + solution -> SimpleScore.ZERO, EnvironmentMode.PHASE_ASSERT); ScoreDirector scoreDirector = scoreDirectorFactory.buildScoreDirector(); var variableDescriptor = TestdataAllowsUnassignedEntityProvidingEntity.buildVariableDescriptorForValue(); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/PillarSwapMoveTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/PillarSwapMoveTest.java index fe21ba2ba0..54d9ed4049 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/PillarSwapMoveTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/PillarSwapMoveTest.java @@ -11,6 +11,7 @@ import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.score.director.ScoreDirector; +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.score.director.ValueRangeManager; import ai.timefold.solver.core.impl.score.director.VariableDescriptorAwareScoreDirector; import ai.timefold.solver.core.impl.score.director.easy.EasyScoreDirectorFactory; @@ -129,7 +130,7 @@ void doMove() { var scoreDirectorFactory = new EasyScoreDirectorFactory<>(TestdataAllowsUnassignedEntityProvidingSolution.buildSolutionDescriptor(), - solution -> SimpleScore.ZERO); + solution -> SimpleScore.ZERO, EnvironmentMode.PHASE_ASSERT); var scoreDirector = scoreDirectorFactory.buildScoreDirector(); var variableDescriptorList = TestdataAllowsUnassignedEntityProvidingEntity.buildEntityDescriptor().getGenuineVariableDescriptorList(); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/SwapMoveTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/SwapMoveTest.java index a20767d443..3238b3e8c6 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/SwapMoveTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/SwapMoveTest.java @@ -12,6 +12,7 @@ import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.score.director.ScoreDirector; import ai.timefold.solver.core.config.heuristic.selector.move.generic.SwapMoveSelectorConfig; +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.score.director.ValueRangeManager; import ai.timefold.solver.core.impl.score.director.VariableDescriptorAwareScoreDirector; import ai.timefold.solver.core.impl.score.director.easy.EasyScoreDirectorFactory; @@ -88,7 +89,7 @@ void doMove() { var scoreDirectorFactory = new EasyScoreDirectorFactory<>(TestdataAllowsUnassignedEntityProvidingSolution.buildSolutionDescriptor(), - solution -> SimpleScore.ZERO); + solution -> SimpleScore.ZERO, EnvironmentMode.PHASE_ASSERT); var scoreDirector = scoreDirectorFactory.buildScoreDirector(); var entityDescriptor = TestdataAllowsUnassignedEntityProvidingEntity.buildEntityDescriptor(); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/move/MoveDirectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/move/MoveDirectorTest.java index 0de9781ee6..c2e831d5e9 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/move/MoveDirectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/move/MoveDirectorTest.java @@ -817,7 +817,8 @@ void testSolverScopeNestedPhase() { void variableListenersAreTriggeredWhenSolutionIsConsistent() { var solutionDescriptor = TestdataShadowedFullSolution.buildSolutionDescriptor(); var scoreCalculator = new TestdataShadowedFullEasyScoreCalculator(); - var easyScoreDirectorFactory = new EasyScoreDirectorFactory<>(solutionDescriptor, scoreCalculator); + var easyScoreDirectorFactory = + new EasyScoreDirectorFactory<>(solutionDescriptor, scoreCalculator, EnvironmentMode.PHASE_ASSERT); var innerScoreDirector = easyScoreDirectorFactory.buildScoreDirector(); var moveDirector = new MoveDirector<>(innerScoreDirector); @@ -864,7 +865,8 @@ void variableListenersAreTriggeredWhenSolutionIsConsistent() { void undoCascadingUpdateShadowVariable() { var solutionDescriptor = TestdataSingleCascadingSolution.buildSolutionDescriptor(); var scoreCalculator = new TestdataSingleCascadingEasyScoreCalculator(); - var scoreDirectorFactory = new EasyScoreDirectorFactory<>(solutionDescriptor, scoreCalculator); + var scoreDirectorFactory = + new EasyScoreDirectorFactory<>(solutionDescriptor, scoreCalculator, EnvironmentMode.PHASE_ASSERT); var innerScoreDirector = scoreDirectorFactory.buildScoreDirector(); var moveDirector = new MoveDirector<>(innerScoreDirector); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorFactoryTest.java index 4ef349878e..be135916e4 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorFactoryTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorFactoryTest.java @@ -9,6 +9,7 @@ import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.score.director.InnerScore; import ai.timefold.solver.core.testdomain.TestdataSolution; @@ -22,7 +23,7 @@ void buildScoreDirector() { EasyScoreCalculator scoreCalculator = mock(EasyScoreCalculator.class); when(scoreCalculator.calculateScore(any(TestdataSolution.class))) .thenAnswer(invocation -> SimpleScore.of(-10)); - var directorFactory = new EasyScoreDirectorFactory<>(solutionDescriptor, scoreCalculator); + var directorFactory = new EasyScoreDirectorFactory<>(solutionDescriptor, scoreCalculator, EnvironmentMode.PHASE_ASSERT); try (var director = directorFactory.buildScoreDirector()) { var solution = new TestdataSolution(); solution.setValueList(Collections.emptyList()); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorTest.java index 9d9d2966d3..e0622dc70f 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/score/director/easy/EasyScoreDirectorTest.java @@ -6,6 +6,7 @@ import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.config.score.trend.InitializingScoreTrendLevel; +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.impl.score.director.InnerScore; import ai.timefold.solver.core.impl.score.trend.InitializingScoreTrend; import ai.timefold.solver.core.testdomain.TestdataValue; @@ -19,7 +20,7 @@ class EasyScoreDirectorTest { @Test void shadowVariableCorruption() { var scoreDirectorFactory = new EasyScoreDirectorFactory<>(TestdataCorruptedShadowedSolution.buildSolutionDescriptor(), - (solution_) -> SimpleScore.of(0)); + (solution_) -> SimpleScore.of(0), EnvironmentMode.PHASE_ASSERT); scoreDirectorFactory .setInitializingScoreTrend(InitializingScoreTrend.buildUniformTrend(InitializingScoreTrendLevel.ONLY_DOWN, 1)); try (var scoreDirector = scoreDirectorFactory.buildScoreDirector()) { diff --git a/core/src/test/java/ai/timefold/solver/core/preview/api/neighborhood/stream/enumerating/UniEnumeratingStreamTest.java b/core/src/test/java/ai/timefold/solver/core/preview/api/neighborhood/stream/enumerating/UniEnumeratingStreamTest.java index d5279ea48f..f492d134b1 100644 --- a/core/src/test/java/ai/timefold/solver/core/preview/api/neighborhood/stream/enumerating/UniEnumeratingStreamTest.java +++ b/core/src/test/java/ai/timefold/solver/core/preview/api/neighborhood/stream/enumerating/UniEnumeratingStreamTest.java @@ -165,7 +165,8 @@ private static DatasetSession createSession( EnumeratingStreamFactory enumeratingStreamFactory, Solution_ solution) { var scoreDirector = - new EasyScoreDirectorFactory<>(enumeratingStreamFactory.getSolutionDescriptor(), s -> SimpleScore.ZERO) + new EasyScoreDirectorFactory<>(enumeratingStreamFactory.getSolutionDescriptor(), s -> SimpleScore.ZERO, + EnvironmentMode.PHASE_ASSERT) .buildScoreDirector(); scoreDirector.setWorkingSolution(solution); var sessionContext = new SessionContext<>(scoreDirector); @@ -529,4 +530,4 @@ void forEachListVariableExcludingPinnedValuesIncludingNull() { .containsExactly(null, value2, value3, value4); } -} \ No newline at end of file +} diff --git a/core/src/test/java/ai/timefold/solver/core/testutil/PlannerTestUtils.java b/core/src/test/java/ai/timefold/solver/core/testutil/PlannerTestUtils.java index 560f99f2fd..0b54228fe0 100644 --- a/core/src/test/java/ai/timefold/solver/core/testutil/PlannerTestUtils.java +++ b/core/src/test/java/ai/timefold/solver/core/testutil/PlannerTestUtils.java @@ -25,6 +25,7 @@ import ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicPhaseConfig; import ai.timefold.solver.core.config.localsearch.LocalSearchPhaseConfig; import ai.timefold.solver.core.config.score.trend.InitializingScoreTrendLevel; +import ai.timefold.solver.core.config.solver.EnvironmentMode; import ai.timefold.solver.core.config.solver.SolverConfig; import ai.timefold.solver.core.config.solver.termination.TerminationConfig; import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; @@ -136,7 +137,8 @@ public static TestdataSolution generateTestdataSolution(String code, int entityA public static InnerScoreDirector mockScoreDirector(SolutionDescriptor solutionDescriptor, boolean useSolution) { - var scoreDirectorFactory = new EasyScoreDirectorFactory<>(solutionDescriptor, solution_ -> SimpleScore.of(0)); + var scoreDirectorFactory = new EasyScoreDirectorFactory<>(solutionDescriptor, solution_ -> SimpleScore.of(0), + EnvironmentMode.PHASE_ASSERT); scoreDirectorFactory .setInitializingScoreTrend(InitializingScoreTrend.buildUniformTrend(InitializingScoreTrendLevel.ONLY_DOWN, 1)); if (useSolution) { From 01a28b58d8d2e9d6be6f30664d3aba4164199bb6 Mon Sep 17 00:00:00 2001 From: fred Date: Wed, 4 Feb 2026 11:06:49 -0300 Subject: [PATCH 12/13] chore: address comments --- .../core/impl/score/director/AbstractScoreDirector.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java index 210d343c02..c0f0923a14 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/score/director/AbstractScoreDirector.java @@ -862,6 +862,10 @@ private void assertValueRangeForListVariable(Object entity, List valueLi return; } var listVariableDescriptor = entityDescriptor.getGenuineListVariableDescriptor(); + if (listVariableDescriptor == null) { + // The entity has no genuine list variable + return; + } assertValueRangeForListVariable(this, listVariableDescriptor, entity, valueList); } @@ -883,7 +887,7 @@ private static void assertValueRangeForBasicVariables(InnerScoreDire var allEntities = variableDescriptor.getEntityDescriptor().extractEntities(scoreDirector.getWorkingSolution()); if (allEntities.contains(value)) { - return; + continue; } } throw new IllegalStateException( From 265c7fd9fed532fd4b74051fa58fd0e32a08da0a Mon Sep 17 00:00:00 2001 From: fred Date: Wed, 4 Feb 2026 11:27:12 -0300 Subject: [PATCH 13/13] chore: fix test --- .../solver/core/impl/neighborhood/NeighborhoodsTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/NeighborhoodsTest.java b/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/NeighborhoodsTest.java index 54929dfd1c..a7de353552 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/NeighborhoodsTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/neighborhood/NeighborhoodsTest.java @@ -86,7 +86,8 @@ void changeMoveBasedLocalSearch() { solution.getEntityList().forEach(e -> e.setValue(secondValue)); var scoreDirector = - new EasyScoreDirectorFactory<>(solutionDescriptor, new TestingEasyScoreCalculator()).buildScoreDirector(); + new EasyScoreDirectorFactory<>(solutionDescriptor, new TestingEasyScoreCalculator(), + EnvironmentMode.PHASE_ASSERT).buildScoreDirector(); scoreDirector.setWorkingSolution(solution); var score = scoreDirector.calculateScore();