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.
- nova_clone/__init__.py +1 -0
- nova_clone/algorithm/__init__.py +2 -0
- nova_clone/algorithm/_build_intbc_graph.py +135 -0
- nova_clone/algorithm/_call_clones_from_coresets.py +235 -0
- nova_clone/algorithm/_clone_call_ops.py +258 -0
- nova_clone/algorithm/_clone_calling_algorithm.py +1571 -0
- nova_clone/algorithm/_compute_cellbc_vs_coreset_overlap_matrix.py +143 -0
- nova_clone/algorithm/_compute_coreset_co_assignment_matrix.py +170 -0
- nova_clone/algorithm/_find_maximal_coresets.py +428 -0
- nova_clone/algorithm/_keep_reliable_edges.py +202 -0
- nova_clone/algorithm/_make_coresets_disjoint.py +114 -0
- nova_clone/algorithm/_prune_coresets.py +164 -0
- nova_clone/algorithm/_prune_custom_intbcs.py +150 -0
- nova_clone/algorithm/_prune_likely_homoplastic_intbcs.py +162 -0
- nova_clone/algorithm/_prune_likely_sequencing_errors.py +201 -0
- nova_clone/algorithm/_prune_low_cellbc_count_intbcs.py +154 -0
- nova_clone/algorithm/_prune_low_umi_count_intbcs.py +152 -0
- nova_clone/algorithm/_types.py +150 -0
- nova_clone/algorithm/_utils.py +53 -0
- nova_clone/algorithm/cassiopeia/__init__.py +1 -0
- nova_clone/algorithm/cassiopeia/_call_lineage_groups.py +270 -0
- nova_clone/caching/__init__.py +24 -0
- nova_clone/caching/_cached.py +175 -0
- nova_clone/caching/_cached_computation.py +369 -0
- nova_clone/caching/_cached_parallel_computation.py +428 -0
- nova_clone/caching/_common.py +200 -0
- nova_clone/data/__init__.py +1 -0
- nova_clone/qc/__init__.py +179 -0
- nova_clone/qc/_clone_intbc_sets.py +234 -0
- nova_clone/qc/_compare_clone_calls.py +218 -0
- nova_clone/qc/_data_retention.py +175 -0
- nova_clone/qc/_homoplasy.py +81 -0
- nova_clone/qc/_inter_clonal_kinship.py +60 -0
- nova_clone/qc/_intra_clonal_kinship.py +172 -0
- nova_clone/qc/_multiplet_rate.py +158 -0
- nova_clone/qc/_number_of_clones.py +153 -0
- nova_clone/qc/_pre_qc.py +485 -0
- nova_clone/simulation/HDBSCAN_method_adjusted.py +68 -0
- nova_clone/simulation/__init__.py +0 -0
- nova_clone/simulation/_caching.py +40 -0
- nova_clone/simulation/benchmarking.py +1221 -0
- nova_clone/simulation/clone_to_sequences.py +408 -0
- nova_clone/simulation/creating_tree.py +416 -0
- nova_clone/simulation/evaluation.py +344 -0
- nova_clone/simulation/jaccard_hierarchical_by_intBC.py +78 -0
- nova_clone/simulation/plot_benchmark_lines.py +226 -0
- nova_clone/simulation/slice_benchmark.py +640 -0
- nova_clone/simulation/slice_benchmark_clone_calling.py +68 -0
- nova_clone-0.1.0.dist-info/METADATA +298 -0
- nova_clone-0.1.0.dist-info/RECORD +53 -0
- nova_clone-0.1.0.dist-info/WHEEL +5 -0
- nova_clone-0.1.0.dist-info/licenses/LICENSE +28 -0
- 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
|
+
)
|