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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.8.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pgap2 = pgap2.main:main
@@ -0,0 +1 @@
1
+ pgap2
pgap2/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ __version__ = '1.0'
2
+ __author__ = 'Congfan Bu'
@@ -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]))