819 lines
28 KiB
Python
819 lines
28 KiB
Python
# pylint: disable=all
|
|
# flake8: noqa
|
|
|
|
# Copied from original Odoo code:
|
|
# https://github.com/odoo/odoo/blob/3e8b930b18b53e0d5cce3170d8c0f5cd8c7f4f6e/odoo/tools/graph.py
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import operator
|
|
import math
|
|
|
|
from odoo import exceptions, _
|
|
|
|
# Limit the max number of iterations in computation of graph.
|
|
GUARD_COUNTER_LIMIT = 20000
|
|
|
|
|
|
class Graph:
|
|
def __init__(self, nodes, transitions, no_ancester=None):
|
|
"""Initialize graph's object
|
|
@param nodes list of ids of nodes in the graph
|
|
@param transitions list of edges in the graph in the form
|
|
(source_node, destination_node)
|
|
@param no_ancester list of nodes with no incoming edges
|
|
"""
|
|
|
|
self.nodes = nodes or []
|
|
self.edges = transitions or []
|
|
self.no_ancester = no_ancester or {}
|
|
trans = {}
|
|
|
|
for t in transitions:
|
|
trans.setdefault(t[0], [])
|
|
trans[t[0]].append(t[1])
|
|
self.transitions = trans
|
|
self.result = {}
|
|
|
|
def init_rank(self):
|
|
""" Computes rank of the nodes of the graph by finding
|
|
initial feasible tree
|
|
"""
|
|
self.edge_wt = {}
|
|
for link in self.links:
|
|
self.edge_wt[link] = (
|
|
self.result[link[1]]['x'] - self.result[link[0]]['x'])
|
|
|
|
tot_node = len(self.partial_order)
|
|
# do until all the nodes in the component are searched
|
|
g_counter = 1
|
|
while self.tight_tree() < tot_node and g_counter < GUARD_COUNTER_LIMIT:
|
|
g_counter += 1
|
|
if g_counter >= GUARD_COUNTER_LIMIT:
|
|
raise exceptions.UserError(_(
|
|
"Cannot compute diagram view. "
|
|
"It seems that the flow is incorrect! "
|
|
"Try to remove some broken routes without using "
|
|
"diagram view."
|
|
))
|
|
list_node = []
|
|
list_edge = []
|
|
|
|
for node in self.nodes:
|
|
if node not in self.reachable_nodes:
|
|
list_node.append(node)
|
|
|
|
for edge in self.edge_wt:
|
|
if edge not in self.tree_edges:
|
|
list_edge.append(edge)
|
|
|
|
slack = 100
|
|
|
|
for edge in list_edge:
|
|
if ((edge[0] in self.reachable_nodes and edge[1] not in self.reachable_nodes) or
|
|
(edge[1] in self.reachable_nodes and edge[0] not in self.reachable_nodes)):
|
|
if slack > self.edge_wt[edge] - 1:
|
|
slack = self.edge_wt[edge] - 1
|
|
new_edge = edge
|
|
|
|
if new_edge[0] not in self.reachable_nodes:
|
|
delta = -(self.edge_wt[new_edge] - 1)
|
|
else:
|
|
delta = self.edge_wt[new_edge] - 1
|
|
|
|
for node in self.result:
|
|
if node in self.reachable_nodes:
|
|
self.result[node]['x'] += delta
|
|
|
|
for edge in self.edge_wt:
|
|
self.edge_wt[edge] = (
|
|
self.result[edge[1]]['x'] - self.result[edge[0]]['x'])
|
|
|
|
self.init_cutvalues()
|
|
|
|
def tight_tree(self):
|
|
self.reachable_nodes = []
|
|
self.tree_edges = []
|
|
self.reachable_node(self.start)
|
|
return len(self.reachable_nodes)
|
|
|
|
def reachable_node(self, node):
|
|
""" Find the nodes of the graph which are only 1 rank apart
|
|
from each other
|
|
"""
|
|
if node not in self.reachable_nodes:
|
|
self.reachable_nodes.append(node)
|
|
for edge in self.edge_wt:
|
|
if edge[0] == node:
|
|
if self.edge_wt[edge] == 1:
|
|
self.tree_edges.append(edge)
|
|
if edge[1] not in self.reachable_nodes:
|
|
self.reachable_nodes.append(edge[1])
|
|
self.reachable_node(edge[1])
|
|
|
|
def init_cutvalues(self):
|
|
""" Initialize cut values of edges of the feasible tree.
|
|
Edges with negative cut-values are removed from the tree
|
|
to optimize rank assignment
|
|
"""
|
|
self.cut_edges = {}
|
|
self.head_nodes = []
|
|
i = 0
|
|
|
|
for edge in self.tree_edges:
|
|
self.head_nodes = []
|
|
rest_edges = []
|
|
rest_edges += self.tree_edges
|
|
del rest_edges[i]
|
|
self.head_component(self.start, rest_edges)
|
|
i += 1
|
|
positive = 0
|
|
negative = 0
|
|
for source_node in self.transitions:
|
|
if source_node in self.head_nodes:
|
|
for dest_node in self.transitions[source_node]:
|
|
if dest_node not in self.head_nodes:
|
|
negative += 1
|
|
else:
|
|
for dest_node in self.transitions[source_node]:
|
|
if dest_node in self.head_nodes:
|
|
positive += 1
|
|
|
|
self.cut_edges[edge] = positive - negative
|
|
|
|
def head_component(self, node, rest_edges):
|
|
""" Find nodes which are reachable from the starting node,
|
|
after removing an edge
|
|
"""
|
|
if node not in self.head_nodes:
|
|
self.head_nodes.append(node)
|
|
|
|
for edge in rest_edges:
|
|
if edge[0] == node:
|
|
self.head_component(edge[1], rest_edges)
|
|
|
|
def process_ranking(self, node, level=0):
|
|
""" Computes initial feasible ranking after making graph acyclic
|
|
with depth-first search
|
|
"""
|
|
if node not in self.result:
|
|
self.result[node] = {'y': None, 'x': level, 'mark': 0}
|
|
else:
|
|
if level > self.result[node]['x']:
|
|
self.result[node]['x'] = level
|
|
|
|
if self.result[node]['mark']==0:
|
|
self.result[node]['mark'] = 1
|
|
for sec_end in self.transitions.get(node, []):
|
|
self.process_ranking(sec_end, level + 1)
|
|
|
|
def make_acyclic(self, parent, node, level, tree):
|
|
""" Computes Partial-order of the nodes with depth-first search
|
|
"""
|
|
if node not in self.partial_order:
|
|
self.partial_order[node] = {'level': level, 'mark': 0}
|
|
if parent:
|
|
tree.append((parent, node))
|
|
|
|
if self.partial_order[node]['mark'] == 0:
|
|
self.partial_order[node]['mark'] = 1
|
|
for sec_end in self.transitions.get(node, []):
|
|
self.links.append((node, sec_end))
|
|
self.make_acyclic(node, sec_end, level + 1, tree)
|
|
|
|
return tree
|
|
|
|
def rev_edges(self, tree):
|
|
""" reverse the direction of the edges whose
|
|
source-node-partail_order > destination-node-partail_order
|
|
to make the graph acyclic
|
|
"""
|
|
is_cyclic = False
|
|
i = 0
|
|
# Note, here 'links' is modified, thus there is not possible
|
|
# to use enumerate
|
|
# TODO: may be it have sense to rewrite without modification
|
|
# of self.links?
|
|
for link in self.links:
|
|
src = link[0]
|
|
des = link[1]
|
|
edge_len = self.partial_order[des]['level'] - self.partial_order[src]['level']
|
|
if edge_len < 0:
|
|
del self.links[i]
|
|
self.links.insert(i, (des, src))
|
|
self.transitions[src].remove(des)
|
|
self.transitions.setdefault(des, []).append(src)
|
|
is_cyclic = True
|
|
elif math.fabs(edge_len) > 1:
|
|
is_cyclic = True
|
|
i += 1
|
|
|
|
return is_cyclic
|
|
|
|
def exchange(self, e, f):
|
|
""" Exchange edges to make feasible-tree optimized
|
|
|
|
:param e: edge with negative cut-value
|
|
:param f: new edge with minimum slack-value
|
|
"""
|
|
del self.tree_edges[self.tree_edges.index(e)]
|
|
self.tree_edges.append(f)
|
|
self.init_cutvalues()
|
|
|
|
def enter_edge(self, edge):
|
|
""" Finds a new_edge with minimum slack value to replace an edge with
|
|
negative cut-value
|
|
|
|
@param edge edge with negative cut-value
|
|
"""
|
|
self.head_nodes = []
|
|
rest_edges = []
|
|
rest_edges += self.tree_edges
|
|
del rest_edges[rest_edges.index(edge)]
|
|
self.head_component(self.start, rest_edges)
|
|
|
|
if edge[1] in self.head_nodes:
|
|
lst_h_nodes = []
|
|
for node in self.result:
|
|
if node not in self.head_nodes:
|
|
lst_h_nodes.append(node)
|
|
self.head_nodes = lst_h_nodes
|
|
|
|
slack = 100
|
|
new_edge = edge
|
|
for source_node in self.transitions:
|
|
if source_node in self.head_nodes:
|
|
for dest_node in self.transitions[source_node]:
|
|
if dest_node not in self.head_nodes:
|
|
if slack > (self.edge_wt[edge] - 1):
|
|
slack = self.edge_wt[edge] - 1
|
|
new_edge = (source_node, dest_node)
|
|
|
|
return new_edge
|
|
|
|
def leave_edge(self):
|
|
"""Returns the edge with negative cut_value(if exists)
|
|
"""
|
|
if self.critical_edges:
|
|
for edge in self.critical_edges:
|
|
self.cut_edges[edge] = 0
|
|
|
|
for edge in self.cut_edges:
|
|
if self.cut_edges[edge] < 0:
|
|
return edge
|
|
|
|
return None
|
|
|
|
def finalize_rank(self, node, level):
|
|
self.result[node]['x'] = level
|
|
for destination in self.optimal_edges.get(node, []):
|
|
self.finalize_rank(destination, level+1)
|
|
|
|
def normalize(self):
|
|
"""The ranks are normalized by setting the least rank to zero.
|
|
"""
|
|
least_rank = min(x['x'] for x in self.result.values())
|
|
|
|
if least_rank != 0:
|
|
for node in self.result:
|
|
self.result[node]['x'] -= least_rank
|
|
|
|
def make_chain(self):
|
|
""" Edges between nodes more than one rank apart are replaced by
|
|
chains of unit length edges between temporary nodes.
|
|
"""
|
|
for edge in self.edge_wt:
|
|
if self.edge_wt[edge] > 1:
|
|
self.transitions[edge[0]].remove(edge[1])
|
|
start = self.result[edge[0]]['x']
|
|
end = self.result[edge[1]]['x']
|
|
|
|
for rank in range(start+1, end):
|
|
if not self.result.get((rank, 'temp'), False):
|
|
self.result[(rank, 'temp')] = {
|
|
'y': None,
|
|
'x': rank,
|
|
'mark': 0,
|
|
}
|
|
|
|
for rank in range(start, end):
|
|
if start == rank:
|
|
self.transitions[edge[0]].append((rank + 1, 'temp'))
|
|
elif rank == end-1:
|
|
self.transitions.setdefault(
|
|
(rank, 'temp'), []).append(edge[1])
|
|
else:
|
|
self.transitions.setdefault(
|
|
(rank, 'temp'), []).append((rank + 1, 'temp'))
|
|
|
|
def init_order(self, node, level):
|
|
"""Initialize orders the nodes in each rank with depth-first search
|
|
"""
|
|
if not self.result[node]['y']:
|
|
self.result[node]['y'] = self.order[level]
|
|
self.order[level] += 1
|
|
|
|
for sec_end in self.transitions.get(node, []):
|
|
if node != sec_end:
|
|
self.init_order(sec_end, self.result[sec_end]['x'])
|
|
|
|
def order_heuristic(self):
|
|
for __ in range(12):
|
|
self.wmedian()
|
|
|
|
def wmedian(self):
|
|
""" Applies median heuristic to find optimized order of the nodes
|
|
within their ranks
|
|
"""
|
|
for level in self.levels:
|
|
|
|
node_median = []
|
|
nodes = self.levels[level]
|
|
for node in nodes:
|
|
node_median.append((node, self.median_value(node, level-1)))
|
|
|
|
sort_list = sorted(node_median, key=operator.itemgetter(1))
|
|
|
|
# Compute new list of nodes for this level (sorted by median value)
|
|
self.levels[level] = [t[0] for t in sort_list]
|
|
|
|
order = 0
|
|
# TODO: Is it correct, may be we have to use nodes from
|
|
# self.levels[level]
|
|
for node in nodes:
|
|
self.result[node]['y'] = order
|
|
order += 1
|
|
|
|
def median_value(self, node, adj_rank):
|
|
""" Returns median value of a vertex , defined as the median position
|
|
of the adjacent vertices
|
|
|
|
:param node: node to process
|
|
:param adj_rank: rank 1 less than the node's rank
|
|
"""
|
|
adj_nodes = self.adj_position(node, adj_rank)
|
|
length = len(adj_nodes)
|
|
m = length // 2
|
|
|
|
if length == 0:
|
|
return -1.0
|
|
if length % 2 == 1:
|
|
return adj_nodes[m] # median of the middle element
|
|
if length == 2:
|
|
return (adj_nodes[0] + adj_nodes[1]) / 2
|
|
|
|
left = adj_nodes[m-1] - adj_nodes[0]
|
|
right = adj_nodes[length-1] - adj_nodes[m]
|
|
return (
|
|
(adj_nodes[m-1] * right) + (adj_nodes[m] * left)
|
|
) / (left + right)
|
|
|
|
def adj_position(self, node, adj_rank):
|
|
""" Returns list of the present positions of the nodes adjacent to node
|
|
in the given adjacent rank.
|
|
|
|
:param node: node to process
|
|
:param adj_rank: rank 1 less than the node's rank
|
|
"""
|
|
pre_level_nodes = self.levels.get(adj_rank, [])
|
|
adj_nodes = []
|
|
|
|
if pre_level_nodes:
|
|
for src in pre_level_nodes:
|
|
if self.transitions.get(src) and node in self.transitions[src]:
|
|
adj_nodes.append(self.result[src]['y'])
|
|
|
|
return adj_nodes
|
|
|
|
def preprocess_order(self):
|
|
levels = {}
|
|
for r in self.partial_order:
|
|
lvl = self.result[r]['x']
|
|
levels.setdefault(lvl, [])
|
|
levels[lvl].append(r)
|
|
|
|
self.levels = levels
|
|
|
|
def graph_order(self):
|
|
""" Finds actual-order of the nodes with respect to maximum number of
|
|
nodes in a rank in component
|
|
"""
|
|
mid_pos = 0.0
|
|
max_level = max(len(x) for x in self.levels.values())
|
|
|
|
for level in self.levels:
|
|
if level:
|
|
no = len(self.levels[level])
|
|
factor = (max_level - no) * 0.10
|
|
lst = self.levels[level]
|
|
lst.reverse()
|
|
|
|
if no % 2 == 0:
|
|
first_half = lst[no // 2:]
|
|
factor = -factor
|
|
else:
|
|
first_half = lst[no // 2 + 1:]
|
|
# for the case when horizontal graph is there
|
|
if max_level == 1:
|
|
self.result[lst[no // 2]]['y'] = (
|
|
mid_pos + (
|
|
self.result[lst[no // 2]]['x'] % 2 * 0.5
|
|
)
|
|
)
|
|
else:
|
|
self.result[lst[no // 2]]['y'] = mid_pos + factor
|
|
|
|
last_half = lst[:no // 2]
|
|
|
|
i = 1
|
|
for node in first_half:
|
|
self.result[node]['y'] = mid_pos - (i + factor)
|
|
i += 1
|
|
|
|
i = 1
|
|
for node in last_half:
|
|
self.result[node]['y'] = mid_pos + (i + factor)
|
|
i += 1
|
|
else:
|
|
self.max_order += max_level+1
|
|
mid_pos = self.result[self.start]['y']
|
|
|
|
def tree_order(self, node, last=0):
|
|
mid_pos = self.result[node]['y']
|
|
l = self.transitions.get(node, [])
|
|
l.reverse()
|
|
no = len(l)
|
|
|
|
rest = no % 2
|
|
first_half = l[no // 2 + rest:]
|
|
last_half = l[:no // 2]
|
|
|
|
for i, child in enumerate(first_half):
|
|
self.result[child]['y'] = mid_pos - (i+1 - (0 if rest else 0.5))
|
|
|
|
if self.transitions.get(child, False):
|
|
if last:
|
|
self.result[child]['y'] = (
|
|
last + len(self.transitions[child]) / 2 + 1)
|
|
last = self.tree_order(child, last)
|
|
|
|
if rest:
|
|
mid_node = l[no // 2]
|
|
self.result[mid_node]['y'] = mid_pos
|
|
|
|
if self.transitions.get(mid_node, False):
|
|
if last:
|
|
self.result[mid_node]['y'] = (
|
|
last + len(self.transitions[mid_node]) / 2 + 1)
|
|
if node != mid_node:
|
|
last = self.tree_order(mid_node)
|
|
else:
|
|
if last:
|
|
self.result[mid_node]['y'] = last + 1
|
|
self.result[node]['y'] = self.result[mid_node]['y']
|
|
mid_pos = self.result[node]['y']
|
|
|
|
i = 1
|
|
last_child = None
|
|
for child in last_half:
|
|
self.result[child]['y'] = mid_pos + (i - (0 if rest else 0.5))
|
|
last_child = child
|
|
i += 1
|
|
if self.transitions.get(child, False):
|
|
if last:
|
|
self.result[child]['y'] = (
|
|
last + len(self.transitions[child]) / 2 + 1)
|
|
if node != child:
|
|
last = self.tree_order(child, last)
|
|
|
|
if last_child:
|
|
last = self.result[last_child]['y']
|
|
|
|
return last
|
|
|
|
def process_order(self):
|
|
""" Finds actual-order of the nodes with respect to maximum number of
|
|
nodes in a rank in component
|
|
"""
|
|
if self.Is_Cyclic:
|
|
max_level = max(len(x) for x in self.levels.values())
|
|
|
|
if max_level % 2:
|
|
self.result[self.start]['y'] = (max_level+1) / 2 + self.max_order + (self.max_order and 1)
|
|
else:
|
|
self.result[self.start]['y'] = max_level / 2 + self.max_order + (self.max_order and 1)
|
|
|
|
self.graph_order()
|
|
|
|
else:
|
|
self.result[self.start]['y'] = 0
|
|
self.tree_order(self.start, 0)
|
|
min_order = math.fabs(min(x['y'] for x in self.result.values()))
|
|
|
|
index = self.start_nodes.index(self.start)
|
|
|
|
roots = []
|
|
if index > 0:
|
|
for start in self.start_nodes[:index]:
|
|
same = True
|
|
for edge in self.tree_list[start][1:]:
|
|
if edge in self.tree_list[self.start]:
|
|
continue
|
|
|
|
same = False
|
|
break
|
|
|
|
if same:
|
|
roots.append(start)
|
|
|
|
if roots:
|
|
min_order += self.max_order
|
|
else:
|
|
min_order += self.max_order + 1
|
|
|
|
for level in self.levels:
|
|
for node in self.levels[level]:
|
|
self.result[node]['y'] += min_order
|
|
|
|
if roots:
|
|
roots.append(self.start)
|
|
one_level_el = self.tree_list[self.start][0][1]
|
|
base = self.result[one_level_el]['y'] # * 2 / (index + 2)
|
|
|
|
no = len(roots)
|
|
first_half = roots[:no // 2]
|
|
|
|
if no % 2 == 0:
|
|
last_half = roots[no // 2:]
|
|
else:
|
|
last_half = roots[no // 2 + 1:]
|
|
|
|
factor = -math.floor(no // 2)
|
|
for start in first_half:
|
|
self.result[start]['y'] = base + factor
|
|
factor += 1
|
|
|
|
if no % 2:
|
|
self.result[roots[no // 2]]['y'] = base + factor
|
|
factor += 1
|
|
|
|
for start in last_half:
|
|
self.result[start]['y'] = base + factor
|
|
factor += 1
|
|
|
|
self.max_order = max(x['y'] for x in self.result.values())
|
|
|
|
def find_starts(self):
|
|
""" Finds other start nodes of the graph in the case
|
|
when graph is disconnected
|
|
"""
|
|
rem_nodes = []
|
|
for node in self.nodes:
|
|
if not self.partial_order.get(node):
|
|
rem_nodes.append(node)
|
|
|
|
g_counter = 0
|
|
while g_counter < GUARD_COUNTER_LIMIT:
|
|
g_counter += 1
|
|
if g_counter >= GUARD_COUNTER_LIMIT:
|
|
raise exceptions.UserError(_(
|
|
"Cannot compute diagram view. "
|
|
"It seems that the flow is incorrect! "
|
|
"Try to remove some broken routes without using "
|
|
"diagram view."
|
|
))
|
|
if len(rem_nodes) == 1:
|
|
self.start_nodes.append(rem_nodes[0])
|
|
break
|
|
else:
|
|
count = 0
|
|
new_start = rem_nodes[0]
|
|
largest_tree = []
|
|
|
|
for node in rem_nodes:
|
|
self.partial_order = {}
|
|
tree = self.make_acyclic(None, node, 0, [])
|
|
if len(tree)+1 > count:
|
|
count = len(tree) + 1
|
|
new_start = node
|
|
largest_tree = tree
|
|
else:
|
|
if not largest_tree:
|
|
new_start = rem_nodes[0]
|
|
rem_nodes.remove(new_start)
|
|
|
|
self.start_nodes.append(new_start)
|
|
|
|
for edge in largest_tree:
|
|
if edge[0] in rem_nodes:
|
|
rem_nodes.remove(edge[0])
|
|
if edge[1] in rem_nodes:
|
|
rem_nodes.remove(edge[1])
|
|
|
|
if not rem_nodes:
|
|
break
|
|
|
|
def rank(self):
|
|
""" Finds the optimized rank of the nodes using
|
|
Network-simplex algorithm
|
|
"""
|
|
self.levels = {}
|
|
self.critical_edges = []
|
|
self.partial_order = {}
|
|
self.links = []
|
|
self.Is_Cyclic = False
|
|
|
|
self.tree_list[self.start] = self.make_acyclic(None, self.start, 0, [])
|
|
self.Is_Cyclic = self.rev_edges(self.tree_list[self.start])
|
|
self.process_ranking(self.start)
|
|
self.init_rank()
|
|
|
|
# make cut values of all tree edges to 0 to optimize feasible tree
|
|
e = self.leave_edge()
|
|
|
|
g_counter = 0
|
|
while e and g_counter < GUARD_COUNTER_LIMIT:
|
|
g_counter += 1
|
|
if g_counter >= GUARD_COUNTER_LIMIT:
|
|
raise exceptions.UserError(_(
|
|
"Cannot compute diagram view. "
|
|
"It seems that the flow is incorrect! "
|
|
"Try to remove some broken routes without using "
|
|
"diagram view."
|
|
))
|
|
|
|
f = self.enter_edge(e)
|
|
if e == f:
|
|
self.critical_edges.append(e)
|
|
else:
|
|
self.exchange(e, f)
|
|
e = self.leave_edge()
|
|
|
|
# finalize rank using optimum feasible tree
|
|
# self.optimal_edges = {}
|
|
# for edge in self.tree_edges:
|
|
# source = self.optimal_edges.setdefault(edge[0], [])
|
|
# source.append(edge[1])
|
|
#
|
|
# self.finalize_rank(self.start, 0)
|
|
|
|
# normalization
|
|
self.normalize()
|
|
for edge in self.edge_wt:
|
|
self.edge_wt[edge] = (
|
|
self.result[edge[1]]['x'] - self.result[edge[0]]['x'])
|
|
|
|
def order_in_rank(self):
|
|
""" Finds optimized order of the nodes within their ranks using
|
|
median heuristic
|
|
"""
|
|
self.make_chain()
|
|
self.preprocess_order()
|
|
self.order = {}
|
|
max_rank = max(x for x in self.levels)
|
|
|
|
for i in range(max_rank + 1):
|
|
self.order[i] = 0
|
|
|
|
self.init_order(self.start, self.result[self.start]['x'])
|
|
|
|
for level in self.levels:
|
|
self.levels[level].sort(key=lambda x: self.result[x]['y'])
|
|
|
|
self.order_heuristic()
|
|
self.process_order()
|
|
|
|
def process(self, starting_node):
|
|
"""Process the graph to find ranks and order of the nodes
|
|
@param starting_node node from where to start the graph search
|
|
"""
|
|
self.start_nodes = starting_node or []
|
|
self.partial_order = {}
|
|
self.links = []
|
|
self.tree_list = {}
|
|
|
|
if self.nodes:
|
|
if self.start_nodes:
|
|
# add dummy edges to the nodes which does not have
|
|
# any incoming edges
|
|
tree = self.make_acyclic(None, self.start_nodes[0], 0, [])
|
|
|
|
for node in self.no_ancester:
|
|
for sec_node in self.transitions.get(node, []):
|
|
if sec_node in self.partial_order:
|
|
self.transitions[self.start_nodes[0]].append(node)
|
|
break
|
|
|
|
self.partial_order = {}
|
|
tree = self.make_acyclic(None, self.start_nodes[0], 0, [])
|
|
|
|
# if graph is disconnected or no start-node is given
|
|
# than to find starting_node for each component of the node
|
|
if len(self.nodes) > len(self.partial_order):
|
|
self.find_starts()
|
|
|
|
self.max_order = 0
|
|
# for each component of the graph find ranks and order of the nodes
|
|
for s in self.start_nodes:
|
|
self.start = s
|
|
# First step: Network simplex algorithm
|
|
self.rank()
|
|
# Second step: ordering nodes within ranks
|
|
self.order_in_rank()
|
|
|
|
def __str__(self):
|
|
result = ''
|
|
for lvl in self.levels:
|
|
result += 'PosY: ' + str(lvl) + '\n'
|
|
for node in self.levels[lvl]:
|
|
result += '\tPosX: %s - Node: %s\n' % (
|
|
str(self.result[node]['y']),
|
|
str(node))
|
|
return result
|
|
|
|
def scale(self, maxx, maxy, nwidth=0, nheight=0, margin=20):
|
|
""" Computes actual co-ordiantes of the nodes
|
|
"""
|
|
# for flat edges ie. source an destination nodes are on the same rank
|
|
for src in self.transitions:
|
|
for des in self.transitions[src]:
|
|
if self.result[des]['x'] - self.result[src]['x'] == 0:
|
|
self.result[src]['x'] += 0.08
|
|
self.result[des]['x'] -= 0.08
|
|
|
|
factorX = maxx + nheight
|
|
factorY = maxy + nwidth
|
|
|
|
for node in self.result:
|
|
self.result[node]['y'] = (
|
|
(self.result[node]['y']) * factorX + margin)
|
|
self.result[node]['x'] = (
|
|
(self.result[node]['x']) * factorY + margin)
|
|
|
|
def result_get(self):
|
|
return self.result
|
|
|
|
|
|
def main():
|
|
starting_node = ['profile'] # put here nodes with flow_start=True
|
|
nodes = [
|
|
'project', 'account', 'hr', 'base', 'product', 'mrp',
|
|
'test', 'profile',
|
|
]
|
|
transitions = [
|
|
('profile', 'mrp'),
|
|
('mrp', 'project'),
|
|
('project', 'product'),
|
|
('mrp', 'hr'),
|
|
('mrp', 'test'),
|
|
('project', 'account'),
|
|
('project', 'hr'),
|
|
('product', 'base'),
|
|
('account', 'product'),
|
|
('account', 'test'),
|
|
('account', 'base'),
|
|
('hr', 'base'),
|
|
('test', 'base')
|
|
]
|
|
|
|
radius = 20
|
|
g = Graph(nodes, transitions)
|
|
g.process(starting_node)
|
|
g.scale(radius * 3, radius * 3, radius, radius)
|
|
|
|
from PIL import Image
|
|
from PIL import ImageDraw
|
|
img = Image.new("RGB", (800, 600), "#ffffff")
|
|
draw = ImageDraw.Draw(img)
|
|
|
|
result = g.result_get()
|
|
node_res = {}
|
|
for node in nodes:
|
|
node_res[node] = result[node]
|
|
|
|
for name, node in node_res.items():
|
|
|
|
draw.arc(
|
|
(int(node['y']-radius),
|
|
int(node['x']-radius),
|
|
int(node['y']+radius),
|
|
int(node['x']+radius)),
|
|
0, 360, (128, 128, 128))
|
|
draw.text(
|
|
(int(node['y']),
|
|
int(node['x'])),
|
|
str(name),
|
|
(128, 128, 128))
|
|
|
|
for t in transitions:
|
|
draw.line(
|
|
(int(node_res[t[0]]['y']),
|
|
int(node_res[t[0]]['x']),
|
|
int(node_res[t[1]]['y']),
|
|
int(node_res[t[1]]['x'])),
|
|
(128, 128, 128))
|
|
img.save("graph.png", "PNG")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|