Skip to content

Commit 9bf5ce4

Browse files
authored
feat(neighborhoods): allow uni pick() and split change moves
Majority AI-generated.
1 parent 41417a2 commit 9bf5ce4

40 files changed

Lines changed: 1665 additions & 382 deletions

File tree

core/src/main/java/ai/timefold/solver/core/impl/neighborhood/DefaultNeighborhoodProvider.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,21 @@
33

44
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningListVariableMetaModel;
55
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningVariableMetaModel;
6+
import ai.timefold.solver.core.preview.api.move.builtin.AssignMoveProvider;
67
import ai.timefold.solver.core.preview.api.move.builtin.ChangeMoveProvider;
8+
import ai.timefold.solver.core.preview.api.move.builtin.ListAssignMoveProvider;
79
import ai.timefold.solver.core.preview.api.move.builtin.ListChangeMoveProvider;
810
import ai.timefold.solver.core.preview.api.move.builtin.ListSwapMoveProvider;
11+
import ai.timefold.solver.core.preview.api.move.builtin.ListUnassignMoveProvider;
912
import ai.timefold.solver.core.preview.api.move.builtin.SwapMoveProvider;
13+
import ai.timefold.solver.core.preview.api.move.builtin.UnassignMoveProvider;
1014
import ai.timefold.solver.core.preview.api.neighborhood.Neighborhood;
1115
import ai.timefold.solver.core.preview.api.neighborhood.NeighborhoodBuilder;
1216
import ai.timefold.solver.core.preview.api.neighborhood.NeighborhoodProvider;
1317

1418
import org.jspecify.annotations.NullMarked;
1519

1620
/**
17-
* Currently only includes change and swap moves.
18-
*
1921
* @param <Solution_>
2022
*/
2123
@NullMarked
@@ -31,11 +33,22 @@ public Neighborhood defineNeighborhood(NeighborhoodBuilder<Solution_> builder) {
3133
// TODO Implement 2-opt and 3-opt moves for list variables.
3234
builder.add(new ListChangeMoveProvider<>(listVariableMetaModel));
3335
builder.add(new ListSwapMoveProvider<>(listVariableMetaModel));
36+
if (listVariableMetaModel.allowsUnassignedValues()) {
37+
builder.add(new ListAssignMoveProvider<>(listVariableMetaModel));
38+
builder.add(new ListUnassignMoveProvider<>(listVariableMetaModel));
39+
}
3440
} else if (variableMetaModel instanceof PlanningVariableMetaModel<Solution_, ?, ?> basicVariableMetaModel) {
3541
hasBasicVariable = true;
3642
builder.add(new ChangeMoveProvider<>(basicVariableMetaModel));
43+
if (basicVariableMetaModel.allowsUnassigned()) {
44+
builder.add(new AssignMoveProvider<>(basicVariableMetaModel));
45+
builder.add(new UnassignMoveProvider<>(basicVariableMetaModel));
46+
}
3747
}
3848
}
49+
// Swap move is the only move which switches all variables of an entity,
50+
// and not just one variable.
51+
// It only needs to be included once per entity.
3952
if (hasBasicVariable) {
4053
builder.add(new SwapMoveProvider<>(entityMetaModel));
4154
}

core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/DefaultMoveStreamFactory.java

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningListVariableMetaModel;
1919
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningVariableMetaModel;
2020
import ai.timefold.solver.core.preview.api.domain.metamodel.PositionInList;
21+
import ai.timefold.solver.core.preview.api.domain.metamodel.UnassignedElement;
2122
import ai.timefold.solver.core.preview.api.neighborhood.stream.MoveStreamFactory;
2223
import ai.timefold.solver.core.preview.api.neighborhood.stream.enumerating.UniEnumeratingStream;
2324
import ai.timefold.solver.core.preview.api.neighborhood.stream.function.BiNeighborhoodsMapper;
@@ -104,6 +105,14 @@ public <A> UniEnumeratingStream<Solution_, A> forEachUnfiltered(Class<A> sourceC
104105
.filter(nodeSharingSupportFunctions.assignedValueFilter);
105106
}
106107

108+
@Override
109+
public <Entity_, Value_> UniEnumeratingStream<Solution_, Value_>
110+
forEachUnassignedValue(PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel) {
111+
var nodeSharingSupportFunctions = getNodeSharingSupportFunctions(variableMetaModel);
112+
return forEach(variableMetaModel.type(), false)
113+
.filter(nodeSharingSupportFunctions.unassignedValueFilter);
114+
}
115+
107116
@Override
108117
public <Entity_, Value_> UniEnumeratingStream<Solution_, PositionInList>
109118
forEachDestination(PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel) {
@@ -130,6 +139,7 @@ public <A> UniEnumeratingStream<Solution_, A> forEachUnfiltered(Class<A> sourceC
130139
if (!variableMetaModel.allowsUnassignedValues()) {
131140
return (UniEnumeratingStream) forEachDestination(variableMetaModel);
132141
}
142+
// We include null, as that signifies the future unassigned element.
133143
var unpinnedEntities = forEach(variableMetaModel.entity().type(), true);
134144
// Stream with unpinned values, which are assigned to any list variable;
135145
// always includes null so that we can later create a position at the end of the list,
@@ -165,11 +175,13 @@ public SolutionDescriptor<Solution_> getSolutionDescriptor() {
165175

166176
public record NodeSharingSupportFunctions<Solution_, Entity_, Value_>(
167177
PlanningVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel,
178+
UniNeighborhoodsPredicate<Solution_, Entity_> assignedValueFilter,
168179
BiNeighborhoodsPredicate<Solution_, Entity_, Value_> differentValueFilter,
169180
BiNeighborhoodsPredicate<Solution_, Entity_, Value_> valueInRangeFilter) {
170181

171182
public NodeSharingSupportFunctions(PlanningVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel) {
172183
this(variableMetaModel,
184+
(solutionView, entity) -> solutionView.getValue(variableMetaModel, entity) != null,
173185
(solutionView, entity, value) -> !Objects.equals(solutionView.getValue(variableMetaModel, entity), value),
174186
(solutionView, entity, value) -> solutionView.isValueInRange(variableMetaModel, entity, value));
175187
}
@@ -181,7 +193,9 @@ public record ListVariableNodeSharingSupportFunctions<Solution_, Entity_, Value_
181193
UniNeighborhoodsPredicate<Solution_, Value_> unpinnedValueFilter,
182194
UniNeighborhoodsPredicate<Solution_, Value_> assignedValueOrNullFilter,
183195
UniNeighborhoodsPredicate<Solution_, Value_> assignedValueFilter,
196+
UniNeighborhoodsPredicate<Solution_, Value_> unassignedValueFilter,
184197
BiNeighborhoodsPredicate<Solution_, Entity_, Value_> valueInRangeFilter,
198+
BiNeighborhoodsPredicate<Solution_, Value_, PositionInList> valueInRangeFilterForPosition,
185199
BiNeighborhoodsMapper<Solution_, Entity_, Value_, ElementPosition> toElementPositionMapper,
186200
BiNeighborhoodsMapper<Solution_, Entity_, Value_, PositionInList> toPositionInListMapper) {
187201

@@ -192,11 +206,20 @@ public ListVariableNodeSharingSupportFunctions(
192206
(solutionView, value) -> value == null
193207
|| solutionView.getPositionOf(variableMetaModel, value) instanceof PositionInList,
194208
(solutionView, value) -> solutionView.getPositionOf(variableMetaModel, value) instanceof PositionInList,
209+
(solutionView, value) -> solutionView.getPositionOf(variableMetaModel, value) instanceof UnassignedElement,
195210
(solutionView, entity, value) -> {
196-
if (entity == null || value == null) {
211+
if (value == null) {
212+
// Necessary for the null to survive until the later stage,
213+
// where we will use it as a special marker to move it to the end of list.
214+
return true;
215+
}
216+
return solutionView.isValueInRange(variableMetaModel, entity, value);
217+
},
218+
(solutionView, value, positionInList) -> {
219+
Entity_ entity = positionInList.entity();
220+
if (value == null) {
197221
// Necessary for the null to survive until the later stage,
198-
// where we will use it as a special marker to either unassign the value,
199-
// or move it to the end of list.
222+
// where we will use it as a special marker to move it to the end of list.
200223
return true;
201224
}
202225
return solutionView.isValueInRange(variableMetaModel, entity, value);
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package ai.timefold.solver.core.impl.neighborhood.stream;
2+
3+
import java.util.Iterator;
4+
import java.util.Objects;
5+
import java.util.random.RandomGenerator;
6+
7+
import ai.timefold.solver.core.impl.neighborhood.stream.enumerating.uni.UniLeftDataset;
8+
import ai.timefold.solver.core.preview.api.move.Move;
9+
import ai.timefold.solver.core.preview.api.neighborhood.NeighborhoodSession;
10+
import ai.timefold.solver.core.preview.api.neighborhood.UniMoveConstructor;
11+
12+
import org.jspecify.annotations.NullMarked;
13+
14+
/**
15+
* Accepts a single dataset coming from one enumerating stream,
16+
* and provides {@link Move} iterators based on that dataset.
17+
* The iterators provide {@link Move moves} constructed by a {@link UniMoveConstructor move constructor},
18+
* which accepts instances of type A.
19+
*
20+
* <p>
21+
* There are two types of iterators:
22+
*
23+
* <ul>
24+
* <li>{@link UniOriginalMoveIterator Original order iterators},
25+
* which iterate through all instances of A in the original order.</li>
26+
* <li>{@link UniRandomMoveIterator Random order iterators},
27+
* which pick A randomly.</li>
28+
* </ul>
29+
*
30+
* @param <Solution_>
31+
* @param <A>
32+
*/
33+
@NullMarked
34+
public final class UniMoveStream<Solution_, A> implements InnerMoveStream<Solution_> {
35+
36+
private final UniLeftDataset<Solution_, A> dataset;
37+
private final UniMoveConstructor<Solution_, A> moveConstructor;
38+
39+
public UniMoveStream(UniLeftDataset<Solution_, A> dataset, UniMoveConstructor<Solution_, A> moveConstructor) {
40+
this.dataset = Objects.requireNonNull(dataset);
41+
this.moveConstructor = Objects.requireNonNull(moveConstructor);
42+
}
43+
44+
@SuppressWarnings("unchecked")
45+
@Override
46+
public MoveIterable<Solution_> getMoveIterable(NeighborhoodSession neighborhoodSession) {
47+
var context = new UniMoveStreamContext<>((DefaultNeighborhoodSession<Solution_>) neighborhoodSession, dataset,
48+
moveConstructor);
49+
return new UniMoveIterable<>(context);
50+
}
51+
52+
private record UniMoveIterable<Solution_, A>(UniMoveStreamContext<Solution_, A> context)
53+
implements
54+
MoveIterable<Solution_> {
55+
56+
private UniMoveIterable(UniMoveStreamContext<Solution_, A> context) {
57+
this.context = Objects.requireNonNull(context);
58+
}
59+
60+
@Override
61+
public Iterator<Move<Solution_>> iterator() {
62+
return new UniOriginalMoveIterator<>(context);
63+
}
64+
65+
@Override
66+
public Iterator<Move<Solution_>> iterator(RandomGenerator random) {
67+
return new UniRandomMoveIterator<>(context, random);
68+
}
69+
70+
}
71+
72+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package ai.timefold.solver.core.impl.neighborhood.stream;
2+
3+
import ai.timefold.solver.core.impl.neighborhood.stream.enumerating.uni.UniLeftDataset;
4+
import ai.timefold.solver.core.impl.neighborhood.stream.enumerating.uni.UniLeftDatasetInstance;
5+
import ai.timefold.solver.core.preview.api.move.Move;
6+
import ai.timefold.solver.core.preview.api.neighborhood.UniMoveConstructor;
7+
8+
import org.jspecify.annotations.NullMarked;
9+
import org.jspecify.annotations.Nullable;
10+
11+
@NullMarked
12+
public record UniMoveStreamContext<Solution_, A>(DefaultNeighborhoodSession<Solution_> neighborhoodSession,
13+
UniLeftDataset<Solution_, A> dataset, UniMoveConstructor<Solution_, A> moveConstructor) {
14+
15+
public UniLeftDatasetInstance<Solution_, A> getDatasetInstance() {
16+
return neighborhoodSession.getLeftDatasetInstance(dataset);
17+
}
18+
19+
public Move<Solution_> buildMove(@Nullable A a) {
20+
return moveConstructor.apply(neighborhoodSession.getSolutionView(), a);
21+
}
22+
23+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package ai.timefold.solver.core.impl.neighborhood.stream;
2+
3+
import java.util.Iterator;
4+
import java.util.NoSuchElementException;
5+
import java.util.Objects;
6+
7+
import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple;
8+
import ai.timefold.solver.core.impl.heuristic.move.AbstractSelectorBasedMove;
9+
import ai.timefold.solver.core.preview.api.move.Move;
10+
11+
import org.jspecify.annotations.NullMarked;
12+
import org.jspecify.annotations.Nullable;
13+
14+
/**
15+
* Original iterator for the uni-move stream.
16+
* Builds moves based on all elements in the dataset, in the order in which they appear.
17+
*/
18+
@NullMarked
19+
final class UniOriginalMoveIterator<Solution_, A> implements Iterator<Move<Solution_>> {
20+
21+
private final UniMoveStreamContext<Solution_, A> context;
22+
23+
private @Nullable Move<Solution_> nextMove;
24+
private @Nullable Iterator<UniTuple<A>> tupleIterator;
25+
26+
public UniOriginalMoveIterator(UniMoveStreamContext<Solution_, A> context) {
27+
this.context = Objects.requireNonNull(context);
28+
}
29+
30+
@Override
31+
public boolean hasNext() {
32+
if (nextMove != null) {
33+
return true;
34+
}
35+
if (tupleIterator == null) { // Only create a possibly expensive instance when we actually need it.
36+
tupleIterator = context.getDatasetInstance().iterator();
37+
}
38+
if (!tupleIterator.hasNext()) {
39+
return false;
40+
}
41+
nextMove = context.buildMove(tupleIterator.next().getA());
42+
if (nextMove instanceof AbstractSelectorBasedMove<Solution_> legacyMove) {
43+
throw new UnsupportedOperationException("""
44+
Neighborhoods do not support legacy moves.
45+
Please refactor your code (%s) to use the new Move API."""
46+
.formatted(legacyMove.getClass().getCanonicalName()));
47+
}
48+
return true;
49+
}
50+
51+
@Override
52+
public Move<Solution_> next() {
53+
if (!hasNext()) {
54+
throw new NoSuchElementException();
55+
}
56+
var result = Objects.requireNonNull(nextMove);
57+
nextMove = null;
58+
return result;
59+
}
60+
61+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package ai.timefold.solver.core.impl.neighborhood.stream;
2+
3+
import java.util.Iterator;
4+
import java.util.NoSuchElementException;
5+
import java.util.Objects;
6+
import java.util.random.RandomGenerator;
7+
8+
import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple;
9+
import ai.timefold.solver.core.impl.heuristic.move.AbstractSelectorBasedMove;
10+
import ai.timefold.solver.core.preview.api.move.Move;
11+
12+
import org.jspecify.annotations.NullMarked;
13+
import org.jspecify.annotations.Nullable;
14+
15+
/**
16+
* An iterator for the uni-move stream which returns elements in random order.
17+
* Implements sampling without replacement via {@link ai.timefold.solver.core.impl.bavet.common.index.UniqueRandomIterator}.
18+
*/
19+
@NullMarked
20+
final class UniRandomMoveIterator<Solution_, A> implements Iterator<Move<Solution_>> {
21+
22+
private final UniMoveStreamContext<Solution_, A> context;
23+
private final RandomGenerator workingRandom;
24+
private @Nullable Iterator<UniTuple<A>> tupleIterator;
25+
26+
private @Nullable Move<Solution_> nextMove;
27+
28+
public UniRandomMoveIterator(UniMoveStreamContext<Solution_, A> context, RandomGenerator workingRandom) {
29+
this.context = Objects.requireNonNull(context);
30+
this.workingRandom = Objects.requireNonNull(workingRandom);
31+
}
32+
33+
@Override
34+
public boolean hasNext() {
35+
if (nextMove != null) {
36+
return true;
37+
}
38+
if (tupleIterator == null) { // Only create a possibly expensive instance when we actually need it.
39+
tupleIterator = context.getDatasetInstance().randomIterator(Objects.requireNonNull(workingRandom));
40+
}
41+
if (!tupleIterator.hasNext()) {
42+
return false;
43+
}
44+
nextMove = context.buildMove(tupleIterator.next().getA());
45+
if (nextMove instanceof AbstractSelectorBasedMove<Solution_> legacyMove) {
46+
throw new UnsupportedOperationException("""
47+
Neighborhoods do not support legacy moves.
48+
Please refactor your code (%s) to use the new Move API."""
49+
.formatted(legacyMove.getClass().getCanonicalName()));
50+
}
51+
return true;
52+
}
53+
54+
@Override
55+
public Move<Solution_> next() {
56+
if (!hasNext()) {
57+
throw new NoSuchElementException();
58+
}
59+
var result = Objects.requireNonNull(nextMove);
60+
nextMove = null;
61+
return result;
62+
}
63+
64+
}

core/src/main/java/ai/timefold/solver/core/impl/neighborhood/stream/sampling/DefaultUniSamplingStream.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
import java.util.Objects;
44

5+
import ai.timefold.solver.core.impl.neighborhood.stream.UniMoveStream;
56
import ai.timefold.solver.core.impl.neighborhood.stream.enumerating.uni.AbstractUniEnumeratingStream;
67
import ai.timefold.solver.core.impl.neighborhood.stream.enumerating.uni.UniLeftDataset;
78
import ai.timefold.solver.core.impl.neighborhood.stream.joiner.BiNeighborhoodsJoinerComber;
9+
import ai.timefold.solver.core.preview.api.neighborhood.UniMoveConstructor;
10+
import ai.timefold.solver.core.preview.api.neighborhood.stream.MoveStream;
811
import ai.timefold.solver.core.preview.api.neighborhood.stream.enumerating.UniEnumeratingStream;
912
import ai.timefold.solver.core.preview.api.neighborhood.stream.joiner.BiNeighborhoodsJoiner;
1013
import ai.timefold.solver.core.preview.api.neighborhood.stream.sampling.BiSamplingStream;
@@ -33,4 +36,9 @@ public <B> BiSamplingStream<Solution_, A, B> pick(UniEnumeratingStream<Solution_
3336
((AbstractUniEnumeratingStream<Solution_, B>) uniEnumeratingStream).createRightDataset(comber));
3437
}
3538

39+
@Override
40+
public MoveStream<Solution_> asMove(UniMoveConstructor<Solution_, A> moveConstructor) {
41+
return new UniMoveStream<>(dataset, Objects.requireNonNull(moveConstructor));
42+
}
43+
3644
}

0 commit comments

Comments
 (0)