diff --git a/benchmark/src/main/resources/benchmark.xsd b/benchmark/src/main/resources/benchmark.xsd index 57f465034a..86ede21c93 100644 --- a/benchmark/src/main/resources/benchmark.xsd +++ b/benchmark/src/main/resources/benchmark.xsd @@ -890,9 +890,15 @@ + + + + + + @@ -1082,9 +1088,15 @@ + + + + + + @@ -1229,9 +1241,15 @@ + + + + + + @@ -2874,6 +2892,12 @@ + + + + + + @@ -2901,6 +2925,18 @@ + + + + + + + + + + + + diff --git a/core/src/build/revapi-differences.json b/core/src/build/revapi-differences.json index f122c49e3e..537f4e3860 100644 --- a/core/src/build/revapi-differences.json +++ b/core/src/build/revapi-differences.json @@ -348,6 +348,108 @@ "oldValue": "{\"constructionHeuristicType\", \"entitySorterManner\", \"valueSorterManner\", \"entityPlacerConfig\", \"moveSelectorConfigList\", \"foragerConfig\"}", "newValue": "{\"constructionHeuristicType\", \"entitySorterManner\", \"valueSorterManner\", \"entityPlacerConfigList\", \"moveSelectorConfigList\", \"foragerConfig\"}", "justification": "New CH configuration with multiple placers" + }, + { + "ignore": true, + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.lang.Class ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig::getSorterWeightFactoryClass()", + "new": "method java.lang.Class ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig::getSorterWeightFactoryClass()", + "justification": "New comparator factory class" + }, + { + "ignore": true, + "code": "java.method.parameterTypeParameterChanged", + "old": "parameter void ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig::setSorterWeightFactoryClass(===java.lang.Class===)", + "new": "parameter void ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig::setSorterWeightFactoryClass(===java.lang.Class===)", + "parameterIndex": "0", + "justification": "New comparator factory class" + }, + { + "ignore": true, + "code": "java.annotation.attributeValueChanged", + "old": "class ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig", + "new": "class ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig", + "annotationType": "jakarta.xml.bind.annotation.XmlType", + "attribute": "propOrder", + "oldValue": "{\"id\", \"mimicSelectorRef\", \"entityClass\", \"cacheType\", \"selectionOrder\", \"nearbySelectionConfig\", \"filterClass\", \"sorterManner\", \"sorterComparatorClass\", \"sorterWeightFactoryClass\", \"sorterOrder\", \"sorterClass\", \"probabilityWeightFactoryClass\", \"selectedCountLimit\"}", + "newValue": "{\"id\", \"mimicSelectorRef\", \"entityClass\", \"cacheType\", \"selectionOrder\", \"nearbySelectionConfig\", \"filterClass\", \"sorterManner\", \"sorterComparatorClass\", \"comparatorClass\", \"sorterWeightFactoryClass\", \"comparatorFactoryClass\", \"sorterOrder\", \"sorterClass\", \"probabilityWeightFactoryClass\", \"selectedCountLimit\"}", + "justification": "New comparator properties" + }, + { + "ignore": true, + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.lang.Class ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig>::getSorterWeightFactoryClass()", + "new": "method java.lang.Class ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig>::getSorterWeightFactoryClass()", + "justification": "New comparator factory class" + }, + { + "ignore": true, + "code": "java.method.parameterTypeParameterChanged", + "old": "parameter void ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig>::setSorterWeightFactoryClass(===java.lang.Class===)", + "new": "parameter void ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig>::setSorterWeightFactoryClass(===java.lang.Class===)", + "parameterIndex": "0", + "justification": "New comparator factory class" + }, + { + "ignore": true, + "code": "java.annotation.attributeValueChanged", + "old": "class ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig>", + "new": "class ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig>", + "annotationType": "jakarta.xml.bind.annotation.XmlType", + "attribute": "propOrder", + "oldValue": "{\"cacheType\", \"selectionOrder\", \"filterClass\", \"sorterComparatorClass\", \"sorterWeightFactoryClass\", \"sorterOrder\", \"sorterClass\", \"probabilityWeightFactoryClass\", \"selectedCountLimit\", \"fixedProbabilityWeight\"}", + "newValue": "{\"cacheType\", \"selectionOrder\", \"filterClass\", \"sorterComparatorClass\", \"comparatorClass\", \"sorterWeightFactoryClass\", \"comparatorFactoryClass\", \"sorterOrder\", \"sorterClass\", \"probabilityWeightFactoryClass\", \"selectedCountLimit\", \"fixedProbabilityWeight\"}", + "justification": "New comparator properties" + }, + { + "ignore": true, + "code": "java.method.returnTypeTypeParametersChanged", + "old": "method java.lang.Class ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig::getSorterWeightFactoryClass()", + "new": "method java.lang.Class ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig::getSorterWeightFactoryClass()", + "justification": "New comparator factory class" + }, + { + "ignore": true, + "code": "java.method.parameterTypeParameterChanged", + "old": "parameter void ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig::setSorterWeightFactoryClass(===java.lang.Class===)", + "new": "parameter void ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig::setSorterWeightFactoryClass(===java.lang.Class===)", + "parameterIndex": "0", + "justification": "New comparator factory class" + }, + { + "ignore": true, + "code": "java.annotation.attributeValueChanged", + "old": "class ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig", + "new": "class ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig", + "annotationType": "jakarta.xml.bind.annotation.XmlType", + "attribute": "propOrder", + "oldValue": "{\"id\", \"mimicSelectorRef\", \"downcastEntityClass\", \"variableName\", \"cacheType\", \"selectionOrder\", \"nearbySelectionConfig\", \"filterClass\", \"sorterManner\", \"sorterComparatorClass\", \"sorterWeightFactoryClass\", \"sorterOrder\", \"sorterClass\", \"probabilityWeightFactoryClass\", \"selectedCountLimit\"}", + "newValue": "{\"id\", \"mimicSelectorRef\", \"downcastEntityClass\", \"variableName\", \"cacheType\", \"selectionOrder\", \"nearbySelectionConfig\", \"filterClass\", \"sorterManner\", \"sorterComparatorClass\", \"comparatorClass\", \"sorterWeightFactoryClass\", \"comparatorFactoryClass\", \"sorterOrder\", \"sorterClass\", \"probabilityWeightFactoryClass\", \"selectedCountLimit\"}", + "justification": "New comparator properties" + }, + { + "ignore": true, + "code": "java.annotation.added", + "old": "class ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig", + "new": "class ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig", + "annotation": "@org.jspecify.annotations.NullMarked", + "justification": "Update config" + }, + { + "ignore": true, + "code": "java.annotation.added", + "old": "class ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig>", + "new": "class ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig>", + "annotation": "@org.jspecify.annotations.NullMarked", + "justification": "Update config" + }, + { + "ignore": true, + "code": "java.annotation.added", + "old": "class ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig", + "new": "class ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig", + "annotation": "@org.jspecify.annotations.NullMarked", + "justification": "Update config" } ] } diff --git a/core/src/main/java/ai/timefold/solver/core/api/domain/common/ComparatorFactory.java b/core/src/main/java/ai/timefold/solver/core/api/domain/common/ComparatorFactory.java new file mode 100644 index 0000000000..df604b1c0f --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/api/domain/common/ComparatorFactory.java @@ -0,0 +1,47 @@ +package ai.timefold.solver.core.api.domain.common; + +import java.util.Comparator; + +import ai.timefold.solver.core.api.domain.entity.PlanningEntity; +import ai.timefold.solver.core.api.domain.solution.PlanningSolution; +import ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicPhaseConfig; +import ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig; +import ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig; +import ai.timefold.solver.core.impl.heuristic.move.Move; +import ai.timefold.solver.core.impl.heuristic.selector.Selector; + +import org.jspecify.annotations.NullMarked; + +/** + * Creates a {@link Comparable} to decide the order of a collection of selections + * (a selection is a {@link PlanningEntity}, a planningValue, a {@link Move} or a {@link Selector}). + * The selections are then sorted by some specific metric, + * normally ascending unless it's configured descending. + * The property {@code sortManner}, + * present in the selector configurations such as {@link ValueSelectorConfig} and {@link EntitySelectorConfig}, + * specifies how the data will be sorted. + * Additionally, + * the property {@code constructionHeuristicType} from {@link ConstructionHeuristicPhaseConfig} can also configure how entities + * and values are sorted. + *

+ * Implementations are expected to be stateless. + * The solver may choose to reuse instances. + * + * @param the solution type, the class with the {@link PlanningSolution} annotation + * @param the selection type + * + * @see ValueSelectorConfig + * @see EntitySelectorConfig + * @see ConstructionHeuristicPhaseConfig + */ +@NullMarked +@FunctionalInterface +public interface ComparatorFactory { + + /** + * @param solution never null, the {@link PlanningSolution} to which the selection belongs or applies to + * @return never null + */ + Comparator createComparator(Solution_ solution); + +} diff --git a/core/src/main/java/ai/timefold/solver/core/api/domain/entity/PlanningEntity.java b/core/src/main/java/ai/timefold/solver/core/api/domain/entity/PlanningEntity.java index ab0e2f9ea2..f041b0b872 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/domain/entity/PlanningEntity.java +++ b/core/src/main/java/ai/timefold/solver/core/api/domain/entity/PlanningEntity.java @@ -7,6 +7,7 @@ import java.lang.annotation.Target; import java.util.Comparator; +import ai.timefold.solver.core.api.domain.common.ComparatorFactory; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.domain.variable.PlanningVariable; import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory; @@ -34,6 +35,40 @@ @Retention(RUNTIME) public @interface PlanningEntity { + /** + * Allows sorting a collection of planning entities for this variable. + * Some algorithms perform better when the entities are sorted based on specific metrics. + *

+ * The {@link Comparator} should sort the data in ascending order. + * For example, prioritize three vehicles by sorting them based on their capacity: + * Vehicle C (4 people), Vehicle A (6 people), Vehicle B (32 people) + *

+ * Do not use together with {@link #comparatorFactoryClass()}. + * + * @return {@link PlanningVariable.NullComparator} when it is null (workaround for annotation limitation) + * @see #comparatorFactoryClass() + */ + Class comparatorClass() default NullComparator.class; + + interface NullComparator extends Comparator { + } + + /** + * The {@link ComparatorFactory} alternative for {@link #comparatorClass()}. + *

+ * Differs from {@link #comparatorClass()} + * because it allows accessing the current solution when creating the comparator. + *

+ * Do not use together with {@link #comparatorClass()}. + * + * @return {@link NullComparatorFactory} when it is null (workaround for annotation limitation) + * @see #comparatorClass() + */ + Class comparatorFactoryClass() default NullComparatorFactory.class; + + interface NullComparatorFactory extends ComparatorFactory { + } + /** * A pinned planning entity is never changed during planning, * this is useful in repeated planning use cases (such as continuous planning and real-time planning). @@ -50,7 +85,7 @@ /** * Workaround for annotation limitation in {@link #pinningFilter()}. - * + * * @deprecated Prefer using {@link PlanningPin}. */ @Deprecated(forRemoval = true, since = "1.23.0") @@ -69,13 +104,21 @@ interface NullPinningFilter extends PinningFilter { *

* Do not use together with {@link #difficultyWeightFactoryClass()}. * + * @deprecated Deprecated in favor of {@link #comparatorClass()}. + * * @return {@link NullDifficultyComparator} when it is null (workaround for annotation limitation) * @see #difficultyWeightFactoryClass() */ + @Deprecated(forRemoval = true, since = "1.28.0") Class difficultyComparatorClass() default NullDifficultyComparator.class; - /** Workaround for annotation limitation in {@link #difficultyComparatorClass()}. */ - interface NullDifficultyComparator extends Comparator { + /** + * Workaround for annotation limitation in {@link #difficultyComparatorClass()}. + * + * @deprecated Deprecated in favor of {@link NullComparator}. + */ + @Deprecated(forRemoval = true, since = "1.28.0") + interface NullDifficultyComparator extends NullComparator { } /** @@ -83,13 +126,23 @@ interface NullDifficultyComparator extends Comparator { *

* Do not use together with {@link #difficultyComparatorClass()}. * + * @deprecated Deprecated in favor of {@link #comparatorFactoryClass()}. + * * @return {@link NullDifficultyWeightFactory} when it is null (workaround for annotation limitation) * @see #difficultyComparatorClass() */ + @Deprecated(forRemoval = true, since = "1.28.0") Class difficultyWeightFactoryClass() default NullDifficultyWeightFactory.class; - /** Workaround for annotation limitation in {@link #difficultyWeightFactoryClass()}. */ - interface NullDifficultyWeightFactory extends SelectionSorterWeightFactory { + /** + * Workaround for annotation limitation in {@link #difficultyWeightFactoryClass()}. + * + * @deprecated Deprecated in favor of {@link NullComparatorFactory}. + */ + @Deprecated(forRemoval = true, since = "1.28.0") + interface NullDifficultyWeightFactory + extends SelectionSorterWeightFactory, + NullComparatorFactory { } } diff --git a/core/src/main/java/ai/timefold/solver/core/api/domain/variable/PlanningListVariable.java b/core/src/main/java/ai/timefold/solver/core/api/domain/variable/PlanningListVariable.java index c6d28fc223..3ada275a51 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/domain/variable/PlanningListVariable.java +++ b/core/src/main/java/ai/timefold/solver/core/api/domain/variable/PlanningListVariable.java @@ -6,11 +6,15 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; +import java.util.Comparator; import java.util.List; +import ai.timefold.solver.core.api.domain.common.ComparatorFactory; import ai.timefold.solver.core.api.domain.entity.PlanningEntity; import ai.timefold.solver.core.api.domain.entity.PlanningPin; import ai.timefold.solver.core.api.domain.entity.PlanningPinToIndex; +import ai.timefold.solver.core.api.domain.variable.PlanningVariable.NullComparator; +import ai.timefold.solver.core.api.domain.variable.PlanningVariable.NullComparatorFactory; /** * Specifies that a bean property (or a field) can be changed and should be optimized by the optimization algorithms. @@ -55,5 +59,29 @@ String[] valueRangeProviderRefs() default {}; - // TODO value comparison: https://issues.redhat.com/browse/PLANNER-2542 + /** + * Allows sorting a collection of planning values for this variable. + * Some algorithms perform better when the values are sorted based on specific metrics. + *

+ * The {@link Comparator} should sort the data in ascending order. + * For example, prioritize three visits by sorting them based on their importance: + * Visit C (SMALL_PRIORITY), Visit A (MEDIUM_PRIORITY), Visit B (HIGH_PRIORITY) + *

+ * Do not use together with {@link #comparatorFactoryClass()}. + * + * @return {@link NullComparator} when it is null (workaround for annotation limitation) + * @see #comparatorFactoryClass() + */ + Class comparatorClass() default NullComparator.class; + + /** + * The {@link ComparatorFactory} alternative for {@link #comparatorClass()}. + *

+ * Do not use together with {@link #comparatorClass()}. + * + * @return {@link NullComparatorFactory} when it is null (workaround for annotation limitation) + * @see #comparatorClass() + */ + Class comparatorFactoryClass() default NullComparatorFactory.class; + } diff --git a/core/src/main/java/ai/timefold/solver/core/api/domain/variable/PlanningVariable.java b/core/src/main/java/ai/timefold/solver/core/api/domain/variable/PlanningVariable.java index 501ab44064..7f9967fd22 100644 --- a/core/src/main/java/ai/timefold/solver/core/api/domain/variable/PlanningVariable.java +++ b/core/src/main/java/ai/timefold/solver/core/api/domain/variable/PlanningVariable.java @@ -8,6 +8,7 @@ import java.lang.annotation.Target; import java.util.Comparator; +import ai.timefold.solver.core.api.domain.common.ComparatorFactory; import ai.timefold.solver.core.api.domain.entity.PlanningEntity; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; @@ -50,6 +51,48 @@ */ boolean allowsUnassigned() default false; + /** + * In some use cases, such as Vehicle Routing, planning entities form a specific graph type, + * as specified by {@link PlanningVariableGraphType}. + * + * @return never null, defaults to {@link PlanningVariableGraphType#NONE} + */ + PlanningVariableGraphType graphType() default PlanningVariableGraphType.NONE; + + /** + * Allows sorting a collection of planning values for this variable. + * Some algorithms perform better when the values are sorted based on specific metrics. + *

+ * The {@link Comparator} should sort the data in ascending order. + * For example, prioritize three visits by sorting them based on their importance: + * Visit C (SMALL_PRIORITY), Visit A (MEDIUM_PRIORITY), Visit B (HIGH_PRIORITY) + *

+ * Do not use together with {@link #comparatorFactoryClass()}. + * + * @return {@link NullComparator} when it is null (workaround for annotation limitation) + * @see #comparatorFactoryClass() + */ + Class comparatorClass() default NullComparator.class; + + interface NullComparator extends Comparator { + } + + /** + * The {@link ComparatorFactory} alternative for {@link #comparatorClass()}. + *

+ * Differs from {@link #comparatorClass()} + * because it allows accessing the current solution when creating the comparator. + *

+ * Do not use together with {@link #comparatorClass()}. + * + * @return {@link NullComparatorFactory} when it is null (workaround for annotation limitation) + * @see #comparatorClass() + */ + Class comparatorFactoryClass() default NullComparatorFactory.class; + + interface NullComparatorFactory extends ComparatorFactory { + } + /** * As defined by {@link #allowsUnassigned()}. * @@ -59,14 +102,6 @@ @Deprecated(forRemoval = true, since = "1.8.0") boolean nullable() default false; - /** - * In some use cases, such as Vehicle Routing, planning entities form a specific graph type, - * as specified by {@link PlanningVariableGraphType}. - * - * @return never null, defaults to {@link PlanningVariableGraphType#NONE} - */ - PlanningVariableGraphType graphType() default PlanningVariableGraphType.NONE; - /** * Allows a collection of planning values for this variable to be sorted by strength. * A strengthWeight estimates how strong a planning value is. @@ -78,13 +113,21 @@ *

* Do not use together with {@link #strengthWeightFactoryClass()}. * + * @deprecated Deprecated in favor of {@link #comparatorClass()}. + * * @return {@link NullStrengthComparator} when it is null (workaround for annotation limitation) * @see #strengthWeightFactoryClass() */ + @Deprecated(forRemoval = true, since = "1.28.0") Class strengthComparatorClass() default NullStrengthComparator.class; - /** Workaround for annotation limitation in {@link #strengthComparatorClass()}. */ - interface NullStrengthComparator extends Comparator { + /** + * Workaround for annotation limitation in {@link #strengthComparatorClass()}. + * + * @deprecated Deprecated in favor of {@link NullComparator}. + */ + @Deprecated(forRemoval = true, since = "1.28.0") + interface NullStrengthComparator extends NullComparator { } /** @@ -92,13 +135,21 @@ interface NullStrengthComparator extends Comparator { *

* Do not use together with {@link #strengthComparatorClass()}. * + * @deprecated Deprecated in favor of {@link #comparatorFactoryClass()}. + * * @return {@link NullStrengthWeightFactory} when it is null (workaround for annotation limitation) * @see #strengthComparatorClass() */ + @Deprecated(forRemoval = true, since = "1.28.0") Class strengthWeightFactoryClass() default NullStrengthWeightFactory.class; - /** Workaround for annotation limitation in {@link #strengthWeightFactoryClass()}. */ - interface NullStrengthWeightFactory extends SelectionSorterWeightFactory { + /** + * Workaround for annotation limitation in {@link #strengthWeightFactoryClass()}. + * + * @deprecated Deprecated in favor of {@link NullComparatorFactory}. + */ + @Deprecated(forRemoval = true, since = "1.28.0") + interface NullStrengthWeightFactory + extends SelectionSorterWeightFactory, NullComparatorFactory { } - } diff --git a/core/src/main/java/ai/timefold/solver/core/config/constructionheuristic/ConstructionHeuristicType.java b/core/src/main/java/ai/timefold/solver/core/config/constructionheuristic/ConstructionHeuristicType.java index e1af4741ac..cda2ed6781 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/constructionheuristic/ConstructionHeuristicType.java +++ b/core/src/main/java/ai/timefold/solver/core/config/constructionheuristic/ConstructionHeuristicType.java @@ -5,8 +5,9 @@ import ai.timefold.solver.core.config.heuristic.selector.entity.EntitySorterManner; import ai.timefold.solver.core.config.heuristic.selector.value.ValueSorterManner; -import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullMarked; +@NullMarked @XmlEnum public enum ConstructionHeuristicType { /** @@ -56,52 +57,30 @@ public enum ConstructionHeuristicType { */ ALLOCATE_FROM_POOL; - public @NonNull EntitySorterManner getDefaultEntitySorterManner() { - switch (this) { - case FIRST_FIT: - case WEAKEST_FIT: - case STRONGEST_FIT: - return EntitySorterManner.NONE; - case FIRST_FIT_DECREASING: - case WEAKEST_FIT_DECREASING: - case STRONGEST_FIT_DECREASING: - return EntitySorterManner.DECREASING_DIFFICULTY; - case ALLOCATE_ENTITY_FROM_QUEUE: - case ALLOCATE_TO_VALUE_FROM_QUEUE: - case CHEAPEST_INSERTION: - case ALLOCATE_FROM_POOL: - return EntitySorterManner.DECREASING_DIFFICULTY_IF_AVAILABLE; - default: - throw new IllegalStateException("The constructionHeuristicType (" + this + ") is not implemented."); - } + public EntitySorterManner getDefaultEntitySorterManner() { + return switch (this) { + case FIRST_FIT, WEAKEST_FIT, STRONGEST_FIT -> EntitySorterManner.NONE; + case FIRST_FIT_DECREASING, WEAKEST_FIT_DECREASING, STRONGEST_FIT_DECREASING -> EntitySorterManner.DESCENDING; + case ALLOCATE_ENTITY_FROM_QUEUE, ALLOCATE_TO_VALUE_FROM_QUEUE, CHEAPEST_INSERTION, ALLOCATE_FROM_POOL -> + EntitySorterManner.DESCENDING_IF_AVAILABLE; + }; } - public @NonNull ValueSorterManner getDefaultValueSorterManner() { - switch (this) { - case FIRST_FIT: - case FIRST_FIT_DECREASING: - return ValueSorterManner.NONE; - case WEAKEST_FIT: - case WEAKEST_FIT_DECREASING: - return ValueSorterManner.INCREASING_STRENGTH; - case STRONGEST_FIT: - case STRONGEST_FIT_DECREASING: - return ValueSorterManner.DECREASING_STRENGTH; - case ALLOCATE_ENTITY_FROM_QUEUE: - case ALLOCATE_TO_VALUE_FROM_QUEUE: - case CHEAPEST_INSERTION: - case ALLOCATE_FROM_POOL: - return ValueSorterManner.INCREASING_STRENGTH_IF_AVAILABLE; - default: - throw new IllegalStateException("The constructionHeuristicType (" + this + ") is not implemented."); - } + public ValueSorterManner getDefaultValueSorterManner() { + return switch (this) { + case FIRST_FIT, FIRST_FIT_DECREASING -> ValueSorterManner.NONE; + case WEAKEST_FIT, WEAKEST_FIT_DECREASING -> ValueSorterManner.ASCENDING; + case STRONGEST_FIT, STRONGEST_FIT_DECREASING -> ValueSorterManner.DESCENDING; + case ALLOCATE_ENTITY_FROM_QUEUE, ALLOCATE_TO_VALUE_FROM_QUEUE, CHEAPEST_INSERTION, ALLOCATE_FROM_POOL -> + ValueSorterManner.ASCENDING_IF_AVAILABLE; + }; } /** * @return {@link ConstructionHeuristicType#values()} without duplicates (abstract types that end up behaving as one of the * other types). */ - public static @NonNull ConstructionHeuristicType @NonNull [] getBluePrintTypes() { + public static ConstructionHeuristicType[] getBluePrintTypes() { return new ConstructionHeuristicType[] { FIRST_FIT, FIRST_FIT_DECREASING, diff --git a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/entity/EntitySelectorConfig.java b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/entity/EntitySelectorConfig.java index 38a265b95f..5e8c52e316 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/entity/EntitySelectorConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/entity/EntitySelectorConfig.java @@ -7,6 +7,7 @@ import jakarta.xml.bind.annotation.XmlElement; import jakarta.xml.bind.annotation.XmlType; +import ai.timefold.solver.core.api.domain.common.ComparatorFactory; import ai.timefold.solver.core.api.domain.entity.PlanningEntity; import ai.timefold.solver.core.config.heuristic.selector.SelectorConfig; import ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType; @@ -20,7 +21,7 @@ import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter; import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory; -import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @XmlType(propOrder = { @@ -33,12 +34,15 @@ "filterClass", "sorterManner", "sorterComparatorClass", + "comparatorClass", "sorterWeightFactoryClass", + "comparatorFactoryClass", "sorterOrder", "sorterClass", "probabilityWeightFactoryClass", "selectedCountLimit" }) +@NullMarked public class EntitySelectorConfig extends SelectorConfig { public static EntitySelectorConfig newMimicSelectorConfig(String mimicSelectorRef) { @@ -46,29 +50,54 @@ public static EntitySelectorConfig newMimicSelectorConfig(String mimicSelectorRe .withMimicSelectorRef(mimicSelectorRef); } + @Nullable @XmlAttribute protected String id = null; @XmlAttribute + @Nullable protected String mimicSelectorRef = null; + @Nullable protected Class entityClass = null; - + @Nullable protected SelectionCacheType cacheType = null; + @Nullable protected SelectionOrder selectionOrder = null; + @Nullable @XmlElement(name = "nearbySelection") protected NearbySelectionConfig nearbySelectionConfig = null; + @Nullable protected Class filterClass = null; + @Nullable protected EntitySorterManner sorterManner = null; + /** + * @deprecated Deprecated in favor of {@link #comparatorClass}. + */ + @Deprecated(forRemoval = true, since = "1.28.0") + @Nullable protected Class sorterComparatorClass = null; + @Nullable + protected Class comparatorClass = null; + /** + * @deprecated Deprecated in favor of {@link #comparatorFactoryClass}. + */ + @Deprecated(forRemoval = true, since = "1.28.0") + @Nullable protected Class sorterWeightFactoryClass = null; + @Nullable + protected Class comparatorFactoryClass = null; + @Nullable protected SelectionSorterOrder sorterOrder = null; + @Nullable protected Class sorterClass = null; + @Nullable protected Class probabilityWeightFactoryClass = null; + @Nullable protected Long selectedCountLimit = null; public EntitySelectorConfig() { @@ -148,22 +177,55 @@ public void setSorterManner(@Nullable EntitySorterManner sorterManner) { this.sorterManner = sorterManner; } + /** + * @deprecated Deprecated in favor of {@link #getComparatorClass()} + */ + @Deprecated(forRemoval = true, since = "1.28.0") public @Nullable Class getSorterComparatorClass() { return sorterComparatorClass; } + /** + * @deprecated Deprecated in favor of {@link #setComparatorClass(Class)} + */ + @Deprecated(forRemoval = true, since = "1.28.0") public void setSorterComparatorClass(@Nullable Class sorterComparatorClass) { this.sorterComparatorClass = sorterComparatorClass; } + public @Nullable Class getComparatorClass() { + return comparatorClass; + } + + public void setComparatorClass(@Nullable Class comparatorClass) { + this.comparatorClass = comparatorClass; + } + + /** + * @deprecated Deprecated in favor of {@link #getComparatorFactoryClass()} + */ + @Deprecated(forRemoval = true, since = "1.28.0") public @Nullable Class getSorterWeightFactoryClass() { return sorterWeightFactoryClass; } + /** + * @deprecated Deprecated in favor of {@link #setComparatorFactoryClass(Class)} + * @param sorterWeightFactoryClass the class + */ + @Deprecated(forRemoval = true, since = "1.28.0") public void setSorterWeightFactoryClass(@Nullable Class sorterWeightFactoryClass) { this.sorterWeightFactoryClass = sorterWeightFactoryClass; } + public @Nullable Class getComparatorFactoryClass() { + return comparatorFactoryClass; + } + + public void setComparatorFactoryClass(@Nullable Class comparatorFactoryClass) { + this.comparatorFactoryClass = comparatorFactoryClass; + } + public @Nullable SelectionSorterOrder getSorterOrder() { return sorterOrder; } @@ -201,74 +263,94 @@ public void setSelectedCountLimit(@Nullable Long selectedCountLimit) { // With methods // ************************************************************************ - public @NonNull EntitySelectorConfig withId(@NonNull String id) { + public EntitySelectorConfig withId(String id) { this.setId(id); return this; } - public @NonNull EntitySelectorConfig withMimicSelectorRef(@NonNull String mimicSelectorRef) { + public EntitySelectorConfig withMimicSelectorRef(String mimicSelectorRef) { this.setMimicSelectorRef(mimicSelectorRef); return this; } - public @NonNull EntitySelectorConfig withEntityClass(@NonNull Class entityClass) { + public EntitySelectorConfig withEntityClass(Class entityClass) { this.setEntityClass(entityClass); return this; } - public @NonNull EntitySelectorConfig withCacheType(@NonNull SelectionCacheType cacheType) { + public EntitySelectorConfig withCacheType(SelectionCacheType cacheType) { this.setCacheType(cacheType); return this; } - public @NonNull EntitySelectorConfig withSelectionOrder(@NonNull SelectionOrder selectionOrder) { + public EntitySelectorConfig withSelectionOrder(SelectionOrder selectionOrder) { this.setSelectionOrder(selectionOrder); return this; } - public @NonNull EntitySelectorConfig withNearbySelectionConfig(@NonNull NearbySelectionConfig nearbySelectionConfig) { + public EntitySelectorConfig withNearbySelectionConfig(NearbySelectionConfig nearbySelectionConfig) { this.setNearbySelectionConfig(nearbySelectionConfig); return this; } - public @NonNull EntitySelectorConfig withFilterClass(@NonNull Class filterClass) { + public EntitySelectorConfig withFilterClass(Class filterClass) { this.setFilterClass(filterClass); return this; } - public @NonNull EntitySelectorConfig withSorterManner(@NonNull EntitySorterManner sorterManner) { + public EntitySelectorConfig withSorterManner(EntitySorterManner sorterManner) { this.setSorterManner(sorterManner); return this; } - public @NonNull EntitySelectorConfig withSorterComparatorClass(@NonNull Class comparatorClass) { + /** + * @deprecated Deprecated in favor of {@link #withComparatorClass(Class)} + */ + @Deprecated(forRemoval = true, since = "1.28.0") + public EntitySelectorConfig withSorterComparatorClass(Class comparatorClass) { this.setSorterComparatorClass(comparatorClass); return this; } - public @NonNull EntitySelectorConfig - withSorterWeightFactoryClass(@NonNull Class weightFactoryClass) { + public EntitySelectorConfig withComparatorClass(Class comparatorClass) { + this.setComparatorClass(comparatorClass); + return this; + } + + /** + * @deprecated Deprecated in favor of {@link #withComparatorFactoryClass(Class)} + * @param weightFactoryClass the factory class + */ + @Deprecated(forRemoval = true, since = "1.28.0") + public EntitySelectorConfig + withSorterWeightFactoryClass(Class weightFactoryClass) { this.setSorterWeightFactoryClass(weightFactoryClass); return this; } - public @NonNull EntitySelectorConfig withSorterOrder(@NonNull SelectionSorterOrder sorterOrder) { + public EntitySelectorConfig + withComparatorFactoryClass(Class comparatorFactoryClass) { + this.setComparatorFactoryClass(comparatorFactoryClass); + return this; + } + + public EntitySelectorConfig withSorterOrder(SelectionSorterOrder sorterOrder) { this.setSorterOrder(sorterOrder); return this; } - public @NonNull EntitySelectorConfig withSorterClass(@NonNull Class sorterClass) { + public EntitySelectorConfig withSorterClass(Class sorterClass) { this.setSorterClass(sorterClass); return this; } - public @NonNull EntitySelectorConfig - withProbabilityWeightFactoryClass(@NonNull Class factoryClass) { + public EntitySelectorConfig + withProbabilityWeightFactoryClass(Class factoryClass) { this.setProbabilityWeightFactoryClass(factoryClass); return this; } - public @NonNull EntitySelectorConfig withSelectedCountLimit(long selectedCountLimit) { + public EntitySelectorConfig withSelectedCountLimit(long selectedCountLimit) { this.setSelectedCountLimit(selectedCountLimit); return this; } @@ -278,7 +360,7 @@ public void setSelectedCountLimit(@Nullable Long selectedCountLimit) { // ************************************************************************ @Override - public @NonNull EntitySelectorConfig inherit(@NonNull EntitySelectorConfig inheritedConfig) { + public EntitySelectorConfig inherit(EntitySelectorConfig inheritedConfig) { id = ConfigUtils.inheritOverwritableProperty(id, inheritedConfig.getId()); mimicSelectorRef = ConfigUtils.inheritOverwritableProperty(mimicSelectorRef, inheritedConfig.getMimicSelectorRef()); @@ -293,8 +375,12 @@ public void setSelectedCountLimit(@Nullable Long selectedCountLimit) { sorterManner, inheritedConfig.getSorterManner()); sorterComparatorClass = ConfigUtils.inheritOverwritableProperty( sorterComparatorClass, inheritedConfig.getSorterComparatorClass()); + comparatorClass = ConfigUtils.inheritOverwritableProperty( + comparatorClass, inheritedConfig.getComparatorClass()); sorterWeightFactoryClass = ConfigUtils.inheritOverwritableProperty( sorterWeightFactoryClass, inheritedConfig.getSorterWeightFactoryClass()); + comparatorFactoryClass = ConfigUtils.inheritOverwritableProperty( + comparatorFactoryClass, inheritedConfig.getComparatorFactoryClass()); sorterOrder = ConfigUtils.inheritOverwritableProperty( sorterOrder, inheritedConfig.getSorterOrder()); sorterClass = ConfigUtils.inheritOverwritableProperty( @@ -307,19 +393,21 @@ public void setSelectedCountLimit(@Nullable Long selectedCountLimit) { } @Override - public @NonNull EntitySelectorConfig copyConfig() { + public EntitySelectorConfig copyConfig() { return new EntitySelectorConfig().inherit(this); } @Override - public void visitReferencedClasses(@NonNull Consumer> classVisitor) { + public void visitReferencedClasses(Consumer> classVisitor) { classVisitor.accept(entityClass); if (nearbySelectionConfig != null) { nearbySelectionConfig.visitReferencedClasses(classVisitor); } classVisitor.accept(filterClass); classVisitor.accept(sorterComparatorClass); + classVisitor.accept(comparatorClass); classVisitor.accept(sorterWeightFactoryClass); + classVisitor.accept(comparatorFactoryClass); classVisitor.accept(sorterClass); classVisitor.accept(probabilityWeightFactoryClass); } @@ -329,41 +417,31 @@ public String toString() { return getClass().getSimpleName() + "(" + entityClass + ")"; } - public static boolean hasSorter(@NonNull EntitySorterManner entitySorterManner, - @NonNull EntityDescriptor entityDescriptor) { - switch (entitySorterManner) { - case NONE: - return false; - case DECREASING_DIFFICULTY: - return true; - case DECREASING_DIFFICULTY_IF_AVAILABLE: - return entityDescriptor.getDecreasingDifficultySorter() != null; - default: - throw new IllegalStateException("The sorterManner (" - + entitySorterManner + ") is not implemented."); - } + public static boolean hasSorter(EntitySorterManner entitySorterManner, + EntityDescriptor entityDescriptor) { + return switch (entitySorterManner) { + case NONE -> false; + case DECREASING_DIFFICULTY, DESCENDING -> true; + case DECREASING_DIFFICULTY_IF_AVAILABLE, DESCENDING_IF_AVAILABLE -> + entityDescriptor.getDescendingSorter() != null; + }; } - public static @NonNull SelectionSorter determineSorter( - @NonNull EntitySorterManner entitySorterManner, @NonNull EntityDescriptor entityDescriptor) { - SelectionSorter sorter; - switch (entitySorterManner) { + public static SelectionSorter determineSorter( + EntitySorterManner entitySorterManner, EntityDescriptor entityDescriptor) { + return switch (entitySorterManner) { case NONE: throw new IllegalStateException("Impossible state: hasSorter() should have returned null."); - case DECREASING_DIFFICULTY: - case DECREASING_DIFFICULTY_IF_AVAILABLE: - sorter = (SelectionSorter) entityDescriptor.getDecreasingDifficultySorter(); + case DECREASING_DIFFICULTY, DECREASING_DIFFICULTY_IF_AVAILABLE, DESCENDING, DESCENDING_IF_AVAILABLE: + var sorter = (SelectionSorter) entityDescriptor.getDescendingSorter(); if (sorter == null) { - throw new IllegalArgumentException("The sorterManner (" + entitySorterManner - + ") on entity class (" + entityDescriptor.getEntityClass() - + ") fails because that entity class's @" + PlanningEntity.class.getSimpleName() - + " annotation does not declare any difficulty comparison."); + throw new IllegalArgumentException( + "The sorterManner (%s) on entity class (%s) fails because that entity class's @%s annotation does not declare any difficulty comparison." + .formatted(entitySorterManner, entityDescriptor.getEntityClass(), + PlanningEntity.class.getSimpleName())); } - return sorter; - default: - throw new IllegalStateException("The sorterManner (" - + entitySorterManner + ") is not implemented."); - } + yield sorter; + }; } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/entity/EntitySorterManner.java b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/entity/EntitySorterManner.java index 02762b65ee..7a54571e9c 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/entity/EntitySorterManner.java +++ b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/entity/EntitySorterManner.java @@ -11,6 +11,16 @@ @XmlEnum public enum EntitySorterManner { NONE, + /** + * @deprecated use {@link #DESCENDING} instead + */ + @Deprecated(forRemoval = true, since = "1.28.0") DECREASING_DIFFICULTY, - DECREASING_DIFFICULTY_IF_AVAILABLE; + /** + * @deprecated use {@link #DESCENDING_IF_AVAILABLE} instead + */ + @Deprecated(forRemoval = true, since = "1.28.0") + DECREASING_DIFFICULTY_IF_AVAILABLE, + DESCENDING, + DESCENDING_IF_AVAILABLE } diff --git a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/MoveSelectorConfig.java b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/MoveSelectorConfig.java index 6d9f7df4f9..291bca6620 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/MoveSelectorConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/move/MoveSelectorConfig.java @@ -7,6 +7,7 @@ import jakarta.xml.bind.annotation.XmlSeeAlso; import jakarta.xml.bind.annotation.XmlType; +import ai.timefold.solver.core.api.domain.common.ComparatorFactory; import ai.timefold.solver.core.config.heuristic.selector.SelectorConfig; import ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType; import ai.timefold.solver.core.config.heuristic.selector.common.SelectionOrder; @@ -35,7 +36,7 @@ import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter; import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory; -import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; /** @@ -67,29 +68,54 @@ "selectionOrder", "filterClass", "sorterComparatorClass", + "comparatorClass", "sorterWeightFactoryClass", + "comparatorFactoryClass", "sorterOrder", "sorterClass", "probabilityWeightFactoryClass", "selectedCountLimit", "fixedProbabilityWeight" }) +@NullMarked public abstract class MoveSelectorConfig> extends SelectorConfig { + @Nullable protected SelectionCacheType cacheType = null; + @Nullable protected SelectionOrder selectionOrder = null; + @Nullable protected Class filterClass = null; + /** + * @deprecated Deprecated in favor of {@link #comparatorClass}. + */ + @Deprecated(forRemoval = true, since = "1.28.0") + @Nullable protected Class sorterComparatorClass = null; + @Nullable + protected Class comparatorClass = null; + /** + * @deprecated Deprecated in favor of {@link #comparatorFactoryClass}. + */ + @Deprecated(forRemoval = true, since = "1.28.0") + @Nullable protected Class sorterWeightFactoryClass = null; + @Nullable + protected Class comparatorFactoryClass = null; + @Nullable protected SelectionSorterOrder sorterOrder = null; + @Nullable protected Class sorterClass = null; + @Nullable protected Class probabilityWeightFactoryClass = null; + @Nullable protected Long selectedCountLimit = null; + @Nullable private Double fixedProbabilityWeight = null; // ************************************************************************ @@ -120,22 +146,55 @@ public void setFilterClass(@Nullable Class filterClas this.filterClass = filterClass; } + /** + * @deprecated Deprecated in favor of {@link #getComparatorClass()} + */ + @Deprecated(forRemoval = true, since = "1.28.0") public @Nullable Class getSorterComparatorClass() { return sorterComparatorClass; } + /** + * @deprecated Deprecated in favor of {@link #setComparatorClass(Class)} + */ + @Deprecated(forRemoval = true, since = "1.28.0") public void setSorterComparatorClass(@Nullable Class sorterComparatorClass) { this.sorterComparatorClass = sorterComparatorClass; } + public @Nullable Class getComparatorClass() { + return comparatorClass; + } + + public void setComparatorClass(@Nullable Class comparatorClass) { + this.comparatorClass = comparatorClass; + } + + /** + * @deprecated Deprecated in favor of {@link #getComparatorFactoryClass()} + */ + @Deprecated(forRemoval = true, since = "1.28.0") public @Nullable Class getSorterWeightFactoryClass() { return sorterWeightFactoryClass; } + /** + * @deprecated Deprecated in favor of {@link #setComparatorFactoryClass(Class)} + * @param sorterWeightFactoryClass the class + */ + @Deprecated(forRemoval = true, since = "1.28.0") public void setSorterWeightFactoryClass(@Nullable Class sorterWeightFactoryClass) { this.sorterWeightFactoryClass = sorterWeightFactoryClass; } + public @Nullable Class getComparatorFactoryClass() { + return comparatorFactoryClass; + } + + public void setComparatorFactoryClass(@Nullable Class comparatorFactoryClass) { + this.comparatorFactoryClass = comparatorFactoryClass; + } + public @Nullable SelectionSorterOrder getSorterOrder() { return sorterOrder; } @@ -181,54 +240,74 @@ public void setFixedProbabilityWeight(@Nullable Double fixedProbabilityWeight) { // With methods // ************************************************************************ - public @NonNull Config_ withCacheType(@NonNull SelectionCacheType cacheType) { + public Config_ withCacheType(SelectionCacheType cacheType) { this.cacheType = cacheType; return (Config_) this; } - public @NonNull Config_ withSelectionOrder(@NonNull SelectionOrder selectionOrder) { + public Config_ withSelectionOrder(SelectionOrder selectionOrder) { this.selectionOrder = selectionOrder; return (Config_) this; } - public @NonNull Config_ withFilterClass(@NonNull Class filterClass) { + public Config_ withFilterClass(Class filterClass) { this.filterClass = filterClass; return (Config_) this; } - public @NonNull Config_ withSorterComparatorClass(@NonNull Class sorterComparatorClass) { + /** + * @deprecated Deprecated in favor of {@link #withComparatorClass(Class)} + */ + @Deprecated(forRemoval = true, since = "1.28.0") + public Config_ withSorterComparatorClass(Class sorterComparatorClass) { this.sorterComparatorClass = sorterComparatorClass; return (Config_) this; } - public @NonNull Config_ withSorterWeightFactoryClass( - @NonNull Class sorterWeightFactoryClass) { + public Config_ withComparatorClass(Class comparatorClass) { + this.setComparatorClass(comparatorClass); + return (Config_) this; + } + + /** + * @deprecated Deprecated in favor of {@link #withComparatorFactoryClass(Class)} + * @param sorterWeightFactoryClass the factory class + */ + @Deprecated(forRemoval = true, since = "1.28.0") + public Config_ withSorterWeightFactoryClass( + Class sorterWeightFactoryClass) { this.sorterWeightFactoryClass = sorterWeightFactoryClass; return (Config_) this; } - public @NonNull Config_ withSorterOrder(@NonNull SelectionSorterOrder sorterOrder) { + public Config_ + withComparatorFactoryClass(Class comparatorFactoryClass) { + this.setComparatorFactoryClass(comparatorFactoryClass); + return (Config_) this; + } + + public Config_ withSorterOrder(SelectionSorterOrder sorterOrder) { this.sorterOrder = sorterOrder; return (Config_) this; } - public @NonNull Config_ withSorterClass(@NonNull Class sorterClass) { + public Config_ withSorterClass(Class sorterClass) { this.sorterClass = sorterClass; return (Config_) this; } - public @NonNull Config_ withProbabilityWeightFactoryClass( - @NonNull Class probabilityWeightFactoryClass) { + public Config_ withProbabilityWeightFactoryClass( + Class probabilityWeightFactoryClass) { this.probabilityWeightFactoryClass = probabilityWeightFactoryClass; return (Config_) this; } - public @NonNull Config_ withSelectedCountLimit(@NonNull Long selectedCountLimit) { + public Config_ withSelectedCountLimit(Long selectedCountLimit) { this.selectedCountLimit = selectedCountLimit; return (Config_) this; } - public @NonNull Config_ withFixedProbabilityWeight(@NonNull Double fixedProbabilityWeight) { + public Config_ withFixedProbabilityWeight(Double fixedProbabilityWeight) { this.fixedProbabilityWeight = fixedProbabilityWeight; return (Config_) this; } @@ -238,12 +317,12 @@ public void setFixedProbabilityWeight(@Nullable Double fixedProbabilityWeight) { * except for {@link UnionMoveSelectorConfig} and {@link CartesianProductMoveSelectorConfig}. * */ - public void extractLeafMoveSelectorConfigsIntoList(@NonNull List<@NonNull MoveSelectorConfig> leafMoveSelectorConfigList) { + public void extractLeafMoveSelectorConfigsIntoList(List leafMoveSelectorConfigList) { leafMoveSelectorConfigList.add(this); } @Override - public @NonNull Config_ inherit(@NonNull Config_ inheritedConfig) { + public Config_ inherit(Config_ inheritedConfig) { inheritCommon(inheritedConfig); return (Config_) this; } @@ -251,14 +330,16 @@ public void extractLeafMoveSelectorConfigsIntoList(@NonNull List<@NonNull MoveSe /** * Does not inherit subclass properties because this class and {@code foldedConfig} can be of a different type. */ - public void inheritFolded(@NonNull MoveSelectorConfig foldedConfig) { + public void inheritFolded(MoveSelectorConfig foldedConfig) { inheritCommon(foldedConfig); } - protected void visitCommonReferencedClasses(@NonNull Consumer> classVisitor) { + protected void visitCommonReferencedClasses(Consumer> classVisitor) { classVisitor.accept(filterClass); classVisitor.accept(sorterComparatorClass); + classVisitor.accept(comparatorClass); classVisitor.accept(sorterWeightFactoryClass); + classVisitor.accept(comparatorFactoryClass); classVisitor.accept(sorterClass); classVisitor.accept(probabilityWeightFactoryClass); } @@ -269,8 +350,12 @@ private void inheritCommon(MoveSelectorConfig inheritedConfig) { filterClass = ConfigUtils.inheritOverwritableProperty(filterClass, inheritedConfig.getFilterClass()); sorterComparatorClass = ConfigUtils.inheritOverwritableProperty( sorterComparatorClass, inheritedConfig.getSorterComparatorClass()); + comparatorClass = ConfigUtils.inheritOverwritableProperty( + comparatorClass, inheritedConfig.getComparatorClass()); sorterWeightFactoryClass = ConfigUtils.inheritOverwritableProperty( sorterWeightFactoryClass, inheritedConfig.getSorterWeightFactoryClass()); + comparatorFactoryClass = ConfigUtils.inheritOverwritableProperty( + comparatorFactoryClass, inheritedConfig.getComparatorFactoryClass()); sorterOrder = ConfigUtils.inheritOverwritableProperty( sorterOrder, inheritedConfig.getSorterOrder()); sorterClass = ConfigUtils.inheritOverwritableProperty( diff --git a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/value/ValueSelectorConfig.java b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/value/ValueSelectorConfig.java index a7b41b3538..0477d37d57 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/value/ValueSelectorConfig.java +++ b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/value/ValueSelectorConfig.java @@ -7,6 +7,7 @@ import jakarta.xml.bind.annotation.XmlElement; import jakarta.xml.bind.annotation.XmlType; +import ai.timefold.solver.core.api.domain.common.ComparatorFactory; import ai.timefold.solver.core.api.domain.variable.PlanningVariable; import ai.timefold.solver.core.config.heuristic.selector.SelectorConfig; import ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType; @@ -20,7 +21,7 @@ import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter; import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory; -import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @XmlType(propOrder = { @@ -34,45 +35,75 @@ "filterClass", "sorterManner", "sorterComparatorClass", + "comparatorClass", "sorterWeightFactoryClass", + "comparatorFactoryClass", "sorterOrder", "sorterClass", "probabilityWeightFactoryClass", "selectedCountLimit" }) +@NullMarked public class ValueSelectorConfig extends SelectorConfig { @XmlAttribute + @Nullable protected String id = null; @XmlAttribute + @Nullable protected String mimicSelectorRef = null; + @Nullable protected Class downcastEntityClass = null; @XmlAttribute + @Nullable protected String variableName = null; + @Nullable protected SelectionCacheType cacheType = null; + @Nullable protected SelectionOrder selectionOrder = null; @XmlElement(name = "nearbySelection") + @Nullable protected NearbySelectionConfig nearbySelectionConfig = null; + @Nullable protected Class filterClass = null; + @Nullable protected ValueSorterManner sorterManner = null; + /** + * @deprecated Deprecated in favor of {@link #comparatorClass}. + */ + @Deprecated(forRemoval = true, since = "1.28.0") + @Nullable protected Class sorterComparatorClass = null; + @Nullable + protected Class comparatorClass = null; + /** + * @deprecated Deprecated in favor of {@link #comparatorFactoryClass}. + */ + @Deprecated(forRemoval = true, since = "1.28.0") + @Nullable protected Class sorterWeightFactoryClass = null; + @Nullable + protected Class comparatorFactoryClass = null; + @Nullable protected SelectionSorterOrder sorterOrder = null; + @Nullable protected Class sorterClass = null; + @Nullable protected Class probabilityWeightFactoryClass = null; + @Nullable protected Long selectedCountLimit = null; public ValueSelectorConfig() { } - public ValueSelectorConfig(@NonNull String variableName) { + public ValueSelectorConfig(String variableName) { this.variableName = variableName; } @@ -154,22 +185,55 @@ public void setSorterManner(@Nullable ValueSorterManner sorterManner) { this.sorterManner = sorterManner; } + /** + * @deprecated Deprecated in favor of {@link #getComparatorClass()} + */ + @Deprecated(forRemoval = true, since = "1.28.0") public @Nullable Class getSorterComparatorClass() { return sorterComparatorClass; } + /** + * @deprecated Deprecated in favor of {@link #setComparatorClass(Class)} + */ + @Deprecated(forRemoval = true, since = "1.28.0") public void setSorterComparatorClass(@Nullable Class sorterComparatorClass) { this.sorterComparatorClass = sorterComparatorClass; } + public @Nullable Class getComparatorClass() { + return comparatorClass; + } + + public void setComparatorClass(@Nullable Class comparatorClass) { + this.comparatorClass = comparatorClass; + } + + /** + * @deprecated Deprecated in favor of {@link #getComparatorFactoryClass()} + */ + @Deprecated(forRemoval = true, since = "1.28.0") public @Nullable Class getSorterWeightFactoryClass() { return sorterWeightFactoryClass; } + /** + * @deprecated Deprecated in favor of {@link #setComparatorFactoryClass(Class)} + * @param sorterWeightFactoryClass the class + */ + @Deprecated(forRemoval = true, since = "1.28.0") public void setSorterWeightFactoryClass(@Nullable Class sorterWeightFactoryClass) { this.sorterWeightFactoryClass = sorterWeightFactoryClass; } + public @Nullable Class getComparatorFactoryClass() { + return comparatorFactoryClass; + } + + public void setComparatorFactoryClass(@Nullable Class comparatorFactoryClass) { + this.comparatorFactoryClass = comparatorFactoryClass; + } + public @Nullable SelectionSorterOrder getSorterOrder() { return sorterOrder; } @@ -207,79 +271,99 @@ public void setSelectedCountLimit(@Nullable Long selectedCountLimit) { // With methods // ************************************************************************ - public @NonNull ValueSelectorConfig withId(@NonNull String id) { + public ValueSelectorConfig withId(String id) { this.setId(id); return this; } - public @NonNull ValueSelectorConfig withMimicSelectorRef(@NonNull String mimicSelectorRef) { + public ValueSelectorConfig withMimicSelectorRef(String mimicSelectorRef) { this.setMimicSelectorRef(mimicSelectorRef); return this; } - public @NonNull ValueSelectorConfig withDowncastEntityClass(@NonNull Class entityClass) { + public ValueSelectorConfig withDowncastEntityClass(Class entityClass) { this.setDowncastEntityClass(entityClass); return this; } - public @NonNull ValueSelectorConfig withVariableName(@NonNull String variableName) { + public ValueSelectorConfig withVariableName(String variableName) { this.setVariableName(variableName); return this; } - public @NonNull ValueSelectorConfig withCacheType(@NonNull SelectionCacheType cacheType) { + public ValueSelectorConfig withCacheType(SelectionCacheType cacheType) { this.setCacheType(cacheType); return this; } - public @NonNull ValueSelectorConfig withSelectionOrder(@NonNull SelectionOrder selectionOrder) { + public ValueSelectorConfig withSelectionOrder(SelectionOrder selectionOrder) { this.setSelectionOrder(selectionOrder); return this; } - public @NonNull ValueSelectorConfig withNearbySelectionConfig(@NonNull NearbySelectionConfig nearbySelectionConfig) { + public ValueSelectorConfig withNearbySelectionConfig(NearbySelectionConfig nearbySelectionConfig) { this.setNearbySelectionConfig(nearbySelectionConfig); return this; } - public @NonNull ValueSelectorConfig withFilterClass(@NonNull Class filterClass) { + public ValueSelectorConfig withFilterClass(Class filterClass) { this.setFilterClass(filterClass); return this; } - public @NonNull ValueSelectorConfig withSorterManner(@NonNull ValueSorterManner sorterManner) { + public ValueSelectorConfig withSorterManner(ValueSorterManner sorterManner) { this.setSorterManner(sorterManner); return this; } - public @NonNull ValueSelectorConfig withSorterComparatorClass(@NonNull Class comparatorClass) { + /** + * @deprecated Deprecated in favor of {@link #withComparatorClass(Class)} + */ + @Deprecated(forRemoval = true, since = "1.28.0") + public ValueSelectorConfig withSorterComparatorClass(Class comparatorClass) { this.setSorterComparatorClass(comparatorClass); return this; } - public @NonNull ValueSelectorConfig - withSorterWeightFactoryClass(@NonNull Class weightFactoryClass) { + public ValueSelectorConfig withComparatorClass(Class comparatorClass) { + this.setComparatorClass(comparatorClass); + return this; + } + + /** + * @deprecated Deprecated in favor of {@link #withComparatorFactoryClass(Class)} + * @param weightFactoryClass the factory class + */ + @Deprecated(forRemoval = true, since = "1.28.0") + public ValueSelectorConfig + withSorterWeightFactoryClass(Class weightFactoryClass) { this.setSorterWeightFactoryClass(weightFactoryClass); return this; } - public @NonNull ValueSelectorConfig withSorterOrder(@NonNull SelectionSorterOrder sorterOrder) { + public ValueSelectorConfig + withComparatorFactoryClass(Class comparatorFactoryClass) { + this.setComparatorFactoryClass(comparatorFactoryClass); + return this; + } + + public ValueSelectorConfig withSorterOrder(SelectionSorterOrder sorterOrder) { this.setSorterOrder(sorterOrder); return this; } - public @NonNull ValueSelectorConfig withSorterClass(@NonNull Class sorterClass) { + public ValueSelectorConfig withSorterClass(Class sorterClass) { this.setSorterClass(sorterClass); return this; } - public @NonNull ValueSelectorConfig - withProbabilityWeightFactoryClass(@NonNull Class factoryClass) { + public ValueSelectorConfig + withProbabilityWeightFactoryClass(Class factoryClass) { this.setProbabilityWeightFactoryClass(factoryClass); return this; } - public @NonNull ValueSelectorConfig withSelectedCountLimit(long selectedCountLimit) { + public ValueSelectorConfig withSelectedCountLimit(long selectedCountLimit) { this.setSelectedCountLimit(selectedCountLimit); return this; } @@ -289,7 +373,7 @@ public void setSelectedCountLimit(@Nullable Long selectedCountLimit) { // ************************************************************************ @Override - public @NonNull ValueSelectorConfig inherit(@NonNull ValueSelectorConfig inheritedConfig) { + public ValueSelectorConfig inherit(ValueSelectorConfig inheritedConfig) { id = ConfigUtils.inheritOverwritableProperty(id, inheritedConfig.getId()); mimicSelectorRef = ConfigUtils.inheritOverwritableProperty(mimicSelectorRef, inheritedConfig.getMimicSelectorRef()); @@ -304,8 +388,12 @@ public void setSelectedCountLimit(@Nullable Long selectedCountLimit) { sorterManner, inheritedConfig.getSorterManner()); sorterComparatorClass = ConfigUtils.inheritOverwritableProperty( sorterComparatorClass, inheritedConfig.getSorterComparatorClass()); + comparatorClass = ConfigUtils.inheritOverwritableProperty( + comparatorClass, inheritedConfig.getComparatorClass()); sorterWeightFactoryClass = ConfigUtils.inheritOverwritableProperty( sorterWeightFactoryClass, inheritedConfig.getSorterWeightFactoryClass()); + comparatorFactoryClass = ConfigUtils.inheritOverwritableProperty( + comparatorFactoryClass, inheritedConfig.getComparatorFactoryClass()); sorterOrder = ConfigUtils.inheritOverwritableProperty( sorterOrder, inheritedConfig.getSorterOrder()); sorterClass = ConfigUtils.inheritOverwritableProperty( @@ -318,19 +406,21 @@ public void setSelectedCountLimit(@Nullable Long selectedCountLimit) { } @Override - public @NonNull ValueSelectorConfig copyConfig() { + public ValueSelectorConfig copyConfig() { return new ValueSelectorConfig().inherit(this); } @Override - public void visitReferencedClasses(@NonNull Consumer> classVisitor) { + public void visitReferencedClasses(Consumer> classVisitor) { classVisitor.accept(downcastEntityClass); if (nearbySelectionConfig != null) { nearbySelectionConfig.visitReferencedClasses(classVisitor); } classVisitor.accept(filterClass); classVisitor.accept(sorterComparatorClass); + classVisitor.accept(comparatorClass); classVisitor.accept(sorterWeightFactoryClass); + classVisitor.accept(comparatorFactoryClass); classVisitor.accept(sorterClass); classVisitor.accept(probabilityWeightFactoryClass); } @@ -340,48 +430,32 @@ public String toString() { return getClass().getSimpleName() + "(" + variableName + ")"; } - public static boolean hasSorter(@NonNull ValueSorterManner valueSorterManner, - @NonNull GenuineVariableDescriptor variableDescriptor) { - switch (valueSorterManner) { - case NONE: - return false; - case INCREASING_STRENGTH: - case DECREASING_STRENGTH: - return true; - case INCREASING_STRENGTH_IF_AVAILABLE: - return variableDescriptor.getIncreasingStrengthSorter() != null; - case DECREASING_STRENGTH_IF_AVAILABLE: - return variableDescriptor.getDecreasingStrengthSorter() != null; - default: - throw new IllegalStateException("The sorterManner (" - + valueSorterManner + ") is not implemented."); - } - } - - public static @NonNull SelectionSorter determineSorter( - @NonNull ValueSorterManner valueSorterManner, @NonNull GenuineVariableDescriptor variableDescriptor) { - SelectionSorter sorter; - switch (valueSorterManner) { - case NONE: - throw new IllegalStateException("Impossible state: hasSorter() should have returned null."); - case INCREASING_STRENGTH: - case INCREASING_STRENGTH_IF_AVAILABLE: - sorter = variableDescriptor.getIncreasingStrengthSorter(); - break; - case DECREASING_STRENGTH: - case DECREASING_STRENGTH_IF_AVAILABLE: - sorter = variableDescriptor.getDecreasingStrengthSorter(); - break; - default: - throw new IllegalStateException("The sorterManner (" - + valueSorterManner + ") is not implemented."); - } + public static boolean hasSorter(ValueSorterManner valueSorterManner, + GenuineVariableDescriptor variableDescriptor) { + return switch (valueSorterManner) { + case NONE -> false; + case INCREASING_STRENGTH, DECREASING_STRENGTH, ASCENDING, DESCENDING -> true; + case INCREASING_STRENGTH_IF_AVAILABLE, ASCENDING_IF_AVAILABLE -> + variableDescriptor.getAscendingSorter() != null; + case DECREASING_STRENGTH_IF_AVAILABLE, DESCENDING_IF_AVAILABLE -> + variableDescriptor.getDescendingSorter() != null; + }; + } + + public static SelectionSorter determineSorter( + ValueSorterManner valueSorterManner, GenuineVariableDescriptor variableDescriptor) { + SelectionSorter sorter = switch (valueSorterManner) { + case NONE -> throw new IllegalStateException("Impossible state: hasSorter() should have returned null."); + case INCREASING_STRENGTH, INCREASING_STRENGTH_IF_AVAILABLE, ASCENDING, ASCENDING_IF_AVAILABLE -> + variableDescriptor.getAscendingSorter(); + case DECREASING_STRENGTH, DECREASING_STRENGTH_IF_AVAILABLE, DESCENDING, DESCENDING_IF_AVAILABLE -> + variableDescriptor.getDescendingSorter(); + }; if (sorter == null) { - throw new IllegalArgumentException("The sorterManner (" + valueSorterManner - + ") on entity class (" + variableDescriptor.getEntityDescriptor().getEntityClass() - + ")'s variable (" + variableDescriptor.getVariableName() - + ") fails because that variable getter's @" + PlanningVariable.class.getSimpleName() - + " annotation does not declare any strength comparison."); + throw new IllegalArgumentException( + "The sorterManner (%s) on entity class (%s)'s variable (%s) fails because that variable getter's @%s annotation does not declare any strength comparison." + .formatted(valueSorterManner, variableDescriptor.getEntityDescriptor().getEntityClass(), + variableDescriptor.getVariableName(), PlanningVariable.class.getSimpleName())); } return sorter; } diff --git a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/value/ValueSorterManner.java b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/value/ValueSorterManner.java index dfe5d79918..7702d36b74 100644 --- a/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/value/ValueSorterManner.java +++ b/core/src/main/java/ai/timefold/solver/core/config/heuristic/selector/value/ValueSorterManner.java @@ -11,10 +11,30 @@ @XmlEnum public enum ValueSorterManner { NONE(true), + /** + * @deprecated use {@link #ASCENDING} instead + */ + @Deprecated(forRemoval = true, since = "1.28.0") INCREASING_STRENGTH(false), + /** + * @deprecated use {@link #ASCENDING_IF_AVAILABLE} instead + */ + @Deprecated(forRemoval = true, since = "1.28.0") INCREASING_STRENGTH_IF_AVAILABLE(true), + /** + * @deprecated use {@link #DESCENDING} instead + */ + @Deprecated(forRemoval = true, since = "1.28.0") DECREASING_STRENGTH(false), - DECREASING_STRENGTH_IF_AVAILABLE(true); + /** + * @deprecated use {@link #DESCENDING_IF_AVAILABLE} instead + */ + @Deprecated(forRemoval = true, since = "1.28.0") + DECREASING_STRENGTH_IF_AVAILABLE(true), + ASCENDING(false), + ASCENDING_IF_AVAILABLE(true), + DESCENDING(false), + DESCENDING_IF_AVAILABLE(true); private final boolean nonePossible; diff --git a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseFactory.java index f14e513706..c125cb8214 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseFactory.java @@ -116,8 +116,6 @@ private EntityPlacerConfig buildDefaultEntityPlacerConfig(HeuristicConfigPoli if (listVariableDescriptor == null) { return Optional.empty(); } - failIfConfigured(phaseConfig.getConstructionHeuristicType(), "constructionHeuristicType"); - failIfConfigured(phaseConfig.getMoveSelectorConfigList(), "moveSelectorConfigList"); // When an entity has both list and basic variables, // the CH configuration will require two separate placers to initialize each variable, // which cannot be deduced automatically by default, since a single placer would be returned @@ -130,13 +128,6 @@ The entity (%s) has both basic and list variables and cannot be deduced automati return Optional.of(listVariableDescriptor); } - private static void failIfConfigured(Object configValue, String configName) { - if (configValue != null) { - throw new IllegalArgumentException("Construction Heuristic phase with a list variable does not support " - + configName + " configuration. Remove the " + configName + " (" + configValue + ") from the config."); - } - } - @SuppressWarnings("rawtypes") public static EntityPlacerConfig buildListVariableQueuedValuePlacerConfig(HeuristicConfigPolicy configPolicy, ListVariableDescriptor variableDescriptor) { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java index b49e018d32..48df189078 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/entity/descriptor/EntityDescriptor.java @@ -63,9 +63,9 @@ import ai.timefold.solver.core.impl.domain.variable.inverserelation.InverseRelationShadowVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.nextprev.NextElementShadowVariableDescriptor; import ai.timefold.solver.core.impl.domain.variable.nextprev.PreviousElementShadowVariableDescriptor; +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.ComparatorFactorySelectionSorter; import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.ComparatorSelectionSorter; import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter; -import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.WeightFactorySelectionSorter; import ai.timefold.solver.core.impl.move.director.MoveDirector; import ai.timefold.solver.core.impl.neighborhood.maybeapi.stream.enumerating.function.UniEnumeratingFilter; import ai.timefold.solver.core.impl.util.CollectionUtils; @@ -111,7 +111,7 @@ public class EntityDescriptor { // Only declared movable filter, excludes inherited and descending movable filters private MovableFilter declaredMovableEntityFilter; - private SelectionSorter decreasingDifficultySorter; + private SelectionSorter descendingSorter; // Only declared variable descriptors, excludes inherited variable descriptors private Map> declaredGenuineVariableDescriptorMap; @@ -243,7 +243,7 @@ private void processEntityAnnotations() { () -> new IllegalStateException("Impossible state as the previous if block would fail first.")); } processMovable(entityAnnotation); - processDifficulty(entityAnnotation); + processSorting(entityAnnotation); } /** @@ -263,35 +263,67 @@ private void processMovable(PlanningEntity entityAnnotation) { } } - private void processDifficulty(PlanningEntity entityAnnotation) { + private void processSorting(PlanningEntity entityAnnotation) { if (entityAnnotation == null) { return; } var difficultyComparatorClass = entityAnnotation.difficultyComparatorClass(); - if (difficultyComparatorClass == PlanningEntity.NullDifficultyComparator.class) { + if (difficultyComparatorClass != null + && PlanningEntity.NullComparator.class.isAssignableFrom(difficultyComparatorClass)) { difficultyComparatorClass = null; } + var comparatorClass = entityAnnotation.comparatorClass(); + if (comparatorClass != null + && PlanningEntity.NullComparator.class.isAssignableFrom(comparatorClass)) { + comparatorClass = null; + } + if (difficultyComparatorClass != null && comparatorClass != null) { + throw new IllegalStateException( + "The entityClass (%s) cannot have a %s (%s) and a %s (%s) at the same time.".formatted(getEntityClass(), + "difficultyComparatorClass", difficultyComparatorClass.getName(), "comparatorClass", + comparatorClass.getName())); + } var difficultyWeightFactoryClass = entityAnnotation.difficultyWeightFactoryClass(); - if (difficultyWeightFactoryClass == PlanningEntity.NullDifficultyWeightFactory.class) { + if (difficultyWeightFactoryClass != null + && PlanningEntity.NullComparatorFactory.class.isAssignableFrom(difficultyWeightFactoryClass)) { difficultyWeightFactoryClass = null; } - if (difficultyComparatorClass != null && difficultyWeightFactoryClass != null) { - throw new IllegalStateException( - "The entityClass (%s) cannot have a difficultyComparatorClass (%s) and a difficultyWeightFactoryClass (%s) at the same time." - .formatted(entityClass, difficultyComparatorClass.getName(), - difficultyWeightFactoryClass.getName())); + var comparatorFactoryClass = entityAnnotation.comparatorFactoryClass(); + if (comparatorFactoryClass != null + && PlanningEntity.NullComparatorFactory.class.isAssignableFrom(comparatorFactoryClass)) { + comparatorFactoryClass = null; } + if (difficultyWeightFactoryClass != null && comparatorFactoryClass != null) { + throw new IllegalStateException( + "The entityClass (%s) cannot have a %s (%s) and a %s (%s) at the same time.".formatted(getEntityClass(), + "difficultyWeightFactoryClass", difficultyWeightFactoryClass.getName(), "comparatorFactoryClass", + comparatorFactoryClass.getName())); + } + // Selected settings + var selectedComparatorPropertyName = "comparatorClass"; + var selectedComparatorClass = comparatorClass; + var selectedComparatorFactoryPropertyName = "comparatorFactoryClass"; + var selectedComparatorFactoryClass = comparatorFactoryClass; if (difficultyComparatorClass != null) { - var difficultyComparator = ConfigUtils.newInstance(this::toString, - "difficultyComparatorClass", difficultyComparatorClass); - decreasingDifficultySorter = new ComparatorSelectionSorter<>( - difficultyComparator, SelectionSorterOrder.DESCENDING); + selectedComparatorPropertyName = "difficultyComparatorClass"; + selectedComparatorClass = difficultyComparatorClass; } if (difficultyWeightFactoryClass != null) { - var difficultyWeightFactory = ConfigUtils.newInstance(this::toString, - "difficultyWeightFactoryClass", difficultyWeightFactoryClass); - decreasingDifficultySorter = new WeightFactorySelectionSorter<>( - difficultyWeightFactory, SelectionSorterOrder.DESCENDING); + selectedComparatorFactoryPropertyName = "difficultyWeightFactoryClass"; + selectedComparatorFactoryClass = difficultyWeightFactoryClass; + } + if (selectedComparatorClass != null && selectedComparatorFactoryClass != null) { + throw new IllegalStateException("The entityClass (%s) cannot have a %s (%s) and a %s (%s) at the same time." + .formatted(entityClass, selectedComparatorPropertyName, selectedComparatorClass.getName(), + selectedComparatorFactoryPropertyName, selectedComparatorFactoryClass.getName())); + } + if (selectedComparatorClass != null) { + var comparator = ConfigUtils.newInstance(this::toString, selectedComparatorPropertyName, selectedComparatorClass); + descendingSorter = new ComparatorSelectionSorter<>(comparator, SelectionSorterOrder.DESCENDING); + } else if (selectedComparatorFactoryClass != null) { + var comparator = ConfigUtils.newInstance(this::toString, selectedComparatorFactoryPropertyName, + selectedComparatorFactoryClass); + descendingSorter = new ComparatorFactorySelectionSorter<>(comparator, SelectionSorterOrder.DESCENDING); } } @@ -643,8 +675,8 @@ public UniEnumeratingFilter getEntityMovablePredicate() { return (UniEnumeratingFilter) entityMovablePredicate; } - public SelectionSorter getDecreasingDifficultySorter() { - return decreasingDifficultySorter; + public SelectionSorter getDescendingSorter() { + return descendingSorter; } public Collection getGenuineVariableNameSet() { diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/BasicVariableDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/BasicVariableDescriptor.java index 8972425907..920cbdc56a 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/BasicVariableDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/BasicVariableDescriptor.java @@ -1,5 +1,8 @@ package ai.timefold.solver.core.impl.domain.variable.descriptor; +import java.util.Comparator; + +import ai.timefold.solver.core.api.domain.common.ComparatorFactory; import ai.timefold.solver.core.api.domain.variable.PlanningVariable; import ai.timefold.solver.core.api.domain.variable.PlanningVariableGraphType; import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor; @@ -41,8 +44,60 @@ protected void processPropertyAnnotations(DescriptorPolicy descriptorPolicy) { processAllowsUnassigned(planningVariableAnnotation); processChained(planningVariableAnnotation); processValueRangeRefs(descriptorPolicy, planningVariableAnnotation.valueRangeProviderRefs()); - processStrength(planningVariableAnnotation.strengthComparatorClass(), - planningVariableAnnotation.strengthWeightFactoryClass()); + var sortingProperties = assertSortingProperties(planningVariableAnnotation); + processSorting(sortingProperties.comparatorPropertyName(), sortingProperties.comparatorClass(), + sortingProperties.comparatorFactoryPropertyName(), sortingProperties.comparatorFactoryClass()); + } + + private SortingProperties assertSortingProperties(PlanningVariable planningVariableAnnotation) { + // Comparator property + var strengthComparatorClass = planningVariableAnnotation.strengthComparatorClass(); + var comparatorClass = planningVariableAnnotation.comparatorClass(); + if (strengthComparatorClass != null + && PlanningVariable.NullComparator.class.isAssignableFrom(strengthComparatorClass)) { + strengthComparatorClass = null; + } + if (comparatorClass != null && PlanningVariable.NullComparator.class.isAssignableFrom(comparatorClass)) { + comparatorClass = null; + } + if (strengthComparatorClass != null && comparatorClass != null) { + throw new IllegalStateException( + "The entityClass (%s) property (%s) cannot have a %s (%s) and a %s (%s) at the same time.".formatted( + entityDescriptor.getEntityClass(), variableMemberAccessor.getName(), "strengthComparatorClass", + strengthComparatorClass.getName(), "comparatorClass", comparatorClass.getName())); + } + // Comparator factory property + var strengthWeightFactoryClass = planningVariableAnnotation.strengthWeightFactoryClass(); + var comparatorFactoryClass = planningVariableAnnotation.comparatorFactoryClass(); + if (strengthWeightFactoryClass != null + && PlanningVariable.NullComparatorFactory.class.isAssignableFrom(strengthWeightFactoryClass)) { + strengthWeightFactoryClass = null; + } + if (comparatorFactoryClass != null + && PlanningVariable.NullComparatorFactory.class.isAssignableFrom(comparatorFactoryClass)) { + comparatorFactoryClass = null; + } + if (strengthWeightFactoryClass != null && comparatorFactoryClass != null) { + throw new IllegalStateException( + "The entityClass (%s) property (%s) cannot have a %s (%s) and a %s (%s) at the same time.".formatted( + entityDescriptor.getEntityClass(), variableMemberAccessor.getName(), "strengthWeightFactoryClass", + strengthWeightFactoryClass.getName(), "comparatorFactoryClass", comparatorFactoryClass.getName())); + } + // Selected settings + var selectedComparatorPropertyName = "comparatorClass"; + var selectedComparatorClass = comparatorClass; + var selectedComparatorFactoryPropertyName = "comparatorFactoryClass"; + var selectedComparatorFactoryClass = comparatorFactoryClass; + if (strengthComparatorClass != null) { + selectedComparatorPropertyName = "strengthComparatorClass"; + selectedComparatorClass = strengthComparatorClass; + } + if (strengthWeightFactoryClass != null) { + selectedComparatorFactoryPropertyName = "strengthWeightFactoryClass"; + selectedComparatorFactoryClass = strengthWeightFactoryClass; + } + return new SortingProperties(selectedComparatorPropertyName, selectedComparatorClass, + selectedComparatorFactoryPropertyName, selectedComparatorFactoryClass); } private void processAllowsUnassigned(PlanningVariable planningVariableAnnotation) { @@ -130,4 +185,9 @@ public SelectionFilter getMovableChainedTrailingValueFilter() return movableChainedTrailingValueFilter; } + private record SortingProperties(String comparatorPropertyName, Class comparatorClass, + String comparatorFactoryPropertyName, Class comparatorFactoryClass) { + + } + } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/GenuineVariableDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/GenuineVariableDescriptor.java index 28d1bc48be..a749abf63f 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/GenuineVariableDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/GenuineVariableDescriptor.java @@ -8,6 +8,7 @@ import java.util.Comparator; import java.util.stream.Stream; +import ai.timefold.solver.core.api.domain.common.ComparatorFactory; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; import ai.timefold.solver.core.api.domain.variable.PlanningListVariable; @@ -19,10 +20,9 @@ import ai.timefold.solver.core.impl.domain.policy.DescriptorPolicy; import ai.timefold.solver.core.impl.domain.valuerange.descriptor.FromSolutionPropertyValueRangeDescriptor; import ai.timefold.solver.core.impl.domain.valuerange.descriptor.ValueRangeDescriptor; +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.ComparatorFactorySelectionSorter; import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.ComparatorSelectionSorter; import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter; -import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory; -import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.WeightFactorySelectionSorter; /** * @param the solution type, the class with the {@link PlanningSolution} annotation @@ -30,8 +30,8 @@ public abstract class GenuineVariableDescriptor extends VariableDescriptor { private ValueRangeDescriptor valueRangeDescriptor; - private SelectionSorter increasingStrengthSorter; - private SelectionSorter decreasingStrengthSorter; + private SelectionSorter ascendingSorter; + private SelectionSorter descendingSorter; // ************************************************************************ // Constructors and simple getters/setters @@ -155,36 +155,33 @@ private ValueRangeDescriptor buildValueRangeDescriptor(DescriptorPoli } } - protected void processStrength(Class strengthComparatorClass, - Class strengthWeightFactoryClass) { - if (strengthComparatorClass == PlanningVariable.NullStrengthComparator.class) { - strengthComparatorClass = null; + @SuppressWarnings("rawtypes") + protected void processSorting(String comparatorPropertyName, Class comparatorClass, + String comparatorFactoryPropertyName, Class comparatorFactoryClass) { + if (comparatorClass != null && PlanningVariable.NullComparator.class.isAssignableFrom(comparatorClass)) { + comparatorClass = null; } - if (strengthWeightFactoryClass == PlanningVariable.NullStrengthWeightFactory.class) { - strengthWeightFactoryClass = null; + if (comparatorFactoryClass != null + && PlanningVariable.NullComparatorFactory.class.isAssignableFrom(comparatorFactoryClass)) { + comparatorFactoryClass = null; } - if (strengthComparatorClass != null && strengthWeightFactoryClass != null) { - throw new IllegalStateException("The entityClass (" + entityDescriptor.getEntityClass() - + ") property (" + variableMemberAccessor.getName() - + ") cannot have a strengthComparatorClass (" + strengthComparatorClass.getName() - + ") and a strengthWeightFactoryClass (" + strengthWeightFactoryClass.getName() - + ") at the same time."); + if (comparatorClass != null && comparatorFactoryClass != null) { + throw new IllegalStateException( + "The entityClass (%s) property (%s) cannot have a %s (%s) and a %s (%s) at the same time.".formatted( + entityDescriptor.getEntityClass(), variableMemberAccessor.getName(), comparatorPropertyName, + comparatorClass.getName(), comparatorFactoryPropertyName, comparatorFactoryClass.getName())); } - if (strengthComparatorClass != null) { - Comparator strengthComparator = newInstance(this::toString, - "strengthComparatorClass", strengthComparatorClass); - increasingStrengthSorter = new ComparatorSelectionSorter<>(strengthComparator, + if (comparatorClass != null) { + Comparator comparator = newInstance(this::toString, comparatorPropertyName, comparatorClass); + ascendingSorter = new ComparatorSelectionSorter<>(comparator, SelectionSorterOrder.ASCENDING); - decreasingStrengthSorter = new ComparatorSelectionSorter<>(strengthComparator, - SelectionSorterOrder.DESCENDING); - } - if (strengthWeightFactoryClass != null) { - SelectionSorterWeightFactory strengthWeightFactory = newInstance(this::toString, - "strengthWeightFactoryClass", strengthWeightFactoryClass); - increasingStrengthSorter = new WeightFactorySelectionSorter<>(strengthWeightFactory, - SelectionSorterOrder.ASCENDING); - decreasingStrengthSorter = new WeightFactorySelectionSorter<>(strengthWeightFactory, + descendingSorter = new ComparatorSelectionSorter<>(comparator, SelectionSorterOrder.DESCENDING); + } else if (comparatorFactoryClass != null) { + ComparatorFactory comparatorFactory = + newInstance(this::toString, comparatorFactoryPropertyName, comparatorFactoryClass); + ascendingSorter = new ComparatorFactorySelectionSorter<>(comparatorFactory, SelectionSorterOrder.ASCENDING); + descendingSorter = new ComparatorFactorySelectionSorter<>(comparatorFactory, SelectionSorterOrder.DESCENDING); } } @@ -238,12 +235,12 @@ public boolean isReinitializable(Object entity) { return value == null; } - public SelectionSorter getIncreasingStrengthSorter() { - return increasingStrengthSorter; + public SelectionSorter getAscendingSorter() { + return ascendingSorter; } - public SelectionSorter getDecreasingStrengthSorter() { - return decreasingStrengthSorter; + public SelectionSorter getDescendingSorter() { + return descendingSorter; } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/ListVariableDescriptor.java b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/ListVariableDescriptor.java index 19aff357f0..76b85c54f8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/ListVariableDescriptor.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/ListVariableDescriptor.java @@ -60,6 +60,8 @@ protected void processPropertyAnnotations(DescriptorPolicy descriptorPolicy) { PlanningListVariable planningVariableAnnotation = variableMemberAccessor.getAnnotation(PlanningListVariable.class); allowsUnassignedValues = planningVariableAnnotation.allowsUnassignedValues(); processValueRangeRefs(descriptorPolicy, planningVariableAnnotation.valueRangeProviderRefs()); + processSorting("comparatorClass", planningVariableAnnotation.comparatorClass(), "comparatorFactoryClass", + planningVariableAnnotation.comparatorFactoryClass()); } @Override diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/AbstractSelectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/AbstractSelectorFactory.java index 91a4923f3a..23681be212 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/AbstractSelectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/AbstractSelectorFactory.java @@ -13,27 +13,39 @@ protected AbstractSelectorFactory(SelectorConfig_ selectorConfig) { } protected void validateCacheTypeVersusSelectionOrder(SelectionCacheType resolvedCacheType, - SelectionOrder resolvedSelectionOrder) { + SelectionOrder resolvedSelectionOrder, boolean hasEntityRange) { switch (resolvedSelectionOrder) { case INHERIT: - throw new IllegalArgumentException("The moveSelectorConfig (" + config - + ") has a resolvedSelectionOrder (" + resolvedSelectionOrder - + ") which should have been resolved by now."); - case ORIGINAL: - case RANDOM: + throw new IllegalArgumentException( + "The moveSelectorConfig (%s) has a resolvedSelectionOrder (%s) which should have been resolved by now." + .formatted(config, resolvedSelectionOrder)); + case ORIGINAL, RANDOM: break; - case SORTED: - case SHUFFLED: - case PROBABILISTIC: + case SORTED: { if (resolvedCacheType.isNotCached()) { - throw new IllegalArgumentException("The moveSelectorConfig (" + config - + ") has a resolvedSelectionOrder (" + resolvedSelectionOrder - + ") which does not support the resolvedCacheType (" + resolvedCacheType + ")."); + throw new IllegalArgumentException( + "The moveSelectorConfig (%s) has a resolvedSelectionOrder (%s) which does not support the resolvedCacheType (%s)." + .formatted(config, resolvedSelectionOrder, resolvedCacheType)); + } + if (hasEntityRange && resolvedCacheType != SelectionCacheType.STEP) { + throw new IllegalArgumentException( + """ + The moveSelectorConfig (%s) has a resolvedSelectionOrder (%s) which does not support the resolvedCacheType (%s). + Maybe set the "cacheType" to STEP.""" + .formatted(config, resolvedSelectionOrder, resolvedCacheType)); + } + break; + } + case SHUFFLED, PROBABILISTIC: + if (resolvedCacheType.isNotCached()) { + throw new IllegalArgumentException( + "The moveSelectorConfig (%s) has a resolvedSelectionOrder (%s) which does not support the resolvedCacheType (%s)." + .formatted(config, resolvedSelectionOrder, resolvedCacheType)); } break; default: - throw new IllegalStateException("The resolvedSelectionOrder (" + resolvedSelectionOrder - + ") is not implemented."); + throw new IllegalStateException( + "The resolvedSelectionOrder (%s) is not implemented.".formatted(resolvedSelectionOrder)); } } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorFactorySelectionSorter.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorFactorySelectionSorter.java new file mode 100644 index 0000000000..0a7483c674 --- /dev/null +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorFactorySelectionSorter.java @@ -0,0 +1,57 @@ +package ai.timefold.solver.core.impl.heuristic.selector.common.decorator; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + +import ai.timefold.solver.core.api.domain.common.ComparatorFactory; +import ai.timefold.solver.core.api.domain.solution.PlanningSolution; +import ai.timefold.solver.core.api.score.director.ScoreDirector; +import ai.timefold.solver.core.config.heuristic.selector.common.decorator.SelectionSorterOrder; + +/** + * Sorts a selection {@link List} based on a {@link ComparatorFactory}. + * + * @param the solution type, the class with the {@link PlanningSolution} annotation + * @param the selection type + */ +public final class ComparatorFactorySelectionSorter implements SelectionSorter { + + private final ComparatorFactory selectionComparatorFactory; + private final SelectionSorterOrder selectionSorterOrder; + + public ComparatorFactorySelectionSorter(ComparatorFactory selectionComparatorFactory, + SelectionSorterOrder selectionSorterOrder) { + this.selectionComparatorFactory = selectionComparatorFactory; + this.selectionSorterOrder = selectionSorterOrder; + } + + private Comparator getAppliedComparator(Comparator comparator) { + return switch (selectionSorterOrder) { + case ASCENDING -> comparator; + case DESCENDING -> Collections.reverseOrder(comparator); + }; + } + + @Override + public void sort(ScoreDirector scoreDirector, List selectionList) { + var appliedComparator = + getAppliedComparator(selectionComparatorFactory.createComparator(scoreDirector.getWorkingSolution())); + selectionList.sort(appliedComparator); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ComparatorFactorySelectionSorter that)) { + return false; + } + return Objects.equals(selectionComparatorFactory, that.selectionComparatorFactory) + && selectionSorterOrder == that.selectionSorterOrder; + } + + @Override + public int hashCode() { + return Objects.hash(selectionComparatorFactory, selectionSorterOrder); + } +} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/SelectionSorterWeightFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/SelectionSorterWeightFactory.java index 4cbfa2157e..8989c4416b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/SelectionSorterWeightFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/SelectionSorterWeightFactory.java @@ -1,5 +1,8 @@ package ai.timefold.solver.core.impl.heuristic.selector.common.decorator; +import java.util.Comparator; + +import ai.timefold.solver.core.api.domain.common.ComparatorFactory; import ai.timefold.solver.core.api.domain.entity.PlanningEntity; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.impl.heuristic.move.Move; @@ -15,11 +18,14 @@ * Implementations are expected to be stateless. * The solver may choose to reuse instances. * + * @deprecated Deprecated in favor of {@link ComparatorFactory}. + * * @param the solution type, the class with the {@link PlanningSolution} annotation * @param the selection type */ +@Deprecated(forRemoval = true, since = "1.28.0") @FunctionalInterface -public interface SelectionSorterWeightFactory { +public interface SelectionSorterWeightFactory extends ComparatorFactory { /** * @param solution never null, the {@link PlanningSolution} to which the selection belongs or applies to @@ -28,4 +34,11 @@ public interface SelectionSorterWeightFactory { */ Comparable createSorterWeight(Solution_ solution, T selection); + /** + * Default implementation for enabling interconnection between the two comparator contracts. + */ + @Override + default Comparator createComparator(Solution_ solution) { + return (v1, v2) -> createSorterWeight(solution, v1).compareTo(createSorterWeight(solution, v2)); + } } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/WeightFactorySelectionSorter.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/WeightFactorySelectionSorter.java deleted file mode 100644 index b91962141f..0000000000 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/WeightFactorySelectionSorter.java +++ /dev/null @@ -1,83 +0,0 @@ -package ai.timefold.solver.core.impl.heuristic.selector.common.decorator; - -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Objects; -import java.util.SortedMap; -import java.util.TreeMap; - -import ai.timefold.solver.core.api.domain.entity.PlanningEntity; -import ai.timefold.solver.core.api.domain.solution.PlanningSolution; -import ai.timefold.solver.core.api.score.director.ScoreDirector; -import ai.timefold.solver.core.config.heuristic.selector.common.decorator.SelectionSorterOrder; -import ai.timefold.solver.core.impl.heuristic.move.Move; -import ai.timefold.solver.core.impl.heuristic.selector.Selector; - -/** - * Sorts a selection {@link List} based on a {@link SelectionSorterWeightFactory}. - * - * @param the solution type, the class with the {@link PlanningSolution} annotation - * @param the selection type - */ -public final class WeightFactorySelectionSorter implements SelectionSorter { - - private final SelectionSorterWeightFactory selectionSorterWeightFactory; - private final Comparator appliedWeightComparator; - - public WeightFactorySelectionSorter(SelectionSorterWeightFactory selectionSorterWeightFactory, - SelectionSorterOrder selectionSorterOrder) { - this.selectionSorterWeightFactory = selectionSorterWeightFactory; - switch (selectionSorterOrder) { - case ASCENDING: - this.appliedWeightComparator = Comparator.naturalOrder(); - break; - case DESCENDING: - this.appliedWeightComparator = Collections.reverseOrder(); - break; - default: - throw new IllegalStateException("The selectionSorterOrder (" + selectionSorterOrder - + ") is not implemented."); - } - } - - @Override - public void sort(ScoreDirector scoreDirector, List selectionList) { - sort(scoreDirector.getWorkingSolution(), selectionList); - } - - /** - * @param solution never null, the {@link PlanningSolution} to which the selections belong or apply to - * @param selectionList never null, a {@link List} - * of {@link PlanningEntity}, planningValue, {@link Move} or {@link Selector} - */ - public void sort(Solution_ solution, List selectionList) { - SortedMap selectionMap = new TreeMap<>(appliedWeightComparator); - for (T selection : selectionList) { - Comparable difficultyWeight = selectionSorterWeightFactory.createSorterWeight(solution, selection); - T previous = selectionMap.put(difficultyWeight, selection); - if (previous != null) { - throw new IllegalStateException("The selectionList contains 2 times the same selection (" - + previous + ") and (" + selection + ")."); - } - } - selectionList.clear(); - selectionList.addAll(selectionMap.values()); - } - - @Override - public boolean equals(Object other) { - if (this == other) - return true; - if (other == null || getClass() != other.getClass()) - return false; - WeightFactorySelectionSorter that = (WeightFactorySelectionSorter) other; - return Objects.equals(selectionSorterWeightFactory, that.selectionSorterWeightFactory) - && Objects.equals(appliedWeightComparator, that.appliedWeightComparator); - } - - @Override - public int hashCode() { - return Objects.hash(selectionSorterWeightFactory, appliedWeightComparator); - } -} diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/EntitySelectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/EntitySelectorFactory.java index 2f5ce4666f..0e9585bd39 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/EntitySelectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/EntitySelectorFactory.java @@ -6,6 +6,7 @@ import java.util.function.Function; import java.util.stream.Stream; +import ai.timefold.solver.core.api.domain.common.ComparatorFactory; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType; import ai.timefold.solver.core.config.heuristic.selector.common.SelectionOrder; @@ -18,12 +19,11 @@ import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; import ai.timefold.solver.core.impl.heuristic.selector.AbstractSelectorFactory; import ai.timefold.solver.core.impl.heuristic.selector.common.ValueRangeRecorderId; +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.ComparatorFactorySelectionSorter; import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.ComparatorSelectionSorter; import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionFilter; import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionProbabilityWeightFactory; import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter; -import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory; -import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.WeightFactorySelectionSorter; import ai.timefold.solver.core.impl.heuristic.selector.entity.decorator.CachingEntitySelector; import ai.timefold.solver.core.impl.heuristic.selector.entity.decorator.FilteringEntityByEntitySelector; import ai.timefold.solver.core.impl.heuristic.selector.entity.decorator.FilteringEntityByValueSelector; @@ -99,7 +99,8 @@ public EntitySelector buildEntitySelector(HeuristicConfigPolicy buildMimicReplaying(HeuristicConfigPolicy determineComparatorClass(EntitySelectorConfig entitySelectorConfig) { + var propertyName = determineComparatorPropertyName(entitySelectorConfig); + if (propertyName.equals("sorterComparatorClass")) { + return entitySelectorConfig.getSorterComparatorClass(); + } else { + return entitySelectorConfig.getComparatorClass(); + } + } + + private static String determineComparatorFactoryPropertyName(EntitySelectorConfig entitySelectorConfig) { + var weightFactoryClass = entitySelectorConfig.getSorterWeightFactoryClass(); + var comparatorFactoryClass = entitySelectorConfig.getComparatorFactoryClass(); + if (weightFactoryClass != null && comparatorFactoryClass != null) { + throw new IllegalArgumentException( + "The entitySelectorConfig (%s) cannot have a %s (%s) and %s (%s) at the same time.".formatted( + entitySelectorConfig, "sorterWeightFactoryClass", weightFactoryClass, + "comparatorFactoryClass", comparatorFactoryClass)); + } + return weightFactoryClass != null ? "sorterWeightFactoryClass" : "comparatorFactoryClass"; + } + + private static Class + determineComparatorFactoryClass(EntitySelectorConfig entitySelectorConfig) { + var propertyName = determineComparatorFactoryPropertyName(entitySelectorConfig); + if (propertyName.equals("sorterWeightFactoryClass")) { + return entitySelectorConfig.getSorterWeightFactoryClass(); + } else { + return entitySelectorConfig.getComparatorFactoryClass(); + } + } + private EntitySelector buildBaseEntitySelector(EntityDescriptor entityDescriptor, SelectionCacheType minimumCacheType, boolean randomSelection) { if (minimumCacheType == SelectionCacheType.SOLVER) { @@ -250,30 +295,36 @@ private EntitySelector applyFiltering(EntitySelector entit protected void validateSorting(SelectionOrder resolvedSelectionOrder) { var sorterManner = config.getSorterManner(); - var sorterComparatorClass = config.getSorterComparatorClass(); - var sorterWeightFactoryClass = config.getSorterWeightFactoryClass(); + var comparatorClass = determineComparatorClass(config); + var comparatorPropertyName = determineComparatorPropertyName(config); + var comparatorFactoryPropertyName = determineComparatorFactoryPropertyName(config); + var comparatorFactoryClass = determineComparatorFactoryClass(config); var sorterOrder = config.getSorterOrder(); var sorterClass = config.getSorterClass(); - if ((sorterManner != null || sorterComparatorClass != null || sorterWeightFactoryClass != null || sorterOrder != null + if ((sorterManner != null || comparatorClass != null || comparatorFactoryClass != null + || sorterOrder != null || sorterClass != null) && resolvedSelectionOrder != SelectionOrder.SORTED) { throw new IllegalArgumentException(""" The entitySelectorConfig (%s) with sorterManner (%s) \ - and sorterComparatorClass (%s) and sorterWeightFactoryClass (%s) and sorterOrder (%s) and sorterClass (%s) \ + and %s (%s) and %s (%s) and sorterOrder (%s) and sorterClass (%s) \ has a resolvedSelectionOrder (%s) that is not %s.""" - .formatted(config, sorterManner, sorterComparatorClass, sorterWeightFactoryClass, sorterOrder, sorterClass, - resolvedSelectionOrder, SelectionOrder.SORTED)); + .formatted(config, sorterManner, comparatorPropertyName, comparatorClass, comparatorFactoryPropertyName, + comparatorFactoryClass, sorterOrder, sorterClass, resolvedSelectionOrder, SelectionOrder.SORTED)); } - assertNotSorterMannerAnd(config, "sorterComparatorClass", EntitySelectorConfig::getSorterComparatorClass); - assertNotSorterMannerAnd(config, "sorterWeightFactoryClass", EntitySelectorConfig::getSorterWeightFactoryClass); + assertNotSorterMannerAnd(config, comparatorPropertyName, EntitySelectorFactory::determineComparatorClass); + assertNotSorterMannerAnd(config, comparatorFactoryPropertyName, + EntitySelectorFactory::determineComparatorFactoryClass); assertNotSorterMannerAnd(config, "sorterClass", EntitySelectorConfig::getSorterClass); assertNotSorterMannerAnd(config, "sorterOrder", EntitySelectorConfig::getSorterOrder); - assertNotSorterClassAnd(config, "sorterComparatorClass", EntitySelectorConfig::getSorterComparatorClass); - assertNotSorterClassAnd(config, "sorterWeightFactoryClass", EntitySelectorConfig::getSorterWeightFactoryClass); + assertNotSorterClassAnd(config, comparatorPropertyName, EntitySelectorFactory::determineComparatorClass); + assertNotSorterClassAnd(config, comparatorFactoryPropertyName, + EntitySelectorFactory::determineComparatorFactoryClass); assertNotSorterClassAnd(config, "sorterOrder", EntitySelectorConfig::getSorterOrder); - if (sorterComparatorClass != null && sorterWeightFactoryClass != null) { + if (comparatorClass != null && comparatorFactoryClass != null) { throw new IllegalArgumentException( - "The entitySelectorConfig (%s) has both a sorterComparatorClass (%s) and a sorterWeightFactoryClass (%s)." - .formatted(config, sorterComparatorClass, sorterWeightFactoryClass)); + "The entitySelectorConfig (%s) has both a %s (%s) and a %s (%s)." + .formatted(config, comparatorPropertyName, comparatorClass, comparatorFactoryPropertyName, + comparatorFactoryClass)); } } @@ -303,31 +354,34 @@ protected EntitySelector applySorting(SelectionCacheType resolvedCach if (resolvedSelectionOrder == SelectionOrder.SORTED) { SelectionSorter sorter; var sorterManner = config.getSorterManner(); + var comparatorClass = determineComparatorClass(config); + var comparatorFactoryClass = determineComparatorFactoryClass(config); if (sorterManner != null) { var entityDescriptor = entitySelector.getEntityDescriptor(); if (!EntitySelectorConfig.hasSorter(sorterManner, entityDescriptor)) { return entitySelector; } sorter = EntitySelectorConfig.determineSorter(sorterManner, entityDescriptor); - } else if (config.getSorterComparatorClass() != null) { - Comparator sorterComparator = - instanceCache.newInstance(config, "sorterComparatorClass", config.getSorterComparatorClass()); + } else if (comparatorClass != null) { + var sorterComparator = + instanceCache.newInstance(config, determineComparatorPropertyName(config), comparatorClass); sorter = new ComparatorSelectionSorter<>(sorterComparator, SelectionSorterOrder.resolve(config.getSorterOrder())); - } else if (config.getSorterWeightFactoryClass() != null) { - SelectionSorterWeightFactory sorterWeightFactory = - instanceCache.newInstance(config, "sorterWeightFactoryClass", config.getSorterWeightFactoryClass()); - sorter = new WeightFactorySelectionSorter<>(sorterWeightFactory, + } else if (comparatorFactoryClass != null) { + var comparatorFactory = instanceCache.newInstance(config, determineComparatorFactoryPropertyName(config), + comparatorFactoryClass); + sorter = new ComparatorFactorySelectionSorter<>(comparatorFactory, SelectionSorterOrder.resolve(config.getSorterOrder())); } else if (config.getSorterClass() != null) { sorter = instanceCache.newInstance(config, "sorterClass", config.getSorterClass()); } else { throw new IllegalArgumentException(""" The entitySelectorConfig (%s) with resolvedSelectionOrder (%s) needs \ - a sorterManner (%s) or a sorterComparatorClass (%s) or a sorterWeightFactoryClass (%s) \ + a sorterManner (%s) or a %s (%s) or a %s (%s) \ or a sorterClass (%s).""" - .formatted(config, resolvedSelectionOrder, sorterManner, config.getSorterComparatorClass(), - config.getSorterWeightFactoryClass(), config.getSorterClass())); + .formatted(config, resolvedSelectionOrder, sorterManner, determineComparatorPropertyName(config), + comparatorClass, determineComparatorFactoryPropertyName(config), comparatorFactoryClass, + config.getSorterClass())); } entitySelector = new SortingEntitySelector<>(entitySelector, resolvedCacheType, sorter); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/SortingEntitySelector.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/SortingEntitySelector.java index 1ece08b10c..7467f25f8b 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/SortingEntitySelector.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/entity/decorator/SortingEntitySelector.java @@ -7,11 +7,13 @@ import ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType; import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter; import ai.timefold.solver.core.impl.heuristic.selector.entity.EntitySelector; +import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.solver.scope.SolverScope; public final class SortingEntitySelector extends AbstractCachingEntitySelector { private final SelectionSorter sorter; + private SolverScope solverScope; public SortingEntitySelector(EntitySelector childEntitySelector, SelectionCacheType cacheType, SelectionSorter sorter) { @@ -23,8 +25,40 @@ public SortingEntitySelector(EntitySelector childEntitySelector, Sele // Worker methods // ************************************************************************ + /** + * The method ensures that cached items are loaded and sorted when the cache is set to STEP. + * This logic is necessary + * for making the node compatible with sorting elements at the STEP level when using entity-range. + * For this specific use case, + * we will fetch and sort the data after the phase has started but before the step begins. + */ + private void ensureStepCacheIsLoaded() { + if (cacheType != SelectionCacheType.STEP || cachedEntityList != null) { + return; + } + // At this stage, + // we attempt to load the entity list + // since the iterator may have been requested prior to the start of the step. + constructCache(solverScope); + } + + @Override + public void phaseStarted(AbstractPhaseScope phaseScope) { + super.phaseStarted(phaseScope); + this.solverScope = phaseScope.getSolverScope(); + } + + @Override + public void phaseEnded(AbstractPhaseScope phaseScope) { + super.phaseEnded(phaseScope); + this.solverScope = null; + } + @Override public void constructCache(SolverScope solverScope) { + if (cachedEntityList != null) { + return; + } super.constructCache(solverScope); sorter.sort(solverScope.getScoreDirector(), cachedEntityList); logger.trace(" Sorted cachedEntityList: size ({}), entitySelector ({}).", @@ -36,18 +70,27 @@ public boolean isNeverEnding() { return false; } + @Override + public long getSize() { + ensureStepCacheIsLoaded(); + return super.getSize(); + } + @Override public Iterator iterator() { + ensureStepCacheIsLoaded(); return cachedEntityList.iterator(); } @Override public ListIterator listIterator() { + ensureStepCacheIsLoaded(); return cachedEntityList.listIterator(); } @Override public ListIterator listIterator(int index) { + ensureStepCacheIsLoaded(); return cachedEntityList.listIterator(index); } diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/DestinationSelectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/DestinationSelectorFactory.java index 0589ad1999..731e1e66c8 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/DestinationSelectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/list/DestinationSelectorFactory.java @@ -1,5 +1,7 @@ package ai.timefold.solver.core.impl.heuristic.selector.list; +import static ai.timefold.solver.core.config.heuristic.selector.entity.EntitySorterManner.NONE; + import java.util.Objects; import ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType; @@ -35,7 +37,24 @@ public DestinationSelector buildDestinationSelector(HeuristicConfigPo public DestinationSelector buildDestinationSelector(HeuristicConfigPolicy configPolicy, SelectionCacheType minimumCacheType, boolean randomSelection, String entityValueRangeRecorderId) { var selectionOrder = SelectionOrder.fromRandomSelectionBoolean(randomSelection); - var entitySelector = EntitySelectorFactory. create(Objects.requireNonNull(config.getEntitySelectorConfig())) + var entitySelectorConfig = Objects.requireNonNull(config.getEntitySelectorConfig()).copyConfig(); + var hasSortManner = configPolicy.getEntitySorterManner() != null + && configPolicy.getEntitySorterManner() != NONE; + var entityDescriptor = deduceEntityDescriptor(configPolicy, entitySelectorConfig.getEntityClass()); + var hasSorter = entityDescriptor.getDescendingSorter() != null; + if (hasSortManner && hasSorter && entitySelectorConfig.getSorterManner() == null) { + if (entityValueRangeRecorderId == null) { + // Solution-range model + entitySelectorConfig.setCacheType(SelectionCacheType.PHASE); + } else { + // The entity-range model requires sorting at each step + // because the list of reachable entities can vary from one entity to another + entitySelectorConfig.setCacheType(SelectionCacheType.STEP); + } + entitySelectorConfig.setSelectionOrder(SelectionOrder.SORTED); + entitySelectorConfig.setSorterManner(configPolicy.getEntitySorterManner()); + } + var entitySelector = EntitySelectorFactory. create(entitySelectorConfig) .buildEntitySelector(configPolicy, minimumCacheType, selectionOrder, new ValueRangeRecorderId(entityValueRangeRecorderId, false)); var valueSelector = buildIterableValueSelector(configPolicy, entitySelector.getEntityDescriptor(), diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/AbstractMoveSelectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/AbstractMoveSelectorFactory.java index ef3f58f0dc..4e2c57cce4 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/AbstractMoveSelectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/AbstractMoveSelectorFactory.java @@ -2,6 +2,7 @@ import java.util.Comparator; +import ai.timefold.solver.core.api.domain.common.ComparatorFactory; import ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType; import ai.timefold.solver.core.config.heuristic.selector.common.SelectionOrder; import ai.timefold.solver.core.config.heuristic.selector.common.decorator.SelectionSorterOrder; @@ -10,12 +11,11 @@ import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; import ai.timefold.solver.core.impl.heuristic.move.Move; import ai.timefold.solver.core.impl.heuristic.selector.AbstractSelectorFactory; +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.ComparatorFactorySelectionSorter; import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.ComparatorSelectionSorter; import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionFilter; import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionProbabilityWeightFactory; import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter; -import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory; -import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.WeightFactorySelectionSorter; import ai.timefold.solver.core.impl.heuristic.selector.move.decorator.CachingMoveSelector; import ai.timefold.solver.core.impl.heuristic.selector.move.decorator.FilteringMoveSelector; import ai.timefold.solver.core.impl.heuristic.selector.move.decorator.ProbabilityMoveSelector; @@ -60,7 +60,7 @@ public MoveSelector buildMoveSelector(HeuristicConfigPolicy determineComparatorClass(MoveSelectorConfig_ moveSelectorConfig) { + var propertyName = determineComparatorPropertyName(moveSelectorConfig); + if (propertyName.equals("sorterComparatorClass")) { + return moveSelectorConfig.getSorterComparatorClass(); + } else { + return moveSelectorConfig.getComparatorClass(); + } + } + + private String determineComparatorFactoryPropertyName(MoveSelectorConfig_ moveSelectorConfig) { + var weightFactoryClass = moveSelectorConfig.getSorterWeightFactoryClass(); + var comparatorFactoryClass = moveSelectorConfig.getComparatorFactoryClass(); + if (weightFactoryClass != null && comparatorFactoryClass != null) { + throw new IllegalArgumentException( + "The moveSelectorConfig (%s) cannot have a %s (%s) and %s (%s) at the same time.".formatted( + moveSelectorConfig, "sorterWeightFactoryClass", weightFactoryClass, + "comparatorFactoryClass", comparatorFactoryClass)); + } + return weightFactoryClass != null ? "sorterWeightFactoryClass" : "comparatorFactoryClass"; + } + + private Class determineComparatorFactoryClass(MoveSelectorConfig_ moveSelectorConfig) { + var propertyName = determineComparatorFactoryPropertyName(moveSelectorConfig); + if (propertyName.equals("sorterWeightFactoryClass")) { + return moveSelectorConfig.getSorterWeightFactoryClass(); + } else { + return moveSelectorConfig.getComparatorFactoryClass(); + } + } + protected boolean isBaseInherentlyCached() { return false; } @@ -149,36 +191,41 @@ private MoveSelector applyFiltering(MoveSelector moveSelec } protected void validateSorting(SelectionOrder resolvedSelectionOrder) { - if ((config.getSorterComparatorClass() != null || config.getSorterWeightFactoryClass() != null + var comparatorClass = determineComparatorClass(config); + var comparatorFactoryClass = determineComparatorFactoryClass(config); + if ((comparatorClass != null || comparatorFactoryClass != null || config.getSorterOrder() != null || config.getSorterClass() != null) && resolvedSelectionOrder != SelectionOrder.SORTED) { - throw new IllegalArgumentException("The moveSelectorConfig (" + config - + ") with sorterComparatorClass (" + config.getSorterComparatorClass() - + ") and sorterWeightFactoryClass (" + config.getSorterWeightFactoryClass() - + ") and sorterOrder (" + config.getSorterOrder() - + ") and sorterClass (" + config.getSorterClass() - + ") has a resolvedSelectionOrder (" + resolvedSelectionOrder - + ") that is not " + SelectionOrder.SORTED + "."); + throw new IllegalArgumentException( + "The moveSelectorConfig (%s) with %s (%s) and %s (%s) and sorterOrder (%s) and sorterClass (%s) has a resolvedSelectionOrder (%s) that is not %s." + .formatted(config, determineComparatorPropertyName(config), comparatorClass, + determineComparatorFactoryPropertyName(config), + comparatorFactoryClass, config.getSorterOrder(), config.getSorterClass(), + resolvedSelectionOrder, SelectionOrder.SORTED)); } - if (config.getSorterComparatorClass() != null && config.getSorterWeightFactoryClass() != null) { - throw new IllegalArgumentException("The moveSelectorConfig (" + config - + ") has both a sorterComparatorClass (" + config.getSorterComparatorClass() - + ") and a sorterWeightFactoryClass (" + config.getSorterWeightFactoryClass() + ")."); + if (comparatorClass != null && comparatorFactoryClass != null) { + throw new IllegalArgumentException( + "The moveSelectorConfig (%s) has both a %s (%s) and a %s (%s).".formatted(config, + determineComparatorPropertyName(config), comparatorClass, + determineComparatorFactoryPropertyName(config), + comparatorFactoryClass)); } - if (config.getSorterComparatorClass() != null && config.getSorterClass() != null) { - throw new IllegalArgumentException("The moveSelectorConfig (" + config - + ") has both a sorterComparatorClass (" + config.getSorterComparatorClass() - + ") and a sorterClass (" + config.getSorterClass() + ")."); + if (comparatorClass != null && config.getSorterClass() != null) { + throw new IllegalArgumentException( + "The moveSelectorConfig (%s) has both a %s (%s) and a sorterClass (%s)." + .formatted(config, determineComparatorPropertyName(config), comparatorClass, + config.getSorterClass())); } - if (config.getSorterWeightFactoryClass() != null && config.getSorterClass() != null) { - throw new IllegalArgumentException("The moveSelectorConfig (" + config - + ") has both a sorterWeightFactoryClass (" + config.getSorterWeightFactoryClass() - + ") and a sorterClass (" + config.getSorterClass() + ")."); + if (comparatorFactoryClass != null && config.getSorterClass() != null) { + throw new IllegalArgumentException( + "The moveSelectorConfig (%s) has both a %s (%s) and a sorterClass (%s).".formatted(config, + determineComparatorFactoryPropertyName(config), comparatorFactoryClass, + config.getSorterClass())); } if (config.getSorterClass() != null && config.getSorterOrder() != null) { - throw new IllegalArgumentException("The moveSelectorConfig (" + config - + ") with sorterClass (" + config.getSorterClass() - + ") has a non-null sorterOrder (" + config.getSorterOrder() + ")."); + throw new IllegalArgumentException( + "The moveSelectorConfig (%s) with sorterClass (%s) has a non-null sorterOrder (%s).".formatted(config, + config.getSorterClass(), config.getSorterOrder())); } } @@ -186,25 +233,26 @@ protected MoveSelector applySorting(SelectionCacheType resolvedCacheT SelectionOrder resolvedSelectionOrder, MoveSelector moveSelector) { if (resolvedSelectionOrder == SelectionOrder.SORTED) { SelectionSorter> sorter; - var sorterComparatorClass = config.getSorterComparatorClass(); - var sorterWeightFactoryClass = config.getSorterWeightFactoryClass(); + var comparatorClass = determineComparatorClass(config); + var comparatorFactoryClass = determineComparatorFactoryClass(config); var sorterClass = config.getSorterClass(); - if (sorterComparatorClass != null) { - Comparator> sorterComparator = - ConfigUtils.newInstance(config, "sorterComparatorClass", sorterComparatorClass); + if (comparatorClass != null) { + var sorterComparator = + ConfigUtils.newInstance(config, determineComparatorPropertyName(config), comparatorClass); sorter = new ComparatorSelectionSorter<>(sorterComparator, SelectionSorterOrder.resolve(config.getSorterOrder())); - } else if (sorterWeightFactoryClass != null) { - SelectionSorterWeightFactory> sorterWeightFactory = - ConfigUtils.newInstance(config, "sorterWeightFactoryClass", sorterWeightFactoryClass); - sorter = new WeightFactorySelectionSorter<>(sorterWeightFactory, + } else if (comparatorFactoryClass != null) { + var comparatorFactory = + ConfigUtils.newInstance(config, determineComparatorFactoryPropertyName(config), comparatorFactoryClass); + sorter = new ComparatorFactorySelectionSorter<>(comparatorFactory, SelectionSorterOrder.resolve(config.getSorterOrder())); } else if (sorterClass != null) { sorter = ConfigUtils.newInstance(config, "sorterClass", sorterClass); } else { throw new IllegalArgumentException( - "The moveSelectorConfig (%s) with resolvedSelectionOrder (%s) needs a sorterComparatorClass (%s) or a sorterWeightFactoryClass (%s) or a sorterClass (%s)." - .formatted(config, resolvedSelectionOrder, sorterComparatorClass, sorterWeightFactoryClass, + "The moveSelectorConfig (%s) with resolvedSelectionOrder (%s) needs a %s (%s) or a %s (%s) or a sorterClass (%s)." + .formatted(config, resolvedSelectionOrder, determineComparatorPropertyName(config), + comparatorClass, determineComparatorFactoryPropertyName(config), comparatorFactoryClass, sorterClass)); } moveSelector = new SortingMoveSelector<>(moveSelector, resolvedCacheType, sorter); diff --git a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java index 7c645dabea..73aab920fa 100644 --- a/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java +++ b/core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactory.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.function.Function; +import ai.timefold.solver.core.api.domain.common.ComparatorFactory; import ai.timefold.solver.core.api.domain.solution.PlanningSolution; import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; import ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType; @@ -17,12 +18,11 @@ import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor; import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; import ai.timefold.solver.core.impl.heuristic.selector.AbstractSelectorFactory; +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.ComparatorFactorySelectionSorter; import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.ComparatorSelectionSorter; import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionFilter; import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionProbabilityWeightFactory; import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter; -import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory; -import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.WeightFactorySelectionSorter; import ai.timefold.solver.core.impl.heuristic.selector.entity.EntitySelector; import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.AssignedListValueSelector; import ai.timefold.solver.core.impl.heuristic.selector.value.decorator.CachingValueSelector; @@ -115,7 +115,7 @@ public ValueSelector buildValueSelector(HeuristicConfigPolicy buildValueSelector(HeuristicConfigPolicy buildMimicReplaying(HeuristicConfigPolicy determineComparatorClass(ValueSelectorConfig valueSelectorConfig) { + var propertyName = determineComparatorPropertyName(valueSelectorConfig); + if (propertyName.equals("sorterComparatorClass")) { + return valueSelectorConfig.getSorterComparatorClass(); + } else { + return valueSelectorConfig.getComparatorClass(); + } + } + + private static String determineComparatorFactoryPropertyName(ValueSelectorConfig valueSelectorConfig) { + var weightFactoryClass = valueSelectorConfig.getSorterWeightFactoryClass(); + var comparatorFactoryClass = valueSelectorConfig.getComparatorFactoryClass(); + if (weightFactoryClass != null && comparatorFactoryClass != null) { + throw new IllegalArgumentException( + "The valueSelectorConfig (%s) cannot have a %s (%s) and %s (%s) at the same time.".formatted( + valueSelectorConfig, "sorterWeightFactoryClass", weightFactoryClass, + "comparatorFactoryClass", comparatorFactoryClass)); + } + return weightFactoryClass != null ? "sorterWeightFactoryClass" : "comparatorFactoryClass"; + } + + private static Class determineComparatorFactoryClass(ValueSelectorConfig valueSelectorConfig) { + var propertyName = determineComparatorFactoryPropertyName(valueSelectorConfig); + if (propertyName.equals("sorterWeightFactoryClass")) { + return valueSelectorConfig.getSorterWeightFactoryClass(); + } else { + return valueSelectorConfig.getComparatorFactoryClass(); + } + } + private ValueSelector buildBaseValueSelector(GenuineVariableDescriptor variableDescriptor, SelectionCacheType minimumCacheType, boolean randomSelection) { var valueRangeDescriptor = variableDescriptor.getValueRangeDescriptor(); @@ -271,30 +315,36 @@ protected ValueSelector applyInitializedChainedValueFilter(HeuristicC protected void validateSorting(SelectionOrder resolvedSelectionOrder) { var sorterManner = config.getSorterManner(); - var sorterComparatorClass = config.getSorterComparatorClass(); - var sorterWeightFactoryClass = config.getSorterWeightFactoryClass(); + var comparatorPropertyName = determineComparatorPropertyName(config); + var comparatorClass = determineComparatorClass(config); + var comparatorFactoryPropertyName = determineComparatorFactoryPropertyName(config); + var comparatorFactoryClass = determineComparatorFactoryClass(config); var sorterOrder = config.getSorterOrder(); var sorterClass = config.getSorterClass(); - if ((sorterManner != null || sorterComparatorClass != null || sorterWeightFactoryClass != null || sorterOrder != null - || sorterClass != null) && resolvedSelectionOrder != SelectionOrder.SORTED) { + if ((sorterManner != null || comparatorClass != null || comparatorFactoryClass != null + || sorterOrder != null || sorterClass != null) && resolvedSelectionOrder != SelectionOrder.SORTED) { throw new IllegalArgumentException(""" The valueSelectorConfig (%s) with sorterManner (%s) \ - and sorterComparatorClass (%s) and sorterWeightFactoryClass (%s) and sorterOrder (%s) and sorterClass (%s) \ + and %s (%s) and %s (%s) and sorterOrder (%s) and sorterClass (%s) \ has a resolvedSelectionOrder (%s) that is not %s.""" - .formatted(config, sorterManner, sorterComparatorClass, sorterWeightFactoryClass, sorterOrder, sorterClass, - resolvedSelectionOrder, SelectionOrder.SORTED)); + .formatted(config, sorterManner, comparatorPropertyName, comparatorClass, comparatorFactoryPropertyName, + comparatorFactoryClass, sorterOrder, sorterClass, resolvedSelectionOrder, + SelectionOrder.SORTED)); } - assertNotSorterMannerAnd(config, "sorterComparatorClass", ValueSelectorConfig::getSorterComparatorClass); - assertNotSorterMannerAnd(config, "sorterWeightFactoryClass", ValueSelectorConfig::getSorterWeightFactoryClass); + assertNotSorterMannerAnd(config, comparatorPropertyName, ValueSelectorFactory::determineComparatorClass); + assertNotSorterMannerAnd(config, comparatorFactoryPropertyName, + ValueSelectorFactory::determineComparatorFactoryClass); assertNotSorterMannerAnd(config, "sorterClass", ValueSelectorConfig::getSorterClass); assertNotSorterMannerAnd(config, "sorterOrder", ValueSelectorConfig::getSorterOrder); - assertNotSorterClassAnd(config, "sorterComparatorClass", ValueSelectorConfig::getSorterComparatorClass); - assertNotSorterClassAnd(config, "sorterWeightFactoryClass", ValueSelectorConfig::getSorterWeightFactoryClass); + assertNotSorterClassAnd(config, comparatorPropertyName, ValueSelectorFactory::determineComparatorClass); + assertNotSorterClassAnd(config, comparatorFactoryPropertyName, + ValueSelectorFactory::determineComparatorFactoryClass); assertNotSorterClassAnd(config, "sorterOrder", ValueSelectorConfig::getSorterOrder); - if (sorterComparatorClass != null && sorterWeightFactoryClass != null) { + if (comparatorClass != null && comparatorFactoryClass != null) { throw new IllegalArgumentException( - "The valueSelectorConfig (%s) has both a sorterComparatorClass (%s) and a sorterWeightFactoryClass (%s)." - .formatted(config, sorterComparatorClass, sorterWeightFactoryClass)); + "The valueSelectorConfig (%s) has both a %s (%s) and a %s (%s)." + .formatted(config, comparatorPropertyName, comparatorClass, comparatorFactoryPropertyName, + comparatorFactoryClass)); } } @@ -324,31 +374,34 @@ protected ValueSelector applySorting(SelectionCacheType resolvedCache if (resolvedSelectionOrder == SelectionOrder.SORTED) { SelectionSorter sorter; var sorterManner = config.getSorterManner(); + var comparatorClass = determineComparatorClass(config); + var comparatorFactoryClass = determineComparatorFactoryClass(config); if (sorterManner != null) { var variableDescriptor = valueSelector.getVariableDescriptor(); if (!ValueSelectorConfig.hasSorter(sorterManner, variableDescriptor)) { return valueSelector; } sorter = ValueSelectorConfig.determineSorter(sorterManner, variableDescriptor); - } else if (config.getSorterComparatorClass() != null) { + } else if (comparatorClass != null) { Comparator sorterComparator = - instanceCache.newInstance(config, "sorterComparatorClass", config.getSorterComparatorClass()); + instanceCache.newInstance(config, determineComparatorPropertyName(config), comparatorClass); sorter = new ComparatorSelectionSorter<>(sorterComparator, SelectionSorterOrder.resolve(config.getSorterOrder())); - } else if (config.getSorterWeightFactoryClass() != null) { - SelectionSorterWeightFactory sorterWeightFactory = - instanceCache.newInstance(config, "sorterWeightFactoryClass", config.getSorterWeightFactoryClass()); - sorter = new WeightFactorySelectionSorter<>(sorterWeightFactory, + } else if (comparatorFactoryClass != null) { + var comparatorFactory = instanceCache.newInstance(config, determineComparatorFactoryPropertyName(config), + comparatorFactoryClass); + sorter = new ComparatorFactorySelectionSorter<>(comparatorFactory, SelectionSorterOrder.resolve(config.getSorterOrder())); } else if (config.getSorterClass() != null) { sorter = instanceCache.newInstance(config, "sorterClass", config.getSorterClass()); } else { throw new IllegalArgumentException(""" The valueSelectorConfig (%s) with resolvedSelectionOrder (%s) needs \ - a sorterManner (%s) or a sorterComparatorClass (%s) or a sorterWeightFactoryClass (%s) \ + a sorterManner (%s) or a %s (%s) or a %s (%s) \ or a sorterClass (%s).""" - .formatted(config, resolvedSelectionOrder, sorterManner, config.getSorterComparatorClass(), - config.getSorterWeightFactoryClass(), config.getSorterClass())); + .formatted(config, resolvedSelectionOrder, sorterManner, determineComparatorPropertyName(config), + comparatorClass, determineComparatorFactoryPropertyName(config), comparatorFactoryClass, + config.getSorterClass())); } if (!valueSelector.getVariableDescriptor().canExtractValueRangeFromSolution() && resolvedCacheType == SelectionCacheType.STEP) { diff --git a/core/src/main/resources/solver.xsd b/core/src/main/resources/solver.xsd index 9ebbdbd4c6..8c6e02fc1c 100644 --- a/core/src/main/resources/solver.xsd +++ b/core/src/main/resources/solver.xsd @@ -399,8 +399,12 @@ + + + + @@ -527,8 +531,12 @@ + + + + @@ -625,8 +633,12 @@ + + + + @@ -1742,6 +1754,10 @@ + + + + @@ -1760,6 +1776,14 @@ + + + + + + + + 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 7b8e81d577..def92dd06f 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 @@ -6,16 +6,46 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.SoftAssertions.assertSoftly; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Objects; 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.constructionheuristic.ConstructionHeuristicPhaseConfig; import ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicType; +import ai.timefold.solver.core.config.constructionheuristic.decider.forager.ConstructionHeuristicForagerConfig; +import ai.timefold.solver.core.config.constructionheuristic.decider.forager.ConstructionHeuristicPickEarlyType; +import ai.timefold.solver.core.config.constructionheuristic.placer.QueuedEntityPlacerConfig; +import ai.timefold.solver.core.config.constructionheuristic.placer.QueuedValuePlacerConfig; +import ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType; +import ai.timefold.solver.core.config.heuristic.selector.common.SelectionOrder; +import ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig; +import ai.timefold.solver.core.config.heuristic.selector.entity.EntitySorterManner; +import ai.timefold.solver.core.config.heuristic.selector.list.DestinationSelectorConfig; +import ai.timefold.solver.core.config.heuristic.selector.move.generic.ChangeMoveSelectorConfig; +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.testdomain.TestdataEntity; import ai.timefold.solver.core.testdomain.TestdataSolution; import ai.timefold.solver.core.testdomain.TestdataValue; +import ai.timefold.solver.core.testdomain.common.DummyHardSoftEasyScoreCalculator; +import ai.timefold.solver.core.testdomain.common.TestdataObjectSortableDescendingComparator; +import ai.timefold.solver.core.testdomain.common.TestdataObjectSortableDescendingFactory; +import ai.timefold.solver.core.testdomain.list.TestdataListEntity; +import ai.timefold.solver.core.testdomain.list.TestdataListSolution; +import ai.timefold.solver.core.testdomain.list.TestdataListValue; +import ai.timefold.solver.core.testdomain.list.sort.comparator.ListOneValuePerEntityEasyScoreCalculator; +import ai.timefold.solver.core.testdomain.list.sort.comparator.TestdataListSortableEntity; +import ai.timefold.solver.core.testdomain.list.sort.comparator.TestdataListSortableSolution; +import ai.timefold.solver.core.testdomain.list.sort.factory.ListOneValuePerEntityFactoryEasyScoreCalculator; +import ai.timefold.solver.core.testdomain.list.sort.factory.TestdataListFactorySortableEntity; +import ai.timefold.solver.core.testdomain.list.sort.factory.TestdataListFactorySortableSolution; +import ai.timefold.solver.core.testdomain.list.sort.invalid.TestdataInvalidListSortableEntity; +import ai.timefold.solver.core.testdomain.list.sort.invalid.TestdataInvalidListSortableSolution; import ai.timefold.solver.core.testdomain.list.unassignedvar.TestdataAllowsUnassignedValuesListEasyScoreCalculator; import ai.timefold.solver.core.testdomain.list.unassignedvar.TestdataAllowsUnassignedValuesListEntity; import ai.timefold.solver.core.testdomain.list.unassignedvar.TestdataAllowsUnassignedValuesListSolution; @@ -24,6 +54,12 @@ import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingScoreCalculator; 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.sort.comparator.ListOneValuePerEntityRangeEasyScoreCalculator; +import ai.timefold.solver.core.testdomain.list.valuerange.sort.comparator.TestdataListSortableEntityProvidingEntity; +import ai.timefold.solver.core.testdomain.list.valuerange.sort.comparator.TestdataListSortableEntityProvidingSolution; +import ai.timefold.solver.core.testdomain.list.valuerange.sort.factory.ListOneValuePerEntityRangeFactoryEasyScoreCalculator; +import ai.timefold.solver.core.testdomain.list.valuerange.sort.factory.TestdataListFactorySortableEntityProvidingEntity; +import ai.timefold.solver.core.testdomain.list.valuerange.sort.factory.TestdataListFactorySortableEntityProvidingSolution; import ai.timefold.solver.core.testdomain.mixed.singleentity.TestdataMixedEntity; import ai.timefold.solver.core.testdomain.mixed.singleentity.TestdataMixedOtherValue; import ai.timefold.solver.core.testdomain.mixed.singleentity.TestdataMixedSolution; @@ -32,17 +68,56 @@ import ai.timefold.solver.core.testdomain.pinned.TestdataPinnedSolution; import ai.timefold.solver.core.testdomain.pinned.unassignedvar.TestdataPinnedAllowsUnassignedEntity; import ai.timefold.solver.core.testdomain.pinned.unassignedvar.TestdataPinnedAllowsUnassignedSolution; +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.sort.comparatordifficulty.OneValuePerEntityDifficultyEasyScoreCalculator; +import ai.timefold.solver.core.testdomain.sort.comparatordifficulty.TestdataDifficultySortableEntity; +import ai.timefold.solver.core.testdomain.sort.comparatordifficulty.TestdataDifficultySortableSolution; +import ai.timefold.solver.core.testdomain.sort.factory.OneValuePerEntityFactoryEasyScoreCalculator; +import ai.timefold.solver.core.testdomain.sort.factory.TestdataFactorySortableEntity; +import ai.timefold.solver.core.testdomain.sort.factory.TestdataFactorySortableSolution; +import ai.timefold.solver.core.testdomain.sort.factorydifficulty.OneValuePerEntityDifficultyFactoryEasyScoreCalculator; +import ai.timefold.solver.core.testdomain.sort.factorydifficulty.TestdataDifficultyFactorySortableEntity; +import ai.timefold.solver.core.testdomain.sort.factorydifficulty.TestdataDifficultyFactorySortableSolution; +import ai.timefold.solver.core.testdomain.sort.invalid.mixed.comparator.TestdataInvalidMixedComparatorSortableEntity; +import ai.timefold.solver.core.testdomain.sort.invalid.mixed.comparator.TestdataInvalidMixedComparatorSortableSolution; +import ai.timefold.solver.core.testdomain.sort.invalid.mixed.strength.TestdataInvalidMixedStrengthSortableEntity; +import ai.timefold.solver.core.testdomain.sort.invalid.mixed.strength.TestdataInvalidMixedStrengthSortableSolution; +import ai.timefold.solver.core.testdomain.sort.invalid.twocomparator.entity.TestdataInvalidTwoEntityComparatorSortableEntity; +import ai.timefold.solver.core.testdomain.sort.invalid.twocomparator.entity.TestdataInvalidTwoEntityComparatorSortableSolution; +import ai.timefold.solver.core.testdomain.sort.invalid.twocomparator.value.TestdataInvalidTwoValueComparatorSortableEntity; +import ai.timefold.solver.core.testdomain.sort.invalid.twocomparator.value.TestdataInvalidTwoValueComparatorSortableSolution; +import ai.timefold.solver.core.testdomain.sort.invalid.twofactory.entity.TestdataInvalidTwoEntityFactorySortableEntity; +import ai.timefold.solver.core.testdomain.sort.invalid.twofactory.entity.TestdataInvalidTwoEntityFactorySortableSolution; +import ai.timefold.solver.core.testdomain.sort.invalid.twofactory.value.TestdataInvalidTwoValueFactorySortableEntity; +import ai.timefold.solver.core.testdomain.sort.invalid.twofactory.value.TestdataInvalidTwoValueFactorySortableSolution; 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.valuerange.entityproviding.unassignedvar.TestdataAllowsUnassignedEntityProvidingEntity; import ai.timefold.solver.core.testdomain.valuerange.entityproviding.unassignedvar.TestdataAllowsUnassignedEntityProvidingScoreCalculator; import ai.timefold.solver.core.testdomain.valuerange.entityproviding.unassignedvar.TestdataAllowsUnassignedEntityProvidingSolution; +import ai.timefold.solver.core.testdomain.valuerange.sort.comparator.OneValuePerEntityComparatorRangeEasyScoreCalculator; +import ai.timefold.solver.core.testdomain.valuerange.sort.comparator.TestdataComparatorSortableEntityProvidingEntity; +import ai.timefold.solver.core.testdomain.valuerange.sort.comparator.TestdataComparatorSortableEntityProvidingSolution; +import ai.timefold.solver.core.testdomain.valuerange.sort.comparatorstrength.OneValuePerEntityStrengthRangeEasyScoreCalculator; +import ai.timefold.solver.core.testdomain.valuerange.sort.comparatorstrength.TestdataStrengthSortableEntityProvidingEntity; +import ai.timefold.solver.core.testdomain.valuerange.sort.comparatorstrength.TestdataStrengthSortableEntityProvidingSolution; +import ai.timefold.solver.core.testdomain.valuerange.sort.factory.OneValuePerEntityFactoryRangeEasyScoreCalculator; +import ai.timefold.solver.core.testdomain.valuerange.sort.factory.TestdataFactorySortableEntityProvidingEntity; +import ai.timefold.solver.core.testdomain.valuerange.sort.factory.TestdataFactorySortableEntityProvidingSolution; +import ai.timefold.solver.core.testdomain.valuerange.sort.factorystrength.OneValuePerEntityStrengthFactoryRangeEasyScoreCalculator; +import ai.timefold.solver.core.testdomain.valuerange.sort.factorystrength.TestdataStrengthFactorySortableEntityProvidingEntity; +import ai.timefold.solver.core.testdomain.valuerange.sort.factorystrength.TestdataStrengthFactorySortableEntityProvidingSolution; import ai.timefold.solver.core.testutil.PlannerTestUtils; +import org.jspecify.annotations.NonNull; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; @Execution(ExecutionMode.CONCURRENT) class DefaultConstructionHeuristicPhaseTest { @@ -303,20 +378,1220 @@ void solveWithEntityValueRangeListVariable() { .hasSameElementsAs(List.of("v3")); } - @Test - void constructionHeuristicAllocateToValueFromQueue() { - var solverConfig = PlannerTestUtils.buildSolverConfig(TestdataSolution.class, TestdataEntity.class) - .withPhases(new ConstructionHeuristicPhaseConfig() - .withConstructionHeuristicType(ConstructionHeuristicType.ALLOCATE_TO_VALUE_FROM_QUEUE)); + private static List generateCommonConfiguration() { + var values = new ArrayList(); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withConstructionHeuristicType(ConstructionHeuristicType.FIRST_FIT_DECREASING) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // the entities are being read in decreasing order of difficulty, + // this is expected: e1[3], e2[2], and e3[1] + new int[] { 2, 1, 0 }, + // Only the entities are sorted, and shuffling the values will alter the expected result + false)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withConstructionHeuristicType(ConstructionHeuristicType.WEAKEST_FIT) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // the values are being read in increase order of strength, + // this is expected: e1[1], e2[2], and e3[3] + new int[] { 0, 1, 2 }, + // Only the values are sorted, and shuffling the entities will alter the expected result + false)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withConstructionHeuristicType(ConstructionHeuristicType.WEAKEST_FIT_DECREASING) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // the entities are being read in decreasing order of difficulty, + // and the values are being read in increase order of strength + // this is expected: e1[3], e2[2], and e3[1] + new int[] { 2, 1, 0 }, + // Both are sorted and the expected result won't be affected + true)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withConstructionHeuristicType(ConstructionHeuristicType.STRONGEST_FIT) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // and the values are being read in decreasing order of strength + // this is expected: e1[3], e2[2], and e3[1] + new int[] { 2, 1, 0 }, + // Only the values are sorted, and shuffling the entities will alter the expected result + false)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withConstructionHeuristicType(ConstructionHeuristicType.STRONGEST_FIT_DECREASING) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // the entities are being read in decreasing order of difficulty, + // and the values are being read in decreasing order of strength + // this is expected: e1[1], e2[2], and e3[3] + new int[] { 0, 1, 2 }, + // Both are sorted and the expected result won't be affected + true)); + // Allocate from pool + // Simple configuration + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withConstructionHeuristicType(ConstructionHeuristicType.ALLOCATE_TO_VALUE_FROM_QUEUE) + .withEntitySorterManner(EntitySorterManner.DECREASING_DIFFICULTY) + .withValueSorterManner(ValueSorterManner.DECREASING_STRENGTH) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // Since we are starting from decreasing strength + // and the entities are being read in decreasing order of difficulty, + // this is expected: e1[1], e2[2], and e3[3] + new int[] { 0, 1, 2 }, + // Both are sorted and the expected result won't be affected + true)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withConstructionHeuristicType(ConstructionHeuristicType.ALLOCATE_TO_VALUE_FROM_QUEUE) + .withEntitySorterManner(EntitySorterManner.DESCENDING) + .withValueSorterManner(ValueSorterManner.DESCENDING) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // Since we are starting from decreasing strength + // and the entities are being read in decreasing order of difficulty, + // this is expected: e1[1], e2[2], and e3[3] + new int[] { 0, 1, 2 }, + // Both are sorted and the expected result won't be affected + true)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withConstructionHeuristicType(ConstructionHeuristicType.ALLOCATE_TO_VALUE_FROM_QUEUE) + .withEntitySorterManner(EntitySorterManner.DECREASING_DIFFICULTY_IF_AVAILABLE) + .withValueSorterManner(ValueSorterManner.DECREASING_STRENGTH_IF_AVAILABLE) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // this is expected: e1[1], e2[2], and e3[3] + new int[] { 0, 1, 2 }, + // Both are sorted and the expected result won't be affected + true)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withConstructionHeuristicType(ConstructionHeuristicType.ALLOCATE_TO_VALUE_FROM_QUEUE) + .withEntitySorterManner(EntitySorterManner.DESCENDING_IF_AVAILABLE) + .withValueSorterManner(ValueSorterManner.DESCENDING_IF_AVAILABLE) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // this is expected: e1[1], e2[2], and e3[3] + new int[] { 0, 1, 2 }, + // Both are sorted and the expected result won't be affected + true)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withConstructionHeuristicType(ConstructionHeuristicType.ALLOCATE_TO_VALUE_FROM_QUEUE) + .withEntitySorterManner(EntitySorterManner.NONE) + .withValueSorterManner(ValueSorterManner.DECREASING_STRENGTH) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // this is expected: e1[3], e2[2], and e3[1] + new int[] { 2, 1, 0 }, + // Only the values are sorted, and shuffling the entities will alter the expected result + false)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withConstructionHeuristicType(ConstructionHeuristicType.ALLOCATE_TO_VALUE_FROM_QUEUE) + .withEntitySorterManner(EntitySorterManner.NONE) + .withValueSorterManner(ValueSorterManner.DESCENDING) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // this is expected: e1[3], e2[2], and e3[1] + new int[] { 2, 1, 0 }, + // Only the values are sorted, and shuffling the entities will alter the expected result + false)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withConstructionHeuristicType(ConstructionHeuristicType.ALLOCATE_TO_VALUE_FROM_QUEUE) + .withEntitySorterManner(EntitySorterManner.DECREASING_DIFFICULTY) + .withValueSorterManner(ValueSorterManner.INCREASING_STRENGTH) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // this is expected: e1[3], e2[2], and e3[1] + new int[] { 2, 1, 0 }, + // Both are sorted and the expected result won't be affected + true)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withConstructionHeuristicType(ConstructionHeuristicType.ALLOCATE_TO_VALUE_FROM_QUEUE) + .withEntitySorterManner(EntitySorterManner.DESCENDING) + .withValueSorterManner(ValueSorterManner.ASCENDING) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // this is expected: e1[3], e2[2], and e3[1] + new int[] { 2, 1, 0 }, + // Both are sorted and the expected result won't be affected + true)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withConstructionHeuristicType(ConstructionHeuristicType.ALLOCATE_TO_VALUE_FROM_QUEUE) + .withEntitySorterManner(EntitySorterManner.DECREASING_DIFFICULTY_IF_AVAILABLE) + .withValueSorterManner(ValueSorterManner.INCREASING_STRENGTH_IF_AVAILABLE) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // this is expected: e1[3], e2[2], and e3[1] + new int[] { 2, 1, 0 }, + // Both are sorted and the expected result won't be affected + true)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withConstructionHeuristicType(ConstructionHeuristicType.ALLOCATE_TO_VALUE_FROM_QUEUE) + .withEntitySorterManner(EntitySorterManner.DESCENDING_IF_AVAILABLE) + .withValueSorterManner(ValueSorterManner.ASCENDING_IF_AVAILABLE) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // this is expected: e1[3], e2[2], and e3[1] + new int[] { 2, 1, 0 }, + // Both are sorted and the expected result won't be affected + true)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withConstructionHeuristicType(ConstructionHeuristicType.ALLOCATE_TO_VALUE_FROM_QUEUE) + .withEntitySorterManner(EntitySorterManner.NONE) + .withValueSorterManner(ValueSorterManner.INCREASING_STRENGTH) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // this is expected: e1[1], e2[2], and e3[3] + new int[] { 0, 1, 2 }, + // Only the values are sorted, and shuffling the entities will alter the expected result + false)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withConstructionHeuristicType(ConstructionHeuristicType.ALLOCATE_TO_VALUE_FROM_QUEUE) + .withEntitySorterManner(EntitySorterManner.NONE) + .withValueSorterManner(ValueSorterManner.ASCENDING) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // this is expected: e1[1], e2[2], and e3[3] + new int[] { 0, 1, 2 }, + // Only the values are sorted, and shuffling the entities will alter the expected result + false)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withConstructionHeuristicType(ConstructionHeuristicType.CHEAPEST_INSERTION), + // Since we are starting from increasing strength + // and the entities are being read in decreasing order of difficulty, + // this is expected: e1[1], e2[2], and e3[3] + new int[] { 2, 1, 0 }, + // Both are sorted by default + true)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withConstructionHeuristicType(ConstructionHeuristicType.ALLOCATE_FROM_POOL), + // Since we are starting from increasing strength + // and the entities are being read in decreasing order of difficulty, + // this is expected: e1[1], e2[2], and e3[3] + new int[] { 2, 1, 0 }, + // Both are sorted by default + true)); + return values; + } - var solution = new TestdataSolution("s1"); - solution.setValueList(Arrays.asList(new TestdataValue("v1"), new TestdataValue("v2"))); - solution.setEntityList(Arrays.asList(new TestdataEntity("e1"))); + private static List + generateAdvancedBasicVariableConfiguration(SelectionCacheType entityDestinationCacheType) { + var values = new ArrayList(); + // Advanced configuration + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfig(new QueuedEntityPlacerConfig() + .withEntitySelectorConfig(new EntitySelectorConfig() + .withId("sortedEntitySelector") + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(SelectionCacheType.PHASE) + .withSorterManner(EntitySorterManner.DECREASING_DIFFICULTY)) + .withMoveSelectorConfigs(new ChangeMoveSelectorConfig() + .withEntitySelectorConfig( + new EntitySelectorConfig().withMimicSelectorRef("sortedEntitySelector")) + .withValueSelectorConfig( + new ValueSelectorConfig() + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(entityDestinationCacheType) + .withSorterManner(ValueSorterManner.DECREASING_STRENGTH)))) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // Since we are starting from decreasing strength + // and the entities are being read in decreasing order of difficulty, + // this is expected: e1[1], e2[2], and e3[3] + new int[] { 0, 1, 2 }, + // Both are sorted and the expected result won't be affected + true)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfig(new QueuedEntityPlacerConfig() + .withEntitySelectorConfig(new EntitySelectorConfig() + .withId("sortedEntitySelector") + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(SelectionCacheType.PHASE) + .withSorterManner(EntitySorterManner.DESCENDING)) + .withMoveSelectorConfigs(new ChangeMoveSelectorConfig() + .withEntitySelectorConfig( + new EntitySelectorConfig().withMimicSelectorRef("sortedEntitySelector")) + .withValueSelectorConfig( + new ValueSelectorConfig() + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(entityDestinationCacheType) + .withSorterManner(ValueSorterManner.DESCENDING)))) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // Since we are starting from decreasing strength + // and the entities are being read in decreasing order of difficulty, + // this is expected: e1[1], e2[2], and e3[3] + new int[] { 0, 1, 2 }, + // Both are sorted and the expected result won't be affected + true)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfig(new QueuedEntityPlacerConfig() + .withEntitySelectorConfig(new EntitySelectorConfig() + .withId("sortedEntitySelector") + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(SelectionCacheType.PHASE) + .withSorterManner(EntitySorterManner.DECREASING_DIFFICULTY)) + .withMoveSelectorConfigs(new ChangeMoveSelectorConfig() + .withEntitySelectorConfig( + new EntitySelectorConfig().withMimicSelectorRef("sortedEntitySelector")) + .withValueSelectorConfig( + new ValueSelectorConfig() + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(entityDestinationCacheType) + .withSorterManner(ValueSorterManner.INCREASING_STRENGTH)))) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // Since we are starting from increasing strength + // and the entities are being read in decreasing order of difficulty, + // this is expected: e1[3], e2[2], and e3[1] + new int[] { 2, 1, 0 }, + // Both are sorted and the expected result won't be affected + true)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfig(new QueuedEntityPlacerConfig() + .withEntitySelectorConfig(new EntitySelectorConfig() + .withId("sortedEntitySelector") + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(SelectionCacheType.PHASE) + .withSorterManner(EntitySorterManner.DESCENDING)) + .withMoveSelectorConfigs(new ChangeMoveSelectorConfig() + .withEntitySelectorConfig( + new EntitySelectorConfig().withMimicSelectorRef("sortedEntitySelector")) + .withValueSelectorConfig( + new ValueSelectorConfig() + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(entityDestinationCacheType) + .withSorterManner(ValueSorterManner.ASCENDING)))) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // Since we are starting from increasing strength + // and the entities are being read in decreasing order of difficulty, + // this is expected: e1[3], e2[2], and e3[1] + new int[] { 2, 1, 0 }, + // Both are sorted and the expected result won't be affected + true)); + return values; + } + + private static List generateBasicVariableConfiguration() { + var values = new ArrayList(); + values.addAll(generateCommonConfiguration()); + values.addAll(generateAdvancedBasicVariableConfiguration(SelectionCacheType.PHASE)); + return values; + } + + @ParameterizedTest + @MethodSource("generateBasicVariableConfiguration") + void solveStrengthBasicVariableQueueComparator(ConstructionHeuristicTestConfig phaseConfig) { + var solverConfig = PlannerTestUtils + .buildSolverConfig(TestdataDifficultySortableSolution.class, TestdataDifficultySortableEntity.class); + solverConfig.withEasyScoreCalculatorClass(OneValuePerEntityDifficultyEasyScoreCalculator.class); + solverConfig.withPhases(phaseConfig.config()); + + var solution = TestdataDifficultySortableSolution.generateSolution(3, 3, phaseConfig.shuffle()); + + solution = PlannerTestUtils.solve(solverConfig, solution); + assertThat(solution).isNotNull(); + if (phaseConfig.expected() != null) { + for (var i = 0; i < 3; i++) { + var id = "Generated Entity %d".formatted(i); + var entity = solution.getEntityList().stream() + .filter(e -> e.getCode().equals(id)) + .findFirst() + .orElseThrow(IllegalArgumentException::new); + assertThat(entity.getValue()).isNotNull(); + assertThat(entity.getValue().getComparatorValue()).isEqualTo(phaseConfig.expected[i]); + } + } + } + + @ParameterizedTest + @MethodSource("generateBasicVariableConfiguration") + void solveStrengthBasicVariableQueueFactory(ConstructionHeuristicTestConfig phaseConfig) { + var solverConfig = PlannerTestUtils + .buildSolverConfig(TestdataDifficultyFactorySortableSolution.class, + TestdataDifficultyFactorySortableEntity.class); + solverConfig.withEasyScoreCalculatorClass(OneValuePerEntityDifficultyFactoryEasyScoreCalculator.class); + solverConfig.withPhases(phaseConfig.config()); + + var solution = TestdataDifficultyFactorySortableSolution.generateSolution(3, 3, phaseConfig.shuffle()); + + solution = PlannerTestUtils.solve(solverConfig, solution); + assertThat(solution).isNotNull(); + if (phaseConfig.expected() != null) { + for (var i = 0; i < 3; i++) { + var id = "Generated Entity %d".formatted(i); + var entity = solution.getEntityList().stream() + .filter(e -> e.getCode().equals(id)) + .findFirst() + .orElseThrow(IllegalArgumentException::new); + assertThat(entity.getValue()).isNotNull(); + assertThat(entity.getValue().getComparatorValue()).isEqualTo(phaseConfig.expected[i]); + } + } + } + + @ParameterizedTest + @MethodSource("generateBasicVariableConfiguration") + void solveStrengthBasicVariableEntityRangeQueueComparator(ConstructionHeuristicTestConfig phaseConfig) { + var solverConfig = + PlannerTestUtils + .buildSolverConfig(TestdataStrengthSortableEntityProvidingSolution.class, + TestdataStrengthSortableEntityProvidingEntity.class) + .withEasyScoreCalculatorClass(OneValuePerEntityStrengthRangeEasyScoreCalculator.class) + .withPhases(phaseConfig.config()); + + var solution = TestdataStrengthSortableEntityProvidingSolution.generateSolution(3, 3, phaseConfig.shuffle()); + + solution = PlannerTestUtils.solve(solverConfig, solution); + assertThat(solution).isNotNull(); + if (phaseConfig.expected() != null) { + for (var i = 0; i < 3; i++) { + var id = "Generated Entity %d".formatted(i); + var entity = solution.getEntityList().stream() + .filter(e -> e.getCode().equals(id)) + .findFirst() + .orElseThrow(IllegalArgumentException::new); + assertThat(entity.getValue()).isNotNull(); + assertThat(entity.getValue().getComparatorValue()).isEqualTo(phaseConfig.expected[i]); + } + } + } + + @ParameterizedTest + @MethodSource("generateBasicVariableConfiguration") + void solveStrengthBasicVariableEntityRangeQueueFactory(ConstructionHeuristicTestConfig phaseConfig) { + var solverConfig = + PlannerTestUtils + .buildSolverConfig(TestdataStrengthFactorySortableEntityProvidingSolution.class, + TestdataStrengthFactorySortableEntityProvidingEntity.class) + .withEasyScoreCalculatorClass(OneValuePerEntityStrengthFactoryRangeEasyScoreCalculator.class) + .withPhases(phaseConfig.config()); + + var solution = TestdataStrengthFactorySortableEntityProvidingSolution.generateSolution(3, 3, phaseConfig.shuffle()); + + solution = PlannerTestUtils.solve(solverConfig, solution); + assertThat(solution).isNotNull(); + if (phaseConfig.expected() != null) { + for (var i = 0; i < 3; i++) { + var id = "Generated Entity %d".formatted(i); + var entity = solution.getEntityList().stream() + .filter(e -> e.getCode().equals(id)) + .findFirst() + .orElseThrow(IllegalArgumentException::new); + assertThat(entity.getValue()).isNotNull(); + assertThat(entity.getValue().getComparatorValue()).isEqualTo(phaseConfig.expected[i]); + } + } + } + + @ParameterizedTest + @MethodSource("generateBasicVariableConfiguration") + void solveBasicVariableQueueComparator(ConstructionHeuristicTestConfig phaseConfig) { + var solverConfig = PlannerTestUtils + .buildSolverConfig(TestdataComparatorSortableSolution.class, TestdataComparatorSortableEntity.class); + solverConfig.withEasyScoreCalculatorClass(OneValuePerEntityComparatorEasyScoreCalculator.class); + solverConfig.withPhases(phaseConfig.config()); + + var solution = TestdataComparatorSortableSolution.generateSolution(3, 3, phaseConfig.shuffle()); + + solution = PlannerTestUtils.solve(solverConfig, solution); + assertThat(solution).isNotNull(); + if (phaseConfig.expected() != null) { + for (var i = 0; i < 3; i++) { + var id = "Generated Entity %d".formatted(i); + var entity = solution.getEntityList().stream() + .filter(e -> e.getCode().equals(id)) + .findFirst() + .orElseThrow(IllegalArgumentException::new); + assertThat(entity.getValue()).isNotNull(); + assertThat(entity.getValue().getComparatorValue()).isEqualTo(phaseConfig.expected[i]); + } + } + } + + @ParameterizedTest + @MethodSource("generateBasicVariableConfiguration") + void solveBasicVariableQueueFactory(ConstructionHeuristicTestConfig phaseConfig) { + var solverConfig = PlannerTestUtils + .buildSolverConfig(TestdataFactorySortableSolution.class, TestdataFactorySortableEntity.class); + solverConfig.withEasyScoreCalculatorClass(OneValuePerEntityFactoryEasyScoreCalculator.class); + solverConfig.withPhases(phaseConfig.config()); + + var solution = TestdataFactorySortableSolution.generateSolution(3, 3, phaseConfig.shuffle()); + + solution = PlannerTestUtils.solve(solverConfig, solution); + assertThat(solution).isNotNull(); + if (phaseConfig.expected() != null) { + for (var i = 0; i < 3; i++) { + var id = "Generated Entity %d".formatted(i); + var entity = solution.getEntityList().stream() + .filter(e -> e.getCode().equals(id)) + .findFirst() + .orElseThrow(IllegalArgumentException::new); + assertThat(entity.getValue()).isNotNull(); + assertThat(entity.getValue().getComparatorValue()).isEqualTo(phaseConfig.expected[i]); + } + } + } + + @ParameterizedTest + @MethodSource("generateBasicVariableConfiguration") + void solveBasicVariableEntityRangeQueueComparator(ConstructionHeuristicTestConfig phaseConfig) { + var solverConfig = + PlannerTestUtils + .buildSolverConfig(TestdataComparatorSortableEntityProvidingSolution.class, + TestdataComparatorSortableEntityProvidingEntity.class) + .withEasyScoreCalculatorClass(OneValuePerEntityComparatorRangeEasyScoreCalculator.class) + .withPhases(phaseConfig.config()); + + var solution = TestdataComparatorSortableEntityProvidingSolution.generateSolution(3, 3, phaseConfig.shuffle()); + + solution = PlannerTestUtils.solve(solverConfig, solution); + assertThat(solution).isNotNull(); + if (phaseConfig.expected() != null) { + for (var i = 0; i < 3; i++) { + var id = "Generated Entity %d".formatted(i); + var entity = solution.getEntityList().stream() + .filter(e -> e.getCode().equals(id)) + .findFirst() + .orElseThrow(IllegalArgumentException::new); + assertThat(entity.getValue()).isNotNull(); + assertThat(entity.getValue().getComparatorValue()).isEqualTo(phaseConfig.expected[i]); + } + } + } + + @ParameterizedTest + @MethodSource("generateBasicVariableConfiguration") + void solveBasicVariableEntityRangeQueueFactory(ConstructionHeuristicTestConfig phaseConfig) { + var solverConfig = + PlannerTestUtils + .buildSolverConfig(TestdataFactorySortableEntityProvidingSolution.class, + TestdataFactorySortableEntityProvidingEntity.class) + .withEasyScoreCalculatorClass(OneValuePerEntityFactoryRangeEasyScoreCalculator.class) + .withPhases(phaseConfig.config()); + + var solution = TestdataFactorySortableEntityProvidingSolution.generateSolution(3, 3, phaseConfig.shuffle()); + + solution = PlannerTestUtils.solve(solverConfig, solution); + assertThat(solution).isNotNull(); + if (phaseConfig.expected() != null) { + for (var i = 0; i < 3; i++) { + var id = "Generated Entity %d".formatted(i); + var entity = solution.getEntityList().stream() + .filter(e -> e.getCode().equals(id)) + .findFirst() + .orElseThrow(IllegalArgumentException::new); + assertThat(entity.getValue()).isNotNull(); + assertThat(entity.getValue().getComparatorValue()).isEqualTo(phaseConfig.expected[i]); + } + } + } + + private static List + generateAdvancedListVariableConfiguration(SelectionCacheType entityDestinationCacheType) { + var values = new ArrayList(); + // Advanced configuration + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfig(new QueuedValuePlacerConfig() + .withValueSelectorConfig(new ValueSelectorConfig() + .withId("sortedValueSelector") + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(SelectionCacheType.PHASE) + .withSorterManner(ValueSorterManner.DECREASING_STRENGTH)) + .withMoveSelectorConfig(new ListChangeMoveSelectorConfig() + .withValueSelectorConfig( + new ValueSelectorConfig().withMimicSelectorRef("sortedValueSelector")) + .withDestinationSelectorConfig(new DestinationSelectorConfig() + .withValueSelectorConfig(new ValueSelectorConfig()) + .withEntitySelectorConfig(new EntitySelectorConfig() + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(entityDestinationCacheType) + .withSorterManner(EntitySorterManner.DECREASING_DIFFICULTY))))) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // Since we are starting from decreasing strength + // and the entities are being read in decreasing order of difficulty, + // this is expected: e1[1], e2[2], and e3[3] + new int[] { 0, 1, 2 }, + // Both are sorted and the expected result won't be affected + true)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfig(new QueuedValuePlacerConfig() + .withValueSelectorConfig(new ValueSelectorConfig() + .withId("sortedValueSelector") + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(SelectionCacheType.PHASE) + .withSorterManner(ValueSorterManner.DESCENDING)) + .withMoveSelectorConfig(new ListChangeMoveSelectorConfig() + .withValueSelectorConfig( + new ValueSelectorConfig().withMimicSelectorRef("sortedValueSelector")) + .withDestinationSelectorConfig(new DestinationSelectorConfig() + .withValueSelectorConfig(new ValueSelectorConfig()) + .withEntitySelectorConfig(new EntitySelectorConfig() + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(entityDestinationCacheType) + .withSorterManner(EntitySorterManner.DESCENDING))))) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // Since we are starting from decreasing strength + // and the entities are being read in decreasing order of difficulty, + // this is expected: e1[1], e2[2], and e3[3] + new int[] { 0, 1, 2 }, + // Both are sorted and the expected result won't be affected + true)); + var nonSortedEntityConfig = new EntitySelectorConfig(); + var isPhaseScope = entityDestinationCacheType == SelectionCacheType.PHASE; + if (isPhaseScope) { + // Hack to prevent the default sorting option, + // which is DECREASING_DIFFICULTY_IF_AVAILABLE + // This hack does not work with STEP scope + nonSortedEntityConfig.setSorterManner(EntitySorterManner.NONE); + nonSortedEntityConfig.setSelectionOrder(SelectionOrder.SORTED); + nonSortedEntityConfig.setCacheType(entityDestinationCacheType); + } + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfig(new QueuedValuePlacerConfig() + .withValueSelectorConfig(new ValueSelectorConfig() + .withId("sortedValueSelector") + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(SelectionCacheType.PHASE) + .withSorterManner(ValueSorterManner.DECREASING_STRENGTH)) + .withMoveSelectorConfig(new ListChangeMoveSelectorConfig() + .withValueSelectorConfig( + new ValueSelectorConfig().withMimicSelectorRef("sortedValueSelector")) + .withDestinationSelectorConfig(new DestinationSelectorConfig() + .withValueSelectorConfig(new ValueSelectorConfig()) + .withEntitySelectorConfig(nonSortedEntityConfig)))) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // this is expected: e1[3], e2[2], and e3[1] + // The step scope will apply the default entity sort manner + isPhaseScope ? new int[] { 2, 1, 0 } : new int[] { 0, 1, 2 }, + // Only the values are sorted, and shuffling the entities will alter the expected result + false)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfig(new QueuedValuePlacerConfig() + .withValueSelectorConfig(new ValueSelectorConfig() + .withId("sortedValueSelector") + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(SelectionCacheType.PHASE) + .withSorterManner(ValueSorterManner.DESCENDING)) + .withMoveSelectorConfig(new ListChangeMoveSelectorConfig() + .withValueSelectorConfig( + new ValueSelectorConfig().withMimicSelectorRef("sortedValueSelector")) + .withDestinationSelectorConfig(new DestinationSelectorConfig() + .withValueSelectorConfig(new ValueSelectorConfig()) + .withEntitySelectorConfig(nonSortedEntityConfig)))) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // this is expected: e1[3], e2[2], and e3[1] + // The step scope will apply the default entity sort manner + isPhaseScope ? new int[] { 2, 1, 0 } : new int[] { 0, 1, 2 }, + // Only the values are sorted, and shuffling the entities will alter the expected result + false)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfig(new QueuedValuePlacerConfig() + .withValueSelectorConfig(new ValueSelectorConfig() + .withId("sortedValueSelector") + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(SelectionCacheType.PHASE) + .withSorterManner(ValueSorterManner.INCREASING_STRENGTH)) + .withMoveSelectorConfig(new ListChangeMoveSelectorConfig() + .withValueSelectorConfig( + new ValueSelectorConfig().withMimicSelectorRef("sortedValueSelector")) + .withDestinationSelectorConfig(new DestinationSelectorConfig() + .withValueSelectorConfig(new ValueSelectorConfig()) + .withEntitySelectorConfig(new EntitySelectorConfig() + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(entityDestinationCacheType) + .withSorterManner(EntitySorterManner.DECREASING_DIFFICULTY))))) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // this is expected: e1[3], e2[2], and e3[1] + new int[] { 2, 1, 0 }, + // Both are sorted and the expected result won't be affected + true)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfig(new QueuedValuePlacerConfig() + .withValueSelectorConfig(new ValueSelectorConfig() + .withId("sortedValueSelector") + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(SelectionCacheType.PHASE) + .withSorterManner(ValueSorterManner.ASCENDING)) + .withMoveSelectorConfig(new ListChangeMoveSelectorConfig() + .withValueSelectorConfig( + new ValueSelectorConfig().withMimicSelectorRef("sortedValueSelector")) + .withDestinationSelectorConfig(new DestinationSelectorConfig() + .withValueSelectorConfig(new ValueSelectorConfig()) + .withEntitySelectorConfig(new EntitySelectorConfig() + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(entityDestinationCacheType) + .withSorterManner(EntitySorterManner.DESCENDING))))) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // this is expected: e1[3], e2[2], and e3[1] + new int[] { 2, 1, 0 }, + // Both are sorted and the expected result won't be affected + true)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfig(new QueuedValuePlacerConfig() + .withValueSelectorConfig(new ValueSelectorConfig() + .withId("sortedValueSelector") + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(SelectionCacheType.PHASE) + .withSorterManner(ValueSorterManner.INCREASING_STRENGTH)) + .withMoveSelectorConfig(new ListChangeMoveSelectorConfig() + .withValueSelectorConfig( + new ValueSelectorConfig().withMimicSelectorRef("sortedValueSelector")) + .withDestinationSelectorConfig(new DestinationSelectorConfig() + .withValueSelectorConfig(new ValueSelectorConfig()) + .withEntitySelectorConfig(nonSortedEntityConfig)))) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // this is expected: e1[1], e2[2], and e3[3] + // The step scope will apply the default entity sort manner + isPhaseScope ? new int[] { 0, 1, 2 } : new int[] { 2, 1, 0 }, + // Only the values are sorted, and shuffling the entities will alter the expected result + false)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfig(new QueuedValuePlacerConfig() + .withValueSelectorConfig(new ValueSelectorConfig() + .withId("sortedValueSelector") + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(SelectionCacheType.PHASE) + .withSorterManner(ValueSorterManner.ASCENDING)) + .withMoveSelectorConfig(new ListChangeMoveSelectorConfig() + .withValueSelectorConfig( + new ValueSelectorConfig().withMimicSelectorRef("sortedValueSelector")) + .withDestinationSelectorConfig(new DestinationSelectorConfig() + .withValueSelectorConfig(new ValueSelectorConfig()) + .withEntitySelectorConfig(nonSortedEntityConfig)))) + .withForagerConfig(new ConstructionHeuristicForagerConfig().withPickEarlyType( + ConstructionHeuristicPickEarlyType.FIRST_FEASIBLE_SCORE_OR_NON_DETERIORATING_HARD)), + // this is expected: e1[1], e2[2], and e3[3] + // The step scope will apply the default entity sort manner + isPhaseScope ? new int[] { 0, 1, 2 } : new int[] { 2, 1, 0 }, + // Only the values are sorted, and shuffling the entities will alter the expected result + false)); + return values; + } + + private static List generateListVariableConfiguration() { + var values = new ArrayList(); + values.addAll(generateCommonConfiguration()); + values.addAll(generateAdvancedListVariableConfiguration(SelectionCacheType.PHASE)); + return values; + } + + @ParameterizedTest + @MethodSource("generateListVariableConfiguration") + void solveListVariableQueueComparator(ConstructionHeuristicTestConfig phaseConfig) { + var solverConfig = + PlannerTestUtils + .buildSolverConfig(TestdataListSortableSolution.class, TestdataListSortableEntity.class) + .withEasyScoreCalculatorClass(ListOneValuePerEntityEasyScoreCalculator.class) + .withPhases(phaseConfig.config()); + + var solution = TestdataListSortableSolution.generateSolution(3, 3, phaseConfig.shuffle()); + + solution = PlannerTestUtils.solve(solverConfig, solution); + assertThat(solution).isNotNull(); + if (phaseConfig.expected() != null) { + for (var i = 0; i < 3; i++) { + var id = "Generated Entity %d".formatted(i); + var entity = solution.getEntityList().stream() + .filter(e -> e.getCode().equals(id)) + .findFirst() + .orElseThrow(IllegalArgumentException::new); + assertThat(entity.getValueList()).hasSize(1); + assertThat(entity.getValueList().get(0).getComparatorValue()).isEqualTo(phaseConfig.expected[i]); + } + } + } + + @ParameterizedTest + @MethodSource("generateListVariableConfiguration") + void solveListVariableQueueFactory(ConstructionHeuristicTestConfig phaseConfig) { + var solverConfig = + PlannerTestUtils + .buildSolverConfig(TestdataListFactorySortableSolution.class, TestdataListFactorySortableEntity.class) + .withEasyScoreCalculatorClass(ListOneValuePerEntityFactoryEasyScoreCalculator.class) + .withPhases(phaseConfig.config()); + + var solution = TestdataListFactorySortableSolution.generateSolution(3, 3, phaseConfig.shuffle()); + + solution = PlannerTestUtils.solve(solverConfig, solution); + assertThat(solution).isNotNull(); + if (phaseConfig.expected() != null) { + for (var i = 0; i < 3; i++) { + var id = "Generated Entity %d".formatted(i); + var entity = solution.getEntityList().stream() + .filter(e -> e.getCode().equals(id)) + .findFirst() + .orElseThrow(IllegalArgumentException::new); + assertThat(entity.getValueList()).hasSize(1); + assertThat(entity.getValueList().get(0).getComparatorValue()).isEqualTo(phaseConfig.expected[i]); + } + } + } + + private static List generateListVariableEntityRangeConfiguration() { + var values = new ArrayList(); + values.addAll(generateCommonConfiguration()); + values.addAll(generateAdvancedListVariableConfiguration(SelectionCacheType.STEP)); + return values; + } + + @ParameterizedTest + @MethodSource("generateListVariableEntityRangeConfiguration") + void solveListVariableEntityRangeQueueComparator(ConstructionHeuristicTestConfig phaseConfig) { + var solverConfig = + PlannerTestUtils + .buildSolverConfig(TestdataListSortableEntityProvidingSolution.class, + TestdataListSortableEntityProvidingEntity.class) + .withEasyScoreCalculatorClass(ListOneValuePerEntityRangeEasyScoreCalculator.class) + .withPhases(phaseConfig.config()); + + var solution = TestdataListSortableEntityProvidingSolution.generateSolution(3, 3, phaseConfig.shuffle()); + + solution = PlannerTestUtils.solve(solverConfig, solution); + assertThat(solution).isNotNull(); + if (phaseConfig.expected() != null) { + for (var i = 0; i < 3; i++) { + var id = "Generated Entity %d".formatted(i); + var entity = solution.getEntityList().stream() + .filter(e -> e.getCode().equals(id)) + .findFirst() + .orElseThrow(IllegalArgumentException::new); + assertThat(entity.getValueList()).hasSize(1); + assertThat(entity.getValueList().get(0).getComparatorValue()).isEqualTo(phaseConfig.expected[i]); + } + } + } + + @ParameterizedTest + @MethodSource("generateListVariableEntityRangeConfiguration") + void solveListVariableEntityRangeQueueFactory(ConstructionHeuristicTestConfig phaseConfig) { + var solverConfig = + PlannerTestUtils + .buildSolverConfig(TestdataListFactorySortableEntityProvidingSolution.class, + TestdataListFactorySortableEntityProvidingEntity.class) + .withEasyScoreCalculatorClass(ListOneValuePerEntityRangeFactoryEasyScoreCalculator.class) + .withPhases(phaseConfig.config()); + + var solution = TestdataListFactorySortableEntityProvidingSolution.generateSolution(3, 3, + phaseConfig.shuffle()); + + solution = PlannerTestUtils.solve(solverConfig, solution); + assertThat(solution).isNotNull(); + if (phaseConfig.expected() != null) { + for (var i = 0; i < 3; i++) { + var id = "Generated Entity %d".formatted(i); + var entity = solution.getEntityList().stream() + .filter(e -> e.getCode().equals(id)) + .findFirst() + .orElseThrow(IllegalArgumentException::new); + assertThat(entity.getValueList()).hasSize(1); + assertThat(entity.getValueList().get(0).getComparatorValue()).isEqualTo(phaseConfig.expected[i]); + } + } + } + + private static List generateEntityFactorySortingConfiguration() { + var values = new ArrayList(); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfig(new QueuedValuePlacerConfig() + .withValueSelectorConfig(new ValueSelectorConfig().withId("sortedValueSelector")) + .withMoveSelectorConfig(new ChangeMoveSelectorConfig() + .withEntitySelectorConfig(new EntitySelectorConfig() + .withId("sortedEntitySelector") + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(SelectionCacheType.PHASE) + .withSorterWeightFactoryClass(TestdataObjectSortableDescendingFactory.class)) + .withValueSelectorConfig( + new ValueSelectorConfig() + .withMimicSelectorRef("sortedValueSelector")) + .withValueSelectorConfig(new ValueSelectorConfig()))), + new int[] { 2, 1, 0 }, + // Only entities are sorted in descending order + false)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfig(new QueuedValuePlacerConfig() + .withValueSelectorConfig(new ValueSelectorConfig().withId("sortedValueSelector")) + .withMoveSelectorConfig(new ChangeMoveSelectorConfig() + .withEntitySelectorConfig(new EntitySelectorConfig() + .withId("sortedEntitySelector") + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(SelectionCacheType.PHASE) + .withComparatorFactoryClass(TestdataObjectSortableDescendingFactory.class)) + .withValueSelectorConfig( + new ValueSelectorConfig() + .withMimicSelectorRef("sortedValueSelector")) + .withValueSelectorConfig(new ValueSelectorConfig()))), + new int[] { 2, 1, 0 }, + // Only entities are sorted in descending order + false)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfig(new QueuedValuePlacerConfig() + .withValueSelectorConfig(new ValueSelectorConfig().withId("sortedValueSelector")) + .withMoveSelectorConfig(new ChangeMoveSelectorConfig() + .withEntitySelectorConfig(new EntitySelectorConfig() + .withId("sortedEntitySelector") + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(SelectionCacheType.PHASE) + .withSorterComparatorClass(TestdataObjectSortableDescendingComparator.class)) + .withValueSelectorConfig( + new ValueSelectorConfig() + .withMimicSelectorRef("sortedValueSelector")) + .withValueSelectorConfig(new ValueSelectorConfig()))), + new int[] { 2, 1, 0 }, + // Only entities are sorted in descending order + false)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfig(new QueuedValuePlacerConfig() + .withValueSelectorConfig(new ValueSelectorConfig().withId("sortedValueSelector")) + .withMoveSelectorConfig(new ChangeMoveSelectorConfig() + .withEntitySelectorConfig(new EntitySelectorConfig() + .withId("sortedEntitySelector") + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(SelectionCacheType.PHASE) + .withComparatorClass(TestdataObjectSortableDescendingComparator.class)) + .withValueSelectorConfig( + new ValueSelectorConfig() + .withMimicSelectorRef("sortedValueSelector")) + .withValueSelectorConfig(new ValueSelectorConfig()))), + new int[] { 2, 1, 0 }, + // Only entities are sorted in descending order + false)); + return values; + } + + @ParameterizedTest + @MethodSource("generateEntityFactorySortingConfiguration") + void solveEntityFactorySorting(ConstructionHeuristicTestConfig phaseConfig) { + var solverConfig = + PlannerTestUtils + .buildSolverConfig(TestdataListSolution.class, TestdataListEntity.class, TestdataListValue.class) + .withEasyScoreCalculatorClass(TestdataListSolutionEasyScoreCalculator.class) + .withPhases(phaseConfig.config()); + + var solution = TestdataListSolution.generateUninitializedSolution(3, 3); + + solution = PlannerTestUtils.solve(solverConfig, solution); + assertThat(solution).isNotNull(); + if (phaseConfig.expected() != null) { + for (var i = 0; i < 3; i++) { + var id = "Generated Entity %d".formatted(i); + var entity = solution.getEntityList().stream() + .filter(e -> e.getCode().equals(id)) + .findFirst() + .orElseThrow(IllegalArgumentException::new); + assertThat(entity.getValueList()).hasSize(1); + assertThat(TestdataObjectSortableDescendingFactory.extractCode(entity.getValueList().get(0).getCode())) + .isEqualTo(phaseConfig.expected[i]); + } + } + } + + private static List generateValueFactorySortingConfiguration() { + var values = new ArrayList(); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfig(new QueuedEntityPlacerConfig() + .withEntitySelectorConfig(new EntitySelectorConfig().withId("sortedEntitySelector")) + .withMoveSelectorConfigs(new ChangeMoveSelectorConfig() + .withEntitySelectorConfig( + new EntitySelectorConfig().withMimicSelectorRef("sortedEntitySelector")) + .withValueSelectorConfig(new ValueSelectorConfig() + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(SelectionCacheType.PHASE) + .withSorterWeightFactoryClass(TestdataObjectSortableDescendingFactory.class)))), + new int[] { 2, 1, 0 }, + // Only values are sorted in descending order + false)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfig(new QueuedEntityPlacerConfig() + .withEntitySelectorConfig(new EntitySelectorConfig().withId("sortedEntitySelector")) + .withMoveSelectorConfigs(new ChangeMoveSelectorConfig() + .withEntitySelectorConfig( + new EntitySelectorConfig().withMimicSelectorRef("sortedEntitySelector")) + .withValueSelectorConfig(new ValueSelectorConfig() + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(SelectionCacheType.PHASE) + .withComparatorFactoryClass(TestdataObjectSortableDescendingFactory.class)))), + new int[] { 2, 1, 0 }, + // Only values are sorted in descending order + false)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfig(new QueuedEntityPlacerConfig() + .withEntitySelectorConfig(new EntitySelectorConfig().withId("sortedEntitySelector")) + .withMoveSelectorConfigs(new ChangeMoveSelectorConfig() + .withEntitySelectorConfig( + new EntitySelectorConfig().withMimicSelectorRef("sortedEntitySelector")) + .withValueSelectorConfig(new ValueSelectorConfig() + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(SelectionCacheType.PHASE) + .withSorterComparatorClass(TestdataObjectSortableDescendingComparator.class)))), + new int[] { 2, 1, 0 }, + // Only values are sorted in descending order + false)); + values.add(new ConstructionHeuristicTestConfig( + new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfig(new QueuedEntityPlacerConfig() + .withEntitySelectorConfig(new EntitySelectorConfig().withId("sortedEntitySelector")) + .withMoveSelectorConfigs(new ChangeMoveSelectorConfig() + .withEntitySelectorConfig( + new EntitySelectorConfig().withMimicSelectorRef("sortedEntitySelector")) + .withValueSelectorConfig(new ValueSelectorConfig() + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(SelectionCacheType.PHASE) + .withComparatorClass(TestdataObjectSortableDescendingComparator.class)))), + new int[] { 2, 1, 0 }, + // Only values are sorted in descending order + false)); + return values; + } + + @ParameterizedTest + @MethodSource("generateValueFactorySortingConfiguration") + void solveValueFactorySorting(ConstructionHeuristicTestConfig phaseConfig) { + var solverConfig = + PlannerTestUtils + .buildSolverConfig(TestdataSolution.class, TestdataEntity.class) + .withEasyScoreCalculatorClass(TestdataSolutionEasyScoreCalculator.class) + .withPhases(phaseConfig.config()); + + var solution = TestdataSolution.generateUninitializedSolution(3, 3); solution = PlannerTestUtils.solve(solverConfig, solution); assertThat(solution).isNotNull(); - assertThat(solution.getEntityList().stream() - .filter(e -> e.getValue() == null)).isEmpty(); + if (phaseConfig.expected() != null) { + for (var i = 0; i < 3; i++) { + var id = "Generated Entity %d".formatted(i); + var entity = solution.getEntityList().stream() + .filter(e -> e.getCode().equals(id)) + .findFirst() + .orElseThrow(IllegalArgumentException::new); + assertThat(entity.getValue()).isNotNull(); + assertThat(TestdataObjectSortableDescendingFactory.extractCode(entity.getValue().getCode())) + .isEqualTo(phaseConfig.expected[i]); + } + } + } + + @Test + void failConstructionHeuristicEntityRange() { + var solverConfig = + PlannerTestUtils + .buildSolverConfig(TestdataListSortableEntityProvidingSolution.class, + TestdataListSortableEntityProvidingEntity.class) + .withEasyScoreCalculatorClass(ListOneValuePerEntityRangeEasyScoreCalculator.class) + .withPhases( + new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfig(new QueuedValuePlacerConfig() + .withValueSelectorConfig(new ValueSelectorConfig() + .withId("sortedValueSelector") + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(SelectionCacheType.PHASE) + .withSorterManner(ValueSorterManner.DECREASING_STRENGTH)) + .withMoveSelectorConfig(new ListChangeMoveSelectorConfig() + .withValueSelectorConfig( + new ValueSelectorConfig() + .withMimicSelectorRef("sortedValueSelector")) + .withDestinationSelectorConfig(new DestinationSelectorConfig() + .withValueSelectorConfig(new ValueSelectorConfig()) + .withEntitySelectorConfig(new EntitySelectorConfig() + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(SelectionCacheType.PHASE) + .withSorterManner( + EntitySorterManner.DECREASING_DIFFICULTY)))))); + var solution = TestdataListSortableEntityProvidingSolution.generateSolution(3, 3, true); + assertThatCode(() -> PlannerTestUtils.solve(solverConfig, solution)) + .hasMessageContaining("resolvedSelectionOrder (SORTED) which does not support the resolvedCacheType (PHASE)") + .hasMessageContaining("Maybe set the \"cacheType\" to STEP."); + + var solverConfig2 = + PlannerTestUtils + .buildSolverConfig(TestdataListSortableEntityProvidingSolution.class, + TestdataListSortableEntityProvidingEntity.class) + .withEasyScoreCalculatorClass(ListOneValuePerEntityRangeEasyScoreCalculator.class) + .withPhases( + new ConstructionHeuristicPhaseConfig() + .withEntityPlacerConfig(new QueuedValuePlacerConfig() + .withValueSelectorConfig(new ValueSelectorConfig() + .withId("sortedValueSelector") + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(SelectionCacheType.PHASE) + .withSorterManner(ValueSorterManner.DESCENDING)) + .withMoveSelectorConfig(new ListChangeMoveSelectorConfig() + .withValueSelectorConfig( + new ValueSelectorConfig() + .withMimicSelectorRef("sortedValueSelector")) + .withDestinationSelectorConfig(new DestinationSelectorConfig() + .withValueSelectorConfig(new ValueSelectorConfig()) + .withEntitySelectorConfig(new EntitySelectorConfig() + .withSelectionOrder(SelectionOrder.SORTED) + .withCacheType(SelectionCacheType.PHASE) + .withSorterManner( + EntitySorterManner.DESCENDING)))))); + assertThatCode(() -> PlannerTestUtils.solve(solverConfig2, solution)) + .hasMessageContaining("resolvedSelectionOrder (SORTED) which does not support the resolvedCacheType (PHASE)") + .hasMessageContaining("Maybe set the \"cacheType\" to STEP."); + } + + @Test + void failConstructionHeuristicListMixedProperties() { + var solverConfig = + PlannerTestUtils + .buildSolverConfig(TestdataInvalidListSortableSolution.class, + TestdataInvalidListSortableEntity.class) + .withEasyScoreCalculatorClass(DummyHardSoftEasyScoreCalculator.class); + var solution = new TestdataInvalidListSortableSolution(); + assertThatCode(() -> PlannerTestUtils.solve(solverConfig, solution)) + .hasMessageContaining( + "The entityClass (class ai.timefold.solver.core.testdomain.list.sort.invalid.TestdataInvalidListSortableEntity) property (valueList)") + .hasMessageContaining( + "cannot have a comparatorClass (ai.timefold.solver.core.testdomain.common.DummyValueComparator)") + .hasMessageContaining( + "comparatorFactoryClass (ai.timefold.solver.core.testdomain.common.DummyValueFactory) at the same time."); + } + + @Test + void failConstructionHeuristicMixedProperties() { + // Strength and Factory properties + var solverConfig = + PlannerTestUtils + .buildSolverConfig(TestdataInvalidMixedStrengthSortableSolution.class, + TestdataInvalidMixedStrengthSortableEntity.class) + .withEasyScoreCalculatorClass(DummyHardSoftEasyScoreCalculator.class); + var solution = new TestdataInvalidMixedStrengthSortableSolution(); + assertThatCode(() -> PlannerTestUtils.solve(solverConfig, solution)) + .hasMessageContaining( + "The entityClass (class ai.timefold.solver.core.testdomain.sort.invalid.mixed.strength.TestdataInvalidMixedStrengthSortableEntity) property (value)") + .hasMessageContaining( + "cannot have a strengthComparatorClass (ai.timefold.solver.core.testdomain.common.DummyValueComparator)") + .hasMessageContaining( + "strengthWeightFactoryClass (ai.timefold.solver.core.testdomain.common.DummyWeightValueFactory) at the same time."); + + // Comparator and Factory properties + var otherSolverConfig = + PlannerTestUtils + .buildSolverConfig(TestdataInvalidMixedComparatorSortableSolution.class, + TestdataInvalidMixedComparatorSortableEntity.class) + .withEasyScoreCalculatorClass(DummyHardSoftEasyScoreCalculator.class); + var otherSolution = new TestdataInvalidMixedComparatorSortableSolution(); + assertThatCode(() -> PlannerTestUtils.solve(otherSolverConfig, otherSolution)) + .hasMessageContaining( + "The entityClass (class ai.timefold.solver.core.testdomain.sort.invalid.mixed.comparator.TestdataInvalidMixedComparatorSortableEntity) property (value)") + .hasMessageContaining( + "cannot have a comparatorClass (ai.timefold.solver.core.testdomain.common.DummyValueComparator)") + .hasMessageContaining( + "comparatorFactoryClass (ai.timefold.solver.core.testdomain.common.DummyValueFactory) at the same time."); + } + + @Test + void failConstructionHeuristicBothProperties() { + // Value + { + // Two comparator properties + var solverConfig = + PlannerTestUtils + .buildSolverConfig(TestdataInvalidTwoValueComparatorSortableSolution.class, + TestdataInvalidTwoValueComparatorSortableEntity.class) + .withEasyScoreCalculatorClass(DummyHardSoftEasyScoreCalculator.class); + var solution = new TestdataInvalidTwoValueComparatorSortableSolution(); + assertThatCode(() -> PlannerTestUtils.solve(solverConfig, solution)) + .hasMessageContaining( + "The entityClass (class ai.timefold.solver.core.testdomain.sort.invalid.twocomparator.value.TestdataInvalidTwoValueComparatorSortableEntity) property (value)") + .hasMessageContaining( + "cannot have a strengthComparatorClass (ai.timefold.solver.core.testdomain.common.DummyValueComparator)") + .hasMessageContaining( + "and a comparatorClass (ai.timefold.solver.core.testdomain.common.DummyValueComparator) at the same time."); + + // Comparator and Factory properties + var otherSolverConfig = + PlannerTestUtils + .buildSolverConfig(TestdataInvalidTwoValueFactorySortableSolution.class, + TestdataInvalidTwoValueFactorySortableEntity.class) + .withEasyScoreCalculatorClass(DummyHardSoftEasyScoreCalculator.class); + var otherSolution = new TestdataInvalidTwoValueFactorySortableSolution(); + assertThatCode(() -> PlannerTestUtils.solve(otherSolverConfig, otherSolution)) + .hasMessageContaining( + "The entityClass (class ai.timefold.solver.core.testdomain.sort.invalid.twofactory.value.TestdataInvalidTwoValueFactorySortableEntity) property (value)") + .hasMessageContaining( + "cannot have a strengthWeightFactoryClass (ai.timefold.solver.core.testdomain.common.DummyWeightValueFactory)") + .hasMessageContaining( + "comparatorFactoryClass (ai.timefold.solver.core.testdomain.common.DummyValueFactory) at the same time."); + } + // Entity + { + // Two comparator properties + var solverConfig = + PlannerTestUtils + .buildSolverConfig(TestdataInvalidTwoEntityComparatorSortableSolution.class, + TestdataInvalidTwoEntityComparatorSortableEntity.class) + .withEasyScoreCalculatorClass(DummyHardSoftEasyScoreCalculator.class); + var solution = new TestdataInvalidTwoEntityComparatorSortableSolution(); + assertThatCode(() -> PlannerTestUtils.solve(solverConfig, solution)) + .hasMessageContaining( + "The entityClass (class ai.timefold.solver.core.testdomain.sort.invalid.twocomparator.entity.TestdataInvalidTwoEntityComparatorSortableEntity)") + .hasMessageContaining( + "cannot have a difficultyComparatorClass (ai.timefold.solver.core.testdomain.common.DummyValueComparator)") + .hasMessageContaining( + "and a comparatorClass (ai.timefold.solver.core.testdomain.common.DummyValueComparator) at the same time."); + + // Comparator and Factory properties + var otherSolverConfig = + PlannerTestUtils + .buildSolverConfig(TestdataInvalidTwoEntityFactorySortableSolution.class, + TestdataInvalidTwoEntityFactorySortableEntity.class) + .withEasyScoreCalculatorClass(DummyHardSoftEasyScoreCalculator.class); + var otherSolution = new TestdataInvalidTwoEntityFactorySortableSolution(); + assertThatCode(() -> PlannerTestUtils.solve(otherSolverConfig, otherSolution)) + .hasMessageContaining( + "The entityClass (class ai.timefold.solver.core.testdomain.sort.invalid.twofactory.entity.TestdataInvalidTwoEntityFactorySortableEntity)") + .hasMessageContaining( + "cannot have a difficultyWeightFactoryClass (ai.timefold.solver.core.testdomain.common.DummyValueFactory)") + .hasMessageContaining( + "comparatorFactoryClass (ai.timefold.solver.core.testdomain.common.DummyValueFactory) at the same time."); + } } @Test @@ -329,4 +1604,48 @@ void failMixedModelDefaultConfiguration() { .hasMessageContaining( "has both basic and list variables and cannot be deduced automatically"); } + + public static class TestdataListSolutionEasyScoreCalculator + implements EasyScoreCalculator { + + @Override + public @NonNull SimpleScore calculateScore(@NonNull TestdataListSolution solution) { + var score = 0; + for (var entity : solution.getEntityList()) { + if (entity.getValueList().size() <= 1) { + score -= 1; + } else { + score -= 10; + } + score--; + } + return SimpleScore.of(score); + } + } + + public static class TestdataSolutionEasyScoreCalculator + implements EasyScoreCalculator { + + @Override + public @NonNull SimpleScore + calculateScore(@NonNull TestdataSolution solution) { + var score = 0; + var distinct = (int) solution.getEntityList().stream() + .map(TestdataEntity::getValue) + .filter(Objects::nonNull) + .distinct() + .count(); + var assigned = solution.getEntityList().stream() + .map(TestdataEntity::getValue) + .filter(Objects::nonNull) + .count(); + var repeated = (int) (assigned - distinct); + score -= repeated; + return SimpleScore.of(score); + } + } + + private record ConstructionHeuristicTestConfig(ConstructionHeuristicPhaseConfig config, int[] expected, boolean shuffle) { + + } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/WeightFactorySelectionSorterTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorFactorySelectionSorterTest.java similarity index 63% rename from core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/WeightFactorySelectionSorterTest.java rename to core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorFactorySelectionSorterTest.java index e820011ca0..3ff74694ab 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/WeightFactorySelectionSorterTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/common/decorator/ComparatorFactorySelectionSorterTest.java @@ -4,8 +4,10 @@ import static org.mockito.Mockito.mock; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; +import ai.timefold.solver.core.api.domain.common.ComparatorFactory; import ai.timefold.solver.core.api.score.director.ScoreDirector; import ai.timefold.solver.core.config.heuristic.selector.common.decorator.SelectionSorterOrder; import ai.timefold.solver.core.testdomain.TestdataEntity; @@ -13,14 +15,15 @@ import org.junit.jupiter.api.Test; -class WeightFactorySelectionSorterTest { +class ComparatorFactorySelectionSorterTest { @Test void sortAscending() { - SelectionSorterWeightFactory weightFactory = (solution, selection) -> Integer - .valueOf(selection.getCode().charAt(0)); - WeightFactorySelectionSorter selectionSorter = new WeightFactorySelectionSorter<>( - weightFactory, SelectionSorterOrder.ASCENDING); + ComparatorFactory comparatorFactory = + sol -> Comparator.comparingInt(v -> Integer.valueOf(v.getCode().charAt(0))); + ComparatorFactorySelectionSorter selectionSorter = + new ComparatorFactorySelectionSorter<>( + comparatorFactory, SelectionSorterOrder.ASCENDING); ScoreDirector scoreDirector = mock(ScoreDirector.class); List selectionList = new ArrayList<>(); selectionList.add(new TestdataEntity("C")); @@ -33,10 +36,11 @@ void sortAscending() { @Test void sortDescending() { - SelectionSorterWeightFactory weightFactory = (solution, selection) -> Integer - .valueOf(selection.getCode().charAt(0)); - WeightFactorySelectionSorter selectionSorter = new WeightFactorySelectionSorter<>( - weightFactory, SelectionSorterOrder.DESCENDING); + ComparatorFactory comparatorFactory = + sol -> Comparator.comparingInt(v -> Integer.valueOf(v.getCode().charAt(0))); + ComparatorFactorySelectionSorter selectionSorter = + new ComparatorFactorySelectionSorter<>( + comparatorFactory, SelectionSorterOrder.DESCENDING); ScoreDirector scoreDirector = mock(ScoreDirector.class); List selectionList = new ArrayList<>(); selectionList.add(new TestdataEntity("C")); diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/entity/EntitySelectorFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/entity/EntitySelectorFactoryTest.java index 6335c09f87..f6f403ca98 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/entity/EntitySelectorFactoryTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/entity/EntitySelectorFactoryTest.java @@ -11,6 +11,7 @@ import ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType; import ai.timefold.solver.core.config.heuristic.selector.common.SelectionOrder; import ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig; +import ai.timefold.solver.core.config.heuristic.selector.entity.EntitySorterManner; import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; import ai.timefold.solver.core.impl.heuristic.selector.SelectorTestUtils; import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionProbabilityWeightFactory; @@ -34,8 +35,7 @@ void phaseOriginal() { EntitySelector entitySelector = EntitySelectorFactory. create(entitySelectorConfig) .buildEntitySelector(buildHeuristicConfigPolicy(), SelectionCacheType.JUST_IN_TIME, SelectionOrder.RANDOM); assertThat(entitySelector) - .isInstanceOf(FromSolutionEntitySelector.class); - assertThat(entitySelector) + .isInstanceOf(FromSolutionEntitySelector.class) .isNotInstanceOf(ShufflingEntitySelector.class); assertThat(entitySelector.getCacheType()).isEqualTo(SelectionCacheType.PHASE); } @@ -48,8 +48,7 @@ void stepOriginal() { EntitySelector entitySelector = EntitySelectorFactory. create(entitySelectorConfig) .buildEntitySelector(buildHeuristicConfigPolicy(), SelectionCacheType.JUST_IN_TIME, SelectionOrder.RANDOM); assertThat(entitySelector) - .isInstanceOf(FromSolutionEntitySelector.class); - assertThat(entitySelector) + .isInstanceOf(FromSolutionEntitySelector.class) .isNotInstanceOf(ShufflingEntitySelector.class); assertThat(entitySelector.getCacheType()).isEqualTo(SelectionCacheType.STEP); } @@ -75,8 +74,7 @@ void phaseRandom() { EntitySelector entitySelector = EntitySelectorFactory. create(entitySelectorConfig) .buildEntitySelector(buildHeuristicConfigPolicy(), SelectionCacheType.JUST_IN_TIME, SelectionOrder.RANDOM); assertThat(entitySelector) - .isInstanceOf(FromSolutionEntitySelector.class); - assertThat(entitySelector) + .isInstanceOf(FromSolutionEntitySelector.class) .isNotInstanceOf(ShufflingEntitySelector.class); assertThat(entitySelector.getCacheType()).isEqualTo(SelectionCacheType.PHASE); } @@ -89,8 +87,7 @@ void stepRandom() { EntitySelector entitySelector = EntitySelectorFactory. create(entitySelectorConfig) .buildEntitySelector(buildHeuristicConfigPolicy(), SelectionCacheType.JUST_IN_TIME, SelectionOrder.RANDOM); assertThat(entitySelector) - .isInstanceOf(FromSolutionEntitySelector.class); - assertThat(entitySelector) + .isInstanceOf(FromSolutionEntitySelector.class) .isNotInstanceOf(ShufflingEntitySelector.class); assertThat(entitySelector.getCacheType()).isEqualTo(SelectionCacheType.STEP); } @@ -153,10 +150,24 @@ void applySorting_withSorterComparatorClass() { applySorting(entitySelectorConfig); } + @Test + void applySorting_withComparatorClass() { + EntitySelectorConfig entitySelectorConfig = new EntitySelectorConfig() + .withComparatorClass(DummyEntityComparator.class); + applySorting(entitySelectorConfig); + } + @Test void applySorting_withSorterWeightFactoryClass() { EntitySelectorConfig entitySelectorConfig = new EntitySelectorConfig() - .withSorterWeightFactoryClass(DummySelectionSorterWeightFactory.class); + .withSorterWeightFactoryClass(DummySelectionComparatorFactory.class); + applySorting(entitySelectorConfig); + } + + @Test + void applySorting_withComparatorFactoryClass() { + EntitySelectorConfig entitySelectorConfig = new EntitySelectorConfig() + .withComparatorFactoryClass(DummySelectionComparatorFactory.class); applySorting(entitySelectorConfig); } @@ -198,6 +209,44 @@ void failFast_ifMimicRecordingIsUsedWithOtherProperty() { .withMessageContaining("has another property"); } + @Test + void failFast_ifBothComparatorsUsed() { + var entitySelectorConfig = new EntitySelectorConfig() + .withSorterManner(EntitySorterManner.DESCENDING) + .withCacheType(SelectionCacheType.PHASE) + .withSelectionOrder(SelectionOrder.SORTED) + .withComparatorClass(DummyEntityComparator.class) + .withSorterComparatorClass(DummyEntityComparator.class); + + assertThatIllegalArgumentException() + .isThrownBy(() -> EntitySelectorFactory. create(entitySelectorConfig) + .buildEntitySelector(buildHeuristicConfigPolicy(), SelectionCacheType.PHASE, SelectionOrder.SORTED)) + .withMessageContaining("The entitySelectorConfig") + .withMessageContaining( + "cannot have a sorterComparatorClass (class ai.timefold.solver.core.impl.heuristic.selector.entity.EntitySelectorFactoryTest$DummyEntityComparator)") + .withMessageContaining( + "and comparatorClass (class ai.timefold.solver.core.impl.heuristic.selector.entity.EntitySelectorFactoryTest$DummyEntityComparator) at the same time"); + } + + @Test + void failFast_ifBothComparatorFactoriesUsed() { + var entitySelectorConfig = new EntitySelectorConfig() + .withSorterManner(EntitySorterManner.DESCENDING) + .withCacheType(SelectionCacheType.PHASE) + .withSelectionOrder(SelectionOrder.SORTED) + .withSorterWeightFactoryClass(DummySelectionComparatorFactory.class) + .withComparatorFactoryClass(DummySelectionComparatorFactory.class); + + assertThatIllegalArgumentException() + .isThrownBy(() -> EntitySelectorFactory. create(entitySelectorConfig) + .buildEntitySelector(buildHeuristicConfigPolicy(), SelectionCacheType.PHASE, SelectionOrder.SORTED)) + .withMessageContaining("The entitySelectorConfig") + .withMessageContaining( + "cannot have a sorterWeightFactoryClass (class ai.timefold.solver.core.impl.heuristic.selector.entity.EntitySelectorFactoryTest$DummySelectionComparatorFactory)") + .withMessageContaining( + "and comparatorFactoryClass (class ai.timefold.solver.core.impl.heuristic.selector.entity.EntitySelectorFactoryTest$DummySelectionComparatorFactory) at the same time"); + } + public static class DummySelectionProbabilityWeightFactory implements SelectionProbabilityWeightFactory { @@ -207,10 +256,11 @@ public double createProbabilityWeight(ScoreDirector scoreDirec } } - public static class DummySelectionSorterWeightFactory + public static class DummySelectionComparatorFactory implements SelectionSorterWeightFactory { + @Override - public Comparable createSorterWeight(TestdataSolution testdataSolution, TestdataEntity selection) { + public Comparable createSorterWeight(TestdataSolution solution, TestdataEntity selection) { return 0; } } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/MoveSelectorFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/MoveSelectorFactoryTest.java index 61148d8ef7..752d5badd6 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/MoveSelectorFactoryTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/MoveSelectorFactoryTest.java @@ -22,6 +22,7 @@ import ai.timefold.solver.core.impl.heuristic.selector.move.decorator.ShufflingMoveSelector; import ai.timefold.solver.core.impl.heuristic.selector.move.decorator.SortingMoveSelector; import ai.timefold.solver.core.testdomain.TestdataSolution; +import ai.timefold.solver.core.testdomain.common.DummyValueFactory; import org.jspecify.annotations.NonNull; import org.junit.jupiter.api.Test; @@ -212,6 +213,45 @@ void applySorting_withSorterComparatorClass() { assertThat(sortingMoveSelector).isExactlyInstanceOf(SortingMoveSelector.class); } + @Test + void applySorting_withComparatorClass() { + final MoveSelector baseMoveSelector = SelectorTestUtils.mockMoveSelector(); + DummyMoveSelectorConfig moveSelectorConfig = new DummyMoveSelectorConfig(); + moveSelectorConfig.setSorterOrder(SelectionSorterOrder.ASCENDING); + moveSelectorConfig.setComparatorClass(DummyComparator.class); + + DummyMoveSelectorFactory moveSelectorFactory = new DummyMoveSelectorFactory(moveSelectorConfig, baseMoveSelector); + MoveSelector sortingMoveSelector = + moveSelectorFactory.applySorting(SelectionCacheType.PHASE, SelectionOrder.SORTED, baseMoveSelector); + assertThat(sortingMoveSelector).isExactlyInstanceOf(SortingMoveSelector.class); + } + + @Test + void applySorting_withWeightFactoryClass() { + final MoveSelector baseMoveSelector = SelectorTestUtils.mockMoveSelector(); + DummyMoveSelectorConfig moveSelectorConfig = new DummyMoveSelectorConfig(); + moveSelectorConfig.setSorterOrder(SelectionSorterOrder.ASCENDING); + moveSelectorConfig.setSorterWeightFactoryClass(DummyValueFactory.class); + + DummyMoveSelectorFactory moveSelectorFactory = new DummyMoveSelectorFactory(moveSelectorConfig, baseMoveSelector); + MoveSelector sortingMoveSelector = + moveSelectorFactory.applySorting(SelectionCacheType.PHASE, SelectionOrder.SORTED, baseMoveSelector); + assertThat(sortingMoveSelector).isExactlyInstanceOf(SortingMoveSelector.class); + } + + @Test + void applySorting_withComparatorFactoryClass() { + final MoveSelector baseMoveSelector = SelectorTestUtils.mockMoveSelector(); + DummyMoveSelectorConfig moveSelectorConfig = new DummyMoveSelectorConfig(); + moveSelectorConfig.setSorterOrder(SelectionSorterOrder.ASCENDING); + moveSelectorConfig.setComparatorFactoryClass(DummyValueFactory.class); + + DummyMoveSelectorFactory moveSelectorFactory = new DummyMoveSelectorFactory(moveSelectorConfig, baseMoveSelector); + MoveSelector sortingMoveSelector = + moveSelectorFactory.applySorting(SelectionCacheType.PHASE, SelectionOrder.SORTED, baseMoveSelector); + assertThat(sortingMoveSelector).isExactlyInstanceOf(SortingMoveSelector.class); + } + @Test void applyProbability_withProbabilityWeightFactoryClass() { final MoveSelector baseMoveSelector = SelectorTestUtils.mockMoveSelector(); @@ -250,6 +290,42 @@ public Move doMove(ScoreDirector scoreDirect assertThat(moveSelector.iterator().hasNext()).isFalse(); } + @Test + void failFast_ifBothComparatorFactoriesUsed() { + final MoveSelector baseMoveSelector = SelectorTestUtils.mockMoveSelector(); + DummyMoveSelectorConfig moveSelectorConfig = new DummyMoveSelectorConfig(); + moveSelectorConfig.setSorterOrder(SelectionSorterOrder.ASCENDING); + moveSelectorConfig.setSorterComparatorClass(DummyComparator.class); + moveSelectorConfig.setComparatorClass(DummyComparator.class); + DummyMoveSelectorFactory moveSelectorFactory = new DummyMoveSelectorFactory(moveSelectorConfig, baseMoveSelector); + assertThatIllegalArgumentException() + .isThrownBy(() -> moveSelectorFactory.applySorting(SelectionCacheType.PHASE, SelectionOrder.SORTED, + baseMoveSelector)) + .withMessageContaining("The moveSelectorConfig") + .withMessageContaining( + "cannot have a sorterComparatorClass (class ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelectorFactoryTest$DummyComparator)") + .withMessageContaining( + "and comparatorClass (class ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelectorFactoryTest$DummyComparator) at the same time"); + } + + @Test + void failFast_ifBothComparatorsFactoriesUsed() { + final MoveSelector baseMoveSelector = SelectorTestUtils.mockMoveSelector(); + DummyMoveSelectorConfig moveSelectorConfig = new DummyMoveSelectorConfig(); + moveSelectorConfig.setSorterOrder(SelectionSorterOrder.ASCENDING); + moveSelectorConfig.setSorterWeightFactoryClass(DummyValueFactory.class); + moveSelectorConfig.setComparatorFactoryClass(DummyValueFactory.class); + DummyMoveSelectorFactory moveSelectorFactory = new DummyMoveSelectorFactory(moveSelectorConfig, baseMoveSelector); + assertThatIllegalArgumentException() + .isThrownBy(() -> moveSelectorFactory.applySorting(SelectionCacheType.PHASE, SelectionOrder.SORTED, + baseMoveSelector)) + .withMessageContaining("The moveSelectorConfig") + .withMessageContaining( + "cannot have a sorterWeightFactoryClass (class ai.timefold.solver.core.testdomain.common.DummyValueFactory)") + .withMessageContaining( + "and comparatorFactoryClass (class ai.timefold.solver.core.testdomain.common.DummyValueFactory) at the same time"); + } + static class DummyMoveSelectorConfig extends MoveSelectorConfig { @Override diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/SortingMoveSelectorTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/SortingMoveSelectorTest.java index da7fe8df12..0a06aafc71 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/SortingMoveSelectorTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/decorator/SortingMoveSelectorTest.java @@ -1,7 +1,9 @@ package ai.timefold.solver.core.impl.heuristic.selector.move.decorator; +import static ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicyTestUtils.buildHeuristicConfigPolicy; import static ai.timefold.solver.core.testutil.PlannerAssert.assertAllCodesOfMoveSelector; import static ai.timefold.solver.core.testutil.PlannerAssert.verifyPhaseLifecycle; +import static ai.timefold.solver.core.testutil.PlannerTestUtils.mockScoreDirector; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -9,18 +11,30 @@ import static org.mockito.Mockito.when; import java.util.Comparator; +import java.util.List; +import java.util.function.Consumer; import ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType; +import ai.timefold.solver.core.config.heuristic.selector.common.SelectionOrder; +import ai.timefold.solver.core.config.heuristic.selector.common.decorator.SelectionSorterOrder; +import ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig; +import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; import ai.timefold.solver.core.impl.heuristic.move.DummyMove; import ai.timefold.solver.core.impl.heuristic.selector.SelectorTestUtils; import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorter; +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory; +import ai.timefold.solver.core.impl.heuristic.selector.move.AbstractMoveSelectorFactory; import ai.timefold.solver.core.impl.heuristic.selector.move.MoveSelector; import ai.timefold.solver.core.impl.phase.scope.AbstractPhaseScope; import ai.timefold.solver.core.impl.phase.scope.AbstractStepScope; import ai.timefold.solver.core.impl.solver.scope.SolverScope; import ai.timefold.solver.core.testdomain.TestdataSolution; +import ai.timefold.solver.core.testutil.CodeAssertable; +import org.jspecify.annotations.NonNull; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; class SortingMoveSelectorTest { @@ -44,6 +58,47 @@ void cacheTypeJustInTime() { assertThatIllegalArgumentException().isThrownBy(() -> runCacheType(SelectionCacheType.JUST_IN_TIME, 5)); } + private static List generateConfiguration() { + return List.of( + new DummySorterMoveSelectorConfig() + .withSorterOrder(SelectionSorterOrder.ASCENDING) + .withSorterWeightFactoryClass(TestCodeAssertableComparatorFactory.class), + new DummySorterMoveSelectorConfig() + .withSorterOrder(SelectionSorterOrder.ASCENDING) + .withSorterComparatorClass(TestCodeAssertableComparator.class), + new DummySorterMoveSelectorConfig() + .withSorterOrder(SelectionSorterOrder.ASCENDING) + .withComparatorFactoryClass(TestCodeAssertableComparatorFactory.class), + new DummySorterMoveSelectorConfig() + .withSorterOrder(SelectionSorterOrder.ASCENDING) + .withComparatorClass(TestCodeAssertableComparator.class)); + } + + @ParameterizedTest + @MethodSource("generateConfiguration") + void applySorting(DummySorterMoveSelectorConfig moveSelectorConfig) { + var baseMoveSelector = SelectorTestUtils.mockMoveSelector( + new DummyMove("jan"), new DummyMove("feb"), new DummyMove("mar"), + new DummyMove("apr"), new DummyMove("may"), new DummyMove("jun")); + var moveSelectorFactory = new DummySorterMoveSelectorFactory(moveSelectorConfig, baseMoveSelector); + var moveSelector = + moveSelectorFactory.buildBaseMoveSelector(buildHeuristicConfigPolicy(), SelectionCacheType.PHASE, false); + + var scoreDirector = mockScoreDirector(TestdataSolution.buildSolutionDescriptor()); + var solverScope = mock(SolverScope.class); + when(solverScope.getScoreDirector()).thenReturn(scoreDirector); + moveSelector.solvingStarted(solverScope); + + var phaseScope = mock(AbstractPhaseScope.class); + when(phaseScope.getSolverScope()).thenReturn(solverScope); + moveSelector.phaseStarted(phaseScope); + + var stepScopeA = mock(AbstractStepScope.class); + when(stepScopeA.getPhaseScope()).thenReturn(phaseScope); + moveSelector.stepStarted(stepScopeA); + assertAllCodesOfMoveSelector(moveSelector, "apr", "feb", "jan", "jun", "mar", "may"); + } + public void runCacheType(SelectionCacheType cacheType, int timesCalled) { MoveSelector childMoveSelector = SelectorTestUtils.mockMoveSelector( new DummyMove("jan"), new DummyMove("feb"), new DummyMove("mar"), @@ -105,4 +160,56 @@ public void runCacheType(SelectionCacheType cacheType, int timesCalled) { verify(childMoveSelector, times(timesCalled)).getSize(); } + private static class DummySorterMoveSelectorConfig extends MoveSelectorConfig { + + @Override + public @NonNull DummySorterMoveSelectorConfig copyConfig() { + throw new UnsupportedOperationException(); + } + + @Override + public void visitReferencedClasses(@NonNull Consumer> classVisitor) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasNearbySelectionConfig() { + return false; + } + } + + private static class DummySorterMoveSelectorFactory + extends AbstractMoveSelectorFactory { + + protected final MoveSelector baseMoveSelector; + + DummySorterMoveSelectorFactory(DummySorterMoveSelectorConfig moveSelectorConfig, + MoveSelector baseMoveSelector) { + super(moveSelectorConfig); + this.baseMoveSelector = baseMoveSelector; + } + + @Override + protected MoveSelector buildBaseMoveSelector(HeuristicConfigPolicy configPolicy, + SelectionCacheType minimumCacheType, + boolean randomSelection) { + return applySorting(minimumCacheType, SelectionOrder.SORTED, baseMoveSelector); + } + } + + public static class TestCodeAssertableComparatorFactory implements SelectionSorterWeightFactory { + + @Override + public Comparable createSorterWeight(Object o, CodeAssertable selection) { + return selection.getCode(); + } + } + + public static class TestCodeAssertableComparator implements Comparator { + @Override + public int compare(CodeAssertable v1, CodeAssertable v2) { + return v1.getCode().compareTo(v2.getCode()); + } + } + } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/ChangeMoveSelectorFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/ChangeMoveSelectorFactoryTest.java index 9597800e06..54e0e9d19c 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/ChangeMoveSelectorFactoryTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/ChangeMoveSelectorFactoryTest.java @@ -125,7 +125,7 @@ void unfoldConfiguredIntoListChangeMoveSelectorConfig() { long selectedCountLimit = 200; ChangeMoveSelectorConfig moveSelectorConfig = new ChangeMoveSelectorConfig() .withEntitySelectorConfig(new EntitySelectorConfig(TestdataListEntity.class) - .withSorterComparatorClass(DummyEntityComparator.class)) + .withComparatorClass(DummyEntityComparator.class)) .withValueSelectorConfig(new ValueSelectorConfig("valueList")) .withCacheType(moveSelectorCacheType) .withSelectionOrder(moveSelectorSelectionOrder) @@ -149,7 +149,7 @@ void unfoldConfiguredIntoListChangeMoveSelectorConfig() { DestinationSelectorConfig destinationSelectorConfig = listChangeMoveSelectorConfig.getDestinationSelectorConfig(); EntitySelectorConfig entitySelectorConfig = destinationSelectorConfig.getEntitySelectorConfig(); assertThat(entitySelectorConfig.getEntityClass()).isEqualTo(TestdataListEntity.class); - assertThat(entitySelectorConfig.getSorterComparatorClass()).isEqualTo(DummyEntityComparator.class); + assertThat(entitySelectorConfig.getComparatorClass()).isEqualTo(DummyEntityComparator.class); ValueSelectorConfig valueSelectorConfig = destinationSelectorConfig.getValueSelectorConfig(); assertThat(valueSelectorConfig.getVariableName()).isEqualTo("valueList"); } diff --git a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactoryTest.java b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactoryTest.java index 0b3f53690c..0b8c2ddfbb 100644 --- a/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactoryTest.java +++ b/core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/value/ValueSelectorFactoryTest.java @@ -15,6 +15,7 @@ import ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType; import ai.timefold.solver.core.config.heuristic.selector.common.SelectionOrder; 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.impl.domain.entity.descriptor.EntityDescriptor; import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor; import ai.timefold.solver.core.impl.heuristic.HeuristicConfigPolicy; @@ -102,8 +103,7 @@ void phaseRandom() { ValueSelector valueSelector = ValueSelectorFactory.create(valueSelectorConfig).buildValueSelector(configPolicy, entityDescriptor, SelectionCacheType.JUST_IN_TIME, SelectionOrder.RANDOM); assertThat(valueSelector) - .isInstanceOf(IterableFromSolutionPropertyValueSelector.class); - assertThat(valueSelector) + .isInstanceOf(IterableFromSolutionPropertyValueSelector.class) .isNotInstanceOf(ShufflingValueSelector.class); assertThat(valueSelector.getCacheType()).isEqualTo(SelectionCacheType.PHASE); } @@ -215,10 +215,24 @@ void applySorting_withSorterComparatorClass() { applySorting(valueSelectorConfig); } + @Test + void applySorting_withComparatorClass() { + ValueSelectorConfig valueSelectorConfig = new ValueSelectorConfig() + .withComparatorClass(DummyValueComparator.class); + applySorting(valueSelectorConfig); + } + @Test void applySorting_withSorterWeightFactoryClass() { ValueSelectorConfig valueSelectorConfig = new ValueSelectorConfig() - .withSorterWeightFactoryClass(DummySelectionSorterWeightFactory.class); + .withSorterWeightFactoryClass(DummySelectionComparatorFactory.class); + applySorting(valueSelectorConfig); + } + + @Test + void applySorting_withComparatorFactoryClass() { + ValueSelectorConfig valueSelectorConfig = new ValueSelectorConfig() + .withComparatorFactoryClass(DummySelectionComparatorFactory.class); applySorting(valueSelectorConfig); } @@ -260,6 +274,46 @@ void failFast_ifMimicRecordingIsUsedWithOtherProperty() { .withMessageContaining("has another property"); } + @Test + void failFast_ifBothComparatorsUsed() { + var valueSelectorConfig = new ValueSelectorConfig() + .withSorterManner(ValueSorterManner.DESCENDING) + .withCacheType(SelectionCacheType.PHASE) + .withSelectionOrder(SelectionOrder.SORTED) + .withSorterComparatorClass(DummyValueComparator.class) + .withComparatorClass(DummyValueComparator.class); + + assertThatIllegalArgumentException() + .isThrownBy(() -> ValueSelectorFactory. create(valueSelectorConfig) + .buildValueSelector(buildHeuristicConfigPolicy(), TestdataEntity.buildEntityDescriptor(), + SelectionCacheType.PHASE, SelectionOrder.SORTED)) + .withMessageContaining("The valueSelectorConfig") + .withMessageContaining( + "cannot have a sorterComparatorClass (class ai.timefold.solver.core.impl.heuristic.selector.value.ValueSelectorFactoryTest$DummyValueComparator)") + .withMessageContaining( + "and comparatorClass (class ai.timefold.solver.core.impl.heuristic.selector.value.ValueSelectorFactoryTest$DummyValueComparator) at the same time"); + } + + @Test + void failFast_ifBothComparatorFactoriesUsed() { + var valueSelectorConfig = new ValueSelectorConfig() + .withSorterManner(ValueSorterManner.DESCENDING) + .withCacheType(SelectionCacheType.PHASE) + .withSelectionOrder(SelectionOrder.SORTED) + .withSorterWeightFactoryClass(DummySelectionComparatorFactory.class) + .withComparatorFactoryClass(DummySelectionComparatorFactory.class); + + assertThatIllegalArgumentException() + .isThrownBy(() -> ValueSelectorFactory. create(valueSelectorConfig) + .buildValueSelector(buildHeuristicConfigPolicy(), TestdataEntity.buildEntityDescriptor(), + SelectionCacheType.PHASE, SelectionOrder.SORTED)) + .withMessageContaining("The valueSelectorConfig") + .withMessageContaining( + "cannot have a sorterWeightFactoryClass (class ai.timefold.solver.core.impl.heuristic.selector.value.ValueSelectorFactoryTest$DummySelectionComparatorFactory)") + .withMessageContaining( + "and comparatorFactoryClass (class ai.timefold.solver.core.impl.heuristic.selector.value.ValueSelectorFactoryTest$DummySelectionComparatorFactory) at the same time"); + } + static Stream applyListValueFiltering() { return Stream.of( arguments(true, ValueSelectorFactory.ListValueFilteringType.ACCEPT_ASSIGNED, AssignedListValueSelector.class), @@ -310,10 +364,11 @@ public double createProbabilityWeight(ScoreDirector scoreDirec } } - public static class DummySelectionSorterWeightFactory + public static class DummySelectionComparatorFactory implements SelectionSorterWeightFactory { + @Override - public Comparable createSorterWeight(TestdataSolution testdataSolution, TestdataValue selection) { + public Comparable createSorterWeight(TestdataSolution solution, TestdataValue selection) { return 0; } } diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/common/DummyHardSoftEasyScoreCalculator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/common/DummyHardSoftEasyScoreCalculator.java new file mode 100644 index 0000000000..3dda9fc6ab --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/common/DummyHardSoftEasyScoreCalculator.java @@ -0,0 +1,14 @@ +package ai.timefold.solver.core.testdomain.common; + +import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; +import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; + +import org.jspecify.annotations.NonNull; + +public class DummyHardSoftEasyScoreCalculator implements EasyScoreCalculator { + + @Override + public @NonNull HardSoftScore calculateScore(@NonNull Object o) { + return HardSoftScore.ZERO; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/common/DummyValueComparator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/common/DummyValueComparator.java new file mode 100644 index 0000000000..ada61c42ef --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/common/DummyValueComparator.java @@ -0,0 +1,13 @@ +package ai.timefold.solver.core.testdomain.common; + +import java.util.Comparator; + +import ai.timefold.solver.core.testdomain.TestdataValue; + +public class DummyValueComparator implements Comparator { + + @Override + public int compare(TestdataValue v1, TestdataValue v2) { + return 0; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/common/DummyValueFactory.java b/core/src/test/java/ai/timefold/solver/core/testdomain/common/DummyValueFactory.java new file mode 100644 index 0000000000..e674b6b488 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/common/DummyValueFactory.java @@ -0,0 +1,13 @@ +package ai.timefold.solver.core.testdomain.common; + +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory; +import ai.timefold.solver.core.testdomain.TestdataValue; +import ai.timefold.solver.core.testdomain.list.sort.factory.TestdataListFactorySortableSolution; + +public class DummyValueFactory implements SelectionSorterWeightFactory { + + @Override + public Comparable createSorterWeight(TestdataListFactorySortableSolution solution, TestdataValue selection) { + return 0; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/common/DummyWeightValueFactory.java b/core/src/test/java/ai/timefold/solver/core/testdomain/common/DummyWeightValueFactory.java new file mode 100644 index 0000000000..6496822201 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/common/DummyWeightValueFactory.java @@ -0,0 +1,14 @@ +package ai.timefold.solver.core.testdomain.common; + +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory; +import ai.timefold.solver.core.testdomain.TestdataValue; +import ai.timefold.solver.core.testdomain.list.sort.factory.TestdataListFactorySortableSolution; + +public class DummyWeightValueFactory + implements SelectionSorterWeightFactory { + + @Override + public Comparable createSorterWeight(TestdataListFactorySortableSolution solution, TestdataValue selection) { + return v -> 0; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/common/TestSortableComparator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/common/TestSortableComparator.java new file mode 100644 index 0000000000..6c7d606c4a --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/common/TestSortableComparator.java @@ -0,0 +1,11 @@ +package ai.timefold.solver.core.testdomain.common; + +import java.util.Comparator; + +public class TestSortableComparator implements Comparator { + + @Override + public int compare(TestSortableObject v1, TestSortableObject v2) { + return v1.getComparatorValue() - v2.getComparatorValue(); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/common/TestSortableFactory.java b/core/src/test/java/ai/timefold/solver/core/testdomain/common/TestSortableFactory.java new file mode 100644 index 0000000000..5ca230331c --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/common/TestSortableFactory.java @@ -0,0 +1,12 @@ +package ai.timefold.solver.core.testdomain.common; + +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory; + +public class TestSortableFactory + implements SelectionSorterWeightFactory { + + @Override + public Comparable createSorterWeight(Object o, TestSortableObject selection) { + return selection; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/common/TestSortableObject.java b/core/src/test/java/ai/timefold/solver/core/testdomain/common/TestSortableObject.java new file mode 100644 index 0000000000..aa750b90d4 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/common/TestSortableObject.java @@ -0,0 +1,11 @@ +package ai.timefold.solver.core.testdomain.common; + +public interface TestSortableObject extends Comparable { + + int getComparatorValue(); + + @Override + default int compareTo(TestSortableObject o) { + return getComparatorValue() - o.getComparatorValue(); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/common/TestdataObjectSortableDescendingComparator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/common/TestdataObjectSortableDescendingComparator.java new file mode 100644 index 0000000000..84d894aafd --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/common/TestdataObjectSortableDescendingComparator.java @@ -0,0 +1,20 @@ +package ai.timefold.solver.core.testdomain.common; + +import java.util.Comparator; + +import ai.timefold.solver.core.testdomain.TestdataObject; + +public class TestdataObjectSortableDescendingComparator implements Comparator { + + @Override + public int compare(TestdataObject o1, TestdataObject o2) { + // Descending order + return extractCode(o2.getCode()) - extractCode(o1.getCode()); + } + + public static int extractCode(String code) { + var idx = code.lastIndexOf(" "); + return Integer.parseInt(code.substring(idx + 1)); + } + +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/common/TestdataObjectSortableDescendingFactory.java b/core/src/test/java/ai/timefold/solver/core/testdomain/common/TestdataObjectSortableDescendingFactory.java new file mode 100644 index 0000000000..e4bff2ea6c --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/common/TestdataObjectSortableDescendingFactory.java @@ -0,0 +1,18 @@ +package ai.timefold.solver.core.testdomain.common; + +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory; +import ai.timefold.solver.core.testdomain.TestdataObject; + +public class TestdataObjectSortableDescendingFactory implements SelectionSorterWeightFactory { + + @Override + public Comparable createSorterWeight(Object solution, TestdataObject selection) { + // Descending order + return -extractCode(selection.getCode()); + } + + public static int extractCode(String code) { + var idx = code.lastIndexOf(" "); + return Integer.parseInt(code.substring(idx + 1)); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/common/TestdataSortableValue.java b/core/src/test/java/ai/timefold/solver/core/testdomain/common/TestdataSortableValue.java new file mode 100644 index 0000000000..037d464faf --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/common/TestdataSortableValue.java @@ -0,0 +1,21 @@ +package ai.timefold.solver.core.testdomain.common; + +import ai.timefold.solver.core.testdomain.TestdataObject; + +public class TestdataSortableValue extends TestdataObject implements TestSortableObject { + + private int strength; + + public TestdataSortableValue() { + } + + public TestdataSortableValue(String code, int strength) { + super(code); + this.strength = strength; + } + + @Override + public int getComparatorValue() { + return strength; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/difficultyweight/TestdataDifficultyFactory.java b/core/src/test/java/ai/timefold/solver/core/testdomain/difficultyweight/TestdataDifficultyFactory.java new file mode 100644 index 0000000000..c76995fd7f --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/difficultyweight/TestdataDifficultyFactory.java @@ -0,0 +1,13 @@ +package ai.timefold.solver.core.testdomain.difficultyweight; + +import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory; + +public class TestdataDifficultyFactory + implements SelectionSorterWeightFactory { + + @Override + public Comparable createSorterWeight(TestdataDifficultyWeightSolution testdataDifficultyWeightSolution, + TestdataDifficultyWeightEntity selection) { + return 0; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/difficultyweight/TestdataDifficultyWeightEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/difficultyweight/TestdataDifficultyWeightEntity.java index 468d1a7092..802bb2a0d0 100644 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/difficultyweight/TestdataDifficultyWeightEntity.java +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/difficultyweight/TestdataDifficultyWeightEntity.java @@ -6,7 +6,7 @@ import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor; import ai.timefold.solver.core.testdomain.TestdataObject; -@PlanningEntity(difficultyWeightFactoryClass = TestdataDifficultyWeightFactory.class) +@PlanningEntity(difficultyWeightFactoryClass = TestdataDifficultyFactory.class) public class TestdataDifficultyWeightEntity extends TestdataObject { public static EntityDescriptor buildEntityDescriptor() { diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/difficultyweight/TestdataDifficultyWeightFactory.java b/core/src/test/java/ai/timefold/solver/core/testdomain/difficultyweight/TestdataDifficultyWeightFactory.java deleted file mode 100644 index 7659168c32..0000000000 --- a/core/src/test/java/ai/timefold/solver/core/testdomain/difficultyweight/TestdataDifficultyWeightFactory.java +++ /dev/null @@ -1,21 +0,0 @@ -package ai.timefold.solver.core.testdomain.difficultyweight; - -import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory; - -public class TestdataDifficultyWeightFactory implements - SelectionSorterWeightFactory { - - @Override - public TestdataDifficultyWeightComparable createSorterWeight(TestdataDifficultyWeightSolution solution, - TestdataDifficultyWeightEntity entity) { - return new TestdataDifficultyWeightComparable(); - } - - public static class TestdataDifficultyWeightComparable implements Comparable { - - @Override - public int compareTo(TestdataDifficultyWeightComparable other) { - return 0; - } - } -} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/sort/comparator/ListOneValuePerEntityEasyScoreCalculator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/sort/comparator/ListOneValuePerEntityEasyScoreCalculator.java new file mode 100644 index 0000000000..fc9034b656 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/sort/comparator/ListOneValuePerEntityEasyScoreCalculator.java @@ -0,0 +1,25 @@ +package ai.timefold.solver.core.testdomain.list.sort.comparator; + +import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; +import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; + +import org.jspecify.annotations.NonNull; + +public class ListOneValuePerEntityEasyScoreCalculator + implements EasyScoreCalculator { + + @Override + public @NonNull HardSoftScore calculateScore(@NonNull TestdataListSortableSolution solution) { + var softScore = 0; + var hardScore = 0; + for (var entity : solution.getEntityList()) { + if (entity.getValueList().size() == 1) { + softScore -= 10; + } else { + hardScore -= 10; + } + hardScore--; + } + return HardSoftScore.of(hardScore, softScore); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/sort/comparator/TestdataListSortableEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/sort/comparator/TestdataListSortableEntity.java new file mode 100644 index 0000000000..015ba9ef1c --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/sort/comparator/TestdataListSortableEntity.java @@ -0,0 +1,41 @@ +package ai.timefold.solver.core.testdomain.list.sort.comparator; + +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.TestSortableObject; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningEntity(difficultyComparatorClass = TestSortableComparator.class) +public class TestdataListSortableEntity extends TestdataObject implements TestSortableObject { + + @PlanningListVariable(valueRangeProviderRefs = "valueRange", comparatorClass = TestSortableComparator.class) + private List valueList; + private int difficulty; + + public TestdataListSortableEntity() { + } + + public TestdataListSortableEntity(String code, int difficulty) { + super(code); + this.difficulty = difficulty; + this.valueList = new ArrayList<>(); + } + + public List getValueList() { + return valueList; + } + + public void setValueList(List valueList) { + this.valueList = valueList; + } + + @Override + public int getComparatorValue() { + return difficulty; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/sort/comparator/TestdataListSortableSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/sort/comparator/TestdataListSortableSolution.java new file mode 100644 index 0000000000..809d024967 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/sort/comparator/TestdataListSortableSolution.java @@ -0,0 +1,83 @@ +package ai.timefold.solver.core.testdomain.list.sort.comparator; + +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 TestdataListSortableSolution { + + public static SolutionDescriptor buildSolutionDescriptor() { + return SolutionDescriptor.buildSolutionDescriptor( + TestdataListSortableSolution.class, + TestdataListSortableEntity.class, + TestdataSortableValue.class); + } + + public static TestdataListSortableSolution generateSolution(int valueCount, int entityCount, boolean shuffle) { + var entityList = new ArrayList<>(IntStream.range(0, entityCount) + .mapToObj(i -> new TestdataListSortableEntity("Generated Entity " + i, 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); + } + TestdataListSortableSolution solution = new TestdataListSortableSolution(); + 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(TestdataListSortableEntity entity) { + this.entityList = entityList.stream() + .filter(e -> e != entity) + .toList(); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/sort/factory/ListOneValuePerEntityFactoryEasyScoreCalculator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/sort/factory/ListOneValuePerEntityFactoryEasyScoreCalculator.java new file mode 100644 index 0000000000..895efffbff --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/sort/factory/ListOneValuePerEntityFactoryEasyScoreCalculator.java @@ -0,0 +1,26 @@ +package ai.timefold.solver.core.testdomain.list.sort.factory; + +import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; +import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; + +import org.jspecify.annotations.NonNull; + +public class ListOneValuePerEntityFactoryEasyScoreCalculator + implements EasyScoreCalculator { + + @Override + public @NonNull HardSoftScore + calculateScore(@NonNull TestdataListFactorySortableSolution solution) { + var softScore = 0; + var hardScore = 0; + for (var entity : solution.getEntityList()) { + if (entity.getValueList().size() == 1) { + softScore -= 10; + } else { + hardScore -= 10; + } + hardScore--; + } + return HardSoftScore.of(hardScore, softScore); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/sort/factory/TestdataListFactorySortableEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/sort/factory/TestdataListFactorySortableEntity.java new file mode 100644 index 0000000000..d9b28a0cbc --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/sort/factory/TestdataListFactorySortableEntity.java @@ -0,0 +1,41 @@ +package ai.timefold.solver.core.testdomain.list.sort.factory; + +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.TestSortableFactory; +import ai.timefold.solver.core.testdomain.common.TestSortableObject; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningEntity(difficultyWeightFactoryClass = TestSortableFactory.class) +public class TestdataListFactorySortableEntity extends TestdataObject implements TestSortableObject { + + @PlanningListVariable(valueRangeProviderRefs = "valueRange", comparatorFactoryClass = TestSortableFactory.class) + private List valueList; + private int difficulty; + + public TestdataListFactorySortableEntity() { + } + + public TestdataListFactorySortableEntity(String code, int difficulty) { + super(code); + this.difficulty = difficulty; + this.valueList = new ArrayList<>(); + } + + public List getValueList() { + return valueList; + } + + public void setValueList(List valueList) { + this.valueList = valueList; + } + + @Override + public int getComparatorValue() { + return difficulty; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/sort/factory/TestdataListFactorySortableSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/sort/factory/TestdataListFactorySortableSolution.java new file mode 100644 index 0000000000..0ae5b91d18 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/sort/factory/TestdataListFactorySortableSolution.java @@ -0,0 +1,83 @@ +package ai.timefold.solver.core.testdomain.list.sort.factory; + +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 TestdataListFactorySortableSolution { + + public static SolutionDescriptor buildSolutionDescriptor() { + return SolutionDescriptor.buildSolutionDescriptor( + TestdataListFactorySortableSolution.class, + TestdataListFactorySortableEntity.class, + TestdataSortableValue.class); + } + + public static TestdataListFactorySortableSolution generateSolution(int valueCount, int entityCount, boolean shuffle) { + var entityList = new ArrayList<>(IntStream.range(0, entityCount) + .mapToObj(i -> new TestdataListFactorySortableEntity("Generated Entity " + i, 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); + } + TestdataListFactorySortableSolution solution = new TestdataListFactorySortableSolution(); + 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(TestdataListFactorySortableEntity entity) { + this.entityList = entityList.stream() + .filter(e -> e != entity) + .toList(); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/sort/invalid/TestdataInvalidListSortableEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/sort/invalid/TestdataInvalidListSortableEntity.java new file mode 100644 index 0000000000..0e184321bb --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/sort/invalid/TestdataInvalidListSortableEntity.java @@ -0,0 +1,45 @@ +package ai.timefold.solver.core.testdomain.list.sort.invalid; + +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.DummyValueComparator; +import ai.timefold.solver.core.testdomain.common.DummyValueFactory; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningEntity +public class TestdataInvalidListSortableEntity extends TestdataObject { + + @PlanningListVariable(valueRangeProviderRefs = "valueRange", comparatorClass = DummyValueComparator.class, + comparatorFactoryClass = DummyValueFactory.class) + private List valueList; + private int difficulty; + + public TestdataInvalidListSortableEntity() { + } + + public TestdataInvalidListSortableEntity(String code, int difficulty) { + super(code); + this.difficulty = difficulty; + this.valueList = new ArrayList<>(); + } + + public List getValueList() { + return valueList; + } + + public void setValueList(List valueList) { + this.valueList = valueList; + } + + public int getDifficulty() { + return difficulty; + } + + public void setDifficulty(int difficulty) { + this.difficulty = difficulty; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/sort/invalid/TestdataInvalidListSortableSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/sort/invalid/TestdataInvalidListSortableSolution.java new file mode 100644 index 0000000000..2c20a86b62 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/sort/invalid/TestdataInvalidListSortableSolution.java @@ -0,0 +1,52 @@ +package ai.timefold.solver.core.testdomain.list.sort.invalid; + +import java.util.List; + +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.valuerange.ValueRangeProvider; +import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningSolution +public class TestdataInvalidListSortableSolution { + + private List valueList; + private List entityList; + private HardSoftScore score; + + @ValueRangeProvider(id = "valueRange") + @PlanningEntityCollectionProperty + 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(TestdataInvalidListSortableEntity entity) { + this.entityList = entityList.stream() + .filter(e -> e != entity) + .toList(); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/sort/comparator/ListOneValuePerEntityRangeEasyScoreCalculator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/sort/comparator/ListOneValuePerEntityRangeEasyScoreCalculator.java new file mode 100644 index 0000000000..8f7cbe807d --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/sort/comparator/ListOneValuePerEntityRangeEasyScoreCalculator.java @@ -0,0 +1,26 @@ +package ai.timefold.solver.core.testdomain.list.valuerange.sort.comparator; + +import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; +import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; + +import org.jspecify.annotations.NonNull; + +public class ListOneValuePerEntityRangeEasyScoreCalculator + implements EasyScoreCalculator { + + @Override + public @NonNull HardSoftScore + calculateScore(@NonNull TestdataListSortableEntityProvidingSolution solution) { + var softScore = 0; + var hardScore = 0; + for (var entity : solution.getEntityList()) { + if (entity.getValueList().size() == 1) { + softScore -= 10; + } else { + hardScore -= 10; + } + hardScore--; + } + return HardSoftScore.of(hardScore, softScore); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/sort/comparator/TestdataListSortableEntityProvidingEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/sort/comparator/TestdataListSortableEntityProvidingEntity.java new file mode 100644 index 0000000000..d31c620292 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/sort/comparator/TestdataListSortableEntityProvidingEntity.java @@ -0,0 +1,55 @@ +package ai.timefold.solver.core.testdomain.list.valuerange.sort.comparator; + +import java.util.ArrayList; +import java.util.List; + +import ai.timefold.solver.core.api.domain.entity.PlanningEntity; +import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; +import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; +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.TestSortableObject; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningEntity(difficultyComparatorClass = TestSortableComparator.class) +public class TestdataListSortableEntityProvidingEntity extends TestdataObject implements TestSortableObject { + + @PlanningListVariable(valueRangeProviderRefs = "valueRange", comparatorClass = TestSortableComparator.class) + private List valueList; + @ValueRangeProvider(id = "valueRange") + @PlanningEntityCollectionProperty + private List valueRange; + + private int difficulty; + + public TestdataListSortableEntityProvidingEntity() { + } + + public TestdataListSortableEntityProvidingEntity(String code, int difficulty) { + super(code); + this.difficulty = difficulty; + this.valueList = new ArrayList<>(); + } + + public List getValueList() { + return valueList; + } + + public void setValueList(List valueList) { + this.valueList = valueList; + } + + public List getValueRange() { + return valueRange; + } + + public void setValueRange(List valueRange) { + this.valueRange = valueRange; + } + + @Override + public int getComparatorValue() { + return difficulty; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/sort/comparator/TestdataListSortableEntityProvidingSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/sort/comparator/TestdataListSortableEntityProvidingSolution.java new file mode 100644 index 0000000000..86ed83d095 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/sort/comparator/TestdataListSortableEntityProvidingSolution.java @@ -0,0 +1,76 @@ +package ai.timefold.solver.core.testdomain.list.valuerange.sort.comparator; + +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.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 TestdataListSortableEntityProvidingSolution { + + public static SolutionDescriptor buildSolutionDescriptor() { + return SolutionDescriptor.buildSolutionDescriptor( + TestdataListSortableEntityProvidingSolution.class, + TestdataListSortableEntityProvidingEntity.class, + TestdataSortableValue.class); + } + + public static TestdataListSortableEntityProvidingSolution generateSolution(int valueCount, int entityCount, + boolean shuffle) { + var entityList = new ArrayList<>(IntStream.range(0, entityCount) + .mapToObj(i -> new TestdataListSortableEntityProvidingEntity("Generated Entity " + i, i)) + .toList()); + var valueList = IntStream.range(0, valueCount) + .mapToObj(i -> new TestdataSortableValue("Generated Value " + i, i)) + .toList(); + var random = new Random(0); + var solution = new TestdataListSortableEntityProvidingSolution(); + for (var entity : entityList) { + var valueRange = new ArrayList<>(valueList); + if (shuffle) { + Collections.shuffle(valueRange, random); + } + entity.setValueRange(valueRange); + } + if (shuffle) { + Collections.shuffle(entityList, random); + } + solution.setEntityList(entityList); + return solution; + } + + private List entityList; + private HardSoftScore score; + + @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(TestdataListSortableEntityProvidingEntity entity) { + this.entityList = entityList.stream() + .filter(e -> e != entity) + .toList(); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/sort/factory/ListOneValuePerEntityRangeFactoryEasyScoreCalculator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/sort/factory/ListOneValuePerEntityRangeFactoryEasyScoreCalculator.java new file mode 100644 index 0000000000..c70072e040 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/sort/factory/ListOneValuePerEntityRangeFactoryEasyScoreCalculator.java @@ -0,0 +1,27 @@ +package ai.timefold.solver.core.testdomain.list.valuerange.sort.factory; + +import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; +import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; + +import org.jspecify.annotations.NonNull; + +public class ListOneValuePerEntityRangeFactoryEasyScoreCalculator + implements EasyScoreCalculator { + + @Override + public @NonNull HardSoftScore + calculateScore( + @NonNull TestdataListFactorySortableEntityProvidingSolution solution) { + var softScore = 0; + var hardScore = 0; + for (var entity : solution.getEntityList()) { + if (entity.getValueList().size() == 1) { + softScore -= 10; + } else { + hardScore -= 10; + } + hardScore--; + } + return HardSoftScore.of(hardScore, softScore); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/sort/factory/TestdataListFactorySortableEntityProvidingEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/sort/factory/TestdataListFactorySortableEntityProvidingEntity.java new file mode 100644 index 0000000000..14e8a45593 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/sort/factory/TestdataListFactorySortableEntityProvidingEntity.java @@ -0,0 +1,56 @@ +package ai.timefold.solver.core.testdomain.list.valuerange.sort.factory; + +import java.util.ArrayList; +import java.util.List; + +import ai.timefold.solver.core.api.domain.entity.PlanningEntity; +import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; +import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; +import ai.timefold.solver.core.api.domain.variable.PlanningListVariable; +import ai.timefold.solver.core.testdomain.TestdataObject; +import ai.timefold.solver.core.testdomain.common.TestSortableFactory; +import ai.timefold.solver.core.testdomain.common.TestSortableObject; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningEntity(difficultyWeightFactoryClass = TestSortableFactory.class) +public class TestdataListFactorySortableEntityProvidingEntity extends TestdataObject + implements TestSortableObject { + + @PlanningListVariable(valueRangeProviderRefs = "valueRange", comparatorFactoryClass = TestSortableFactory.class) + private List valueList; + @ValueRangeProvider(id = "valueRange") + @PlanningEntityCollectionProperty + private List valueRange; + + private int difficulty; + + public TestdataListFactorySortableEntityProvidingEntity() { + } + + public TestdataListFactorySortableEntityProvidingEntity(String code, int difficulty) { + super(code); + this.difficulty = difficulty; + this.valueList = new ArrayList<>(); + } + + public List getValueList() { + return valueList; + } + + public void setValueList(List valueList) { + this.valueList = valueList; + } + + public List getValueRange() { + return valueRange; + } + + public void setValueRange(List valueRange) { + this.valueRange = valueRange; + } + + @Override + public int getComparatorValue() { + return difficulty; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/sort/factory/TestdataListFactorySortableEntityProvidingSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/sort/factory/TestdataListFactorySortableEntityProvidingSolution.java new file mode 100644 index 0000000000..c56e2ba78b --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/list/valuerange/sort/factory/TestdataListFactorySortableEntityProvidingSolution.java @@ -0,0 +1,76 @@ +package ai.timefold.solver.core.testdomain.list.valuerange.sort.factory; + +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.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 TestdataListFactorySortableEntityProvidingSolution { + + public static SolutionDescriptor buildSolutionDescriptor() { + return SolutionDescriptor.buildSolutionDescriptor( + TestdataListFactorySortableEntityProvidingSolution.class, + TestdataListFactorySortableEntityProvidingEntity.class, + TestdataSortableValue.class); + } + + public static TestdataListFactorySortableEntityProvidingSolution generateSolution(int valueCount, int entityCount, + boolean shuffle) { + var entityList = new ArrayList<>(IntStream.range(0, entityCount) + .mapToObj(i -> new TestdataListFactorySortableEntityProvidingEntity("Generated Entity " + i, i)) + .toList()); + var valueList = IntStream.range(0, valueCount) + .mapToObj(i -> new TestdataSortableValue("Generated Value " + i, i)) + .toList(); + var solution = new TestdataListFactorySortableEntityProvidingSolution(); + var random = new Random(0); + for (var entity : entityList) { + var valueRange = new ArrayList<>(valueList); + if (shuffle) { + Collections.shuffle(valueRange, random); + } + entity.setValueRange(valueRange); + } + if (shuffle) { + Collections.shuffle(entityList, random); + } + solution.setEntityList(entityList); + return solution; + } + + private List entityList; + private HardSoftScore score; + + @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(TestdataListFactorySortableEntityProvidingEntity entity) { + this.entityList = entityList.stream() + .filter(e -> e != entity) + .toList(); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/sort/comparator/OneValuePerEntityComparatorEasyScoreCalculator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/comparator/OneValuePerEntityComparatorEasyScoreCalculator.java new file mode 100644 index 0000000000..41b83def85 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/comparator/OneValuePerEntityComparatorEasyScoreCalculator.java @@ -0,0 +1,27 @@ +package ai.timefold.solver.core.testdomain.sort.comparator; + +import java.util.Objects; + +import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; +import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; + +import org.jspecify.annotations.NonNull; + +public class OneValuePerEntityComparatorEasyScoreCalculator + implements EasyScoreCalculator { + + @Override + public @NonNull HardSoftScore calculateScore(@NonNull TestdataComparatorSortableSolution solution) { + var distinct = (int) solution.getEntityList().stream() + .map(TestdataComparatorSortableEntity::getValue) + .filter(Objects::nonNull) + .distinct() + .count(); + var assigned = solution.getEntityList().stream() + .map(TestdataComparatorSortableEntity::getValue) + .filter(Objects::nonNull) + .count(); + var repeated = (int) (assigned - distinct); + return HardSoftScore.of(-repeated, -distinct); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/sort/comparator/TestdataComparatorSortableEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/comparator/TestdataComparatorSortableEntity.java new file mode 100644 index 0000000000..7acebfe425 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/comparator/TestdataComparatorSortableEntity.java @@ -0,0 +1,37 @@ +package ai.timefold.solver.core.testdomain.sort.comparator; + +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.TestSortableObject; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningEntity(comparatorClass = TestSortableComparator.class) +public class TestdataComparatorSortableEntity extends TestdataObject implements TestSortableObject { + + @PlanningVariable(valueRangeProviderRefs = "valueRange", comparatorClass = TestSortableComparator.class) + private TestdataSortableValue value; + private int difficulty; + + public TestdataComparatorSortableEntity() { + } + + public TestdataComparatorSortableEntity(String code, int difficulty) { + super(code); + this.difficulty = difficulty; + } + + public TestdataSortableValue getValue() { + return value; + } + + public void setValue(TestdataSortableValue value) { + this.value = value; + } + + @Override + public int getComparatorValue() { + return difficulty; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/sort/comparator/TestdataComparatorSortableSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/comparator/TestdataComparatorSortableSolution.java new file mode 100644 index 0000000000..f76c016a50 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/comparator/TestdataComparatorSortableSolution.java @@ -0,0 +1,83 @@ +package ai.timefold.solver.core.testdomain.sort.comparator; + +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 TestdataComparatorSortableSolution { + + public static SolutionDescriptor buildSolutionDescriptor() { + return SolutionDescriptor.buildSolutionDescriptor( + TestdataComparatorSortableSolution.class, + TestdataComparatorSortableEntity.class, + TestdataSortableValue.class); + } + + public static TestdataComparatorSortableSolution generateSolution(int valueCount, int entityCount, boolean shuffle) { + var entityList = new ArrayList<>(IntStream.range(0, entityCount) + .mapToObj(i -> new TestdataComparatorSortableEntity("Generated Entity " + i, 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); + } + TestdataComparatorSortableSolution solution = new TestdataComparatorSortableSolution(); + 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(TestdataComparatorSortableEntity entity) { + this.entityList = entityList.stream() + .filter(e -> e != entity) + .toList(); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/sort/comparatordifficulty/OneValuePerEntityDifficultyEasyScoreCalculator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/comparatordifficulty/OneValuePerEntityDifficultyEasyScoreCalculator.java new file mode 100644 index 0000000000..0bfe22b776 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/comparatordifficulty/OneValuePerEntityDifficultyEasyScoreCalculator.java @@ -0,0 +1,27 @@ +package ai.timefold.solver.core.testdomain.sort.comparatordifficulty; + +import java.util.Objects; + +import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; +import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; + +import org.jspecify.annotations.NonNull; + +public class OneValuePerEntityDifficultyEasyScoreCalculator + implements EasyScoreCalculator { + + @Override + public @NonNull HardSoftScore calculateScore(@NonNull TestdataDifficultySortableSolution solution) { + var distinct = (int) solution.getEntityList().stream() + .map(TestdataDifficultySortableEntity::getValue) + .filter(Objects::nonNull) + .distinct() + .count(); + var assigned = solution.getEntityList().stream() + .map(TestdataDifficultySortableEntity::getValue) + .filter(Objects::nonNull) + .count(); + var repeated = (int) (assigned - distinct); + return HardSoftScore.of(-repeated, -distinct); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/sort/comparatordifficulty/TestdataDifficultySortableEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/comparatordifficulty/TestdataDifficultySortableEntity.java new file mode 100644 index 0000000000..3c8ea7ac36 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/comparatordifficulty/TestdataDifficultySortableEntity.java @@ -0,0 +1,37 @@ +package ai.timefold.solver.core.testdomain.sort.comparatordifficulty; + +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.TestSortableObject; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningEntity(difficultyComparatorClass = TestSortableComparator.class) +public class TestdataDifficultySortableEntity extends TestdataObject implements TestSortableObject { + + @PlanningVariable(valueRangeProviderRefs = "valueRange", strengthComparatorClass = TestSortableComparator.class) + private TestdataSortableValue value; + private int difficulty; + + public TestdataDifficultySortableEntity() { + } + + public TestdataDifficultySortableEntity(String code, int difficulty) { + super(code); + this.difficulty = difficulty; + } + + public TestdataSortableValue getValue() { + return value; + } + + public void setValue(TestdataSortableValue value) { + this.value = value; + } + + @Override + public int getComparatorValue() { + return difficulty; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/sort/comparatordifficulty/TestdataDifficultySortableSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/comparatordifficulty/TestdataDifficultySortableSolution.java new file mode 100644 index 0000000000..643879c4e1 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/comparatordifficulty/TestdataDifficultySortableSolution.java @@ -0,0 +1,83 @@ +package ai.timefold.solver.core.testdomain.sort.comparatordifficulty; + +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 TestdataDifficultySortableSolution { + + public static SolutionDescriptor buildSolutionDescriptor() { + return SolutionDescriptor.buildSolutionDescriptor( + TestdataDifficultySortableSolution.class, + TestdataDifficultySortableEntity.class, + TestdataSortableValue.class); + } + + public static TestdataDifficultySortableSolution generateSolution(int valueCount, int entityCount, boolean shuffle) { + var entityList = new ArrayList<>(IntStream.range(0, entityCount) + .mapToObj(i -> new TestdataDifficultySortableEntity("Generated Entity " + i, 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); + } + TestdataDifficultySortableSolution solution = new TestdataDifficultySortableSolution(); + 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(TestdataDifficultySortableEntity entity) { + this.entityList = entityList.stream() + .filter(e -> e != entity) + .toList(); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/sort/factory/OneValuePerEntityFactoryEasyScoreCalculator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/factory/OneValuePerEntityFactoryEasyScoreCalculator.java new file mode 100644 index 0000000000..19d08a52e9 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/factory/OneValuePerEntityFactoryEasyScoreCalculator.java @@ -0,0 +1,28 @@ +package ai.timefold.solver.core.testdomain.sort.factory; + +import java.util.Objects; + +import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; +import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; + +import org.jspecify.annotations.NonNull; + +public class OneValuePerEntityFactoryEasyScoreCalculator + implements EasyScoreCalculator { + + @Override + public @NonNull HardSoftScore + calculateScore(@NonNull TestdataFactorySortableSolution solution) { + var distinct = (int) solution.getEntityList().stream() + .map(TestdataFactorySortableEntity::getValue) + .filter(Objects::nonNull) + .distinct() + .count(); + var assigned = solution.getEntityList().stream() + .map(TestdataFactorySortableEntity::getValue) + .filter(Objects::nonNull) + .count(); + var repeated = (int) (assigned - distinct); + return HardSoftScore.of(-repeated, -distinct); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/sort/factory/TestdataFactorySortableEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/factory/TestdataFactorySortableEntity.java new file mode 100644 index 0000000000..f38c8f15c4 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/factory/TestdataFactorySortableEntity.java @@ -0,0 +1,37 @@ +package ai.timefold.solver.core.testdomain.sort.factory; + +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.TestSortableFactory; +import ai.timefold.solver.core.testdomain.common.TestSortableObject; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningEntity(comparatorFactoryClass = TestSortableFactory.class) +public class TestdataFactorySortableEntity extends TestdataObject implements TestSortableObject { + + @PlanningVariable(valueRangeProviderRefs = "valueRange", comparatorFactoryClass = TestSortableFactory.class) + private TestdataSortableValue value; + private int difficulty; + + public TestdataFactorySortableEntity() { + } + + public TestdataFactorySortableEntity(String code, int difficulty) { + super(code); + this.difficulty = difficulty; + } + + public TestdataSortableValue getValue() { + return value; + } + + public void setValue(TestdataSortableValue value) { + this.value = value; + } + + @Override + public int getComparatorValue() { + return difficulty; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/sort/factory/TestdataFactorySortableSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/factory/TestdataFactorySortableSolution.java new file mode 100644 index 0000000000..c775c9a308 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/factory/TestdataFactorySortableSolution.java @@ -0,0 +1,83 @@ +package ai.timefold.solver.core.testdomain.sort.factory; + +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 TestdataFactorySortableSolution { + + public static SolutionDescriptor buildSolutionDescriptor() { + return SolutionDescriptor.buildSolutionDescriptor( + TestdataFactorySortableSolution.class, + TestdataFactorySortableEntity.class, + TestdataSortableValue.class); + } + + public static TestdataFactorySortableSolution generateSolution(int valueCount, int entityCount, boolean shuffle) { + var entityList = new ArrayList<>(IntStream.range(0, entityCount) + .mapToObj(i -> new TestdataFactorySortableEntity("Generated Entity " + i, 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); + } + TestdataFactorySortableSolution solution = new TestdataFactorySortableSolution(); + 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(TestdataFactorySortableEntity entity) { + this.entityList = entityList.stream() + .filter(e -> e != entity) + .toList(); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/sort/factorydifficulty/OneValuePerEntityDifficultyFactoryEasyScoreCalculator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/factorydifficulty/OneValuePerEntityDifficultyFactoryEasyScoreCalculator.java new file mode 100644 index 0000000000..14a79fe330 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/factorydifficulty/OneValuePerEntityDifficultyFactoryEasyScoreCalculator.java @@ -0,0 +1,28 @@ +package ai.timefold.solver.core.testdomain.sort.factorydifficulty; + +import java.util.Objects; + +import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; +import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; + +import org.jspecify.annotations.NonNull; + +public class OneValuePerEntityDifficultyFactoryEasyScoreCalculator + implements EasyScoreCalculator { + + @Override + public @NonNull HardSoftScore + calculateScore(@NonNull TestdataDifficultyFactorySortableSolution solution) { + var distinct = (int) solution.getEntityList().stream() + .map(TestdataDifficultyFactorySortableEntity::getValue) + .filter(Objects::nonNull) + .distinct() + .count(); + var assigned = solution.getEntityList().stream() + .map(TestdataDifficultyFactorySortableEntity::getValue) + .filter(Objects::nonNull) + .count(); + var repeated = (int) (assigned - distinct); + return HardSoftScore.of(-repeated, -distinct); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/sort/factorydifficulty/TestdataDifficultyFactorySortableEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/factorydifficulty/TestdataDifficultyFactorySortableEntity.java new file mode 100644 index 0000000000..c45b154a89 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/factorydifficulty/TestdataDifficultyFactorySortableEntity.java @@ -0,0 +1,37 @@ +package ai.timefold.solver.core.testdomain.sort.factorydifficulty; + +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.TestSortableFactory; +import ai.timefold.solver.core.testdomain.common.TestSortableObject; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningEntity(difficultyWeightFactoryClass = TestSortableFactory.class) +public class TestdataDifficultyFactorySortableEntity extends TestdataObject implements TestSortableObject { + + @PlanningVariable(valueRangeProviderRefs = "valueRange", strengthWeightFactoryClass = TestSortableFactory.class) + private TestdataSortableValue value; + private int difficulty; + + public TestdataDifficultyFactorySortableEntity() { + } + + public TestdataDifficultyFactorySortableEntity(String code, int difficulty) { + super(code); + this.difficulty = difficulty; + } + + public TestdataSortableValue getValue() { + return value; + } + + public void setValue(TestdataSortableValue value) { + this.value = value; + } + + @Override + public int getComparatorValue() { + return difficulty; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/sort/factorydifficulty/TestdataDifficultyFactorySortableSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/factorydifficulty/TestdataDifficultyFactorySortableSolution.java new file mode 100644 index 0000000000..a1a6b75668 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/factorydifficulty/TestdataDifficultyFactorySortableSolution.java @@ -0,0 +1,83 @@ +package ai.timefold.solver.core.testdomain.sort.factorydifficulty; + +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 TestdataDifficultyFactorySortableSolution { + + public static SolutionDescriptor buildSolutionDescriptor() { + return SolutionDescriptor.buildSolutionDescriptor( + TestdataDifficultyFactorySortableSolution.class, + TestdataDifficultyFactorySortableEntity.class, + TestdataSortableValue.class); + } + + public static TestdataDifficultyFactorySortableSolution generateSolution(int valueCount, int entityCount, boolean shuffle) { + var entityList = new ArrayList<>(IntStream.range(0, entityCount) + .mapToObj(i -> new TestdataDifficultyFactorySortableEntity("Generated Entity " + i, 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); + } + TestdataDifficultyFactorySortableSolution solution = new TestdataDifficultyFactorySortableSolution(); + 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(TestdataDifficultyFactorySortableEntity entity) { + this.entityList = entityList.stream() + .filter(e -> e != entity) + .toList(); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/mixed/comparator/TestdataInvalidMixedComparatorSortableEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/mixed/comparator/TestdataInvalidMixedComparatorSortableEntity.java new file mode 100644 index 0000000000..b664a20a7d --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/mixed/comparator/TestdataInvalidMixedComparatorSortableEntity.java @@ -0,0 +1,41 @@ +package ai.timefold.solver.core.testdomain.sort.invalid.mixed.comparator; + +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.DummyValueComparator; +import ai.timefold.solver.core.testdomain.common.DummyValueFactory; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningEntity +public class TestdataInvalidMixedComparatorSortableEntity extends TestdataObject { + + @PlanningVariable(valueRangeProviderRefs = "valueRange", comparatorClass = DummyValueComparator.class, + comparatorFactoryClass = DummyValueFactory.class) + private TestdataSortableValue value; + private int difficulty; + + public TestdataInvalidMixedComparatorSortableEntity() { + } + + public TestdataInvalidMixedComparatorSortableEntity(String code, int difficulty) { + super(code); + this.difficulty = difficulty; + } + + public TestdataSortableValue getValue() { + return value; + } + + public void setValue(TestdataSortableValue value) { + this.value = value; + } + + public int getDifficulty() { + return difficulty; + } + + public void setDifficulty(int difficulty) { + this.difficulty = difficulty; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/mixed/comparator/TestdataInvalidMixedComparatorSortableSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/mixed/comparator/TestdataInvalidMixedComparatorSortableSolution.java new file mode 100644 index 0000000000..89ada324c7 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/mixed/comparator/TestdataInvalidMixedComparatorSortableSolution.java @@ -0,0 +1,52 @@ +package ai.timefold.solver.core.testdomain.sort.invalid.mixed.comparator; + +import java.util.List; + +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.valuerange.ValueRangeProvider; +import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningSolution +public class TestdataInvalidMixedComparatorSortableSolution { + + private List valueList; + private List entityList; + private HardSoftScore score; + + @ValueRangeProvider(id = "valueRange") + @PlanningEntityCollectionProperty + 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(TestdataInvalidMixedComparatorSortableEntity entity) { + this.entityList = entityList.stream() + .filter(e -> e != entity) + .toList(); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/mixed/strength/TestdataInvalidMixedStrengthSortableEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/mixed/strength/TestdataInvalidMixedStrengthSortableEntity.java new file mode 100644 index 0000000000..1e9a860520 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/mixed/strength/TestdataInvalidMixedStrengthSortableEntity.java @@ -0,0 +1,41 @@ +package ai.timefold.solver.core.testdomain.sort.invalid.mixed.strength; + +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.DummyValueComparator; +import ai.timefold.solver.core.testdomain.common.DummyWeightValueFactory; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningEntity +public class TestdataInvalidMixedStrengthSortableEntity extends TestdataObject { + + @PlanningVariable(valueRangeProviderRefs = "valueRange", strengthComparatorClass = DummyValueComparator.class, + strengthWeightFactoryClass = DummyWeightValueFactory.class) + private TestdataSortableValue value; + private int difficulty; + + public TestdataInvalidMixedStrengthSortableEntity() { + } + + public TestdataInvalidMixedStrengthSortableEntity(String code, int difficulty) { + super(code); + this.difficulty = difficulty; + } + + public TestdataSortableValue getValue() { + return value; + } + + public void setValue(TestdataSortableValue value) { + this.value = value; + } + + public int getDifficulty() { + return difficulty; + } + + public void setDifficulty(int difficulty) { + this.difficulty = difficulty; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/mixed/strength/TestdataInvalidMixedStrengthSortableSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/mixed/strength/TestdataInvalidMixedStrengthSortableSolution.java new file mode 100644 index 0000000000..3b65d483d2 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/mixed/strength/TestdataInvalidMixedStrengthSortableSolution.java @@ -0,0 +1,52 @@ +package ai.timefold.solver.core.testdomain.sort.invalid.mixed.strength; + +import java.util.List; + +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.valuerange.ValueRangeProvider; +import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningSolution +public class TestdataInvalidMixedStrengthSortableSolution { + + private List valueList; + private List entityList; + private HardSoftScore score; + + @ValueRangeProvider(id = "valueRange") + @PlanningEntityCollectionProperty + 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(TestdataInvalidMixedStrengthSortableEntity entity) { + this.entityList = entityList.stream() + .filter(e -> e != entity) + .toList(); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/twocomparator/entity/TestdataInvalidTwoEntityComparatorSortableEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/twocomparator/entity/TestdataInvalidTwoEntityComparatorSortableEntity.java new file mode 100644 index 0000000000..0258960ace --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/twocomparator/entity/TestdataInvalidTwoEntityComparatorSortableEntity.java @@ -0,0 +1,39 @@ +package ai.timefold.solver.core.testdomain.sort.invalid.twocomparator.entity; + +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.DummyValueComparator; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningEntity(comparatorClass = DummyValueComparator.class, difficultyComparatorClass = DummyValueComparator.class) +public class TestdataInvalidTwoEntityComparatorSortableEntity extends TestdataObject { + + @PlanningVariable(valueRangeProviderRefs = "valueRange", comparatorClass = DummyValueComparator.class) + private TestdataSortableValue value; + private int difficulty; + + public TestdataInvalidTwoEntityComparatorSortableEntity() { + } + + public TestdataInvalidTwoEntityComparatorSortableEntity(String code, int difficulty) { + super(code); + this.difficulty = difficulty; + } + + public TestdataSortableValue getValue() { + return value; + } + + public void setValue(TestdataSortableValue value) { + this.value = value; + } + + public int getDifficulty() { + return difficulty; + } + + public void setDifficulty(int difficulty) { + this.difficulty = difficulty; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/twocomparator/entity/TestdataInvalidTwoEntityComparatorSortableSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/twocomparator/entity/TestdataInvalidTwoEntityComparatorSortableSolution.java new file mode 100644 index 0000000000..98b0f366da --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/twocomparator/entity/TestdataInvalidTwoEntityComparatorSortableSolution.java @@ -0,0 +1,52 @@ +package ai.timefold.solver.core.testdomain.sort.invalid.twocomparator.entity; + +import java.util.List; + +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.valuerange.ValueRangeProvider; +import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningSolution +public class TestdataInvalidTwoEntityComparatorSortableSolution { + + private List valueList; + private List entityList; + private HardSoftScore score; + + @ValueRangeProvider(id = "valueRange") + @PlanningEntityCollectionProperty + 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(TestdataInvalidTwoEntityComparatorSortableEntity entity) { + this.entityList = entityList.stream() + .filter(e -> e != entity) + .toList(); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/twocomparator/value/TestdataInvalidTwoValueComparatorSortableEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/twocomparator/value/TestdataInvalidTwoValueComparatorSortableEntity.java new file mode 100644 index 0000000000..bdbbaa19bd --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/twocomparator/value/TestdataInvalidTwoValueComparatorSortableEntity.java @@ -0,0 +1,40 @@ +package ai.timefold.solver.core.testdomain.sort.invalid.twocomparator.value; + +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.DummyValueComparator; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningEntity +public class TestdataInvalidTwoValueComparatorSortableEntity extends TestdataObject { + + @PlanningVariable(valueRangeProviderRefs = "valueRange", comparatorClass = DummyValueComparator.class, + strengthComparatorClass = DummyValueComparator.class) + private TestdataSortableValue value; + private int difficulty; + + public TestdataInvalidTwoValueComparatorSortableEntity() { + } + + public TestdataInvalidTwoValueComparatorSortableEntity(String code, int difficulty) { + super(code); + this.difficulty = difficulty; + } + + public TestdataSortableValue getValue() { + return value; + } + + public void setValue(TestdataSortableValue value) { + this.value = value; + } + + public int getDifficulty() { + return difficulty; + } + + public void setDifficulty(int difficulty) { + this.difficulty = difficulty; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/twocomparator/value/TestdataInvalidTwoValueComparatorSortableSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/twocomparator/value/TestdataInvalidTwoValueComparatorSortableSolution.java new file mode 100644 index 0000000000..c2eaadd32e --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/twocomparator/value/TestdataInvalidTwoValueComparatorSortableSolution.java @@ -0,0 +1,52 @@ +package ai.timefold.solver.core.testdomain.sort.invalid.twocomparator.value; + +import java.util.List; + +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.valuerange.ValueRangeProvider; +import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningSolution +public class TestdataInvalidTwoValueComparatorSortableSolution { + + private List valueList; + private List entityList; + private HardSoftScore score; + + @ValueRangeProvider(id = "valueRange") + @PlanningEntityCollectionProperty + 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(TestdataInvalidTwoValueComparatorSortableEntity entity) { + this.entityList = entityList.stream() + .filter(e -> e != entity) + .toList(); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/twofactory/entity/TestdataInvalidTwoEntityFactorySortableEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/twofactory/entity/TestdataInvalidTwoEntityFactorySortableEntity.java new file mode 100644 index 0000000000..5c37dff2c7 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/twofactory/entity/TestdataInvalidTwoEntityFactorySortableEntity.java @@ -0,0 +1,39 @@ +package ai.timefold.solver.core.testdomain.sort.invalid.twofactory.entity; + +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.DummyValueFactory; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningEntity(comparatorFactoryClass = DummyValueFactory.class, difficultyWeightFactoryClass = DummyValueFactory.class) +public class TestdataInvalidTwoEntityFactorySortableEntity extends TestdataObject { + + @PlanningVariable(valueRangeProviderRefs = "valueRange", comparatorFactoryClass = DummyValueFactory.class) + private TestdataSortableValue value; + private int difficulty; + + public TestdataInvalidTwoEntityFactorySortableEntity() { + } + + public TestdataInvalidTwoEntityFactorySortableEntity(String code, int difficulty) { + super(code); + this.difficulty = difficulty; + } + + public TestdataSortableValue getValue() { + return value; + } + + public void setValue(TestdataSortableValue value) { + this.value = value; + } + + public int getDifficulty() { + return difficulty; + } + + public void setDifficulty(int difficulty) { + this.difficulty = difficulty; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/twofactory/entity/TestdataInvalidTwoEntityFactorySortableSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/twofactory/entity/TestdataInvalidTwoEntityFactorySortableSolution.java new file mode 100644 index 0000000000..8399a12602 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/twofactory/entity/TestdataInvalidTwoEntityFactorySortableSolution.java @@ -0,0 +1,52 @@ +package ai.timefold.solver.core.testdomain.sort.invalid.twofactory.entity; + +import java.util.List; + +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.valuerange.ValueRangeProvider; +import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningSolution +public class TestdataInvalidTwoEntityFactorySortableSolution { + + private List valueList; + private List entityList; + private HardSoftScore score; + + @ValueRangeProvider(id = "valueRange") + @PlanningEntityCollectionProperty + 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(TestdataInvalidTwoEntityFactorySortableEntity entity) { + this.entityList = entityList.stream() + .filter(e -> e != entity) + .toList(); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/twofactory/value/TestdataInvalidTwoValueFactorySortableEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/twofactory/value/TestdataInvalidTwoValueFactorySortableEntity.java new file mode 100644 index 0000000000..d7592e526c --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/twofactory/value/TestdataInvalidTwoValueFactorySortableEntity.java @@ -0,0 +1,41 @@ +package ai.timefold.solver.core.testdomain.sort.invalid.twofactory.value; + +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.DummyValueFactory; +import ai.timefold.solver.core.testdomain.common.DummyWeightValueFactory; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningEntity +public class TestdataInvalidTwoValueFactorySortableEntity extends TestdataObject { + + @PlanningVariable(valueRangeProviderRefs = "valueRange", comparatorFactoryClass = DummyValueFactory.class, + strengthWeightFactoryClass = DummyWeightValueFactory.class) + private TestdataSortableValue value; + private int difficulty; + + public TestdataInvalidTwoValueFactorySortableEntity() { + } + + public TestdataInvalidTwoValueFactorySortableEntity(String code, int difficulty) { + super(code); + this.difficulty = difficulty; + } + + public TestdataSortableValue getValue() { + return value; + } + + public void setValue(TestdataSortableValue value) { + this.value = value; + } + + public int getDifficulty() { + return difficulty; + } + + public void setDifficulty(int difficulty) { + this.difficulty = difficulty; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/twofactory/value/TestdataInvalidTwoValueFactorySortableSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/twofactory/value/TestdataInvalidTwoValueFactorySortableSolution.java new file mode 100644 index 0000000000..91114015a3 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/sort/invalid/twofactory/value/TestdataInvalidTwoValueFactorySortableSolution.java @@ -0,0 +1,52 @@ +package ai.timefold.solver.core.testdomain.sort.invalid.twofactory.value; + +import java.util.List; + +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.valuerange.ValueRangeProvider; +import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningSolution +public class TestdataInvalidTwoValueFactorySortableSolution { + + private List valueList; + private List entityList; + private HardSoftScore score; + + @ValueRangeProvider(id = "valueRange") + @PlanningEntityCollectionProperty + 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(TestdataInvalidTwoValueFactorySortableEntity entity) { + this.entityList = entityList.stream() + .filter(e -> e != entity) + .toList(); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/comparator/OneValuePerEntityComparatorRangeEasyScoreCalculator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/comparator/OneValuePerEntityComparatorRangeEasyScoreCalculator.java new file mode 100644 index 0000000000..5af384b43f --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/comparator/OneValuePerEntityComparatorRangeEasyScoreCalculator.java @@ -0,0 +1,28 @@ +package ai.timefold.solver.core.testdomain.valuerange.sort.comparator; + +import java.util.Objects; + +import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; +import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; + +import org.jspecify.annotations.NonNull; + +public class OneValuePerEntityComparatorRangeEasyScoreCalculator + implements EasyScoreCalculator { + + @Override + public @NonNull HardSoftScore + calculateScore(@NonNull TestdataComparatorSortableEntityProvidingSolution solution) { + var distinct = (int) solution.getEntityList().stream() + .map(TestdataComparatorSortableEntityProvidingEntity::getValue) + .filter(Objects::nonNull) + .distinct() + .count(); + var assigned = solution.getEntityList().stream() + .map(TestdataComparatorSortableEntityProvidingEntity::getValue) + .filter(Objects::nonNull) + .count(); + var repeated = (int) (assigned - distinct); + return HardSoftScore.of(-repeated, -distinct); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/comparator/TestdataComparatorSortableEntityProvidingEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/comparator/TestdataComparatorSortableEntityProvidingEntity.java new file mode 100644 index 0000000000..8fc6b593bd --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/comparator/TestdataComparatorSortableEntityProvidingEntity.java @@ -0,0 +1,53 @@ +package ai.timefold.solver.core.testdomain.valuerange.sort.comparator; + +import java.util.List; + +import ai.timefold.solver.core.api.domain.entity.PlanningEntity; +import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; +import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; +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.TestSortableObject; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningEntity(difficultyComparatorClass = TestSortableComparator.class) +public class TestdataComparatorSortableEntityProvidingEntity extends TestdataObject implements TestSortableObject { + + @PlanningVariable(valueRangeProviderRefs = "valueRange", strengthComparatorClass = TestSortableComparator.class) + private TestdataSortableValue value; + @ValueRangeProvider(id = "valueRange") + @PlanningEntityCollectionProperty + private List valueRange; + + private int difficulty; + + public TestdataComparatorSortableEntityProvidingEntity() { + } + + public TestdataComparatorSortableEntityProvidingEntity(String code, int difficulty) { + super(code); + this.difficulty = difficulty; + } + + public TestdataSortableValue getValue() { + return value; + } + + public void setValue(TestdataSortableValue value) { + this.value = value; + } + + public List getValueRange() { + return valueRange; + } + + public void setValueRange(List valueRange) { + this.valueRange = valueRange; + } + + @Override + public int getComparatorValue() { + return difficulty; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/comparator/TestdataComparatorSortableEntityProvidingSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/comparator/TestdataComparatorSortableEntityProvidingSolution.java new file mode 100644 index 0000000000..0a108b6d9a --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/comparator/TestdataComparatorSortableEntityProvidingSolution.java @@ -0,0 +1,75 @@ +package ai.timefold.solver.core.testdomain.valuerange.sort.comparator; + +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.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 TestdataComparatorSortableEntityProvidingSolution { + + public static SolutionDescriptor buildSolutionDescriptor() { + return SolutionDescriptor.buildSolutionDescriptor( + TestdataComparatorSortableEntityProvidingSolution.class, + TestdataComparatorSortableEntityProvidingEntity.class); + } + + public static TestdataComparatorSortableEntityProvidingSolution generateSolution(int valueCount, int entityCount, + boolean shuffle) { + var entityList = new ArrayList<>(IntStream.range(0, entityCount) + .mapToObj(i -> new TestdataComparatorSortableEntityProvidingEntity("Generated Entity " + i, i)) + .toList()); + var valueList = IntStream.range(0, valueCount) + .mapToObj(i -> new TestdataSortableValue("Generated Value " + i, i)) + .toList(); + var random = new Random(0); + var solution = new TestdataComparatorSortableEntityProvidingSolution(); + for (var entity : entityList) { + var valueRange = new ArrayList<>(valueList); + if (shuffle) { + Collections.shuffle(valueRange, random); + } + entity.setValueRange(valueRange); + } + if (shuffle) { + Collections.shuffle(entityList, random); + } + solution.setEntityList(entityList); + return solution; + } + + private List entityList; + private HardSoftScore score; + + @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(TestdataComparatorSortableEntityProvidingEntity entity) { + this.entityList = entityList.stream() + .filter(e -> e != entity) + .toList(); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/comparatorstrength/OneValuePerEntityStrengthRangeEasyScoreCalculator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/comparatorstrength/OneValuePerEntityStrengthRangeEasyScoreCalculator.java new file mode 100644 index 0000000000..37ff33cf77 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/comparatorstrength/OneValuePerEntityStrengthRangeEasyScoreCalculator.java @@ -0,0 +1,28 @@ +package ai.timefold.solver.core.testdomain.valuerange.sort.comparatorstrength; + +import java.util.Objects; + +import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; +import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; + +import org.jspecify.annotations.NonNull; + +public class OneValuePerEntityStrengthRangeEasyScoreCalculator + implements EasyScoreCalculator { + + @Override + public @NonNull HardSoftScore + calculateScore(@NonNull TestdataStrengthSortableEntityProvidingSolution solution) { + var distinct = (int) solution.getEntityList().stream() + .map(TestdataStrengthSortableEntityProvidingEntity::getValue) + .filter(Objects::nonNull) + .distinct() + .count(); + var assigned = solution.getEntityList().stream() + .map(TestdataStrengthSortableEntityProvidingEntity::getValue) + .filter(Objects::nonNull) + .count(); + var repeated = (int) (assigned - distinct); + return HardSoftScore.of(-repeated, -distinct); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/comparatorstrength/TestdataStrengthSortableEntityProvidingEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/comparatorstrength/TestdataStrengthSortableEntityProvidingEntity.java new file mode 100644 index 0000000000..d9a8a4b30a --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/comparatorstrength/TestdataStrengthSortableEntityProvidingEntity.java @@ -0,0 +1,53 @@ +package ai.timefold.solver.core.testdomain.valuerange.sort.comparatorstrength; + +import java.util.List; + +import ai.timefold.solver.core.api.domain.entity.PlanningEntity; +import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; +import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; +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.TestSortableObject; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningEntity(difficultyComparatorClass = TestSortableComparator.class) +public class TestdataStrengthSortableEntityProvidingEntity extends TestdataObject implements TestSortableObject { + + @PlanningVariable(valueRangeProviderRefs = "valueRange", strengthComparatorClass = TestSortableComparator.class) + private TestdataSortableValue value; + @ValueRangeProvider(id = "valueRange") + @PlanningEntityCollectionProperty + private List valueRange; + + private int difficulty; + + public TestdataStrengthSortableEntityProvidingEntity() { + } + + public TestdataStrengthSortableEntityProvidingEntity(String code, int difficulty) { + super(code); + this.difficulty = difficulty; + } + + public TestdataSortableValue getValue() { + return value; + } + + public void setValue(TestdataSortableValue value) { + this.value = value; + } + + public List getValueRange() { + return valueRange; + } + + public void setValueRange(List valueRange) { + this.valueRange = valueRange; + } + + @Override + public int getComparatorValue() { + return difficulty; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/comparatorstrength/TestdataStrengthSortableEntityProvidingSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/comparatorstrength/TestdataStrengthSortableEntityProvidingSolution.java new file mode 100644 index 0000000000..e2d6537c83 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/comparatorstrength/TestdataStrengthSortableEntityProvidingSolution.java @@ -0,0 +1,75 @@ +package ai.timefold.solver.core.testdomain.valuerange.sort.comparatorstrength; + +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.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 TestdataStrengthSortableEntityProvidingSolution { + + public static SolutionDescriptor buildSolutionDescriptor() { + return SolutionDescriptor.buildSolutionDescriptor( + TestdataStrengthSortableEntityProvidingSolution.class, + TestdataStrengthSortableEntityProvidingEntity.class); + } + + public static TestdataStrengthSortableEntityProvidingSolution generateSolution(int valueCount, int entityCount, + boolean shuffle) { + var entityList = new ArrayList<>(IntStream.range(0, entityCount) + .mapToObj(i -> new TestdataStrengthSortableEntityProvidingEntity("Generated Entity " + i, i)) + .toList()); + var valueList = IntStream.range(0, valueCount) + .mapToObj(i -> new TestdataSortableValue("Generated Value " + i, i)) + .toList(); + var random = new Random(0); + var solution = new TestdataStrengthSortableEntityProvidingSolution(); + for (var entity : entityList) { + var valueRange = new ArrayList<>(valueList); + if (shuffle) { + Collections.shuffle(valueRange, random); + } + entity.setValueRange(valueRange); + } + if (shuffle) { + Collections.shuffle(entityList, random); + } + solution.setEntityList(entityList); + return solution; + } + + private List entityList; + private HardSoftScore score; + + @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(TestdataStrengthSortableEntityProvidingEntity entity) { + this.entityList = entityList.stream() + .filter(e -> e != entity) + .toList(); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/factory/OneValuePerEntityFactoryRangeEasyScoreCalculator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/factory/OneValuePerEntityFactoryRangeEasyScoreCalculator.java new file mode 100644 index 0000000000..dd4a09a01f --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/factory/OneValuePerEntityFactoryRangeEasyScoreCalculator.java @@ -0,0 +1,29 @@ +package ai.timefold.solver.core.testdomain.valuerange.sort.factory; + +import java.util.Objects; + +import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; +import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; + +import org.jspecify.annotations.NonNull; + +public class OneValuePerEntityFactoryRangeEasyScoreCalculator + implements EasyScoreCalculator { + + @Override + public @NonNull HardSoftScore + calculateScore( + @NonNull TestdataFactorySortableEntityProvidingSolution solution) { + var distinct = (int) solution.getEntityList().stream() + .map(TestdataFactorySortableEntityProvidingEntity::getValue) + .filter(Objects::nonNull) + .distinct() + .count(); + var assigned = solution.getEntityList().stream() + .map(TestdataFactorySortableEntityProvidingEntity::getValue) + .filter(Objects::nonNull) + .count(); + var repeated = (int) (assigned - distinct); + return HardSoftScore.of(-repeated, -distinct); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/factory/TestdataFactorySortableEntityProvidingEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/factory/TestdataFactorySortableEntityProvidingEntity.java new file mode 100644 index 0000000000..9e59a830d9 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/factory/TestdataFactorySortableEntityProvidingEntity.java @@ -0,0 +1,54 @@ +package ai.timefold.solver.core.testdomain.valuerange.sort.factory; + +import java.util.List; + +import ai.timefold.solver.core.api.domain.entity.PlanningEntity; +import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; +import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; +import ai.timefold.solver.core.api.domain.variable.PlanningVariable; +import ai.timefold.solver.core.testdomain.TestdataObject; +import ai.timefold.solver.core.testdomain.common.TestSortableFactory; +import ai.timefold.solver.core.testdomain.common.TestSortableObject; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningEntity(difficultyWeightFactoryClass = TestSortableFactory.class) +public class TestdataFactorySortableEntityProvidingEntity extends TestdataObject + implements TestSortableObject { + + @PlanningVariable(valueRangeProviderRefs = "valueRange", comparatorFactoryClass = TestSortableFactory.class) + private TestdataSortableValue value; + @ValueRangeProvider(id = "valueRange") + @PlanningEntityCollectionProperty + private List valueRange; + + private int difficulty; + + public TestdataFactorySortableEntityProvidingEntity() { + } + + public TestdataFactorySortableEntityProvidingEntity(String code, int difficulty) { + super(code); + this.difficulty = difficulty; + } + + public TestdataSortableValue getValue() { + return value; + } + + public void setValue(TestdataSortableValue value) { + this.value = value; + } + + public List getValueRange() { + return valueRange; + } + + public void setValueRange(List valueRange) { + this.valueRange = valueRange; + } + + @Override + public int getComparatorValue() { + return difficulty; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/factory/TestdataFactorySortableEntityProvidingSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/factory/TestdataFactorySortableEntityProvidingSolution.java new file mode 100644 index 0000000000..e6f04f6300 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/factory/TestdataFactorySortableEntityProvidingSolution.java @@ -0,0 +1,75 @@ +package ai.timefold.solver.core.testdomain.valuerange.sort.factory; + +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.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 TestdataFactorySortableEntityProvidingSolution { + + public static SolutionDescriptor buildSolutionDescriptor() { + return SolutionDescriptor.buildSolutionDescriptor( + TestdataFactorySortableEntityProvidingSolution.class, + TestdataFactorySortableEntityProvidingEntity.class); + } + + public static TestdataFactorySortableEntityProvidingSolution generateSolution(int valueCount, int entityCount, + boolean shuffle) { + var entityList = new ArrayList<>(IntStream.range(0, entityCount) + .mapToObj(i -> new TestdataFactorySortableEntityProvidingEntity("Generated Entity " + i, i)) + .toList()); + var valueList = IntStream.range(0, valueCount) + .mapToObj(i -> new TestdataSortableValue("Generated Value " + i, i)) + .toList(); + var solution = new TestdataFactorySortableEntityProvidingSolution(); + var random = new Random(0); + for (var entity : entityList) { + var valueRange = new ArrayList<>(valueList); + if (shuffle) { + Collections.shuffle(valueRange, random); + } + entity.setValueRange(valueRange); + } + if (shuffle) { + Collections.shuffle(entityList, random); + } + solution.setEntityList(entityList); + return solution; + } + + private List entityList; + private HardSoftScore score; + + @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(TestdataFactorySortableEntityProvidingEntity entity) { + this.entityList = entityList.stream() + .filter(e -> e != entity) + .toList(); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/factorystrength/OneValuePerEntityStrengthFactoryRangeEasyScoreCalculator.java b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/factorystrength/OneValuePerEntityStrengthFactoryRangeEasyScoreCalculator.java new file mode 100644 index 0000000000..ab7e172c11 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/factorystrength/OneValuePerEntityStrengthFactoryRangeEasyScoreCalculator.java @@ -0,0 +1,29 @@ +package ai.timefold.solver.core.testdomain.valuerange.sort.factorystrength; + +import java.util.Objects; + +import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; +import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; + +import org.jspecify.annotations.NonNull; + +public class OneValuePerEntityStrengthFactoryRangeEasyScoreCalculator + implements EasyScoreCalculator { + + @Override + public @NonNull HardSoftScore + calculateScore( + @NonNull TestdataStrengthFactorySortableEntityProvidingSolution solution) { + var distinct = (int) solution.getEntityList().stream() + .map(TestdataStrengthFactorySortableEntityProvidingEntity::getValue) + .filter(Objects::nonNull) + .distinct() + .count(); + var assigned = solution.getEntityList().stream() + .map(TestdataStrengthFactorySortableEntityProvidingEntity::getValue) + .filter(Objects::nonNull) + .count(); + var repeated = (int) (assigned - distinct); + return HardSoftScore.of(-repeated, -distinct); + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/factorystrength/TestdataStrengthFactorySortableEntityProvidingEntity.java b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/factorystrength/TestdataStrengthFactorySortableEntityProvidingEntity.java new file mode 100644 index 0000000000..2c342cae16 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/factorystrength/TestdataStrengthFactorySortableEntityProvidingEntity.java @@ -0,0 +1,54 @@ +package ai.timefold.solver.core.testdomain.valuerange.sort.factorystrength; + +import java.util.List; + +import ai.timefold.solver.core.api.domain.entity.PlanningEntity; +import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; +import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; +import ai.timefold.solver.core.api.domain.variable.PlanningVariable; +import ai.timefold.solver.core.testdomain.TestdataObject; +import ai.timefold.solver.core.testdomain.common.TestSortableFactory; +import ai.timefold.solver.core.testdomain.common.TestSortableObject; +import ai.timefold.solver.core.testdomain.common.TestdataSortableValue; + +@PlanningEntity(difficultyWeightFactoryClass = TestSortableFactory.class) +public class TestdataStrengthFactorySortableEntityProvidingEntity extends TestdataObject + implements TestSortableObject { + + @PlanningVariable(valueRangeProviderRefs = "valueRange", strengthWeightFactoryClass = TestSortableFactory.class) + private TestdataSortableValue value; + @ValueRangeProvider(id = "valueRange") + @PlanningEntityCollectionProperty + private List valueRange; + + private int difficulty; + + public TestdataStrengthFactorySortableEntityProvidingEntity() { + } + + public TestdataStrengthFactorySortableEntityProvidingEntity(String code, int difficulty) { + super(code); + this.difficulty = difficulty; + } + + public TestdataSortableValue getValue() { + return value; + } + + public void setValue(TestdataSortableValue value) { + this.value = value; + } + + public List getValueRange() { + return valueRange; + } + + public void setValueRange(List valueRange) { + this.valueRange = valueRange; + } + + @Override + public int getComparatorValue() { + return difficulty; + } +} diff --git a/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/factorystrength/TestdataStrengthFactorySortableEntityProvidingSolution.java b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/factorystrength/TestdataStrengthFactorySortableEntityProvidingSolution.java new file mode 100644 index 0000000000..75d9f2b883 --- /dev/null +++ b/core/src/test/java/ai/timefold/solver/core/testdomain/valuerange/sort/factorystrength/TestdataStrengthFactorySortableEntityProvidingSolution.java @@ -0,0 +1,75 @@ +package ai.timefold.solver.core.testdomain.valuerange.sort.factorystrength; + +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.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 TestdataStrengthFactorySortableEntityProvidingSolution { + + public static SolutionDescriptor buildSolutionDescriptor() { + return SolutionDescriptor.buildSolutionDescriptor( + TestdataStrengthFactorySortableEntityProvidingSolution.class, + TestdataStrengthFactorySortableEntityProvidingEntity.class); + } + + public static TestdataStrengthFactorySortableEntityProvidingSolution generateSolution(int valueCount, int entityCount, + boolean shuffle) { + var entityList = new ArrayList<>(IntStream.range(0, entityCount) + .mapToObj(i -> new TestdataStrengthFactorySortableEntityProvidingEntity("Generated Entity " + i, i)) + .toList()); + var valueList = IntStream.range(0, valueCount) + .mapToObj(i -> new TestdataSortableValue("Generated Value " + i, i)) + .toList(); + var solution = new TestdataStrengthFactorySortableEntityProvidingSolution(); + var random = new Random(0); + for (var entity : entityList) { + var valueRange = new ArrayList<>(valueList); + if (shuffle) { + Collections.shuffle(valueRange, random); + } + entity.setValueRange(valueRange); + } + if (shuffle) { + Collections.shuffle(entityList, random); + } + solution.setEntityList(entityList); + return solution; + } + + private List entityList; + private HardSoftScore score; + + @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(TestdataStrengthFactorySortableEntityProvidingEntity entity) { + this.entityList = entityList.stream() + .filter(e -> e != entity) + .toList(); + } +} diff --git a/docs/src/modules/ROOT/pages/optimization-algorithms/construction-heuristics.adoc b/docs/src/modules/ROOT/pages/optimization-algorithms/construction-heuristics.adoc index 79dbda1baf..469471a4a3 100644 --- a/docs/src/modules/ROOT/pages/optimization-algorithms/construction-heuristics.adoc +++ b/docs/src/modules/ROOT/pages/optimization-algorithms/construction-heuristics.adoc @@ -69,12 +69,15 @@ For a very advanced configuration, see <>, but assigns the more difficult planning entities first, because they are less likely to fit in the leftovers. -So it sorts the planning entities on decreasing difficulty. +Like <>, but analyzes the "more difficult" planning entities first, +because they are less likely to fit in the leftovers. +It sorts the planning entities in descending order by any given metric, +meaning that "more difficult" planning entities are added earlier in the list while "less difficult" values are included later. image::optimization-algorithms/construction-heuristics/firstFitDecreasingNQueens04.png[align="center"] -Requires the model to support xref:using-timefold-solver/modeling-planning-problems.adoc#planningEntityDifficulty[planning entity difficulty comparison]. +Requires the model +to support xref:using-timefold-solver/modeling-planning-problems.adoc#planningEntitySorting[planning entity sorting]. [NOTE] ==== @@ -118,10 +121,13 @@ For a very advanced configuration, see <>, but it analyzes the "weaker" planning values first, +because the "stronger" planning values are more likely to be able to accommodate later planning entities. +It sorts the planning values in ascending order by any given metric, +meaning that "weaker" planning values are added earlier in the list while "stronger" values are included later. -Requires the model to support xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueStrength[planning value strength comparison]. +Requires the model +to support xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueSorting[planning value sorting]. [NOTE] ==== @@ -165,11 +171,12 @@ For a very advanced configuration, see <> and <>. +It sorts the planning entities in descending order and the planning values in ascending order based on defined metrics. -Requires the model to support xref:using-timefold-solver/modeling-planning-problems.adoc#planningEntityDifficulty[planning entity difficulty comparison] -and xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueStrength[planning value strength comparison]. +Requires the model +to support xref:using-timefold-solver/modeling-planning-problems.adoc#planningEntitySorting[planning entity sorting] +and xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueSorting[planning value sorting]. [NOTE] ==== @@ -214,10 +221,13 @@ For a very advanced configuration, see <>, but it analyzes the "stronger" planning values first, +because the "stronger" planning values are more likely to have a lower soft cost to use. +So it sorts the planning values in descending order by any given metric, +meaning that "stronger" planning values are added earlier in the list. -Requires the model to support xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueStrength[planning value strength comparison]. +Requires the model +to support xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueSorting[planning value sorting]. [NOTE] ==== @@ -261,11 +271,12 @@ For a very advanced configuration, see <> and <>. +It sorts the planning entities and the planning values in descending order based on defined metrics. -Requires the model to support xref:using-timefold-solver/modeling-planning-problems.adoc#planningEntityDifficulty[planning entity difficulty comparison] -and xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueStrength[planning value strength comparison]. +Requires the model +to support xref:using-timefold-solver/modeling-planning-problems.adoc#planningEntitySorting[planning entity sorting] +and xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueSorting[planning value sorting]. [NOTE] ==== @@ -310,8 +321,10 @@ For a very advanced configuration, see <>, <>, -<>, <>, +Allocate Entity From Queue is a versatile generic form of <>, +<>, +<>, +<>, <> and <>. It works like this: @@ -338,27 +351,29 @@ Verbose simple configuration: ---- ALLOCATE_ENTITY_FROM_QUEUE - DECREASING_DIFFICULTY_IF_AVAILABLE - INCREASING_STRENGTH_IF_AVAILABLE + DESCENDING_IF_AVAILABLE + ASCENDING_IF_AVAILABLE ---- The `entitySorterManner` options are: -* ``DECREASING_DIFFICULTY``: Initialize the more difficult planning entities first. +* ``DESCENDING``: Evaluate the planning entities in descending order based on a given metric. This usually increases pruning (and therefore improves scalability). -Requires the model to support xref:using-timefold-solver/modeling-planning-problems.adoc#planningEntityDifficulty[planning entity difficulty comparison]. -* `DECREASING_DIFFICULTY_IF_AVAILABLE` (default): If the model supports xref:using-timefold-solver/modeling-planning-problems.adoc#planningEntityDifficulty[planning entity difficulty comparison], behave like ``DECREASING_DIFFICULTY``, else like ``NONE``. +Requires the model +to support xref:using-timefold-solver/modeling-planning-problems.adoc#planningEntitySorting[planning entity sorting]. +* `DESCENDING_IF_AVAILABLE` (default): If the model supports xref:using-timefold-solver/modeling-planning-problems.adoc#planningEntitySorting[planning entity sorting], behave like ``DESCENDING``, else like ``NONE``. * ``NONE``: Initialize the planning entities in original order. The `valueSorterManner` options are: -* ``INCREASING_STRENGTH``: Evaluate the planning values in increasing strength. -Requires the model to support xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueStrength[planning value strength comparison]. -* `INCREASING_STRENGTH_IF_AVAILABLE` (default): If the model supports xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueStrength[planning value strength comparison], behave like ``INCREASING_STRENGTH``, else like ``NONE``. -* ``DECREASING_STRENGTH``: Evaluate the planning values in decreasing strength. -Requires the model to support xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueStrength[planning value strength comparison]. -* ``DECREASING_STRENGTH_IF_AVAILABLE``: If the model supports xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueStrength[planning value strength comparison], behave like ``DECREASING_STRENGTH``, else like ``NONE``. +* ``ASCENDING``: Evaluate the planning values in ascending order based on a given metric. +Requires the model +to support xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueSorting[planning value sorting]. +* `ASCENDING_IF_AVAILABLE` (default): If the model supports xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueSorting[planning value sorting], behave like ``ASCENDING``, else like ``NONE``. +* ``DESCENDING``: Evaluate the planning values in descending order based on a given metric. +Requires the model to support xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueSorting[planning value sorting]. +* ``DESCENDING_IF_AVAILABLE``: If the model supports xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueSorting[planning value sorting], behave like ``DESCENDING``, else like ``NONE``. * ``NONE``: Try the planning values in original order. Advanced configuration with <> for a single entity class with one variable: @@ -370,14 +385,14 @@ Advanced configuration with <> for PHASE SORTED - DECREASING_DIFFICULTY + DESCENDING PHASE SORTED - INCREASING_STRENGTH + ASCENDING @@ -529,8 +544,8 @@ Verbose simple configuration: ---- ALLOCATE_TO_VALUE_FROM_QUEUE - DECREASING_DIFFICULTY_IF_AVAILABLE - INCREASING_STRENGTH_IF_AVAILABLE + DESCENDING_IF_AVAILABLE + ASCENDING_IF_AVAILABLE ---- @@ -543,16 +558,20 @@ Advanced configuration for a single entity class with a single variable: PHASE SORTED - INCREASING_STRENGTH + ASCENDING - - - PHASE - SORTED - DECREASING_DIFFICULTY - - - + + + + + + PHASE + SORTED + DESCENDING + + + + ---- @@ -635,7 +654,7 @@ This algorithm has not been implemented yet. [#allocateFromPoolAlgorithm] === Algorithm description -Allocate From Pool is a versatile, generic form of <> and <>. +Allocate From Pool is a versatile generic form of <> and <>. It works like this: . Put all entity-value combinations in a pool. @@ -661,8 +680,8 @@ Verbose simple configuration: ---- ALLOCATE_FROM_POOL - DECREASING_DIFFICULTY_IF_AVAILABLE - INCREASING_STRENGTH_IF_AVAILABLE + DESCENDING_IF_AVAILABLE + ASCENDING_IF_AVAILABLE ---- @@ -678,12 +697,12 @@ Advanced configuration with <> for a singl PHASE SORTED - DECREASING_DIFFICULTY + DESCENDING PHASE SORTED - INCREASING_STRENGTH + ASCENDING @@ -750,14 +769,14 @@ Advanced configuration for a single entity class with a list variable and a sing PHASE SORTED - DECREASING_DIFFICULTY + DESCENDING PHASE SORTED - INCREASING_STRENGTH + ASCENDING @@ -769,7 +788,7 @@ Advanced configuration for a single entity class with a list variable and a sing PHASE SORTED - INCREASING_STRENGTH + ASCENDING @@ -887,4 +906,4 @@ It is supported to only partition the Construction Heuristic phase. Other `Selector` customizations can also reduce the number of moves generated by step: * xref:optimization-algorithms/overview.adoc#filteredSelection[Filtered selection] -* xref:optimization-algorithms/overview.adoc#limitedSelection[Limited selection] \ No newline at end of file +* xref:optimization-algorithms/overview.adoc#limitedSelection[Limited selection] diff --git a/docs/src/modules/ROOT/pages/optimization-algorithms/exhaustive-search.adoc b/docs/src/modules/ROOT/pages/optimization-algorithms/exhaustive-search.adoc index fd4e96ea8d..3885e7df00 100644 --- a/docs/src/modules/ROOT/pages/optimization-algorithms/exhaustive-search.adoc +++ b/docs/src/modules/ROOT/pages/optimization-algorithms/exhaustive-search.adoc @@ -114,8 +114,8 @@ Advanced configuration: BRANCH_AND_BOUND DEPTH_FIRST - DECREASING_DIFFICULTY_IF_AVAILABLE - INCREASING_STRENGTH_IF_AVAILABLE + DESCENDING_IF_AVAILABLE + ASCENDING_IF_AVAILABLE ---- @@ -160,17 +160,21 @@ The `nodeExplorationType` options are: The `entitySorterManner` options are: -* ``DECREASING_DIFFICULTY``: Initialize the more difficult planning entities first. This usually increases pruning (and therefore improves scalability). -Requires the model to support xref:using-timefold-solver/modeling-planning-problems.adoc#planningEntityDifficulty[planning entity difficulty comparison]. -* `DECREASING_DIFFICULTY_IF_AVAILABLE` (default): If the model supports xref:using-timefold-solver/modeling-planning-problems.adoc#planningEntityDifficulty[planning entity difficulty comparison], behave like ``DECREASING_DIFFICULTY``, else like ``NONE``. +* ``DESCENDING``: Evaluate the planning entities in descending order based on a given metric. +This usually increases pruning (and therefore improves scalability). +Requires the model +to support xref:using-timefold-solver/modeling-planning-problems.adoc#planningEntitySorting[planning entity sorting]. +* `DESCENDING_IF_AVAILABLE` (default): If the model supports xref:using-timefold-solver/modeling-planning-problems.adoc#planningEntitySorting[planning entity sorting], behave like ``DESCENDING``, else like ``NONE``. * ``NONE``: Initialize the planning entities in original order. The `valueSorterManner` options are: -* ``INCREASING_STRENGTH``: Evaluate the planning values in increasing strength. Requires the model to support xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueStrength[planning value strength comparison]. -* `INCREASING_STRENGTH_IF_AVAILABLE` (default): If the model supports xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueStrength[planning value strength comparison], behave like ``INCREASING_STRENGTH``, else like ``NONE``. -* ``DECREASING_STRENGTH``: Evaluate the planning values in decreasing strength. Requires the model to support xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueStrength[planning value strength comparison]. -* ``DECREASING_STRENGTH_IF_AVAILABLE``: If the model supports xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueStrength[planning value strength comparison], behave like ``DECREASING_STRENGTH``, else like ``NONE``. +* ``ASCENDING``: Evaluate the planning values in ascending order based on a given metric. +Requires the model +to support xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueSorting[planning value sorting]. +* `ASCENDING_IF_AVAILABLE` (default): If the model supports xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueSorting[planning value sorting], behave like ``ASCENDING``, else like ``NONE``. +* ``DESCENDING``: Evaluate the planning values in descending order based on a given metric. Requires the model to support xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueSorting[planning value sorting]. +* ``DESCENDING_IF_AVAILABLE``: If the model supports xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueSorting[planning value sorting], behave like ``DESCENDING``, else like ``NONE``. * ``NONE``: Try the planning values in original order. @@ -196,4 +200,4 @@ Use Construction Heuristics with Local Search instead: those can handle thousand ==== Throwing hardware at these scalability issues has no noticeable impact. Moore's law cannot win against the onslaught of a few more planning entities in the dataset. -==== \ No newline at end of file +==== diff --git a/docs/src/modules/ROOT/pages/optimization-algorithms/overview.adoc b/docs/src/modules/ROOT/pages/optimization-algorithms/overview.adoc index 18bcaf17bb..103e93bbdb 100644 --- a/docs/src/modules/ROOT/pages/optimization-algorithms/overview.adoc +++ b/docs/src/modules/ROOT/pages/optimization-algorithms/overview.adoc @@ -125,7 +125,8 @@ However, this basic procedure provides a good starting configuration that will p . Start with a quick configuration that involves little or no configuration and optimization code: See xref:optimization-algorithms/construction-heuristics.adoc#firstFit[First Fit]. -. Next, implement xref:using-timefold-solver/modeling-planning-problems.adoc#planningEntityDifficulty[planning entity difficulty] comparison +. Next, +implement xref:using-timefold-solver/modeling-planning-problems.adoc#planningEntitySorting[planning entity sorting] and turn it into xref:optimization-algorithms/construction-heuristics.adoc#firstFitDecreasing[First Fit Decreasing]. . Next, add Late Acceptance behind it: @@ -1538,25 +1539,25 @@ If you do explicitly configure the ``Selector``, it overwrites the default setti Some `Selector` types implement a `SorterManner` out of the box: * `EntitySelector` supports: -** ``DECREASING_DIFFICULTY``: Sorts the planning entities according to decreasing xref:using-timefold-solver/modeling-planning-problems.adoc#planningEntityDifficulty[planning entity difficulty]. Requires that planning entity difficulty is annotated on the domain model. +** ``DESCENDING``: Sorts the planning entities in descending order based on a give metric (xref:using-timefold-solver/modeling-planning-problems.adoc#planningEntitySorting[planning entity sorting]). Requires that planning entity is annotated on the domain model. + [source,xml,options="nowrap"] ---- PHASE SORTED - DECREASING_DIFFICULTY + DESCENDING ---- * `ValueSelector` supports: -** ``INCREASING_STRENGTH``: Sorts the planning values according to increasing xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueStrength[planning value strength]. Requires that planning value strength is annotated on the domain model. +** ``ASCENDING``: Sorts the planning values in ascending order based on a given metric (xref:using-timefold-solver/modeling-planning-problems.adoc#planningValueSorting[planning value sorting]). Requires that planning value is annotated on the domain model. + [source,xml,options="nowrap"] ---- PHASE SORTED - INCREASING_STRENGTH + ASCENDING ---- @@ -1568,7 +1569,7 @@ An easy way to sort a `Selector` is with a plain old ``Comparator``: [source,java,options="nowrap"] ---- -public class VisitDifficultyComparator implements Comparator { +public class VisitComparator implements Comparator { public int compare(Visit a, Visit b) { return new CompareToBuilder() @@ -1587,7 +1588,7 @@ You'll also need to configure it (unless it's annotated on the domain model and PHASE SORTED - ...VisitDifficultyComparator + ...VisitComparator DESCENDING ---- @@ -1599,16 +1600,16 @@ The solver may choose to reuse them in different contexts. ==== -[#sortedSelectionBySelectionSorterWeightFactory] -===== Sorted selection by `SelectionSorterWeightFactory` +[#sortedSelectionByComparatorFactory] +===== [[sortedSelectionBySelectionSorterWeightFactory]]Sorted selection by `ComparatorFactory` -If you need the entire solution to sort a ``Selector``, use a `SelectionSorterWeightFactory` instead: +If you need the entire solution to sort a ``Selector``, use a `ComparatorFactory` instead: [source,java,options="nowrap"] ---- -public interface SelectionSorterWeightFactory { +public interface ComparatorFactory { - Comparable createSorterWeight(Solution_ solution, T selection); + Comparator createComparator(Solution_ solution); } ---- @@ -1620,14 +1621,14 @@ You'll also need to configure it (unless it's annotated on the domain model and PHASE SORTED - ...MyDifficultyWeightFactory + ...MyComparatorFactory DESCENDING ---- [NOTE] ==== -`SelectionSorterWeightFactory` implementations are expected to be stateless. +`ComparatorFactory` implementations are expected to be stateless. The solver may choose to reuse them in different contexts. ==== @@ -2223,4 +2224,4 @@ add the `moveIteratorFactoryCustomProperties` element and use xref:using-timefol [WARNING] ==== A custom `MoveIteratorFactory` implementation must ensure that it does not move xref:responding-to-change/responding-to-change.adoc#pinnedPlanningEntities[pinned entities]. -==== \ No newline at end of file +==== diff --git a/docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-to-latest-version.adoc b/docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-to-latest-version.adoc index 587e864eb5..de79033a06 100644 --- a/docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-to-latest-version.adoc +++ b/docs/src/modules/ROOT/pages/upgrading-timefold-solver/upgrade-to-latest-version.adoc @@ -59,6 +59,17 @@ Every upgrade note indicates how likely your code will be affected by that chang The upgrade recipe often lists the changes as they apply to Java code. We kindly ask Kotlin users to translate the changes accordingly. +=== Upgrade from 1.27.0 to 1.28.0 + +.icon:info-circle[role=yellow] `SelectionSorterWeightFactory` deprecated for removal +[%collapsible%open] +==== +The `SelectionSorterWeightFactory` sorter class has been deprecated in favor of `ComparatorFactory`. +The sorting settings now utilize a `Comparator` class rather than a `Comparable` class, +ensuring all related settings are consistent and simpler. +The old class remains valid and can be easily converted to use the `ComparatorFactory` instead. +==== + === Upgrade from 1.22.0 to 1.23.0 .icon:info-circle[role=yellow] `@PlanningEntity` `pinningFilter` deprecated for removal diff --git a/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc b/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc index f35a380a66..c7f3004f81 100644 --- a/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc +++ b/docs/src/modules/ROOT/pages/using-timefold-solver/modeling-planning-problems.adoc @@ -372,27 +372,28 @@ As Timefold Solver will mutate the <> or < { +public class VisitComparator implements Comparator { public int compare(Visit a, Visit b) { return new CompareToBuilder() @@ -412,20 +413,20 @@ public class VisitDifficultyComparator implements Comparator { } ---- -Alternatively, you can also set a `difficultyWeightFactoryClass` to the `@PlanningEntity` annotation, +Alternatively, you can also set a `comparatorFactoryClass` to the `@PlanningEntity` annotation, so that you have access to the rest of the problem facts from the solution too. See xref:optimization-algorithms/overview.adoc#sortedSelection[sorted selection] for more information. [IMPORTANT] ==== -Difficulty should be implemented ascending: easy entities are lower, difficult entities are higher. +Entities should be sorted in ascending order: "easy" entities are lower, "difficult" entities are higher. For example, in bin packing: small item < medium item < big item. -Although most algorithms start with the more difficult entities first, they just reverse the ordering. +Although most algorithms start in descending order, they just reverse the ordering. ==== -_None of the current planning variable states should be used to compare planning entity difficulty._ +_None of the current planning variable states should be used to compare planning entities._ During Construction Heuristics, those variables are likely to be `null` anyway. For example, a ``Lesson``'s `timeslot` variable should not be used. @@ -663,62 +664,70 @@ Furthermore, it must be a mutable `Collection` because once Timefold Solver star it will add and remove elements to the ``Collection``s of those shadow variables accordingly. ==== -[#planningValueAndPlanningValueRange] -== Planning value and planning value range - -[#planningValue] -=== Planning value +[#planningListVariable] +== Planning list variable (VRP, Task assigning, ...) -A planning value is a possible value for a genuine planning variable. -Usually, a planning value is a problem fact, but it can also be any object, for example an ``Integer``. -It can even be another planning entity or even an interface implemented by both a planning entity and a problem fact. +Use the planning list variable to model problems where the goal is to distribute a number of workload elements among limited resources in a specific order. +This includes, for example, vehicle routing, traveling salesman, task assigning, and similar problems. [NOTE] ==== -Primitive types (such as ``int``) are not allowed. +Use a <> instead of a planning list variable, +if you need any of the following planning techniques: + +- xref:optimization-algorithms/exhaustive-search.adoc#exhaustiveSearch[exhaustive search], +- xref:enterprise-edition/enterprise-edition.adoc#partitionedSearch[partitioned search], +- coexistence with another list variable. ==== -A planning value range is the set of possible planning values for a planning variable. -Planning value ranges need to come from a finite collection. +For example, the vehicle routing problem can be modeled as follows: +image::quickstart/vehicle-routing/vehicleRoutingClassDiagramAnnotated.png[] -[#planningValueRangeProvider] -=== Planning value range provider +This model is closer to the reality than the chained model. +Each vehicle has a list of customers to go to in the order given by the list. +And indeed, the object model matches the natural language description of the problem: +[tabs] +==== +Java:: ++ +[source,java,options="nowrap"] +---- +@PlanningEntity +class Vehicle { -[#planningValueRangeProviderOverview] -==== Overview + int capacity; + Depot depot; -The value range of a planning variable is defined with the `@ValueRangeProvider` annotation. -A `@ValueRangeProvider` may carry a property ``id``, which is referenced by the ``@PlanningVariable``'s property ``valueRangeProviderRefs``. + @PlanningListVariable + List customers = new ArrayList<>(); +} +---- -This annotation can be located on two types of methods: -* On the Solution: All planning entities share the same value range. -* On the planning entity: The value range differs per planning entity. This is less common. +==== +Planning list variable can be used if the domain meets the following criteria: -[NOTE] -==== -A `@ValueRangeProvider` annotation needs to be on a member -in a class with a `@PlanningSolution` or a `@PlanningEntity` annotation. -It is ignored on parent classes or subclasses without those annotations. -==== +. There is a one-to-many relationship between the planning entity and the planning value. -The return type of that method can be three types: +. The order in which planning values are assigned to an entity's list variable is significant. -* ``Collection``: The value range is defined by a `Collection` (usually a ``List``) of its possible values. -* Array: The value range is defined by an array of its possible values. -* ``CountableValueRange``: The value range is defined by its bounds. This is less common. +. Each planning value is assigned to exactly one planning entity. +No planning value may appear in multiple entities. -[#valueRangeProviderOnSolution] -==== `ValueRangeProvider` on the solution -All instances of the same planning entity class share the same set of possible planning values for that planning variable. -This is the most common way to configure a value range. +[#planningListVariableAllowingUnassigned] +=== Allowing unassigned values -The `@PlanningSolution` implementation has a method that returns a `Collection` (or a ``CountableValueRange``). -Any value from that `Collection` is a possible planning value for this planning variable. +By default, all planning values have to be assigned to exactly one list variable across the entire planning model. +In an xref:responding-to-change/responding-to-change.adoc#overconstrainedPlanning[over-constrained use case], +this can be counterproductive. +For example: in task assignment with too many tasks for the workforce, +we would rather leave low priority tasks unassigned instead of assigning them to an overloaded worker. + +To allow a planning value to be unassigned, set `allowsUnassignedValues` to ``true``: [tabs] ==== @@ -726,60 +735,76 @@ Java:: + [source,java,options="nowrap"] ---- -@PlanningVariable -public Timeslot getTimeslot() { - return timeslot; -} ----- -+ -[source,java,options="nowrap"] ----- -@PlanningSolution -public class Timetable { - ... - - @ValueRangeProvider - public List getTimeslots() { - return timeslots; - } - +@PlanningListVariable(allowsUnassignedValues = true) +public List getCustomers() { + return customers; } ---- ==== [IMPORTANT] ==== -That `Collection` (or ``CountableValueRange``) must not contain the value ``null``, -not even for a <>. +Constraint Streams filter out unassigned planning values by default. +Use xref:constraints-and-score/score-calculation.adoc#constraintStreamsForEach[forEachIncludingUnassigned()] to avoid such unwanted behaviour. +Using a planning list variable with unassigned values implies +that your score calculation is responsible for punishing (or even rewarding) these unassigned values. + +Failure to penalize unassigned values can cause a solution with *all* values unassigned to be the best solution. +See the xref:responding-to-change/responding-to-change.adoc#overconstrainedPlanningWithNullValues[overconstrained planning with `null` variable values] section in the docs for more infomation. ==== -xref:using-timefold-solver/configuration.adoc#annotationAlternatives[Annotating the field] instead of the property works too: +xref:responding-to-change/responding-to-change.adoc[Repeated planning] +(especially xref:responding-to-change/responding-to-change.adoc#realTimePlanning[real-time planning]) +does not mix well with a planning list variable that allows unassigned values. +Every time the Solver starts or a problem fact change is made, +the xref:optimization-algorithms/construction-heuristics.adoc#constructionHeuristics[Construction Heuristics] +will try to initialize all the `null` variables again, which can be a huge waste of time. +One way to deal with this is to filter the entity selector of the placer in the construction heuristic. -[tabs] -==== -Java:: -+ -[source,java,options="nowrap"] +[source,xml,options="nowrap"] ---- -@PlanningSolution -public class Timetable { + + ... + + + + ... + + + ... + + + ... + + ... + +---- - @ValueRangeProvider - private List timeslots; -} ----- +[#listVariableShadowVariables] +=== List variable shadow variables +When the planning entity uses a <>, +you can use the following built-in annotations to derive shadow variables from that genuine planning variable. -==== +- xref:listVariableShadowVariablesInverseRelation[@InverseRelationShadowVariable]: Used to get the planning entity containing the planning variable list to which a planning value is assigned; +- xref:listVariableShadowVariablesIndex[@IndexShadowVariable]: Used to get the index of a planning value's position it's assigned planning variable list; +- xref:listVariableShadowVariablesPreviousAndNext[@PreviousElementShadowVariable]: Used to get a planning value's predecessor in its assigned planning variable list; +- xref:listVariableShadowVariablesPreviousAndNext[@NextElementShadowVariable]: Used to get a planning value's successor in its assigned planning variable list; +- xref:tailChainVariable[@CascadingUpdateShadowVariable]: Used to update a set of connected elements; +If the built-in shadow variable annotations are insufficient, xref:customShadowVariable[@ShadowVariable] can be used to create custom handlers. -[#valueRangeProviderOnPlanningEntity] -==== `ValueRangeProvider` on the Planning Entity +[#listVariableShadowVariablesInverseRelation] +==== Inverse relation shadow variable -Each planning entity has its own value range (a set of possible planning values) for the planning variable. -For example, if a teacher can *never* teach in a room that does not belong to their department, lectures of that teacher can limit their room value range to the rooms of their department. +Use the `@InverseRelationShadowVariable` annotation to establish bi-directional relationship between the entity and the elements assigned to its list variable. +The type of the inverse shadow variable is the planning entity itself +because there is a one-to-many relationship between the entity and the element classes. + +The planning entity side has a genuine list variable: [tabs] ==== @@ -787,65 +812,55 @@ Java:: + [source,java,options="nowrap"] ---- - @PlanningVariable - public Room getRoom() { - return room; - } +@PlanningEntity +public class Vehicle { - @ValueRangeProvider - public List getPossibleRoomList() { - return getCourse().getTeacher().getDepartment().getRoomList(); + @PlanningListVariable + public List getCustomers() { + return customers; } + + public void setCustomers(List customers) {...} +} ---- -==== -Never use this to enforce a soft constraint (or even a hard constraint when the problem might not have a feasible solution). For example: __Unless there is no other way__, a teacher cannot teach in a room that does not belong to their department. -In this case, the teacher should _not_ be limited in their room value range (because sometimes there is no other way). -[NOTE] -==== -By limiting the value range specifically of one planning entity, you are effectively creating a __built-in hard constraint__. -This can have the benefit of severely lowering the number of possible solutions; however, it can also take away the freedom of the optimization algorithms to temporarily break that constraint in order to escape from a local optimum. ==== -A planning entity should _not_ use other planning entities to determine its value range. -That would only try to make the planning entity solve the planning problem itself and interfere with the optimization algorithms. - -Every entity has its own `List` instance, unless multiple entities have the same value range. -For example, if teacher A and B belong to the same department, they use the same `List` instance. -Furthermore, each `List` contains a subset of the same set of planning value instances. -For example, if department A and B can both use room X, then their `List` instances contain the same `Room` instance. +On the element side: -[NOTE] -==== -A `ValueRangeProvider` on the planning entity consumes more memory than `ValueRangeProvider` on the Solution and disables certain automatic performance optimizations. -==== +- Annotate the class with `@PlanningEntity` to make it a shadow planning entity. +- <>, otherwise Timefold Solver won't detect it and the shadow variable won't update. +- Create a property with the genuine planning entity type. +- Annotate it with `@InverseRelationShadowVariable` and set `sourceVariableName` to the name of the genuine planning list variable. -[WARNING] -==== -A `ValueRangeProvider` on the planning entity is not currently compatible with a <> variable. +[tabs] ==== +Java:: ++ +[source,java,options="nowrap"] +---- +@PlanningEntity +public class Customer { -[#referencingValueRangeProviders] -==== Referencing ``ValueRangeProvider``s + @InverseRelationShadowVariable(sourceVariableName = "customers") + public Vehicle getVehicle() { + return vehicle; + } -There are two ways how to match a planning variable to a value range provider. -The simplest way is to have value range provider auto-detected. -Another way is to explicitly reference the value range provider. + public void setVehicle(Vehicle vehicle) {...} +} +---- +==== -[#anonymousValueRangeProviders] -===== Anonymous ``ValueRangeProvider``s +[#listVariableShadowVariablesIndex] +==== Index shadow variable -We already described the first approach. -By not providing any `valueRangeProviderRefs` on the `@PlanningVariable` annotation, -Timefold Solver will go over every ``@ValueRangeProvider``-annotated method or field which does not have an ``id`` property set, -and will match planning variables with value ranges where their types match. +While the `@InverseRelationShadowVariable` allows to establish the bi-directional relationship between the entity +and the elements assigned to its list variable, +`@IndexShadowVariable` provides a pointer into the entity's list variable where the element is assigned. -In the following example, -the planning variable ``car`` will be matched to the value range returned by ``getCompanyCarList()``, -as they both use the ``Car`` type. -It will not match ``getPersonalCarList()``, -because that value range provider is not anonymous; it specifies an ``id``. +The planning entity side has a genuine list variable: [tabs] ==== @@ -853,31 +868,29 @@ Java:: + [source,java,options="nowrap"] ---- - @PlanningVariable - public Car getCar() { - return car; +@PlanningEntity +public class Vehicle { + + @PlanningListVariable + public List getCustomers() { + return customers; } - @ValueRangeProvider - public List getCompanyCarList() { - return companyCarList; - } - - @ValueRangeProvider(id = "personalCarRange") - public List getPersonalCarList() { - return personalCarList; - } + public void setCustomers(List customers) {...} +} ---- ==== -Automatic matching also accounts for polymorphism. -In the following example, -the planning variable ``car`` will be matched to ``getCompanyCarList()`` and ``getPersonalCarList()``, -as both ``CompanyCar`` and ``PersonalCar`` are ``Car``s. -It will not match ``getAirplanes()``, -as an ``Airplane`` is not a ``Car``. +On the element side: + +- Annotate the class with `@PlanningEntity` to make it a shadow planning entity. +- <>, +otherwise Timefold Solver won't detect it and the shadow variable won't update. +- Create a property which returns an `Integer`. +`Integer` is required instead of `int`, as the index may be `null` if the element is not yet assigned to the list variable. +- Annotate it with `@IndexShadowVariable` and set `sourceVariableName` to the name of the genuine planning list variable. [tabs] ==== @@ -885,36 +898,27 @@ Java:: + [source,java,options="nowrap"] ---- - @PlanningVariable - public Car getCar() { - return car; - } - - @ValueRangeProvider - public List getCompanyCarList() { - return companyCarList; - } +@PlanningEntity +public class Customer { - @ValueRangeProvider - public List getPersonalCarList() { - return personalCarList; + @IndexShadowVariable(sourceVariableName = "customers") + public Integer getIndexInVehicle() { + return indexInVehicle; } - @ValueRangeProvider - public List getAirplanes() { - return airplaneList; - } +} ---- ==== -[#explicitlyReferencingValueRangeProviders] -===== Explicitly referenced ``ValueRangeProvider``s +[#listVariableShadowVariablesPreviousAndNext] +==== Previous and next element shadow variable -In more complicated cases where auto-detection is not sufficient or where clarity is preferred over simplicity, -value range providers can also be referenced explicitly. +Use `@PreviousElementShadowVariable` or `@NextElementShadowVariable` to get a reference to an element that is assigned to the same entity's list variable one index lower (previous element) or one index higher (next element). -In the following example, -the ``car`` planning variable will only be matched to value range provided by methods ``getCompanyCarList()``. +NOTE: The previous and next element shadow variables may be `null` even in a fully initialized solution. +The first element's previous shadow variable is `null` and the last element's next shadow variable is `null`. + +The planning entity side has a genuine list variable: [tabs] ==== @@ -922,26 +926,22 @@ Java:: + [source,java,options="nowrap"] ---- - @PlanningVariable(valueRangeProviderRefs = {"companyCarRange"}) - public Car getCar() { - return car; - } +@PlanningEntity +public class Vehicle { - @ValueRangeProvider(id = "companyCarRange") - public List getCompanyCarList() { - return companyCarList; + @PlanningListVariable + public List getCustomers() { + return customers; } - @ValueRangeProvider(id = "personalCarRange") - public List getPersonalCarList() { - return personalCarList; - } + public void setCustomers(List customers) {...} +} ---- ==== -Explicitly referenced value range providers can also be combined, for example: +On the element side: [tabs] ==== @@ -949,28 +949,36 @@ Java:: + [source,java,options="nowrap"] ---- - @PlanningVariable(valueRangeProviderRefs = { "companyCarRange", "personalCarRange" }) - public Car getCar() { - return car; - } +@PlanningEntity +public class Customer { - @ValueRangeProvider(id = "companyCarRange") - public List getCompanyCarList() { - return companyCarList; + @PreviousElementShadowVariable(sourceVariableName = "customers") + public Customer getPreviousCustomer() { + return previousCustomer; } - @ValueRangeProvider(id = "personalCarRange") - public List getPersonalCarList() { - return personalCarList; + public void setPreviousCustomer(Customer previousCustomer) {...} + + @NextElementShadowVariable(sourceVariableName = "customers") + public Customer getNextCustomer() { + return nextCustomer; } + + public void setNextCustomer(Customer nextCustomer) {...} ---- ==== +[#tailChainVariable] +=== Updating tail chains -[#valueRangeFactory] -==== `ValueRangeFactory` +The annotation `@CascadingUpdateShadowVariable` enables updates a set of connected elements. +Timefold Solver triggers a user-defined logic after all events are processed. +Hence, the related listener is the final one executed during the event lifecycle. +Moreover, +it automatically propagates changes to the subsequent elements in the list +when the value of the related shadow variable changes. -Instead of a ``Collection``, you can also return ``CountableValueRange``, built by the ``ValueRangeFactory``: +The planning entity side has a genuine list variable: [tabs] ==== @@ -978,18 +986,22 @@ Java:: + [source,java,options="nowrap"] ---- - @ValueRangeProvider - public CountableValueRange getDelayRange() { - return ValueRangeFactory.createIntValueRange(0, 5000); +@PlanningEntity +public class Vehicle { + + @PlanningListVariable + public List getCustomers() { + return customers; } + + public void setCustomers(List customers) {...} +} ---- ==== -A `CountableValueRange` uses far less memory, because it only holds the bounds. -In the example above, a `Collection` would need to hold all `5000` ints, instead of just the two bounds. -Furthermore, an `incrementUnit` can be specified, for example if you have to buy stocks in units of 200 pieces: +On the element side: [tabs] ==== @@ -997,153 +1009,133 @@ Java:: + [source,java,options="nowrap"] ---- - @ValueRangeProvider - public CountableValueRange getStockAmountRange() { - // Range: 0, 200, 400, 600, ..., 9999600, 9999800, 10000000 - return ValueRangeFactory.createIntValueRange(0, 10000000, 200); - } ----- -==== +@PlanningEntity +public class Customer { -The `ValueRangeFactory` has creation methods for several value class types: + @InverseRelationShadowVariable(sourceVariableName = "customers") + private Vehicle vehicle; + @PreviousElementShadowVariable(sourceVariableName = "customers") + private Customer previousCustomer; + @CascadingUpdateShadowVariable(targetMethodName = "updateArrivalTime") + private LocalDateTime arrivalTime; -* ``boolean``: A boolean range. -* ``int``: A 32bit integer range. -* ``long``: A 64bit integer range. -* ``BigInteger``: An arbitrary-precision integer range. -* ``BigDecimal``: A decimal point range. By default, the increment unit is the lowest non-zero value in the scale of the bounds. -* `Temporal` (such as ``LocalDate``, ``LocalDateTime``, ...): A time range. + ... + public void updateArrivalTime() {...} +---- +==== -[#planningValueStrength] -=== Planning value strength +The `targetMethodName` refers to the user-defined logic that updates the annotated shadow variable. +The method must be implemented in the defining entity class, be non-static, and not include any parameters. -Some optimization algorithms work a bit more efficiently if they have an estimation of which planning values are stronger, which means they are more likely to satisfy a planning entity. -For example: in bin packing bigger containers are more likely to fit an item. -Usually, the efficiency gain of planning value strength is far less than that of <>. +In the previous example, +the cascade update listener calls `updateArrivalTime` after all shadow variables have been updated, +including `vehicle` and `previousCustomer`. +It then automatically calls `updateArrivalTime` for the subsequent customers +and stops when the `arrivalTime` value does not change after running target method +or when it reaches the end. -[NOTE] +[WARNING] +==== +A user-defined logic can only change shadow variables. +Changing a genuine planning variable or a problem fact will result in score corruption. ==== -*Do not try to use planning value strength to implement a business constraint.* -It will not affect the score function: if we have infinite solving time, the returned solution will be the same. -To affect the score function, xref:constraints-and-score/overview.adoc#formalizeTheBusinessConstraints[add a score constraint]. -Only consider adding planning value strength too if it can make the solver more efficient. +[NOTE] +==== +When distinct target methods are used by separate `@CascadingUpdateShadowVariable` variables in the same model, +the order of their execution is undefined. ==== -To allow the heuristics to take advantage of that domain specific information, -set a `strengthComparatorClass` to the `@PlanningVariable` annotation: +==== Multiple sources -[source,java,options="nowrap"] ----- - @PlanningVariable(..., strengthComparatorClass = VehicleStrengthComparator.class) - public Vehicle getVehicle() { - return vehicle; - } ----- +If the user-defined logic requires updating multiple shadow variables, +apply the `@CascadingUpdateShadowVariable` to all shadow variables. +[tabs] +==== +Java:: ++ [source,java,options="nowrap"] ---- -public class VehicleStrengthComparator implements Comparator { +@PlanningEntity +public class Customer { - public int compare(Vehicle a, Vehicle b) { - return new CompareToBuilder() - .append(a.getCapacity(), b.getCapacity()) - .append(a.getId(), b.getId()) - .toComparison(); - } + @PreviousElementShadowVariable(sourceVariableName = "customers") + private Customer previousCustomer; + @NextElementShadowVariable(sourceVariableName = "customers") + private Customer nextCustomer; + @CascadingUpdateShadowVariable(targetMethodName = "updateWeightAndArrivalTime") + private LocalDateTime arrivalTime; + @CascadingUpdateShadowVariable(targetMethodName = "updateWeightAndArrivalTime") + private Integer weightAtVisit; + ... -} + public void updateWeightAndArrivalTime() {...} ---- -[NOTE] -==== -If you have multiple planning value classes in the _same_ value range, -the `strengthComparatorClass` needs to implement a `Comparator` of a common superclass (for example ``Comparator``) -and be able to handle comparing instances of those different classes. -==== - -Alternatively, you can also set a `strengthWeightFactoryClass` to the `@PlanningVariable` annotation, -so you have access to the rest of the problem facts from the solution too. - -See xref:optimization-algorithms/overview.adoc#sortedSelection[sorted selection] for more information. -[IMPORTANT] -==== -Strength should be implemented ascending: weaker values are lower, stronger values are higher. -In bin packing, small container < medium container < big container. ==== -_None of the current planning variable state in any of the planning entities should be used to compare planning values._ -During construction heuristics, those variables are likely to be ``null``. -For example, none of the `timeslot` variables of any `Lesson` may be used to determine the strength of a ``Timeslot``. +Timefold Solver triggers the user-defined logic in `updateWeightAndArrivalTime` at the end of the event lifecycle. +It stops when both `arrivalTime` and `weightAtVisit` values do not change or when it reaches the end. +[#planningValueAndPlanningValueRange] +== Planning value and planning value range -[#planningListVariable] -== Planning list variable (VRP, Task assigning, ...) +[#planningValue] +=== Planning value -Use the planning list variable to model problems where the goal is to distribute a number of workload elements among limited resources in a specific order. -This includes, for example, vehicle routing, traveling salesman, task assigning, and similar problems. +A planning value is a possible value for a genuine planning variable. +Usually, a planning value is a problem fact, but it can also be any object, for example an ``Integer``. +It can even be another planning entity or even an interface implemented by both a planning entity and a problem fact. [NOTE] ==== -Use a <> instead of a planning list variable, -if you need any of the following planning techniques: - -- <> or <>, -- xref:optimization-algorithms/exhaustive-search.adoc#exhaustiveSearch[exhaustive search], -- xref:enterprise-edition/enterprise-edition.adoc#partitionedSearch[partitioned search], -- coexistence with another list variable. +Primitive types (such as ``int``) are not allowed. ==== -For example, the vehicle routing problem can be modeled as follows: - -image::quickstart/vehicle-routing/vehicleRoutingClassDiagramAnnotated.png[] +A planning value range is the set of possible planning values for a planning variable. +Planning value ranges need to come from a finite collection. -This model is closer to the reality than the chained model. -Each vehicle has a list of customers to go to in the order given by the list. -And indeed, the object model matches the natural language description of the problem: -[tabs] -==== -Java:: -+ -[source,java,options="nowrap"] ----- -@PlanningEntity -class Vehicle { +[#planningValueRangeProvider] +=== Planning value range provider - int capacity; - Depot depot; - @PlanningListVariable - List customers = new ArrayList<>(); -} ----- +[#planningValueRangeProviderOverview] +==== Overview +The value range of a planning variable is defined with the `@ValueRangeProvider` annotation. +A `@ValueRangeProvider` may carry a property ``id``, which is referenced by the ``@PlanningVariable``'s property ``valueRangeProviderRefs``. -==== +This annotation can be located on two types of methods: -Planning list variable can be used if the domain meets the following criteria: +* On the Solution: All planning entities share the same value range. +* On the planning entity: The value range differs per planning entity. This is less common. -. There is a one-to-many relationship between the planning entity and the planning value. -. The order in which planning values are assigned to an entity's list variable is significant. +[NOTE] +==== +A `@ValueRangeProvider` annotation needs to be on a member +in a class with a `@PlanningSolution` or a `@PlanningEntity` annotation. +It is ignored on parent classes or subclasses without those annotations. +==== -. Each planning value is assigned to exactly one planning entity. -No planning value may appear in multiple entities. +The return type of that method can be three types: +* ``Collection``: The value range is defined by a `Collection` (usually a ``List``) of its possible values. +* Array: The value range is defined by an array of its possible values. +* ``CountableValueRange``: The value range is defined by its bounds. This is less common. -[#planningListVariableAllowingUnassigned] -=== Allowing unassigned values +[#valueRangeProviderOnSolution] +==== `ValueRangeProvider` on the solution -By default, all planning values have to be assigned to exactly one list variable across the entire planning model. -In an xref:responding-to-change/responding-to-change.adoc#overconstrainedPlanning[over-constrained use case], -this can be counterproductive. -For example: in task assignment with too many tasks for the workforce, -we would rather leave low priority tasks unassigned instead of assigning them to an overloaded worker. +All instances of the same planning entity class share the same set of possible planning values for that planning variable. +This is the most common way to configure a value range. -To allow a planning value to be unassigned, set `allowsUnassignedValues` to ``true``: +The `@PlanningSolution` implementation has a method that returns a `Collection` (or a ``CountableValueRange``). +Any value from that `Collection` is a possible planning value for this planning variable. [tabs] ==== @@ -1151,76 +1143,60 @@ Java:: + [source,java,options="nowrap"] ---- -@PlanningListVariable(allowsUnassignedValues = true) -public List getCustomers() { - return customers; +@PlanningVariable +public Timeslot getTimeslot() { + return timeslot; +} +---- ++ +[source,java,options="nowrap"] +---- +@PlanningSolution +public class Timetable { + ... + + @ValueRangeProvider + public List getTimeslots() { + return timeslots; + } + } ---- ==== [IMPORTANT] ==== -Constraint Streams filter out unassigned planning values by default. -Use xref:constraints-and-score/score-calculation.adoc#constraintStreamsForEach[forEachIncludingUnassigned()] to avoid such unwanted behaviour. -Using a planning list variable with unassigned values implies -that your score calculation is responsible for punishing (or even rewarding) these unassigned values. - -Failure to penalize unassigned values can cause a solution with *all* values unassigned to be the best solution. -See the xref:responding-to-change/responding-to-change.adoc#overconstrainedPlanningWithNullValues[overconstrained planning with `null` variable values] section in the docs for more infomation. +That `Collection` (or ``CountableValueRange``) must not contain the value ``null``, +not even for a <>. ==== -xref:responding-to-change/responding-to-change.adoc[Repeated planning] -(especially xref:responding-to-change/responding-to-change.adoc#realTimePlanning[real-time planning]) -does not mix well with a planning list variable that allows unassigned values. -Every time the Solver starts or a problem fact change is made, -the xref:optimization-algorithms/construction-heuristics.adoc#constructionHeuristics[Construction Heuristics] -will try to initialize all the `null` variables again, which can be a huge waste of time. -One way to deal with this is to filter the entity selector of the placer in the construction heuristic. +xref:using-timefold-solver/configuration.adoc#annotationAlternatives[Annotating the field] instead of the property works too: -[source,xml,options="nowrap"] +[tabs] +==== +Java:: ++ +[source,java,options="nowrap"] ---- - - ... - - - - ... - - - ... - - - +@PlanningSolution +public class Timetable { ... - - ... - ----- - -[#listVariableShadowVariables] -=== List variable shadow variables + @ValueRangeProvider + private List timeslots; -When the planning entity uses a <>, -you can use the following built-in annotations to derive shadow variables from that genuine planning variable. +} +---- -- xref:listVariableShadowVariablesInverseRelation[@InverseRelationShadowVariable]: Used to get the planning entity containing the planning variable list to which a planning value is assigned; -- xref:listVariableShadowVariablesIndex[@IndexShadowVariable]: Used to get the index of a planning value's position it's assigned planning variable list; -- xref:listVariableShadowVariablesPreviousAndNext[@PreviousElementShadowVariable]: Used to get a planning value's predecessor in its assigned planning variable list; -- xref:listVariableShadowVariablesPreviousAndNext[@NextElementShadowVariable]: Used to get a planning value's successor in its assigned planning variable list; -- xref:tailChainVariable[@CascadingUpdateShadowVariable]: Used to update a set of connected elements; -If the built-in shadow variable annotations are insufficient, xref:customShadowVariable[@ShadowVariable] can be used to create custom handlers. +==== -[#listVariableShadowVariablesInverseRelation] -==== Inverse relation shadow variable -Use the `@InverseRelationShadowVariable` annotation to establish bi-directional relationship between the entity and the elements assigned to its list variable. -The type of the inverse shadow variable is the planning entity itself -because there is a one-to-many relationship between the entity and the element classes. +[#valueRangeProviderOnPlanningEntity] +==== `ValueRangeProvider` on the Planning Entity -The planning entity side has a genuine list variable: +Each planning entity has its own value range (a set of possible planning values) for the planning variable. +For example, if a teacher can *never* teach in a room that does not belong to their department, lectures of that teacher can limit their room value range to the rooms of their department. [tabs] ==== @@ -1228,55 +1204,65 @@ Java:: + [source,java,options="nowrap"] ---- -@PlanningEntity -public class Vehicle { - - @PlanningListVariable - public List getCustomers() { - return customers; + @PlanningVariable + public Room getRoom() { + return room; } - public void setCustomers(List customers) {...} -} + @ValueRangeProvider + public List getPossibleRoomList() { + return getCourse().getTeacher().getDepartment().getRoomList(); + } ---- +==== +Never use this to enforce a soft constraint (or even a hard constraint when the problem might not have a feasible solution). For example: __Unless there is no other way__, a teacher cannot teach in a room that does not belong to their department. +In this case, the teacher should _not_ be limited in their room value range (because sometimes there is no other way). +[NOTE] +==== +By limiting the value range specifically of one planning entity, you are effectively creating a __built-in hard constraint__. +This can have the benefit of severely lowering the number of possible solutions; however, it can also take away the freedom of the optimization algorithms to temporarily break that constraint in order to escape from a local optimum. ==== -On the element side: +A planning entity should _not_ use other planning entities to determine its value range. +That would only try to make the planning entity solve the planning problem itself and interfere with the optimization algorithms. -- Annotate the class with `@PlanningEntity` to make it a shadow planning entity. -- <>, otherwise Timefold Solver won't detect it and the shadow variable won't update. -- Create a property with the genuine planning entity type. -- Annotate it with `@InverseRelationShadowVariable` and set `sourceVariableName` to the name of the genuine planning list variable. +Every entity has its own `List` instance, unless multiple entities have the same value range. +For example, if teacher A and B belong to the same department, they use the same `List` instance. +Furthermore, each `List` contains a subset of the same set of planning value instances. +For example, if department A and B can both use room X, then their `List` instances contain the same `Room` instance. -[tabs] +[NOTE] +==== +A `ValueRangeProvider` on the planning entity consumes more memory than `ValueRangeProvider` on the Solution and disables certain automatic performance optimizations. ==== -Java:: -+ -[source,java,options="nowrap"] ----- -@PlanningEntity -public class Customer { - - @InverseRelationShadowVariable(sourceVariableName = "customers") - public Vehicle getVehicle() { - return vehicle; - } - public void setVehicle(Vehicle vehicle) {...} -} ----- +[WARNING] +==== +A `ValueRangeProvider` on the planning entity is not currently compatible with a <> variable. ==== -[#listVariableShadowVariablesIndex] -==== Index shadow variable +[#referencingValueRangeProviders] +==== Referencing ``ValueRangeProvider``s + +There are two ways how to match a planning variable to a value range provider. +The simplest way is to have value range provider auto-detected. +Another way is to explicitly reference the value range provider. + +[#anonymousValueRangeProviders] +===== Anonymous ``ValueRangeProvider``s -While the `@InverseRelationShadowVariable` allows to establish the bi-directional relationship between the entity -and the elements assigned to its list variable, -`@IndexShadowVariable` provides a pointer into the entity's list variable where the element is assigned. +We already described the first approach. +By not providing any `valueRangeProviderRefs` on the `@PlanningVariable` annotation, +Timefold Solver will go over every ``@ValueRangeProvider``-annotated method or field which does not have an ``id`` property set, +and will match planning variables with value ranges where their types match. -The planning entity side has a genuine list variable: +In the following example, +the planning variable ``car`` will be matched to the value range returned by ``getCompanyCarList()``, +as they both use the ``Car`` type. +It will not match ``getPersonalCarList()``, +because that value range provider is not anonymous; it specifies an ``id``. [tabs] ==== @@ -1284,29 +1270,31 @@ Java:: + [source,java,options="nowrap"] ---- -@PlanningEntity -public class Vehicle { + @PlanningVariable + public Car getCar() { + return car; + } - @PlanningListVariable - public List getCustomers() { - return customers; + @ValueRangeProvider + public List getCompanyCarList() { + return companyCarList; } - public void setCustomers(List customers) {...} -} + @ValueRangeProvider(id = "personalCarRange") + public List getPersonalCarList() { + return personalCarList; + } ---- ==== -On the element side: - -- Annotate the class with `@PlanningEntity` to make it a shadow planning entity. -- <>, -otherwise Timefold Solver won't detect it and the shadow variable won't update. -- Create a property which returns an `Integer`. -`Integer` is required instead of `int`, as the index may be `null` if the element is not yet assigned to the list variable. -- Annotate it with `@IndexShadowVariable` and set `sourceVariableName` to the name of the genuine planning list variable. +Automatic matching also accounts for polymorphism. +In the following example, +the planning variable ``car`` will be matched to ``getCompanyCarList()`` and ``getPersonalCarList()``, +as both ``CompanyCar`` and ``PersonalCar`` are ``Car``s. +It will not match ``getAirplanes()``, +as an ``Airplane`` is not a ``Car``. [tabs] ==== @@ -1314,27 +1302,36 @@ Java:: + [source,java,options="nowrap"] ---- -@PlanningEntity -public class Customer { + @PlanningVariable + public Car getCar() { + return car; + } - @IndexShadowVariable(sourceVariableName = "customers") - public Integer getIndexInVehicle() { - return indexInVehicle; + @ValueRangeProvider + public List getCompanyCarList() { + return companyCarList; } -} + @ValueRangeProvider + public List getPersonalCarList() { + return personalCarList; + } + + @ValueRangeProvider + public List getAirplanes() { + return airplaneList; + } ---- ==== -[#listVariableShadowVariablesPreviousAndNext] -==== Previous and next element shadow variable - -Use `@PreviousElementShadowVariable` or `@NextElementShadowVariable` to get a reference to an element that is assigned to the same entity's list variable one index lower (previous element) or one index higher (next element). +[#explicitlyReferencingValueRangeProviders] +===== Explicitly referenced ``ValueRangeProvider``s -NOTE: The previous and next element shadow variables may be `null` even in a fully initialized solution. -The first element's previous shadow variable is `null` and the last element's next shadow variable is `null`. +In more complicated cases where auto-detection is not sufficient or where clarity is preferred over simplicity, +value range providers can also be referenced explicitly. -The planning entity side has a genuine list variable: +In the following example, +the ``car`` planning variable will only be matched to value range provided by methods ``getCompanyCarList()``. [tabs] ==== @@ -1342,22 +1339,26 @@ Java:: + [source,java,options="nowrap"] ---- -@PlanningEntity -public class Vehicle { + @PlanningVariable(valueRangeProviderRefs = {"companyCarRange"}) + public Car getCar() { + return car; + } - @PlanningListVariable - public List getCustomers() { - return customers; + @ValueRangeProvider(id = "companyCarRange") + public List getCompanyCarList() { + return companyCarList; } - public void setCustomers(List customers) {...} -} + @ValueRangeProvider(id = "personalCarRange") + public List getPersonalCarList() { + return personalCarList; + } ---- ==== -On the element side: +Explicitly referenced value range providers can also be combined, for example: [tabs] ==== @@ -1365,36 +1366,28 @@ Java:: + [source,java,options="nowrap"] ---- -@PlanningEntity -public class Customer { - - @PreviousElementShadowVariable(sourceVariableName = "customers") - public Customer getPreviousCustomer() { - return previousCustomer; + @PlanningVariable(valueRangeProviderRefs = { "companyCarRange", "personalCarRange" }) + public Car getCar() { + return car; } - public void setPreviousCustomer(Customer previousCustomer) {...} - - @NextElementShadowVariable(sourceVariableName = "customers") - public Customer getNextCustomer() { - return nextCustomer; + @ValueRangeProvider(id = "companyCarRange") + public List getCompanyCarList() { + return companyCarList; } - public void setNextCustomer(Customer nextCustomer) {...} + @ValueRangeProvider(id = "personalCarRange") + public List getPersonalCarList() { + return personalCarList; + } ---- ==== -[#tailChainVariable] -=== Updating tail chains -The annotation `@CascadingUpdateShadowVariable` enables updates a set of connected elements. -Timefold Solver triggers a user-defined logic after all events are processed. -Hence, the related listener is the final one executed during the event lifecycle. -Moreover, -it automatically propagates changes to the subsequent elements in the list -when the value of the related shadow variable changes. +[#valueRangeFactory] +==== `ValueRangeFactory` -The planning entity side has a genuine list variable: +Instead of a ``Collection``, you can also return ``CountableValueRange``, built by the ``ValueRangeFactory``: [tabs] ==== @@ -1402,22 +1395,18 @@ Java:: + [source,java,options="nowrap"] ---- -@PlanningEntity -public class Vehicle { - - @PlanningListVariable - public List getCustomers() { - return customers; + @ValueRangeProvider + public CountableValueRange getDelayRange() { + return ValueRangeFactory.createIntValueRange(0, 5000); } - - public void setCustomers(List customers) {...} -} ---- ==== +A `CountableValueRange` uses far less memory, because it only holds the bounds. +In the example above, a `Collection` would need to hold all `5000` ints, instead of just the two bounds. -On the element side: +Furthermore, an `incrementUnit` can be specified, for example if you have to buy stocks in units of 200 pieces: [tabs] ==== @@ -1425,76 +1414,112 @@ Java:: + [source,java,options="nowrap"] ---- -@PlanningEntity -public class Customer { + @ValueRangeProvider + public CountableValueRange getStockAmountRange() { + // Range: 0, 200, 400, 600, ..., 9999600, 9999800, 10000000 + return ValueRangeFactory.createIntValueRange(0, 10000000, 200); + } +---- +==== - @InverseRelationShadowVariable(sourceVariableName = "customers") - private Vehicle vehicle; - @PreviousElementShadowVariable(sourceVariableName = "customers") - private Customer previousCustomer; - @CascadingUpdateShadowVariable(targetMethodName = "updateArrivalTime") - private LocalDateTime arrivalTime; +The `ValueRangeFactory` has creation methods for several value class types: - ... +* ``boolean``: A boolean range. +* ``int``: A 32bit integer range. +* ``long``: A 64bit integer range. +* ``BigInteger``: An arbitrary-precision integer range. +* ``BigDecimal``: A decimal point range. By default, the increment unit is the lowest non-zero value in the scale of the bounds. +* `Temporal` (such as ``LocalDate``, ``LocalDateTime``, ...): A time range. - public void updateArrivalTime() {...} ----- -==== -The `targetMethodName` refers to the user-defined logic that updates the annotated shadow variable. -The method must be implemented in the defining entity class, be non-static, and not include any parameters. +[#planningValueSorting] +=== [[planningValueStrength]]Planning value sorting -In the previous example, -the cascade update listener calls `updateArrivalTime` after all shadow variables have been updated, -including `vehicle` and `previousCustomer`. -It then automatically calls `updateArrivalTime` for the subsequent customers -and stops when the `arrivalTime` value does not change after running target method -or when it reaches the end. +Some optimization algorithms work a bit more efficiently +if the planning values are sorted according to a given metric, +which means they are more likely to satisfy a planning entity requirement. +For example: in bin packing bigger containers are more likely to fit an item. -[WARNING] +[NOTE] ==== -A user-defined logic can only change shadow variables. -Changing a genuine planning variable or a problem fact will result in score corruption. +*Do not try to use planning value order to implement a business constraint.* +It will not affect the score function: if we have infinite solving time, the returned solution will be the same. + +To affect the score function, xref:constraints-and-score/overview.adoc#formalizeTheBusinessConstraints[add a score constraint]. +Only consider adding planning value sort order if it can make the solver more efficient. ==== +To allow the heuristics to take advantage of that domain specific information, +set a `comparatorClass` to the `@PlanningVariable` annotation: + +[source,java,options="nowrap"] +---- + @PlanningVariable(..., comparatorClass = VehicleComparator.class) + public Vehicle getVehicle() { + return vehicle; + } +---- + +[source,java,options="nowrap"] +---- +public class VehicleComparator implements Comparator { + + public int compare(Vehicle a, Vehicle b) { + return new CompareToBuilder() + .append(a.getCapacity(), b.getCapacity()) + .append(a.getId(), b.getId()) + .toComparison(); + } + +} +---- + [NOTE] ==== -When distinct target methods are used by separate `@CascadingUpdateShadowVariable` variables in the same model, -the order of their execution is undefined. +If you have multiple planning value classes in the _same_ value range, +the `comparatorClass` needs to implement a `Comparator` of a common superclass (for example ``Comparator``) +and be able to handle comparing instances of those different classes. ==== -==== Multiple sources +If the model uses a xref:using-timefold-solver/modeling-planning-problems#planningListVariable[list variable], +the setting process is similar, +and the property `comparatorClass` must be specified in the `@PlanningListVariable` annotation: -If the user-defined logic requires updating multiple shadow variables, -apply the `@CascadingUpdateShadowVariable` to all shadow variables. +[source,java,options="nowrap"] +---- + @PlanningListVariable(..., comparatorClass = CustomerComparator.class) + List customers = new ArrayList<>(); +---- -[tabs] -==== -Java:: -+ [source,java,options="nowrap"] ---- -@PlanningEntity -public class Customer { +public class CustomerComparator implements Comparator { - @PreviousElementShadowVariable(sourceVariableName = "customers") - private Customer previousCustomer; - @NextElementShadowVariable(sourceVariableName = "customers") - private Customer nextCustomer; - @CascadingUpdateShadowVariable(targetMethodName = "updateWeightAndArrivalTime") - private LocalDateTime arrivalTime; - @CascadingUpdateShadowVariable(targetMethodName = "updateWeightAndArrivalTime") - private Integer weightAtVisit; - ... + public int compare(Customer a, Customer b) { + return new CompareToBuilder() + .append(a.getPriority(), b.getPriority()) + .append(a.getId(), b.getId()) + .toComparison(); + } - public void updateWeightAndArrivalTime() {...} +} ---- +Alternatively, +you can also set a `comparatorFactoryClass` to the `@PlanningVariable` or `@PlanningListVariable` annotations, +so you have access to the rest of the problem facts from the solution too. + +See xref:optimization-algorithms/overview.adoc#sortedSelection[sorted selection] for more information. +[IMPORTANT] +==== +Values should be sorted in ascending order: "weaker" values are lower, and "stronger" values are higher. +In bin packing, small container < medium container < big container. ==== -Timefold Solver triggers the user-defined logic in `updateWeightAndArrivalTime` at the end of the event lifecycle. -It stops when both `arrivalTime` and `weightAtVisit` values do not change or when it reaches the end. +_None of the current planning variable state in any of the planning entities should be used to compare planning values._ +During construction heuristics, those variables are likely to be ``null``. +For example, none of the `timeslot` variables of any `Lesson` may be used to determine the order of a ``Timeslot``. [#chainedPlanningVariable] == Chained planning variable (TSP, VRP, ...) diff --git a/migration/src/main/java/ai/timefold/solver/migration/v8/SortingMigrationRecipe.java b/migration/src/main/java/ai/timefold/solver/migration/v8/SortingMigrationRecipe.java new file mode 100644 index 0000000000..d697e8aacd --- /dev/null +++ b/migration/src/main/java/ai/timefold/solver/migration/v8/SortingMigrationRecipe.java @@ -0,0 +1,123 @@ +package ai.timefold.solver.migration.v8; + +import java.util.List; + +import ai.timefold.solver.migration.AbstractRecipe; + +import org.openrewrite.Recipe; +import org.openrewrite.java.ChangeAnnotationAttributeName; +import org.openrewrite.java.ChangeMethodName; +import org.openrewrite.java.ChangeType; +import org.openrewrite.java.ReplaceConstantWithAnotherConstant; + +public class SortingMigrationRecipe extends AbstractRecipe { + @Override + public String getDisplayName() { + return "Use non-deprecated related sorting fields and methods"; + } + + @Override + public String getDescription() { + return "Use non-deprecated related sorting fields and methods."; + } + + @Override + public List getRecipeList() { + return List.of( + // Update PlanningVariable sorting fields + new ChangeAnnotationAttributeName("ai.timefold.solver.core.api.domain.variable.PlanningVariable", + "strengthComparatorClass", "comparatorClass"), + new ChangeAnnotationAttributeName("ai.timefold.solver.core.api.domain.variable.PlanningVariable", + "strengthWeightFactoryClass", "comparatorFactoryClass"), + new ChangeType("ai.timefold.solver.core.api.domain.variable.PlanningVariable.NullStrengthComparator", + "ai.timefold.solver.core.api.domain.variable.PlanningVariable.NullComparator", true), + new ChangeType("ai.timefold.solver.core.api.domain.variable.PlanningVariable.NullStrengthWeightFactory", + "ai.timefold.solver.core.api.domain.variable.PlanningVariable.NullComparatorFactory", true), + // Update PlanningEntity sorting fields + new ChangeAnnotationAttributeName("ai.timefold.solver.core.api.domain.entity.PlanningEntity", + "difficultyComparatorClass", "comparatorClass"), + new ChangeAnnotationAttributeName("ai.timefold.solver.core.api.domain.entity.PlanningEntity", + "difficultyWeightFactoryClass", "comparatorFactoryClass"), + new ChangeType("ai.timefold.solver.core.api.domain.entity.PlanningEntity.NullDifficultyComparator", + "ai.timefold.solver.core.api.domain.entity.PlanningEntity.NullComparator", true), + new ChangeType("ai.timefold.solver.core.api.domain.entity.PlanningEntity.NullDifficultyWeightFactory", + "ai.timefold.solver.core.api.domain.entity.PlanningEntity.NullComparatorFactory", true), + // Update MoveSelectorConfig sorting methods + new ChangeMethodName( + "ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig getSorterComparatorClass(..)", + "getComparatorClass", true, null), + new ChangeMethodName( + "ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig setSorterComparatorClass(..)", + "setComparatorClass", true, null), + new ChangeMethodName( + "ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig withSorterComparatorClass(..)", + "withComparatorClass", true, null), + new ChangeMethodName( + "ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig getSorterWeightFactoryClass(..)", + "getComparatorFactoryClass", true, null), + new ChangeMethodName( + "ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig setSorterWeightFactoryClass(..)", + "setComparatorFactoryClass", true, null), + new ChangeMethodName( + "ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig withSorterWeightFactoryClass(..)", + "withComparatorFactoryClass", true, null), + // Update EntitySelectorConfig sorting methods + new ChangeMethodName( + "ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig getSorterComparatorClass(..)", + "getComparatorClass", true, null), + new ChangeMethodName( + "ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig setSorterComparatorClass(..)", + "setComparatorClass", true, null), + new ChangeMethodName( + "ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig withSorterComparatorClass(..)", + "withComparatorClass", true, null), + new ChangeMethodName( + "ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig getSorterWeightFactoryClass(..)", + "getComparatorFactoryClass", true, null), + new ChangeMethodName( + "ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig setSorterWeightFactoryClass(..)", + "setComparatorFactoryClass", true, null), + new ChangeMethodName( + "ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig withSorterWeightFactoryClass(..)", + "withComparatorFactoryClass", true, null), + // Update ValueSelectorConfig sorting methods + new ChangeMethodName( + "ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig getSorterComparatorClass(..)", + "getComparatorClass", true, null), + new ChangeMethodName( + "ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig setSorterComparatorClass(..)", + "setComparatorClass", true, null), + new ChangeMethodName( + "ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig withSorterComparatorClass(..)", + "withComparatorClass", true, null), + new ChangeMethodName( + "ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig getSorterWeightFactoryClass(..)", + "getComparatorFactoryClass", true, null), + new ChangeMethodName( + "ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig setSorterWeightFactoryClass(..)", + "setComparatorFactoryClass", true, null), + new ChangeMethodName( + "ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig withSorterWeightFactoryClass(..)", + "withComparatorFactoryClass", true, null), + // Update EntitySorterManner + new ReplaceConstantWithAnotherConstant( + "ai.timefold.solver.core.config.heuristic.selector.entity.EntitySorterManner.DECREASING_DIFFICULTY", + "ai.timefold.solver.core.config.heuristic.selector.entity.EntitySorterManner.DESCENDING"), + new ReplaceConstantWithAnotherConstant( + "ai.timefold.solver.core.config.heuristic.selector.entity.EntitySorterManner.DECREASING_DIFFICULTY_IF_AVAILABLE", + "ai.timefold.solver.core.config.heuristic.selector.entity.EntitySorterManner.DESCENDING_IF_AVAILABLE"), + // Update ValueSorterManner + new ReplaceConstantWithAnotherConstant( + "ai.timefold.solver.core.config.heuristic.selector.value.ValueSorterManner.DECREASING_STRENGTH", + "ai.timefold.solver.core.config.heuristic.selector.value.ValueSorterManner.DESCENDING"), + new ReplaceConstantWithAnotherConstant( + "ai.timefold.solver.core.config.heuristic.selector.value.ValueSorterManner.DECREASING_STRENGTH_IF_AVAILABLE", + "ai.timefold.solver.core.config.heuristic.selector.value.ValueSorterManner.DESCENDING_IF_AVAILABLE"), + new ReplaceConstantWithAnotherConstant( + "ai.timefold.solver.core.config.heuristic.selector.value.ValueSorterManner.INCREASING_STRENGTH", + "ai.timefold.solver.core.config.heuristic.selector.value.ValueSorterManner.ASCENDING"), + new ReplaceConstantWithAnotherConstant( + "ai.timefold.solver.core.config.heuristic.selector.value.ValueSorterManner.INCREASING_STRENGTH_IF_AVAILABLE", + "ai.timefold.solver.core.config.heuristic.selector.value.ValueSorterManner.ASCENDING_IF_AVAILABLE")); + } +} diff --git a/migration/src/main/resources/META-INF/rewrite/ToLatest.yml b/migration/src/main/resources/META-INF/rewrite/ToLatest.yml index f62aa8f3ef..8bb9c7de32 100644 --- a/migration/src/main/resources/META-INF/rewrite/ToLatest.yml +++ b/migration/src/main/resources/META-INF/rewrite/ToLatest.yml @@ -34,5 +34,6 @@ recipeList: - ai.timefold.solver.migration.v8.AsConstraintRecipe - ai.timefold.solver.migration.v8.RemoveConstraintPackageRecipe - ai.timefold.solver.migration.v8.SolutionManagerRecommendAssignmentRecipe + - ai.timefold.solver.migration.v8.SortingMigrationRecipe - org.openrewrite.java.RemoveUnusedImports - ai.timefold.solver.migration.ChangeVersion diff --git a/migration/src/test/java/ai/timefold/solver/migration/v8/SortingMigrationRecipeTest.java b/migration/src/test/java/ai/timefold/solver/migration/v8/SortingMigrationRecipeTest.java new file mode 100644 index 0000000000..86e097c457 --- /dev/null +++ b/migration/src/test/java/ai/timefold/solver/migration/v8/SortingMigrationRecipeTest.java @@ -0,0 +1,147 @@ +package ai.timefold.solver.migration.v8; + +import static org.openrewrite.java.Assertions.java; + +import ai.timefold.solver.migration.AbstractRecipe; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; +import org.openrewrite.test.RecipeSpec; +import org.openrewrite.test.RewriteTest; + +@Execution(ExecutionMode.CONCURRENT) +class SortingMigrationRecipeTest implements RewriteTest { + + @Override + public void defaults(RecipeSpec spec) { + spec.recipe(new SortingMigrationRecipe()) + .parser(AbstractRecipe.JAVA_PARSER); + } + + @Test + void migrate() { + runTest( + """ + changeMoveConfig.withSorterWeightFactoryClass(SelectionSorterWeightFactory.class); + changeMoveConfig.withSorterComparatorClass(Comparator.class); + changeMoveConfig.setSorterWeightFactoryClass(SelectionSorterWeightFactory.class); + changeMoveConfig.setSorterComparatorClass(Comparator.class); + changeMoveConfig.getSorterWeightFactoryClass(); + changeMoveConfig.getSorterComparatorClass(); + swapMoveConfig.withSorterWeightFactoryClass(SelectionSorterWeightFactory.class); + swapMoveConfig.withSorterComparatorClass(Comparator.class); + swapMoveConfig.setSorterWeightFactoryClass(SelectionSorterWeightFactory.class); + swapMoveConfig.setSorterComparatorClass(Comparator.class); + swapMoveConfig.getSorterWeightFactoryClass(); + swapMoveConfig.getSorterComparatorClass(); + entityConfig.withSorterWeightFactoryClass(SelectionSorterWeightFactory.class); + entityConfig.withSorterComparatorClass(Comparator.class); + entityConfig.setSorterWeightFactoryClass(SelectionSorterWeightFactory.class); + entityConfig.setSorterComparatorClass(Comparator.class); + entityConfig.getSorterWeightFactoryClass(); + entityConfig.getSorterComparatorClass(); + entityConfig.setSorterManner(EntitySorterManner.DECREASING_DIFFICULTY_IF_AVAILABLE); + entityConfig.setSorterManner(EntitySorterManner.DECREASING_DIFFICULTY); + valueConfig.withSorterWeightFactoryClass(SelectionSorterWeightFactory.class); + valueConfig.withSorterComparatorClass(Comparator.class); + valueConfig.setSorterWeightFactoryClass(SelectionSorterWeightFactory.class); + valueConfig.setSorterComparatorClass(Comparator.class); + valueConfig.getSorterWeightFactoryClass(); + valueConfig.getSorterComparatorClass(); + valueConfig.setSorterManner(ValueSorterManner.INCREASING_STRENGTH); + valueConfig.setSorterManner(ValueSorterManner.INCREASING_STRENGTH_IF_AVAILABLE); + valueConfig.setSorterManner(ValueSorterManner.DECREASING_STRENGTH); + valueConfig.setSorterManner(ValueSorterManner.DECREASING_STRENGTH_IF_AVAILABLE);""", + """ + changeMoveConfig.withComparatorFactoryClass(SelectionSorterWeightFactory.class); + changeMoveConfig.withComparatorClass(Comparator.class); + changeMoveConfig.setComparatorFactoryClass(SelectionSorterWeightFactory.class); + changeMoveConfig.setComparatorClass(Comparator.class); + changeMoveConfig.getComparatorFactoryClass(); + changeMoveConfig.getComparatorClass(); + swapMoveConfig.withComparatorFactoryClass(SelectionSorterWeightFactory.class); + swapMoveConfig.withComparatorClass(Comparator.class); + swapMoveConfig.setComparatorFactoryClass(SelectionSorterWeightFactory.class); + swapMoveConfig.setComparatorClass(Comparator.class); + swapMoveConfig.getComparatorFactoryClass(); + swapMoveConfig.getComparatorClass(); + entityConfig.withComparatorFactoryClass(SelectionSorterWeightFactory.class); + entityConfig.withComparatorClass(Comparator.class); + entityConfig.setComparatorFactoryClass(SelectionSorterWeightFactory.class); + entityConfig.setComparatorClass(Comparator.class); + entityConfig.getComparatorFactoryClass(); + entityConfig.getComparatorClass(); + entityConfig.setSorterManner(EntitySorterManner.DESCENDING_IF_AVAILABLE); + entityConfig.setSorterManner(EntitySorterManner.DESCENDING); + valueConfig.withComparatorFactoryClass(SelectionSorterWeightFactory.class); + valueConfig.withComparatorClass(Comparator.class); + valueConfig.setComparatorFactoryClass(SelectionSorterWeightFactory.class); + valueConfig.setComparatorClass(Comparator.class); + valueConfig.getComparatorFactoryClass(); + valueConfig.getComparatorClass(); + valueConfig.setSorterManner(ValueSorterManner.ASCENDING); + valueConfig.setSorterManner(ValueSorterManner.ASCENDING_IF_AVAILABLE); + valueConfig.setSorterManner(ValueSorterManner.DESCENDING); + valueConfig.setSorterManner(ValueSorterManner.DESCENDING_IF_AVAILABLE);"""); + } + + private void runTest(String contentBefore, String contentAfter) { + rewriteRun(java(adjustBefore(contentBefore), adjustAfter(contentAfter))); + } + + private static String adjustBefore(String content) { + return """ + import java.util.Comparator; + import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory; + import ai.timefold.solver.core.api.domain.entity.PlanningEntity; + import ai.timefold.solver.core.api.domain.variable.PlanningVariable; + import ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig; + import ai.timefold.solver.core.config.heuristic.selector.entity.EntitySorterManner; + import ai.timefold.solver.core.config.heuristic.selector.move.generic.ChangeMoveSelectorConfig; + import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListSwapMoveSelectorConfig; + import ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig; + import ai.timefold.solver.core.config.heuristic.selector.value.ValueSorterManner; + + @PlanningEntity(difficultyWeightFactoryClass = PlanningEntity.NullDifficultyWeightFactory.class, difficultyComparatorClass = PlanningEntity.NullDifficultyComparator.class) + public class Test { + @PlanningVariable(strengthComparatorClass = PlanningVariable.NullStrengthComparator.class) + private Object value; + @PlanningVariable(strengthWeightFactoryClass = PlanningVariable.NullStrengthWeightFactory.class) + private Object value2; + public void validate(ChangeMoveSelectorConfig changeMoveConfig, ListSwapMoveSelectorConfig swapMoveConfig, EntitySelectorConfig entityConfig, ValueSelectorConfig valueConfig) { + %8s%s + } + }""" + .formatted("", content); + } + + private static String adjustAfter(String content) { + return """ + import java.util.Comparator; + import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory; + import ai.timefold.solver.core.api.domain.entity.PlanningEntity; + import ai.timefold.solver.core.api.domain.variable.PlanningVariable; + import ai.timefold.solver.core.api.domain.variable.PlanningVariable.NullComparator; + import ai.timefold.solver.core.api.domain.variable.PlanningVariable.NullComparatorFactory; + import ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig; + import ai.timefold.solver.core.config.heuristic.selector.entity.EntitySorterManner; + import ai.timefold.solver.core.config.heuristic.selector.move.generic.ChangeMoveSelectorConfig; + import ai.timefold.solver.core.config.heuristic.selector.move.generic.list.ListSwapMoveSelectorConfig; + import ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig; + import ai.timefold.solver.core.config.heuristic.selector.value.ValueSorterManner; + + @PlanningEntity(comparatorFactoryClass = PlanningEntity.NullComparatorFactory.class, comparatorClass = PlanningEntity.NullComparator.class) + public class Test { + @PlanningVariable(comparatorClass = NullComparator.class) + private Object value; + @PlanningVariable(comparatorFactoryClass = NullComparatorFactory.class) + private Object value2; + public void validate(ChangeMoveSelectorConfig changeMoveConfig, ListSwapMoveSelectorConfig swapMoveConfig, EntitySelectorConfig entityConfig, ValueSelectorConfig valueConfig) { + %8s%s + } + }""" + .formatted("", content); + } + +} diff --git a/spring-integration/spring-boot-integration-test/src/test/resources/solver-full.xml b/spring-integration/spring-boot-integration-test/src/test/resources/solver-full.xml index 3c09f52e7b..de68fe4d71 100644 --- a/spring-integration/spring-boot-integration-test/src/test/resources/solver-full.xml +++ b/spring-integration/spring-boot-integration-test/src/test/resources/solver-full.xml @@ -103,8 +103,8 @@ java.lang.Object NONE - java.lang.Object - java.lang.Object + java.lang.Object + java.lang.Object DESCENDING java.lang.Object java.lang.Object @@ -121,8 +121,8 @@ java.lang.Object INCREASING_STRENGTH - java.lang.Object - java.lang.Object + java.lang.Object + java.lang.Object DESCENDING java.lang.Object java.lang.Object @@ -1147,4 +1147,4 @@ - \ No newline at end of file +