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 ADDED
@@ -0,0 +1,5 @@
1
+ from .comparison import Comparison
2
+ from .insert import Insert
3
+ from .mapping import Mapping
4
+
5
+ __version__ = "0.8.3"
maphelios/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
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()
@@ -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