Skip to content

Commit 9eb30e8

Browse files
authored
feat: enable sorting for CH with list variable (#1833)
1 parent 5936b3d commit 9eb30e8

File tree

106 files changed

+6541
-1092
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

106 files changed

+6541
-1092
lines changed

benchmark/src/main/resources/benchmark.xsd

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -890,9 +890,15 @@
890890
<xs:element minOccurs="0" name="sorterComparatorClass" type="xs:string"/>
891891

892892

893+
<xs:element minOccurs="0" name="comparatorClass" type="xs:string"/>
894+
895+
893896
<xs:element minOccurs="0" name="sorterWeightFactoryClass" type="xs:string"/>
894897

895898

899+
<xs:element minOccurs="0" name="comparatorFactoryClass" type="xs:string"/>
900+
901+
896902
<xs:element minOccurs="0" name="sorterOrder" type="tns:selectionSorterOrder"/>
897903

898904

@@ -1082,9 +1088,15 @@
10821088
<xs:element minOccurs="0" name="sorterComparatorClass" type="xs:string"/>
10831089

10841090

1091+
<xs:element minOccurs="0" name="comparatorClass" type="xs:string"/>
1092+
1093+
10851094
<xs:element minOccurs="0" name="sorterWeightFactoryClass" type="xs:string"/>
10861095

10871096

1097+
<xs:element minOccurs="0" name="comparatorFactoryClass" type="xs:string"/>
1098+
1099+
10881100
<xs:element minOccurs="0" name="sorterOrder" type="tns:selectionSorterOrder"/>
10891101

10901102

@@ -1229,9 +1241,15 @@
12291241
<xs:element minOccurs="0" name="sorterComparatorClass" type="xs:string"/>
12301242

12311243

1244+
<xs:element minOccurs="0" name="comparatorClass" type="xs:string"/>
1245+
1246+
12321247
<xs:element minOccurs="0" name="sorterWeightFactoryClass" type="xs:string"/>
12331248

12341249

1250+
<xs:element minOccurs="0" name="comparatorFactoryClass" type="xs:string"/>
1251+
1252+
12351253
<xs:element minOccurs="0" name="sorterOrder" type="tns:selectionSorterOrder"/>
12361254

12371255

@@ -2874,6 +2892,12 @@
28742892

28752893

28762894
<xs:enumeration value="DECREASING_DIFFICULTY_IF_AVAILABLE"/>
2895+
2896+
2897+
<xs:enumeration value="DESCENDING"/>
2898+
2899+
2900+
<xs:enumeration value="DESCENDING_IF_AVAILABLE"/>
28772901

28782902

28792903
</xs:restriction>
@@ -2901,6 +2925,18 @@
29012925

29022926

29032927
<xs:enumeration value="DECREASING_STRENGTH_IF_AVAILABLE"/>
2928+
2929+
2930+
<xs:enumeration value="ASCENDING"/>
2931+
2932+
2933+
<xs:enumeration value="ASCENDING_IF_AVAILABLE"/>
2934+
2935+
2936+
<xs:enumeration value="DESCENDING"/>
2937+
2938+
2939+
<xs:enumeration value="DESCENDING_IF_AVAILABLE"/>
29042940

29052941

29062942
</xs:restriction>

core/src/build/revapi-differences.json

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,108 @@
348348
"oldValue": "{\"constructionHeuristicType\", \"entitySorterManner\", \"valueSorterManner\", \"entityPlacerConfig\", \"moveSelectorConfigList\", \"foragerConfig\"}",
349349
"newValue": "{\"constructionHeuristicType\", \"entitySorterManner\", \"valueSorterManner\", \"entityPlacerConfigList\", \"moveSelectorConfigList\", \"foragerConfig\"}",
350350
"justification": "New CH configuration with multiple placers"
351+
},
352+
{
353+
"ignore": true,
354+
"code": "java.method.returnTypeTypeParametersChanged",
355+
"old": "method java.lang.Class<? extends ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory> ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig::getSorterWeightFactoryClass()",
356+
"new": "method java.lang.Class<? extends ai.timefold.solver.core.api.domain.common.ComparatorFactory> ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig::getSorterWeightFactoryClass()",
357+
"justification": "New comparator factory class"
358+
},
359+
{
360+
"ignore": true,
361+
"code": "java.method.parameterTypeParameterChanged",
362+
"old": "parameter void ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig::setSorterWeightFactoryClass(===java.lang.Class<? extends ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory>===)",
363+
"new": "parameter void ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig::setSorterWeightFactoryClass(===java.lang.Class<? extends ai.timefold.solver.core.api.domain.common.ComparatorFactory>===)",
364+
"parameterIndex": "0",
365+
"justification": "New comparator factory class"
366+
},
367+
{
368+
"ignore": true,
369+
"code": "java.annotation.attributeValueChanged",
370+
"old": "class ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig",
371+
"new": "class ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig",
372+
"annotationType": "jakarta.xml.bind.annotation.XmlType",
373+
"attribute": "propOrder",
374+
"oldValue": "{\"id\", \"mimicSelectorRef\", \"entityClass\", \"cacheType\", \"selectionOrder\", \"nearbySelectionConfig\", \"filterClass\", \"sorterManner\", \"sorterComparatorClass\", \"sorterWeightFactoryClass\", \"sorterOrder\", \"sorterClass\", \"probabilityWeightFactoryClass\", \"selectedCountLimit\"}",
375+
"newValue": "{\"id\", \"mimicSelectorRef\", \"entityClass\", \"cacheType\", \"selectionOrder\", \"nearbySelectionConfig\", \"filterClass\", \"sorterManner\", \"sorterComparatorClass\", \"comparatorClass\", \"sorterWeightFactoryClass\", \"comparatorFactoryClass\", \"sorterOrder\", \"sorterClass\", \"probabilityWeightFactoryClass\", \"selectedCountLimit\"}",
376+
"justification": "New comparator properties"
377+
},
378+
{
379+
"ignore": true,
380+
"code": "java.method.returnTypeTypeParametersChanged",
381+
"old": "method java.lang.Class<? extends ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory> ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig<Config_ extends ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig<Config_>>::getSorterWeightFactoryClass()",
382+
"new": "method java.lang.Class<? extends ai.timefold.solver.core.api.domain.common.ComparatorFactory> ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig<Config_ extends ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig<Config_>>::getSorterWeightFactoryClass()",
383+
"justification": "New comparator factory class"
384+
},
385+
{
386+
"ignore": true,
387+
"code": "java.method.parameterTypeParameterChanged",
388+
"old": "parameter void ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig<Config_ extends ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig<Config_>>::setSorterWeightFactoryClass(===java.lang.Class<? extends ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory>===)",
389+
"new": "parameter void ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig<Config_ extends ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig<Config_>>::setSorterWeightFactoryClass(===java.lang.Class<? extends ai.timefold.solver.core.api.domain.common.ComparatorFactory>===)",
390+
"parameterIndex": "0",
391+
"justification": "New comparator factory class"
392+
},
393+
{
394+
"ignore": true,
395+
"code": "java.annotation.attributeValueChanged",
396+
"old": "class ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig<Config_ extends ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig<Config_>>",
397+
"new": "class ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig<Config_ extends ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig<Config_>>",
398+
"annotationType": "jakarta.xml.bind.annotation.XmlType",
399+
"attribute": "propOrder",
400+
"oldValue": "{\"cacheType\", \"selectionOrder\", \"filterClass\", \"sorterComparatorClass\", \"sorterWeightFactoryClass\", \"sorterOrder\", \"sorterClass\", \"probabilityWeightFactoryClass\", \"selectedCountLimit\", \"fixedProbabilityWeight\"}",
401+
"newValue": "{\"cacheType\", \"selectionOrder\", \"filterClass\", \"sorterComparatorClass\", \"comparatorClass\", \"sorterWeightFactoryClass\", \"comparatorFactoryClass\", \"sorterOrder\", \"sorterClass\", \"probabilityWeightFactoryClass\", \"selectedCountLimit\", \"fixedProbabilityWeight\"}",
402+
"justification": "New comparator properties"
403+
},
404+
{
405+
"ignore": true,
406+
"code": "java.method.returnTypeTypeParametersChanged",
407+
"old": "method java.lang.Class<? extends ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory> ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig::getSorterWeightFactoryClass()",
408+
"new": "method java.lang.Class<? extends ai.timefold.solver.core.api.domain.common.ComparatorFactory> ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig::getSorterWeightFactoryClass()",
409+
"justification": "New comparator factory class"
410+
},
411+
{
412+
"ignore": true,
413+
"code": "java.method.parameterTypeParameterChanged",
414+
"old": "parameter void ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig::setSorterWeightFactoryClass(===java.lang.Class<? extends ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory>===)",
415+
"new": "parameter void ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig::setSorterWeightFactoryClass(===java.lang.Class<? extends ai.timefold.solver.core.api.domain.common.ComparatorFactory>===)",
416+
"parameterIndex": "0",
417+
"justification": "New comparator factory class"
418+
},
419+
{
420+
"ignore": true,
421+
"code": "java.annotation.attributeValueChanged",
422+
"old": "class ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig",
423+
"new": "class ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig",
424+
"annotationType": "jakarta.xml.bind.annotation.XmlType",
425+
"attribute": "propOrder",
426+
"oldValue": "{\"id\", \"mimicSelectorRef\", \"downcastEntityClass\", \"variableName\", \"cacheType\", \"selectionOrder\", \"nearbySelectionConfig\", \"filterClass\", \"sorterManner\", \"sorterComparatorClass\", \"sorterWeightFactoryClass\", \"sorterOrder\", \"sorterClass\", \"probabilityWeightFactoryClass\", \"selectedCountLimit\"}",
427+
"newValue": "{\"id\", \"mimicSelectorRef\", \"downcastEntityClass\", \"variableName\", \"cacheType\", \"selectionOrder\", \"nearbySelectionConfig\", \"filterClass\", \"sorterManner\", \"sorterComparatorClass\", \"comparatorClass\", \"sorterWeightFactoryClass\", \"comparatorFactoryClass\", \"sorterOrder\", \"sorterClass\", \"probabilityWeightFactoryClass\", \"selectedCountLimit\"}",
428+
"justification": "New comparator properties"
429+
},
430+
{
431+
"ignore": true,
432+
"code": "java.annotation.added",
433+
"old": "class ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig",
434+
"new": "class ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig",
435+
"annotation": "@org.jspecify.annotations.NullMarked",
436+
"justification": "Update config"
437+
},
438+
{
439+
"ignore": true,
440+
"code": "java.annotation.added",
441+
"old": "class ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig<Config_ extends ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig<Config_>>",
442+
"new": "class ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig<Config_ extends ai.timefold.solver.core.config.heuristic.selector.move.MoveSelectorConfig<Config_>>",
443+
"annotation": "@org.jspecify.annotations.NullMarked",
444+
"justification": "Update config"
445+
},
446+
{
447+
"ignore": true,
448+
"code": "java.annotation.added",
449+
"old": "class ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig",
450+
"new": "class ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig",
451+
"annotation": "@org.jspecify.annotations.NullMarked",
452+
"justification": "Update config"
351453
}
352454
]
353455
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package ai.timefold.solver.core.api.domain.common;
2+
3+
import java.util.Comparator;
4+
5+
import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
6+
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
7+
import ai.timefold.solver.core.config.constructionheuristic.ConstructionHeuristicPhaseConfig;
8+
import ai.timefold.solver.core.config.heuristic.selector.entity.EntitySelectorConfig;
9+
import ai.timefold.solver.core.config.heuristic.selector.value.ValueSelectorConfig;
10+
import ai.timefold.solver.core.impl.heuristic.move.Move;
11+
import ai.timefold.solver.core.impl.heuristic.selector.Selector;
12+
13+
import org.jspecify.annotations.NullMarked;
14+
15+
/**
16+
* Creates a {@link Comparable} to decide the order of a collection of selections
17+
* (a selection is a {@link PlanningEntity}, a planningValue, a {@link Move} or a {@link Selector}).
18+
* The selections are then sorted by some specific metric,
19+
* normally ascending unless it's configured descending.
20+
* The property {@code sortManner},
21+
* present in the selector configurations such as {@link ValueSelectorConfig} and {@link EntitySelectorConfig},
22+
* specifies how the data will be sorted.
23+
* Additionally,
24+
* the property {@code constructionHeuristicType} from {@link ConstructionHeuristicPhaseConfig} can also configure how entities
25+
* and values are sorted.
26+
* <p>
27+
* Implementations are expected to be stateless.
28+
* The solver may choose to reuse instances.
29+
*
30+
* @param <Solution_> the solution type, the class with the {@link PlanningSolution} annotation
31+
* @param <T> the selection type
32+
*
33+
* @see ValueSelectorConfig
34+
* @see EntitySelectorConfig
35+
* @see ConstructionHeuristicPhaseConfig
36+
*/
37+
@NullMarked
38+
@FunctionalInterface
39+
public interface ComparatorFactory<Solution_, T> {
40+
41+
/**
42+
* @param solution never null, the {@link PlanningSolution} to which the selection belongs or applies to
43+
* @return never null
44+
*/
45+
Comparator<T> createComparator(Solution_ solution);
46+
47+
}

core/src/main/java/ai/timefold/solver/core/api/domain/entity/PlanningEntity.java

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import java.lang.annotation.Target;
88
import java.util.Comparator;
99

10+
import ai.timefold.solver.core.api.domain.common.ComparatorFactory;
1011
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
1112
import ai.timefold.solver.core.api.domain.variable.PlanningVariable;
1213
import ai.timefold.solver.core.impl.heuristic.selector.common.decorator.SelectionSorterWeightFactory;
@@ -34,6 +35,40 @@
3435
@Retention(RUNTIME)
3536
public @interface PlanningEntity {
3637

38+
/**
39+
* Allows sorting a collection of planning entities for this variable.
40+
* Some algorithms perform better when the entities are sorted based on specific metrics.
41+
* <p>
42+
* The {@link Comparator} should sort the data in ascending order.
43+
* For example, prioritize three vehicles by sorting them based on their capacity:
44+
* Vehicle C (4 people), Vehicle A (6 people), Vehicle B (32 people)
45+
* <p>
46+
* Do not use together with {@link #comparatorFactoryClass()}.
47+
*
48+
* @return {@link PlanningVariable.NullComparator} when it is null (workaround for annotation limitation)
49+
* @see #comparatorFactoryClass()
50+
*/
51+
Class<? extends Comparator> comparatorClass() default NullComparator.class;
52+
53+
interface NullComparator<T> extends Comparator<T> {
54+
}
55+
56+
/**
57+
* The {@link ComparatorFactory} alternative for {@link #comparatorClass()}.
58+
* <p>
59+
* Differs from {@link #comparatorClass()}
60+
* because it allows accessing the current solution when creating the comparator.
61+
* <p>
62+
* Do not use together with {@link #comparatorClass()}.
63+
*
64+
* @return {@link NullComparatorFactory} when it is null (workaround for annotation limitation)
65+
* @see #comparatorClass()
66+
*/
67+
Class<? extends ComparatorFactory> comparatorFactoryClass() default NullComparatorFactory.class;
68+
69+
interface NullComparatorFactory<Solution_, T> extends ComparatorFactory<Solution_, T> {
70+
}
71+
3772
/**
3873
* A pinned planning entity is never changed during planning,
3974
* this is useful in repeated planning use cases (such as continuous planning and real-time planning).
@@ -50,7 +85,7 @@
5085

5186
/**
5287
* Workaround for annotation limitation in {@link #pinningFilter()}.
53-
*
88+
*
5489
* @deprecated Prefer using {@link PlanningPin}.
5590
*/
5691
@Deprecated(forRemoval = true, since = "1.23.0")
@@ -69,27 +104,45 @@ interface NullPinningFilter extends PinningFilter {
69104
* <p>
70105
* Do not use together with {@link #difficultyWeightFactoryClass()}.
71106
*
107+
* @deprecated Deprecated in favor of {@link #comparatorClass()}.
108+
*
72109
* @return {@link NullDifficultyComparator} when it is null (workaround for annotation limitation)
73110
* @see #difficultyWeightFactoryClass()
74111
*/
112+
@Deprecated(forRemoval = true, since = "1.28.0")
75113
Class<? extends Comparator> difficultyComparatorClass() default NullDifficultyComparator.class;
76114

77-
/** Workaround for annotation limitation in {@link #difficultyComparatorClass()}. */
78-
interface NullDifficultyComparator extends Comparator {
115+
/**
116+
* Workaround for annotation limitation in {@link #difficultyComparatorClass()}.
117+
*
118+
* @deprecated Deprecated in favor of {@link NullComparator}.
119+
*/
120+
@Deprecated(forRemoval = true, since = "1.28.0")
121+
interface NullDifficultyComparator<T> extends NullComparator<T> {
79122
}
80123

81124
/**
82125
* The {@link SelectionSorterWeightFactory} alternative for {@link #difficultyComparatorClass()}.
83126
* <p>
84127
* Do not use together with {@link #difficultyComparatorClass()}.
85128
*
129+
* @deprecated Deprecated in favor of {@link #comparatorFactoryClass()}.
130+
*
86131
* @return {@link NullDifficultyWeightFactory} when it is null (workaround for annotation limitation)
87132
* @see #difficultyComparatorClass()
88133
*/
134+
@Deprecated(forRemoval = true, since = "1.28.0")
89135
Class<? extends SelectionSorterWeightFactory> difficultyWeightFactoryClass() default NullDifficultyWeightFactory.class;
90136

91-
/** Workaround for annotation limitation in {@link #difficultyWeightFactoryClass()}. */
92-
interface NullDifficultyWeightFactory extends SelectionSorterWeightFactory {
137+
/**
138+
* Workaround for annotation limitation in {@link #difficultyWeightFactoryClass()}.
139+
*
140+
* @deprecated Deprecated in favor of {@link NullComparatorFactory}.
141+
*/
142+
@Deprecated(forRemoval = true, since = "1.28.0")
143+
interface NullDifficultyWeightFactory<Solution_, T>
144+
extends SelectionSorterWeightFactory<Solution_, T>,
145+
NullComparatorFactory<Solution_, T> {
93146
}
94147

95148
}

0 commit comments

Comments
 (0)