genome-comparator 0.4.6__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Marc-Olivier Duceppe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,140 @@
1
+ Metadata-Version: 2.4
2
+ Name: genome-comparator
3
+ Version: 0.4.6
4
+ Summary: Fast genome comparison with Mash: distance matrix, UPGMA/NJ/ME trees with bootstrap support, and interactive PCoA
5
+ Author-email: Marc-Olivier Duceppe <marc-olivier.duceppe@inspection.gc.ca>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/duceppemo/genome_comparator
8
+ Project-URL: Documentation, https://github.com/duceppemo/genome_comparator/wiki
9
+ Project-URL: Changelog, https://github.com/duceppemo/genome_comparator/wiki/Changelog
10
+ Project-URL: Issues, https://github.com/duceppemo/genome_comparator/issues
11
+ Project-URL: DOI, https://doi.org/10.5281/zenodo.22920856
12
+ Keywords: bioinformatics,genomics,mash,minhash,phylogenetics,microbial genomics,pcoa
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Environment :: Console
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Operating System :: MacOS
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: numpy>=1.24
29
+ Requires-Dist: pandas>=2.0
30
+ Requires-Dist: scipy>=1.10
31
+ Requires-Dist: scikit-bio>=0.7
32
+ Requires-Dist: plotly>=5.15
33
+ Requires-Dist: openpyxl>=3.1
34
+ Provides-Extra: test
35
+ Requires-Dist: pytest>=7; extra == "test"
36
+ Requires-Dist: pytest-cov>=5; extra == "test"
37
+ Dynamic: license-file
38
+
39
+ <p align="center">
40
+ <img src="https://raw.githubusercontent.com/duceppemo/genome_comparator/master/assets/logo.png" width="180" alt="genome_comparator logo">
41
+ </p>
42
+
43
+ <h1 align="center">genome_comparator</h1>
44
+
45
+ <p align="center">
46
+ <a href="https://github.com/duceppemo/genome_comparator/actions/workflows/tests.yml"><img src="https://github.com/duceppemo/genome_comparator/actions/workflows/tests.yml/badge.svg" alt="Tests"></a>
47
+ <a href="https://github.com/duceppemo/genome_comparator/releases/latest"><img src="https://img.shields.io/github/v/release/duceppemo/genome_comparator" alt="Release"></a>
48
+ <a href="https://pypi.org/project/genome-comparator/"><img src="https://img.shields.io/pypi/v/genome-comparator" alt="PyPI"></a>
49
+ <a href="https://codecov.io/gh/duceppemo/genome_comparator"><img src="https://codecov.io/gh/duceppemo/genome_comparator/graph/badge.svg" alt="Coverage"></a>
50
+ <img src="https://img.shields.io/badge/python-3.10%E2%80%933.14-blue" alt="Python 3.10–3.14">
51
+ <a href="LICENSE"><img src="https://img.shields.io/github/license/duceppemo/genome_comparator" alt="License"></a>
52
+ <a href="https://github.com/duceppemo/genome_comparator/wiki"><img src="https://img.shields.io/badge/docs-wiki-informational" alt="Documentation"></a>
53
+ <a href="https://doi.org/10.5281/zenodo.22920856"><img src="https://zenodo.org/badge/DOI/10.5281/zenodo.22920856.svg" alt="DOI"></a>
54
+ </p>
55
+
56
+ Quickly compare and visualize distances between genomes, from assemblies (fasta) or reads (fastq), using
57
+ [Mash](https://github.com/marbl/Mash). Produces a distance matrix, trees (UPGMA, neighbour joining, minimum
58
+ evolution) with optional bootstrap support, and an interactive PCoA plot.
59
+
60
+ <p align="center">
61
+ <img src="https://raw.githubusercontent.com/duceppemo/genome_comparator/master/assets/tree.png" width="49%" alt="Neighbour joining tree of 22 Listeria genomes with bootstrap support values">
62
+ <img src="https://raw.githubusercontent.com/duceppemo/genome_comparator/master/assets/pcoa.png" width="49%" alt="PCoA of 22 Listeria genomes coloured by species">
63
+ <br>
64
+ <sub>22 public <i>Listeria</i> genomes from the <a href="https://github.com/duceppemo/genome_comparator/wiki/Tutorial">tutorial</a>: 3 seconds, or under 2 minutes with 100 bootstrap replicates.</sub>
65
+ </p>
66
+
67
+ ## Features
68
+ * **Assemblies or reads**: fasta or fastq (gzipped or not), paired-end reads combined per sample.
69
+ * **Fast and scalable**: thousands of genomes; sketches are computed in parallel and reused between runs.
70
+ * **Trees**: UPGMA, neighbour joining and minimum evolution, with optional **bootstrap support**.
71
+ * **Interactive PCoA** with metadata on hover and colourblind-friendly colouring by any metadata column.
72
+ * **Safe by default**: ambiguous sample names and unreadable files are reported, never silently merged or ignored.
73
+
74
+ ## How it works
75
+ ```mermaid
76
+ flowchart LR
77
+ A[fasta / fastq files] -->|mash sketch| B[one sketch per sample]
78
+ B -->|mash triangle| C[pairwise distance matrix]
79
+ C --> D[UPGMA / NJ / ME trees]
80
+ C --> E[PCoA plot]
81
+ B -. re-sketch with other hash seeds .-> F[bootstrap replicates]
82
+ F -. support values .-> D
83
+ ```
84
+
85
+ ## Installation
86
+ With pip. Mash is not available from PyPI: install it separately (e.g. `conda install -c bioconda mash`).
87
+ ```
88
+ pip install genome-comparator
89
+ ```
90
+
91
+ Or from source, in a conda environment that includes Mash:
92
+ ```
93
+ git clone https://github.com/duceppemo/genome_comparator
94
+ cd genome_comparator
95
+ conda env create -f environment.yml
96
+ conda activate genome_comparator
97
+ pip install --no-deps .
98
+ ```
99
+
100
+ ## Quick start
101
+ ```
102
+ genome-comparator -i /input/folder/ -o /output/folder/ --nj --pcoa
103
+ ```
104
+
105
+ New to the tool? Follow the **[tutorial](https://github.com/duceppemo/genome_comparator/wiki/Tutorial)**: it downloads
106
+ 22 public genomes and walks through every output in a few minutes.
107
+
108
+ ## Documentation
109
+ The **[wiki](https://github.com/duceppemo/genome_comparator/wiki)** covers
110
+ [usage and options](https://github.com/duceppemo/genome_comparator/wiki/Usage),
111
+ [output files](https://github.com/duceppemo/genome_comparator/wiki/Output-files),
112
+ [interpreting the results](https://github.com/duceppemo/genome_comparator/wiki/How-it-works),
113
+ [bootstrap support](https://github.com/duceppemo/genome_comparator/wiki/Bootstrap-support),
114
+ [performance](https://github.com/duceppemo/genome_comparator/wiki/Performance),
115
+ [related tools](https://github.com/duceppemo/genome_comparator/wiki/Related-tools),
116
+ [troubleshooting](https://github.com/duceppemo/genome_comparator/wiki/Troubleshooting) and the
117
+ [FAQ](https://github.com/duceppemo/genome_comparator/wiki/FAQ).
118
+
119
+ ## Citing
120
+ If you use genome_comparator, please cite it and Mash, which computes the distances:
121
+
122
+ > Duceppe M-O. genome_comparator: fast comparison and visualization of genome distances with Mash. Zenodo.
123
+ > https://doi.org/10.5281/zenodo.22920856
124
+
125
+ This DOI always points to the latest version; each release also has its own DOI, listed on
126
+ [Zenodo](https://doi.org/10.5281/zenodo.22920856). GitHub's **"Cite this repository"** button gives the same citation
127
+ in APA and BibTeX formats.
128
+
129
+
130
+ > Ondov BD, Treangen TJ, Melsted P, Mallonee AB, Bergman NH, Koren S, Phillippy AM. Mash: fast genome and metagenome
131
+ > distance estimation using MinHash. *Genome Biology* 17, 132 (2016). https://doi.org/10.1186/s13059-016-0997-x
132
+
133
+ ## Contributing
134
+ Bug reports, questions and pull requests are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md).
135
+
136
+ ## Author
137
+ Marc-Olivier Duceppe, Canadian Food Inspection Agency (CFIA): marc-olivier.duceppe@inspection.gc.ca
138
+
139
+ ## License
140
+ [MIT](LICENSE)
@@ -0,0 +1,102 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/duceppemo/genome_comparator/master/assets/logo.png" width="180" alt="genome_comparator logo">
3
+ </p>
4
+
5
+ <h1 align="center">genome_comparator</h1>
6
+
7
+ <p align="center">
8
+ <a href="https://github.com/duceppemo/genome_comparator/actions/workflows/tests.yml"><img src="https://github.com/duceppemo/genome_comparator/actions/workflows/tests.yml/badge.svg" alt="Tests"></a>
9
+ <a href="https://github.com/duceppemo/genome_comparator/releases/latest"><img src="https://img.shields.io/github/v/release/duceppemo/genome_comparator" alt="Release"></a>
10
+ <a href="https://pypi.org/project/genome-comparator/"><img src="https://img.shields.io/pypi/v/genome-comparator" alt="PyPI"></a>
11
+ <a href="https://codecov.io/gh/duceppemo/genome_comparator"><img src="https://codecov.io/gh/duceppemo/genome_comparator/graph/badge.svg" alt="Coverage"></a>
12
+ <img src="https://img.shields.io/badge/python-3.10%E2%80%933.14-blue" alt="Python 3.10–3.14">
13
+ <a href="LICENSE"><img src="https://img.shields.io/github/license/duceppemo/genome_comparator" alt="License"></a>
14
+ <a href="https://github.com/duceppemo/genome_comparator/wiki"><img src="https://img.shields.io/badge/docs-wiki-informational" alt="Documentation"></a>
15
+ <a href="https://doi.org/10.5281/zenodo.22920856"><img src="https://zenodo.org/badge/DOI/10.5281/zenodo.22920856.svg" alt="DOI"></a>
16
+ </p>
17
+
18
+ Quickly compare and visualize distances between genomes, from assemblies (fasta) or reads (fastq), using
19
+ [Mash](https://github.com/marbl/Mash). Produces a distance matrix, trees (UPGMA, neighbour joining, minimum
20
+ evolution) with optional bootstrap support, and an interactive PCoA plot.
21
+
22
+ <p align="center">
23
+ <img src="https://raw.githubusercontent.com/duceppemo/genome_comparator/master/assets/tree.png" width="49%" alt="Neighbour joining tree of 22 Listeria genomes with bootstrap support values">
24
+ <img src="https://raw.githubusercontent.com/duceppemo/genome_comparator/master/assets/pcoa.png" width="49%" alt="PCoA of 22 Listeria genomes coloured by species">
25
+ <br>
26
+ <sub>22 public <i>Listeria</i> genomes from the <a href="https://github.com/duceppemo/genome_comparator/wiki/Tutorial">tutorial</a>: 3 seconds, or under 2 minutes with 100 bootstrap replicates.</sub>
27
+ </p>
28
+
29
+ ## Features
30
+ * **Assemblies or reads**: fasta or fastq (gzipped or not), paired-end reads combined per sample.
31
+ * **Fast and scalable**: thousands of genomes; sketches are computed in parallel and reused between runs.
32
+ * **Trees**: UPGMA, neighbour joining and minimum evolution, with optional **bootstrap support**.
33
+ * **Interactive PCoA** with metadata on hover and colourblind-friendly colouring by any metadata column.
34
+ * **Safe by default**: ambiguous sample names and unreadable files are reported, never silently merged or ignored.
35
+
36
+ ## How it works
37
+ ```mermaid
38
+ flowchart LR
39
+ A[fasta / fastq files] -->|mash sketch| B[one sketch per sample]
40
+ B -->|mash triangle| C[pairwise distance matrix]
41
+ C --> D[UPGMA / NJ / ME trees]
42
+ C --> E[PCoA plot]
43
+ B -. re-sketch with other hash seeds .-> F[bootstrap replicates]
44
+ F -. support values .-> D
45
+ ```
46
+
47
+ ## Installation
48
+ With pip. Mash is not available from PyPI: install it separately (e.g. `conda install -c bioconda mash`).
49
+ ```
50
+ pip install genome-comparator
51
+ ```
52
+
53
+ Or from source, in a conda environment that includes Mash:
54
+ ```
55
+ git clone https://github.com/duceppemo/genome_comparator
56
+ cd genome_comparator
57
+ conda env create -f environment.yml
58
+ conda activate genome_comparator
59
+ pip install --no-deps .
60
+ ```
61
+
62
+ ## Quick start
63
+ ```
64
+ genome-comparator -i /input/folder/ -o /output/folder/ --nj --pcoa
65
+ ```
66
+
67
+ New to the tool? Follow the **[tutorial](https://github.com/duceppemo/genome_comparator/wiki/Tutorial)**: it downloads
68
+ 22 public genomes and walks through every output in a few minutes.
69
+
70
+ ## Documentation
71
+ The **[wiki](https://github.com/duceppemo/genome_comparator/wiki)** covers
72
+ [usage and options](https://github.com/duceppemo/genome_comparator/wiki/Usage),
73
+ [output files](https://github.com/duceppemo/genome_comparator/wiki/Output-files),
74
+ [interpreting the results](https://github.com/duceppemo/genome_comparator/wiki/How-it-works),
75
+ [bootstrap support](https://github.com/duceppemo/genome_comparator/wiki/Bootstrap-support),
76
+ [performance](https://github.com/duceppemo/genome_comparator/wiki/Performance),
77
+ [related tools](https://github.com/duceppemo/genome_comparator/wiki/Related-tools),
78
+ [troubleshooting](https://github.com/duceppemo/genome_comparator/wiki/Troubleshooting) and the
79
+ [FAQ](https://github.com/duceppemo/genome_comparator/wiki/FAQ).
80
+
81
+ ## Citing
82
+ If you use genome_comparator, please cite it and Mash, which computes the distances:
83
+
84
+ > Duceppe M-O. genome_comparator: fast comparison and visualization of genome distances with Mash. Zenodo.
85
+ > https://doi.org/10.5281/zenodo.22920856
86
+
87
+ This DOI always points to the latest version; each release also has its own DOI, listed on
88
+ [Zenodo](https://doi.org/10.5281/zenodo.22920856). GitHub's **"Cite this repository"** button gives the same citation
89
+ in APA and BibTeX formats.
90
+
91
+
92
+ > Ondov BD, Treangen TJ, Melsted P, Mallonee AB, Bergman NH, Koren S, Phillippy AM. Mash: fast genome and metagenome
93
+ > distance estimation using MinHash. *Genome Biology* 17, 132 (2016). https://doi.org/10.1186/s13059-016-0997-x
94
+
95
+ ## Contributing
96
+ Bug reports, questions and pull requests are welcome: see [CONTRIBUTING.md](CONTRIBUTING.md).
97
+
98
+ ## Author
99
+ Marc-Olivier Duceppe, Canadian Food Inspection Agency (CFIA): marc-olivier.duceppe@inspection.gc.ca
100
+
101
+ ## License
102
+ [MIT](LICENSE)
@@ -0,0 +1,3 @@
1
+ """Quickly compare and visualize distances between genomes using Mash."""
2
+
3
+ __version__ = '0.4.6'
@@ -0,0 +1,6 @@
1
+ """Allow running the main command with "python -m genome_comparator"."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == '__main__':
6
+ main()
@@ -0,0 +1,78 @@
1
+ """
2
+ Bootstrap support for trees built from Mash distances.
3
+
4
+ There is no alignment to resample, so each replicate re-sketches the genomes with a different hash seed
5
+ ("mash sketch -S"), which picks a different random subset of k-mers (the approach used by mashtree).
6
+ The support of a clade is the percentage of replicate trees that contain it.
7
+
8
+ Clades are stored as integer bitmasks over the tip names (bit i set = tip i is in the clade),
9
+ which keeps memory and comparisons cheap on trees with thousands of tips.
10
+ """
11
+
12
+ from collections import Counter
13
+
14
+ ROOTED_TREES = ('hc',) # Hierarchical clustering trees are rooted, NJ and ME trees are not
15
+
16
+
17
+ def clade_masks(tree, index):
18
+ """
19
+ :param index: dict {tip name: bit position}
20
+ :return: dict {internal node: bitmask of the tips below it}, root excluded
21
+ """
22
+ masks = dict()
23
+ for node in tree.postorder(include_self=False):
24
+ if node.is_tip():
25
+ masks[node] = 1 << index[node.name]
26
+ else:
27
+ mask = 0
28
+ for child in node.children:
29
+ mask |= masks[child]
30
+ masks[node] = mask
31
+ return {node: mask for node, mask in masks.items() if not node.is_tip()}
32
+
33
+
34
+ def canonical_split(mask, n_tips):
35
+ """An unrooted split and its complement are the same bipartition. Keep the side without tip 0."""
36
+ return mask ^ ((1 << n_tips) - 1) if mask & 1 else mask
37
+
38
+
39
+ def node_splits(tree, index, rooted):
40
+ """
41
+ :return: dict {internal node: clade (rooted) or bipartition (unrooted)}, trivial splits excluded
42
+ """
43
+ n_tips = len(index)
44
+ splits = dict()
45
+ for node, mask in clade_masks(tree, index).items():
46
+ if not rooted:
47
+ mask = canonical_split(mask, n_tips)
48
+ size = bin(mask).count('1')
49
+ if 1 < size < n_tips - (0 if rooted else 1):
50
+ splits[node] = mask
51
+ return splits
52
+
53
+
54
+ class SupportCounter:
55
+ """Count how often the clades of a reference tree are found in replicate trees."""
56
+
57
+ def __init__(self, tree, rooted):
58
+ self.tree = tree
59
+ self.rooted = rooted
60
+ self.index = {name: i for i, name in enumerate(sorted(t.name for t in tree.tips()))}
61
+ self.splits = node_splits(tree, self.index, rooted)
62
+ self.wanted = set(self.splits.values())
63
+ self.counts = Counter()
64
+ self.replicates = 0
65
+
66
+ def add(self, replicate_tree):
67
+ self.add_splits(set(node_splits(replicate_tree, self.index, self.rooted).values()))
68
+
69
+ def add_splits(self, found):
70
+ """Add the clades (bitmasks) of one replicate tree, e.g. computed in another process."""
71
+ self.counts.update(found & self.wanted)
72
+ self.replicates += 1
73
+
74
+ def assign(self):
75
+ """Store the support (% of replicates) in the "support" attribute of each internal node."""
76
+ for node, mask in self.splits.items():
77
+ node.support = round(100 * self.counts[mask] / self.replicates) if self.replicates else None
78
+ return self.tree
@@ -0,0 +1,240 @@
1
+ """Command line entry points."""
2
+
3
+ import logging
4
+ import os
5
+ import sys
6
+ from argparse import ArgumentParser, ArgumentTypeError, ArgumentDefaultsHelpFormatter
7
+ from pathlib import Path
8
+
9
+ from . import __version__, matrix, ordination, trees
10
+ from .mash import MAX_KMER_SIZE, MashError
11
+ from .matrix import MatrixError
12
+ from .pipeline import MIN_SAMPLES, GenomeComparator, analyze_matrix, step
13
+ from .samples import SampleError
14
+ from .tree_tools import collapse, read_rename_table, rename_tips
15
+
16
+ log = logging.getLogger('genome_comparator')
17
+
18
+ # Expected errors are reported without a traceback
19
+ USER_ERRORS = (MashError, MatrixError, SampleError, ValueError, OSError)
20
+
21
+
22
+ def setup_logging(verbose=False, log_file=None):
23
+ """Configure the package logger only, so importing genome_comparator never alters the root logger."""
24
+ for handler in list(log.handlers):
25
+ log.removeHandler(handler)
26
+ handler.close()
27
+ handlers = [logging.StreamHandler(sys.stderr)]
28
+ if log_file:
29
+ handlers.append(logging.FileHandler(log_file, mode='w'))
30
+ formatter = logging.Formatter('%(asctime)s %(levelname)-7s %(message)s', datefmt='%H:%M:%S')
31
+ for handler in handlers:
32
+ handler.setFormatter(formatter)
33
+ log.addHandler(handler)
34
+ log.setLevel(logging.DEBUG if verbose else logging.INFO)
35
+
36
+
37
+ def int_range(low, high=None):
38
+ def check(value):
39
+ try:
40
+ value = int(value)
41
+ except ValueError:
42
+ raise ArgumentTypeError('"{}" is not an integer'.format(value))
43
+ if value < low or (high is not None and value > high):
44
+ raise ArgumentTypeError('must be between {} and {}'.format(low, high) if high is not None
45
+ else 'must be >= {}'.format(low))
46
+ return value
47
+ return check
48
+
49
+
50
+ def available_cpus():
51
+ try:
52
+ return len(os.sched_getaffinity(0)) # Respects cgroup/taskset limits on Linux (e.g. SLURM jobs)
53
+ except AttributeError:
54
+ return os.cpu_count() or 1
55
+
56
+
57
+ def add_tree_arguments(parser):
58
+ group = parser.add_argument_group('trees and ordination')
59
+ group.add_argument('--linkage', choices=trees.LINKAGE_METHODS, default='average',
60
+ help='Hierarchical clustering method for the "_hc" tree. "average" is UPGMA.')
61
+ group.add_argument('--nj', action='store_true',
62
+ help='Also build a neighbour joining tree. Slower than --me on very large datasets.')
63
+ group.add_argument('--me', action='store_true',
64
+ help='Also build a balanced minimum evolution tree (with NNI). '
65
+ 'Similar to NJ but much faster on large datasets.')
66
+ group.add_argument('--pcoa', '--pca', dest='pcoa', action='store_true',
67
+ help='Also run a principal coordinates analysis (PCoA) and save an interactive html plot.')
68
+ group.add_argument('--metadata', metavar='metadata.tsv',
69
+ help='Tab-separated file, first column is the sample name. '
70
+ 'Extra columns are shown when hovering over PCoA points.')
71
+ group.add_argument('--color-by', metavar='COLUMN',
72
+ help='Metadata column used to colour the PCoA points.')
73
+
74
+
75
+ def check_tree_arguments(parser, args):
76
+ if args.color_by and not args.metadata:
77
+ parser.error('--color-by requires --metadata')
78
+ if args.metadata and not args.pcoa:
79
+ parser.error('--metadata is only used with --pcoa')
80
+ if args.metadata:
81
+ try:
82
+ ordination.read_metadata(args.metadata, args.color_by) # Fail now rather than after hours of work
83
+ except (ValueError, OSError) as e:
84
+ parser.error(str(e))
85
+
86
+
87
+ def tree_kwargs(args):
88
+ return dict(linkage=args.linkage, nj=args.nj, me=args.me, pcoa=args.pcoa,
89
+ metadata=args.metadata, color_by=args.color_by)
90
+
91
+
92
+ def run_safely(func):
93
+ try:
94
+ func()
95
+ except USER_ERRORS as e:
96
+ log.error('%s', e)
97
+ log.debug('Traceback:', exc_info=True) # Shown with --verbose
98
+ sys.exit(1)
99
+ except KeyboardInterrupt:
100
+ log.error('Interrupted')
101
+ sys.exit(130)
102
+
103
+
104
+ def main(argv=None):
105
+ """genome-comparator: compare genomes from a folder of fasta/fastq files."""
106
+ max_cpu = available_cpus()
107
+ parser = ArgumentParser(prog='genome-comparator', formatter_class=ArgumentDefaultsHelpFormatter,
108
+ description='Compare genomes (assemblies or reads) with Mash and build '
109
+ 'a distance matrix, trees and an optional PCoA plot.')
110
+ parser.add_argument('-i', '--input', metavar='/input/folder', required=True,
111
+ help='Folder containing the fasta or fastq files (searched recursively)')
112
+ parser.add_argument('-o', '--output', metavar='/output/folder', required=True,
113
+ help='Folder to hold the result files')
114
+ parser.add_argument('-t', '--threads', metavar='N', type=int_range(1), default=max_cpu,
115
+ help='Number of threads')
116
+ parser.add_argument('-k', '--kmer-size', '--kmer_size', dest='kmer_size', type=int_range(1, MAX_KMER_SIZE),
117
+ default=21, help='k-mer size used by Mash')
118
+ parser.add_argument('-s', '--sketch-size', '--sketch_size', dest='sketch_size', type=int_range(1),
119
+ default=10000, help='Number of min-hashes per sketch')
120
+ parser.add_argument('-m', '--min-copies', type=int_range(1), default=2,
121
+ help='Reads only: minimum copies of a k-mer to be included in the sketch '
122
+ '(filters out sequencing errors)')
123
+ add_tree_arguments(parser)
124
+ parser.add_argument('-b', '--bootstrap', metavar='N', type=int_range(0), default=0,
125
+ help='Number of bootstrap replicates for tree support values. Each replicate sketches all '
126
+ 'the samples again with a different hash seed, so N replicates take about N times '
127
+ 'longer than a normal run.')
128
+ parser.add_argument('--phylip', action='store_true',
129
+ help='Also save the distance matrix in phylip format (for rapidnj, fastme, etc.)')
130
+ parser.add_argument('--force', action='store_true',
131
+ help='Sketch all samples again, even if up-to-date sketches exist in the output folder')
132
+ parser.add_argument('--clean', action='store_true',
133
+ help='Remove the individual sketch files at the end')
134
+ parser.add_argument('-v', '--verbose', action='store_true', help='Show debug messages')
135
+ parser.add_argument('--version', action='version', version='%(prog)s ' + __version__)
136
+ args = parser.parse_args(argv)
137
+ check_tree_arguments(parser, args)
138
+
139
+ if args.threads > max_cpu:
140
+ args.threads = max_cpu
141
+
142
+ output = Path(args.output).expanduser()
143
+ output.mkdir(parents=True, exist_ok=True)
144
+ setup_logging(args.verbose, output / 'genome_comparator.log')
145
+ log.info('genome_comparator %s: %s', __version__, ' '.join(sys.argv))
146
+
147
+ run_safely(GenomeComparator(args.input, args.output, threads=args.threads, kmer_size=args.kmer_size,
148
+ sketch_size=args.sketch_size, min_copies=args.min_copies, phylip=args.phylip,
149
+ force=args.force, clean=args.clean, bootstrap=args.bootstrap,
150
+ **tree_kwargs(args)).run)
151
+
152
+
153
+ def mash_phylo_main(argv=None):
154
+ """Deprecated name of the main command, kept so existing scripts keep working."""
155
+ sys.stderr.write('Warning: "mash-phylo" is deprecated and will be removed in a future version. '
156
+ 'Use "genome-comparator" instead (same options).\n')
157
+ main(argv)
158
+
159
+
160
+ def dendrogram_main(argv=None):
161
+ """dendrogram-from-matrix: build trees / PCoA from an existing square distance matrix."""
162
+ parser = ArgumentParser(prog='dendrogram-from-matrix', formatter_class=ArgumentDefaultsHelpFormatter,
163
+ description='Build trees and an optional PCoA plot from a square distance matrix. '
164
+ 'First row and first column hold the sample names.')
165
+ parser.add_argument('-i', '--input', metavar='my_square_matrix.tsv', required=True,
166
+ help='Square distance matrix (.tsv, .csv, .xlsx or .xls)')
167
+ parser.add_argument('-o', '--output', metavar='/output/folder', required=True,
168
+ help='Folder to hold the result files')
169
+ add_tree_arguments(parser)
170
+ parser.add_argument('-v', '--verbose', action='store_true', help='Show debug messages')
171
+ parser.add_argument('--version', action='version', version='%(prog)s ' + __version__)
172
+ args = parser.parse_args(argv)
173
+ check_tree_arguments(parser, args)
174
+ setup_logging(args.verbose)
175
+
176
+ def run():
177
+ with step('Reading distance matrix'):
178
+ df = matrix.read_matrix(args.input)
179
+ log.info('%d x %d matrix', *df.shape)
180
+ if len(df) < MIN_SAMPLES:
181
+ raise MatrixError('At least {} samples are required to build a tree'.format(MIN_SAMPLES))
182
+ name = Path(args.input).stem
183
+ for out in analyze_matrix(df, Path(args.output).expanduser(), name, **tree_kwargs(args)):
184
+ log.info('Output: %s', out)
185
+
186
+ run_safely(run)
187
+
188
+
189
+ def collapse_main(argv=None):
190
+ """tree-collapser: collapse clades whose tips are closer than a distance threshold."""
191
+ parser = ArgumentParser(prog='tree-collapser',
192
+ description='Collapse clades whose average distance to their tips is smaller '
193
+ 'than a threshold. Collapsed clades are replaced by a single tip '
194
+ 'named "<first tip> {<other tips>}".')
195
+ parser.add_argument('-i', '--input', metavar='tree.nwk', required=True, help='Newick input tree')
196
+ parser.add_argument('-o', '--output', metavar='tree_collapsed.nwk', required=True, help='Newick output tree')
197
+ parser.add_argument('-d', '--distance', metavar='0.01', type=float, required=True,
198
+ help='Distance threshold. Clades with an average distance to their tips smaller than '
199
+ 'this value are collapsed.')
200
+ parser.add_argument('--version', action='version', version='%(prog)s ' + __version__)
201
+ args = parser.parse_args(argv)
202
+ setup_logging()
203
+
204
+ def run():
205
+ tree = trees.read_newick(args.input)
206
+ n = collapse(tree, args.distance)
207
+ trees.write_newick(tree, args.output)
208
+ log.info('Collapsed %d clade(s)', n)
209
+
210
+ run_safely(run)
211
+
212
+
213
+ def rename_main(argv=None):
214
+ """tree-renamer: rename tree tips from a two-column table."""
215
+ parser = ArgumentParser(prog='tree-renamer', description='Rename the tips of a Newick tree.')
216
+ parser.add_argument('-i', '--input', metavar='input_tree.nwk', required=True, help='Input tree in Newick format')
217
+ parser.add_argument('-o', '--output', metavar='renamed_tree.nwk', required=True,
218
+ help='Renamed tree in Newick format')
219
+ parser.add_argument('-r', '--rename-table', metavar='rename_table.tsv', required=True,
220
+ help='Tab-separated file with two columns: current tip name, new name')
221
+ parser.add_argument('--version', action='version', version='%(prog)s ' + __version__)
222
+ args = parser.parse_args(argv)
223
+ setup_logging()
224
+
225
+ def run():
226
+ tree = trees.read_newick(args.input)
227
+ not_found, duplicates = rename_tips(tree, read_rename_table(args.rename_table))
228
+ trees.write_newick(tree, args.output)
229
+ if not_found:
230
+ log.warning('%d name(s) from the rename table were not found in the tree: %s',
231
+ len(not_found), ', '.join(sorted(not_found)[:10]))
232
+ if duplicates:
233
+ log.warning('%d name(s) are now shared by several tips: %s',
234
+ len(duplicates), ', '.join(sorted(duplicates)[:10]))
235
+
236
+ run_safely(run)
237
+
238
+
239
+ if __name__ == '__main__':
240
+ main()