brain_loop_search.packing
This module contains the classes that can be used to commit any task of packing a graph, i.e. reduce the undesired vertices based on a set of rules and an ontology tree that describe the affiliation among the vertices.
1""" 2This module contains the classes that can be used to commit any task of packing a graph, i.e. reduce the undesired 3vertices based on a set of rules and an ontology tree that describe the affiliation among the vertices. 4""" 5import typing 6 7import numpy as np 8import pandas as pd 9 10 11class Ontology: 12 """ 13 The prior knowledge about tree relationship among the vertices in graph, namely ontology, used for packing graph 14 vertices. 15This is a meta class that doesn't implement any function, so any subclass this with the same functions can use it 16 with other modules smoothly. 17 18 I used this as an abstract interface and make derivation for ccfv3 ontology in brain. 19 You can follow my subclass for your own. 20 21 I would recommend using pandas for tree representation for its speed and also because you can save some useful info 22 as appended columns. 23 """ 24 25 def __init__(self): 26 """ 27 Here, some preprocessing can be done make other functions easier and faster to operate. Also, you need to 28 ensure that this is actually a tree. 29 """ 30 pass 31 32 def check_include(self, vert: typing.Iterable): 33 """ 34 check if the vertices given is included 35 :param vert: the vertices to check 36 :return: True or False. 37 """ 38 pass 39 40 def levels_of(self, vert: typing.Iterable) -> pd.Series: 41 """ 42 Level is defined as the largest count from the current node to its leaves in the tree, starting from 0. 43 :param vert: the vertices to check 44 :return: the levels of each vertex in the ontology 45 """ 46 pass 47 48 def depths_of(self, vert: typing.Iterable) -> pd.Series: 49 """ 50 Depth is defined as the count from the current node to its root in the tree, starting from 0. 51 :param vert: the vertices to check 52 :return: the depths of each vertex in the ontology 53 """ 54 pass 55 56 def ancestors_of(self, vert: typing.Iterable) -> pd.Series: 57 """ 58 The parents of each vertex as a list, starting from the tree root to its immediate parent, the length of which 59 equals the depth. 60 61 :param vert: the vertices to check 62 :return: the lists of parents of each vertex in the ontology 63 """ 64 pass 65 66 def immediate_children_of(self, vert: typing.Iterable) -> pd.Series: 67 """ 68 The immediate children of each vertex as a list, starting from the tree root to its direct parent. 69 70 :param vert: the vertices to check 71 :return: the lists of children of each vertex in the ontology 72 """ 73 74 75class VertexPacker: 76 """ 77 Given a set of vertices in a graph, e.g. brain structures in a macroscopic brain connectome, 78 rearrange them by filtering and merging into a new set, based on a filtering rule and according 79 to an ontology. 80 81 This is the precursor step before packing the whole graph. A directed graph may not share its rows 82 with columns, so this might need to be done once for each, even with different rules. 83 84 Also, for a complexed graph, where you may expect heterogeneous level hierarchies, you can pack the graph multiple 85 times, and their vertices apply different rules naturally. This way, you'll also utilize this decoupled facility. 86 87 This class uses a state machine functionality, by which each time you trigger the alteration each the 88 stash changes. When you need a diversion or something you can retrieve the intermediate result and start anew. 89 """ 90 91 def __init__(self, vertices: typing.Iterable, ontology: Ontology): 92 """ 93 Initialize the vertex packer with an initial set of vertices and ontology. It also checks the viability of 94 the vertices. 95 96 :param vertices: a set of entities in the ontology to filter and merge. 97 :param ontology: a derivation of the abstract class `Ontology`. 98 """ 99 vertices = pd.Series(vertices) 100 assert ontology.check_include(vertices), f"vertices contain unrecognizable ID " 101 self._vert = vertices 102 self._ont = ontology 103 104 def stash(self): 105 """ 106 For retrieving the intermediate or final results. To remove mutability, it's turned to tuple. If you need a 107 diversion of processing, you can retrieve the intermediate result and make a new pathway. 108 109 :return: the stashed vertices' copy as a numpy array. 110 """ 111 return self._vert.copy() 112 113 def filter_by_level(self, fro: int = None, to: int = None, invert=False): 114 """ 115 Only retain the vertices within the level range, c style. 116 117 Levels are the max count from the current node to its leaves in the tree, staring from 0. 118 119 :param fro: the starting level, inclusive, default as None, meaning no limit. 120 :param to: the ending level, exclusive, default as None, meaning no limit. 121 :param invert: whether retain the otherwise non-descendents 122 """ 123 levels = self._ont.levels_of(self._vert) 124 if fro is not None: 125 levels = levels[levels >= fro] 126 if to is not None: 127 levels = levels[levels < to] 128 if invert: 129 self._vert = self._vert[~self._vert.isin(levels.index)] 130 else: 131 self._vert = levels.index 132 133 def filter_by_depth(self, fro: int = None, to: int = None, invert=False): 134 """ 135 Only retain the vertices within the depth range, c style. 136 137 Depths are the count from the current node to its root in the tree, staring from 0. 138 139 :param fro: the starting depth, inclusive, default as None, meaning no limit. 140 :param to: the ending depth, exclusive, default as None, meaning no limit. 141 :param invert: whether retain the otherwise non-descendents 142 """ 143 depths = self._ont.depths_of(self._vert) 144 if fro is not None: 145 depths = depths[depths >= fro] 146 if to is not None: 147 depths = depths[depths < to] 148 if invert: 149 self._vert = self._vert[~self._vert.isin(depths.index)] 150 else: 151 self._vert = depths.index 152 153 def filter_super(self): 154 """ 155 Remove any vertices that happen to be the ancestor of another. 156 """ 157 un = set.union(*map(set, self._ont.ancestors_of(self._vert))) 158 self._vert = self._vert[~self._vert.isin(un)] 159 160 def filter_sub(self): 161 """ 162 Remove any vertices that happen to be the descendent of another. 163 """ 164 vert = set(self._vert) 165 tf = [vert.isdisjoint(i) for i in self._ont.ancestors_of(self._vert)] 166 self._vert = self._vert[tf] 167 168 def filter_by_immediate_child_of(self, parents: typing.Iterable, invert=False): 169 """ 170 Only retain the direct children under some vertices, which is convenient when you have a big super node with 171 many branches below. 172 173 :param parents: the direct parent vertices. 174 :param invert: whether retain the otherwise non-descendents 175 """ 176 un = set.union(*map(set, self._ont.immediate_children_of(parents))) 177 if invert: 178 self._vert = self._vert[~self._vert.isin(un)] 179 else: 180 self._vert = self._vert[self._vert.isin(un)] 181 182 def filter_by_descendants_of(self, parents: typing.Iterable, include_parents=False, invert=False): 183 """ 184 Only retain the descendents of some vertices. 185 186 :param parents: the ancestors. 187 :param include_parents: whether to allow the parents to exist in the result, default as not. 188 :param invert: whether retain the otherwise non-descendents 189 """ 190 parents = set(parents) 191 tf = map(lambda p: not parents.isdisjoint(p), self._ont.ancestors_of(self._vert)) 192 if include_parents: 193 tf = [*tf] | self._vert.isin(parents) 194 if invert: 195 tf = [not i for i in tf] 196 self._vert = self._vert[tf] 197 198 def merge_by_level(self, thr): 199 """ 200 Merge the vertices until they are all above the min level or no merge can be done. 201 202 Levels are the max count from the current node to its leaves in the tree, staring from 0. 203 204 :param thr: the min level. 205 """ 206 vert = set() 207 for i, p, l in zip(self._vert, self._ont.ancestors_of(self._vert), self._ont.levels_of(self._vert)): 208 if l < thr: 209 vert.add(p[l - thr]) 210 else: 211 vert.add(i) 212 self._vert = pd.Series(list(vert)) 213 214 def merge_by_depth(self, thr): 215 """ 216 Merge the vertice until they are all below the max depth or no merge can be done. 217 218 Depths are the count from the current node to its root in the tree, staring from 0. 219 220 :param thr: the max depth. 221 """ 222 vert = set() 223 for i, p in zip(self._vert, self._ont.ancestors_of(self._vert)): 224 if len(p) > thr: 225 vert.add(p[thr]) 226 else: 227 vert.add(i) 228 self._vert = pd.Series(list(vert)) 229 230 231class GraphPacker: 232 """ 233 Given a directed graph (in adjacent matrix form), according to an ontology, 234 rearrange as a new graph with superior structures or just merge redundant edges. The projection is from 235 rows to columns 236 237 Taking brain graph as an example, a group of neurons project from regions to regions, 238 meaning edge (a, b) can have redundant occurrences with different weights. This is where merging needs to be done. 239 240 Sometimes, you want to look into their coarser regions' relations rather than the finer ones. This is where 241 rearrangement needs to be done. 242 """ 243 244 def __init__(self, adj_mat: pd.DataFrame, ontology: Ontology): 245 """ 246 :param adj_mat: the adjacent matrix in pandas dataframe, the projection is from rows to columns. 247 :param ontology: a derivation of the abstract class `Ontology`. 248 """ 249 # check adjacent matrix 250 assert ontology.check_include(adj_mat.index), f"rows contain unrecognizable ID" 251 assert ontology.check_include(adj_mat.columns), f"columns contain unrecognizable ID" 252 self._mat = adj_mat.to_numpy() 253 self._rows = adj_mat.index 254 self._cols = adj_mat.columns 255 self._ont = ontology 256 257 def pack(self, new_rows: typing.Iterable, new_cols: typing.Iterable, def_val: float = -1, 258 superior_as_complement: bool = False, aggr_func=lambda x: 1 / (1 / x).sum()): 259 """ 260 Specifying new rows and columns (usually generated by feeding the original to VertexPacker, but you can 261 provide your own list, long as it's within the ontology), 262 retrieve a new graph in adjacent matrix. 263 264 Packing usually involves merging redundant edges or edges inferior to a superior one. Sometimes, conflicts can 265 take place where the superior edge and inferior edge coexist. Normally this is an undefined behavior, but 266 considering in some ontology assignment, areas where the inferior don't cover in the superior, namely, 267 for set A containing set B and set C, A - (B|C) can be represented using the superior. So here, by turning this 268 on, you can always merge them. Otherwise, merge is done only for mutually excluded vertices. 269 270 :param new_rows: the new 'project from' vertices. 271 :param new_cols: the new 'project to' vertices. 272 :param def_val: the default value for non-connecting edges. 273 :param superior_as_complement: do merge for both superior and inferior vertices when encountered, 274 default as False. 275 :param aggr_func: the aggregation function. 276 :return: a new adjacent matrix as pandas dataframe. 277 """ 278 new_rows = pd.Series(new_rows) 279 new_cols = pd.Series(new_cols) 280 assert new_rows.is_unique, "new row items are not unique" 281 assert new_cols.is_unique, "new column items are not unique" 282 283 new_mat = np.ones([len(new_rows), len(new_cols)]) * def_val 284 285 def vertex_map_gen(all_vert, new_vert): 286 # prefilter 287 packer = VertexPacker(all_vert.unique(), self._ont) 288 packer.filter_by_descendants_of(new_vert, include_parents=True) 289 needed_vert = packer.stash() 290 # map 291 map = [] 292 for v in new_vert: 293 packer = VertexPacker(needed_vert, self._ont) 294 packer.filter_by_descendants_of([v], include_parents=True) 295 nodes = packer.stash() 296 if not superior_as_complement: # filter the sub nodes in this mode 297 packer = VertexPacker(nodes, self._ont) 298 packer.filter_sub() 299 nodes = packer.stash() 300 map.append(np.where(all_vert.isin(nodes))) 301 return map 302 303 row_map = vertex_map_gen(self._rows, new_rows) 304 col_map = vertex_map_gen(self._cols, new_cols) 305 306 # calculate the edges in the new matrix 307 for i, (row, fro_ind) in enumerate(zip(new_rows, row_map)): 308 if len(fro_ind) == 0: 309 continue 310 for j, (col, to_ind) in enumerate(zip(new_cols, col_map)): 311 if row == col or len(to_ind) == 0: 312 continue 313 dat = self._mat[fro_ind][:,to_ind].reshape(-1) 314 dat = dat[dat != def_val] 315 if len(dat) == 0: 316 continue 317 new_mat[i, j] = aggr_func(dat) 318 return pd.DataFrame(new_mat, index=new_rows, columns=new_cols)
12class Ontology: 13 """ 14 The prior knowledge about tree relationship among the vertices in graph, namely ontology, used for packing graph 15 vertices. 16This is a meta class that doesn't implement any function, so any subclass this with the same functions can use it 17 with other modules smoothly. 18 19 I used this as an abstract interface and make derivation for ccfv3 ontology in brain. 20 You can follow my subclass for your own. 21 22 I would recommend using pandas for tree representation for its speed and also because you can save some useful info 23 as appended columns. 24 """ 25 26 def __init__(self): 27 """ 28 Here, some preprocessing can be done make other functions easier and faster to operate. Also, you need to 29 ensure that this is actually a tree. 30 """ 31 pass 32 33 def check_include(self, vert: typing.Iterable): 34 """ 35 check if the vertices given is included 36 :param vert: the vertices to check 37 :return: True or False. 38 """ 39 pass 40 41 def levels_of(self, vert: typing.Iterable) -> pd.Series: 42 """ 43 Level is defined as the largest count from the current node to its leaves in the tree, starting from 0. 44 :param vert: the vertices to check 45 :return: the levels of each vertex in the ontology 46 """ 47 pass 48 49 def depths_of(self, vert: typing.Iterable) -> pd.Series: 50 """ 51 Depth is defined as the count from the current node to its root in the tree, starting from 0. 52 :param vert: the vertices to check 53 :return: the depths of each vertex in the ontology 54 """ 55 pass 56 57 def ancestors_of(self, vert: typing.Iterable) -> pd.Series: 58 """ 59 The parents of each vertex as a list, starting from the tree root to its immediate parent, the length of which 60 equals the depth. 61 62 :param vert: the vertices to check 63 :return: the lists of parents of each vertex in the ontology 64 """ 65 pass 66 67 def immediate_children_of(self, vert: typing.Iterable) -> pd.Series: 68 """ 69 The immediate children of each vertex as a list, starting from the tree root to its direct parent. 70 71 :param vert: the vertices to check 72 :return: the lists of children of each vertex in the ontology 73 """
The prior knowledge about tree relationship among the vertices in graph, namely ontology, used for packing graph vertices. This is a meta class that doesn't implement any function, so any subclass this with the same functions can use it with other modules smoothly.
I used this as an abstract interface and make derivation for ccfv3 ontology in brain.
You can follow my subclass for your own.
I would recommend using pandas for tree representation for its speed and also because you can save some useful info
as appended columns.
26 def __init__(self): 27 """ 28 Here, some preprocessing can be done make other functions easier and faster to operate. Also, you need to 29 ensure that this is actually a tree. 30 """ 31 pass
Here, some preprocessing can be done make other functions easier and faster to operate. Also, you need to ensure that this is actually a tree.
33 def check_include(self, vert: typing.Iterable): 34 """ 35 check if the vertices given is included 36 :param vert: the vertices to check 37 :return: True or False. 38 """ 39 pass
check if the vertices given is included
Parameters
- vert: the vertices to check
Returns
True or False.
41 def levels_of(self, vert: typing.Iterable) -> pd.Series: 42 """ 43 Level is defined as the largest count from the current node to its leaves in the tree, starting from 0. 44 :param vert: the vertices to check 45 :return: the levels of each vertex in the ontology 46 """ 47 pass
Level is defined as the largest count from the current node to its leaves in the tree, starting from 0.
Parameters
- vert: the vertices to check
Returns
the levels of each vertex in the ontology
49 def depths_of(self, vert: typing.Iterable) -> pd.Series: 50 """ 51 Depth is defined as the count from the current node to its root in the tree, starting from 0. 52 :param vert: the vertices to check 53 :return: the depths of each vertex in the ontology 54 """ 55 pass
Depth is defined as the count from the current node to its root in the tree, starting from 0.
Parameters
- vert: the vertices to check
Returns
the depths of each vertex in the ontology
57 def ancestors_of(self, vert: typing.Iterable) -> pd.Series: 58 """ 59 The parents of each vertex as a list, starting from the tree root to its immediate parent, the length of which 60 equals the depth. 61 62 :param vert: the vertices to check 63 :return: the lists of parents of each vertex in the ontology 64 """ 65 pass
The parents of each vertex as a list, starting from the tree root to its immediate parent, the length of which equals the depth.
Parameters
- vert: the vertices to check
Returns
the lists of parents of each vertex in the ontology
67 def immediate_children_of(self, vert: typing.Iterable) -> pd.Series: 68 """ 69 The immediate children of each vertex as a list, starting from the tree root to its direct parent. 70 71 :param vert: the vertices to check 72 :return: the lists of children of each vertex in the ontology 73 """
The immediate children of each vertex as a list, starting from the tree root to its direct parent.
Parameters
- vert: the vertices to check
Returns
the lists of children of each vertex in the ontology
76class VertexPacker: 77 """ 78 Given a set of vertices in a graph, e.g. brain structures in a macroscopic brain connectome, 79 rearrange them by filtering and merging into a new set, based on a filtering rule and according 80 to an ontology. 81 82 This is the precursor step before packing the whole graph. A directed graph may not share its rows 83 with columns, so this might need to be done once for each, even with different rules. 84 85 Also, for a complexed graph, where you may expect heterogeneous level hierarchies, you can pack the graph multiple 86 times, and their vertices apply different rules naturally. This way, you'll also utilize this decoupled facility. 87 88 This class uses a state machine functionality, by which each time you trigger the alteration each the 89 stash changes. When you need a diversion or something you can retrieve the intermediate result and start anew. 90 """ 91 92 def __init__(self, vertices: typing.Iterable, ontology: Ontology): 93 """ 94 Initialize the vertex packer with an initial set of vertices and ontology. It also checks the viability of 95 the vertices. 96 97 :param vertices: a set of entities in the ontology to filter and merge. 98 :param ontology: a derivation of the abstract class `Ontology`. 99 """ 100 vertices = pd.Series(vertices) 101 assert ontology.check_include(vertices), f"vertices contain unrecognizable ID " 102 self._vert = vertices 103 self._ont = ontology 104 105 def stash(self): 106 """ 107 For retrieving the intermediate or final results. To remove mutability, it's turned to tuple. If you need a 108 diversion of processing, you can retrieve the intermediate result and make a new pathway. 109 110 :return: the stashed vertices' copy as a numpy array. 111 """ 112 return self._vert.copy() 113 114 def filter_by_level(self, fro: int = None, to: int = None, invert=False): 115 """ 116 Only retain the vertices within the level range, c style. 117 118 Levels are the max count from the current node to its leaves in the tree, staring from 0. 119 120 :param fro: the starting level, inclusive, default as None, meaning no limit. 121 :param to: the ending level, exclusive, default as None, meaning no limit. 122 :param invert: whether retain the otherwise non-descendents 123 """ 124 levels = self._ont.levels_of(self._vert) 125 if fro is not None: 126 levels = levels[levels >= fro] 127 if to is not None: 128 levels = levels[levels < to] 129 if invert: 130 self._vert = self._vert[~self._vert.isin(levels.index)] 131 else: 132 self._vert = levels.index 133 134 def filter_by_depth(self, fro: int = None, to: int = None, invert=False): 135 """ 136 Only retain the vertices within the depth range, c style. 137 138 Depths are the count from the current node to its root in the tree, staring from 0. 139 140 :param fro: the starting depth, inclusive, default as None, meaning no limit. 141 :param to: the ending depth, exclusive, default as None, meaning no limit. 142 :param invert: whether retain the otherwise non-descendents 143 """ 144 depths = self._ont.depths_of(self._vert) 145 if fro is not None: 146 depths = depths[depths >= fro] 147 if to is not None: 148 depths = depths[depths < to] 149 if invert: 150 self._vert = self._vert[~self._vert.isin(depths.index)] 151 else: 152 self._vert = depths.index 153 154 def filter_super(self): 155 """ 156 Remove any vertices that happen to be the ancestor of another. 157 """ 158 un = set.union(*map(set, self._ont.ancestors_of(self._vert))) 159 self._vert = self._vert[~self._vert.isin(un)] 160 161 def filter_sub(self): 162 """ 163 Remove any vertices that happen to be the descendent of another. 164 """ 165 vert = set(self._vert) 166 tf = [vert.isdisjoint(i) for i in self._ont.ancestors_of(self._vert)] 167 self._vert = self._vert[tf] 168 169 def filter_by_immediate_child_of(self, parents: typing.Iterable, invert=False): 170 """ 171 Only retain the direct children under some vertices, which is convenient when you have a big super node with 172 many branches below. 173 174 :param parents: the direct parent vertices. 175 :param invert: whether retain the otherwise non-descendents 176 """ 177 un = set.union(*map(set, self._ont.immediate_children_of(parents))) 178 if invert: 179 self._vert = self._vert[~self._vert.isin(un)] 180 else: 181 self._vert = self._vert[self._vert.isin(un)] 182 183 def filter_by_descendants_of(self, parents: typing.Iterable, include_parents=False, invert=False): 184 """ 185 Only retain the descendents of some vertices. 186 187 :param parents: the ancestors. 188 :param include_parents: whether to allow the parents to exist in the result, default as not. 189 :param invert: whether retain the otherwise non-descendents 190 """ 191 parents = set(parents) 192 tf = map(lambda p: not parents.isdisjoint(p), self._ont.ancestors_of(self._vert)) 193 if include_parents: 194 tf = [*tf] | self._vert.isin(parents) 195 if invert: 196 tf = [not i for i in tf] 197 self._vert = self._vert[tf] 198 199 def merge_by_level(self, thr): 200 """ 201 Merge the vertices until they are all above the min level or no merge can be done. 202 203 Levels are the max count from the current node to its leaves in the tree, staring from 0. 204 205 :param thr: the min level. 206 """ 207 vert = set() 208 for i, p, l in zip(self._vert, self._ont.ancestors_of(self._vert), self._ont.levels_of(self._vert)): 209 if l < thr: 210 vert.add(p[l - thr]) 211 else: 212 vert.add(i) 213 self._vert = pd.Series(list(vert)) 214 215 def merge_by_depth(self, thr): 216 """ 217 Merge the vertice until they are all below the max depth or no merge can be done. 218 219 Depths are the count from the current node to its root in the tree, staring from 0. 220 221 :param thr: the max depth. 222 """ 223 vert = set() 224 for i, p in zip(self._vert, self._ont.ancestors_of(self._vert)): 225 if len(p) > thr: 226 vert.add(p[thr]) 227 else: 228 vert.add(i) 229 self._vert = pd.Series(list(vert))
Given a set of vertices in a graph, e.g. brain structures in a macroscopic brain connectome, rearrange them by filtering and merging into a new set, based on a filtering rule and according to an ontology.
This is the precursor step before packing the whole graph. A directed graph may not share its rows with columns, so this might need to be done once for each, even with different rules.
Also, for a complexed graph, where you may expect heterogeneous level hierarchies, you can pack the graph multiple times, and their vertices apply different rules naturally. This way, you'll also utilize this decoupled facility.
This class uses a state machine functionality, by which each time you trigger the alteration each the stash changes. When you need a diversion or something you can retrieve the intermediate result and start anew.
92 def __init__(self, vertices: typing.Iterable, ontology: Ontology): 93 """ 94 Initialize the vertex packer with an initial set of vertices and ontology. It also checks the viability of 95 the vertices. 96 97 :param vertices: a set of entities in the ontology to filter and merge. 98 :param ontology: a derivation of the abstract class `Ontology`. 99 """ 100 vertices = pd.Series(vertices) 101 assert ontology.check_include(vertices), f"vertices contain unrecognizable ID " 102 self._vert = vertices 103 self._ont = ontology
Initialize the vertex packer with an initial set of vertices and ontology. It also checks the viability of the vertices.
Parameters
- vertices: a set of entities in the ontology to filter and merge.
- ontology: a derivation of the abstract class
Ontology
.
105 def stash(self): 106 """ 107 For retrieving the intermediate or final results. To remove mutability, it's turned to tuple. If you need a 108 diversion of processing, you can retrieve the intermediate result and make a new pathway. 109 110 :return: the stashed vertices' copy as a numpy array. 111 """ 112 return self._vert.copy()
For retrieving the intermediate or final results. To remove mutability, it's turned to tuple. If you need a diversion of processing, you can retrieve the intermediate result and make a new pathway.
Returns
the stashed vertices' copy as a numpy array.
114 def filter_by_level(self, fro: int = None, to: int = None, invert=False): 115 """ 116 Only retain the vertices within the level range, c style. 117 118 Levels are the max count from the current node to its leaves in the tree, staring from 0. 119 120 :param fro: the starting level, inclusive, default as None, meaning no limit. 121 :param to: the ending level, exclusive, default as None, meaning no limit. 122 :param invert: whether retain the otherwise non-descendents 123 """ 124 levels = self._ont.levels_of(self._vert) 125 if fro is not None: 126 levels = levels[levels >= fro] 127 if to is not None: 128 levels = levels[levels < to] 129 if invert: 130 self._vert = self._vert[~self._vert.isin(levels.index)] 131 else: 132 self._vert = levels.index
Only retain the vertices within the level range, c style.
Levels are the max count from the current node to its leaves in the tree, staring from 0.
Parameters
- fro: the starting level, inclusive, default as None, meaning no limit.
- to: the ending level, exclusive, default as None, meaning no limit.
- invert: whether retain the otherwise non-descendents
134 def filter_by_depth(self, fro: int = None, to: int = None, invert=False): 135 """ 136 Only retain the vertices within the depth range, c style. 137 138 Depths are the count from the current node to its root in the tree, staring from 0. 139 140 :param fro: the starting depth, inclusive, default as None, meaning no limit. 141 :param to: the ending depth, exclusive, default as None, meaning no limit. 142 :param invert: whether retain the otherwise non-descendents 143 """ 144 depths = self._ont.depths_of(self._vert) 145 if fro is not None: 146 depths = depths[depths >= fro] 147 if to is not None: 148 depths = depths[depths < to] 149 if invert: 150 self._vert = self._vert[~self._vert.isin(depths.index)] 151 else: 152 self._vert = depths.index
Only retain the vertices within the depth range, c style.
Depths are the count from the current node to its root in the tree, staring from 0.
Parameters
- fro: the starting depth, inclusive, default as None, meaning no limit.
- to: the ending depth, exclusive, default as None, meaning no limit.
- invert: whether retain the otherwise non-descendents
154 def filter_super(self): 155 """ 156 Remove any vertices that happen to be the ancestor of another. 157 """ 158 un = set.union(*map(set, self._ont.ancestors_of(self._vert))) 159 self._vert = self._vert[~self._vert.isin(un)]
Remove any vertices that happen to be the ancestor of another.
161 def filter_sub(self): 162 """ 163 Remove any vertices that happen to be the descendent of another. 164 """ 165 vert = set(self._vert) 166 tf = [vert.isdisjoint(i) for i in self._ont.ancestors_of(self._vert)] 167 self._vert = self._vert[tf]
Remove any vertices that happen to be the descendent of another.
169 def filter_by_immediate_child_of(self, parents: typing.Iterable, invert=False): 170 """ 171 Only retain the direct children under some vertices, which is convenient when you have a big super node with 172 many branches below. 173 174 :param parents: the direct parent vertices. 175 :param invert: whether retain the otherwise non-descendents 176 """ 177 un = set.union(*map(set, self._ont.immediate_children_of(parents))) 178 if invert: 179 self._vert = self._vert[~self._vert.isin(un)] 180 else: 181 self._vert = self._vert[self._vert.isin(un)]
Only retain the direct children under some vertices, which is convenient when you have a big super node with many branches below.
Parameters
- parents: the direct parent vertices.
- invert: whether retain the otherwise non-descendents
183 def filter_by_descendants_of(self, parents: typing.Iterable, include_parents=False, invert=False): 184 """ 185 Only retain the descendents of some vertices. 186 187 :param parents: the ancestors. 188 :param include_parents: whether to allow the parents to exist in the result, default as not. 189 :param invert: whether retain the otherwise non-descendents 190 """ 191 parents = set(parents) 192 tf = map(lambda p: not parents.isdisjoint(p), self._ont.ancestors_of(self._vert)) 193 if include_parents: 194 tf = [*tf] | self._vert.isin(parents) 195 if invert: 196 tf = [not i for i in tf] 197 self._vert = self._vert[tf]
Only retain the descendents of some vertices.
Parameters
- parents: the ancestors.
- include_parents: whether to allow the parents to exist in the result, default as not.
- invert: whether retain the otherwise non-descendents
199 def merge_by_level(self, thr): 200 """ 201 Merge the vertices until they are all above the min level or no merge can be done. 202 203 Levels are the max count from the current node to its leaves in the tree, staring from 0. 204 205 :param thr: the min level. 206 """ 207 vert = set() 208 for i, p, l in zip(self._vert, self._ont.ancestors_of(self._vert), self._ont.levels_of(self._vert)): 209 if l < thr: 210 vert.add(p[l - thr]) 211 else: 212 vert.add(i) 213 self._vert = pd.Series(list(vert))
Merge the vertices until they are all above the min level or no merge can be done.
Levels are the max count from the current node to its leaves in the tree, staring from 0.
Parameters
- thr: the min level.
215 def merge_by_depth(self, thr): 216 """ 217 Merge the vertice until they are all below the max depth or no merge can be done. 218 219 Depths are the count from the current node to its root in the tree, staring from 0. 220 221 :param thr: the max depth. 222 """ 223 vert = set() 224 for i, p in zip(self._vert, self._ont.ancestors_of(self._vert)): 225 if len(p) > thr: 226 vert.add(p[thr]) 227 else: 228 vert.add(i) 229 self._vert = pd.Series(list(vert))
Merge the vertice until they are all below the max depth or no merge can be done.
Depths are the count from the current node to its root in the tree, staring from 0.
Parameters
- thr: the max depth.
232class GraphPacker: 233 """ 234 Given a directed graph (in adjacent matrix form), according to an ontology, 235 rearrange as a new graph with superior structures or just merge redundant edges. The projection is from 236 rows to columns 237 238 Taking brain graph as an example, a group of neurons project from regions to regions, 239 meaning edge (a, b) can have redundant occurrences with different weights. This is where merging needs to be done. 240 241 Sometimes, you want to look into their coarser regions' relations rather than the finer ones. This is where 242 rearrangement needs to be done. 243 """ 244 245 def __init__(self, adj_mat: pd.DataFrame, ontology: Ontology): 246 """ 247 :param adj_mat: the adjacent matrix in pandas dataframe, the projection is from rows to columns. 248 :param ontology: a derivation of the abstract class `Ontology`. 249 """ 250 # check adjacent matrix 251 assert ontology.check_include(adj_mat.index), f"rows contain unrecognizable ID" 252 assert ontology.check_include(adj_mat.columns), f"columns contain unrecognizable ID" 253 self._mat = adj_mat.to_numpy() 254 self._rows = adj_mat.index 255 self._cols = adj_mat.columns 256 self._ont = ontology 257 258 def pack(self, new_rows: typing.Iterable, new_cols: typing.Iterable, def_val: float = -1, 259 superior_as_complement: bool = False, aggr_func=lambda x: 1 / (1 / x).sum()): 260 """ 261 Specifying new rows and columns (usually generated by feeding the original to VertexPacker, but you can 262 provide your own list, long as it's within the ontology), 263 retrieve a new graph in adjacent matrix. 264 265 Packing usually involves merging redundant edges or edges inferior to a superior one. Sometimes, conflicts can 266 take place where the superior edge and inferior edge coexist. Normally this is an undefined behavior, but 267 considering in some ontology assignment, areas where the inferior don't cover in the superior, namely, 268 for set A containing set B and set C, A - (B|C) can be represented using the superior. So here, by turning this 269 on, you can always merge them. Otherwise, merge is done only for mutually excluded vertices. 270 271 :param new_rows: the new 'project from' vertices. 272 :param new_cols: the new 'project to' vertices. 273 :param def_val: the default value for non-connecting edges. 274 :param superior_as_complement: do merge for both superior and inferior vertices when encountered, 275 default as False. 276 :param aggr_func: the aggregation function. 277 :return: a new adjacent matrix as pandas dataframe. 278 """ 279 new_rows = pd.Series(new_rows) 280 new_cols = pd.Series(new_cols) 281 assert new_rows.is_unique, "new row items are not unique" 282 assert new_cols.is_unique, "new column items are not unique" 283 284 new_mat = np.ones([len(new_rows), len(new_cols)]) * def_val 285 286 def vertex_map_gen(all_vert, new_vert): 287 # prefilter 288 packer = VertexPacker(all_vert.unique(), self._ont) 289 packer.filter_by_descendants_of(new_vert, include_parents=True) 290 needed_vert = packer.stash() 291 # map 292 map = [] 293 for v in new_vert: 294 packer = VertexPacker(needed_vert, self._ont) 295 packer.filter_by_descendants_of([v], include_parents=True) 296 nodes = packer.stash() 297 if not superior_as_complement: # filter the sub nodes in this mode 298 packer = VertexPacker(nodes, self._ont) 299 packer.filter_sub() 300 nodes = packer.stash() 301 map.append(np.where(all_vert.isin(nodes))) 302 return map 303 304 row_map = vertex_map_gen(self._rows, new_rows) 305 col_map = vertex_map_gen(self._cols, new_cols) 306 307 # calculate the edges in the new matrix 308 for i, (row, fro_ind) in enumerate(zip(new_rows, row_map)): 309 if len(fro_ind) == 0: 310 continue 311 for j, (col, to_ind) in enumerate(zip(new_cols, col_map)): 312 if row == col or len(to_ind) == 0: 313 continue 314 dat = self._mat[fro_ind][:,to_ind].reshape(-1) 315 dat = dat[dat != def_val] 316 if len(dat) == 0: 317 continue 318 new_mat[i, j] = aggr_func(dat) 319 return pd.DataFrame(new_mat, index=new_rows, columns=new_cols)
Given a directed graph (in adjacent matrix form), according to an ontology, rearrange as a new graph with superior structures or just merge redundant edges. The projection is from rows to columns
Taking brain graph as an example, a group of neurons project from regions to regions, meaning edge (a, b) can have redundant occurrences with different weights. This is where merging needs to be done.
Sometimes, you want to look into their coarser regions' relations rather than the finer ones. This is where rearrangement needs to be done.
245 def __init__(self, adj_mat: pd.DataFrame, ontology: Ontology): 246 """ 247 :param adj_mat: the adjacent matrix in pandas dataframe, the projection is from rows to columns. 248 :param ontology: a derivation of the abstract class `Ontology`. 249 """ 250 # check adjacent matrix 251 assert ontology.check_include(adj_mat.index), f"rows contain unrecognizable ID" 252 assert ontology.check_include(adj_mat.columns), f"columns contain unrecognizable ID" 253 self._mat = adj_mat.to_numpy() 254 self._rows = adj_mat.index 255 self._cols = adj_mat.columns 256 self._ont = ontology
Parameters
- adj_mat: the adjacent matrix in pandas dataframe, the projection is from rows to columns.
- ontology: a derivation of the abstract class
Ontology
.
258 def pack(self, new_rows: typing.Iterable, new_cols: typing.Iterable, def_val: float = -1, 259 superior_as_complement: bool = False, aggr_func=lambda x: 1 / (1 / x).sum()): 260 """ 261 Specifying new rows and columns (usually generated by feeding the original to VertexPacker, but you can 262 provide your own list, long as it's within the ontology), 263 retrieve a new graph in adjacent matrix. 264 265 Packing usually involves merging redundant edges or edges inferior to a superior one. Sometimes, conflicts can 266 take place where the superior edge and inferior edge coexist. Normally this is an undefined behavior, but 267 considering in some ontology assignment, areas where the inferior don't cover in the superior, namely, 268 for set A containing set B and set C, A - (B|C) can be represented using the superior. So here, by turning this 269 on, you can always merge them. Otherwise, merge is done only for mutually excluded vertices. 270 271 :param new_rows: the new 'project from' vertices. 272 :param new_cols: the new 'project to' vertices. 273 :param def_val: the default value for non-connecting edges. 274 :param superior_as_complement: do merge for both superior and inferior vertices when encountered, 275 default as False. 276 :param aggr_func: the aggregation function. 277 :return: a new adjacent matrix as pandas dataframe. 278 """ 279 new_rows = pd.Series(new_rows) 280 new_cols = pd.Series(new_cols) 281 assert new_rows.is_unique, "new row items are not unique" 282 assert new_cols.is_unique, "new column items are not unique" 283 284 new_mat = np.ones([len(new_rows), len(new_cols)]) * def_val 285 286 def vertex_map_gen(all_vert, new_vert): 287 # prefilter 288 packer = VertexPacker(all_vert.unique(), self._ont) 289 packer.filter_by_descendants_of(new_vert, include_parents=True) 290 needed_vert = packer.stash() 291 # map 292 map = [] 293 for v in new_vert: 294 packer = VertexPacker(needed_vert, self._ont) 295 packer.filter_by_descendants_of([v], include_parents=True) 296 nodes = packer.stash() 297 if not superior_as_complement: # filter the sub nodes in this mode 298 packer = VertexPacker(nodes, self._ont) 299 packer.filter_sub() 300 nodes = packer.stash() 301 map.append(np.where(all_vert.isin(nodes))) 302 return map 303 304 row_map = vertex_map_gen(self._rows, new_rows) 305 col_map = vertex_map_gen(self._cols, new_cols) 306 307 # calculate the edges in the new matrix 308 for i, (row, fro_ind) in enumerate(zip(new_rows, row_map)): 309 if len(fro_ind) == 0: 310 continue 311 for j, (col, to_ind) in enumerate(zip(new_cols, col_map)): 312 if row == col or len(to_ind) == 0: 313 continue 314 dat = self._mat[fro_ind][:,to_ind].reshape(-1) 315 dat = dat[dat != def_val] 316 if len(dat) == 0: 317 continue 318 new_mat[i, j] = aggr_func(dat) 319 return pd.DataFrame(new_mat, index=new_rows, columns=new_cols)
Specifying new rows and columns (usually generated by feeding the original to VertexPacker, but you can provide your own list, long as it's within the ontology), retrieve a new graph in adjacent matrix.
Packing usually involves merging redundant edges or edges inferior to a superior one. Sometimes, conflicts can take place where the superior edge and inferior edge coexist. Normally this is an undefined behavior, but considering in some ontology assignment, areas where the inferior don't cover in the superior, namely, for set A containing set B and set C, A - (B|C) can be represented using the superior. So here, by turning this on, you can always merge them. Otherwise, merge is done only for mutually excluded vertices.
Parameters
- new_rows: the new 'project from' vertices.
- new_cols: the new 'project to' vertices.
- def_val: the default value for non-connecting edges.
- superior_as_complement: do merge for both superior and inferior vertices when encountered, default as False.
- aggr_func: the aggregation function.
Returns
a new adjacent matrix as pandas dataframe.