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
nova_clone/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .algorithm import clone_calling_algorithm
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module for building an IntBC graph from a list of intBC tables.
|
|
3
|
+
|
|
4
|
+
The main function exposed by this module is `build_intbc_graph`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
from collections import defaultdict
|
|
11
|
+
from typing import Set
|
|
12
|
+
|
|
13
|
+
from ._types import *
|
|
14
|
+
from ._utils import _cell_intbc_sets_from_intbc_tables
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _init_logger():
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
logger.setLevel(logging.INFO)
|
|
20
|
+
fmt_str = "[%(asctime)s] - %(name)s - %(levelname)s - %(message)s"
|
|
21
|
+
formatter = logging.Formatter(fmt_str)
|
|
22
|
+
|
|
23
|
+
consoleHandler = logging.StreamHandler(sys.stdout)
|
|
24
|
+
consoleHandler.setFormatter(formatter)
|
|
25
|
+
logger.addHandler(consoleHandler)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
_init_logger()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BuildIntBCGraphReturnType(NamedTuple):
|
|
32
|
+
"""
|
|
33
|
+
Object returned by the build_intbc_graph function.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
intbc_graph: The intBC graph, of type GraphType.
|
|
37
|
+
total_time: Total time taken.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
intbc_graph: GraphType
|
|
41
|
+
total_time: float
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def build_intbc_graph(
|
|
45
|
+
intbc_table_per_sample: Dict[SampleNameType, IntBCTableType],
|
|
46
|
+
) -> BuildIntBCGraphReturnType:
|
|
47
|
+
"""
|
|
48
|
+
Build the intBC graph from a list of intBC tables.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
intbc_table_per_sample: intBC table per sample.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
intbc_graph: The intBC graph, of type GraphType.
|
|
55
|
+
"""
|
|
56
|
+
st = time.time()
|
|
57
|
+
logger = logging.getLogger(__name__)
|
|
58
|
+
logger.info("Going to build_intbc_graph ...")
|
|
59
|
+
edge_counts = defaultdict(lambda: defaultdict(int))
|
|
60
|
+
node_counts = defaultdict(int)
|
|
61
|
+
cell_intbc_sets = _cell_intbc_sets_from_intbc_tables(
|
|
62
|
+
intbc_table_per_sample=intbc_table_per_sample,
|
|
63
|
+
)
|
|
64
|
+
for cell_intbc_set in cell_intbc_sets.values():
|
|
65
|
+
for intbc_1 in cell_intbc_set:
|
|
66
|
+
node_counts[intbc_1] += 1
|
|
67
|
+
for intbc_2 in cell_intbc_set:
|
|
68
|
+
if intbc_1 == intbc_2:
|
|
69
|
+
continue
|
|
70
|
+
edge_counts[intbc_1][intbc_2] += 1
|
|
71
|
+
# Make sure that intBCs with no neighbors are present in the edge dict!
|
|
72
|
+
for intbc in node_counts:
|
|
73
|
+
edge_counts[intbc]
|
|
74
|
+
# Convert to a normal Dict
|
|
75
|
+
edge_counts = {x: {y: z for (y, z) in d.items()} for (x, d) in edge_counts.items()}
|
|
76
|
+
node_counts = {x: y for (x, y) in node_counts.items()}
|
|
77
|
+
intbc_graph = (edge_counts, node_counts)
|
|
78
|
+
total_time = time.time() - st
|
|
79
|
+
res = BuildIntBCGraphReturnType(
|
|
80
|
+
intbc_graph=intbc_graph,
|
|
81
|
+
total_time=total_time,
|
|
82
|
+
)
|
|
83
|
+
num_nodes = len(node_counts)
|
|
84
|
+
num_edges = sum([len(x) for x in edge_counts.values()]) // 2
|
|
85
|
+
logger.info(
|
|
86
|
+
f"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}%"
|
|
87
|
+
)
|
|
88
|
+
logger.info(f"Done with build_intbc_graph! Total time: {total_time}")
|
|
89
|
+
return res
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_build_intbc_graph():
|
|
93
|
+
"""
|
|
94
|
+
Basic test.
|
|
95
|
+
"""
|
|
96
|
+
intbc_table_1 = pd.DataFrame(
|
|
97
|
+
data=[
|
|
98
|
+
["cellBC_1", "AAA", 40],
|
|
99
|
+
["cellBC_1", "BBB", 40],
|
|
100
|
+
["cellBC_2", "AAA", 40],
|
|
101
|
+
["cellBC_2", "BBB", 40],
|
|
102
|
+
["cellBC_2", "CCC", 40],
|
|
103
|
+
["cellBC_3", "DDD", 40],
|
|
104
|
+
["cellBC_3", "EEE", 40],
|
|
105
|
+
["cellBC_3", "FFF", 40],
|
|
106
|
+
],
|
|
107
|
+
columns=["cellBC", "intBC", "UMI_count"],
|
|
108
|
+
)
|
|
109
|
+
intbc_table_2 = pd.DataFrame(
|
|
110
|
+
data=[
|
|
111
|
+
["cellBC_1", "EEE", 10],
|
|
112
|
+
["cellBC_1", "FFF", 40],
|
|
113
|
+
["cellBC_2", "GGG", 40], # Just observed once
|
|
114
|
+
],
|
|
115
|
+
columns=["cellBC", "intBC", "UMI_count"],
|
|
116
|
+
)
|
|
117
|
+
graph = build_intbc_graph(
|
|
118
|
+
intbc_table_per_sample={
|
|
119
|
+
"sample1": intbc_table_1,
|
|
120
|
+
"sample2": intbc_table_2,
|
|
121
|
+
},
|
|
122
|
+
).intbc_graph
|
|
123
|
+
expected_graph = (
|
|
124
|
+
{
|
|
125
|
+
"AAA": {"BBB": 2, "CCC": 1},
|
|
126
|
+
"BBB": {"AAA": 2, "CCC": 1},
|
|
127
|
+
"CCC": {"AAA": 1, "BBB": 1},
|
|
128
|
+
"DDD": {"EEE": 1, "FFF": 1},
|
|
129
|
+
"EEE": {"DDD": 1, "FFF": 2},
|
|
130
|
+
"FFF": {"DDD": 1, "EEE": 2},
|
|
131
|
+
"GGG": {},
|
|
132
|
+
},
|
|
133
|
+
{"AAA": 2, "BBB": 2, "CCC": 1, "DDD": 1, "EEE": 2, "FFF": 2, "GGG": 1},
|
|
134
|
+
)
|
|
135
|
+
assert graph == expected_graph
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module for calling clones once coresets have been identified.
|
|
3
|
+
|
|
4
|
+
The main function exposed by this module is `call_clones_from_coresets`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
import editdistance
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pandas as pd
|
|
12
|
+
import tqdm
|
|
13
|
+
|
|
14
|
+
from ._compute_cellbc_vs_coreset_overlap_matrix import (
|
|
15
|
+
compute_cellbc_vs_coreset_overlap_matrix,
|
|
16
|
+
)
|
|
17
|
+
from ._prune_low_umi_count_intbcs import prune_low_umi_count_intbcs
|
|
18
|
+
from ._types import *
|
|
19
|
+
from ._utils import _cell_intbc_sets_from_intbc_tables
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import sys
|
|
23
|
+
|
|
24
|
+
from ._types import *
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _init_logger():
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
logger.setLevel(logging.INFO)
|
|
30
|
+
fmt_str = "[%(asctime)s] - %(name)s - %(levelname)s - %(message)s"
|
|
31
|
+
formatter = logging.Formatter(fmt_str)
|
|
32
|
+
|
|
33
|
+
consoleHandler = logging.StreamHandler(sys.stdout)
|
|
34
|
+
consoleHandler.setFormatter(formatter)
|
|
35
|
+
logger.addHandler(consoleHandler)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
_init_logger()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class CallClonesFromCoresetsReturnType(NamedTuple):
|
|
42
|
+
"""
|
|
43
|
+
Object returned by the `call_clones_from_coresets` function.
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
clone_call: The clone call.
|
|
47
|
+
total_time: Total time taken.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
clone_call: CloneCallType
|
|
51
|
+
total_time: float
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def call_clones_from_coresets(
|
|
55
|
+
intbc_table_per_sample: Dict[SampleNameType, IntBCTableType],
|
|
56
|
+
min_intbc_umi_count: int,
|
|
57
|
+
coresets: List[Coreset[str]],
|
|
58
|
+
min_overlap_for_assigning_cellbc: int,
|
|
59
|
+
min_intbc_counts_for_adding_intbc_to_clone_intbc_set: int,
|
|
60
|
+
min_intbc_fraction_for_adding_to_clone_intbc_set: float,
|
|
61
|
+
edit_distance_cutoff: int,
|
|
62
|
+
custom_intbc_filtering_function: str,
|
|
63
|
+
) -> CallClonesFromCoresetsReturnType:
|
|
64
|
+
"""
|
|
65
|
+
Call clones given the coresets characterizing each clone.
|
|
66
|
+
"""
|
|
67
|
+
st = time.time()
|
|
68
|
+
|
|
69
|
+
# Validate arguments
|
|
70
|
+
validate_intbc_table_per_sample(intbc_table_per_sample=intbc_table_per_sample)
|
|
71
|
+
|
|
72
|
+
# Filter intBCs based on min_intbc_umi_count
|
|
73
|
+
all_cellbc = set(
|
|
74
|
+
list(
|
|
75
|
+
_cell_intbc_sets_from_intbc_tables(
|
|
76
|
+
intbc_table_per_sample=intbc_table_per_sample
|
|
77
|
+
).keys()
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
intbc_table_per_sample = prune_low_umi_count_intbcs(
|
|
81
|
+
intbc_table_per_sample=intbc_table_per_sample,
|
|
82
|
+
min_intbc_umi_count=min_intbc_umi_count
|
|
83
|
+
).intbc_table_per_sample
|
|
84
|
+
cell_intbc_sets = _cell_intbc_sets_from_intbc_tables(
|
|
85
|
+
intbc_table_per_sample=intbc_table_per_sample,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
cellbc_vs_coreset_overlap_matrix = compute_cellbc_vs_coreset_overlap_matrix( # We MUST prune low-count UMIs before doing this! Else the background RNA will mess it up. In constrast, it's OK to not have discarded sequencing errors (bc they are not in the coresets anyway) nor homoplastic intBCs (because they are also not in the coresets anyway). Sequencing errors and homoplastic intBCs will be recovered de-novo.
|
|
89
|
+
cellbc_intbc_sets=cell_intbc_sets,
|
|
90
|
+
coresets=coresets,
|
|
91
|
+
).cellbc_vs_coreset_overlap_matrix
|
|
92
|
+
|
|
93
|
+
cells = sorted(list(cell_intbc_sets.keys()))
|
|
94
|
+
num_clones = cellbc_vs_coreset_overlap_matrix.shape[1]
|
|
95
|
+
num_cells = cellbc_vs_coreset_overlap_matrix.shape[0]
|
|
96
|
+
assert len(cells) == num_cells
|
|
97
|
+
|
|
98
|
+
# First compute clone_intbc_counts by using the confidently mapped cells.
|
|
99
|
+
clone_intbc_counts = {
|
|
100
|
+
clone_idx: defaultdict(int) for clone_idx in range(num_clones)
|
|
101
|
+
}
|
|
102
|
+
clone_mapping = {cell: {} for cell in cell_intbc_sets}
|
|
103
|
+
clone_to_cells_mapping = {clone_idx: set() for clone_idx in range(num_clones)}
|
|
104
|
+
clone_sizes = defaultdict(int)
|
|
105
|
+
for cell in tqdm.tqdm(cells):
|
|
106
|
+
clones_assigned = list(
|
|
107
|
+
np.where(
|
|
108
|
+
cellbc_vs_coreset_overlap_matrix.loc[cell, :]
|
|
109
|
+
>= min_overlap_for_assigning_cellbc
|
|
110
|
+
)[0]
|
|
111
|
+
)
|
|
112
|
+
clone_mapping[cell] = set(clones_assigned)
|
|
113
|
+
if len(clones_assigned) == 1:
|
|
114
|
+
# Is confidently assigned, so use to compute clone intBC sets
|
|
115
|
+
clone_idx = clones_assigned[0]
|
|
116
|
+
clone_to_cells_mapping[clone_idx].add(cell)
|
|
117
|
+
clone_sizes[clone_idx] += 1
|
|
118
|
+
for intbc in cell_intbc_sets[cell]:
|
|
119
|
+
clone_intbc_counts[clone_idx][intbc] += 1
|
|
120
|
+
|
|
121
|
+
# Now it's time to figure out the intBC sets of each clone from their intBC
|
|
122
|
+
# counts
|
|
123
|
+
clone_intbc_sets = {}
|
|
124
|
+
for clone_idx in tqdm.tqdm(range(num_clones)):
|
|
125
|
+
clone_size = clone_sizes[clone_idx]
|
|
126
|
+
kept_intbcs = []
|
|
127
|
+
intbc_counts = sorted(
|
|
128
|
+
clone_intbc_counts[clone_idx].items(), key=lambda x: -x[1]
|
|
129
|
+
)
|
|
130
|
+
for intbc, counts in intbc_counts:
|
|
131
|
+
if custom_intbc_filtering_function(intbc):
|
|
132
|
+
# Is weird, so we discard it immediately
|
|
133
|
+
continue
|
|
134
|
+
else:
|
|
135
|
+
# First see if it looks anything like another *kept* intBC
|
|
136
|
+
ds = {
|
|
137
|
+
kept_intbc: editdistance.eval(intbc, kept_intbc)
|
|
138
|
+
for kept_intbc in kept_intbcs
|
|
139
|
+
}
|
|
140
|
+
close_kept_intbcs = [
|
|
141
|
+
kept_intbc
|
|
142
|
+
for (kept_intbc, d) in ds.items()
|
|
143
|
+
if d <= edit_distance_cutoff
|
|
144
|
+
]
|
|
145
|
+
if len(close_kept_intbcs) == 0:
|
|
146
|
+
# If it meets the fraction & count cutoff, we keep it
|
|
147
|
+
intbc_fraction = counts / clone_size
|
|
148
|
+
if (
|
|
149
|
+
(
|
|
150
|
+
(
|
|
151
|
+
intbc_fraction
|
|
152
|
+
>= min_intbc_fraction_for_adding_to_clone_intbc_set
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
and counts
|
|
156
|
+
>= min_intbc_counts_for_adding_intbc_to_clone_intbc_set
|
|
157
|
+
):
|
|
158
|
+
kept_intbcs.append(intbc)
|
|
159
|
+
clone_intbc_sets[clone_idx] = set(kept_intbcs)
|
|
160
|
+
|
|
161
|
+
# At this point, we have our clone intBC sets! It's time to compute the
|
|
162
|
+
# cell intBC sets (simply by intersection)
|
|
163
|
+
observed_cell_intbc_sets = cell_intbc_sets
|
|
164
|
+
cell_intbc_sets = {}
|
|
165
|
+
for cell, clones in tqdm.tqdm(clone_mapping.items()):
|
|
166
|
+
observed = observed_cell_intbc_sets[cell]
|
|
167
|
+
if len(clones) != 1:
|
|
168
|
+
# We did not confidently map the cell, so we just drop all intBCs
|
|
169
|
+
cell_intbc_set = set()
|
|
170
|
+
else:
|
|
171
|
+
clone_idx = list(clones)[0]
|
|
172
|
+
clone_intbc_set = clone_intbc_sets[clone_idx]
|
|
173
|
+
cell_intbc_set = observed.intersection(clone_intbc_set)
|
|
174
|
+
cell_intbc_sets[cell] = cell_intbc_set
|
|
175
|
+
|
|
176
|
+
cell_to_clones_mapping = {
|
|
177
|
+
x: set(["clone_" + str(y) for y in ys]) for (x, ys) in clone_mapping.items()
|
|
178
|
+
}
|
|
179
|
+
for cellbc in all_cellbc:
|
|
180
|
+
if cellbc not in cell_to_clones_mapping:
|
|
181
|
+
cell_to_clones_mapping[cellbc] = (
|
|
182
|
+
set()
|
|
183
|
+
) # These lost all their intBCs due to low UMI counts
|
|
184
|
+
if cellbc not in cell_intbc_sets:
|
|
185
|
+
cell_intbc_sets[cellbc] = (
|
|
186
|
+
set()
|
|
187
|
+
) # These lost all their intBCs due to low UMI counts
|
|
188
|
+
clone_intbc_sets = {("clone_" + str(x)): y for (x, y) in clone_intbc_sets.items()}
|
|
189
|
+
clone_to_cells_mapping = {
|
|
190
|
+
("clone_" + str(x)): y for (x, y) in clone_to_cells_mapping.items()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
clone_call = CloneCallType(
|
|
194
|
+
clone_to_cells_mapping=clone_to_cells_mapping,
|
|
195
|
+
cell_to_clones_mapping=cell_to_clones_mapping,
|
|
196
|
+
clone_intbc_sets=clone_intbc_sets,
|
|
197
|
+
cell_intbc_sets=cell_intbc_sets,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
total_time = time.time() - st
|
|
201
|
+
res = CallClonesFromCoresetsReturnType(
|
|
202
|
+
clone_call=clone_call,
|
|
203
|
+
total_time=total_time,
|
|
204
|
+
)
|
|
205
|
+
return res
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def test_call_clones_from_coresets():
|
|
209
|
+
intbc_table = pd.DataFrame(
|
|
210
|
+
[
|
|
211
|
+
["c1", "A", 5],
|
|
212
|
+
["c1", "B", 5],
|
|
213
|
+
["c2", "C", 5],
|
|
214
|
+
],
|
|
215
|
+
columns=["cellBC", "intBC", "UMI_count"],
|
|
216
|
+
)
|
|
217
|
+
out = call_clones_from_coresets(
|
|
218
|
+
intbc_table_per_sample={"s0": intbc_table},
|
|
219
|
+
min_intbc_umi_count=1,
|
|
220
|
+
coresets=[{"A", "B"}, {"C"}],
|
|
221
|
+
min_overlap_for_assigning_cellbc=1,
|
|
222
|
+
min_intbc_counts_for_adding_intbc_to_clone_intbc_set=1,
|
|
223
|
+
min_intbc_fraction_for_adding_to_clone_intbc_set=0.0,
|
|
224
|
+
edit_distance_cutoff=0,
|
|
225
|
+
custom_intbc_filtering_function=lambda intbc: False,
|
|
226
|
+
)
|
|
227
|
+
assert out.total_time >= 0.0
|
|
228
|
+
cc = out.clone_call
|
|
229
|
+
c1, c2 = "s0_c1", "s0_c2"
|
|
230
|
+
assert cc.clone_to_cells_mapping["clone_0"] == {c1}
|
|
231
|
+
assert cc.clone_to_cells_mapping["clone_1"] == {c2}
|
|
232
|
+
assert cc.clone_intbc_sets["clone_0"] == {"A", "B"}
|
|
233
|
+
assert cc.clone_intbc_sets["clone_1"] == {"C"}
|
|
234
|
+
assert cc.cell_intbc_sets[c1] == {"A", "B"}
|
|
235
|
+
assert cc.cell_intbc_sets[c2] == {"C"}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Operations for e.g. merging clones, dropping clones, in a clone call.
|
|
3
|
+
"""
|
|
4
|
+
import typing as t
|
|
5
|
+
from ._types import CloneCallType
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def merge_clones(
|
|
11
|
+
clones: t.List[str],
|
|
12
|
+
clone_call: CloneCallType,
|
|
13
|
+
new_clone_name: str,
|
|
14
|
+
) -> CloneCallType:
|
|
15
|
+
"""
|
|
16
|
+
Merge the given clones into one.
|
|
17
|
+
|
|
18
|
+
clone_to_cells_mapping: Dict[CloneType, Set[CellBCType]]
|
|
19
|
+
=> we replace the clones by a new key with union of cells.
|
|
20
|
+
cell_to_clones_mapping: Dict[CellBCType, Set[CloneType]]
|
|
21
|
+
=> we collapse the merged clones into one. To be conservative, multiplets contained in exclusively the `clones` become unmapped cells.
|
|
22
|
+
clone_intbc_sets: Dict[CloneType, Set[IntBCType]]
|
|
23
|
+
=> we take the union.
|
|
24
|
+
cell_intbc_sets: Dict[CellBCType, Set[IntBCType]]
|
|
25
|
+
=> Multiplets contained in exclusively the `clones` have their intBC sets cleared.
|
|
26
|
+
"""
|
|
27
|
+
clone_to_cells_mapping = deepcopy(clone_call.clone_to_cells_mapping)
|
|
28
|
+
cell_to_clones_mapping = deepcopy(clone_call.cell_to_clones_mapping)
|
|
29
|
+
clone_intbc_sets = deepcopy(clone_call.clone_intbc_sets)
|
|
30
|
+
cell_intbc_sets = deepcopy(clone_call.cell_intbc_sets)
|
|
31
|
+
new_clone_to_cells_mapping = defaultdict(set)
|
|
32
|
+
new_cell_to_clones_mapping = {}
|
|
33
|
+
new_clone_intbc_sets = defaultdict(set)
|
|
34
|
+
new_cell_intbc_sets = {}
|
|
35
|
+
clones_set = set(clones)
|
|
36
|
+
for clone in clone_to_cells_mapping:
|
|
37
|
+
if clone not in clones:
|
|
38
|
+
new_clone_to_cells_mapping[clone] = clone_to_cells_mapping[clone]
|
|
39
|
+
else:
|
|
40
|
+
new_clone_to_cells_mapping[new_clone_name] = new_clone_to_cells_mapping[new_clone_name].union(
|
|
41
|
+
clone_to_cells_mapping[clone]
|
|
42
|
+
)
|
|
43
|
+
for cell in cell_to_clones_mapping:
|
|
44
|
+
cell_clones = cell_to_clones_mapping[cell]
|
|
45
|
+
new_cell_clones = set()
|
|
46
|
+
if len(cell_clones) >= 2 and all(clone in clones_set for clone in cell_clones):
|
|
47
|
+
# This is a multiplet that mapped only to the merged clones.
|
|
48
|
+
# We unmap it to be safe, i.e. we leave `new_cell_clones` empty.
|
|
49
|
+
pass
|
|
50
|
+
else:
|
|
51
|
+
for clone in cell_clones:
|
|
52
|
+
if clone in clones_set:
|
|
53
|
+
new_cell_clones.add(new_clone_name)
|
|
54
|
+
else:
|
|
55
|
+
new_cell_clones.add(clone)
|
|
56
|
+
new_cell_to_clones_mapping[cell] = new_cell_clones
|
|
57
|
+
for clone in clone_intbc_sets:
|
|
58
|
+
intbcs = clone_intbc_sets[clone]
|
|
59
|
+
if clone not in clones_set:
|
|
60
|
+
new_clone_intbc_sets[clone] = intbcs
|
|
61
|
+
else:
|
|
62
|
+
new_clone_intbc_sets[new_clone_name] = new_clone_intbc_sets[new_clone_name].union(intbcs)
|
|
63
|
+
new_cell_intbc_sets = cell_intbc_sets
|
|
64
|
+
for cell in new_cell_intbc_sets:
|
|
65
|
+
if len(new_cell_to_clones_mapping[cell]) == 0:
|
|
66
|
+
new_cell_intbc_sets[cell] = set()
|
|
67
|
+
return CloneCallType( # NOTE: We cast to dict to avoid returning defaultdict
|
|
68
|
+
clone_to_cells_mapping=dict(new_clone_to_cells_mapping),
|
|
69
|
+
cell_to_clones_mapping=dict(new_cell_to_clones_mapping),
|
|
70
|
+
clone_intbc_sets=dict(new_clone_intbc_sets),
|
|
71
|
+
cell_intbc_sets=dict(new_cell_intbc_sets),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _get_test_clone_call():
|
|
77
|
+
clone_call = CloneCallType(
|
|
78
|
+
clone_to_cells_mapping={
|
|
79
|
+
"clone_1": {"cell_1", "cell_2", "cell_3"},
|
|
80
|
+
"clone_2": {"cell_4", "cell_5", "cell_6"},
|
|
81
|
+
"clone_3": {"cell_7", "cell_8"},
|
|
82
|
+
"clone_4": {"cell_10"},
|
|
83
|
+
},
|
|
84
|
+
cell_to_clones_mapping={
|
|
85
|
+
"cell_1": {"clone_1"},
|
|
86
|
+
"cell_2": {"clone_1"},
|
|
87
|
+
"cell_3": {"clone_1"},
|
|
88
|
+
"cell_4": {"clone_2"},
|
|
89
|
+
"cell_5": {"clone_2"},
|
|
90
|
+
"cell_6": {"clone_2"},
|
|
91
|
+
"cell_7": {"clone_3"},
|
|
92
|
+
"cell_8": {"clone_3"},
|
|
93
|
+
"cell_9": {"clone_2", "clone_3"},
|
|
94
|
+
"cell_10": {"clone_4"},
|
|
95
|
+
"cell_11": set(),
|
|
96
|
+
"cell_12": {"clone_1", "clone_4"}
|
|
97
|
+
},
|
|
98
|
+
clone_intbc_sets={
|
|
99
|
+
"clone_1": {"A", "B"},
|
|
100
|
+
"clone_2": {"C", "D"},
|
|
101
|
+
"clone_3": {"E", "F"},
|
|
102
|
+
"clone_4": {"F", "G"},
|
|
103
|
+
},
|
|
104
|
+
cell_intbc_sets={
|
|
105
|
+
"cell_1": {"A", "B"},
|
|
106
|
+
"cell_2": {"A", "B"},
|
|
107
|
+
"cell_3": {"A", "B"},
|
|
108
|
+
"cell_4": {"C", "D"},
|
|
109
|
+
"cell_5": {"C", "D"},
|
|
110
|
+
"cell_6": {"C", "D"},
|
|
111
|
+
"cell_7": {"E", "F"},
|
|
112
|
+
"cell_8": {"E", "F"},
|
|
113
|
+
"cell_9": {},
|
|
114
|
+
"cell_10": {"F", "G"},
|
|
115
|
+
"cell_11": set(),
|
|
116
|
+
"cell_12": {"A", "B", "F", "G"},
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
return clone_call
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_merge_clones():
|
|
123
|
+
clone_call = _get_test_clone_call()
|
|
124
|
+
clone_call = merge_clones(
|
|
125
|
+
clones=["clone_2", "clone_3", "clone_4"],
|
|
126
|
+
clone_call=clone_call,
|
|
127
|
+
new_clone_name="clone_2_3_4"
|
|
128
|
+
)
|
|
129
|
+
assert clone_call.clone_to_cells_mapping == {
|
|
130
|
+
"clone_1": {"cell_1", "cell_2", "cell_3"},
|
|
131
|
+
"clone_2_3_4": {"cell_4", "cell_5", "cell_6", "cell_7", "cell_8", "cell_10"},
|
|
132
|
+
}
|
|
133
|
+
assert clone_call.cell_to_clones_mapping == {
|
|
134
|
+
"cell_1": {"clone_1"},
|
|
135
|
+
"cell_2": {"clone_1"},
|
|
136
|
+
"cell_3": {"clone_1"},
|
|
137
|
+
"cell_4": {"clone_2_3_4"},
|
|
138
|
+
"cell_5": {"clone_2_3_4"},
|
|
139
|
+
"cell_6": {"clone_2_3_4"},
|
|
140
|
+
"cell_7": {"clone_2_3_4"},
|
|
141
|
+
"cell_8": {"clone_2_3_4"},
|
|
142
|
+
"cell_9": set(),
|
|
143
|
+
"cell_10": {"clone_2_3_4"},
|
|
144
|
+
"cell_11": set(),
|
|
145
|
+
"cell_12": {"clone_1", "clone_2_3_4"},
|
|
146
|
+
}
|
|
147
|
+
assert clone_call.clone_intbc_sets == {
|
|
148
|
+
"clone_1": {"A", "B"},
|
|
149
|
+
"clone_2_3_4": {"C", "D", "E", "F", "G"},
|
|
150
|
+
}
|
|
151
|
+
assert clone_call.cell_intbc_sets == {
|
|
152
|
+
"cell_1": {"A", "B"},
|
|
153
|
+
"cell_2": {"A", "B"},
|
|
154
|
+
"cell_3": {"A", "B"},
|
|
155
|
+
"cell_4": {"C", "D"},
|
|
156
|
+
"cell_5": {"C", "D"},
|
|
157
|
+
"cell_6": {"C", "D"},
|
|
158
|
+
"cell_7": {"E", "F"},
|
|
159
|
+
"cell_8": {"E", "F"},
|
|
160
|
+
"cell_9": set(),
|
|
161
|
+
"cell_10": {"F", "G"},
|
|
162
|
+
"cell_11": set(),
|
|
163
|
+
"cell_12": {"A", "B", "F", "G"},
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def drop_clones(
|
|
168
|
+
clones: t.List[str],
|
|
169
|
+
clone_call: CloneCallType
|
|
170
|
+
) -> CloneCallType:
|
|
171
|
+
"""
|
|
172
|
+
Drop the given clones.
|
|
173
|
+
|
|
174
|
+
clone_to_cells_mapping: Dict[CloneType, Set[CellBCType]]
|
|
175
|
+
=> we drop the clones from here.
|
|
176
|
+
cell_to_clones_mapping: Dict[CellBCType, Set[CloneType]]
|
|
177
|
+
=> We unmap every cell which maps to *any* of the dropped clones.
|
|
178
|
+
clone_intbc_sets: Dict[CloneType, Set[IntBCType]]
|
|
179
|
+
=> we drop the clones from here
|
|
180
|
+
cell_intbc_sets: Dict[CellBCType, Set[IntBCType]]
|
|
181
|
+
=> we clear the intBC set of every cell that we unmapped
|
|
182
|
+
"""
|
|
183
|
+
clone_to_cells_mapping = deepcopy(clone_call.clone_to_cells_mapping)
|
|
184
|
+
cell_to_clones_mapping = deepcopy(clone_call.cell_to_clones_mapping)
|
|
185
|
+
clone_intbc_sets = deepcopy(clone_call.clone_intbc_sets)
|
|
186
|
+
cell_intbc_sets = deepcopy(clone_call.cell_intbc_sets)
|
|
187
|
+
new_clone_to_cells_mapping = defaultdict(set)
|
|
188
|
+
new_cell_to_clones_mapping = {}
|
|
189
|
+
new_clone_intbc_sets = defaultdict(set)
|
|
190
|
+
new_cell_intbc_sets = {}
|
|
191
|
+
clones_set = set(clones)
|
|
192
|
+
for clone in clone_to_cells_mapping:
|
|
193
|
+
if clone not in clones:
|
|
194
|
+
new_clone_to_cells_mapping[clone] = clone_to_cells_mapping[clone]
|
|
195
|
+
for cell in cell_to_clones_mapping:
|
|
196
|
+
cell_clones = cell_to_clones_mapping[cell]
|
|
197
|
+
new_cell_clones = set()
|
|
198
|
+
if any([clone in clones_set for clone in cell_clones]):
|
|
199
|
+
new_cell_to_clones_mapping[cell] = set()
|
|
200
|
+
else:
|
|
201
|
+
new_cell_to_clones_mapping[cell] = cell_clones
|
|
202
|
+
for clone in clone_intbc_sets:
|
|
203
|
+
intbcs = clone_intbc_sets[clone]
|
|
204
|
+
if clone not in clones:
|
|
205
|
+
new_clone_intbc_sets[clone] = intbcs
|
|
206
|
+
for cell in cell_intbc_sets:
|
|
207
|
+
if len(new_cell_to_clones_mapping[cell]) == 0:
|
|
208
|
+
new_cell_intbc_sets[cell] = set()
|
|
209
|
+
else:
|
|
210
|
+
new_cell_intbc_sets[cell] = cell_intbc_sets[cell]
|
|
211
|
+
return CloneCallType( # NOTE: We cast to dict to avoid returning defaultdict
|
|
212
|
+
clone_to_cells_mapping=dict(new_clone_to_cells_mapping),
|
|
213
|
+
cell_to_clones_mapping=dict(new_cell_to_clones_mapping),
|
|
214
|
+
clone_intbc_sets=dict(new_clone_intbc_sets),
|
|
215
|
+
cell_intbc_sets=dict(new_cell_intbc_sets),
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_drop_clones():
|
|
220
|
+
clone_call = _get_test_clone_call()
|
|
221
|
+
clone_call = drop_clones(
|
|
222
|
+
clones=["clone_2", "clone_3", "clone_4"],
|
|
223
|
+
clone_call=clone_call,
|
|
224
|
+
)
|
|
225
|
+
assert clone_call.clone_to_cells_mapping == {
|
|
226
|
+
"clone_1": {"cell_1", "cell_2", "cell_3"},
|
|
227
|
+
}
|
|
228
|
+
assert clone_call.cell_to_clones_mapping == {
|
|
229
|
+
"cell_1": {"clone_1"},
|
|
230
|
+
"cell_2": {"clone_1"},
|
|
231
|
+
"cell_3": {"clone_1"},
|
|
232
|
+
"cell_4": set(),
|
|
233
|
+
"cell_5": set(),
|
|
234
|
+
"cell_6": set(),
|
|
235
|
+
"cell_7": set(),
|
|
236
|
+
"cell_8": set(),
|
|
237
|
+
"cell_9": set(),
|
|
238
|
+
"cell_10": set(),
|
|
239
|
+
"cell_11": set(),
|
|
240
|
+
"cell_12": set(),
|
|
241
|
+
}
|
|
242
|
+
assert clone_call.clone_intbc_sets == {
|
|
243
|
+
"clone_1": {"A", "B"},
|
|
244
|
+
}
|
|
245
|
+
assert clone_call.cell_intbc_sets == {
|
|
246
|
+
"cell_1": {"A", "B"},
|
|
247
|
+
"cell_2": {"A", "B"},
|
|
248
|
+
"cell_3": {"A", "B"},
|
|
249
|
+
"cell_4": set(),
|
|
250
|
+
"cell_5": set(),
|
|
251
|
+
"cell_6": set(),
|
|
252
|
+
"cell_7": set(),
|
|
253
|
+
"cell_8": set(),
|
|
254
|
+
"cell_9": set(),
|
|
255
|
+
"cell_10": set(),
|
|
256
|
+
"cell_11": set(),
|
|
257
|
+
"cell_12": set(),
|
|
258
|
+
}
|