Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,9 @@ public int compareTo(ConstraintRef other) {
return id.compareTo(other.id);
}

@Override
public String toString() {
return id;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package ai.timefold.solver.core.impl.bavet;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Function;
import java.util.stream.Stream;

import ai.timefold.solver.core.impl.bavet.common.AbstractNode;
import ai.timefold.solver.core.impl.bavet.common.AbstractRootNode;
import ai.timefold.solver.core.impl.bavet.common.AbstractTwoInputNode;
import ai.timefold.solver.core.impl.bavet.common.Propagator;
import ai.timefold.solver.core.impl.bavet.common.tuple.ActivitySupport;

import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

/**
* Represents Bavet's network of nodes, specific to a particular session.
*/
@NullMarked
public abstract class AbstractBavetNodeNetwork {

protected static AbstractNode[][] buildLayeredNodes(List<AbstractNode> nodeList) {
var layerMap = new TreeMap<Long, List<AbstractNode>>();
nodeList.forEach(node -> layerMap.computeIfAbsent(node.getLayerIndex(), unused -> new ArrayList<>()).add(node));
var layerCount = layerMap.size();
var layeredNodes = new AbstractNode[layerCount][];
for (var i = 0; i < layerCount; i++) {
var layer = layerMap.get((long) i);
layeredNodes[i] = layer.toArray(new AbstractNode[0]);
}
return layeredNodes;
}

private final Map<Class<?>, List<AbstractRootNode<?>>> declaredClassToNodeMap;

private final AbstractNode[][] layeredNodes;
private final Function<AbstractNode, Propagator> propagatorFunction;
/**
* A subset of {@code layeredNodes}.
* Once non-null, only contains propagators of nodes which are active.
* See {@link ActivitySupport#isActive()} for details.
*/
private Propagator @Nullable [][] layeredActivePropagators;
/**
* For testing only: the set of nodes that remained active after {@link #settle()}; null before settle.
*/
private @Nullable Set<AbstractNode> activeNodeSet;

/**
* @param declaredClassToNodeMap starting nodes, one for each class used in the constraints;
* root nodes, layer index 0.
* @param layeredNodes nodes grouped first by their layer, then by their index within the layer;
* propagation needs to happen in this order.
*/
protected AbstractBavetNodeNetwork(Map<Class<?>, List<AbstractRootNode<?>>> declaredClassToNodeMap,
AbstractNode[][] layeredNodes, Function<AbstractNode, Propagator> propagatorFunction) {
this.declaredClassToNodeMap = declaredClassToNodeMap;
this.layeredNodes = layeredNodes;
this.propagatorFunction = propagatorFunction;
}

public int forEachNodeCount() {
return declaredClassToNodeMap.size();
}

/**
*
* @param factClass
* @return if {@link #isActivationCheckComplete()} is true, only returns active root nodes;
* otherwise returns all root nodes.
* This means that if this information was ever read before activation checks were complete,
* it should be re-read after to make sure no inactive nodes are included.
*/
public Stream<AbstractRootNode<?>> getRootNodesAcceptingType(Class<?> factClass) {
return declaredClassToNodeMap.entrySet().stream()
.flatMap(entry -> entry.getValue().stream())
.filter(tupleSourceRoot -> tupleSourceRoot.allowsInstancesOf(factClass))
.filter(node -> !isActivationCheckComplete() || activeNodeSet.contains(node));
}

public void settle() {
if (layeredActivePropagators == null) {
// Remove inactive nodes and settle the layers in one go.
var initializedRootNodes = Collections.newSetFromMap(new IdentityHashMap<>());
declaredClassToNodeMap.forEach((declaredClass, rootNodes) -> rootNodes.forEach(rootNode -> {
if (initializedRootNodes.add(rootNode)) {
// Ensure one initialization per node.
// Root nodes are filled from a session, which can always produce.
rootNode.afterAllFactsInserted(true);
}
}));

var activeNodes = Collections.<AbstractNode> newSetFromMap(new IdentityHashMap<>());
layeredActivePropagators = Arrays.stream(layeredNodes)
.map(layer -> Arrays.stream(layer)
.filter(s -> switch (s) {
case ActivitySupport activityEnabled -> activityEnabled.isActive();
case AbstractTwoInputNode<?, ?> twoInputNode -> twoInputNode.isActive();
})
.peek(activeNodes::add)

Check warning on line 108 in core/src/main/java/ai/timefold/solver/core/impl/bavet/AbstractBavetNodeNetwork.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this use of "Stream.peek".

See more on https://sonarcloud.io/project/issues?id=ai.timefold%3Atimefold-solver&issues=AZ6g93wCr9478Y5ejaN1&open=AZ6g93wCr9478Y5ejaN1&pullRequest=2337
.map(propagatorFunction).toArray(Propagator[]::new))
.filter(layer -> layer.length > 0).peek(AbstractBavetNodeNetwork::settleLayer).toArray(Propagator[][]::new);

Check warning on line 110 in core/src/main/java/ai/timefold/solver/core/impl/bavet/AbstractBavetNodeNetwork.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this use of "Stream.peek".

See more on https://sonarcloud.io/project/issues?id=ai.timefold%3Atimefold-solver&issues=AZ6mSvTc-CL5mO8zLDlO&open=AZ6mSvTc-CL5mO8zLDlO&pullRequest=2337
this.activeNodeSet = activeNodes;
return;
}
// Simplified loop when the layers were already trimmed.
for (var layer : layeredActivePropagators) {
settleLayer(layer);
}
}

public boolean isActivationCheckComplete() {
return layeredActivePropagators != null;
}

Set<AbstractNode> getActiveNodes() {
if (activeNodeSet == null) {
throw new IllegalStateException("Impossible state: getActiveNodes() called before settle().");
}
return activeNodeSet;
}

/**
* For testing only. All nodes in the network, regardless of activity.
*/
List<AbstractNode> getNodes() {
return Arrays.stream(layeredNodes).flatMap(Arrays::stream).toList();
}

private static void settleLayer(Propagator[] nodesInLayer) {
if (nodesInLayer.length == 1) { // Avoid iteration.
nodesInLayer[0].propagateEverything();
} else {
for (var node : nodesInLayer) {
node.propagateRetracts();
}
for (var node : nodesInLayer) {
node.propagateUpdates();
}
for (var node : nodesInLayer) {
node.propagateInserts();
}
}
}

@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof AbstractBavetNodeNetwork that))
return false;
return Objects.equals(declaredClassToNodeMap, that.declaredClassToNodeMap)
&& Objects.deepEquals(layeredNodes, that.layeredNodes);
}

@Override
public int hashCode() {
return Objects.hash(declaredClassToNodeMap, Arrays.deepHashCode(layeredNodes));
}

@Override
public String toString() {
return "%s with %d forEach nodes.".formatted(getClass().getSimpleName(), forEachNodeCount());
}

}
Original file line number Diff line number Diff line change
@@ -1,36 +1,38 @@
package ai.timefold.solver.core.impl.bavet;

import java.util.IdentityHashMap;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import ai.timefold.solver.core.impl.bavet.common.BavetRootNode;
import ai.timefold.solver.core.impl.bavet.common.BavetRootNode.LifecycleOperation;
import ai.timefold.solver.core.impl.bavet.common.AbstractRootNode;
import ai.timefold.solver.core.impl.bavet.common.AbstractRootNode.LifecycleOperation;

public abstract class AbstractSession {
public abstract class AbstractSession<Network_ extends AbstractBavetNodeNetwork> {

private final NodeNetwork nodeNetwork;
private final Map<Class<?>, BavetRootNode<Object>[]> insertEffectiveClassToNodeArrayMap;
private final Map<Class<?>, BavetRootNode<Object>[]> updateEffectiveClassToNodeArrayMap;
private final Map<Class<?>, BavetRootNode<Object>[]> retractEffectiveClassToNodeArrayMap;
private boolean settled = true;
protected final Network_ nodeNetwork;
private final Map<Class<?>, AbstractRootNode<Object>[]> insertEffectiveClassToNodeArrayMap;
private final Map<Class<?>, AbstractRootNode<Object>[]> updateEffectiveClassToNodeArrayMap;
private final Map<Class<?>, AbstractRootNode<Object>[]> retractEffectiveClassToNodeArrayMap;
private boolean initialized = false;
private boolean settled = false;

protected AbstractSession(NodeNetwork nodeNetwork) {
protected AbstractSession(Network_ nodeNetwork) {
this.nodeNetwork = nodeNetwork;
this.insertEffectiveClassToNodeArrayMap = new IdentityHashMap<>(nodeNetwork.forEachNodeCount());
this.updateEffectiveClassToNodeArrayMap = new IdentityHashMap<>(nodeNetwork.forEachNodeCount());
this.retractEffectiveClassToNodeArrayMap = new IdentityHashMap<>(nodeNetwork.forEachNodeCount());
this.insertEffectiveClassToNodeArrayMap = HashMap.newHashMap(nodeNetwork.forEachNodeCount());
this.updateEffectiveClassToNodeArrayMap = HashMap.newHashMap(nodeNetwork.forEachNodeCount());
this.retractEffectiveClassToNodeArrayMap = HashMap.newHashMap(nodeNetwork.forEachNodeCount());
}

public final void insert(Object fact) {
settled = false;
var factClass = fact.getClass();
for (var node : findNodes(factClass, BavetRootNode.LifecycleOperation.INSERT)) {
for (var node : findNodes(factClass, AbstractRootNode.LifecycleOperation.INSERT)) {
node.insert(fact);
}
}

@SuppressWarnings("unchecked")
private BavetRootNode<Object>[] findNodes(Class<?> factClass, LifecycleOperation lifecycleOperation) {
private AbstractRootNode<Object>[] findNodes(Class<?> factClass, LifecycleOperation lifecycleOperation) {
var effectiveClassToNodeArrayMap = switch (lifecycleOperation) {
case INSERT -> insertEffectiveClassToNodeArrayMap;
case UPDATE -> updateEffectiveClassToNodeArrayMap;
Expand All @@ -41,7 +43,7 @@ private BavetRootNode<Object>[] findNodes(Class<?> factClass, LifecycleOperation
if (nodeArray == null) {
nodeArray = nodeNetwork.getRootNodesAcceptingType(factClass)
.filter(node -> node.supports(lifecycleOperation))
.toArray(BavetRootNode[]::new);
.toArray(AbstractRootNode[]::new);
effectiveClassToNodeArrayMap.put(factClass, nodeArray);
Comment thread
triceo marked this conversation as resolved.
}
return nodeArray;
Expand All @@ -50,15 +52,15 @@ private BavetRootNode<Object>[] findNodes(Class<?> factClass, LifecycleOperation
public final void update(Object fact) {
settled = false;
var factClass = fact.getClass();
for (var node : findNodes(factClass, BavetRootNode.LifecycleOperation.UPDATE)) {
for (var node : findNodes(factClass, AbstractRootNode.LifecycleOperation.UPDATE)) {
node.update(fact);
}
}

public final void retract(Object fact) {
settled = false;
var factClass = fact.getClass();
for (var node : findNodes(factClass, BavetRootNode.LifecycleOperation.RETRACT)) {
for (var node : findNodes(factClass, AbstractRootNode.LifecycleOperation.RETRACT)) {
node.retract(fact);
}
}
Expand All @@ -68,11 +70,23 @@ public final void settle() {
return;
}
nodeNetwork.settle();
if (!initialized && nodeNetwork.isActivationCheckComplete()) {
removeInactiveRootNodes(insertEffectiveClassToNodeArrayMap);
removeInactiveRootNodes(updateEffectiveClassToNodeArrayMap);
removeInactiveRootNodes(retractEffectiveClassToNodeArrayMap);
initialized = true;
}
settled = true;
}

public final void summarizeProfileIfPresent() {
nodeNetwork.summarizeProfileIfPresent();
private void removeInactiveRootNodes(Map<Class<?>, AbstractRootNode<Object>[]> effectiveClassToNodeArrayMap) {
// Use getActiveNodes() for this, to not rerun the activity checking logic again.
effectiveClassToNodeArrayMap.replaceAll((k, v) -> Arrays.stream(v)
.filter(nodeNetwork.getActiveNodes()::contains)
.toArray(AbstractRootNode[]::new));
}

public Network_ getNodeNetwork() {
return nodeNetwork;
}
}

This file was deleted.

Loading
Loading