PGAP2 1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- PGAP2-1.0.dist-info/LICENSE +21 -0
- PGAP2-1.0.dist-info/METADATA +153 -0
- PGAP2-1.0.dist-info/RECORD +35 -0
- PGAP2-1.0.dist-info/WHEEL +5 -0
- PGAP2-1.0.dist-info/entry_points.txt +2 -0
- PGAP2-1.0.dist-info/top_level.txt +1 -0
- pgap2/__init__.py +2 -0
- pgap2/dependencies/maskrc-svg.py +236 -0
- pgap2/lib/basic.py +203 -0
- pgap2/lib/panclust.py +24 -0
- pgap2/lib/pangenome.py +343 -0
- pgap2/lib/pangp.py +121 -0
- pgap2/lib/phylogeny.py +596 -0
- pgap2/lib/pklcheck.py +80 -0
- pgap2/lib/species.py +160 -0
- pgap2/lib/strain.py +7 -0
- pgap2/lib/tree.py +210 -0
- pgap2/main.py +33 -0
- pgap2/postprocess/baps.py +144 -0
- pgap2/postprocess/profile.py +122 -0
- pgap2/postprocess/singletree.py +130 -0
- pgap2/postprocess/stat.py +511 -0
- pgap2/postprocess/tajimas_d.py +124 -0
- pgap2/utils/__init__.py +0 -0
- pgap2/utils/alignment.py +45 -0
- pgap2/utils/arrangement_detector.py +122 -0
- pgap2/utils/data_loader.py +684 -0
- pgap2/utils/draw.py +1095 -0
- pgap2/utils/gene_retriever.py +212 -0
- pgap2/utils/generate_tree.py +310 -0
- pgap2/utils/partition.py +851 -0
- pgap2/utils/postprocess.py +18 -0
- pgap2/utils/preprocess.py +475 -0
- pgap2/utils/supply.py +138 -0
- pgap2/utils/tools.py +615 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Bu Congfan
|
|
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,153 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: PGAP2
|
|
3
|
+
Version: 1.0
|
|
4
|
+
Summary: PGAP2: a comprehensive pan-genome analysis pipeline for prokaryotic genomes
|
|
5
|
+
Author-email: bucongfan <bucf@big.ac.cn>
|
|
6
|
+
Maintainer-email: bucongfan <bucf@big.ac.cn>
|
|
7
|
+
License: MIT License
|
|
8
|
+
Project-URL: homepage, https://github.com/bucongfan/PGAP2
|
|
9
|
+
Project-URL: documentation, https://github.com/bucongfan/PGAP2/wiki
|
|
10
|
+
Project-URL: repository, https://github.com/bucongfan/PGAP2
|
|
11
|
+
Keywords: pan-genome,prokaryotic,genomes,pipeline,cluster
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: biopython==1.82
|
|
16
|
+
Requires-Dist: networkx==3.3
|
|
17
|
+
Requires-Dist: pyfastani==0.5.1
|
|
18
|
+
Requires-Dist: edlib==1.3.9
|
|
19
|
+
Requires-Dist: loguru==0.6.0
|
|
20
|
+
Requires-Dist: numpy==1.23.3
|
|
21
|
+
Requires-Dist: pandas==1.5.0
|
|
22
|
+
Requires-Dist: pyecharts==2.0.1
|
|
23
|
+
Requires-Dist: scikit_learn==1.1.2
|
|
24
|
+
Requires-Dist: scipy==1.9.1
|
|
25
|
+
Requires-Dist: tqdm==4.64.1
|
|
26
|
+
Requires-Dist: miniFasta
|
|
27
|
+
Requires-Dist: tajimas_d
|
|
28
|
+
|
|
29
|
+
# PGAP2
|
|
30
|
+
|
|
31
|
+
Pan-Genome Analysis Pipeline 2
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
### Basic usage
|
|
36
|
+
The input directory contains all the genome and annotation files.
|
|
37
|
+
|
|
38
|
+
PGAP2 supports multiple input formats: GFF files in the same format as those output by Prokka, GFF files with their corresponding genome FASTA files in separate files, GenBank flat files (GBFF), or just genome FASTA files (with `--reannot` required).
|
|
39
|
+
|
|
40
|
+
Different formats of input files can be mixed in one input directory. PGAP2 will recognize and process them based on their prefixes and suffixes.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pgap2 main -i inputdir/ -o outputdir/
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Preprocessing
|
|
47
|
+
Quality checks and visualization are conducted by PGAP2 during the preprocessing step. PGAP2 generates an interactive HTML file and corresponding vector figures to help users understand their input data. The input data and pre-alignment results are stored as a pickle file for quick restarting of the same calculation step.
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pgap2 prep -i inputdir/ -o outputdir/
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Postprocessing
|
|
54
|
+
The postprocessing pipeline is performed by PGAP2. There are various submodules integrated into the postprocessing module, such as statistical analysis, single-copy tree building, population clustering, and Tajima's D test. Regardless of which submodule you want to use, you can always run it as follows:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pgap2 post [submodule] [options] -i inputdir/ -o outputdir/
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The inputdir is the outputdir of main module.
|
|
61
|
+
|
|
62
|
+
PGAP2 also support statistical analysis using a PAV file indepandently:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pgap2 post profile --pav your_pav_file -o outputdir/
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
The best way to install full version of PGAP2 package is using conda:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
conda create -n pgap2 -c conda-forge -c bioconda -c defaults pgap2
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
alternatively it is often faster to use the [mamba](https://github.com/mamba-org/mamba) solver
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
conda create -n pgap2 -c conda-forge mamba
|
|
79
|
+
conda activate pgap2
|
|
80
|
+
mamba install -c conda-forge -c bioconda -c defaults pgap2
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Or sometimes you only want to carry out a specific function, such as partioning and don't want install too many extra softwares for fully version of PGAP2, then you can just install PGAP2:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install pgap2
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Or via source file:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
git clone https://github.com/bucongfan/PGAP2
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
And then install extra software that only necessary for a specific function by yourself.
|
|
96
|
+
|
|
97
|
+
Dependencies of PGAP2 are list below, and PGAP2 will check them whether in environment path or in pgap2/dependencies folder.
|
|
98
|
+
|
|
99
|
+
### Preprocessing
|
|
100
|
+
+ One of clustering software
|
|
101
|
+
- [cd-hit](https://github.com/weizhongli/cdhit)
|
|
102
|
+
- [MMseqs2](https://github.com/soedinglab/MMseqs2)
|
|
103
|
+
+ One of alignment software
|
|
104
|
+
- [diamond](https://github.com/bbuchfink/diamond)
|
|
105
|
+
- [blast+ ](https://ftp.ncbi.nlm.nih.gov/blast/executables/blast+/LATEST/)
|
|
106
|
+
|
|
107
|
+
### Main
|
|
108
|
+
+ One of clustering software
|
|
109
|
+
- [cd-hit](https://github.com/weizhongli/cdhit)
|
|
110
|
+
- [MMseqs2](https://github.com/soedinglab/MMseqs2)
|
|
111
|
+
+ [mcl](https://github.com/micans/mcl)
|
|
112
|
+
+ One of alignment software
|
|
113
|
+
- [diamond](https://github.com/bbuchfink/diamond)
|
|
114
|
+
- [blast+ ](https://ftp.ncbi.nlm.nih.gov/blast/executables/blast+/LATEST/)
|
|
115
|
+
+ Using `--retrieve` to retrieve missing gene loci
|
|
116
|
+
- [miniprot](https://github.com/lh3/miniprot)
|
|
117
|
+
- [seqtk](https://github.com/lh3/seqtk)
|
|
118
|
+
+ Using `--reannot` to re-annotate your genome
|
|
119
|
+
- [prodigal](https://github.com/hyattpd/Prodigal)
|
|
120
|
+
|
|
121
|
+
### Postprocessing
|
|
122
|
+
+ One of MSA software
|
|
123
|
+
- [muscle](https://github.com/rcedgar/muscle)
|
|
124
|
+
- [mafft](https://github.com/GSLBiotech/mafft)
|
|
125
|
+
- [tcoffee](https://github.com/cbcrg/tcoffee)
|
|
126
|
+
+ [ClipKIT](https://github.com/JLSteenwyk/ClipKIT)
|
|
127
|
+
+ One of phylogenetic tree construction software
|
|
128
|
+
- [IQ-TREE](http://www.iqtree.org/)
|
|
129
|
+
- [FastTree](https://morgannprice.github.io/fasttree/)
|
|
130
|
+
- [RAxML-ng](https://github.com/amkozlov/raxml-ng)
|
|
131
|
+
+ [ClonalFrameML](https://github.com/xavierdidelot/ClonalFrameML)
|
|
132
|
+
+ [maskrc-svg](https://github.com/kwongj/maskrc-svg)
|
|
133
|
+
+ [fastbaps](https://github.com/gtonkinhill/fastbaps)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
### Visulization in Preprocessing and Postprocessing modules
|
|
138
|
+
PGAP2 will call Rscript in your environment virable. The library should have:
|
|
139
|
+
|
|
140
|
+
+ ggpubr
|
|
141
|
+
+ ggrepel
|
|
142
|
+
+ dplyr
|
|
143
|
+
+ tidyr
|
|
144
|
+
+ patchwork
|
|
145
|
+
+ optparse
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
## Detailed documentation
|
|
150
|
+
Please refer documentation from [wiki](https://github.com/bucongfan/PGAP2/wiki).
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
pgap2/__init__.py,sha256=KGDiYGQ24L9jFOuALBe7Hb4oSp0j4bVnQFVrqqec4_Y,46
|
|
2
|
+
pgap2/main.py,sha256=EHUEoEOfZzpiMC473og4OMDaagDwOlcHeCP-i_kdXsg,962
|
|
3
|
+
pgap2/dependencies/maskrc-svg.py,sha256=GfHGHdpPAvVHCouPUPc_n7ifL_VWMr1VMKmVBZXuO9E,8270
|
|
4
|
+
pgap2/lib/basic.py,sha256=O2EXGiYaiaC_sVywfnfHj0qVHz2mBDQhkiL_wLwOysM,8617
|
|
5
|
+
pgap2/lib/panclust.py,sha256=oeqvoss-zQ3gvo_RiM6MVktbTzeC66Zcd9nMO4tt04w,660
|
|
6
|
+
pgap2/lib/pangenome.py,sha256=wvJHUQQTqaheStSjLwBT3MXd4yoQr7_9mofnmJdNYR8,14128
|
|
7
|
+
pgap2/lib/pangp.py,sha256=KXZW9c-ml1Kdw0LBbBAJNgWYcBCmLo5r1Ol-kMSg1sY,5312
|
|
8
|
+
pgap2/lib/phylogeny.py,sha256=8tWhDdiKHhUCNvl93bi54_3vdECnCJ7mDO_6xnJDxA4,28559
|
|
9
|
+
pgap2/lib/pklcheck.py,sha256=jwCujfdHHyGbBuN1529MbonyfZXDgCtjzEnjr1M51MY,3340
|
|
10
|
+
pgap2/lib/species.py,sha256=QntgxrLDV6fyX84ELCaAXMxuzYA3ImLX7Op5qqDJQZk,6253
|
|
11
|
+
pgap2/lib/strain.py,sha256=ecAooYQ7xfAkACmhQRqJrl8tzFzEOei5RyqPikyCqtA,253
|
|
12
|
+
pgap2/lib/tree.py,sha256=wal0duNuSFeng3L_Vu0udDFa9oEdDpgiIuwaYSioXNg,7990
|
|
13
|
+
pgap2/postprocess/baps.py,sha256=x1hMTfDv7ERo1-lOUcwCegmT9zHAo7y1NQJ75S62P5M,8182
|
|
14
|
+
pgap2/postprocess/profile.py,sha256=KUZLYfxEvEwvTl7c7OtDhMajKeGrei50OoulFs9EEK8,5736
|
|
15
|
+
pgap2/postprocess/singletree.py,sha256=sJMGBzYbXtuIJMF6N-wg2jYlDr8gwAjEoIGCjp0BnGc,7152
|
|
16
|
+
pgap2/postprocess/stat.py,sha256=R4vrT7kTyoFCTazzmmJxpvROZRKXRCEIhA800xs5sEc,21375
|
|
17
|
+
pgap2/postprocess/tajimas_d.py,sha256=h_XE2PgEPYrVMuazidmduP5x2Z9TCVXBPijeUACu9ps,6485
|
|
18
|
+
pgap2/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
+
pgap2/utils/alignment.py,sha256=VYueZheJhXlww8c3XrmF9tgPUxJ__CZhbBqsgdXFD0c,1855
|
|
20
|
+
pgap2/utils/arrangement_detector.py,sha256=Gna4pMbKfua_LYA4wx8a5OG1FlqGB7pUcFFyhtPe8rk,5130
|
|
21
|
+
pgap2/utils/data_loader.py,sha256=EWNhYV6jAEb4tnbmPvu_gKrsBLqKiMTsgDg-hMZQuQY,29817
|
|
22
|
+
pgap2/utils/draw.py,sha256=vSbj6L9wh1dmqzmAIuwmz46ZoiyZRFYGQc8SeXa7oXU,50729
|
|
23
|
+
pgap2/utils/gene_retriever.py,sha256=PsuhHdrbhRIbkayVqp6h02z1JwmmYSIkJD9sTweRr7I,9628
|
|
24
|
+
pgap2/utils/generate_tree.py,sha256=7487dWjrEWKKdO9HXdAMe8ELOPkEtHY4vnO6CkLQS_Y,12083
|
|
25
|
+
pgap2/utils/partition.py,sha256=heGNtgstzFTLHIhr2eq_2fT9xLxzr6irUqGLVCiKRlQ,38102
|
|
26
|
+
pgap2/utils/postprocess.py,sha256=VqiRiNckc9IpnKr9eBA2IgzGj2SWn5vliahIO-ixSXc,776
|
|
27
|
+
pgap2/utils/preprocess.py,sha256=Vh2GL21V5_RiGjyhoRaM2a6LIvGi-IJovovcb2hCspk,23549
|
|
28
|
+
pgap2/utils/supply.py,sha256=83Vo80Tj0kM4Re1eTfX0IyiU4WElf0qvfW74xJlm2vY,4345
|
|
29
|
+
pgap2/utils/tools.py,sha256=80c7qH1aBIkNESKvpsMlY6n_7x6SYCIA9PH0gkUYuIM,23077
|
|
30
|
+
PGAP2-1.0.dist-info/LICENSE,sha256=-LEOI81wEfbzLxih-Qcm5Twrd-HuWvnnMa7qkSko74o,1067
|
|
31
|
+
PGAP2-1.0.dist-info/METADATA,sha256=W7_DkQgvVtdKXglzhu9yx6392JtFm3LayH6ULHdRKgo,5276
|
|
32
|
+
PGAP2-1.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
33
|
+
PGAP2-1.0.dist-info/entry_points.txt,sha256=0RpheP8MSd7czJOfbtxwZ0ACQQtdWhQadgsB5NWCPyI,42
|
|
34
|
+
PGAP2-1.0.dist-info/top_level.txt,sha256=p4Z3YBbJIYqbR0Qje7P4JrJkOwV463ifKqVj7Zcrr1k,6
|
|
35
|
+
PGAP2-1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pgap2
|
pgap2/__init__.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Script by Jason Kwong & Torsten Seemann
|
|
3
|
+
# Script to mask recombination from CFML output and draw SVG of recombinant regions
|
|
4
|
+
|
|
5
|
+
# Usage
|
|
6
|
+
import argparse
|
|
7
|
+
from argparse import RawTextHelpFormatter
|
|
8
|
+
|
|
9
|
+
parser = argparse.ArgumentParser(
|
|
10
|
+
formatter_class=RawTextHelpFormatter,
|
|
11
|
+
description='Mask recombination from ClonalFrameML/Gubbins output and draw SVG of recombinant regions',
|
|
12
|
+
usage='\n %(prog)s --aln FASTA --out OUTFILE [--gubbins] <PREFIX>')
|
|
13
|
+
parser.add_argument('prefix', metavar='PREFIX', help='prefix used for CFML/Gubbins input files (required)')
|
|
14
|
+
parser.add_argument('--gubbins', action='store_true', help='parse as Gubbins instead of ClonalFrameML')
|
|
15
|
+
parser.add_argument('--aln', metavar='FASTA', required=True, help='multiFASTA alignment used as input for CFML (required)')
|
|
16
|
+
parser.add_argument('--out', metavar='OUTFILE', default='maskrc.aln', help='output file for masked alignment (default="maskrc.aln")')
|
|
17
|
+
parser.add_argument('--symbol', metavar='CHAR', default='?', help='symbol to use for masking (default="?")')
|
|
18
|
+
parser.add_argument('--regions', metavar='FILE', help='output recombinant regions to file')
|
|
19
|
+
parser.add_argument('--svg', metavar='FILE', help='draw SVG output of recombinant regions and save as specified file')
|
|
20
|
+
parser.add_argument('--svgsize', metavar='WIDExHIGH', default='800x600', help='specify width and height of SVG in pixels (default="800x600")')
|
|
21
|
+
parser.add_argument('--svgorder', metavar='FILE', help='specify file containing list of taxa (1 per line) in desired order')
|
|
22
|
+
parser.add_argument('--svgcolour', metavar='COLOUR', default='black', help='specify colour of recombination regions in HEX format (default=black)')
|
|
23
|
+
parser.add_argument('--consensus', action='store_true', help='add consensus row of recombination hotspots')
|
|
24
|
+
parser.add_argument('--version', action='version', version='%(prog)s 0.5')
|
|
25
|
+
|
|
26
|
+
args = parser.parse_args()
|
|
27
|
+
|
|
28
|
+
prefix = str(args.prefix)
|
|
29
|
+
treefn = prefix + ('.labelled_tree.newick', '.final_tree.tre')[args.gubbins]
|
|
30
|
+
coordfn = prefix + ('.importation_status.txt', '.recombination_predictions.gff')[args.gubbins]
|
|
31
|
+
|
|
32
|
+
import os
|
|
33
|
+
import sys
|
|
34
|
+
import csv
|
|
35
|
+
from collections import defaultdict
|
|
36
|
+
from ete3 import Tree
|
|
37
|
+
from Bio import SeqIO
|
|
38
|
+
from Bio.Seq import Seq
|
|
39
|
+
from Bio.Seq import MutableSeq
|
|
40
|
+
from Bio.SeqRecord import SeqRecord
|
|
41
|
+
import svgwrite
|
|
42
|
+
from BCBio import GFF
|
|
43
|
+
import pprint
|
|
44
|
+
|
|
45
|
+
# Functions
|
|
46
|
+
def msg(*args, **kwargs):
|
|
47
|
+
print(*args, file=sys.stderr, **kwargs)
|
|
48
|
+
|
|
49
|
+
def err(*args, **kwargs):
|
|
50
|
+
msg(*args, **kwargs)
|
|
51
|
+
sys.exit(1);
|
|
52
|
+
|
|
53
|
+
def check_file(f):
|
|
54
|
+
msg('Checking file: {}'.format(f))
|
|
55
|
+
if os.path.isfile(f) == False:
|
|
56
|
+
err('ERROR: Cannot find "{}". Check output files exist in this directory.'.format(f))
|
|
57
|
+
|
|
58
|
+
# Check input files (output from CFML)
|
|
59
|
+
check_file(treefn)
|
|
60
|
+
check_file(coordfn)
|
|
61
|
+
|
|
62
|
+
# Setup lists and dictionary
|
|
63
|
+
d = defaultdict(list)
|
|
64
|
+
seqLIST = []
|
|
65
|
+
leafLIST = []
|
|
66
|
+
t = Tree(treefn, format=1)
|
|
67
|
+
|
|
68
|
+
# Set taxa order for SVG
|
|
69
|
+
if args.svgorder:
|
|
70
|
+
msg('Setting taxa order using --svgorder {}'.format(args.svgorder));
|
|
71
|
+
with open(args.svgorder) as file:
|
|
72
|
+
for row in file:
|
|
73
|
+
leafLIST.append(row.rstrip())
|
|
74
|
+
else:
|
|
75
|
+
msg('Setting taxa order using leaves from: {}'.format(treefn));
|
|
76
|
+
for leaf in t:
|
|
77
|
+
leafLIST.append(leaf.name)
|
|
78
|
+
|
|
79
|
+
# gubbins GFF
|
|
80
|
+
# node="internal_13->2016-04538";neg_log_likelihood="3764.359443";taxa="2016-04538";snp_count="108";
|
|
81
|
+
# node="internal_ROOT->internal_10";neg_log_likelihood="47939.649932";taxa=" 2016-23066 2015-19014";snp_count="8";
|
|
82
|
+
# node="internal_15->internal_16";neg_log_likelihood="574.375212";taxa=" 2008-16004 2010-22387";snp_count="39";
|
|
83
|
+
# node="internal_15->internal_16";neg_log_likelihood="418.840895";taxa=" 2008-16004 2010-22387";snp_count="38";
|
|
84
|
+
# node="internal_12->internal_14";neg_log_likelihood="17258.115166";taxa=" 2012-21203 2008-20031 2008-16004 2010-22387";
|
|
85
|
+
|
|
86
|
+
# clonalframeml "BED"
|
|
87
|
+
#Node Beg End
|
|
88
|
+
#2016-11776 5624 6619
|
|
89
|
+
#2016-11776 10778 11326
|
|
90
|
+
#Reference 2489729 2490893
|
|
91
|
+
#NODE_10 1165936 1166465
|
|
92
|
+
|
|
93
|
+
# internal
|
|
94
|
+
# ['Reference', ['2434777', '2434933', 'ancestral']],
|
|
95
|
+
# ['2016-17702', ['2483546', '2485711', 'ancestral']],
|
|
96
|
+
# ['2016-17705', ['2483546', '2485711', 'ancestral']],
|
|
97
|
+
# ['2016-11776', ['2483546', '2485711', 'ancestral']],
|
|
98
|
+
# ['2016-11778', ['2483546', '2485711', 'ancestral']],
|
|
99
|
+
# ['2016-11777', ['2483546', '2485711', 'ancestral']],
|
|
100
|
+
# ['2016-17701', ['2483546', '2485711', 'ancestral']],
|
|
101
|
+
# ['Reference', ['2483546', '2485711', 'ancestral']]]
|
|
102
|
+
|
|
103
|
+
if args.gubbins:
|
|
104
|
+
msg('Parsing Gubbins file: {}'.format(coordfn))
|
|
105
|
+
with open(coordfn) as gff:
|
|
106
|
+
for rec in GFF.parse(gff):
|
|
107
|
+
for f in rec.features:
|
|
108
|
+
taxa = f.qualifiers['taxa'][0].strip().split()
|
|
109
|
+
state = ('ancestral', 'extant')[ len(taxa)==1 ]
|
|
110
|
+
for taxon in taxa:
|
|
111
|
+
# BCBio-GFF module parser seems to extract coordinate-1 for f.location.start
|
|
112
|
+
seqLIST.append( [ taxon, [ int(f.location.start)+1, int(f.location.end), state ] ] )
|
|
113
|
+
else:
|
|
114
|
+
msg('Parsing ClonalFrameML file: {}'.format(coordfn))
|
|
115
|
+
with open(coordfn) as csvfile:
|
|
116
|
+
RCseqs = csv.reader(csvfile, delimiter='\t')
|
|
117
|
+
next(csvfile)
|
|
118
|
+
for row in RCseqs:
|
|
119
|
+
seq = row[0]
|
|
120
|
+
RCstart = row[1]
|
|
121
|
+
RCstop = row[2]
|
|
122
|
+
node = t.search_nodes(name=seq)[0]
|
|
123
|
+
if node.is_leaf():
|
|
124
|
+
leaf_names = [node.name]
|
|
125
|
+
z = 'extant'
|
|
126
|
+
else:
|
|
127
|
+
leaves = node.get_leaves()
|
|
128
|
+
leaf_names = [leaf.name for leaf in leaves]
|
|
129
|
+
z = 'ancestral'
|
|
130
|
+
for l in leaf_names:
|
|
131
|
+
seqLIST.append([l, [RCstart, RCstop, z]])
|
|
132
|
+
|
|
133
|
+
# Build dictionary of recombinant regions
|
|
134
|
+
for k,v in seqLIST:
|
|
135
|
+
d[k].append(v)
|
|
136
|
+
|
|
137
|
+
# Mask recombination in sequences
|
|
138
|
+
seqALN = []
|
|
139
|
+
for record in SeqIO.parse(args.aln, 'fasta'):
|
|
140
|
+
msg('Reading {} ... '.format(record.id))
|
|
141
|
+
seqlen = len(record.seq)
|
|
142
|
+
regions = d.get(record.id, None)
|
|
143
|
+
newrec = MutableSeq(str(record.seq))
|
|
144
|
+
if regions:
|
|
145
|
+
for a in regions:
|
|
146
|
+
start = int(a[0]) - 1
|
|
147
|
+
end = int(a[1])
|
|
148
|
+
lenMASK = end - start
|
|
149
|
+
newrec[start:end] = (args.symbol)*lenMASK
|
|
150
|
+
seqALN.append(SeqRecord(Seq(str(newrec)), record.id, description=''))
|
|
151
|
+
|
|
152
|
+
# Write masked alignment to file
|
|
153
|
+
msg('Writing masked alignment to {} ... '.format(args.out))
|
|
154
|
+
SeqIO.write(seqALN, args.out, 'fasta')
|
|
155
|
+
|
|
156
|
+
# Write recombinant regions to file
|
|
157
|
+
if args.regions:
|
|
158
|
+
with open(args.regions, 'w') as csvfile:
|
|
159
|
+
csvwriter = csv.writer(csvfile, delimiter='\t')
|
|
160
|
+
for k,v in list(d.items()):
|
|
161
|
+
for b in v:
|
|
162
|
+
x = b[0]
|
|
163
|
+
y = b[1]
|
|
164
|
+
z = b[2]
|
|
165
|
+
csvwriter.writerow([k,x,y,z])
|
|
166
|
+
|
|
167
|
+
# Draw SVG
|
|
168
|
+
svgsize = args.svgsize.split('x',1)
|
|
169
|
+
width = int(svgsize[0])
|
|
170
|
+
height = int(svgsize[1])
|
|
171
|
+
numseqs = len(leafLIST)
|
|
172
|
+
h = height/float(numseqs)
|
|
173
|
+
s = width/float(int(seqlen))
|
|
174
|
+
fsize = int(h*0.9)
|
|
175
|
+
font = 'font-size:{}px; font-family:Arial'.format(fsize)
|
|
176
|
+
colour = args.svgcolour
|
|
177
|
+
main_colour = 'black'
|
|
178
|
+
ancestral_colour = '#A9A9A9'
|
|
179
|
+
interval = 500000 # Tick interval in bp
|
|
180
|
+
|
|
181
|
+
dwg = svgwrite.Drawing(args.svg)
|
|
182
|
+
|
|
183
|
+
def rect(x,p,w,colour): # Draw regions
|
|
184
|
+
dwg.add(dwg.rect(insert=((x*s)+5, (p*h)+5), size=(w, h*0.8), fill=colour))
|
|
185
|
+
if args.consensus:
|
|
186
|
+
dwg.add(dwg.rect(insert=((x*s)+5, 5), size=(w, h*0.8), fill=main_colour))
|
|
187
|
+
|
|
188
|
+
def label(n,p): # Sequence ID labels
|
|
189
|
+
dwg.add(dwg.text(n, insert=((seqlen*s)+15, (((p+1)*h)-(0.2*h))+5), fill=main_colour, style=font))
|
|
190
|
+
|
|
191
|
+
def ticks(q): # Draw tick marks
|
|
192
|
+
count = interval
|
|
193
|
+
while count < seqlen:
|
|
194
|
+
dwg.add(dwg.line((count*s,((q)*h)), (count*s,((q)*h)-3), stroke=main_colour))
|
|
195
|
+
dwg.add(dwg.text((count/1000000.0), insert=((count*s)-6, ((q)*h)+13), fill=main_colour, style=font))
|
|
196
|
+
count = count + interval
|
|
197
|
+
|
|
198
|
+
def box(width,h): # Box with 5px margin
|
|
199
|
+
dwg.add(dwg.line((0,0), ((width+10),0), stroke=main_colour))
|
|
200
|
+
dwg.add(dwg.line(((width+10),0), ((width+10),((p+1)*h)), stroke=main_colour))
|
|
201
|
+
dwg.add(dwg.line(((width+10),((p+1)*h)), (0,((p+1)*h)), stroke=main_colour))
|
|
202
|
+
dwg.add(dwg.line((0,((p+1)*h)), (0,0), stroke=main_colour))
|
|
203
|
+
|
|
204
|
+
# Draw recombinant regions and labels
|
|
205
|
+
if args.svg:
|
|
206
|
+
msg('Drawing SVG to {} ...'.format(args.svg))
|
|
207
|
+
if args.consensus:
|
|
208
|
+
p = 1
|
|
209
|
+
dwg.add(dwg.text('Consensus', insert=((seqlen*s)+15, (0.8*h)+5), fill=main_colour, style=font))
|
|
210
|
+
else:
|
|
211
|
+
p = 0
|
|
212
|
+
for seq in leafLIST:
|
|
213
|
+
v = d.get(seq, None)
|
|
214
|
+
if v:
|
|
215
|
+
for b in v:
|
|
216
|
+
x = int(b[0])
|
|
217
|
+
y = int(b[1])
|
|
218
|
+
z = b[2]
|
|
219
|
+
w = (y-x)*s
|
|
220
|
+
if w < 1:
|
|
221
|
+
w = 1
|
|
222
|
+
if z == 'extant':
|
|
223
|
+
rect(x,p,w,colour) # regions
|
|
224
|
+
elif z == 'ancestral':
|
|
225
|
+
rect(x,p,w,ancestral_colour)
|
|
226
|
+
label(seq,p) # labels
|
|
227
|
+
p = p+1
|
|
228
|
+
# Draw bounding box
|
|
229
|
+
box(width,h)
|
|
230
|
+
# Add tick marks
|
|
231
|
+
ticks(p+1)
|
|
232
|
+
dwg.save()
|
|
233
|
+
|
|
234
|
+
# Exit
|
|
235
|
+
msg('Done.')
|
|
236
|
+
sys.exit(0)
|
pgap2/lib/basic.py
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from loguru import logger
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
|
|
6
|
+
from Bio.Seq import Seq
|
|
7
|
+
from Bio.SeqRecord import SeqRecord
|
|
8
|
+
|
|
9
|
+
from pgap2.lib.pangenome import Pangenome
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Basic():
|
|
13
|
+
def __init__(self, pg: Pangenome) -> None:
|
|
14
|
+
self.outdir = pg.outdir
|
|
15
|
+
self.strain_num = pg.strain_num
|
|
16
|
+
self.strain_dict = pg.strain_dict
|
|
17
|
+
|
|
18
|
+
self.gcode = pg.gcode
|
|
19
|
+
self.orth_id: float = pg.orth_id
|
|
20
|
+
self.para_id: float = pg.para_id
|
|
21
|
+
self.dup_id: float = pg.dup_id
|
|
22
|
+
if os.path.exists(f'{pg.outdir}/pgap2.partition.gene_content.csv'):
|
|
23
|
+
self.pan_array = f'{pg.outdir}/pgap2.partition.gene_content.csv'
|
|
24
|
+
else:
|
|
25
|
+
raise ValueError(logger.error(
|
|
26
|
+
f'BUG: {pg.outdir}/pgap2.partition.gene_content.csv not found. Tell me in github'))
|
|
27
|
+
if os.path.exists(f'{pg.outdir}/pgap2.partition.gene_content.pav'):
|
|
28
|
+
self.pav_array = f'{pg.outdir}/pgap2.partition.gene_content.pav'
|
|
29
|
+
else:
|
|
30
|
+
raise ValueError(logger.error(
|
|
31
|
+
f'BUG: {pg.outdir}/pgap2.partition.gene_content.pav not found. Tell me in github'))
|
|
32
|
+
self.pan_attr = pg.pan_attr
|
|
33
|
+
self.forbidden_chars = set(" ;:,()'")
|
|
34
|
+
|
|
35
|
+
def dumper(self):
|
|
36
|
+
attr = {
|
|
37
|
+
'Project location': self.outdir,
|
|
38
|
+
'Genome count': self.strain_num,
|
|
39
|
+
'Genetic code': self.gcode,
|
|
40
|
+
'Ortholog threshold': self.orth_id,
|
|
41
|
+
'Paralog threshold': self.para_id,
|
|
42
|
+
'Duplication threshold': self.dup_id,
|
|
43
|
+
'Profile file': self.pan_array,
|
|
44
|
+
'PAV file': self.pav_array,
|
|
45
|
+
'Cluster number': len(self.pan_attr),
|
|
46
|
+
}
|
|
47
|
+
max_key_length = max(len(key) for key in attr.keys())
|
|
48
|
+
|
|
49
|
+
for key, value in attr.items():
|
|
50
|
+
yield f'{key:{max_key_length}}: {value}'
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def get_gene_from_comma(gene_str):
|
|
54
|
+
gene_list = []
|
|
55
|
+
for each_genes in gene_str.split(','):
|
|
56
|
+
if ';' in each_genes:
|
|
57
|
+
gene_list.extend(each_genes.split(';'))
|
|
58
|
+
else:
|
|
59
|
+
gene_list.append(each_genes)
|
|
60
|
+
return gene_list
|
|
61
|
+
|
|
62
|
+
def load_used_clusters(self, clusts, file):
|
|
63
|
+
self.customized_cluster = set()
|
|
64
|
+
if clusts and os.path.exists(clusts):
|
|
65
|
+
with open(clusts) as fh:
|
|
66
|
+
for line in fh:
|
|
67
|
+
if line.startswith('#'):
|
|
68
|
+
continue
|
|
69
|
+
clust_name = line.strip()
|
|
70
|
+
self.customized_cluster.add(clust_name)
|
|
71
|
+
logger.info(
|
|
72
|
+
f'Total {len(self.customized_cluster)} customized clusters were loaded.')
|
|
73
|
+
else:
|
|
74
|
+
with open(file, 'r') as fh:
|
|
75
|
+
for line in fh:
|
|
76
|
+
if line.startswith('#'):
|
|
77
|
+
continue
|
|
78
|
+
else:
|
|
79
|
+
clust_name = line.strip().split('\t')[0]
|
|
80
|
+
self.customized_cluster.add(clust_name)
|
|
81
|
+
|
|
82
|
+
def load_pav(self, file):
|
|
83
|
+
self.pav = pd.read_csv(file, sep='\t', index_col=0)
|
|
84
|
+
|
|
85
|
+
def phylogeny_from_detail_pav(self, file, core_thre, also_pan, para_strategy):
|
|
86
|
+
self.core_thre = core_thre
|
|
87
|
+
self.phylogeny_dict = {}
|
|
88
|
+
core_clust_num = 0
|
|
89
|
+
pan_clust_num = 0
|
|
90
|
+
with open(file, 'r') as fh:
|
|
91
|
+
detail_header = {}
|
|
92
|
+
for line in fh:
|
|
93
|
+
line = line.strip()
|
|
94
|
+
if line.startswith('#'):
|
|
95
|
+
for i, col_name in enumerate(line.split('\t')):
|
|
96
|
+
if i == 0: # remove the first '#'
|
|
97
|
+
col_name = col_name[1:]
|
|
98
|
+
if i == 13: # add the last column
|
|
99
|
+
...
|
|
100
|
+
else:
|
|
101
|
+
detail_header[col_name] = i
|
|
102
|
+
continue
|
|
103
|
+
line = line.strip().split('\t')
|
|
104
|
+
|
|
105
|
+
cluster = line[detail_header.get('Clust')]
|
|
106
|
+
para_gene = int(line[detail_header.get('para_gene')])
|
|
107
|
+
group = line[detail_header.get('group')]
|
|
108
|
+
if para_gene > 0 and para_strategy == 'drop':
|
|
109
|
+
continue
|
|
110
|
+
involved_strain = int(
|
|
111
|
+
line[detail_header.get('involved_strain')])
|
|
112
|
+
if involved_strain == 1:
|
|
113
|
+
continue
|
|
114
|
+
if cluster not in self.customized_cluster:
|
|
115
|
+
continue
|
|
116
|
+
if involved_strain/self.strain_num >= core_thre:
|
|
117
|
+
core_clust_num += 1
|
|
118
|
+
clust_gene_list = self.get_gene_from_comma(line[13])
|
|
119
|
+
self.phylogeny_dict.update(
|
|
120
|
+
{cluster: {'Type': 'Core', 'Gene': clust_gene_list, 'Group': group}})
|
|
121
|
+
elif also_pan:
|
|
122
|
+
pan_clust_num += 1
|
|
123
|
+
clust_gene_list = self.get_gene_from_comma(line[13])
|
|
124
|
+
self.phylogeny_dict.update(
|
|
125
|
+
{cluster: {'Type': 'Pan', 'Gene': clust_gene_list, 'Group': group}})
|
|
126
|
+
|
|
127
|
+
logger.info(
|
|
128
|
+
f'Read total cluster number: {core_clust_num + pan_clust_num} (single clouds will be discarded as it is meaningless for phylogenetic analysis)')
|
|
129
|
+
logger.info(
|
|
130
|
+
f'Core cluster number: {core_clust_num}, Pan cluster number: {pan_clust_num}')
|
|
131
|
+
|
|
132
|
+
def phylogeny_from_id(self, file):
|
|
133
|
+
self.used_cluster = defaultdict(list)
|
|
134
|
+
|
|
135
|
+
gene_count = 0
|
|
136
|
+
involved_genes = {}
|
|
137
|
+
for cluster, cluster_info in self.phylogeny_dict.items():
|
|
138
|
+
for gene in cluster_info['Gene']:
|
|
139
|
+
involved_genes[gene] = cluster
|
|
140
|
+
with open(file, 'r') as fh:
|
|
141
|
+
id_header = {}
|
|
142
|
+
for line in fh:
|
|
143
|
+
line = line.strip()
|
|
144
|
+
if line.startswith('#'):
|
|
145
|
+
for i, group in enumerate(line.split('\t')):
|
|
146
|
+
if i == 0:
|
|
147
|
+
group = group[1:]
|
|
148
|
+
id_header[group] = i
|
|
149
|
+
continue
|
|
150
|
+
line = line.split('\t')
|
|
151
|
+
gene_index = line[id_header.get('Gene_index')]
|
|
152
|
+
if gene_index not in involved_genes:
|
|
153
|
+
continue
|
|
154
|
+
cluster = involved_genes[gene_index]
|
|
155
|
+
cds = line[id_header.get('Nucleotide_sequence')]
|
|
156
|
+
description = '{}:{}:{}'.format(line[id_header.get(
|
|
157
|
+
'Strain')], line[id_header.get('Contig')], line[id_header.get('Gene_ID')])
|
|
158
|
+
record = SeqRecord(Seq(cds), id=gene_index,
|
|
159
|
+
description=description)
|
|
160
|
+
self.used_cluster[cluster].append(record)
|
|
161
|
+
gene_count += 1
|
|
162
|
+
logger.info(
|
|
163
|
+
f'Get total gene number: {gene_count} from {len(self.used_cluster)} clusters')
|
|
164
|
+
|
|
165
|
+
def get_real_strain_name(self, symbol):
|
|
166
|
+
strain_name = self.strain_dict[symbol].strain_name
|
|
167
|
+
clean_strain = ''.join(
|
|
168
|
+
char if char not in self.forbidden_chars else '_' for char in strain_name)
|
|
169
|
+
if strain_name != clean_strain:
|
|
170
|
+
logger.warning(
|
|
171
|
+
f'Forbidden char found in {strain_name}, will be cleaned to {clean_strain}')
|
|
172
|
+
return clean_strain, strain_name
|
|
173
|
+
|
|
174
|
+
def stat_from_pav(self, file):
|
|
175
|
+
self.para_dict = {'Strict_core': [[], []], 'Core': [[], []],
|
|
176
|
+
'Soft_core': [[], []], 'Shell': [[], []], 'Cloud': [[], []]}
|
|
177
|
+
with open(file, 'r') as fh:
|
|
178
|
+
for line in fh:
|
|
179
|
+
if line.startswith('#'):
|
|
180
|
+
continue
|
|
181
|
+
line = line.strip().split('\t')
|
|
182
|
+
group = str(line[3])
|
|
183
|
+
|
|
184
|
+
def stat_from_detail_pav(self, file):
|
|
185
|
+
self.min_dict = {'Strict_core': [], 'Core': [],
|
|
186
|
+
'Soft_core': [], 'Shell': [], 'Cloud': []}
|
|
187
|
+
self.uni_dict = {'Strict_core': [], 'Core': [],
|
|
188
|
+
'Soft_core': [], 'Shell': [], 'Cloud': []}
|
|
189
|
+
self.mean_dict = {'Strict_core': [], 'Core': [],
|
|
190
|
+
'Soft_core': [], 'Shell': [], 'Cloud': []}
|
|
191
|
+
self.var_dict = {'Strict_core': [], 'Core': [],
|
|
192
|
+
'Soft_core': [], 'Shell': [], 'Cloud': []}
|
|
193
|
+
with open(file, 'r') as fh:
|
|
194
|
+
for line in fh:
|
|
195
|
+
if line.startswith('#'):
|
|
196
|
+
continue
|
|
197
|
+
line = line.strip().split('\t')
|
|
198
|
+
group = str(line[3])
|
|
199
|
+
|
|
200
|
+
self.min_dict[group].append(float(line[5]))
|
|
201
|
+
self.mean_dict[group].append(float(line[6]))
|
|
202
|
+
self.var_dict[group].append(float(line[7]))
|
|
203
|
+
self.uni_dict[group].append(float(line[8]))
|