Skip to content

Commit e6843d4

Browse files
committed
test: shadow variables
1 parent 21bfa9c commit e6843d4

10 files changed

Lines changed: 195 additions & 17 deletions

File tree

core/src/main/java/ai/timefold/solver/core/impl/heuristic/HeuristicConfigPolicy.java

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929
public class HeuristicConfigPolicy<Solution_> {
3030

31-
private final Set<PreviewFeature> previewFeatureList;
31+
private final Set<PreviewFeature> previewFeatureSet;
3232
private final EnvironmentMode environmentMode;
3333
private final String logIndentation;
3434
private final Integer moveThreadCount;
@@ -50,7 +50,7 @@ public class HeuristicConfigPolicy<Solution_> {
5050
private final Map<String, ValueMimicRecorder<Solution_>> valueMimicRecorderMap = new HashMap<>();
5151

5252
private HeuristicConfigPolicy(Builder<Solution_> builder) {
53-
this.previewFeatureList = builder.previewFeatureList;
53+
this.previewFeatureSet = builder.previewFeatureSet;
5454
this.environmentMode = builder.environmentMode;
5555
this.logIndentation = builder.logIndentation;
5656
this.moveThreadCount = builder.moveThreadCount;
@@ -134,7 +134,7 @@ public Random getRandom() {
134134

135135
public Builder<Solution_> cloneBuilder() {
136136
return new Builder<Solution_>()
137-
.withPreviewFeatureList(previewFeatureList)
137+
.withPreviewFeatureSet(previewFeatureSet)
138138
.withEnvironmentMode(environmentMode)
139139
.withMoveThreadCount(moveThreadCount)
140140
.withMoveThreadBufferSize(moveThreadBufferSize)
@@ -229,11 +229,12 @@ public ThreadFactory buildThreadFactory(ChildThreadType childThreadType) {
229229
}
230230

231231
public void ensurePreviewFeature(PreviewFeature previewFeature) {
232-
ensurePreviewFeature(previewFeature, previewFeatureList);
232+
ensurePreviewFeature(previewFeature, previewFeatureSet);
233233
}
234234

235-
public static void ensurePreviewFeature(PreviewFeature previewFeature, Collection<PreviewFeature> previewFeatureList) {
236-
if (previewFeatureList == null || !previewFeatureList.contains(previewFeature)) {
235+
public static void ensurePreviewFeature(PreviewFeature previewFeature,
236+
Collection<PreviewFeature> previewFeatureCollection) {
237+
if (previewFeatureCollection == null || !previewFeatureCollection.contains(previewFeature)) {
237238
throw new IllegalStateException("""
238239
The preview feature %s is not enabled.
239240
Maybe add %s to <enablePreviewFeature> in your configuration file?"""
@@ -248,7 +249,7 @@ public String toString() {
248249

249250
public static class Builder<Solution_> {
250251

251-
private Set<PreviewFeature> previewFeatureList;
252+
private Set<PreviewFeature> previewFeatureSet;
252253
private EnvironmentMode environmentMode;
253254
private Integer moveThreadCount;
254255
private Integer moveThreadBufferSize;
@@ -269,8 +270,8 @@ public static class Builder<Solution_> {
269270
private Class<? extends NearbyDistanceMeter<?, ?>> nearbyDistanceMeterClass;
270271
private Random random;
271272

272-
public Builder<Solution_> withPreviewFeatureList(Set<PreviewFeature> previewFeatureList) {
273-
this.previewFeatureList = previewFeatureList;
273+
public Builder<Solution_> withPreviewFeatureSet(Set<PreviewFeature> previewFeatureSet) {
274+
this.previewFeatureSet = previewFeatureSet;
274275
return this;
275276
}
276277

core/src/main/java/ai/timefold/solver/core/impl/solver/DefaultSolverFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ public <Score_ extends Score<Score_>> ScoreDirectorFactory<Solution_, Score_> ge
131131
var previewFeaturesEnabled = solverConfig.getEnablePreviewFeatureSet();
132132

133133
var configPolicy = new HeuristicConfigPolicy.Builder<Solution_>()
134-
.withPreviewFeatureList(previewFeaturesEnabled)
134+
.withPreviewFeatureSet(previewFeaturesEnabled)
135135
.withEnvironmentMode(environmentMode)
136136
.withMoveThreadCount(moveThreadCount)
137137
.withMoveThreadBufferSize(solverConfig.getMoveThreadBufferSize())

core/src/test/java/ai/timefold/solver/core/config/solver/SolverConfigTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ void withConstraintProviderClass() {
166166
}
167167

168168
@Test
169-
void withEnablePreviewFeatureList() {
169+
void withEnablePreviewFeatureSet() {
170170
var solverConfig = new SolverConfig();
171171
assertThat(solverConfig.getEnablePreviewFeatureSet()).isNull();
172172
solverConfig.withPreviewFeature(PreviewFeature.DIVERSIFIED_LATE_ACCEPTANCE);

core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/placer/entity/QueuedMultiplePlacerFactoryTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
import static ai.timefold.solver.core.config.heuristic.selector.value.ValueSorterManner.INCREASING_STRENGTH_IF_AVAILABLE;
55
import static ai.timefold.solver.core.config.score.trend.InitializingScoreTrendLevel.ANY;
66
import static ai.timefold.solver.core.config.solver.EnvironmentMode.PHASE_ASSERT;
7+
import static ai.timefold.solver.core.config.solver.PreviewFeature.DECLARATIVE_SHADOW_VARIABLES;
78
import static org.assertj.core.api.Assertions.assertThat;
89
import static org.mockito.Mockito.mock;
910
import static org.mockito.Mockito.when;
1011

1112
import java.util.List;
1213
import java.util.Random;
14+
import java.util.Set;
1315

1416
import ai.timefold.solver.core.config.constructionheuristic.placer.QueuedEntityPlacerConfig;
1517
import ai.timefold.solver.core.config.constructionheuristic.placer.QueuedValuePlacerConfig;
@@ -157,6 +159,7 @@ void testPinnedPlacersForConstructionHeuristic() {
157159
.withInitializedChainedValueFilterEnabled(true)
158160
.withUnassignedValuesAllowed(true)
159161
.withRandom(new Random(0))
162+
.withPreviewFeatureSet(Set.of(DECLARATIVE_SHADOW_VARIABLES))
160163
.build();
161164
var valueSelectorConfig = new ValueSelectorConfig("valueList")
162165
.withId("valueList");

core/src/test/java/ai/timefold/solver/core/impl/solver/DefaultSolverTest.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import static ai.timefold.solver.core.config.heuristic.selector.entity.EntitySorterManner.DECREASING_DIFFICULTY;
44
import static ai.timefold.solver.core.config.heuristic.selector.entity.EntitySorterManner.DECREASING_DIFFICULTY_IF_AVAILABLE;
5+
import static ai.timefold.solver.core.config.solver.PreviewFeature.DECLARATIVE_SHADOW_VARIABLES;
56
import static org.assertj.core.api.Assertions.assertThat;
67
import static org.assertj.core.api.Assertions.assertThatCode;
78
import static org.assertj.core.api.Assertions.fail;
@@ -1535,6 +1536,7 @@ void solveMoveConfigListVar(MoveSelectorConfig moveSelectionConfig) {
15351536
// Solver config
15361537
var solverConfig = PlannerTestUtils.buildSolverConfig(
15371538
TestdataListSolution.class, TestdataListEntity.class, TestdataListValue.class)
1539+
.withPreviewFeature(DECLARATIVE_SHADOW_VARIABLES)
15381540
.withPhases(new ConstructionHeuristicPhaseConfig(), localSearchConfig)
15391541
.withEasyScoreCalculatorClass(DummySimpleScoreEasyScoreCalculator.class);
15401542

@@ -1643,15 +1645,41 @@ void solveMixedModel() {
16431645
var solverConfig = PlannerTestUtils.buildSolverConfig(
16441646
TestdataMixedSolution.class, TestdataMixedEntity.class, TestdataMixedValue.class,
16451647
TestdataMixedOtherValue.class)
1648+
.withPreviewFeature(DECLARATIVE_SHADOW_VARIABLES)
16461649
.withPhases(new ConstructionHeuristicPhaseConfig(),
16471650
new LocalSearchPhaseConfig().withTerminationConfig(new TerminationConfig().withStepCountLimit(16)))
16481651
.withEasyScoreCalculatorClass(TestdataMixedEasyScoreCalculator.class);
16491652

16501653
var problem = TestdataMixedSolution.generateUninitializedSolution(2, 2, 2);
16511654
var solution = PlannerTestUtils.solve(solverConfig, problem);
1655+
1656+
// Check the solution
16521657
assertThat(solution.getEntityList().stream()
16531658
.filter(e -> e.getBasicValue() == null || e.getSecondBasicValue() == null || e.getValueList().isEmpty()))
16541659
.isEmpty();
1660+
1661+
// Check custom listener execution
1662+
assertThat(solution.getValueList().stream().allMatch(v -> v.getShadowVariableListenerValue().equals(v.getIndex())))
1663+
.isTrue();
1664+
1665+
// Check cascading shadow variable
1666+
assertThat(solution.getValueList().stream().allMatch(v -> v.getCascadingShadowVariableValue().equals(v.getIndex() + 1)))
1667+
.isTrue();
1668+
1669+
// Check declarative shadow variable from basic variable - genuine entity
1670+
assertThat(solution.getEntityList().stream()
1671+
.allMatch(v -> v.getDeclarativeShadowVariableValue().equals(v.getBasicValue().getStrength())))
1672+
.isTrue();
1673+
1674+
// Check declarative shadow variable from basic variable - shadow entity
1675+
assertThat(solution.getOtherValueList().stream()
1676+
.allMatch(v -> v.getDeclarativeShadowVariableValue().equals(v.getEntityList().size() + 2)))
1677+
.isTrue();
1678+
1679+
// Check declarative shadow variable from list variable - shadow entity
1680+
assertThat(
1681+
solution.getValueList().stream().allMatch(v -> v.getDeclarativeShadowVariableValue().equals(v.getIndex() + 2)))
1682+
.isTrue();
16551683
}
16561684

16571685
private static List<Pair<EntitySorterManner, ValueSorterManner>> getSortMannerList() {
@@ -1669,6 +1697,7 @@ void solveMixedModelWithSortManner(Pair<EntitySorterManner, ValueSorterManner> s
16691697
var solverConfig = PlannerTestUtils.buildSolverConfig(
16701698
TestdataMixedSolution.class, TestdataMixedEntity.class, TestdataMixedValue.class,
16711699
TestdataMixedOtherValue.class)
1700+
.withPreviewFeature(DECLARATIVE_SHADOW_VARIABLES)
16721701
.withPhases(new ConstructionHeuristicPhaseConfig(),
16731702
new LocalSearchPhaseConfig()
16741703
.withMoveSelectorConfig(
@@ -1699,6 +1728,7 @@ void solvePinnedMixedModel() {
16991728
var solverConfig = PlannerTestUtils.buildSolverConfig(
17001729
TestdataMixedSolution.class, TestdataMixedEntity.class, TestdataMixedValue.class,
17011730
TestdataMixedOtherValue.class)
1731+
.withPreviewFeature(DECLARATIVE_SHADOW_VARIABLES)
17021732
.withPhases(new ConstructionHeuristicPhaseConfig())
17031733
.withEasyScoreCalculatorClass(TestdataMixedEasyScoreCalculator.class);
17041734

@@ -1805,6 +1835,7 @@ void solveCustomConfigMixedModel() {
18051835
var solverConfig = PlannerTestUtils.buildSolverConfig(
18061836
TestdataMixedSolution.class, TestdataMixedEntity.class, TestdataMixedValue.class,
18071837
TestdataMixedOtherValue.class)
1838+
.withPreviewFeature(DECLARATIVE_SHADOW_VARIABLES)
18081839
.withPhases(new ConstructionHeuristicPhaseConfig()
18091840
.withEntityPlacerConfigList(valuePlacerConfig, entityPlacerConfig),
18101841
new LocalSearchPhaseConfig().withTerminationConfig(new TerminationConfig().withStepCountLimit(16)))
@@ -1866,6 +1897,7 @@ void solveMoveConfigMixedModel(MoveSelectorConfig moveSelectionConfig) {
18661897
var solverConfig = PlannerTestUtils.buildSolverConfig(
18671898
TestdataMixedSolution.class, TestdataMixedEntity.class, TestdataMixedValue.class,
18681899
TestdataMixedOtherValue.class)
1900+
.withPreviewFeature(DECLARATIVE_SHADOW_VARIABLES)
18691901
.withPhases(new ConstructionHeuristicPhaseConfig(), localSearchConfig)
18701902
.withEasyScoreCalculatorClass(TestdataMixedEasyScoreCalculator.class);
18711903

core/src/test/java/ai/timefold/solver/core/testdomain/mixed/singleentity/TestdataMixedEntity.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@
88
import ai.timefold.solver.core.api.domain.entity.PlanningPinToIndex;
99
import ai.timefold.solver.core.api.domain.variable.PlanningListVariable;
1010
import ai.timefold.solver.core.api.domain.variable.PlanningVariable;
11+
import ai.timefold.solver.core.api.domain.variable.ShadowVariable;
12+
import ai.timefold.solver.core.preview.api.domain.variable.declarative.ShadowSources;
1113
import ai.timefold.solver.core.testdomain.TestdataObject;
1214

1315
@PlanningEntity(difficultyComparatorClass = TestdataMixedEntityComparator.class)
1416
public class TestdataMixedEntity extends TestdataObject {
1517

16-
@PlanningVariable(valueRangeProviderRefs = "otherValueRange", strengthComparatorClass = TestdataMixedOtherValueComparator.class)
18+
@PlanningVariable(valueRangeProviderRefs = "otherValueRange",
19+
strengthComparatorClass = TestdataMixedOtherValueComparator.class)
1720
private TestdataMixedOtherValue basicValue;
1821

1922
@PlanningVariable(valueRangeProviderRefs = "otherValueRange")
@@ -30,6 +33,9 @@ public class TestdataMixedEntity extends TestdataObject {
3033

3134
private int difficulty;
3235

36+
@ShadowVariable(supplierName = "updateDeclarativeShadowValue")
37+
private Integer declarativeShadowVariableValue;
38+
3339
public TestdataMixedEntity() {
3440
// Required for cloner
3541
}
@@ -84,7 +90,24 @@ public int getDifficulty() {
8490
return difficulty;
8591
}
8692

93+
public Integer getDeclarativeShadowVariableValue() {
94+
return declarativeShadowVariableValue;
95+
}
96+
97+
public void setDeclarativeShadowVariableValue(Integer declarativeShadowVariableValue) {
98+
this.declarativeShadowVariableValue = declarativeShadowVariableValue;
99+
}
100+
87101
public void setDifficulty(int difficulty) {
88102
this.difficulty = difficulty;
103+
104+
}
105+
106+
@ShadowSources("basicValue")
107+
public Integer updateDeclarativeShadowValue() {
108+
if (basicValue != null) {
109+
return basicValue.getStrength();
110+
}
111+
return null;
89112
}
90113
}

core/src/test/java/ai/timefold/solver/core/testdomain/mixed/singleentity/TestdataMixedOtherValue.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
77
import ai.timefold.solver.core.api.domain.variable.InverseRelationShadowVariable;
8+
import ai.timefold.solver.core.api.domain.variable.ShadowVariable;
9+
import ai.timefold.solver.core.preview.api.domain.variable.declarative.ShadowSources;
810
import ai.timefold.solver.core.testdomain.TestdataObject;
911

1012
@PlanningEntity
@@ -13,6 +15,9 @@ public class TestdataMixedOtherValue extends TestdataObject {
1315
@InverseRelationShadowVariable(sourceVariableName = "basicValue")
1416
private List<TestdataMixedEntity> entityList;
1517

18+
@ShadowVariable(supplierName = "updateDeclarativeShadowValue")
19+
private Integer declarativeShadowVariableValue;
20+
1621
private int strength;
1722

1823
public TestdataMixedOtherValue() {
@@ -40,4 +45,20 @@ public int getStrength() {
4045
public void setStrength(int strength) {
4146
this.strength = strength;
4247
}
48+
49+
public Integer getDeclarativeShadowVariableValue() {
50+
return declarativeShadowVariableValue;
51+
}
52+
53+
public void setDeclarativeShadowVariableValue(Integer declarativeShadowVariableValue) {
54+
this.declarativeShadowVariableValue = declarativeShadowVariableValue;
55+
}
56+
57+
@ShadowSources("entityList")
58+
public Integer updateDeclarativeShadowValue() {
59+
if (entityList != null) {
60+
return entityList.size() + 2;
61+
}
62+
return null;
63+
}
4364
}

core/src/test/java/ai/timefold/solver/core/testdomain/mixed/singleentity/TestdataMixedSolution.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package ai.timefold.solver.core.testdomain.mixed.singleentity;
22

3+
import static ai.timefold.solver.core.config.solver.PreviewFeature.DECLARATIVE_SHADOW_VARIABLES;
4+
35
import java.util.ArrayList;
46
import java.util.List;
7+
import java.util.Set;
58

69
import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty;
710
import ai.timefold.solver.core.api.domain.solution.PlanningScore;
811
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
9-
import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty;
1012
import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider;
1113
import ai.timefold.solver.core.api.score.buildin.simple.SimpleScore;
1214
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;
@@ -15,8 +17,8 @@
1517
public class TestdataMixedSolution {
1618

1719
public static SolutionDescriptor<TestdataMixedSolution> buildSolutionDescriptor() {
18-
return SolutionDescriptor.buildSolutionDescriptor(TestdataMixedSolution.class, TestdataMixedEntity.class,
19-
TestdataMixedValue.class, TestdataMixedOtherValue.class);
20+
return SolutionDescriptor.buildSolutionDescriptor(Set.of(DECLARATIVE_SHADOW_VARIABLES), TestdataMixedSolution.class,
21+
TestdataMixedEntity.class, TestdataMixedValue.class, TestdataMixedOtherValue.class);
2022
}
2123

2224
public static TestdataMixedSolution generateUninitializedSolution(int entityListSize, int valueListSize,
@@ -42,10 +44,10 @@ public static TestdataMixedSolution generateUninitializedSolution(int entityList
4244
}
4345

4446
@ValueRangeProvider(id = "valueRange")
45-
@ProblemFactCollectionProperty
47+
@PlanningEntityCollectionProperty
4648
private List<TestdataMixedValue> valueList;
4749
@ValueRangeProvider(id = "otherValueRange")
48-
@ProblemFactCollectionProperty
50+
@PlanningEntityCollectionProperty
4951
private List<TestdataMixedOtherValue> otherValueList;
5052
@PlanningEntityCollectionProperty
5153
private List<TestdataMixedEntity> entityList;

0 commit comments

Comments
 (0)