diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/composite/NullAllowingCountableValueRange.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/composite/NullAllowingCountableValueRange.java index 93315c8bf7a..6fb7fb49aad 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/composite/NullAllowingCountableValueRange.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/composite/NullAllowingCountableValueRange.java @@ -55,7 +55,8 @@ public boolean contains(T value) { @Override public ValueRange sort(ValueRangeSorter sorter) { - return childValueRange.sort(sorter); + var sortedRange = (CountableValueRange) childValueRange.sort(sorter); + return new NullAllowingCountableValueRange<>(sortedRange); } @Override diff --git a/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java index 896627d688c..f634873d65d 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java @@ -12,8 +12,12 @@ import java.util.List; import java.util.Objects; +import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore; import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; +import ai.timefold.solver.core.api.score.stream.Constraint; +import ai.timefold.solver.core.api.score.stream.ConstraintFactory; +import ai.timefold.solver.core.api.score.stream.ConstraintProvider; import ai.timefold.solver.core.api.solver.SolutionManager; import ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicPhaseConfig; import ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicType; @@ -31,6 +35,8 @@ import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListChangeMoveSelectorConfig; import ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig; import ai.timefold.solver.core.config.heuristic.selector.value.ValueSorterManner; +import ai.timefold.solver.core.config.solver.SolverConfig; +import ai.timefold.solver.core.config.solver.termination.TerminationConfig; import ai.timefold.solver.core.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataSolution; import ai.timefold.solver.core.testdomain.TestdataValue; @@ -53,6 +59,8 @@ 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.unassignedvar.sort.TestdataAllowsUnassignedListSortableEntity; +import ai.timefold.solver.core.testdomain.list.unassignedvar.sort.TestdataAllowsUnassignedListSortableSolution; import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingEntity; import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingScoreCalculator; import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingSolution; @@ -98,6 +106,8 @@ import ai.timefold.solver.core.testdomain.unassignedvar.TestdataAllowsUnassignedEasyScoreCalculator; import ai.timefold.solver.core.testdomain.unassignedvar.TestdataAllowsUnassignedEntity; import ai.timefold.solver.core.testdomain.unassignedvar.TestdataAllowsUnassignedSolution; +import ai.timefold.solver.core.testdomain.unassignedvar.sort.TestdataAllowsUnassignedSortableEntity; +import ai.timefold.solver.core.testdomain.unassignedvar.sort.TestdataAllowsUnassignedSortableSolution; 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; @@ -1450,6 +1460,32 @@ void solveValueFactorySorting(ConstructionHeuristicTestConfig phaseConfig) { } } + @Test + void penalizeBasicVariable() { + var solverConfig = new SolverConfig() + .withSolutionClass(TestdataAllowsUnassignedSortableSolution.class) + .withEntityClasses(TestdataAllowsUnassignedSortableEntity.class) + .withConstraintProviderClass(PenalizeAssignedConstraintProvider.class) + .withPhases(new ConstructionHeuristicPhaseConfig() + .withTerminationConfig(new TerminationConfig().withStepCountLimit(3))); + var problem = TestdataAllowsUnassignedSortableSolution.generateSolution(1, 1, false); + var solution = PlannerTestUtils.solve(solverConfig, problem); + assertThat(solution.getEntityList().get(0).getValue()).isNull(); + } + + @Test + void penalizeListVariable() { + var solverConfig = new SolverConfig() + .withSolutionClass(TestdataAllowsUnassignedListSortableSolution.class) + .withEntityClasses(TestdataAllowsUnassignedListSortableEntity.class) + .withConstraintProviderClass(ListPenalizeAssignedConstraintProvider.class) + .withPhases(new ConstructionHeuristicPhaseConfig() + .withTerminationConfig(new TerminationConfig().withStepCountLimit(3))); + var problem = TestdataAllowsUnassignedListSortableSolution.generateSolution(1, 1, false); + var solution = PlannerTestUtils.solve(solverConfig, problem); + assertThat(solution.getEntityList().get(0).getValueList()).isEmpty(); + } + @Test void failConstructionHeuristicEntityRange() { var solverConfig = @@ -1712,6 +1748,35 @@ public static class TestdataSolutionEasyScoreCalculator } } + public static class PenalizeAssignedConstraintProvider implements ConstraintProvider { + + @Override + public Constraint @NonNull [] defineConstraints(@NonNull ConstraintFactory constraintFactory) { + return new Constraint[] { penalizeAssigned(constraintFactory) }; + } + + Constraint penalizeAssigned(ConstraintFactory constraintFactory) { + return constraintFactory.forEach(TestdataAllowsUnassignedSortableEntity.class) + .penalize(HardSoftScore.ONE_HARD) + .asConstraint("penalize assigned"); + } + } + + public static class ListPenalizeAssignedConstraintProvider implements ConstraintProvider { + + @Override + public Constraint @NonNull [] defineConstraints(@NonNull ConstraintFactory constraintFactory) { + return new Constraint[] { penalizeAssigned(constraintFactory) }; + } + + Constraint penalizeAssigned(ConstraintFactory constraintFactory) { + return constraintFactory.forEach(TestdataAllowsUnassignedListSortableEntity.class) + .filter(entity -> !entity.getValueList().isEmpty()) + .penalize(HardSoftScore.ONE_HARD) + .asConstraint("penalize assigned"); + } + } + private record ConstructionHeuristicTestConfig(ConstructionHeuristicPhaseConfig config, int[] expected, boolean shuffle) { } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/composite/NullAllowingCountableValueRangeTest.java b/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/composite/NullAllowingCountableValueRangeTest.java index 0c71acf1f36..b39acbe1e83 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/composite/NullAllowingCountableValueRangeTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/domain/valuerange/buildin/composite/NullAllowingCountableValueRangeTest.java @@ -106,16 +106,16 @@ void sort() { new ComparatorFactorySelectionSorter<>(solution -> integerComparator, SelectionSorterOrder.DESCENDING)); assertAllElementsOfIterator(((CountableValueRange) new NullAllowingCountableValueRange<>( (new ListValueRange<>(Arrays.asList(-15, 25, 0, 1, -1)))).sort(ascComparatorSorter)).createOriginalIterator(), - -15, -1, 0, 1, 25); + null, -15, -1, 0, 1, 25); assertAllElementsOfIterator(((CountableValueRange) new NullAllowingCountableValueRange<>( (new ListValueRange<>(Arrays.asList(-15, 25, 0, 1, -1)))).sort(ascComparatorFactorySorter)) - .createOriginalIterator(), -15, -1, 0, 1, 25); + .createOriginalIterator(), null, -15, -1, 0, 1, 25); assertAllElementsOfIterator(((CountableValueRange) new NullAllowingCountableValueRange<>( (new ListValueRange<>(Arrays.asList(-15, 25, 0, 1, -1)))).sort(descComparatorSorter)).createOriginalIterator(), - 25, 1, 0, -1, -15); + null, 25, 1, 0, -1, -15); assertAllElementsOfIterator(((CountableValueRange) new NullAllowingCountableValueRange<>( (new ListValueRange<>(Arrays.asList(-15, 25, 0, 1, -1)))).sort(descComparatorFactorySorter)) - .createOriginalIterator(), 25, 1, 0, -1, -15); + .createOriginalIterator(), null, 25, 1, 0, -1, -15); } } diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/unassignedvar/sort/TestdataAllowsUnassignedListSortableEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/unassignedvar/sort/TestdataAllowsUnassignedListSortableEntity.java new file mode 100644 index 00000000000..5d25fa0a66d --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/unassignedvar/sort/TestdataAllowsUnassignedListSortableEntity.java @@ -0,0 +1,35 @@ +package ai.timefold.solver.core.testdomain.list.unassignedvar.sort; + +import java.util.ArrayList; +import java.util.List; + +import ai.timefold.solver.core.api.domain.entity.PlanningEntity; +import ai.timefold.solver.core.api.domain.variable.PlanningListVariable; +import ai.timefold.solver.core.testdomain.TestdataObject; +import ai.timefold.solver.core.testdomain.common.TestSortableComparator; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningEntity +public class TestdataAllowsUnassignedListSortableEntity extends TestdataObject { + + @PlanningListVariable(allowsUnassignedValues = true, valueRangeProviderRefs = "valueRange", + comparatorClass = TestSortableComparator.class) + private List valueList; + + public TestdataAllowsUnassignedListSortableEntity() { + } + + public TestdataAllowsUnassignedListSortableEntity(String code) { + super(code); + this.valueList = new ArrayList<>(); + } + + public List getValueList() { + return valueList; + } + + public void setValueList(List valueList) { + this.valueList = valueList; + } + +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/unassignedvar/sort/TestdataAllowsUnassignedListSortableSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/unassignedvar/sort/TestdataAllowsUnassignedListSortableSolution.java new file mode 100644 index 00000000000..e94011c546e --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/unassignedvar/sort/TestdataAllowsUnassignedListSortableSolution.java @@ -0,0 +1,84 @@ +package ai.timefold.solver.core.testdomain.list.unassignedvar.sort; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.stream.IntStream; + +import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; +import ai.timefold.solver.core.api.domain.solution.PlanningScore; +import ai.timefold.solver.core.api.domain.solution.PlanningSolution; +import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty; +import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; +import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; +import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningSolution +public class TestdataAllowsUnassignedListSortableSolution { + + public static SolutionDescriptor buildSolutionDescriptor() { + return SolutionDescriptor.buildSolutionDescriptor( + TestdataAllowsUnassignedListSortableSolution.class, + TestdataAllowsUnassignedListSortableEntity.class, + TestdataSortableValue.class); + } + + public static TestdataAllowsUnassignedListSortableSolution generateSolution(int valueCount, int entityCount, + boolean shuffle) { + var entityList = new ArrayList<>(IntStream.range(0, entityCount) + .mapToObj(i -> new TestdataAllowsUnassignedListSortableEntity("Generated Entity " + i)) + .toList()); + var valueList = new ArrayList<>(IntStream.range(0, valueCount) + .mapToObj(i -> new TestdataSortableValue("Generated Value " + i, i)) + .toList()); + if (shuffle) { + var random = new Random(0); + Collections.shuffle(entityList, random); + Collections.shuffle(valueList, random); + } + TestdataAllowsUnassignedListSortableSolution solution = new TestdataAllowsUnassignedListSortableSolution(); + solution.setValueList(valueList); + solution.setEntityList(entityList); + return solution; + } + + private List valueList; + private List entityList; + private HardSoftScore score; + + @ValueRangeProvider(id = "valueRange") + @ProblemFactCollectionProperty + public List getValueList() { + return valueList; + } + + public void setValueList(List valueList) { + this.valueList = valueList; + } + + @PlanningEntityCollectionProperty + public List getEntityList() { + return entityList; + } + + public void setEntityList(List entityList) { + this.entityList = entityList; + } + + @PlanningScore + public HardSoftScore getScore() { + return score; + } + + public void setScore(HardSoftScore score) { + this.score = score; + } + + public void removeEntity(TestdataAllowsUnassignedListSortableEntity entity) { + this.entityList = entityList.stream() + .filter(e -> e != entity) + .toList(); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/unassignedvar/sort/TestdataAllowsUnassignedSortableEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/unassignedvar/sort/TestdataAllowsUnassignedSortableEntity.java new file mode 100644 index 00000000000..df7bdcef278 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/unassignedvar/sort/TestdataAllowsUnassignedSortableEntity.java @@ -0,0 +1,30 @@ +package ai.timefold.solver.core.testdomain.unassignedvar.sort; + +import ai.timefold.solver.core.api.domain.entity.PlanningEntity; +import ai.timefold.solver.core.api.domain.variable.PlanningVariable; +import ai.timefold.solver.core.testdomain.TestdataObject; +import ai.timefold.solver.core.testdomain.common.TestSortableComparator; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningEntity +public class TestdataAllowsUnassignedSortableEntity extends TestdataObject { + + @PlanningVariable(allowsUnassigned = true, valueRangeProviderRefs = "valueRange", + comparatorClass = TestSortableComparator.class) + private TestdataSortableValue value; + + public TestdataAllowsUnassignedSortableEntity() { + } + + public TestdataAllowsUnassignedSortableEntity(String code) { + super(code); + } + + public TestdataSortableValue getValue() { + return value; + } + + public void setValue(TestdataSortableValue value) { + this.value = value; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/unassignedvar/sort/TestdataAllowsUnassignedSortableSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/unassignedvar/sort/TestdataAllowsUnassignedSortableSolution.java new file mode 100644 index 00000000000..f0df90767ca --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/unassignedvar/sort/TestdataAllowsUnassignedSortableSolution.java @@ -0,0 +1,84 @@ +package ai.timefold.solver.core.testdomain.unassignedvar.sort; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.stream.IntStream; + +import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; +import ai.timefold.solver.core.api.domain.solution.PlanningScore; +import ai.timefold.solver.core.api.domain.solution.PlanningSolution; +import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty; +import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; +import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; +import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningSolution +public class TestdataAllowsUnassignedSortableSolution { + + public static SolutionDescriptor buildSolutionDescriptor() { + return SolutionDescriptor.buildSolutionDescriptor( + TestdataAllowsUnassignedSortableSolution.class, + TestdataAllowsUnassignedSortableEntity.class, + TestdataSortableValue.class); + } + + public static TestdataAllowsUnassignedSortableSolution generateSolution(int valueCount, int entityCount, + boolean shuffle) { + var entityList = new ArrayList<>(IntStream.range(0, entityCount) + .mapToObj(i -> new TestdataAllowsUnassignedSortableEntity("Generated Entity " + i)) + .toList()); + var valueList = new ArrayList<>(IntStream.range(0, valueCount) + .mapToObj(i -> new TestdataSortableValue("Generated Value " + i, i)) + .toList()); + if (shuffle) { + var random = new Random(0); + Collections.shuffle(entityList, random); + Collections.shuffle(valueList, random); + } + TestdataAllowsUnassignedSortableSolution solution = new TestdataAllowsUnassignedSortableSolution(); + solution.setValueList(valueList); + solution.setEntityList(entityList); + return solution; + } + + private List valueList; + private List entityList; + private HardSoftScore score; + + @ValueRangeProvider(id = "valueRange") + @ProblemFactCollectionProperty + public List getValueList() { + return valueList; + } + + public void setValueList(List valueList) { + this.valueList = valueList; + } + + @PlanningEntityCollectionProperty + public List getEntityList() { + return entityList; + } + + public void setEntityList(List entityList) { + this.entityList = entityList; + } + + @PlanningScore + public HardSoftScore getScore() { + return score; + } + + public void setScore(HardSoftScore score) { + this.score = score; + } + + public void removeEntity(TestdataAllowsUnassignedSortableEntity entity) { + this.entityList = entityList.stream() + .filter(e -> e != entity) + .toList(); + } +}