using System.Collections.Generic;
using System.Linq;
using NodeNetwork.ViewModels;
namespace NodeNetwork.Toolkit
{
///
/// This class is a collection of various graph algoritms.
///
public class GraphAlgorithms
{
/*
* Algorithm:
* 1. Pick next ready node from the nodes-to-check list, set as current
*
* a. Mark current node as busy
* b. For each input of the current node, find the connected node
* c. Check connected node state
* -> if ready
* => set connected node as current and goto a
* -> if busy
* => recursion found!
* => mark all busy nodes as broken!
* => mark all nodes connected to outputs of broken nodes as broken
* => goto 1
* d. Mark current node as ready
*
* 2. Goto 1
*/
private enum NodeState
{
Ready,
Busy,
Error
}
///
/// Searches for loops in a network.
/// A loop is a connection sequence that starts and ends at the same node.
///
/// the network to search for loops.
/// an enumeration of connections involved in loops
public static IEnumerable FindLoops(NetworkViewModel network)
{
Stack nodesToCheck = new Stack(network.Nodes.Items);
Dictionary nodeStates = new Dictionary(nodesToCheck.Count);
while (nodesToCheck.Count > 0)
{
NodeViewModel currentNode = nodesToCheck.Peek();
NodeState state;
if (!nodeStates.TryGetValue(currentNode, out state))
{
state = NodeState.Ready;
}
if (state == NodeState.Error)
{
nodesToCheck.Pop();
continue;
}
ConnectionViewModel recursiveConnection = FindLoops(nodeStates, currentNode);
if (recursiveConnection != null)
{
yield return recursiveConnection;
}
nodesToCheck.Pop();
}
}
private static ConnectionViewModel FindLoops(Dictionary nodeStates, NodeViewModel node)
{
nodeStates[node] = NodeState.Busy;
//Get the nodes connected to the inputs of node and check their state
//If they are Ready, check them recursively.
//If they are Busy, we found recursion.
//If they are Error, the node was already found to be part of a loop and so we ignore it.
List nodesToCheck = new List();
foreach (NodeInputViewModel input in node.Inputs.Items)
{
foreach (ConnectionViewModel con in input.Connections.Items)
{
NodeViewModel connectedNode = con.Output.Parent;
if (!nodeStates.TryGetValue(connectedNode, out var connectedNodeState))
{
connectedNodeState = NodeState.Ready;
}
if (connectedNodeState == NodeState.Ready)
{
nodesToCheck.Add(con);
}
else if (connectedNodeState == NodeState.Busy)
{
//Found recursion!
List keys = new List(nodeStates.Keys);
foreach (NodeViewModel cur in keys)
{
if (nodeStates[cur] == NodeState.Busy)
{
nodeStates[cur] = NodeState.Error;
}
}
return con;
}
else if (connectedNodeState == NodeState.Error)
{
//connected node is already marked with error state, no further action required
}
}
}
foreach (ConnectionViewModel con in nodesToCheck)
{
NodeViewModel currentNode = con.Output.Parent;
ConnectionViewModel result = FindLoops(nodeStates, currentNode);
if (result != null)
{
return result;
}
}
nodeStates[node] = NodeState.Ready;
return null;
}
///
/// Returns the nodes connected to the starting node, then the nodes connected to those nodes, ... and so on.
/// If the subgraph that contains the starting nodes has a loop, then this function will keep producing the values in the loop.
/// A call to FindLoops is recommended before using this function
///
/// The node from which to branch out
/// Include nodes connected through node inputs?
/// Include nodes connected through node outputs?
/// Include the starting node? (will be first)
/// An enumeration of the nodes connected to the starting node.
public static IEnumerable GetConnectedNodesTunneling(NodeViewModel startingNode, bool includeInputs = true, bool includeOutputs = false, bool includeSelf = false)
{
if (includeSelf)
{
yield return startingNode;
}
if (includeInputs)
{
IEnumerable inputNodes = startingNode.Inputs.Items.SelectMany(i => i.Connections.Items).Select(c => c.Output.Parent);
foreach (NodeViewModel nodeVM in inputNodes)
{
foreach (NodeViewModel subNodeVM in GetConnectedNodesTunneling(nodeVM, includeInputs, includeOutputs, true))
{
if (subNodeVM != startingNode)
{
yield return subNodeVM;
}
}
}
}
if (includeOutputs)
{
IEnumerable outputNodes = startingNode.Outputs.Items.SelectMany(i => i.Connections.Items).Select(c => c.Input.Parent);
foreach (NodeViewModel nodeVM in outputNodes)
{
foreach (NodeViewModel subNodeVM in GetConnectedNodesTunneling(nodeVM, includeInputs, includeOutputs, true))
{
if (subNodeVM != startingNode)
{
yield return subNodeVM;
}
}
}
}
}
///
/// Similar to GetConnectedNodesTunneling, but returns the outermost nodes first.
/// If the subgraph that contains the starting nodes has a loop, then this function will never return.
/// A call to FindLoops is recommended before using this function
///
public static IEnumerable GetConnectedNodesBubbling(NodeViewModel startingNode, bool includeInputs = true, bool includeOutputs = false, bool includeSelf = false)
{
return GetConnectedNodesTunneling(startingNode, includeInputs, includeOutputs, includeSelf).Reverse();
}
///
/// Returns the starting nodes in the network.
/// Starting nodes are nodes that do not have inputs connected to an output.
///
/// The network to find starting nodes in
/// An enumerable of starting nodes
public static IEnumerable FindStartingNodes(NetworkViewModel network)
{
return FindStartingNodes(network.Nodes.Items);
}
///
/// Returns the starting nodes in the node group.
/// Starting nodes are nodes that do not have inputs connected to an output of a node in the group.
///
public static IEnumerable FindStartingNodes(IEnumerable nodeGroup)
{
Queue todo = new Queue(nodeGroup);
HashSet nodes = new HashSet(todo);
while (todo.Count > 0)
{
NodeViewModel cur = todo.Dequeue();
bool hasInputConnection = false;
foreach (NodeInputViewModel input in cur.Inputs.Items)
{
if (input.Connections.Items.Any(c => nodes.Contains(c.Output.Parent)))
{
hasInputConnection = true;
break;
}
}
if (!hasInputConnection)
{
yield return cur;
}
}
}
///
/// Takes the provided set of nodes and returns the nodes are connected to the source node, directly or indirectly.
/// This method uses breadth-first search and keeps track of visited nodes, so it can handle networks with loops.
///
/// The node from which the search for connected nodes starts
///
/// The nodes to look for when searching.
/// If this set contains the sourcenode, the first item returned will be the source node.
///
/// An enumeration of connected nodes
public static IEnumerable FindConnectedNodes(NodeViewModel sourceNode, IEnumerable nodes)
{
HashSet nodesSet = new HashSet(nodes);
HashSet visitedNodes = new HashSet();
Queue nodeQueue = new Queue();
nodeQueue.Enqueue(sourceNode);
while (nodeQueue.Count > 0)
{
NodeViewModel node = nodeQueue.Dequeue();
if (nodesSet.Remove(node))
{
yield return node;
if (nodesSet.Count == 0)
{
yield break;
}
}
foreach (NodeInputViewModel input in node.Inputs.Items)
{
foreach (NodeViewModel connectedNode in input.Connections.Items.Select(c => c.Output.Parent))
{
if (visitedNodes.Add(connectedNode))
{
nodeQueue.Enqueue(connectedNode);
}
}
}
foreach (NodeOutputViewModel output in node.Outputs.Items)
{
foreach (NodeViewModel connectedNode in output.Connections.Items.Select(c => c.Input.Parent))
{
if (visitedNodes.Add(connectedNode))
{
nodeQueue.Enqueue(connectedNode);
}
}
}
}
}
///
/// Takes the provided set of nodes and groups these nodes in sets that are connected, directly or indirectly.
/// Because this method uses FindConnectedNodes, it is capable of handling networks with loops.
///
/// the nodes to group into sets
public static IEnumerable> FindSubGraphs(IEnumerable nodes)
{
HashSet nodesSet = new HashSet(nodes);
while (nodesSet.Count > 0)
{
NodeViewModel curNode = nodesSet.First();
List subGraphMembers = new List(FindConnectedNodes(curNode, nodesSet));
foreach (NodeViewModel subGraphMember in subGraphMembers)
{
nodesSet.Remove(subGraphMember);
}
yield return subGraphMembers;
}
}
///
/// Returns true if the given set of nodes form continuous subgraphs.
/// The given set of nodes is split into subgraphs based on the connections between the nodes.
/// If for each subgraph it is true that all nodes of the subgraph are in the provided set, then true is returned.
/// Otherwise false is returned.
/// Because this method uses FindSubGraphs, it is capable of handling networks with loops.
///
public static bool IsContinuousSubGraphSet(HashSet nodesInSubGraphSet)
{
return FindSubGraphs(FindStartingNodes(nodesInSubGraphSet))
.All(subGraph => IsContinuousSubGroup(nodesInSubGraphSet, subGraph));
}
public static bool IsContinuousSubGraphSet(IEnumerable nodesInSubGraphSet)
=> IsContinuousSubGraphSet(new HashSet(nodesInSubGraphSet));
private static bool IsContinuousSubGroup(HashSet groupNodesSet, IEnumerable subGraphStartingNodes)
{
Queue queue = new Queue(subGraphStartingNodes);
HashSet visitedNodes = new HashSet(queue);
//Transitions from inside to outside to inside the group are not allowed in a continuous group.
//Since we start from the starting nodes of the current subgroup, which are inside,
//we only need to check for a transitions from outside to inside.
while (queue.Count > 0)
{
NodeViewModel cur = queue.Dequeue();
foreach (NodeOutputViewModel output in cur.Outputs.Items)
{
foreach (ConnectionViewModel con in output.Connections.Items)
{
NodeViewModel connectedNode = con.Input.Parent;
if (groupNodesSet.Contains(connectedNode) && !groupNodesSet.Contains(cur))
{
//Found transision from outside to inside
return false;
}
if (!visitedNodes.Add(connectedNode))
{
continue;
}
queue.Enqueue(connectedNode);
}
}
}
return true;
}
}
}