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.
- genome_comparator-0.4.6/LICENSE +21 -0
- genome_comparator-0.4.6/PKG-INFO +140 -0
- genome_comparator-0.4.6/README.md +102 -0
- genome_comparator-0.4.6/genome_comparator/__init__.py +3 -0
- genome_comparator-0.4.6/genome_comparator/__main__.py +6 -0
- genome_comparator-0.4.6/genome_comparator/bootstrap.py +78 -0
- genome_comparator-0.4.6/genome_comparator/cli.py +240 -0
- genome_comparator-0.4.6/genome_comparator/mash.py +253 -0
- genome_comparator-0.4.6/genome_comparator/matrix.py +78 -0
- genome_comparator-0.4.6/genome_comparator/ordination.py +113 -0
- genome_comparator-0.4.6/genome_comparator/pipeline.py +324 -0
- genome_comparator-0.4.6/genome_comparator/samples.py +105 -0
- genome_comparator-0.4.6/genome_comparator/tree_tools.py +76 -0
- genome_comparator-0.4.6/genome_comparator/trees.py +93 -0
- genome_comparator-0.4.6/genome_comparator.egg-info/PKG-INFO +140 -0
- genome_comparator-0.4.6/genome_comparator.egg-info/SOURCES.txt +22 -0
- genome_comparator-0.4.6/genome_comparator.egg-info/dependency_links.txt +1 -0
- genome_comparator-0.4.6/genome_comparator.egg-info/entry_points.txt +6 -0
- genome_comparator-0.4.6/genome_comparator.egg-info/requires.txt +10 -0
- genome_comparator-0.4.6/genome_comparator.egg-info/top_level.txt +1 -0
- genome_comparator-0.4.6/pyproject.toml +70 -0
- genome_comparator-0.4.6/setup.cfg +4 -0
- genome_comparator-0.4.6/tests/test_pipeline.py +99 -0
- genome_comparator-0.4.6/tests/test_units.py +316 -0
|
@@ -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,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()
|