Skip to content

Commit f2ef168

Browse files
authored
chore: support pinning in Move Streams (#1560)
By default, enumerations produce results without pinned entities. If pinned entities are required, they have to be specifically requested.
1 parent 3b36628 commit f2ef168

File tree

80 files changed

+1286
-814
lines changed

Some content is hidden

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

80 files changed

+1286
-814
lines changed

core/src/main/java/ai/timefold/solver/core/api/domain/entity/PinningFilter.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,25 @@
22

33
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
44

5-
import org.jspecify.annotations.NonNull;
5+
import org.jspecify.annotations.NullMarked;
66

77
/**
88
* Decides on accepting or discarding a {@link PlanningEntity}.
99
* A pinned {@link PlanningEntity}'s planning variables are never changed.
1010
*
1111
* @param <Solution_> the solution type, the class with the {@link PlanningSolution} annotation
1212
* @param <Entity_> the entity type, the class with the {@link PlanningEntity} annotation
13+
* @deprecated Use {@link PlanningPin} instead.
1314
*/
15+
@Deprecated(forRemoval = true, since = "1.23.0")
16+
@NullMarked
1417
public interface PinningFilter<Solution_, Entity_> {
1518

1619
/**
1720
* @param solution working solution to which the entity belongs
1821
* @param entity a {@link PlanningEntity}
1922
* @return true if the entity it is pinned, false if the entity is movable.
2023
*/
21-
boolean accept(@NonNull Solution_ solution, @NonNull Entity_ entity);
24+
boolean accept(Solution_ solution, Entity_ entity);
2225

2326
}

core/src/main/java/ai/timefold/solver/core/api/domain/entity/PlanningEntity.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,18 +37,23 @@
3737
/**
3838
* A pinned planning entity is never changed during planning,
3939
* this is useful in repeated planning use cases (such as continuous planning and real-time planning).
40-
* <p>
4140
* This applies to all the planning variables of this planning entity.
42-
* To pin individual variables, see https://issues.redhat.com/browse/PLANNER-124
4341
* <p>
4442
* The method {@link PinningFilter#accept(Object, Object)} returns false if the selection entity is pinned
4543
* and it returns true if the selection entity is movable
4644
*
4745
* @return {@link NullPinningFilter} when it is null (workaround for annotation limitation)
46+
* @deprecated Prefer using {@link PlanningPin}.
4847
*/
48+
@Deprecated(forRemoval = true, since = "1.23.0")
4949
Class<? extends PinningFilter> pinningFilter() default NullPinningFilter.class;
5050

51-
/** Workaround for annotation limitation in {@link #pinningFilter()} ()}. */
51+
/**
52+
* Workaround for annotation limitation in {@link #pinningFilter()}.
53+
*
54+
* @deprecated Prefer using {@link PlanningPin}.
55+
*/
56+
@Deprecated(forRemoval = true, since = "1.23.0")
5257
interface NullPinningFilter extends PinningFilter {
5358
}
5459

core/src/main/java/ai/timefold/solver/core/api/domain/entity/PlanningPin.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
import java.lang.annotation.Target;
99

1010
import ai.timefold.solver.core.api.domain.variable.PlanningListVariable;
11+
import ai.timefold.solver.core.api.solver.change.ProblemChange;
1112

1213
/**
1314
* Specifies that a boolean property (or field) of a {@link PlanningEntity} determines if the planning entity is pinned.
14-
* A pinned planning entity is never changed during planning.
15+
* A pinned planning entity is never changed during planning;
16+
* to change a pinned planning entity, even to make it not pinned anymore, trigger a {@link ProblemChange}.
1517
* For example, it allows the user to pin a shift to a specific employee before solving
1618
* and the solver will not undo that, regardless of the constraints.
1719
* <p>
@@ -20,9 +22,9 @@
2022
* It applies to all the planning variables of that planning entity.
2123
* If set on an entity with {@link PlanningListVariable},
2224
* this will pin the entire list of planning values as well.
23-
* <p>
24-
* This is syntactic sugar for {@link PlanningEntity#pinningFilter()},
25-
* which is a more flexible and verbose way to pin a planning entity.
25+
*
26+
* @see PlanningPinToIndex Read more about how to only pin part of the planning list variable.
27+
* @see ProblemChange Use ProblemChange to trigger pinning changes.
2628
*/
2729
@Target({ METHOD, FIELD })
2830
@Retention(RUNTIME)

core/src/main/java/ai/timefold/solver/core/api/domain/entity/PlanningPinToIndex.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.lang.annotation.Target;
99

1010
import ai.timefold.solver.core.api.domain.variable.PlanningListVariable;
11+
import ai.timefold.solver.core.api.solver.change.ProblemChange;
1112

1213
/**
1314
* Specifies that an {@code int} property (or field) of a {@link PlanningEntity} determines
@@ -32,6 +33,7 @@
3233
* </ul>
3334
*
3435
* To pin the entire list and disallow any changes, use {@link PlanningPin} instead.
36+
* The index must never change during planning; to change it, trigger a {@link ProblemChange}.
3537
*
3638
* <p>
3739
* Example: Assuming a list of values {@code [A, B, C]}:
@@ -47,6 +49,9 @@
4749
* If the same entity also specifies a {@link PlanningPin} and the pin is enabled,
4850
* any value of {@link PlanningPinToIndex} is ignored.
4951
* In other words, enabling {@link PlanningPin} pins the entire list without exception.
52+
*
53+
* @see PlanningPin Pin the entire entity.
54+
* @see ProblemChange Use ProblemChange to trigger pinning changes.
5055
*/
5156
@Target({ METHOD, FIELD })
5257
@Retention(RUNTIME)

core/src/main/java/ai/timefold/solver/core/api/solver/change/ProblemChange.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,9 @@
8181
public interface ProblemChange<Solution_> {
8282

8383
/**
84-
* Do the change on the {@link PlanningSolution}. Every modification to the {@link PlanningSolution} must
85-
* be done via the {@link ProblemChangeDirector}, otherwise the {@link Score} calculation will be corrupted.
84+
* Do the change on the {@link PlanningSolution}.
85+
* Every modification to the {@link PlanningSolution} must be done via the {@link ProblemChangeDirector},
86+
* otherwise the {@link Score} calculation will be corrupted.
8687
*
8788
* @param workingSolution the {@link PlanningSolution working solution} which contains the problem facts
8889
* (and {@link PlanningEntity planning entities}) to change

core/src/main/java/ai/timefold/solver/core/impl/bavet/AbstractSession.java

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@
22

33
import java.util.IdentityHashMap;
44
import java.util.Map;
5+
import java.util.stream.Stream;
56

67
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
78
import ai.timefold.solver.core.impl.bavet.uni.AbstractForEachUniNode;
89
import ai.timefold.solver.core.impl.bavet.uni.AbstractForEachUniNode.LifecycleOperation;
10+
import ai.timefold.solver.core.impl.domain.variable.supply.SupplyManager;
911

10-
public abstract class AbstractSession {
12+
public abstract class AbstractSession implements AutoCloseable {
1113

1214
private final NodeNetwork nodeNetwork;
13-
private final Map<Class<?>, AbstractForEachUniNode<Object, Object>[]> initializeEffectiveClassToNodeArrayMap;
14-
private final Map<Class<?>, AbstractForEachUniNode<Object, Object>[]> insertEffectiveClassToNodeArrayMap;
15-
private final Map<Class<?>, AbstractForEachUniNode<Object, Object>[]> updateEffectiveClassToNodeArrayMap;
16-
private final Map<Class<?>, AbstractForEachUniNode<Object, Object>[]> retractEffectiveClassToNodeArrayMap;
15+
private final Map<Class<?>, AbstractForEachUniNode.InitializableForEachNode<Object>[]> initializeEffectiveClassToNodeArrayMap;
16+
private final Map<Class<?>, AbstractForEachUniNode<Object>[]> insertEffectiveClassToNodeArrayMap;
17+
private final Map<Class<?>, AbstractForEachUniNode<Object>[]> updateEffectiveClassToNodeArrayMap;
18+
private final Map<Class<?>, AbstractForEachUniNode<Object>[]> retractEffectiveClassToNodeArrayMap;
1719

1820
protected AbstractSession(NodeNetwork nodeNetwork) {
1921
this.nodeNetwork = nodeNetwork;
@@ -23,9 +25,9 @@ protected AbstractSession(NodeNetwork nodeNetwork) {
2325
this.retractEffectiveClassToNodeArrayMap = new IdentityHashMap<>(nodeNetwork.forEachNodeCount());
2426
}
2527

26-
public final void initialize(Object workingSolution) {
27-
for (var node : findNodes(PlanningSolution.class, LifecycleOperation.INITIALIZE)) {
28-
node.initialize(workingSolution);
28+
public final void initialize(Object workingSolution, SupplyManager supplyManager) {
29+
for (var node : findInitializableNodes()) {
30+
node.initialize(workingSolution, supplyManager);
2931
}
3032
}
3133

@@ -37,9 +39,8 @@ public final void insert(Object fact) {
3739
}
3840

3941
@SuppressWarnings("unchecked")
40-
private AbstractForEachUniNode<Object, Object>[] findNodes(Class<?> factClass, LifecycleOperation lifecycleOperation) {
42+
private AbstractForEachUniNode<Object>[] findNodes(Class<?> factClass, LifecycleOperation lifecycleOperation) {
4143
var effectiveClassToNodeArrayMap = switch (lifecycleOperation) {
42-
case INITIALIZE -> initializeEffectiveClassToNodeArrayMap;
4344
case INSERT -> insertEffectiveClassToNodeArrayMap;
4445
case UPDATE -> updateEffectiveClassToNodeArrayMap;
4546
case RETRACT -> retractEffectiveClassToNodeArrayMap;
@@ -55,6 +56,29 @@ private AbstractForEachUniNode<Object, Object>[] findNodes(Class<?> factClass, L
5556
return nodeArray;
5657
}
5758

59+
@SuppressWarnings("unchecked")
60+
private AbstractForEachUniNode.InitializableForEachNode<Object>[] findInitializableNodes() {
61+
// There will only be one solution class in the problem.
62+
// Therefore we do not need to know what it is, and using the annotation class will serve as a unique key.
63+
var factClass = PlanningSolution.class;
64+
var effectiveClassToNodeArrayMap = initializeEffectiveClassToNodeArrayMap;
65+
// Map.computeIfAbsent() would have created lambdas on the hot path, this will not.
66+
var nodeArray = effectiveClassToNodeArrayMap.get(factClass);
67+
if (nodeArray == null) {
68+
nodeArray = nodeNetwork.getForEachNodes(factClass)
69+
.flatMap(node -> {
70+
if (node instanceof AbstractForEachUniNode.InitializableForEachNode<?> initializableForEachNode) {
71+
return Stream.of(initializableForEachNode);
72+
} else {
73+
return Stream.empty();
74+
}
75+
})
76+
.toArray(AbstractForEachUniNode.InitializableForEachNode[]::new);
77+
effectiveClassToNodeArrayMap.put(factClass, nodeArray);
78+
}
79+
return nodeArray;
80+
}
81+
5882
public final void update(Object fact) {
5983
var factClass = fact.getClass();
6084
for (var node : findNodes(factClass, LifecycleOperation.UPDATE)) {
@@ -73,4 +97,13 @@ public void settle() {
7397
nodeNetwork.settle();
7498
}
7599

100+
@Override
101+
public final void close() {
102+
for (var node : findInitializableNodes()) {
103+
// Initializable nodes get a supply manager, fair to assume they will be demanding supplies.
104+
// Give them the opportunity to cancel those demands.
105+
node.close();
106+
}
107+
}
108+
76109
}

core/src/main/java/ai/timefold/solver/core/impl/bavet/NodeNetwork.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
* @param layeredNodes nodes grouped first by their layer, then by their index within the layer;
2020
* propagation needs to happen in this order.
2121
*/
22-
public record NodeNetwork(Map<Class<?>, List<AbstractForEachUniNode<?, ?>>> declaredClassToNodeMap,
22+
public record NodeNetwork(Map<Class<?>, List<AbstractForEachUniNode<?>>> declaredClassToNodeMap,
2323
Propagator[][] layeredNodes) {
2424

2525
public static final NodeNetwork EMPTY = new NodeNetwork(Map.of(), new Propagator[0][0]);
@@ -32,7 +32,7 @@ public int layerCount() {
3232
return layeredNodes.length;
3333
}
3434

35-
public Stream<AbstractForEachUniNode<?, ?>> getForEachNodes(Class<?> factClass) {
35+
public Stream<AbstractForEachUniNode<?>> getForEachNodes(Class<?> factClass) {
3636
// The node needs to match the fact, or the node needs to be applicable to the entire solution.
3737
// The latter is for FromSolution nodes.
3838
return declaredClassToNodeMap.entrySet()

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public void addNode(AbstractNode node, Stream_ creator) {
4747
public void addNode(AbstractNode node, Stream_ creator, Stream_ parent) {
4848
reversedNodeList.add(node);
4949
nodeCreatorMap.put(node, creator);
50-
if (!(node instanceof AbstractForEachUniNode<?, ?>)) {
50+
if (!(node instanceof AbstractForEachUniNode<?>)) {
5151
if (parent == null) {
5252
throw new IllegalStateException("Impossible state: The node (%s) has no parent (%s)."
5353
.formatted(node, parent));
@@ -148,7 +148,7 @@ public AbstractNode findParentNode(Stream_ childNodeCreator) {
148148
}
149149

150150
public static NodeNetwork buildNodeNetwork(List<AbstractNode> nodeList,
151-
Map<Class<?>, List<AbstractForEachUniNode<?, ?>>> declaredClassToNodeMap) {
151+
Map<Class<?>, List<AbstractForEachUniNode<?>>> declaredClassToNodeMap) {
152152
var layerMap = new TreeMap<Long, List<Propagator>>();
153153
for (var node : nodeList) {
154154
layerMap.computeIfAbsent(node.getLayerIndex(), k -> new ArrayList<>())
@@ -206,7 +206,7 @@ public <BuildHelper_ extends AbstractNodeBuildHelper<Stream_>> List<AbstractNode
206206
@SuppressWarnings("unchecked")
207207
private static <Stream_ extends BavetStream> long determineLayerIndex(AbstractNode node,
208208
AbstractNodeBuildHelper<Stream_> buildHelper) {
209-
if (node instanceof AbstractForEachUniNode<?, ?>) { // ForEach nodes, and only they, are in layer 0.
209+
if (node instanceof AbstractForEachUniNode<?>) { // ForEach nodes, and only they, are in layer 0.
210210
return 0;
211211
} else if (node instanceof AbstractTwoInputNode<?, ?> joinNode) {
212212
var nodeCreator = (BavetStreamBinaryOperation<?>) buildHelper.getNodeCreatingStream(joinNode);

core/src/main/java/ai/timefold/solver/core/impl/bavet/uni/AbstractForEachUniNode.java

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import ai.timefold.solver.core.impl.bavet.common.tuple.TupleLifecycle;
1010
import ai.timefold.solver.core.impl.bavet.common.tuple.TupleState;
1111
import ai.timefold.solver.core.impl.bavet.common.tuple.UniTuple;
12+
import ai.timefold.solver.core.impl.domain.variable.supply.SupplyManager;
1213

1314
import org.jspecify.annotations.NullMarked;
1415

@@ -21,9 +22,9 @@
2122
* @param <A>
2223
*/
2324
@NullMarked
24-
public abstract sealed class AbstractForEachUniNode<Solution_, A>
25+
public abstract sealed class AbstractForEachUniNode<A>
2526
extends AbstractNode
26-
permits ForEachExcludingUnassignedUniNode, ForEachIncludingUnassignedUniNode {
27+
permits ForEachExcludingUnassignedUniNode, ForEachExcludingPinnedUniNode, ForEachIncludingUnassignedUniNode {
2728

2829
private final Class<A> forEachClass;
2930
private final int outputStoreSize;
@@ -37,8 +38,6 @@ protected AbstractForEachUniNode(Class<A> forEachClass, TupleLifecycle<UniTuple<
3738
this.propagationQueue = new StaticPropagationQueue<>(nextNodesTupleLifecycle);
3839
}
3940

40-
public abstract void initialize(Solution_ workingSolution);
41-
4241
public void insert(A a) {
4342
var tuple = new UniTuple<>(a, outputStoreSize);
4443
var old = tupleMap.put(a, tuple);
@@ -115,10 +114,6 @@ public final String toString() {
115114
* on tuples within a node in Bavet.
116115
*/
117116
public enum LifecycleOperation {
118-
/**
119-
* Called when initializing a new working solution.
120-
*/
121-
INITIALIZE,
122117
/**
123118
* Represents the operation of inserting a new tuple into the node.
124119
* This operation is typically performed when a new fact is added to the working solution
@@ -140,4 +135,13 @@ public enum LifecycleOperation {
140135
RETRACT
141136
}
142137

138+
public interface InitializableForEachNode<Solution_> extends AutoCloseable {
139+
140+
void initialize(Solution_ workingSolution, SupplyManager supplyManager);
141+
142+
@Override
143+
void close(); // Drop the checked exception.
144+
145+
}
146+
143147
}

0 commit comments

Comments
 (0)