Skip to content

Commit 5f541c4

Browse files
committed
feat: enable entity value range for LS and remaining move types
1 parent 0ed9176 commit 5f541c4

17 files changed

Lines changed: 231 additions & 73 deletions

File tree

core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/KOptListMove.java

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import ai.timefold.solver.core.api.score.director.ScoreDirector;
1010
import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor;
1111
import ai.timefold.solver.core.impl.heuristic.move.AbstractMove;
12+
import ai.timefold.solver.core.impl.score.director.ValueRangeResolver;
1213
import ai.timefold.solver.core.impl.score.director.VariableDescriptorAwareScoreDirector;
1314

1415
/**
@@ -113,7 +114,28 @@ protected void doMoveOnGenuineVariables(ScoreDirector<Solution_> scoreDirector)
113114

114115
@Override
115116
public boolean isMoveDoable(ScoreDirector<Solution_> scoreDirector) {
116-
return !equivalent2Opts.isEmpty();
117+
var firstPass = !equivalent2Opts.isEmpty();
118+
var secondPass = true;
119+
// When the value range is located at the entity,
120+
// we need to check if the destination's value range accepts the upcoming values
121+
if (firstPass && !listVariableDescriptor.canExtractValueRangeFromSolution()) {
122+
// The changes will be applied to a single entity. No need to check the value ranges.
123+
secondPass = originalEntities.length == 1;
124+
if (!secondPass) {
125+
ValueRangeResolver<Solution_> valueRangeResolver =
126+
((VariableDescriptorAwareScoreDirector<Solution_>) scoreDirector).getValueRangeResolver();
127+
// We need to compute the combined list of values to check the source and destination
128+
var combinedList = computeCombinedList(listVariableDescriptor, originalEntities).copy();
129+
// All moves are applied, but the entity remains unchanged,
130+
// only the combined list of elements is updated.
131+
equivalent2Opts.forEach(move -> move.doMoveOnGenuineVariables(combinedList));
132+
// The sublist for each delegate is recalculated.
133+
combinedList.moveElementsOfDelegates(newEndIndices);
134+
// At this point, we can check if the new arrangement of elements meets the entity value ranges.
135+
secondPass = combinedList.isElementsFromDelegateInEntityValueRange(listVariableDescriptor, valueRangeResolver);
136+
}
137+
}
138+
return firstPass && secondPass;
117139
}
118140

119141
@Override

core/src/main/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/kopt/MultipleDelegateList.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import ai.timefold.solver.core.api.function.TriConsumer;
1515
import ai.timefold.solver.core.impl.domain.variable.ListVariableStateSupply;
1616
import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor;
17+
import ai.timefold.solver.core.impl.score.director.ValueRangeResolver;
1718

1819
/**
1920
* A list that delegates get and set operations to multiple delegates.
@@ -113,6 +114,30 @@ public void moveElementsOfDelegates(int[] newDelegateEndIndices) {
113114
}
114115
}
115116

117+
/**
118+
* When the value range is assigned to the entity,
119+
* we must verify whether the new arrangement of the elements conforms to the entity's value range.
120+
* As a result,
121+
* each sublist generated after applying all changes is validated against the related value ranges.
122+
*/
123+
public <S> boolean isElementsFromDelegateInEntityValueRange(ListVariableDescriptor<S> listVariableDescriptor,
124+
ValueRangeResolver<S> valueRangeResolver) {
125+
if (listVariableDescriptor.getValueRangeDescriptor().canExtractValueRangeFromSolution()) {
126+
return true;
127+
}
128+
for (var i = 0; i < delegateEntities.length; i++) {
129+
var entity = delegateEntities[i];
130+
var valueRange =
131+
valueRangeResolver.extractValueRange(listVariableDescriptor.getValueRangeDescriptor(), null, entity);
132+
for (var value : delegates[i]) {
133+
if (!valueRange.contains(value)) {
134+
return false;
135+
}
136+
}
137+
}
138+
return true;
139+
}
140+
116141
@Override
117142
public int size() {
118143
return totalSize;

core/src/test/java/ai/timefold/solver/core/impl/constructionheuristic/DefaultConstructionHeuristicPhaseTest.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingEntity;
3737
import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingScoreCalculator;
3838
import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingSolution;
39+
import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingValue;
3940
import ai.timefold.solver.core.testdomain.mixed.singleentity.TestdataMixedEntity;
4041
import ai.timefold.solver.core.testdomain.mixed.singleentity.TestdataMixedOtherValue;
4142
import ai.timefold.solver.core.testdomain.mixed.singleentity.TestdataMixedSolution;
@@ -366,9 +367,9 @@ void solveWithEntityValueRangeListVariable() {
366367
.withEasyScoreCalculatorClass(TestdataListEntityProvidingScoreCalculator.class)
367368
.withPhases(new ConstructionHeuristicPhaseConfig());
368369

369-
var value1 = new TestdataListValue("v1");
370-
var value2 = new TestdataListValue("v2");
371-
var value3 = new TestdataListValue("v3");
370+
var value1 = new TestdataListEntityProvidingValue("v1");
371+
var value2 = new TestdataListEntityProvidingValue("v2");
372+
var value3 = new TestdataListEntityProvidingValue("v3");
372373
var entity1 = new TestdataListEntityProvidingEntity("e1", List.of(value1, value2));
373374
var entity2 = new TestdataListEntityProvidingEntity("e2", List.of(value2, value3));
374375

core/src/test/java/ai/timefold/solver/core/impl/domain/solution/descriptor/SolutionDescriptorTest.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,10 @@
4242
import ai.timefold.solver.core.testdomain.invalid.nosolution.TestdataNoSolution;
4343
import ai.timefold.solver.core.testdomain.invalid.variablemap.TestdataMapConfigurationSolution;
4444
import ai.timefold.solver.core.testdomain.list.TestdataListSolution;
45-
import ai.timefold.solver.core.testdomain.list.TestdataListValue;
4645
import ai.timefold.solver.core.testdomain.list.unassignedvar.TestdataAllowsUnassignedValuesListSolution;
4746
import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingEntity;
4847
import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingSolution;
48+
import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingValue;
4949
import ai.timefold.solver.core.testdomain.reflect.generic.TestdataGenericEntity;
5050
import ai.timefold.solver.core.testdomain.reflect.generic.TestdataGenericSolution;
5151
import ai.timefold.solver.core.testdomain.solutionproperties.TestdataNoProblemFactPropertySolution;
@@ -501,11 +501,11 @@ void listVariableProblemScaleEntityProvidingValueRange() {
501501
var solutionDescriptor = TestdataListEntityProvidingSolution.buildSolutionDescriptor();
502502
var solution = new TestdataListEntityProvidingSolution();
503503
var valueRangeResolver = new ValueRangeState<TestdataListEntityProvidingSolution>();
504-
var v1 = new TestdataListValue("1");
505-
var v2 = new TestdataListValue("2");
504+
var v1 = new TestdataListEntityProvidingValue("1");
505+
var v2 = new TestdataListEntityProvidingValue("2");
506506
solution.setEntityList(List.of(
507507
new TestdataListEntityProvidingEntity("e1", List.of(v1, v2)),
508-
new TestdataListEntityProvidingEntity("e2", List.of(v1, v2, new TestdataListValue("3")))));
508+
new TestdataListEntityProvidingEntity("e2", List.of(v1, v2, new TestdataListEntityProvidingValue("3")))));
509509
assertSoftly(softly -> {
510510
softly.assertThat(solutionDescriptor.getGenuineEntityCount(solution)).isEqualTo(2L);
511511
softly.assertThat(solutionDescriptor.getGenuineVariableCount(solution)).isEqualTo(2L);

core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListAssignMoveTest.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import ai.timefold.solver.core.testdomain.list.TestdataListValue;
2020
import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingEntity;
2121
import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingSolution;
22+
import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingValue;
2223

2324
import org.junit.jupiter.api.BeforeEach;
2425
import org.junit.jupiter.api.Test;
@@ -70,9 +71,9 @@ void doMove() {
7071

7172
@Test
7273
void isMoveDoableValueRangeProviderOnEntity() {
73-
var v1 = new TestdataListValue("1");
74-
var v2 = new TestdataListValue("2");
75-
var v3 = new TestdataListValue("3");
74+
var v1 = new TestdataListEntityProvidingValue("1");
75+
var v2 = new TestdataListEntityProvidingValue("2");
76+
var v3 = new TestdataListEntityProvidingValue("3");
7677
var e1 = new TestdataListEntityProvidingEntity("e1", List.of(v1, v2));
7778
var e2 = new TestdataListEntityProvidingEntity("e2", List.of(v1, v3));
7879
// different entity => valid value

core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListChangeMoveSelectorTest.java

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import ai.timefold.solver.core.testdomain.list.unassignedvar.TestdataAllowsUnassignedValuesListValue;
4040
import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingEntity;
4141
import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingSolution;
42+
import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingValue;
4243
import ai.timefold.solver.core.testdomain.list.valuerange.pinned.TestdataListPinnedEntityProvidingEntity;
4344
import ai.timefold.solver.core.testdomain.list.valuerange.pinned.TestdataListPinnedEntityProvidingSolution;
4445
import ai.timefold.solver.core.testdomain.list.valuerange.unassignedvar.TestdataListUnassignedEntityProvidingEntity;
@@ -109,13 +110,11 @@ void original() {
109110

110111
@Test
111112
void originalWithEntityValueRange() {
112-
var v1 = new TestdataListValue("1");
113-
var v2 = new TestdataListValue("2");
114-
var v3 = new TestdataListValue("3");
115-
var a = new TestdataListEntityProvidingEntity("A", List.of(v1, v2));
116-
a.setValueList(List.of(v2, v1));
117-
var b = new TestdataListEntityProvidingEntity("B", List.of(v2, v3));
118-
b.setValueList(List.of(v3));
113+
var v1 = new TestdataListEntityProvidingValue("1");
114+
var v2 = new TestdataListEntityProvidingValue("2");
115+
var v3 = new TestdataListEntityProvidingValue("3");
116+
var a = new TestdataListEntityProvidingEntity("A", List.of(v1, v2), List.of(v2, v1));
117+
var b = new TestdataListEntityProvidingEntity("B", List.of(v2, v3), List.of(v3));
119118
var solution = new TestdataListEntityProvidingSolution();
120119
solution.setEntityList(List.of(a, b));
121120

@@ -435,13 +434,11 @@ void random() {
435434

436435
@Test
437436
void randomWithEntityValueRange() {
438-
var v1 = new TestdataListValue("1");
439-
var v2 = new TestdataListValue("2");
440-
var v3 = new TestdataListValue("3");
441-
var a = new TestdataListEntityProvidingEntity("A", List.of(v1, v2));
442-
a.setValueList(List.of(v2, v1));
443-
var b = new TestdataListEntityProvidingEntity("B", List.of(v2, v3));
444-
b.setValueList(List.of(v3));
437+
var v1 = new TestdataListEntityProvidingValue("1");
438+
var v2 = new TestdataListEntityProvidingValue("2");
439+
var v3 = new TestdataListEntityProvidingValue("3");
440+
var a = new TestdataListEntityProvidingEntity("A", List.of(v1, v2), List.of(v2, v1));
441+
var b = new TestdataListEntityProvidingEntity("B", List.of(v2, v3), List.of(v3));
445442
var solution = new TestdataListEntityProvidingSolution();
446443
solution.setEntityList(List.of(a, b));
447444

core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListChangeMoveTest.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import ai.timefold.solver.core.testdomain.list.TestdataListValue;
2424
import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingEntity;
2525
import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingSolution;
26+
import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingValue;
2627

2728
import org.junit.jupiter.api.BeforeEach;
2829
import org.junit.jupiter.api.Test;
@@ -68,10 +69,11 @@ void isMoveDoable() {
6869

6970
@Test
7071
void isMoveDoableValueRangeProviderOnEntity() {
71-
var e1 = new TestdataListEntityProvidingEntity("e1", List.of(v1, v2));
72-
e1.setValueList(List.of(v1, v2));
73-
var e2 = new TestdataListEntityProvidingEntity("e2", List.of(v1, v3));
74-
e2.setValueList(List.of(v3));
72+
var v1 = new TestdataListEntityProvidingValue("1");
73+
var v2 = new TestdataListEntityProvidingValue("2");
74+
var v3 = new TestdataListEntityProvidingValue("3");
75+
var e1 = new TestdataListEntityProvidingEntity("e1", List.of(v1, v2), List.of(v1, v2));
76+
var e2 = new TestdataListEntityProvidingEntity("e2", List.of(v1, v3), List.of(v3));
7577
// different entity => valid value
7678
assertThat(new ListChangeMove<>(otherVariableDescriptor, e1, 0, e2, 0).isMoveDoable(otherInnerScoreDirector))
7779
.isTrue();

core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/ListSwapMoveTest.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import ai.timefold.solver.core.testdomain.list.TestdataListValue;
2121
import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingEntity;
2222
import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingSolution;
23+
import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingValue;
2324

2425
import org.junit.jupiter.api.BeforeEach;
2526
import org.junit.jupiter.api.Test;
@@ -59,10 +60,12 @@ void isMoveDoable() {
5960

6061
@Test
6162
void isMoveDoableValueRangeProviderOnEntity() {
62-
var e1 = new TestdataListEntityProvidingEntity("e1", List.of(v1, v2, v3));
63-
e1.setValueList(List.of(v1, v2));
64-
var e2 = new TestdataListEntityProvidingEntity("e2", List.of(v1, v3, v4));
65-
e2.setValueList(List.of(v3, v4));
63+
var v1 = new TestdataListEntityProvidingValue("1");
64+
var v2 = new TestdataListEntityProvidingValue("2");
65+
var v3 = new TestdataListEntityProvidingValue("3");
66+
var v4 = new TestdataListEntityProvidingValue("4");
67+
var e1 = new TestdataListEntityProvidingEntity("e1", List.of(v1, v2, v3), List.of(v1, v2));
68+
var e2 = new TestdataListEntityProvidingEntity("e2", List.of(v1, v3, v4), List.of(v3, v4));
6669
// different entity => valid left and right
6770
assertThat(new ListSwapMove<>(otherVariableDescriptor, e1, 0, e2, 0).isMoveDoable(otherInnerScoreDirector))
6871
.isTrue();

core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/SubListChangeMoveTest.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import ai.timefold.solver.core.testdomain.list.TestdataListValue;
1919
import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingEntity;
2020
import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingSolution;
21+
import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingValue;
2122

2223
import org.junit.jupiter.api.BeforeEach;
2324
import org.junit.jupiter.api.Test;
@@ -63,10 +64,12 @@ void isMoveDoable() {
6364

6465
@Test
6566
void isMoveDoableValueRangeProviderOnEntity() {
66-
var e1 = new TestdataListEntityProvidingEntity("e1", List.of(v1, v2, v4));
67-
e1.setValueList(List.of(v1, v4, v2));
68-
var e2 = new TestdataListEntityProvidingEntity("e2", List.of(v1, v3, v4));
69-
e2.setValueList(List.of(v3));
67+
var v1 = new TestdataListEntityProvidingValue("1");
68+
var v2 = new TestdataListEntityProvidingValue("2");
69+
var v3 = new TestdataListEntityProvidingValue("3");
70+
var v4 = new TestdataListEntityProvidingValue("4");
71+
var e1 = new TestdataListEntityProvidingEntity("e1", List.of(v1, v2, v4), List.of(v1, v4, v2));
72+
var e2 = new TestdataListEntityProvidingEntity("e2", List.of(v1, v3, v4), List.of(v3));
7073
// different entity => valid sublist
7174
assertThat(
7275
new SubListChangeMove<>(otherVariableDescriptor, e1, 0, 2, e2, 0, false).isMoveDoable(otherInnerScoreDirector))

core/src/test/java/ai/timefold/solver/core/impl/heuristic/selector/move/generic/list/SubListSwapMoveTest.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import ai.timefold.solver.core.testdomain.list.TestdataListValue;
2020
import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingEntity;
2121
import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingSolution;
22+
import ai.timefold.solver.core.testdomain.list.valuerange.TestdataListEntityProvidingValue;
2223

2324
import org.junit.jupiter.api.BeforeEach;
2425
import org.junit.jupiter.api.Test;
@@ -67,10 +68,13 @@ void isMoveDoable() {
6768

6869
@Test
6970
void isMoveDoableValueRangeProviderOnEntity() {
70-
var e1 = new TestdataListEntityProvidingEntity("e1", List.of(v1, v2, v3));
71-
e1.setValueList(List.of(v1, v4, v2));
72-
var e2 = new TestdataListEntityProvidingEntity("e2", List.of(v1, v3, v4, v5));
73-
e2.setValueList(List.of(v3, v5));
71+
var v1 = new TestdataListEntityProvidingValue("1");
72+
var v2 = new TestdataListEntityProvidingValue("2");
73+
var v3 = new TestdataListEntityProvidingValue("3");
74+
var v4 = new TestdataListEntityProvidingValue("4");
75+
var v5 = new TestdataListEntityProvidingValue("5");
76+
var e1 = new TestdataListEntityProvidingEntity("e1", List.of(v1, v2, v3), List.of(v1, v4, v2));
77+
var e2 = new TestdataListEntityProvidingEntity("e2", List.of(v1, v3, v4, v5), List.of(v3, v5));
7478
// different entity => valid sublist
7579
assertThat(
7680
new SubListSwapMove<>(otherVariableDescriptor, e1, 0, 2, e2, 0, 1, false).isMoveDoable(otherInnerScoreDirector))

0 commit comments

Comments
 (0)