nova-clone 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. nova_clone/__init__.py +1 -0
  2. nova_clone/algorithm/__init__.py +2 -0
  3. nova_clone/algorithm/_build_intbc_graph.py +135 -0
  4. nova_clone/algorithm/_call_clones_from_coresets.py +235 -0
  5. nova_clone/algorithm/_clone_call_ops.py +258 -0
  6. nova_clone/algorithm/_clone_calling_algorithm.py +1571 -0
  7. nova_clone/algorithm/_compute_cellbc_vs_coreset_overlap_matrix.py +143 -0
  8. nova_clone/algorithm/_compute_coreset_co_assignment_matrix.py +170 -0
  9. nova_clone/algorithm/_find_maximal_coresets.py +428 -0
  10. nova_clone/algorithm/_keep_reliable_edges.py +202 -0
  11. nova_clone/algorithm/_make_coresets_disjoint.py +114 -0
  12. nova_clone/algorithm/_prune_coresets.py +164 -0
  13. nova_clone/algorithm/_prune_custom_intbcs.py +150 -0
  14. nova_clone/algorithm/_prune_likely_homoplastic_intbcs.py +162 -0
  15. nova_clone/algorithm/_prune_likely_sequencing_errors.py +201 -0
  16. nova_clone/algorithm/_prune_low_cellbc_count_intbcs.py +154 -0
  17. nova_clone/algorithm/_prune_low_umi_count_intbcs.py +152 -0
  18. nova_clone/algorithm/_types.py +150 -0
  19. nova_clone/algorithm/_utils.py +53 -0
  20. nova_clone/algorithm/cassiopeia/__init__.py +1 -0
  21. nova_clone/algorithm/cassiopeia/_call_lineage_groups.py +270 -0
  22. nova_clone/caching/__init__.py +24 -0
  23. nova_clone/caching/_cached.py +175 -0
  24. nova_clone/caching/_cached_computation.py +369 -0
  25. nova_clone/caching/_cached_parallel_computation.py +428 -0
  26. nova_clone/caching/_common.py +200 -0
  27. nova_clone/data/__init__.py +1 -0
  28. nova_clone/qc/__init__.py +179 -0
  29. nova_clone/qc/_clone_intbc_sets.py +234 -0
  30. nova_clone/qc/_compare_clone_calls.py +218 -0
  31. nova_clone/qc/_data_retention.py +175 -0
  32. nova_clone/qc/_homoplasy.py +81 -0
  33. nova_clone/qc/_inter_clonal_kinship.py +60 -0
  34. nova_clone/qc/_intra_clonal_kinship.py +172 -0
  35. nova_clone/qc/_multiplet_rate.py +158 -0
  36. nova_clone/qc/_number_of_clones.py +153 -0
  37. nova_clone/qc/_pre_qc.py +485 -0
  38. nova_clone/simulation/HDBSCAN_method_adjusted.py +68 -0
  39. nova_clone/simulation/__init__.py +0 -0
  40. nova_clone/simulation/_caching.py +40 -0
  41. nova_clone/simulation/benchmarking.py +1221 -0
  42. nova_clone/simulation/clone_to_sequences.py +408 -0
  43. nova_clone/simulation/creating_tree.py +416 -0
  44. nova_clone/simulation/evaluation.py +344 -0
  45. nova_clone/simulation/jaccard_hierarchical_by_intBC.py +78 -0
  46. nova_clone/simulation/plot_benchmark_lines.py +226 -0
  47. nova_clone/simulation/slice_benchmark.py +640 -0
  48. nova_clone/simulation/slice_benchmark_clone_calling.py +68 -0
  49. nova_clone-0.1.0.dist-info/METADATA +298 -0
  50. nova_clone-0.1.0.dist-info/RECORD +53 -0
  51. nova_clone-0.1.0.dist-info/WHEEL +5 -0
  52. nova_clone-0.1.0.dist-info/licenses/LICENSE +28 -0
  53. nova_clone-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,428 @@
1
+ """
2
+ This module implements finding of coresets int the intBC graph.
3
+
4
+ A "coreset" is a set of intBCs which exist in one and only one clone. Thus,
5
+ coresets are signatures left by the clones in the intBC graph. To find
6
+ these signatures, we use the following characterization of coresets:
7
+
8
+ Suppose intbc_1, ..., intbc_k are intBCs all of which co-occurr in _some_
9
+ clone. Then, if k is large enough (typically k>=2), then they must co-occurr
10
+ in the *same* clone, i.e. {intbc_1, ..., intbc_k} is a coreset. This is because
11
+ homoplasy only cause clones to share at most one intBC (with high probability).
12
+ Moreover, one can show that if A and B are two coresets that intersect in at
13
+ least a certain number of intBCs (typically k >= 2), then they represent the
14
+ same clone and their union is a coreset for that clone.
15
+
16
+ The main function exposed by this module is `keep_reliable_edges`, and it finds
17
+ maximal coresets in the intBC graph using the above two properties.
18
+ """
19
+
20
+ import logging
21
+ import sys
22
+ import time
23
+ from typing import Set
24
+
25
+ from multiset import Multiset
26
+ import networkx as nx
27
+ import numpy as np
28
+ import tqdm
29
+
30
+ from ._types import *
31
+
32
+
33
+ def _init_logger():
34
+ logger = logging.getLogger(__name__)
35
+ logger.setLevel(logging.INFO)
36
+ fmt_str = "[%(asctime)s] - %(name)s - %(levelname)s - %(message)s"
37
+ formatter = logging.Formatter(fmt_str)
38
+
39
+ consoleHandler = logging.StreamHandler(sys.stdout)
40
+ consoleHandler.setFormatter(formatter)
41
+ logger.addHandler(consoleHandler)
42
+
43
+
44
+ _init_logger()
45
+
46
+
47
+ class FindMaximalCoresetsReturnType(NamedTuple):
48
+ """
49
+ Object returned by the `find_maximal_coresets` function.
50
+
51
+ Attributes:
52
+ maximal_coresets: the maximal coresets found.
53
+ total_time: Total time taken.
54
+ """
55
+
56
+ maximal_coresets: List[Coreset[str]]
57
+ total_time: float
58
+
59
+
60
+ def maximum_cliques_containing_node(
61
+ graph: GraphType,
62
+ node: str,
63
+ ) -> List[List[str]]:
64
+ """
65
+ Find all maximum cliques containing the given node.
66
+
67
+ The `graph` must be a digraph.
68
+ """
69
+ # We need to create a nx.Graph to run the clique finding method.
70
+ edge_counts, node_counts = graph
71
+ neighboring_nodes = sorted(list(edge_counts[node].keys()))
72
+ if len(neighboring_nodes) == 0:
73
+ # Vertex is isolated
74
+ return [tuple([node])]
75
+ G = nx.Graph()
76
+ G.add_nodes_from(neighboring_nodes)
77
+ edges = []
78
+ # Check DiGraph condition for `node` node.
79
+ for x in neighboring_nodes:
80
+ if x != node:
81
+ assert x in edge_counts[node] # Check DiGraph condition.
82
+ assert node in edge_counts[x] # Check DiGraph condition.
83
+ for x in neighboring_nodes:
84
+ for y in neighboring_nodes:
85
+ if y in edge_counts[x]:
86
+ assert x in edge_counts[y] # Check DiGraph condition.
87
+ if x < y:
88
+ edges.append((x, y))
89
+ G.add_edges_from(edges)
90
+ maximal_cliques = list(nx.find_cliques(G))
91
+ max_clique_size = max([len(x) for x in maximal_cliques])
92
+ maximum_cliques = [x for x in maximal_cliques if len(x) == max_clique_size]
93
+ # We need to add back in the original node.
94
+ # We sort for deterministic output.
95
+ maximum_cliques = sorted([tuple(sorted([node] + x)) for x in maximum_cliques])
96
+ return maximum_cliques
97
+
98
+
99
+ def test_maximum_cliques_containing_node():
100
+ graph = (
101
+ {
102
+ "1": {"2": 10, "6": 10},
103
+ "2": {"1": 10, "3": 10, "4": 10, "5": 10, "6": 10},
104
+ "3": {"2": 10, "4": 10, "5": 10},
105
+ "4": {"2": 10, "3": 10, "5": 10},
106
+ "5": {"2": 10, "3": 10, "4": 10},
107
+ "6": {"1": 10, "2": 10},
108
+ },
109
+ {
110
+ "1": 100,
111
+ "2": 100,
112
+ "3": 100,
113
+ "4": 100,
114
+ "5": 100,
115
+ "6": 100,
116
+ },
117
+ )
118
+ cliques = maximum_cliques_containing_node(
119
+ graph=graph,
120
+ node="2",
121
+ )
122
+ assert cliques == [("2", "3", "4", "5")]
123
+ cliques = maximum_cliques_containing_node(
124
+ graph=graph,
125
+ node="1",
126
+ )
127
+ assert cliques == [("1", "2", "6")]
128
+
129
+
130
+ def find_maximum_cliques_over_all_nodes(
131
+ graph: GraphType,
132
+ ) -> List[Set[str]]:
133
+ """
134
+ List of all maximum cliques per node. Repetitions excluded.
135
+
136
+ If a node has more than one maximum clique, the lexicographically smallest
137
+ one is chosen.
138
+ """
139
+ maximum_clique_for_node = {}
140
+ nodes = list(graph[1].keys())
141
+ cliques_containing_node = {node: set() for node in nodes}
142
+ nodes_with_more_than_one_maximum_clique = {}
143
+ maximum_clique_counts = defaultdict(int)
144
+
145
+ for node in tqdm.tqdm(nodes):
146
+ cliques = maximum_cliques_containing_node(graph, node)
147
+ if len(cliques) > 1:
148
+ nodes_with_more_than_one_maximum_clique[node] = cliques
149
+ clique = cliques[0]
150
+ maximum_clique_for_node[node] = clique
151
+ for node in clique:
152
+ cliques_containing_node[node].add(clique)
153
+ maximum_clique_counts[clique] += 1
154
+
155
+ # Check that any two maximum cliques are non-included. We do this by
156
+ # iterating over each node, and looking at cliques covering this node.
157
+ for node, maximum_cliques in cliques_containing_node.items():
158
+ maximum_cliques = list(maximum_cliques)
159
+ for i, c1 in enumerate(maximum_cliques):
160
+ for j in range(len(maximum_cliques)):
161
+ if j == i:
162
+ continue
163
+ c2 = maximum_cliques[j]
164
+ if set(c1).issubset(c2):
165
+ raise Exception(f"Maximum clique {c1} is a subset of {c2}!")
166
+
167
+ cliques = list(maximum_clique_counts.keys())
168
+ cliques = [set(clique) for clique in cliques]
169
+ return cliques
170
+
171
+
172
+ def test_find_maximum_cliques_over_all_nodes():
173
+ graph = (
174
+ {
175
+ "1": {"2": 10, "6": 10},
176
+ "2": {"1": 10, "3": 10, "4": 10, "5": 10, "6": 10},
177
+ "3": {"2": 10, "4": 10, "5": 10},
178
+ "4": {"2": 10, "3": 10, "5": 10},
179
+ "5": {"2": 10, "3": 10, "4": 10},
180
+ "6": {"1": 10, "2": 10},
181
+ },
182
+ {
183
+ "1": 100,
184
+ "2": 100,
185
+ "3": 100,
186
+ "4": 100,
187
+ "5": 100,
188
+ "6": 100,
189
+ },
190
+ )
191
+ cliques = find_maximum_cliques_over_all_nodes(
192
+ graph=graph,
193
+ )
194
+ assert {"1", "2", "6"} in cliques
195
+ assert {"2", "3", "4", "5"} in cliques
196
+ assert len(cliques) == 2
197
+
198
+
199
+ def merge_coresets(
200
+ elements: List[str],
201
+ coresets: List[Set[str]],
202
+ min_overlap_for_merging_coresets: int,
203
+ ) -> List[Set[str]]:
204
+ """
205
+ Given a list of known coresets, return the maximal coresets.
206
+
207
+ Maximal coresets are obtained by repeatedly merging any pair of cliques
208
+ that intersect in at least `min_overlap_for_merging_coresets` elements.
209
+ """
210
+ coresets_original = coresets
211
+ coresets = sorted([tuple(sorted(list(coreset))) for coreset in coresets])
212
+ # print(f"Computing elements ...") # Too slow, will just require it as an
213
+ # argument.
214
+ coresets_containing_elements = {element: set() for element in elements}
215
+ for coreset in coresets:
216
+ for element in coreset:
217
+ coresets_containing_elements[element].add(coreset)
218
+
219
+ # print("Computing coreset_overlap_graph__edge_set ...")
220
+ coreset_overlap_graph__edge_set = set()
221
+ # We iterate over elements to find coresets with high intersection to merge
222
+ for element, coresets_for_element in coresets_containing_elements.items():
223
+ coresets_for_element_list = list(coresets_for_element)
224
+ for i, c1 in enumerate(coresets_for_element_list):
225
+ for j in range(i + 1, len(coresets_for_element_list)):
226
+ c2 = coresets_for_element_list[j]
227
+ if (
228
+ len(set(c1).intersection(set(c2)))
229
+ >= min_overlap_for_merging_coresets
230
+ ):
231
+ edge = (min(c1, c2), max(c1, c2))
232
+ coreset_overlap_graph__edge_set.add(edge)
233
+
234
+ # print("Creating coreset_merge_graph ...")
235
+ # print(f"coreset_merge_graph will have
236
+ # {len(coreset_overlap_graph__edge_set)} edges")
237
+ coreset_merge_graph = {coreset: [] for coreset in coresets}
238
+ for c1, c2 in coreset_overlap_graph__edge_set:
239
+ coreset_merge_graph[c1].append(c2)
240
+ coreset_merge_graph[c2].append(c1)
241
+
242
+ # print("Merging cliques ...")
243
+ visited = {coreset: False for coreset in coresets}
244
+
245
+ coresets_to_merge = []
246
+
247
+ def dfs(curr_coreset):
248
+ visited[curr_coreset] = True
249
+ coresets_to_merge.append(curr_coreset)
250
+ for neighboring_coreset in coreset_merge_graph[curr_coreset]:
251
+ if not visited[neighboring_coreset]:
252
+ dfs(neighboring_coreset)
253
+
254
+ final_coresets = []
255
+
256
+ for coreset in coresets:
257
+ if not visited[coreset]:
258
+ coresets_to_merge = []
259
+ dfs(coreset)
260
+ final_coresets.append(tuple(sorted(list(set(sum(coresets_to_merge, ()))))))
261
+ final_coresets = sorted(final_coresets)
262
+ final_coresets = [set(coreset) for coreset in final_coresets]
263
+
264
+ # Check if there is more reducing to do (if so, we do so recursively)
265
+ if coresets_original != final_coresets:
266
+ # print("Need to recurse!")
267
+ return merge_coresets(
268
+ elements=elements,
269
+ coresets=final_coresets,
270
+ min_overlap_for_merging_coresets=min_overlap_for_merging_coresets,
271
+ )
272
+ else:
273
+ return final_coresets
274
+
275
+
276
+ def test_merge_coresets():
277
+ coresets_test = [
278
+ {"a", "b", "c"},
279
+ {"b", "c", "d"},
280
+ {"a", "d", "e"},
281
+ {
282
+ "a",
283
+ },
284
+ ]
285
+ res = merge_coresets(
286
+ elements=["a", "b", "c", "d", "e"],
287
+ coresets=coresets_test,
288
+ min_overlap_for_merging_coresets=2,
289
+ )
290
+ assert res == [{"a"}, {"a", "b", "c", "d", "e"}]
291
+ coresets_test = [
292
+ {"a", "b", "c"},
293
+ {"b", "c", "d"},
294
+ {"d", "e", "f"},
295
+ ]
296
+ res = merge_coresets(
297
+ elements=["a", "b", "c", "d", "e", "f"],
298
+ coresets=coresets_test,
299
+ min_overlap_for_merging_coresets=3,
300
+ )
301
+ assert res == coresets_test
302
+ res = merge_coresets(
303
+ elements=["a", "b", "c", "d", "e", "f"],
304
+ coresets=coresets_test,
305
+ min_overlap_for_merging_coresets=2,
306
+ )
307
+ assert res == [{"a", "b", "c", "d"}, {"d", "e", "f"}]
308
+ res = merge_coresets(
309
+ elements=["a", "b", "c", "d", "e", "f"],
310
+ coresets=coresets_test,
311
+ min_overlap_for_merging_coresets=1,
312
+ )
313
+ assert res == [{"a", "b", "c", "d", "e", "f"}]
314
+
315
+
316
+ def duplicate_single_intbcs(maximal_coresets: List[set]) -> List[Multiset]:
317
+ """
318
+ Elevates single-intBC coresets into multisets which repeat the intBC twice,
319
+ thereby giving it higher statistical support in all subsequent steps of the algorithm.
320
+ """
321
+ res = []
322
+ for coreset in maximal_coresets:
323
+ if len(coreset) == 1:
324
+ res.append(Multiset(coreset) + Multiset(coreset))
325
+ else:
326
+ res.append(coreset)
327
+ return res
328
+
329
+
330
+ def test_duplicate_single_intbcs():
331
+ res = duplicate_single_intbcs([set(["a"]), set(["b", "c"])])
332
+ assert(
333
+ res == [Multiset(["a", "a"]), {"b", "c"}]
334
+ )
335
+
336
+
337
+ def find_maximal_coresets(
338
+ graph: GraphType,
339
+ min_coreset_size: int,
340
+ min_overlap_for_merging_coresets: int,
341
+ recover_single_intbc_clones: bool = False,
342
+ ) -> FindMaximalCoresetsReturnType:
343
+ """
344
+ Find maximal coresets.
345
+
346
+ Coresets are defined as follows:
347
+ - A k-clique for any k >= `min_coreset_size` is a coreset.
348
+ - If two coresets intersect in at least `min_overlap_for_merging_coresets`
349
+ nodes, then their union is a coreset.
350
+
351
+ A maximal coreset is a coreset that is not strictly contained in any larger
352
+ coreset, using the above closure rules.
353
+
354
+ If `recover_single_intbc_clones`, then maximal coresets with just 1 intBC are elevated to
355
+ a multiset with 2 intBCs (the same intBC twice).
356
+ """
357
+ if recover_single_intbc_clones and min_coreset_size != 1:
358
+ raise ValueError(
359
+ f"You are trying to recover_single_intbc_clones; this requires `min_coreset_size=1`."
360
+ )
361
+ st = time.time()
362
+ logger = logging.getLogger(__name__)
363
+ logger.info("Going to find_maximal_coresets ...")
364
+
365
+ coresets = find_maximum_cliques_over_all_nodes(graph)
366
+ # Throw away singletons, since we need a k-clique with
367
+ # k >= min_coreset_size for a coreset.
368
+ coresets = [coreset for coreset in coresets if len(coreset) >= min_coreset_size]
369
+
370
+ maximal_coresets = merge_coresets(
371
+ elements=graph[0].keys(),
372
+ coresets=coresets,
373
+ min_overlap_for_merging_coresets=min_overlap_for_merging_coresets,
374
+ )
375
+ if recover_single_intbc_clones:
376
+ maximal_coresets = duplicate_single_intbcs(maximal_coresets)
377
+
378
+ total_time = time.time() - st
379
+
380
+ res = FindMaximalCoresetsReturnType(
381
+ maximal_coresets=maximal_coresets,
382
+ total_time=total_time,
383
+ )
384
+ logger.info(
385
+ f"Found {len(maximal_coresets)} coresets, with average size {np.mean([len(x) for x in maximal_coresets]):.2f}"
386
+ )
387
+ logger.info(f"Done with find_maximal_coresets! Total time: {total_time}")
388
+
389
+ return res
390
+
391
+
392
+ def test_find_maximal_coresets():
393
+ graph = (
394
+ {
395
+ "1": {"2": 10, "6": 10},
396
+ "2": {"1": 10, "3": 10, "4": 10, "5": 10, "6": 10},
397
+ "3": {"2": 10, "4": 10, "5": 10},
398
+ "4": {"2": 10, "3": 10, "5": 10},
399
+ "5": {"2": 10, "3": 10, "4": 10},
400
+ "6": {"1": 10, "2": 10},
401
+ },
402
+ {
403
+ "1": 100,
404
+ "2": 100,
405
+ "3": 100,
406
+ "4": 100,
407
+ "5": 100,
408
+ "6": 100,
409
+ },
410
+ )
411
+ coresets = find_maximal_coresets(
412
+ graph=graph,
413
+ min_coreset_size=3,
414
+ min_overlap_for_merging_coresets=2,
415
+ ).maximal_coresets
416
+ assert coresets == [{"1", "2", "6"}, {"2", "3", "4", "5"}]
417
+ coresets = find_maximal_coresets(
418
+ graph=graph,
419
+ min_coreset_size=4,
420
+ min_overlap_for_merging_coresets=2,
421
+ ).maximal_coresets
422
+ assert coresets == [{"2", "3", "4", "5"}]
423
+ coresets = find_maximal_coresets(
424
+ graph=graph,
425
+ min_coreset_size=3,
426
+ min_overlap_for_merging_coresets=1,
427
+ ).maximal_coresets
428
+ assert coresets == [{"1", "2", "3", "4", "5", "6"}]
@@ -0,0 +1,202 @@
1
+ """
2
+ This module implements filtering of edges in the intBC graph which are
3
+ unreliable, i.e. likely due to noise rather than true co-occurrence
4
+ in some clone.
5
+
6
+ The main function exposed by this module is `keep_reliable_edges`.
7
+ """
8
+
9
+ import logging
10
+ import sys
11
+ import time
12
+
13
+ from ._types import *
14
+
15
+
16
+ def _init_logger():
17
+ logger = logging.getLogger(__name__)
18
+ logger.setLevel(logging.INFO)
19
+ fmt_str = "[%(asctime)s] - %(name)s - %(levelname)s - %(message)s"
20
+ formatter = logging.Formatter(fmt_str)
21
+
22
+ consoleHandler = logging.StreamHandler(sys.stdout)
23
+ consoleHandler.setFormatter(formatter)
24
+ logger.addHandler(consoleHandler)
25
+
26
+
27
+ _init_logger()
28
+
29
+
30
+ class KeepReliableEdgesReturnType(NamedTuple):
31
+ """
32
+ Object returned by the keep_reliable_edges function.
33
+
34
+ Attributes:
35
+ intbc_graph: the new graph after pruning away the unreliable edges.
36
+ total_time: Total time taken.
37
+ """
38
+
39
+ intbc_graph: GraphType
40
+ total_time: float
41
+
42
+
43
+ def keep_reliable_edges(
44
+ intbc_graph: GraphType,
45
+ ratio_cutoff: float,
46
+ ) -> KeepReliableEdgesReturnType:
47
+ """
48
+ Keeps only edges in the intBC graph which are reliable.
49
+
50
+ We say that an edge in the intBC graph is "reliable" if we are very certain
51
+ that there exists one and only one clone in which its two intBCs are
52
+ integrations. Provided that the graph has been pruned from sequencing
53
+ errors, reliable pairs will have a large ratio. Here the ratio is defined
54
+ as:
55
+ ratio(x, y) = c(x, y) / min(c_x, c_y) = max { p(x|y), p(y|x) }
56
+
57
+ Args:
58
+ ratio_cutoff: Only edges with ratio greater than or equal to this will
59
+ be kept.
60
+ """
61
+ st = time.time()
62
+ logger = logging.getLogger(__name__)
63
+ logger.info("Going to keep_reliable_edges ...")
64
+ edge_counts, intbc_counts = intbc_graph
65
+ pruned_edge_counts = {intbc: defaultdict(int) for intbc in intbc_counts}
66
+ total_edges = 0
67
+ number_of_pruned_edges = 0
68
+ for intbc, counts in intbc_counts.items():
69
+ for neighbor, co_counts in edge_counts[intbc].items():
70
+ total_edges += 1
71
+ ratio = co_counts / min(counts, intbc_counts[neighbor])
72
+ if ratio >= ratio_cutoff:
73
+ pruned_edge_counts[intbc][neighbor] = co_counts
74
+ else:
75
+ number_of_pruned_edges += 1
76
+
77
+ pruned_edge_counts = {
78
+ x: {y: z for (y, z) in d.items()} for (x, d) in pruned_edge_counts.items()
79
+ }
80
+ pruned_intbc_counts = {x: y for (x, y) in intbc_counts.items()} # Same as original.
81
+ pruned_graph = (pruned_edge_counts, pruned_intbc_counts)
82
+ total_time = time.time() - st
83
+ res = KeepReliableEdgesReturnType(
84
+ intbc_graph=pruned_graph,
85
+ total_time=total_time,
86
+ )
87
+ edge_counts, node_counts = pruned_graph
88
+ num_nodes = len(node_counts)
89
+ num_edges = sum([len(x) for x in edge_counts.values()]) // 2
90
+ logger.info(
91
+ f"Pruned {(100.0 * number_of_pruned_edges / (total_edges + 1e-9)):.2f}% edges from the intBC graph. Pruned IntBC graph has {num_nodes} nodes and {num_edges} edges. Density level: {100.0 * num_edges / (num_nodes * (num_nodes - 1.0) / 2.0 + 1e-9):.2f}%"
92
+ )
93
+ logger.info(f"Done with keep_reliable_edges! Total time: {total_time}")
94
+ return res
95
+
96
+
97
+ def test_keep_reliable_edges():
98
+ graph = (
99
+ {
100
+ "1": {"2": 500, "3": 100, "4": 100},
101
+ "2": {"1": 500, "3": 100, "4": 100},
102
+ "3": {"1": 100, "2": 100, "4": 600},
103
+ "4": {"1": 100, "2": 100, "3": 600},
104
+ },
105
+ {
106
+ "1": 1000,
107
+ "2": 1000,
108
+ "3": 1000,
109
+ "4": 1000,
110
+ },
111
+ )
112
+ pruned_graph = keep_reliable_edges(
113
+ intbc_graph=graph,
114
+ ratio_cutoff=0.0,
115
+ ).intbc_graph
116
+ assert graph == pruned_graph
117
+ pruned_graph = keep_reliable_edges(
118
+ intbc_graph=graph,
119
+ ratio_cutoff=0.2,
120
+ ).intbc_graph
121
+ assert pruned_graph == (
122
+ {
123
+ "1": {"2": 500},
124
+ "2": {"1": 500},
125
+ "3": {"4": 600},
126
+ "4": {"3": 600},
127
+ },
128
+ {
129
+ "1": 1000,
130
+ "2": 1000,
131
+ "3": 1000,
132
+ "4": 1000,
133
+ },
134
+ )
135
+ pruned_graph = keep_reliable_edges(
136
+ intbc_graph=graph,
137
+ ratio_cutoff=0.55,
138
+ ).intbc_graph
139
+ assert pruned_graph == (
140
+ {
141
+ "1": {},
142
+ "2": {},
143
+ "3": {"4": 600},
144
+ "4": {"3": 600},
145
+ },
146
+ {
147
+ "1": 1000,
148
+ "2": 1000,
149
+ "3": 1000,
150
+ "4": 1000,
151
+ },
152
+ )
153
+ pruned_graph = keep_reliable_edges(
154
+ intbc_graph=graph,
155
+ ratio_cutoff=0.65,
156
+ ).intbc_graph
157
+ assert pruned_graph == (
158
+ {
159
+ "1": {},
160
+ "2": {},
161
+ "3": {},
162
+ "4": {},
163
+ },
164
+ {
165
+ "1": 1000,
166
+ "2": 1000,
167
+ "3": 1000,
168
+ "4": 1000,
169
+ },
170
+ )
171
+ graph = (
172
+ {
173
+ "1": {"2": 500, "3": 100, "4": 100},
174
+ "2": {"1": 500, "3": 100, "4": 100},
175
+ "3": {"1": 100, "2": 100, "4": 600},
176
+ "4": {"1": 100, "2": 100, "3": 600},
177
+ },
178
+ {
179
+ "1": 1000,
180
+ "2": 1000,
181
+ "3": 1000,
182
+ "4": 800,
183
+ },
184
+ )
185
+ pruned_graph = keep_reliable_edges(
186
+ intbc_graph=graph,
187
+ ratio_cutoff=0.65,
188
+ ).intbc_graph
189
+ assert pruned_graph == (
190
+ {
191
+ "1": {},
192
+ "2": {},
193
+ "3": {"4": 600},
194
+ "4": {"3": 600},
195
+ },
196
+ {
197
+ "1": 1000,
198
+ "2": 1000,
199
+ "3": 1000,
200
+ "4": 800,
201
+ },
202
+ )