maphelios 0.8.3__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.
- maphelios/__init__.py +5 -0
- maphelios/__main__.py +6 -0
- maphelios/cli.py +106 -0
- maphelios/comparison.py +352 -0
- maphelios/genome_funcs.py +136 -0
- maphelios/helper.py +169 -0
- maphelios/insert.py +241 -0
- maphelios/mapping.py +699 -0
- maphelios/pacbio.py +620 -0
- maphelios-0.8.3.dist-info/METADATA +77 -0
- maphelios-0.8.3.dist-info/RECORD +15 -0
- maphelios-0.8.3.dist-info/WHEEL +5 -0
- maphelios-0.8.3.dist-info/entry_points.txt +2 -0
- maphelios-0.8.3.dist-info/licenses/LICENSE +21 -0
- maphelios-0.8.3.dist-info/top_level.txt +1 -0
maphelios/__init__.py
ADDED
maphelios/__main__.py
ADDED
maphelios/cli.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
|
|
5
|
+
from .main import Pipeline
|
|
6
|
+
from .plotting import plot_insert, plot_insert_dists, plot_genome
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_args(*args):
|
|
10
|
+
# Create the CLI using argparse
|
|
11
|
+
# TODO: maybe switch from argparse to click
|
|
12
|
+
parser = argparse.ArgumentParser(description="Generating plots")
|
|
13
|
+
|
|
14
|
+
subparsers = parser.add_subparsers(help="sub-command help")
|
|
15
|
+
|
|
16
|
+
parser_ncbi = subparsers.add_parser("ncbi", help="Download genome from NCBI")
|
|
17
|
+
parser_ncbi.add_argument("search_term", help="search term for BLAST")
|
|
18
|
+
parser_ncbi.add_argument("email", help="Your email address - needed by BLAST")
|
|
19
|
+
parser_ncbi.add_argument(
|
|
20
|
+
"--retmax",
|
|
21
|
+
default=200,
|
|
22
|
+
help="Maximum number of records to download from NCBI",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
parser_genome = subparsers.add_parser("file", help="Specify a local genome.")
|
|
26
|
+
parser_genome.add_argument("path_to_genome")
|
|
27
|
+
|
|
28
|
+
parser.add_argument("seq_file", help="file with the sequences")
|
|
29
|
+
parser.add_argument("output_prefix", help="Output directory")
|
|
30
|
+
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--fwd_suffix", default="_F", help="Suffix for forward seq. default='_F'"
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"--rev_suffix", default="_R", help="Suffix for reverse seq. default='_R'"
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--insert_types", help="matched, unmatched or both inserts", default="both"
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--filter",
|
|
42
|
+
default=0.5,
|
|
43
|
+
help="Threshold value for filtering out blast results with low coverage",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Get all the command line arguments
|
|
47
|
+
return parser.parse_args(*args)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def main():
|
|
51
|
+
args = get_args()
|
|
52
|
+
if "path_to_genome" in args:
|
|
53
|
+
search_term = None
|
|
54
|
+
email = None
|
|
55
|
+
retmax = None
|
|
56
|
+
path_to_genome = args.path_to_genome
|
|
57
|
+
else:
|
|
58
|
+
search_term = args.search_term
|
|
59
|
+
email = args.email
|
|
60
|
+
retmax = int(args.retmax)
|
|
61
|
+
path_to_genome = None
|
|
62
|
+
|
|
63
|
+
pipeline = Pipeline(
|
|
64
|
+
args.seq_file,
|
|
65
|
+
args.output_prefix,
|
|
66
|
+
genome_file=path_to_genome,
|
|
67
|
+
search_term=search_term,
|
|
68
|
+
email=email,
|
|
69
|
+
retmax=retmax,
|
|
70
|
+
fwd_suffix=args.fwd_suffix,
|
|
71
|
+
rev_suffix=args.rev_suffix,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Create linear plots of inserts with annotations
|
|
75
|
+
for seq_id in pipeline.seq_ids:
|
|
76
|
+
inserts = pipeline.get_inserts(
|
|
77
|
+
seq_id, insert_types=args.insert_types, filter_threshold=args.filter
|
|
78
|
+
)
|
|
79
|
+
for i, insert in enumerate(inserts):
|
|
80
|
+
fig, axs = plt.subplots(2, 1, figsize=(10, 10), height_ratios=[3, 5])
|
|
81
|
+
axs = plot_insert(insert, pipeline.genome, axs=axs)
|
|
82
|
+
fig.savefig(pipeline.work_dir / f"{seq_id}_hit{i}.png", dpi=300)
|
|
83
|
+
plt.close()
|
|
84
|
+
|
|
85
|
+
# Get all inserts (used in both next plots)
|
|
86
|
+
inserts = pipeline.get_all_inserts(args.insert_types, filter_threshold=args.filter)
|
|
87
|
+
|
|
88
|
+
# Create a plot of genome / all contigs as circular plot with inserts
|
|
89
|
+
# layered on top
|
|
90
|
+
fig, ax = plt.subplots(figsize=(10, 30))
|
|
91
|
+
ax = plot_on_genome(inserts, pipeline.genome, ax=ax)
|
|
92
|
+
fig.savefig(pipeline.work_dir / "genome_plot.png", dpi=300)
|
|
93
|
+
|
|
94
|
+
# Plots of distributions of insert sizes
|
|
95
|
+
fig, axs = plt.subplots(1, 3, figsize=(12, 5))
|
|
96
|
+
axs = plot_insert_dists(inserts, axs=axs)
|
|
97
|
+
fig.savefig(pipeline.work_dir / "insert_length_dist.png", dpi=300)
|
|
98
|
+
|
|
99
|
+
df = pipeline.to_dataframe(
|
|
100
|
+
insert_types=args.insert_types, filter_threshold=args.filter
|
|
101
|
+
)
|
|
102
|
+
df.to_csv(pipeline.work_dir / "inserts.csv", index=False)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
main()
|
maphelios/comparison.py
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from itertools import chain, cycle
|
|
3
|
+
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from dna_features_viewer import CircularGraphicRecord
|
|
8
|
+
from matplotlib import color_sequences
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Clusters(dict):
|
|
12
|
+
def __init__(self, clusters, genomes=None):
|
|
13
|
+
super(Clusters, self).__init__(clusters)
|
|
14
|
+
|
|
15
|
+
self.genomes = genomes or {g for g, clust_idx in self.keys()}
|
|
16
|
+
self.cluster_labels = list(self.keys())
|
|
17
|
+
|
|
18
|
+
def __contains__(self, item):
|
|
19
|
+
return item in self.cluster_labels
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def insert_ids(self):
|
|
23
|
+
insert_ids = {
|
|
24
|
+
genome: list(
|
|
25
|
+
chain.from_iterable(
|
|
26
|
+
(
|
|
27
|
+
insert_ids
|
|
28
|
+
for (g, clust_idx), insert_ids in self.items()
|
|
29
|
+
if g == genome
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
)
|
|
33
|
+
for genome in self.genomes
|
|
34
|
+
}
|
|
35
|
+
return insert_ids
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def insert_labels(self):
|
|
39
|
+
seq_labels = defaultdict(dict)
|
|
40
|
+
for g, clust_idx in self.cluster_labels:
|
|
41
|
+
insert_ids = self.__getitem__((g, clust_idx))
|
|
42
|
+
# Set it such that only one label is given per cluster
|
|
43
|
+
for insert_id in insert_ids:
|
|
44
|
+
seq_labels[g][insert_id] = None
|
|
45
|
+
seq_labels[g][insert_ids[-1]] = f"Cluster {clust_idx}"
|
|
46
|
+
return seq_labels
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def other_repr(self):
|
|
50
|
+
other = defaultdict(list)
|
|
51
|
+
for (g, clust_idx), insert_ids in self.items():
|
|
52
|
+
other[g].append((clust_idx, insert_ids))
|
|
53
|
+
return other
|
|
54
|
+
|
|
55
|
+
def subset(self, selection):
|
|
56
|
+
return Clusters(
|
|
57
|
+
{
|
|
58
|
+
(g, clust_idx): self.__getitem__((g, clust_idx))
|
|
59
|
+
for g, clust_idx in selection
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Comparison(dict):
|
|
65
|
+
|
|
66
|
+
def __init__(self, *args, **kwargs):
|
|
67
|
+
super(Comparison, self).__init__(*args, **kwargs)
|
|
68
|
+
|
|
69
|
+
self.insert_ids = {g: mapping.insert_ids for g, mapping in self.items()}
|
|
70
|
+
|
|
71
|
+
def get_inserts(
|
|
72
|
+
self,
|
|
73
|
+
genomes=None,
|
|
74
|
+
seq_ids=None,
|
|
75
|
+
insert_ids=None,
|
|
76
|
+
insert_type="both",
|
|
77
|
+
filter_threshold=None,
|
|
78
|
+
**kwargs,
|
|
79
|
+
):
|
|
80
|
+
if genomes is None:
|
|
81
|
+
genomes = self.keys()
|
|
82
|
+
|
|
83
|
+
return [
|
|
84
|
+
self.__getitem__(g).get(
|
|
85
|
+
insert_ids=insert_ids,
|
|
86
|
+
seq_ids=seq_ids,
|
|
87
|
+
insert_type=insert_type,
|
|
88
|
+
filter_threshold=filter_threshold,
|
|
89
|
+
)
|
|
90
|
+
for g in genomes
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
def get_inserts_df(
|
|
94
|
+
self, genomes=None, insert_type="both", filter_threshold=None, **kwargs
|
|
95
|
+
):
|
|
96
|
+
if genomes is None:
|
|
97
|
+
genomes = self.keys()
|
|
98
|
+
|
|
99
|
+
inserts_dfs = [
|
|
100
|
+
self.__getitem__(genome)
|
|
101
|
+
.to_dataframe(insert_type=insert_type, filter_threshold=filter_threshold)
|
|
102
|
+
.assign(genome=genome)
|
|
103
|
+
for genome in genomes
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
if len(inserts_dfs):
|
|
107
|
+
return pd.concat(inserts_dfs, ignore_index=True)
|
|
108
|
+
|
|
109
|
+
def get_insert_presence_df(
|
|
110
|
+
self, genomes=None, insert_type="both", filter_threshold=None, **kwargs
|
|
111
|
+
):
|
|
112
|
+
if genomes is None:
|
|
113
|
+
genomes = self.keys()
|
|
114
|
+
|
|
115
|
+
dfs = [
|
|
116
|
+
pd.DataFrame(
|
|
117
|
+
self.__getitem__(g).get_seq_ids(insert_type, filter_threshold),
|
|
118
|
+
columns=["insert_ids"],
|
|
119
|
+
).assign(genome=g)
|
|
120
|
+
for g in genomes
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
all_seq_ids = sorted(
|
|
124
|
+
set(chain.from_iterable(self.__getitem__(g).seq_ids for g in genomes))
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
df_insert_presence = (
|
|
128
|
+
pd.concat(dfs, ignore_index=True)
|
|
129
|
+
.groupby(["insert_ids", "genome"], as_index=False)
|
|
130
|
+
.agg(num_inserts=pd.NamedAgg("insert_ids", "count"))
|
|
131
|
+
.pivot(index="insert_ids", columns="genome", values="num_inserts")
|
|
132
|
+
.reindex(columns=genomes, index=all_seq_ids)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return df_insert_presence
|
|
136
|
+
|
|
137
|
+
def get_clusters(
|
|
138
|
+
self,
|
|
139
|
+
genomes=None,
|
|
140
|
+
insert_type="both",
|
|
141
|
+
filter_threshold=None,
|
|
142
|
+
plain_dict=True,
|
|
143
|
+
**kwargs,
|
|
144
|
+
):
|
|
145
|
+
if genomes is None:
|
|
146
|
+
genomes = self.keys()
|
|
147
|
+
|
|
148
|
+
if plain_dict:
|
|
149
|
+
return {
|
|
150
|
+
g: self.__getitem__(g).get_clusters(insert_type, filter_threshold)
|
|
151
|
+
for g in genomes
|
|
152
|
+
}
|
|
153
|
+
else:
|
|
154
|
+
return Clusters(
|
|
155
|
+
{
|
|
156
|
+
(g, clust_idx): cluster
|
|
157
|
+
for g in genomes
|
|
158
|
+
for clust_idx, cluster in enumerate(
|
|
159
|
+
self.__getitem__(g).get_clusters(insert_type, filter_threshold)
|
|
160
|
+
)
|
|
161
|
+
},
|
|
162
|
+
genomes=genomes,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def get_labels(
|
|
166
|
+
self,
|
|
167
|
+
seq_ids,
|
|
168
|
+
insert_ids=None,
|
|
169
|
+
genomes=None,
|
|
170
|
+
insert_type="both",
|
|
171
|
+
filter_threshold=None,
|
|
172
|
+
):
|
|
173
|
+
if genomes is None:
|
|
174
|
+
genomes = self.keys()
|
|
175
|
+
|
|
176
|
+
if insert_ids is None:
|
|
177
|
+
insert_ids = {g: None for g in genomes}
|
|
178
|
+
|
|
179
|
+
if seq_ids is not None or insert_ids is not None:
|
|
180
|
+
return {
|
|
181
|
+
g: self.__getitem__(g).get_labels(
|
|
182
|
+
seq_ids, insert_ids.get(g), genomes, insert_type, filter_threshold
|
|
183
|
+
)
|
|
184
|
+
for g in genomes
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
def get_genes_df(
|
|
188
|
+
self,
|
|
189
|
+
seq_ids=None,
|
|
190
|
+
insert_ids=None,
|
|
191
|
+
insert_type="both",
|
|
192
|
+
filter_threshold=None,
|
|
193
|
+
buffer=4000,
|
|
194
|
+
**kwargs,
|
|
195
|
+
):
|
|
196
|
+
if seq_ids is None:
|
|
197
|
+
seq_ids = {x: None for x in self.keys()}
|
|
198
|
+
elif isinstance(seq_ids, (list, tuple)):
|
|
199
|
+
seq_ids = {x: seq_ids for x in self.keys()}
|
|
200
|
+
|
|
201
|
+
if insert_ids is None:
|
|
202
|
+
insert_ids = {x: None for x in self.keys()}
|
|
203
|
+
elif isinstance(insert_ids, (list, tuple)):
|
|
204
|
+
insert_ids = {x: insert_ids for x in self.keys()}
|
|
205
|
+
|
|
206
|
+
genomes = set(seq_ids.keys()) | set(insert_ids.keys())
|
|
207
|
+
|
|
208
|
+
genes_dfs = [
|
|
209
|
+
self.__getitem__(g)
|
|
210
|
+
.genes_to_dataframe(
|
|
211
|
+
seq_ids=seq_ids.get(g),
|
|
212
|
+
insert_ids=insert_ids.get(g),
|
|
213
|
+
insert_type=insert_type,
|
|
214
|
+
filter_threshold=filter_threshold,
|
|
215
|
+
buffer=buffer,
|
|
216
|
+
)
|
|
217
|
+
.assign(genome=g)
|
|
218
|
+
for g in genomes
|
|
219
|
+
]
|
|
220
|
+
|
|
221
|
+
if len(genes_dfs):
|
|
222
|
+
drop_by = ["start", "end", "type", "insert_idx", "seq_id", "genome"]
|
|
223
|
+
df_genes = pd.concat(genes_dfs, ignore_index=True).drop_duplicates(
|
|
224
|
+
subset=drop_by
|
|
225
|
+
)
|
|
226
|
+
df_genes.insert(0, "genome", df_genes.pop("genome"))
|
|
227
|
+
return df_genes
|
|
228
|
+
|
|
229
|
+
def get_graphic_features(
|
|
230
|
+
self,
|
|
231
|
+
seq_ids=None,
|
|
232
|
+
insert_ids=None,
|
|
233
|
+
genomes_order=None,
|
|
234
|
+
seq_labels=None,
|
|
235
|
+
show_contig_labels=True,
|
|
236
|
+
insert_type="both",
|
|
237
|
+
filter_threshold=None,
|
|
238
|
+
palette="tab10",
|
|
239
|
+
**kwargs,
|
|
240
|
+
):
|
|
241
|
+
if seq_ids is None:
|
|
242
|
+
seq_ids = {x: None for x in self.keys()}
|
|
243
|
+
elif isinstance(seq_ids, (list, tuple)):
|
|
244
|
+
seq_ids = {x: seq_ids for x in self.keys()}
|
|
245
|
+
|
|
246
|
+
if insert_ids is None:
|
|
247
|
+
insert_ids = {x: None for x in self.keys()}
|
|
248
|
+
elif isinstance(insert_ids, (list, tuple)):
|
|
249
|
+
insert_ids = {x: insert_ids for x in self.keys()}
|
|
250
|
+
|
|
251
|
+
if genomes_order is None:
|
|
252
|
+
genomes_order = set(seq_ids.keys()) | set(insert_ids.keys())
|
|
253
|
+
|
|
254
|
+
if seq_labels is None:
|
|
255
|
+
seq_labels = {x: None for x in self.keys()}
|
|
256
|
+
|
|
257
|
+
palette = cycle(color_sequences[palette])
|
|
258
|
+
|
|
259
|
+
res = {
|
|
260
|
+
g: self.__getitem__(g).get_graphic_features(
|
|
261
|
+
seq_ids=seq_ids.get(g),
|
|
262
|
+
insert_ids=insert_ids.get(g),
|
|
263
|
+
insert_type=insert_type,
|
|
264
|
+
filter_threshold=filter_threshold,
|
|
265
|
+
contig_labels=show_contig_labels,
|
|
266
|
+
seq_labels=seq_labels.get(g),
|
|
267
|
+
col1=col,
|
|
268
|
+
col2=col,
|
|
269
|
+
linecolor=col,
|
|
270
|
+
**kwargs,
|
|
271
|
+
)
|
|
272
|
+
for g, col in zip(genomes_order, palette)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return res
|
|
276
|
+
|
|
277
|
+
def plot(
|
|
278
|
+
self,
|
|
279
|
+
seq_ids=None,
|
|
280
|
+
insert_ids=None,
|
|
281
|
+
genomes_order=None,
|
|
282
|
+
seq_labels=None,
|
|
283
|
+
insert_type="both",
|
|
284
|
+
filter_threshold=None,
|
|
285
|
+
feature_kwargs=None,
|
|
286
|
+
show_contig_labels=True,
|
|
287
|
+
show_titles=False,
|
|
288
|
+
palette="tab10",
|
|
289
|
+
facet_wrap=None,
|
|
290
|
+
fig=None,
|
|
291
|
+
**kwargs,
|
|
292
|
+
):
|
|
293
|
+
feature_kwargs = {} if feature_kwargs is None else feature_kwargs
|
|
294
|
+
|
|
295
|
+
# Get the graphic feature objects
|
|
296
|
+
res = self.get_graphic_features(
|
|
297
|
+
seq_ids,
|
|
298
|
+
insert_ids,
|
|
299
|
+
genomes_order,
|
|
300
|
+
seq_labels,
|
|
301
|
+
show_contig_labels,
|
|
302
|
+
insert_type,
|
|
303
|
+
filter_threshold,
|
|
304
|
+
palette,
|
|
305
|
+
**feature_kwargs,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# Set all the plotting options based on input
|
|
309
|
+
if "figsize" not in kwargs:
|
|
310
|
+
kwargs["figsize"] = (10, 8)
|
|
311
|
+
|
|
312
|
+
genome_labels = res.keys()
|
|
313
|
+
num_genomes = len(genome_labels)
|
|
314
|
+
|
|
315
|
+
# Set the number of axes in the figure based on facet_wrap option
|
|
316
|
+
nrows, ncols = 1, 1
|
|
317
|
+
if facet_wrap is not None:
|
|
318
|
+
ncols = min(facet_wrap, num_genomes)
|
|
319
|
+
nrows = num_genomes // ncols
|
|
320
|
+
|
|
321
|
+
# Create figure and axes if not provided
|
|
322
|
+
if fig is None:
|
|
323
|
+
fig, axs = plt.subplots(nrows=nrows, ncols=ncols, **kwargs)
|
|
324
|
+
else:
|
|
325
|
+
axs = fig.get_axes()
|
|
326
|
+
|
|
327
|
+
# Check that axs is iterable even if a single axes object
|
|
328
|
+
if isinstance(axs, plt.Axes):
|
|
329
|
+
axs = np.array([axs])
|
|
330
|
+
|
|
331
|
+
# Make and label all the plots
|
|
332
|
+
if facet_wrap is None:
|
|
333
|
+
features = list(chain.from_iterable([x[1] for x in res.values()]))
|
|
334
|
+
rec = CircularGraphicRecord(max([x[0] for x in res.values()]), features)
|
|
335
|
+
_ = rec.plot(axs[0], annotate_inline=False)
|
|
336
|
+
|
|
337
|
+
if show_titles:
|
|
338
|
+
axs[0].set_title(", ".join(list(genome_labels)))
|
|
339
|
+
|
|
340
|
+
else:
|
|
341
|
+
for (label, (seq_len, features)), ax in zip(res.items(), axs.flatten()):
|
|
342
|
+
rec = CircularGraphicRecord(seq_len, features)
|
|
343
|
+
_ = rec.plot(ax, annotate_inline=False)
|
|
344
|
+
|
|
345
|
+
if show_titles:
|
|
346
|
+
ax.set_title(label)
|
|
347
|
+
|
|
348
|
+
# Remove redundant axis
|
|
349
|
+
for ax in axs[num_genomes:]:
|
|
350
|
+
ax.axis("off")
|
|
351
|
+
|
|
352
|
+
return fig
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import warnings
|
|
3
|
+
from hashlib import sha1
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from BCBio import GFF
|
|
7
|
+
from Bio import Entrez, SearchIO, SeqIO
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _download_genome(search_term, retmax=100):
|
|
11
|
+
# Set the email address for NCBI queries
|
|
12
|
+
# Entrez.email = email
|
|
13
|
+
|
|
14
|
+
# Get record ids from NCBI
|
|
15
|
+
with warnings.catch_warnings():
|
|
16
|
+
warnings.simplefilter("ignore")
|
|
17
|
+
with Entrez.esearch(db="nucleotide", term=search_term, retmax=retmax) as handle:
|
|
18
|
+
res = Entrez.read(handle)
|
|
19
|
+
|
|
20
|
+
# Download these records from NCBI rettype="gbwithparts" is used to
|
|
21
|
+
# download the sequences with record features
|
|
22
|
+
with Entrez.efetch(
|
|
23
|
+
db="nucleotide",
|
|
24
|
+
id=",".join(res["IdList"]),
|
|
25
|
+
rettype="gbwithparts",
|
|
26
|
+
retmode="text",
|
|
27
|
+
) as handle:
|
|
28
|
+
data_gb = SeqIO.to_dict(SeqIO.parse(handle, "gb"))
|
|
29
|
+
|
|
30
|
+
return data_gb
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def download_genome(search_term, retmax=100, output_path=None):
|
|
34
|
+
|
|
35
|
+
if output_path is None:
|
|
36
|
+
data_gb = _download_genome(search_term, retmax)
|
|
37
|
+
|
|
38
|
+
else:
|
|
39
|
+
output_path = Path(output_path)
|
|
40
|
+
|
|
41
|
+
# Check output_path is given
|
|
42
|
+
if Path(output_path).exists():
|
|
43
|
+
# Read in the file
|
|
44
|
+
data_gb = SeqIO.to_dict(SeqIO.parse(output_path, "genbank"))
|
|
45
|
+
|
|
46
|
+
# Otherwise retrieve the records from NCBI
|
|
47
|
+
else:
|
|
48
|
+
data_gb = _download_genome(search_term, retmax)
|
|
49
|
+
SeqIO.write(data_gb.values(), output_path, "genbank")
|
|
50
|
+
|
|
51
|
+
return data_gb
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_genome_file(work_dir, search_term, retmax):
|
|
55
|
+
work_dir = Path(work_dir)
|
|
56
|
+
# Hash the search term to use as filename in cache dir
|
|
57
|
+
search_hashed = sha1((search_term + str(retmax)).encode()).hexdigest()
|
|
58
|
+
return work_dir / f"db_{search_hashed}.gbk"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_genome(genome_file, genome_fasta, search_term=None, retmax=None):
|
|
62
|
+
# Get genome
|
|
63
|
+
if search_term is not None:
|
|
64
|
+
genome = download_genome(search_term, retmax, genome_file)
|
|
65
|
+
else:
|
|
66
|
+
if genome_file.suffix == ".gff":
|
|
67
|
+
genome = SeqIO.to_dict(GFF.parse(genome_file))
|
|
68
|
+
elif genome_file.suffix == ".gbk":
|
|
69
|
+
genome = SeqIO.to_dict(SeqIO.parse(genome_file, "genbank"))
|
|
70
|
+
elif genome_file.suffix in (".fa", ".fasta"):
|
|
71
|
+
genome = SeqIO.to_dict(SeqIO.parse(genome_file, "fasta"))
|
|
72
|
+
else:
|
|
73
|
+
raise RuntimeError("Wrong format expected either `.gbk` or `.gff` file.")
|
|
74
|
+
|
|
75
|
+
defined_seqs = [x for x in genome.values() if x.seq.defined]
|
|
76
|
+
|
|
77
|
+
if not len(defined_seqs):
|
|
78
|
+
message = f"Genome file {genome_file} does not contain any sequences!"
|
|
79
|
+
if search_term is not None:
|
|
80
|
+
message = (
|
|
81
|
+
f"Search term '{search_term}' did not yield a genome with sequences"
|
|
82
|
+
)
|
|
83
|
+
raise RuntimeError(message)
|
|
84
|
+
|
|
85
|
+
# Save in fasta format (only acceptable format for makeblastdb)
|
|
86
|
+
SeqIO.write(defined_seqs, genome_fasta, "fasta")
|
|
87
|
+
|
|
88
|
+
return genome
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _correct_hit_id(x):
|
|
92
|
+
"""Helper function to get rid red| or emb| added by BLAST to contig ID"""
|
|
93
|
+
if "|" in x.id:
|
|
94
|
+
x.id = x.id.split("|")[1]
|
|
95
|
+
return x
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def run_blast(seq_file, db_file, blast_output, blast_options=None):
|
|
99
|
+
# TODO: check how to catch errors from blast
|
|
100
|
+
# make blast database
|
|
101
|
+
run1 = subprocess.run(
|
|
102
|
+
f"makeblastdb -in '{db_file}' -parse_seqids -dbtype nucl",
|
|
103
|
+
shell=True,
|
|
104
|
+
check=True,
|
|
105
|
+
stdout=subprocess.PIPE,
|
|
106
|
+
stderr=subprocess.PIPE,
|
|
107
|
+
)
|
|
108
|
+
if run1.returncode != 0:
|
|
109
|
+
raise OSError(f"Failed to run makeblastdb: {run1.stderr.decode()}")
|
|
110
|
+
|
|
111
|
+
if blast_options is None:
|
|
112
|
+
blast_options = ""
|
|
113
|
+
|
|
114
|
+
# align input sequences with query using blastn with default options
|
|
115
|
+
run2 = subprocess.run(
|
|
116
|
+
"blastn "
|
|
117
|
+
f"-query '{seq_file}' -db '{db_file}' "
|
|
118
|
+
f"-out '{blast_output}' -outfmt 5 {blast_options}",
|
|
119
|
+
shell=True,
|
|
120
|
+
check=True,
|
|
121
|
+
stdout=subprocess.PIPE,
|
|
122
|
+
stderr=subprocess.PIPE,
|
|
123
|
+
)
|
|
124
|
+
if run2.returncode != 0:
|
|
125
|
+
raise OSError(f"Failed to run blastn: {run2.stderr.decode()}")
|
|
126
|
+
|
|
127
|
+
# Return a dictionary correcting the hit IDs which get an unnecessary
|
|
128
|
+
# prefix from BLAST
|
|
129
|
+
return {
|
|
130
|
+
x.id: x.hit_map(_correct_hit_id)
|
|
131
|
+
for x in SearchIO.parse(blast_output, "blast-xml")
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def run_minimap2(seq_file, db_file, output_file):
|
|
136
|
+
pass
|