Now that we're closer to switching to P3 for good, these helpers have outlived their usefulness, and mostly add noise. All remaining dict.iter*() or dict.view*() must be converted to the normal keys(), values() or items() calls. Whenever the result is likely to be used for more than the scope of a loop, or when the dict needs to be modified during iteration, the calls must be wrapped in a ``list()``, to protect the new P3 semantics. Those cases are very exceptional. Also removed some dead code or improved the API to remove unnecessary conversions.
756 lines
24 KiB
Python
Executable File
756 lines
24 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import operator
|
|
import math
|
|
|
|
|
|
class graph(object):
|
|
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
|
|
while self.tight_tree()<tot_node:
|
|
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):
|
|
"""Initailize 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
|
|
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:
|
|
l = []
|
|
for node in self.result:
|
|
if node not in self.head_nodes:
|
|
l.append(node)
|
|
self.head_nodes = l
|
|
|
|
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.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 i in range(12):
|
|
self.wmedian()
|
|
|
|
|
|
def wmedian(self):
|
|
"""Applies median heuristic to find optimzed order of the nodes with in 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))
|
|
|
|
new_list = [tuple[0] for tuple in sort_list]
|
|
|
|
self.levels[level] = new_list
|
|
order = 0
|
|
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)
|
|
l = len(adj_nodes)
|
|
m = l/2
|
|
|
|
if l==0:
|
|
return -1.0
|
|
elif l%2 == 1:
|
|
return adj_nodes[m]#median of the middle element
|
|
elif l==2:
|
|
return (adj_nodes[0]+adj_nodes[1])/2
|
|
else:
|
|
left = adj_nodes[m-1] - adj_nodes[0]
|
|
right = adj_nodes[l-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:
|
|
l = self.result[r]['x']
|
|
levels.setdefault(l,[])
|
|
levels[l].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.values())
|
|
|
|
for level in self.levels:
|
|
if level:
|
|
no = len(self.levels[level])
|
|
factor = (max_level - no) * 0.10
|
|
list = self.levels[level]
|
|
list.reverse()
|
|
|
|
if no%2==0:
|
|
first_half = list[no/2:]
|
|
factor = -factor
|
|
else:
|
|
first_half = list[no/2+1:]
|
|
if max_level==1:#for the case when horizontal graph is there
|
|
self.result[list[no/2]]['y'] = mid_pos + (self.result[list[no/2]]['x']%2 * 0.5)
|
|
else:
|
|
self.result[list[no/2]]['y'] = mid_pos + factor
|
|
|
|
last_half = list[: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.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.values()))
|
|
|
|
index = self.start_nodes.index(self.start)
|
|
same = False
|
|
|
|
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
|
|
else:
|
|
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.values())
|
|
|
|
def find_starts(self):
|
|
"""Finds other start nodes of the graph in the case when graph is disconneted
|
|
"""
|
|
rem_nodes = []
|
|
for node in self.nodes:
|
|
if not self.partial_order.get(node):
|
|
rem_nodes.append(node)
|
|
cnt = 0
|
|
while True:
|
|
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()
|
|
|
|
while e :
|
|
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
|
|
self.rank() # First step:Netwoek simplex algorithm
|
|
self.order_in_rank() #Second step: ordering nodes within ranks
|
|
|
|
|
|
def __str__(self):
|
|
result = ''
|
|
for l in self.levels:
|
|
result += 'PosY: ' + str(l) + '\n'
|
|
for node in self.levels[l]:
|
|
result += '\tPosX: '+ str(self.result[node]['y']) + ' - Node:' + str(node) + "\n"
|
|
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
|
|
|
|
if __name__=='__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")
|