Skip to content

Commit f8dbcca

Browse files
committed
chore: improve Neighborhoods design for scalability
1 parent f47b08f commit f8dbcca

File tree

46 files changed

+604
-883
lines changed

Some content is hidden

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

46 files changed

+604
-883
lines changed

core/src/main/java/ai/timefold/solver/core/impl/bavet/bi/joiner/BiJoinerComber.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
*/
1515
public final class BiJoinerComber<A, B> {
1616

17-
public static <A, B> BiJoinerComber<A, B> comb(BiJoiner<A, B>[] joiners) {
17+
public static <A, B> BiJoinerComber<A, B> comb(BiJoiner<A, B>... joiners) {
1818
List<DefaultBiJoiner<A, B>> defaultJoinerList = new ArrayList<>(joiners.length);
1919
List<BiPredicate<A, B>> filteringList = new ArrayList<>(joiners.length);
2020

core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/ComparisonIndexer.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package ai.timefold.solver.core.impl.bavet.common.index;
22

33
import java.util.Comparator;
4+
import java.util.Iterator;
45
import java.util.Map;
56
import java.util.NavigableMap;
67
import java.util.Objects;
8+
import java.util.Queue;
79
import java.util.TreeMap;
810
import java.util.function.Consumer;
911
import java.util.function.Supplier;
@@ -172,4 +174,26 @@ public String toString() {
172174
return "size = " + comparisonMap.size();
173175
}
174176

177+
private record MergingIterator<T>(Queue<Iterator<T>> iterators) implements Iterator<T> {
178+
179+
@Override
180+
public boolean hasNext() {
181+
while (!iterators.isEmpty()) {
182+
var currentIterator = iterators.peek();
183+
if (currentIterator.hasNext()) {
184+
return true;
185+
} else {
186+
iterators.poll();
187+
}
188+
}
189+
return false;
190+
}
191+
192+
@Override
193+
public T next() {
194+
return iterators.peek().next();
195+
}
196+
197+
}
198+
175199
}

core/src/main/java/ai/timefold/solver/core/impl/bavet/common/index/Indexer.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState;
66
import ai.timefold.solver.core.impl.util.ElementAwareListEntry;
77

8+
import org.jspecify.annotations.NullMarked;
9+
810
/**
911
* An indexer for entity or fact {@code X},
1012
* maps a property or a combination of properties of {@code X}, denoted by {@code indexKeys},
@@ -20,6 +22,7 @@
2022
* For example for {@code from(A).join(B)}, the tuple is {@code UniTuple<A>} xor {@code UniTuple<B>}.
2123
* For example for {@code Bi<A, B>.join(C)}, the tuple is {@code BiTuple<A, B>} xor {@code UniTuple<C>}.
2224
*/
25+
@NullMarked
2326
public sealed interface Indexer<T> permits ComparisonIndexer, EqualsIndexer, NoneIndexer {
2427

2528
ElementAwareListEntry<T> put(Object indexKeys, T tuple);

core/src/main/java/ai/timefold/solver/core/impl/bavet/common/joiner/JoinerType.java

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,14 @@ public enum JoinerType {
2121
}
2222

2323
public JoinerType flip() {
24-
switch (this) {
25-
case LESS_THAN:
26-
return GREATER_THAN;
27-
case LESS_THAN_OR_EQUAL:
28-
return GREATER_THAN_OR_EQUAL;
29-
case GREATER_THAN:
30-
return LESS_THAN;
31-
case GREATER_THAN_OR_EQUAL:
32-
return LESS_THAN_OR_EQUAL;
33-
default:
34-
throw new IllegalStateException("The joinerType (" + this + ") cannot be flipped.");
35-
}
24+
return switch (this) {
25+
case LESS_THAN -> GREATER_THAN;
26+
case LESS_THAN_OR_EQUAL -> GREATER_THAN_OR_EQUAL;
27+
case GREATER_THAN -> LESS_THAN;
28+
case GREATER_THAN_OR_EQUAL -> LESS_THAN_OR_EQUAL;
29+
default -> throw new IllegalStateException("The joinerType (%s) cannot be flipped."
30+
.formatted(this));
31+
};
3632
}
3733

3834
public boolean matches(Object left, Object right) {

core/src/main/java/ai/timefold/solver/core/impl/domain/variable/descriptor/ListVariableDescriptor.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import ai.timefold.solver.core.impl.domain.variable.ListVariableStateDemand;
1616
import ai.timefold.solver.core.impl.domain.variable.inverserelation.InverseRelationShadowVariableDescriptor;
1717
import ai.timefold.solver.core.impl.move.director.MoveDirector;
18-
import ai.timefold.solver.core.impl.neighborhood.maybeapi.stream.enumerating.function.BiEnumeratingFilter;
18+
import ai.timefold.solver.core.impl.neighborhood.maybeapi.stream.enumerating.function.BiEnumeratingPredicate;
1919

2020
public final class ListVariableDescriptor<Solution_> extends GenuineVariableDescriptor<Solution_> {
2121

@@ -24,7 +24,7 @@ public final class ListVariableDescriptor<Solution_> extends GenuineVariableDesc
2424
var list = getValue(entity);
2525
return list.contains(element);
2626
};
27-
private final BiEnumeratingFilter<Solution_, Object, Object> entityContainsPinnedValuePredicate =
27+
private final BiEnumeratingPredicate<Solution_, Object, Object> entityContainsPinnedValuePredicate =
2828
(solutionView, value, entity) -> {
2929
var moveDirector = (MoveDirector<Solution_, ?>) solutionView;
3030
return moveDirector.isPinned(this, value);
@@ -47,8 +47,8 @@ public <A> BiPredicate<A, Object> getInListPredicate() {
4747
}
4848

4949
@SuppressWarnings("unchecked")
50-
public <A, B> BiEnumeratingFilter<Solution_, A, B> getEntityContainsPinnedValuePredicate() {
51-
return (BiEnumeratingFilter<Solution_, A, B>) entityContainsPinnedValuePredicate;
50+
public <A, B> BiEnumeratingPredicate<Solution_, A, B> getEntityContainsPinnedValuePredicate() {
51+
return (BiEnumeratingPredicate<Solution_, A, B>) entityContainsPinnedValuePredicate;
5252
}
5353

5454
public boolean allowsUnassignedValues() {

core/src/main/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/MoveStreamFactory.java

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,11 @@
55
import ai.timefold.solver.core.api.domain.entity.PlanningPinToIndex;
66
import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty;
77
import ai.timefold.solver.core.api.domain.variable.PlanningListVariable;
8-
import ai.timefold.solver.core.impl.neighborhood.maybeapi.stream.enumerating.BiEnumeratingStream;
98
import ai.timefold.solver.core.impl.neighborhood.maybeapi.stream.enumerating.EnumeratingStream;
109
import ai.timefold.solver.core.impl.neighborhood.maybeapi.stream.enumerating.UniEnumeratingStream;
1110
import ai.timefold.solver.core.impl.neighborhood.maybeapi.stream.enumerating.function.UniEnumeratingFilter;
12-
import ai.timefold.solver.core.impl.neighborhood.maybeapi.stream.sampling.BiSamplingStream;
1311
import ai.timefold.solver.core.impl.neighborhood.maybeapi.stream.sampling.UniSamplingStream;
1412
import ai.timefold.solver.core.preview.api.domain.metamodel.ElementPosition;
15-
import ai.timefold.solver.core.preview.api.domain.metamodel.GenuineVariableMetaModel;
1613
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningListVariableMetaModel;
1714
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningSolutionMetaModel;
1815
import ai.timefold.solver.core.preview.api.domain.metamodel.UnassignedElement;
@@ -61,18 +58,6 @@ public interface MoveStreamFactory<Solution_> {
6158
*/
6259
<A> UniEnumeratingStream<Solution_, A> forEachUnfiltered(Class<A> sourceClass, boolean includeNull);
6360

64-
/**
65-
* Enumerate possible values for any given entity,
66-
* where entities are obtained using {@link #forEach(Class, boolean)},
67-
* with the class matching the entity type of the variable.
68-
* If the variable allows unassigned values, the resulting stream will include a null value.
69-
*
70-
* @param variableMetaModel the meta model of the variable to enumerate
71-
* @return enumerating stream with all possible values of a given variable
72-
*/
73-
<Entity_, Value_> BiEnumeratingStream<Solution_, Entity_, Value_>
74-
forEachEntityValuePair(GenuineVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel);
75-
7661
/**
7762
* Enumerate all possible positions of a list variable to which a value can be assigned.
7863
* This will eliminate all positions on {@link PlanningPin pinned entities},
@@ -89,6 +74,4 @@ public interface MoveStreamFactory<Solution_> {
8974

9075
<A> UniSamplingStream<Solution_, A> pick(UniEnumeratingStream<Solution_, A> enumeratingStream);
9176

92-
<A, B> BiSamplingStream<Solution_, A, B> pick(BiEnumeratingStream<Solution_, A, B> enumeratingStream);
93-
9477
}

core/src/main/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/ChangeMoveDefinition.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import ai.timefold.solver.core.impl.neighborhood.maybeapi.MoveDefinition;
66
import ai.timefold.solver.core.impl.neighborhood.maybeapi.MoveStream;
77
import ai.timefold.solver.core.impl.neighborhood.maybeapi.MoveStreamFactory;
8+
import ai.timefold.solver.core.impl.neighborhood.maybeapi.stream.enumerating.EnumeratingJoiners;
9+
import ai.timefold.solver.core.impl.neighborhood.stream.DefaultMoveStreamFactory;
810
import ai.timefold.solver.core.preview.api.domain.metamodel.PlanningVariableMetaModel;
911

1012
import org.jspecify.annotations.NullMarked;
@@ -21,13 +23,12 @@ public ChangeMoveDefinition(PlanningVariableMetaModel<Solution_, Entity_, Value_
2123

2224
@Override
2325
public MoveStream<Solution_> build(MoveStreamFactory<Solution_> moveStreamFactory) {
24-
var enumeratingStream =
25-
moveStreamFactory.forEachEntityValuePair(variableMetaModel)
26-
.filter((solutionView, entity, value) -> {
27-
Value_ currentValue = solutionView.getValue(variableMetaModel, Objects.requireNonNull(entity));
28-
return !Objects.equals(currentValue, value);
29-
});
30-
return moveStreamFactory.pick(enumeratingStream)
26+
var nodeSharingSupportFunctions =
27+
((DefaultMoveStreamFactory<Solution_>) moveStreamFactory).getNodeSharingSupportFunctions(variableMetaModel);
28+
return moveStreamFactory.pick(moveStreamFactory.forEach(variableMetaModel.entity().type(), false))
29+
.pick(moveStreamFactory.forEach(variableMetaModel.type(), variableMetaModel.allowsUnassigned()),
30+
EnumeratingJoiners.filtering(nodeSharingSupportFunctions.differentValueFilter()),
31+
EnumeratingJoiners.filtering(nodeSharingSupportFunctions.valueInRangeFilter()))
3132
.asMove((solution, entity, value) -> Moves.change(Objects.requireNonNull(entity), value, variableMetaModel));
3233
}
3334

core/src/main/java/ai/timefold/solver/core/impl/neighborhood/maybeapi/move/ListChangeMoveDefinition.java

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,10 @@ public ListChangeMoveDefinition(PlanningListVariableMetaModel<Solution_, Entity_
4646
@Override
4747
public MoveStream<Solution_> build(MoveStreamFactory<Solution_> moveStreamFactory) {
4848
var entityValuePairs = moveStreamFactory.forEachAssignablePosition(variableMetaModel);
49-
// The stream of these positions is joined with the stream of all existing values,
50-
// filtering out those which would not result in a valid move.
51-
var enumeratingStream = moveStreamFactory.forEach(variableMetaModel.type(), false)
52-
.join(entityValuePairs, EnumeratingJoiners.filtering(this::isValidChange));
53-
// When picking from this stream, we decide what kind of move we need to create,
54-
// based on whether the value is assigned or unassigned.
55-
return moveStreamFactory.pick(enumeratingStream)
56-
.asMove((solutionView, value, targetPosition) -> {
49+
var availableValues = moveStreamFactory.forEach(variableMetaModel.type(), false);
50+
return moveStreamFactory.pick(entityValuePairs)
51+
.pick(availableValues, EnumeratingJoiners.filtering(this::isValidChange))
52+
.asMove((solutionView, targetPosition, value) -> {
5753
var currentPosition = solutionView.getPositionOf(variableMetaModel, Objects.requireNonNull(value));
5854
if (targetPosition instanceof UnassignedElement) {
5955
var currentElementPosition = currentPosition.ensureAssigned();
@@ -68,7 +64,7 @@ public MoveStream<Solution_> build(MoveStreamFactory<Solution_> moveStreamFactor
6864
});
6965
}
7066

71-
private boolean isValidChange(SolutionView<Solution_> solutionView, Value_ value, ElementPosition targetPosition) {
67+
private boolean isValidChange(SolutionView<Solution_> solutionView, ElementPosition targetPosition, Value_ value) {
7268
var currentPosition = solutionView.getPositionOf(variableMetaModel, value);
7369
if (currentPosition.equals(targetPosition)) { // No change needed.
7470
return false;
Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
package ai.timefold.solver.core.impl.neighborhood.maybeapi.move;
22

33
import java.util.Objects;
4+
import java.util.function.Function;
45

6+
import ai.timefold.solver.core.api.domain.lookup.PlanningId;
7+
import ai.timefold.solver.core.impl.domain.common.accessor.MemberAccessor;
8+
import ai.timefold.solver.core.impl.domain.solution.descriptor.DefaultPlanningSolutionMetaModel;
9+
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;
510
import ai.timefold.solver.core.impl.neighborhood.maybeapi.MoveDefinition;
611
import ai.timefold.solver.core.impl.neighborhood.maybeapi.MoveStream;
712
import ai.timefold.solver.core.impl.neighborhood.maybeapi.MoveStreamFactory;
@@ -17,46 +22,59 @@ public class ListSwapMoveDefinition<Solution_, Entity_, Value_>
1722
implements MoveDefinition<Solution_> {
1823

1924
private final PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel;
25+
private final Function<Entity_, Comparable> planningIdGetter;
2026

2127
public ListSwapMoveDefinition(PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel) {
2228
this.variableMetaModel = Objects.requireNonNull(variableMetaModel);
29+
this.planningIdGetter = getPlanningIdGetter(variableMetaModel.entity().type());
30+
}
31+
32+
private <A> Function<A, Comparable> getPlanningIdGetter(Class<A> sourceClass) {
33+
SolutionDescriptor<Solution_> solutionDescriptor =
34+
((DefaultPlanningSolutionMetaModel<Solution_>) variableMetaModel.entity().solution()).solutionDescriptor();
35+
MemberAccessor planningIdMemberAccessor = solutionDescriptor.getPlanningIdAccessor(sourceClass);
36+
if (planningIdMemberAccessor == null) {
37+
throw new IllegalArgumentException(
38+
"The fromClass (%s) has no member with a @%s annotation, so the pairs cannot be made unique ([A,B] vs [B,A])."
39+
.formatted(sourceClass, PlanningId.class.getSimpleName()));
40+
}
41+
return planningIdMemberAccessor.getGetterFunction();
2342
}
2443

2544
@Override
2645
public MoveStream<Solution_> build(MoveStreamFactory<Solution_> moveStreamFactory) {
2746
var assignedValueStream = moveStreamFactory.forEach(variableMetaModel.type(), false)
28-
.filter((solutionView,
29-
value) -> solutionView.getPositionOf(variableMetaModel, value) instanceof PositionInList);
30-
var validAssignedValuePairStream = assignedValueStream.join(assignedValueStream,
31-
EnumeratingJoiners.filtering((SolutionView<Solution_> solutionView, Value_ leftValue,
32-
Value_ rightValue) -> !Objects.equals(leftValue, rightValue)));
33-
// Ensure unique pairs; without demanding PlanningId, this becomes tricky.
34-
// Convert values to their locations in list.
35-
var validAssignedValueUniquePairStream =
36-
validAssignedValuePairStream
37-
.map((solutionView, leftValue, rightValue) -> new UniquePair<>(leftValue, rightValue))
38-
.distinct()
39-
.map((solutionView, pair) -> FullElementPosition.of(variableMetaModel, solutionView, pair.first()),
40-
(solutionView, pair) -> FullElementPosition.of(variableMetaModel, solutionView, pair.second()));
41-
// Eliminate pairs that cannot be swapped due to value range restrictions.
42-
var result = validAssignedValueUniquePairStream
43-
.filter((solutionView, leftPosition, rightPosition) -> solutionView.isValueInRange(variableMetaModel,
44-
rightPosition.entity(), leftPosition.value())
45-
&& solutionView.isValueInRange(variableMetaModel, leftPosition.entity(), rightPosition.value()));
46-
// Finally pick the moves.
47-
return moveStreamFactory.pick(result)
47+
.filter((solutionView, value) -> solutionView.getPositionOf(variableMetaModel, value) instanceof PositionInList)
48+
.map((solutionView, value) -> new FullElementPosition<>(value,
49+
solutionView.getPositionOf(variableMetaModel, value).ensureAssigned(), planningIdGetter));
50+
return moveStreamFactory.pick(assignedValueStream)
51+
.pick(assignedValueStream,
52+
EnumeratingJoiners.lessThan(a -> a),
53+
EnumeratingJoiners.filtering(this::isValidSwap))
4854
.asMove((solutionView, leftPosition, rightPosition) -> Moves.swap(leftPosition.elementPosition,
4955
rightPosition.elementPosition, variableMetaModel));
5056
}
5157

58+
private boolean isValidSwap(SolutionView<Solution_> solutionView,
59+
FullElementPosition<Entity_, Value_> leftPosition,
60+
FullElementPosition<Entity_, Value_> rightPosition) {
61+
if (Objects.equals(leftPosition, rightPosition)) {
62+
return false;
63+
}
64+
return solutionView.isValueInRange(variableMetaModel, rightPosition.entity(), leftPosition.value())
65+
&& solutionView.isValueInRange(variableMetaModel, leftPosition.entity(), rightPosition.value());
66+
}
67+
5268
@NullMarked
53-
private record FullElementPosition<Entity_, Value_>(Value_ value, PositionInList elementPosition) {
69+
private record FullElementPosition<Entity_, Value_>(Value_ value, PositionInList elementPosition,
70+
Function<Entity_, Comparable> planningIdGetter) implements Comparable<FullElementPosition<Entity_, Value_>> {
5471

5572
public static <Solution_, Entity_, Value_> FullElementPosition<Entity_, Value_> of(
5673
PlanningListVariableMetaModel<Solution_, Entity_, Value_> variableMetaModel,
57-
SolutionView<Solution_> solutionView, Value_ value) {
74+
SolutionView<Solution_> solutionView, Value_ value,
75+
Function<Entity_, Comparable> planningIdGetter) {
5876
var assignedElement = solutionView.getPositionOf(variableMetaModel, value).ensureAssigned();
59-
return new FullElementPosition<>(value, assignedElement);
77+
return new FullElementPosition<>(value, assignedElement, planningIdGetter);
6078
}
6179

6280
public Entity_ entity() {
@@ -67,6 +85,15 @@ public int index() {
6785
return elementPosition.index();
6886
}
6987

88+
@Override
89+
public int compareTo(FullElementPosition<Entity_, Value_> o) {
90+
var entityComparison = planningIdGetter.apply(this.entity()).compareTo(planningIdGetter.apply(o.entity()));
91+
if (entityComparison != 0) {
92+
return entityComparison;
93+
}
94+
return Integer.compare(this.index(), o.index());
95+
}
96+
7097
}
7198

7299
}

0 commit comments

Comments
 (0)