package eu.stratosphere.util.dag;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import eu.stratosphere.util.IdentityList;
/**
* Finds successive partitions of independent graph nodes and returns them in ascending order.<br>
* The partitions are organized level-wise in a way that all nodes of level X are only referenced from nodes of level
* X-1 or less.<br>
* Applying the partitioner to a DAG will result in all start nodes forming the first partition or level. All nodes that
* are referenced from the start nodes are partitioned next if there is no other input to these nodes.<br>
* <br>
* Example: given a graph with start nodes A, B and references A->C, B->D, and C->D will result in the three
* levels [A, B], [C], [D]. A and B are both independent from other nodes from the beginning. C only depends from A and
* thus from nodes of the first level. However, D depends additionally on C and thus can not be included in the second
* level.
*
* @author Arvid Heise
*/
public class GraphLevelPartitioner {
private static <Node> void gatherNodes(final List<Node> nodes, final ConnectionNavigator<Node> navigator,
final Node node) {
if (!nodes.contains(node))
if (node != null) {
nodes.add(node);
for (final Node child : navigator.getConnectedNodes(node))
gatherNodes(nodes, navigator, child);
}
}
/**
* Partitions the DAG given by the start nodes and all nodes reachable by the navigator.
*
* @param startNodes
* the start nodes
* @param navigator
* the navigator
* @param <Node>
* the type of the node
* @return the levels of the DAG
* @throws IllegalStateException
* if the graph contains cycles
*/
public static <Node> List<Level<Node>> getLevels(final Iterable<? extends Node> startNodes,
final ConnectionNavigator<Node> navigator) {
return getLevels(startNodes.iterator(), navigator);
}
/**
* Partitions the DAG given by the start nodes and all nodes reachable by the navigator.
*
* @param startNodes
* the start nodes
* @param navigator
* the navigator
* @param <Node>
* the type of the node
* @return the levels of the DAG
* @throws IllegalStateException
* if the graph contains cycles
*/
public static <Node> List<Level<Node>> getLevels(final Iterator<? extends Node> startNodes,
final ConnectionNavigator<Node> navigator) {
final List<Node> remainingNodes = new IdentityList<Node>();
while (startNodes.hasNext())
gatherNodes(remainingNodes, navigator, startNodes.next());
final List<Node> usedNodes = new IdentityList<Node>();
final List<Level<Node>> levels = new ArrayList<Level<Node>>();
while (!remainingNodes.isEmpty()) {
final List<Node> independentNodes = new ArrayList<Node>();
for (final Node node : remainingNodes)
if (isIndependent(node, usedNodes, navigator))
independentNodes.add(node);
if (independentNodes.isEmpty())
throw new IllegalStateException("graph does not have nodes without input");
levels.add(new Level<Node>(independentNodes, navigator));
remainingNodes.removeAll(independentNodes);
usedNodes.addAll(independentNodes);
}
return levels;
}
/**
* Partitions the DAG given by the start nodes and all nodes reachable by the navigator.
*
* @param startNodes
* the start nodes
* @param navigator
* the navigator
* @param <Node>
* the type of the node
* @return the levels of the DAG
* @throws IllegalStateException
* if the graph contains cycles
*/
public static <Node> List<Level<Node>> getLevels(final Node[] startNodes, final ConnectionNavigator<Node> navigator) {
return getLevels(Arrays.asList(startNodes).iterator(), navigator);
}
private static <Node> boolean isIndependent(final Node node, final Collection<Node> usedNodes,
final ConnectionNavigator<Node> navigator) {
for (final Object input : navigator.getConnectedNodes(node))
if (!usedNodes.contains(input))
return false;
return true;
}
/**
* A level contains only nodes that depend (are referenced from) on nodes of the previous levels.
*
* @author Arvid Heise
* @see GraphLevelPartitioner
* @param <Node>
* the type of the node
*/
public static class Level<Node> {
private final IdentityHashMap<Object, List<Object>> outgoings = new IdentityHashMap<Object, List<Object>>();
private final List<Node> levelNodes;
private Level(final List<Node> nodes, final ConnectionNavigator<Node> navigator) {
this.levelNodes = nodes;
// initializes all outgoing links
for (final Node node : nodes) {
final ArrayList<Object> links = new ArrayList<Object>();
for (final Object connectedNode : navigator.getConnectedNodes(node))
links.add(connectedNode);
this.outgoings.put(node, links);
}
}
/**
* Adds a node to this level at a specific index.
*
* @param index
* the index of the node after successful insertion
* @param node
* the node to add
* @throws IndexOutOfBoundsException
* if the index is out of bounds
*/
public void add(final int index, final Node node) {
this.levelNodes.add(index, node);
this.outgoings.put(node, new ArrayList<Object>());
}
/**
* Adds a node to this level
*
* @param node
* the node to add
*/
public void add(final Node node) {
this.levelNodes.add(node);
this.outgoings.put(node, new ArrayList<Object>());
}
/**
* Returns all nodes of this level.
*
* @return all nodes in this level
*/
public List<Node> getLevelNodes() {
return this.levelNodes;
}
/**
* Returns all outgoing links from a node of this level.
*
* @param node
* the node
* @return all outgoing links
*/
public List<Object> getLinks(final Node node) {
return this.outgoings.get(node);
}
@Override
public String toString() {
return this.levelNodes.toString();
}
/**
* Updates a link from the given node to a given load. The update is only reflected in this virtual level and
* the actual graph remains untouched.
*
* @param fromNode
* the node from which the link originates
* @param oldTarget
* the old node to which the link goes or null if the newTarget should only be added
* @param newTarget
* the new target of the node
*/
public void updateLink(final Object fromNode, final Object oldTarget, final Object newTarget) {
if (oldTarget == null)
this.outgoings.get(fromNode).add(newTarget);
else
this.outgoings.get(fromNode).set(this.outgoings.get(fromNode).indexOf(oldTarget), newTarget);
}
}
}