From aece567bf3727498eab43f0189242875b44e434d Mon Sep 17 00:00:00 2001 From: Shanta Kabra Date: Fri, 29 Aug 2008 14:53:03 +0530 Subject: [PATCH] * Graph algorithm updations bzr revid: shanta.kabra@gmail.com-20080829092303-4qn1f85o9p25trap --- bin/addons/base/ir/workflow/workflow.py | 31 +- bin/tools/graph.py | 548 +++++++++++++++++++----- 2 files changed, 464 insertions(+), 115 deletions(-) diff --git a/bin/addons/base/ir/workflow/workflow.py b/bin/addons/base/ir/workflow/workflow.py index 8b90cfee140..45d2c752661 100644 --- a/bin/addons/base/ir/workflow/workflow.py +++ b/bin/addons/base/ir/workflow/workflow.py @@ -57,31 +57,42 @@ class workflow(osv.osv): # scale = [stepx, stepy, posx, posy ] # + def graph_get(self, cr, uid, id, scale, context={}): nodes= [] + nodes_name = [] transitions = [] start = [] tres = {} + no_ancester = [] workflow = self.browse(cr, uid, id, context) for a in workflow.activities: - nodes.append((a.id,a.name)) + nodes_name.append((a.id,a.name)) + nodes.append(a.id) if a.flow_start: - start.append((a.id,a.name)) + start.append(a.id) + else: + if not a.in_transitions: + no_ancester.append(a.id) + for t in a.out_transitions: - transitions.append( ((a.id,a.name), (t.act_to.id,t.act_to.name)) ) - tres[t.id] = (a.id,t.act_to.id) - g = graph(nodes, transitions) + transitions.append((a.id, t.act_to.id)) + tres[t.id] = (a.id, t.act_to.id) + + + g = graph(nodes, transitions, no_ancester) g.process(start) g.scale(*scale) result = g.result_get() results = {} - - - for r in result.items(): - r[1]['name'] = r[0][1] - results[str(r[0][0])] = r[1] + + for node in nodes_name: + results[str(node[0])] = result[node[0]] + results[str(node[0])]['name'] = node[1] + return {'node': results, 'transition': tres} + def create(self, cr, user, vals, context=None): if not context: diff --git a/bin/tools/graph.py b/bin/tools/graph.py index f07f8d5aa0d..9e419690ec9 100644 --- a/bin/tools/graph.py +++ b/bin/tools/graph.py @@ -27,12 +27,22 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. ############################################################################### +import operator class graph(object): - def __init__(self, nodes, transitions): - self.nodes = nodes - self.links = transitions + def __init__(self, nodes, transitions, no_ancester=None): + """Initailize 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]) @@ -40,23 +50,21 @@ class graph(object): self.result = {} self.levels = {} - def get_parent(self,node): - count = 0 - for item in self.transitions: - if self.transitions[item].__contains__(node): - count +=1 - return count def init_rank(self): - self.temp = {} + """Computes rank of the nodes of the graph by finding initial feasible tree + """ + self.edge_wt = {} for link in self.links: - self.temp[link] = self.result[link[1]]['y'] - self.result[link[0]]['y'] + self.edge_wt[link] = self.result[link[1]]['y'] - self.result[link[0]]['y'] cnt = 0 list_node = [] list_edge = [] - - while self.tight_tree()self.temp[edge]-1): - slack = self.temp[edge]-1 + 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.temp[new_edge]-1) + delta = -(self.edge_wt[new_edge]-1) else: - delta = self.temp[new_edge]-1 + delta = self.edge_wt[new_edge]-1 for node in self.result: if node in self.reachable_nodes: self.result[node]['y'] += delta - for link in self.temp: - self.temp[link] = self.result[link[1]]['y'] - self.result[link[0]]['y'] - - self.init_cutvalues() - + for link in self.edge_wt: + self.edge_wt[link] = self.result[link[1]]['y'] - self.result[link[0]]['y'] + self.init_cutvalues() def tight_tree(self,): @@ -100,13 +106,15 @@ class graph(object): self.reachable_node(self.start) return self.reachable_nodes.__len__() - def reachable_node(self,node): + 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 link in self.temp: + for link in self.edge_wt: if link[0]==node: -# print link[0] - if self.temp[link]==1: + if self.edge_wt[link]==1: self.tree_edges.append(link) if link[1] not in self.reachable_nodes: self.reachable_nodes.append(link[1]) @@ -114,15 +122,19 @@ class graph(object): 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 rest_edges.__delitem__(i) - self.head_component(self.start,rest_edges) + self.head_component(self.start, rest_edges) i+=1 positive = 0 negative = 0 @@ -137,11 +149,11 @@ class graph(object): 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 link in rest_edges: @@ -150,6 +162,9 @@ class graph(object): 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] = {'x': None, 'y':level, 'mark':0} else: @@ -160,104 +175,437 @@ class graph(object): for t in self.transitions.get(node, []): self.process_ranking(t, 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 t in self.transitions.get(node, []): + self.links.append((node, t)) + self.make_acyclic(node, t, level+1, tree) - def preprocess_order(self): - levels = {} - for r in self.result: - l = self.result[r]['y'] - levels.setdefault(l,[]) - levels[l].append(r) - self.levels = levels + return tree - def process_order(self, level): - self.levels[level].sort(lambda x,y: cmp(self.result[x]['x'], self.result[y]['x'])) - for nodepos in range(len(self.levels[level])): - node = self.levels[level][nodepos] - if nodepos == 0: - left = self.result[node]['x']- 0.5 - else: - left = (self.result[node]['x'] + self.result[self.levels[level][nodepos-1]]['x']) / 2.0 - - if nodepos == (len(self.levels[level])-1): - right = self.result[node]['x'] + 0.5 - else: - right = (self.result[node]['x'] + self.result[self.levels[level][nodepos+1]]['x']) / 2.0 - - - if self.transitions.get(node, False): - if len(self.transitions[node])==1: - pos = (left+right)/2.0 - step = 0 - else: - pos = left - step = (-left+right) / (len(self.transitions[node])-1) - - for n2 in self.transitions[node]: - self.result[n2]['x'] = pos - pos += step - - def exchange(self,e,f): + + 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: + self.links.__delitem__(i) + self.links.insert(i, (des, src)) + self.transitions[src].remove(des) + self.transitions.setdefault(des, []).append(src) + Is_Cyclic = True + elif edge_len > 1: + Is_Cyclic = True + i += 1 + + return Is_Cyclic + + def exchange(self, e, f): + """Exchange edges to make feasible-tree optimized + @param edge: edge with negative cut-value + @param edge: new edge with minimum slack-value + """ self.tree_edges.__delitem__(self.tree_edges.index(e)) self.tree_edges.append(f) self.init_cutvalues() - def enter_edge(self,edge): + 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 rest_edges.__delitem__(rest_edges.index(edge)) - self.head_component(self.start,rest_edges) + self.head_component(self.start, rest_edges) + + if self.head_nodes.__contains__(edge[1]): + list = [] + for node in self.result: + if not self.head_nodes.__contains__(node): + list.append(node) + self.head_nodes = list + 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.temp[edge]-1)): - slack = self.temp[edge]-1 + 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 () - - def process(self, starting_node): - pos = (len(starting_node) - 1.0)/2.0 - self.start = starting_node[0] - for s in starting_node: - self.process_ranking(s) - self.result[s]['x'] = pos - pos += 1.0 - self.init_rank() - #normalize + return None + + def normalize(self): + """The ranks are normalized by setting the least rank to zero. + """ + least_rank=100 - #normalization for node in self.result: if least_rank>self.result[node]['y']: least_rank = self.result[node]['y'] if(least_rank!=0): - diff = least_rank for node in self.result: - self.result[node]['y']-=least_rank + self.result[node]['y']-=least_rank - e = self.leave_edge() - #while e: - f = self.enter_edge(e) - self.exchange(e,f) - e = self.leave_edge() + 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]]['y'] + end = self.result[edge[1]]['y'] + + for rank in range(start+1, end): + if not self.result.get((rank, 'temp'), False): + self.result[(rank, 'temp')] = {'x': None, 'y': 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]['x']: + if self.order.get(level)==0: + self.result[node]['x'] = self.order[level] + self.order[level] = self.order[level]+1 + else: + self.result[node]['x'] = self.order[level-1] + self.order[level-1] = self.order[level-1]+1 + + for sec_end in self.transitions.get(node, []): + self.init_order(sec_end, level+1) + + + 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 = [] + for node in self.levels[level]: + 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 self.levels[level]: + self.result[node]['x'] = 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 n in pre_level_nodes: + if (self.transitions.get(n) and self.transitions[n].__contains__(node)): + adj_nodes.append(self.result[n]['x']) + + return adj_nodes + + + def preprocess_order(self): + levels = {} + for r in self.partial_order: + l = self.result[r]['y'] + 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 = None + max_level = max(map(lambda x: len(x), self.levels.values())) + + for level in self.levels: + if level: + no = len(self.levels[level]) + factor = (max_level - no) * 0.10 + self.levels[level].reverse() + list = self.levels[level] + + if no%2==0: + first_half = list[no/2:] + factor = -factor + else: + first_half = list[no/2+1:] + if max_level==1: + self.result[list[no/2]]['x'] = mid_pos + (self.result[list[no/2]]['y']%2 * 0.5) + else: + self.result[list[no/2]]['x'] = mid_pos + factor + + last_half = list[:no/2] + i=1 + for node in first_half: + self.result[node]['x'] = mid_pos - (i + factor) + i += 1 + + i=1 + for node in last_half: + self.result[node]['x'] = mid_pos + (i + factor) + i += 1 + else: + self.max_order += max_level+1 + mid_pos = self.result[self.start]['x'] + + def tree_order(self, node): + mid_pos = self.result[node]['x'] + l = self.transitions[node] + l.reverse() + no = len(l) + if no%2==0: + first_half = l[no/2:] + else: + first_half = l[no/2+1:] + self.result[l[no/2]]['x'] = mid_pos + if self.transitions.get((l[no/2]), False): + self.tree_order(l[no/2]) + last_half = l[:no/2] + + i=1 + for child in first_half: + self.result[child]['x'] = mid_pos - i + i += 1 + if self.transitions.get(child, False): + self.tree_order(child) + i=1 + for child in last_half: + self.result[child]['x'] = mid_pos + i + i += 1 + if self.transitions.get(child, False): + self.tree_order(child) + + def process_order(self): + """Finds actual-order of the nodes with respect to maximum number of nodes in a rank in component + """ + max_level = max(map(lambda x: len(x), self.levels.values())) + if max_level%2: + self.result[self.start]['x'] = (max_level+1)/2 + self.max_order + else: + self.result[self.start]['x'] = (max_level)/2 + self.max_order + + if self.Is_Cyclic: + self.graph_order() + else: + self.tree_order(self.start) + + 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 + + self.start_nodes.append(new_start) + for edge in largest_tree: + if rem_nodes.__contains__(edge[0]): + rem_nodes.remove(edge[0]) + if rem_nodes.__contains__(edge[1]): + rem_nodes.remove(edge[1]) + if not rem_nodes: + break + + + + def rank(self): + """Finds the optimized rank of the nodes using Network-simplex algorithm + + @param start: starting node of the component + """ + + self.levels = {} + self.order = {} + self.critical_edges = [] + self.partial_order = {} + self.links = [] + self.Is_Cyclic = False + + tree = self.make_acyclic(None, self.start, 0, []) + self.Is_Cyclic = self.rev_edges(tree) + self.process_ranking(self.start) + self.init_rank() + + #make cut values of all tree edges to 0 to optimize ranking + 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() + + #normalization + self.normalize() + for edge in self.edge_wt: + self.edge_wt[edge] = self.result[edge[1]]['y'] - self.result[edge[0]]['y'] + + def order_in_rank(self): + """Finds optimized order of the nodes within their ranks using median heuristic + + @param start: starting node of the component + """ + + self.make_chain() self.preprocess_order() - for n in self.levels: - self.process_order(n) + + self.order = {} + for i in self.levels: + self.order[i] = 0 + + self.init_order(self.start, 0) + + for level in self.levels: + self.levels[level].sort(lambda x,y: cmp(self.result[x]['x'], self.result[y]['x'])) + + self.order_heuristic() + + for level in self.levels: + for node in self.levels[level]: + try: + if int(node): + continue + except e: + self.levels[level].remove(node) + self.result.__delitem__(node) + 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 = [] + + if self.start_nodes: + #add dummy edges to the nodes which does not have any incoming edges + for node in self.no_ancester: + self.transitions[self.start_nodes[0]].append(node) + + tree = self.make_acyclic(None, self.start_nodes[0], 0, []) + + # if graph is disconnected 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 + @@ -270,22 +618,12 @@ class graph(object): return result def scale(self, maxx, maxy, plusx2=0, plusy2=0): - plusx = - min(map(lambda x: x['x'],self.result.values())) - plusy = - min(map(lambda x: x['y'],self.result.values())) - - maxcurrent = 1.0 - diff = 1.0 - for l in self.levels: - for n in range(1, len(self.levels[l])): - n1 = self.levels[l][n] - n2 = self.levels[l][n-1] - diff = abs(self.result[n2]['x']-self.result[n1]['x']) - if diff