at master 227 lines 7.5 kB view raw
1from toolz import curried as tlz 2from toolz import curry 3 4from .lib import ( 5 debug, 6 debug_plot, 7 DEBUG_PLOT, 8 find_vertex_by_name_or_none, 9 graph_is_empty, 10 is_None, 11 subcomponent_multi, 12 unnest_iterable 13) 14 15 16@curry 17def coerce_to_singly_rooted_graph(fake_root_name, graph): 18 """Add single root to the graph connected to all existing roots. 19 20 If graph has only one root, return the graph unchanged and the name 21 of the root vertex. 22 23 Otherwise return a modified graph (copy) and a name of the added root 24 vertex. 25 """ 26 roots = graph.vs.select(lambda v: len(graph.predecessors(v)) == 0) 27 root_names = roots["name"] 28 29 if len(root_names) == 1: 30 return graph, root_names[0] 31 else: 32 edges = [(fake_root_name, v) for v in root_names] 33 graph_with_root = graph + fake_root_name + edges 34 return graph_with_root, fake_root_name 35 36 37@curry 38def remove_vertex(vertex_name, graph): 39 """Remove vertex with given name, returning copy of input graph if vertex 40 with given name is found in the graph 41 """ 42 vertex = find_vertex_by_name_or_none(graph)(vertex_name) 43 44 return graph - vertex_name if vertex else graph 45 46 47def get_children_of(graph, vertex_names): 48 return unnest_iterable(map( 49 graph.successors, 50 tlz.remove( 51 is_None, 52 map( 53 find_vertex_by_name_or_none(graph), 54 vertex_names 55 ) 56 ) 57 )) 58 59 60def as_list(x): 61 return x if isinstance(x, list) else [x] 62 63 64@curry 65def split_path_spec_to_indices(graph, split_path_spec): 66 debug("split_path_spec", split_path_spec) 67 if isinstance(split_path_spec, dict): 68 if "children_of" in split_path_spec: 69 children_of = split_path_spec["children_of"] 70 71 return get_children_of(graph, as_list(children_of)) 72 else: 73 raise Exception( 74 "Unexpected split path spec: dict with invalid keys." 75 "Valid: [\"children_of\"]" 76 ) 77 else: 78 vertex = find_vertex_by_name_or_none(graph)(split_path_spec) 79 return [] if is_None(vertex) else [vertex.index] 80 81 82call_count = 0 83 84 85@curry 86def split_paths(split_paths, graph_in): 87 debug("____") 88 debug("split_paths:", split_paths) 89 debug("graph_in:", graph_in) 90 91 if DEBUG_PLOT: 92 global call_count 93 graph_name_prefix = f"split_paths_{call_count}_" 94 call_count += 1 95 96 # Convert list of split_paths into list of vertex indices. Ignores 97 # split_paths which don"t match any vertices in the graph. 98 # All edges pointing at the indices will be deleted from the graph. 99 split_path_indices = list(unnest_iterable(map( 100 split_path_spec_to_indices(graph_in), 101 split_paths 102 ))) 103 104 debug("split_path_indices:", split_path_indices) 105 106 # Short circuit if there is nothing to do (split_paths didn"t match any 107 # vertices in the graph). 108 if len(split_path_indices) == 0: 109 if DEBUG_PLOT: 110 layout = graph_in.layout('tree') 111 debug_plot(graph_in, f"{graph_name_prefix}input", layout=layout) 112 debug_plot(graph_in, f"{graph_name_prefix}result", layout=layout) 113 114 return {"rest": graph_in} 115 116 # If graph has multiple roots, add a single one connecting all existing 117 # roots to make it easy to split the graph into 2 sets of vertices after 118 # deleting edges pointing at split_path_indices. 119 fake_root_name = "__root__" 120 graph, root_name = coerce_to_singly_rooted_graph(fake_root_name, graph_in) 121 122 debug("root_name", root_name) 123 124 if ( 125 find_vertex_by_name_or_none(graph)(root_name).index 126 in split_path_indices 127 ): 128 if DEBUG_PLOT: 129 layout = graph_in.layout('tree') 130 debug_plot(graph_in, f"{graph_name_prefix}input", layout=layout) 131 debug_plot( 132 graph_in, 133 f"{graph_name_prefix}result", 134 layout=layout, 135 vertex_color="green" 136 ) 137 138 return {"main": graph_in} 139 140 # Copy graph if coerce_to_singly_rooted_graph has not already created 141 # a copy, since we are going to mutate the graph and don"t want to 142 # mutate a function argument. 143 graph = graph if graph is not graph_in else graph.copy() 144 145 if DEBUG_PLOT: 146 layout = graph.layout('tree') 147 debug_plot(graph, f"{graph_name_prefix}input", layout=layout) 148 149 # Get incidences of all vertices which can be reached split_path_indices 150 # (including split_path_indices). This is a set of all split_paths and their 151 # dependencies. 152 split_off_vertex_indices = frozenset( 153 subcomponent_multi(graph, split_path_indices)) 154 debug("split_off_vertex_indices", split_off_vertex_indices) 155 156 # Delete edges which point at any of the vertices in split_path_indices. 157 graph.delete_edges(_target_in=split_path_indices) 158 159 if DEBUG_PLOT: 160 debug_plot(graph, f"{graph_name_prefix}deleted_edges", layout=layout) 161 162 # Get incidences of all vertices which can be reached from the root. Since 163 # edges pointing at split_path_indices have been deleted, none of the 164 # split_path_indices will be included. Dependencies of rest_with_common will 165 # only be included if they can be reached from any vertex which is itself 166 # not in split_off_vertex_indices. 167 rest_with_common = frozenset(graph.subcomponent(root_name, mode="out")) 168 debug("rest_with_common", rest_with_common) 169 170 # Get a set of all dependencies common to split_path_indices and the rest 171 # of the graph. 172 common = split_off_vertex_indices.intersection(rest_with_common) 173 debug("common", common) 174 175 # Get a set of vertices which cannot be reached from split_path_indices. 176 rest_without_common = rest_with_common.difference(common) 177 debug("rest_without_common", rest_without_common) 178 179 # Get a set of split_path_indices and their dependencies which cannot be 180 # reached from the rest of the graph. 181 split_off_without_common = split_off_vertex_indices.difference(common) 182 debug("split_off_without_common", split_off_without_common) 183 184 if DEBUG_PLOT: 185 def choose_color(index): 186 if (index in split_off_without_common): 187 return "green" 188 elif (index in rest_without_common): 189 return "red" 190 else: 191 return "purple" 192 193 vertex_color = [choose_color(v.index) for v in graph.vs] 194 195 debug_plot( 196 graph, 197 f"{graph_name_prefix}result", 198 layout=layout, 199 vertex_color=vertex_color 200 ) 201 202 # Return subgraphs based on calculated sets of vertices. 203 204 result_keys = ["main", "common", "rest"] 205 result_values = [ 206 # Split paths and their deps (unreachable from rest of the graph). 207 graph.induced_subgraph(split_off_without_common), 208 # Dependencies of split paths which can be reached from the rest of the 209 # graph. 210 graph.induced_subgraph(common), 211 # Rest of the graph (without dependencies common with split paths). 212 graph.induced_subgraph(rest_without_common), 213 ] 214 215 debug('result_values', result_values[0].vs["name"]) 216 217 return tlz.valfilter( 218 tlz.complement(graph_is_empty), 219 dict(zip( 220 result_keys, 221 ( 222 result_values if root_name != fake_root_name 223 # If root was added, remove it 224 else tlz.map(remove_vertex(fake_root_name), result_values) 225 ) 226 )) 227 )