Skip to content

Commit b701ef9

Browse files
authored
chore: move streams support for swap moves (TimefoldAI#1765)
1 parent 14bc23c commit b701ef9

24 files changed

Lines changed: 1228 additions & 269 deletions

File tree

core/src/main/java/ai/timefold/solver/core/impl/constructionheuristic/decider/ConstructionHeuristicDecider.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import ai.timefold.solver.core.impl.constructionheuristic.scope.ConstructionHeuristicPhaseScope;
1010
import ai.timefold.solver.core.impl.constructionheuristic.scope.ConstructionHeuristicStepScope;
1111
import ai.timefold.solver.core.impl.heuristic.move.LegacyMoveAdapter;
12-
import ai.timefold.solver.core.impl.move.generic.NoChangeMove;
12+
import ai.timefold.solver.core.impl.heuristic.move.NoChangeMove;
1313
import ai.timefold.solver.core.impl.phase.scope.SolverLifecyclePoint;
1414
import ai.timefold.solver.core.impl.solver.scope.SolverScope;
1515
import ai.timefold.solver.core.impl.solver.termination.PhaseTermination;
@@ -133,10 +133,10 @@ public void decideNextStep(ConstructionHeuristicStepScope<Solution_> stepScope,
133133
private static <Solution_> boolean isAllowedNonDoableMove(Move<Solution_> move) {
134134
if (move instanceof LegacyMoveAdapter<Solution_> legacyMove) {
135135
var adaptedMove = legacyMove.legacyMove();
136-
return adaptedMove instanceof ai.timefold.solver.core.impl.heuristic.move.NoChangeMove<Solution_>
136+
return adaptedMove instanceof NoChangeMove<Solution_>
137137
|| adaptedMove instanceof ai.timefold.solver.core.impl.heuristic.selector.move.generic.ChangeMove<Solution_>;
138138
} else {
139-
return move instanceof NoChangeMove<Solution_>;
139+
return false;
140140
}
141141
}
142142

core/src/main/java/ai/timefold/solver/core/impl/exhaustivesearch/DefaultExhaustiveSearchPhase.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import ai.timefold.solver.core.impl.exhaustivesearch.scope.ExhaustiveSearchPhaseScope;
1515
import ai.timefold.solver.core.impl.exhaustivesearch.scope.ExhaustiveSearchStepScope;
1616
import ai.timefold.solver.core.impl.heuristic.selector.entity.EntitySelector;
17-
import ai.timefold.solver.core.impl.move.generic.CompositeMove;
17+
import ai.timefold.solver.core.impl.move.streams.maybeapi.generic.move.CompositeMove;
1818
import ai.timefold.solver.core.impl.phase.AbstractPhase;
1919
import ai.timefold.solver.core.impl.solver.scope.SolverScope;
2020
import ai.timefold.solver.core.impl.solver.termination.PhaseTermination;
@@ -166,6 +166,10 @@ protected <Score_ extends Score<Score_>> void restoreWorkingSolution(ExhaustiveS
166166
restoreMoveList.addAll(oldMoveList);
167167
Collections.reverse(newMoveList);
168168
restoreMoveList.addAll(newMoveList);
169+
if (restoreMoveList.isEmpty()) {
170+
// No moves to restore, so the working solution is already correct
171+
return;
172+
}
169173
var compositeMove = CompositeMove.buildMove(restoreMoveList);
170174
phaseScope.getScoreDirector().executeMove(compositeMove);
171175
var startingStepScore = stepScope.<Score_> getStartingStepScore();

core/src/main/java/ai/timefold/solver/core/impl/heuristic/move/LegacyMoveAdapter.java

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import ai.timefold.solver.core.api.score.director.ScoreDirector;
66
import ai.timefold.solver.core.impl.move.InnerMutableSolutionView;
77
import ai.timefold.solver.core.impl.move.director.MoveDirector;
8-
import ai.timefold.solver.core.impl.move.generic.NoChangeMove;
98
import ai.timefold.solver.core.preview.api.move.Move;
109
import ai.timefold.solver.core.preview.api.move.MutableSolutionView;
1110
import ai.timefold.solver.core.preview.api.move.Rebaser;
@@ -14,7 +13,7 @@
1413
import org.jspecify.annotations.NullMarked;
1514

1615
/**
17-
* Adapts {@link ai.timefold.solver.core.impl.heuristic.move.Move} a legacy move)
16+
* Adapts {@link ai.timefold.solver.core.impl.heuristic.move.Move a legacy move}
1817
* to {@link Move a new move}.
1918
* Once the move selector framework is removed, this may be removed as well.
2019
*
@@ -30,12 +29,10 @@ public record LegacyMoveAdapter<Solution_>(
3029
* A move is only doable if:
3130
*
3231
* <ul>
33-
* <li>It is a new {@link Move} and not a {@link NoChangeMove}</li>
34-
* <li>It is a legacy move and its {@link AbstractMove#isMoveDoable(ScoreDirector)} return false.</li>
32+
* <li>It is a new {@link Move}.</li>
33+
* <li>It is a legacy move and its {@link AbstractMove#isMoveDoable(ScoreDirector)} return {@code true}.</li>
3534
* </ul>
3635
*
37-
* New moves are doable by default.
38-
*
3936
* @param moveDirector never null
4037
* @param move never null
4138
* @return true if the move is doable
@@ -44,7 +41,7 @@ public static <Solution_> boolean isDoable(MoveDirector<Solution_, ?> moveDirect
4441
if (move instanceof LegacyMoveAdapter<Solution_> legacyMoveAdapter) {
4542
return legacyMoveAdapter.isMoveDoable(moveDirector);
4643
} else {
47-
return !(move instanceof NoChangeMove<Solution_>);
44+
return true; // New moves are always doable.
4845
}
4946
}
5047

core/src/main/java/ai/timefold/solver/core/impl/move/director/MoveDirector.java

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package ai.timefold.solver.core.impl.move.director;
22

3-
import java.util.List;
43
import java.util.Objects;
54
import java.util.function.BiFunction;
65

@@ -64,15 +63,33 @@ public final <Entity_, Value_> void assignValue(PlanningListVariableMetaModel<So
6463
}
6564

6665
@Override
67-
public final <Entity_, Value_> void unassignValue(
68-
PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel,
69-
Value_ movedValue, Entity_ sourceEntity, int sourceIndex) {
66+
public <Entity_, Value_> void unassignValue(PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel,
67+
Value_ value) {
68+
var locationInList = getPositionOf(variableMetaModel, value)
69+
.ensureAssigned(() -> """
70+
The value (%s) is not assigned to a list variable.
71+
This may indicate score corruption or a problem with the move's implementation."""
72+
.formatted(value));
73+
unassignValue(variableMetaModel, value, locationInList.entity(), locationInList.index());
74+
}
75+
76+
@Override
77+
public <Entity_, Value_> Value_ unassignValue(PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel,
78+
Entity_ entity, int index) {
79+
var value = getValueAtIndex(variableMetaModel, entity, index);
80+
unassignValue(variableMetaModel, value, entity, index);
81+
return value;
82+
}
83+
84+
private <Entity_, Value_> void unassignValue(
85+
PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel, Value_ movedValue, Entity_ entity,
86+
int index) {
7087
var variableDescriptor =
7188
((DefaultPlanningListVariableMetaModel<Solution_, Entity_, Value_>) variableMetaModel).variableDescriptor();
7289
externalScoreDirector.beforeListVariableElementUnassigned(variableDescriptor, movedValue);
73-
externalScoreDirector.beforeListVariableChanged(variableDescriptor, sourceEntity, sourceIndex, sourceIndex + 1);
74-
variableDescriptor.getValue(sourceEntity).remove(sourceIndex);
75-
externalScoreDirector.afterListVariableChanged(variableDescriptor, sourceEntity, sourceIndex, sourceIndex);
90+
externalScoreDirector.beforeListVariableChanged(variableDescriptor, entity, index, index + 1);
91+
variableDescriptor.getValue(entity).remove(index);
92+
externalScoreDirector.afterListVariableChanged(variableDescriptor, entity, index, index);
7693
externalScoreDirector.afterListVariableElementUnassigned(variableDescriptor, movedValue);
7794
}
7895

@@ -92,6 +109,7 @@ public final <Entity_, Value_> void changeVariable(PlanningVariableMetaModel<Sol
92109
return moveValueInList(variableMetaModel, sourceEntity, sourceIndex, destinationIndex);
93110
}
94111
var variableDescriptor = extractVariableDescriptor(variableMetaModel);
112+
95113
externalScoreDirector.beforeListVariableChanged(variableDescriptor, sourceEntity, sourceIndex, sourceIndex + 1);
96114
var element = (Value_) variableDescriptor.removeElement(sourceEntity, sourceIndex);
97115
externalScoreDirector.afterListVariableChanged(variableDescriptor, sourceEntity, sourceIndex, sourceIndex);
@@ -108,21 +126,57 @@ public final <Entity_, Value_> void changeVariable(PlanningVariableMetaModel<Sol
108126
@SuppressWarnings("unchecked")
109127
@Override
110128
public final <Entity_, Value_> @Nullable Value_ moveValueInList(
111-
PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel, Entity_ entity, int sourceIndex,
129+
PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel, Entity_ sourceEntity, int sourceIndex,
112130
int destinationIndex) {
113131
if (sourceIndex == destinationIndex) {
114132
return null;
115-
} else if (sourceIndex > destinationIndex) { // Always start from the lower index.
116-
return moveValueInList(variableMetaModel, entity, destinationIndex, sourceIndex);
117133
}
118134
var variableDescriptor = extractVariableDescriptor(variableMetaModel);
119-
var toIndex = destinationIndex + 1;
120-
externalScoreDirector.beforeListVariableChanged(variableDescriptor, entity, sourceIndex, toIndex);
121-
var variable = (List<Value_>) variableDescriptor.getValue(entity);
122-
var value = variable.remove(sourceIndex);
123-
variable.add(destinationIndex, value);
124-
externalScoreDirector.afterListVariableChanged(variableDescriptor, entity, sourceIndex, toIndex);
125-
return value;
135+
var fromIndex = Math.min(sourceIndex, destinationIndex);
136+
var toIndex = Math.max(sourceIndex, destinationIndex) + 1;
137+
138+
externalScoreDirector.beforeListVariableChanged(variableDescriptor, sourceEntity, fromIndex, toIndex);
139+
Value_ element = (Value_) variableDescriptor.removeElement(sourceEntity, sourceIndex);
140+
variableDescriptor.addElement(sourceEntity, destinationIndex, element);
141+
externalScoreDirector.afterListVariableChanged(variableDescriptor, sourceEntity, fromIndex, toIndex);
142+
143+
return element;
144+
}
145+
146+
@Override
147+
public <Entity_, Value_> void swapValuesBetweenLists(
148+
PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel, Entity_ leftEntity, int leftIndex,
149+
Entity_ rightEntity, int rightIndex) {
150+
if (leftEntity == rightEntity) {
151+
swapValuesInList(variableMetaModel, leftEntity, leftIndex, rightIndex);
152+
} else {
153+
var variableDescriptor = extractVariableDescriptor(variableMetaModel);
154+
externalScoreDirector.beforeListVariableChanged(variableDescriptor, leftEntity, leftIndex, leftIndex + 1);
155+
externalScoreDirector.beforeListVariableChanged(variableDescriptor, rightEntity, rightIndex, rightIndex + 1);
156+
var oldLeftElement = variableDescriptor.setElement(leftEntity, leftIndex,
157+
variableDescriptor.getElement(rightEntity, rightIndex));
158+
variableDescriptor.setElement(rightEntity, rightIndex, oldLeftElement);
159+
externalScoreDirector.afterListVariableChanged(variableDescriptor, leftEntity, leftIndex, leftIndex + 1);
160+
externalScoreDirector.afterListVariableChanged(variableDescriptor, rightEntity, rightIndex, rightIndex + 1);
161+
}
162+
}
163+
164+
@Override
165+
public <Entity_, Value_> void swapValuesInList(PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel,
166+
Entity_ entity, int leftIndex, int rightIndex) {
167+
if (leftIndex == rightIndex) {
168+
return;
169+
}
170+
171+
var variableDescriptor = extractVariableDescriptor(variableMetaModel);
172+
var fromIndex = Math.min(leftIndex, rightIndex);
173+
var toIndex = Math.max(leftIndex, rightIndex) + 1;
174+
175+
externalScoreDirector.beforeListVariableChanged(variableDescriptor, entity, fromIndex, toIndex);
176+
var oldLeftElement =
177+
variableDescriptor.setElement(entity, leftIndex, variableDescriptor.getElement(entity, rightIndex));
178+
variableDescriptor.setElement(entity, rightIndex, oldLeftElement);
179+
externalScoreDirector.afterListVariableChanged(variableDescriptor, entity, fromIndex, toIndex);
126180
}
127181

128182
@Override

core/src/main/java/ai/timefold/solver/core/impl/move/generic/NoChangeMove.java

Lines changed: 0 additions & 57 deletions
This file was deleted.
Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
11
package ai.timefold.solver.core.impl.move.streams.maybeapi.generic.move;
22

3-
import java.util.ArrayList;
4-
import java.util.Collections;
5-
import java.util.LinkedHashSet;
63
import java.util.List;
7-
import java.util.Set;
84

9-
import ai.timefold.solver.core.api.score.director.ScoreDirector;
105
import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultPlanningListVariableMetaModel;
116
import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultPlanningVariableMetaModel;
127
import ai.timefold.solver.core.impl.domain.solution.descriptor.InnerVariableMetaModel;
138
import ai.timefold.solver.core.impl.domain.variable.descriptor.GenuineVariableDescriptor;
149
import ai.timefold.solver.core.impl.domain.variable.descriptor.ListVariableDescriptor;
1510
import ai.timefold.solver.core.impl.domain.variable.descriptor.VariableDescriptor;
11+
import ai.timefold.solver.core.preview.api.domain.metamodel.GenuineVariableMetaModel;
1612
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningListVariableMetaModel;
1713
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningVariableMetaModel;
1814
import ai.timefold.solver.core.preview.api.domain.metamodel.VariableMetaModel;
@@ -26,9 +22,8 @@ public abstract class AbstractMove<Solution_> implements Move<Solution_> {
2622
private static final char OPENING_PARENTHESES = '(';
2723
private static final char CLOSING_PARENTHESES = ')';
2824

29-
@Override
3025
public final String describe() {
31-
var metaModels = getVariableMetaModels();
26+
var metaModels = variableMetaModels();
3227
var substring = switch (metaModels.size()) {
3328
case 0 -> "";
3429
case 1 -> OPENING_PARENTHESES + getVariableDescriptor(metaModels.get(0)).getSimpleEntityAndVariableName()
@@ -37,7 +32,7 @@ public final String describe() {
3732
var stringBuilder = new StringBuilder()
3833
.append(OPENING_PARENTHESES);
3934
var first = true;
40-
for (var variableMetaModel : getVariableMetaModels()) {
35+
for (var variableMetaModel : metaModels) {
4136
if (first) {
4237
first = false;
4338
} else {
@@ -54,9 +49,7 @@ public final String describe() {
5449

5550
public abstract String toString();
5651

57-
protected List<VariableMetaModel<Solution_, ?, ?>> getVariableMetaModels() {
58-
return Collections.emptyList();
59-
}
52+
public abstract List<? extends GenuineVariableMetaModel<Solution_, ?, ?>> variableMetaModels();
6053

6154
@SuppressWarnings("unchecked")
6255
protected static <Solution_> VariableDescriptor<Solution_>
@@ -74,20 +67,4 @@ public final String describe() {
7467
return ((DefaultPlanningListVariableMetaModel<Solution_, ?, ?>) variableMetaModel).variableDescriptor();
7568
}
7669

77-
public static <E> List<E> rebaseList(List<E> externalObjectList, ScoreDirector<?> destinationScoreDirector) {
78-
var rebasedObjectList = new ArrayList<E>(externalObjectList.size());
79-
for (var entity : externalObjectList) {
80-
rebasedObjectList.add(destinationScoreDirector.lookUpWorkingObject(entity));
81-
}
82-
return rebasedObjectList;
83-
}
84-
85-
public static <E> Set<E> rebaseSet(Set<E> externalObjectSet, ScoreDirector<?> destinationScoreDirector) {
86-
var rebasedObjectSet = new LinkedHashSet<E>(externalObjectSet.size());
87-
for (var entity : externalObjectSet) {
88-
rebasedObjectSet.add(destinationScoreDirector.lookUpWorkingObject(entity));
89-
}
90-
return rebasedObjectSet;
91-
}
92-
9370
}

core/src/main/java/ai/timefold/solver/core/impl/move/streams/maybeapi/generic/move/ChainedChangeMove.java

Lines changed: 0 additions & 56 deletions
This file was deleted.

0 commit comments

Comments
 (0)