PGAP2 2.0__tar.gz → 2.3__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.
Files changed (91) hide show
  1. {pgap2-2.0 → pgap2-2.3/PGAP2.egg-info}/PKG-INFO +3 -1
  2. {pgap2-2.0 → pgap2-2.3}/PGAP2.egg-info/SOURCES.txt +23 -1
  3. {pgap2-2.0 → pgap2-2.3}/PGAP2.egg-info/requires.txt +2 -0
  4. {pgap2-2.0/PGAP2.egg-info → pgap2-2.3}/PKG-INFO +3 -1
  5. pgap2-2.3/pgap2/cli/neighborhood.py +209 -0
  6. {pgap2-2.0 → pgap2-2.3}/pgap2/cli/postprocess.py +2 -0
  7. pgap2-2.3/pgap2/cli/sqlite_cmd.py +84 -0
  8. {pgap2-2.0 → pgap2-2.3}/pgap2/lib/pangenome.py +13 -13
  9. {pgap2-2.0 → pgap2-2.3}/pgap2/lib/phylogeny.py +71 -19
  10. pgap2-2.3/pgap2/lib/synteny.py +1055 -0
  11. {pgap2-2.0 → pgap2-2.3}/pgap2/main.py +4 -0
  12. pgap2-2.3/pgap2/models/__init__.py +30 -0
  13. pgap2-2.3/pgap2/models/baps.py +22 -0
  14. pgap2-2.3/pgap2/models/base.py +8 -0
  15. pgap2-2.3/pgap2/models/gene_annotation.py +40 -0
  16. pgap2-2.3/pgap2/models/gene_cluster.py +48 -0
  17. pgap2-2.3/pgap2/models/gene_neighborhood.py +79 -0
  18. pgap2-2.3/pgap2/models/pav.py +31 -0
  19. pgap2-2.3/pgap2/models/phylogeny.py +26 -0
  20. pgap2-2.3/pgap2/models/preprocess.py +53 -0
  21. pgap2-2.3/pgap2/models/project.py +53 -0
  22. pgap2-2.3/pgap2/models/project_file.py +54 -0
  23. pgap2-2.3/pgap2/models/statistics.py +83 -0
  24. pgap2-2.3/pgap2/models/strain.py +60 -0
  25. pgap2-2.3/pgap2/models/tajimas_d.py +25 -0
  26. {pgap2-2.0 → pgap2-2.3}/pgap2/postprocess/baps.py +6 -3
  27. {pgap2-2.0 → pgap2-2.3}/pgap2/postprocess/graph.py +3 -4
  28. pgap2-2.3/pgap2/postprocess/neighborhood.py +184 -0
  29. {pgap2-2.0 → pgap2-2.3}/pgap2/postprocess/profile.py +2 -1
  30. {pgap2-2.0 → pgap2-2.3}/pgap2/postprocess/singletree.py +7 -4
  31. {pgap2-2.0 → pgap2-2.3}/pgap2/postprocess/stat.py +103 -21
  32. {pgap2-2.0 → pgap2-2.3}/pgap2/utils/data_loader.py +1 -1
  33. pgap2-2.3/pgap2/utils/neighborhood.py +140 -0
  34. {pgap2-2.0 → pgap2-2.3}/pgap2/utils/partition_add.py +1 -1
  35. pgap2-2.3/pgap2/utils/sqlite_builder.py +1211 -0
  36. {pgap2-2.0 → pgap2-2.3}/pyproject.toml +3 -1
  37. pgap2-2.3/tests/test_pangenome.py +19 -0
  38. pgap2-2.3/tests/test_post.py +95 -0
  39. pgap2-2.3/tests/test_synteny.py +861 -0
  40. pgap2-2.0/tests/test_post.py +0 -43
  41. {pgap2-2.0 → pgap2-2.3}/LICENSE +0 -0
  42. {pgap2-2.0 → pgap2-2.3}/MANIFEST.in +0 -0
  43. {pgap2-2.0 → pgap2-2.3}/PGAP2.egg-info/dependency_links.txt +0 -0
  44. {pgap2-2.0 → pgap2-2.3}/PGAP2.egg-info/entry_points.txt +0 -0
  45. {pgap2-2.0 → pgap2-2.3}/PGAP2.egg-info/top_level.txt +0 -0
  46. {pgap2-2.0 → pgap2-2.3}/README.md +0 -0
  47. {pgap2-2.0 → pgap2-2.3}/pgap2/__init__.py +0 -0
  48. {pgap2-2.0 → pgap2-2.3}/pgap2/cli/__init__.py +0 -0
  49. {pgap2-2.0 → pgap2-2.3}/pgap2/cli/partition.py +0 -0
  50. {pgap2-2.0 → pgap2-2.3}/pgap2/cli/partition_add.py +0 -0
  51. {pgap2-2.0 → pgap2-2.3}/pgap2/cli/partition_merge.py +0 -0
  52. {pgap2-2.0 → pgap2-2.3}/pgap2/cli/partition_minus.py +0 -0
  53. {pgap2-2.0 → pgap2-2.3}/pgap2/cli/preprocess.py +0 -0
  54. {pgap2-2.0 → pgap2-2.3}/pgap2/dependencies/draw_post_profile.r +0 -0
  55. {pgap2-2.0 → pgap2-2.3}/pgap2/dependencies/draw_post_stat.r +0 -0
  56. {pgap2-2.0 → pgap2-2.3}/pgap2/dependencies/draw_prep.r +0 -0
  57. {pgap2-2.0 → pgap2-2.3}/pgap2/dependencies/maskrc-svg.py +0 -0
  58. {pgap2-2.0 → pgap2-2.3}/pgap2/dependencies/run_fastbaps +0 -0
  59. {pgap2-2.0 → pgap2-2.3}/pgap2/lib/__init__.py +0 -0
  60. {pgap2-2.0 → pgap2-2.3}/pgap2/lib/basic.py +0 -0
  61. {pgap2-2.0 → pgap2-2.3}/pgap2/lib/panclust.py +0 -0
  62. {pgap2-2.0 → pgap2-2.3}/pgap2/lib/pangp.py +0 -0
  63. {pgap2-2.0 → pgap2-2.3}/pgap2/lib/pklcheck.py +0 -0
  64. {pgap2-2.0 → pgap2-2.3}/pgap2/lib/species.py +0 -0
  65. {pgap2-2.0 → pgap2-2.3}/pgap2/lib/strain.py +0 -0
  66. {pgap2-2.0 → pgap2-2.3}/pgap2/lib/tree.py +0 -0
  67. {pgap2-2.0 → pgap2-2.3}/pgap2/postprocess/__init__.py +0 -0
  68. {pgap2-2.0 → pgap2-2.3}/pgap2/postprocess/reference_layout.py +0 -0
  69. {pgap2-2.0 → pgap2-2.3}/pgap2/postprocess/tajimas_d.py +0 -0
  70. {pgap2-2.0 → pgap2-2.3}/pgap2/toy_input/287_91.fa +0 -0
  71. {pgap2-2.0 → pgap2-2.3}/pgap2/toy_input/287_91.gff +0 -0
  72. {pgap2-2.0 → pgap2-2.3}/pgap2/toy_input/AKU_12601.gff +0 -0
  73. {pgap2-2.0 → pgap2-2.3}/pgap2/toy_input/Readme_input.md +0 -0
  74. {pgap2-2.0 → pgap2-2.3}/pgap2/toy_input/SC_B67.gbk +0 -0
  75. {pgap2-2.0 → pgap2-2.3}/pgap2/utils/__init__.py +0 -0
  76. {pgap2-2.0 → pgap2-2.3}/pgap2/utils/alignment.py +0 -0
  77. {pgap2-2.0 → pgap2-2.3}/pgap2/utils/arrangement_detector.py +0 -0
  78. {pgap2-2.0 → pgap2-2.3}/pgap2/utils/draw.py +0 -0
  79. {pgap2-2.0 → pgap2-2.3}/pgap2/utils/gene_retriever.py +0 -0
  80. {pgap2-2.0 → pgap2-2.3}/pgap2/utils/generate_tree.py +0 -0
  81. {pgap2-2.0 → pgap2-2.3}/pgap2/utils/graph_utils.py +0 -0
  82. {pgap2-2.0 → pgap2-2.3}/pgap2/utils/partition.py +0 -0
  83. {pgap2-2.0 → pgap2-2.3}/pgap2/utils/partition_merge.py +0 -0
  84. {pgap2-2.0 → pgap2-2.3}/pgap2/utils/partition_minus.py +0 -0
  85. {pgap2-2.0 → pgap2-2.3}/pgap2/utils/postprocess.py +0 -0
  86. {pgap2-2.0 → pgap2-2.3}/pgap2/utils/preprocess.py +0 -0
  87. {pgap2-2.0 → pgap2-2.3}/pgap2/utils/supply.py +0 -0
  88. {pgap2-2.0 → pgap2-2.3}/pgap2/utils/tools.py +0 -0
  89. {pgap2-2.0 → pgap2-2.3}/setup.cfg +0 -0
  90. {pgap2-2.0 → pgap2-2.3}/tests/test_main.py +0 -0
  91. {pgap2-2.0 → pgap2-2.3}/tests/test_prep.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PGAP2
3
- Version: 2.0
3
+ Version: 2.3
4
4
  Summary: PGAP2: a comprehensive pan-genome analysis pipeline for prokaryotic genomes
5
5
  Author-email: bucongfan <bucf@big.ac.cn>
6
6
  Maintainer-email: bucongfan <bucf@big.ac.cn>
@@ -28,6 +28,8 @@ Requires-Dist: ete3==3.1.3
28
28
  Requires-Dist: svgwrite==1.4.3
29
29
  Requires-Dist: miniFasta==3.0.2
30
30
  Requires-Dist: tajimas_d==2.0.2
31
+ Requires-Dist: sqlalchemy>=2.0
32
+ Requires-Dist: igraph
31
33
  Dynamic: license-file
32
34
 
33
35
  ![image](https://github.com/bucongfan/PGAP2/blob/main/pgap2.logo.png)
@@ -11,12 +11,14 @@ PGAP2.egg-info/top_level.txt
11
11
  pgap2/__init__.py
12
12
  pgap2/main.py
13
13
  pgap2/cli/__init__.py
14
+ pgap2/cli/neighborhood.py
14
15
  pgap2/cli/partition.py
15
16
  pgap2/cli/partition_add.py
16
17
  pgap2/cli/partition_merge.py
17
18
  pgap2/cli/partition_minus.py
18
19
  pgap2/cli/postprocess.py
19
20
  pgap2/cli/preprocess.py
21
+ pgap2/cli/sqlite_cmd.py
20
22
  pgap2/dependencies/draw_post_profile.r
21
23
  pgap2/dependencies/draw_post_stat.r
22
24
  pgap2/dependencies/draw_prep.r
@@ -31,10 +33,26 @@ pgap2/lib/phylogeny.py
31
33
  pgap2/lib/pklcheck.py
32
34
  pgap2/lib/species.py
33
35
  pgap2/lib/strain.py
36
+ pgap2/lib/synteny.py
34
37
  pgap2/lib/tree.py
38
+ pgap2/models/__init__.py
39
+ pgap2/models/baps.py
40
+ pgap2/models/base.py
41
+ pgap2/models/gene_annotation.py
42
+ pgap2/models/gene_cluster.py
43
+ pgap2/models/gene_neighborhood.py
44
+ pgap2/models/pav.py
45
+ pgap2/models/phylogeny.py
46
+ pgap2/models/preprocess.py
47
+ pgap2/models/project.py
48
+ pgap2/models/project_file.py
49
+ pgap2/models/statistics.py
50
+ pgap2/models/strain.py
51
+ pgap2/models/tajimas_d.py
35
52
  pgap2/postprocess/__init__.py
36
53
  pgap2/postprocess/baps.py
37
54
  pgap2/postprocess/graph.py
55
+ pgap2/postprocess/neighborhood.py
38
56
  pgap2/postprocess/profile.py
39
57
  pgap2/postprocess/reference_layout.py
40
58
  pgap2/postprocess/singletree.py
@@ -53,14 +71,18 @@ pgap2/utils/draw.py
53
71
  pgap2/utils/gene_retriever.py
54
72
  pgap2/utils/generate_tree.py
55
73
  pgap2/utils/graph_utils.py
74
+ pgap2/utils/neighborhood.py
56
75
  pgap2/utils/partition.py
57
76
  pgap2/utils/partition_add.py
58
77
  pgap2/utils/partition_merge.py
59
78
  pgap2/utils/partition_minus.py
60
79
  pgap2/utils/postprocess.py
61
80
  pgap2/utils/preprocess.py
81
+ pgap2/utils/sqlite_builder.py
62
82
  pgap2/utils/supply.py
63
83
  pgap2/utils/tools.py
64
84
  tests/test_main.py
85
+ tests/test_pangenome.py
65
86
  tests/test_post.py
66
- tests/test_prep.py
87
+ tests/test_prep.py
88
+ tests/test_synteny.py
@@ -14,3 +14,5 @@ ete3==3.1.3
14
14
  svgwrite==1.4.3
15
15
  miniFasta==3.0.2
16
16
  tajimas_d==2.0.2
17
+ sqlalchemy>=2.0
18
+ igraph
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PGAP2
3
- Version: 2.0
3
+ Version: 2.3
4
4
  Summary: PGAP2: a comprehensive pan-genome analysis pipeline for prokaryotic genomes
5
5
  Author-email: bucongfan <bucf@big.ac.cn>
6
6
  Maintainer-email: bucongfan <bucf@big.ac.cn>
@@ -28,6 +28,8 @@ Requires-Dist: ete3==3.1.3
28
28
  Requires-Dist: svgwrite==1.4.3
29
29
  Requires-Dist: miniFasta==3.0.2
30
30
  Requires-Dist: tajimas_d==2.0.2
31
+ Requires-Dist: sqlalchemy>=2.0
32
+ Requires-Dist: igraph
31
33
  Dynamic: license-file
32
34
 
33
35
  ![image](https://github.com/bucongfan/PGAP2/blob/main/pgap2.logo.png)
@@ -0,0 +1,209 @@
1
+ """
2
+ CLI definitions for the 'neigh' (conserved gene neighborhood) subcommand.
3
+
4
+ This subcommand runs the full partition pipeline (identical to 'main')
5
+ followed by a conserved gene neighborhood discovery step inspired by
6
+ spacedust's agglomerative clustering algorithm.
7
+ """
8
+
9
+ import os
10
+ import argparse
11
+
12
+ from argparse import ArgumentParser, _SubParsersAction
13
+
14
+ from pgap2.utils.supply import sfw, tqdm_
15
+ from pgap2.utils.supply import set_verbosity_level
16
+ from pgap2.utils.graph_utils import check_min_falen, check_gcode
17
+
18
+
19
+ def launch(args: argparse.Namespace):
20
+ from pgap2.utils.neighborhood import main
21
+ main(
22
+ indir=os.path.abspath(args.indir),
23
+ outdir=os.path.abspath(args.outdir),
24
+ falen=args.min_falen, threads=args.threads, evalue=args.evalue,
25
+ aligner=args.aligner, clust_method=args.clust_method,
26
+ orth_id=args.orth_id, para_id=args.para_id, dup_id=args.dup_id,
27
+ id_attr_key=args.id_attr_key, type_filter=args.type_filter,
28
+ coverage=0.98, fast_mode=args.fast_mode, hconf_thre=args.hconf_thre,
29
+ LD=args.LD, AS=args.AS, AL=args.AL, max_targets=args.max_targets,
30
+ context_similarity=0, flank=5,
31
+ accurate=args.accurate,
32
+ exhaust_orth=args.exhaust_orth,
33
+ gcode=args.gcode,
34
+ disable=args.disable, annot=args.annot, retrieve=args.retrieve,
35
+ radius=args.radius, sensitivity=args.sensitivity, ins=args.ins,
36
+ # neighborhood-specific parameters
37
+ max_gap=args.max_gap,
38
+ min_genes=args.min_genes,
39
+ pval_threshold=args.pval,
40
+ min_genomes=args.min_genomes,
41
+ resolution=args.resolution,
42
+ use_order=args.use_order,
43
+ )
44
+
45
+
46
+ def neighborhood_portal(args):
47
+ set_verbosity_level(args.outdir, args.verbose,
48
+ args.debug, 'neighborhood')
49
+
50
+ if args.clust_method == 'mmseqs2':
51
+ sfw.check_dependency("mmseqs2")
52
+ elif args.clust_method == 'cdhit':
53
+ sfw.check_dependency("cdhit")
54
+
55
+ sfw.check_dependency("mcl")
56
+ sfw.check_dependency("mcxdeblast")
57
+ if args.aligner == 'diamond':
58
+ sfw.check_dependency("diamond")
59
+ elif args.aligner == 'blastp':
60
+ sfw.check_dependency("blastp")
61
+ sfw.check_dependency("makeblastdb")
62
+
63
+ if args.retrieve:
64
+ sfw.check_dependency("miniprot")
65
+ sfw.check_dependency("seqtk")
66
+ tqdm_.set_total_step(14) # 12 partition + 2 neighborhood steps
67
+ else:
68
+ tqdm_.set_total_step(9) # 7 partition + 2 neighborhood steps
69
+
70
+ if args.annot:
71
+ sfw.check_dependency("prodigal")
72
+
73
+ launch(args)
74
+
75
+
76
+ def neighborhood_cmd(subparser: _SubParsersAction):
77
+
78
+ subparser_neigh: ArgumentParser = subparser.add_parser(
79
+ 'neigh',
80
+ help='Conserved gene neighborhood (synteny block) analysis. '
81
+ 'Runs the full partition pipeline then discovers conserved '
82
+ 'gene neighborhoods across genomes (spacedust-style).',
83
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter)
84
+
85
+ # ---- Partition parameters (same as 'main') ----
86
+ subparser_neigh.add_argument(
87
+ '--indir', '-i', required=True,
88
+ help='Input directory containing genome files. Same prefix = same strain.')
89
+ subparser_neigh.add_argument(
90
+ '--outdir', '-o', required=False, default='.',
91
+ help='Output directory')
92
+ subparser_neigh.add_argument(
93
+ '--dup_id', required=False, type=float, default=0.99,
94
+ help='Maximum identity for most recent duplication events.')
95
+ subparser_neigh.add_argument(
96
+ '--orth_id', required=False, type=float, default=0.98,
97
+ help='Maximum identity between most similar pan-clusters.')
98
+ subparser_neigh.add_argument(
99
+ '--para_id', required=False, type=float, default=0.7,
100
+ help='Paralogous identity threshold.')
101
+ subparser_neigh.add_argument(
102
+ '--type-filter', required=False, type=str, default='CDS',
103
+ help='GFF feature type (3rd column) to include.')
104
+ subparser_neigh.add_argument(
105
+ '--id-attr-key', required=False, type=str, default='ID',
106
+ help='GFF attribute key for record ID.')
107
+ subparser_neigh.add_argument(
108
+ '--hconf_thre', required=False, type=float, default=1,
109
+ help='High confidence cluster threshold.')
110
+ subparser_neigh.add_argument(
111
+ '--exhaust_orth', '-e', required=False, action='store_true',
112
+ default=False, help='Exhaustive paralog splitting.')
113
+ subparser_neigh.add_argument(
114
+ '--sensitivity', '-s', required=False, type=str, default='strict',
115
+ choices=('soft', 'moderate', 'strict'),
116
+ help='Node connectivity sensitivity.')
117
+ subparser_neigh.add_argument(
118
+ '--ins', '-n', required=False, action='store_true', default=False,
119
+ help='Ignore insertion sequence influence.')
120
+ subparser_neigh.add_argument(
121
+ '--fast', '-f', dest='fast_mode', required=False,
122
+ action='store_true', default=False,
123
+ help='Fast mode (skip fine feature analysis).')
124
+ subparser_neigh.add_argument(
125
+ '--accurate', '-a', required=False, action='store_true',
126
+ default=False, help='Bidirection check for paralog partition.')
127
+ subparser_neigh.add_argument(
128
+ '--retrieve', '-r', required=False, action='store_true',
129
+ default=False, help='Retrieve genes lost during annotation.')
130
+ subparser_neigh.add_argument(
131
+ '--threads', '-t', required=False, default=1, type=int,
132
+ help='Number of threads.')
133
+ subparser_neigh.add_argument(
134
+ '--max_targets', '-k', required=False, type=int, default=2000,
135
+ help='Maximum alignment targets per query.')
136
+ subparser_neigh.add_argument(
137
+ '--LD', required=False, type=float, default=0.6,
138
+ help='Minimum gene length difference proportion.')
139
+ subparser_neigh.add_argument(
140
+ '--AS', required=False, type=float, default=0.6,
141
+ help='Coverage for shorter sequence.')
142
+ subparser_neigh.add_argument(
143
+ '--AL', required=False, type=float, default=0.6,
144
+ help='Coverage for longer sequence.')
145
+ subparser_neigh.add_argument(
146
+ '--evalue', required=False, type=float, default=1E-5,
147
+ help='Aligner E-value threshold.')
148
+ subparser_neigh.add_argument(
149
+ '--aligner', required=False, type=str, default='diamond',
150
+ choices=('diamond', 'blastp'), help='Pairwise aligner.')
151
+ subparser_neigh.add_argument(
152
+ '--clust_method', required=False, type=str, default='mmseqs2',
153
+ choices=('cdhit', 'mmseqs2'), help='Gene clustering method.')
154
+ subparser_neigh.add_argument(
155
+ '--radius', required=False, type=int, default=3,
156
+ help='Search region radius.')
157
+ subparser_neigh.add_argument(
158
+ '--min_falen', '-m', required=False, type=check_min_falen,
159
+ default=20, help='Minimum protein length (at least 11).')
160
+ subparser_neigh.add_argument(
161
+ '--gcode', required=False, type=check_gcode, default=11,
162
+ help='Genetic code.')
163
+ subparser_neigh.add_argument(
164
+ '--annot', required=False, action='store_true', default=False,
165
+ help='Re-annotate genomes with Prodigal.')
166
+
167
+ # ---- Neighborhood-specific parameters ----
168
+ neigh_group = subparser_neigh.add_argument_group(
169
+ 'Neighborhood parameters',
170
+ 'Parameters specific to conserved gene neighborhood detection.')
171
+ neigh_group.add_argument(
172
+ '--max_gap', required=False, type=int, default=3,
173
+ help='Maximum gene gap allowed between hits for merging into a '
174
+ 'syntenic block. Larger values allow more intervening genes.')
175
+ neigh_group.add_argument(
176
+ '-mg','--min_genes', required=False, type=int, default=2,
177
+ help='Minimum number of genes to define a syntenic block.')
178
+ neigh_group.add_argument(
179
+ '--pval', required=False, type=float, default=0.01,
180
+ help='P-value threshold for gene-order filtering (only used '
181
+ 'when --use_order is enabled).')
182
+ neigh_group.add_argument(
183
+ '--use_order', required=False, action='store_true', default=False,
184
+ help='Enable gene-order conservation filtering. When set, blocks '
185
+ 'are filtered by P_order <= --pval, edge weights use '
186
+ '-log10(P_order), and neighborhood P-values are computed '
187
+ 'via Fisher\'s combined test. Default: off (accept blocks '
188
+ 'purely based on --min_genes co-localization).')
189
+ neigh_group.add_argument(
190
+ '-mgs','--min_genomes', required=False, type=int, default=2,
191
+ help='Minimum number of genomes a neighborhood must span to be reported.')
192
+ neigh_group.add_argument(
193
+ '-re','--resolution', required=False, type=float, default=1.0,
194
+ help='Louvain resolution parameter. Higher values produce smaller, '
195
+ 'more focused neighborhoods; lower values produce larger, '
196
+ 'more inclusive ones. Try 2.0-10.0 if neighborhoods are too large.')
197
+
198
+ # ---- Common flags ----
199
+ subparser_neigh.add_argument(
200
+ '--verbose', '-v', required=False, action='store_true',
201
+ default=False, help='Verbose output')
202
+ subparser_neigh.add_argument(
203
+ '--debug', '-D', required=False, action='store_true',
204
+ default=False, help='Debug mode (very verbose)')
205
+ subparser_neigh.add_argument(
206
+ '--disable', required=False, action='store_true',
207
+ default=False, help='Disable progress bar')
208
+
209
+ subparser_neigh.set_defaults(func=neighborhood_portal)
@@ -7,6 +7,7 @@ from pgap2.postprocess.baps import baps_cmd
7
7
  from pgap2.postprocess.singletree import singletree_cmd
8
8
  from pgap2.postprocess.tajimas_d import tajimas_d_cmd
9
9
  from pgap2.postprocess.graph import graph_cmd
10
+ from pgap2.postprocess.neighborhood import neigh_cmd
10
11
  from argparse import ArgumentParser, _SubParsersAction
11
12
  import argparse
12
13
 
@@ -21,3 +22,4 @@ def postprocess_cmd(subparser: _SubParsersAction):
21
22
  baps_cmd(sub_subparsers)
22
23
  tajimas_d_cmd(sub_subparsers)
23
24
  graph_cmd(sub_subparsers)
25
+ neigh_cmd(sub_subparsers)
@@ -0,0 +1,84 @@
1
+ """CLI definitions for the ``pgap2 sqlite`` subcommand.
2
+
3
+ Generates a self-contained SQLite database from PGAP2 output files.
4
+
5
+ Usage::
6
+
7
+ pgap2 sqlite -i /path/to/output
8
+ pgap2 sqlite -i /path/to/output -o /path/to/output/pgap2.db
9
+ pgap2 sqlite -i /path/to/output --name "My Project" --species "E. coli"
10
+ """
11
+
12
+ import os
13
+ import argparse
14
+ from argparse import ArgumentParser, _SubParsersAction
15
+ from pathlib import Path
16
+
17
+
18
+ def launch(args: argparse.Namespace):
19
+ from pgap2.utils.sqlite_builder import build_sqlite, parse_readme
20
+
21
+ indir = os.path.abspath(args.indir)
22
+
23
+ # Resolve output path: default is <indir>/pgap2.db
24
+ if args.output:
25
+ output_path = os.path.abspath(args.output)
26
+ else:
27
+ output_path = os.path.join(indir, "pgap2.db")
28
+
29
+ # Try to derive project name / species from README.md if not provided
30
+ project_name = args.name
31
+ species = args.species
32
+
33
+ readme_path = Path(indir) / "README.md"
34
+ if readme_path.exists():
35
+ readme_info = parse_readme(readme_path)
36
+ if not project_name:
37
+ project_name = readme_info.get("project_name", "")
38
+ if not species:
39
+ species = readme_info.get("species", "")
40
+
41
+ if not project_name:
42
+ project_name = Path(indir).name
43
+ if not species:
44
+ species = "Unknown"
45
+
46
+ info = build_sqlite(
47
+ data_dir=indir,
48
+ output_path=output_path,
49
+ project_name=project_name,
50
+ species=species,
51
+ description=args.description or "",
52
+ )
53
+
54
+ return 0
55
+
56
+
57
+ def sqlite_portal(args):
58
+ """Entry point called by argparse (dependency checks + launch)."""
59
+ launch(args)
60
+
61
+
62
+ def sqlite_cmd(subparser: _SubParsersAction):
63
+ """Register the ``sqlite`` subcommand."""
64
+ sp: ArgumentParser = subparser.add_parser(
65
+ "sqlite",
66
+ help="Generate a SQLite database from PGAP2 output files",
67
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
68
+ )
69
+ sp.add_argument(
70
+ "--indir", "-i", required=True,
71
+ help="PGAP2 output directory (contains .pav, .detail.tsv, etc.)")
72
+ sp.add_argument(
73
+ "--output", "-o", required=False, default=None,
74
+ help="Output SQLite file path (default: <indir>/pgap2.db)")
75
+ sp.add_argument(
76
+ "--name", "-n", required=False, default=None,
77
+ help="Project name (auto-detected from README.md if present)")
78
+ sp.add_argument(
79
+ "--species", "-s", required=False, default=None,
80
+ help="Species name (auto-detected from README.md if present)")
81
+ sp.add_argument(
82
+ "--description", "-d", required=False, default="",
83
+ help="Optional project description")
84
+ sp.set_defaults(func=sqlite_portal)
@@ -272,8 +272,7 @@ class Pangenome():
272
272
  bar = tqdm(total=self.total_gene_num,
273
273
  unit=' Gene', desc=tqdm_.step(6), disable=self.disable_tqdm)
274
274
  loaded_contig_name = set()
275
- if retrieve:
276
- coord_pat = re.compile(r'\[(\d+):\d+\]\(([+-])\)')
275
+ coord_pat = re.compile(r'\[(\d+):\d+\]\(([+-])\)')
277
276
 
278
277
  with open(flat_annot_file) as fh:
279
278
  for line in fh:
@@ -283,28 +282,29 @@ class Pangenome():
283
282
  bar.update()
284
283
  gene_index = lines[0]
285
284
  contig_name = lines[2]
286
- if contig_name not in loaded_contig_name:
287
- loaded_contig_name.add(contig_name)
288
- self.annot_contig_map.update(
289
- {':'.join(gene_index.split(':')[:2]): contig_name})
285
+ contig_key = ':'.join(gene_index.split(':')[:2])
286
+ if contig_key not in loaded_contig_name:
287
+ #may duplicate when use contig name as the global var, chaning to genome_id:contig_id
288
+ loaded_contig_name.add(contig_key)
289
+ self.annot_contig_map[contig_key] = contig_name
290
290
  gene_len = int(lines[4])
291
291
  gene_id = lines[5]
292
292
  gene_name = lines[6]
293
293
  gene_product = lines[7]
294
+ strand = ''
295
+ m = coord_pat.search(lines[3])
296
+ if m:
297
+ start = int(m.group(1))
298
+ strand = m.group(2) # '+' or '-'
294
299
  if retrieve:
295
- # start = int(re.search(r'\[(\d+):', lines[3]).group(1))
296
- # strand = re.search(r'\[(\+|-)\d+:\d+\]', lines[3]).group(1)
297
- m = coord_pat.search(lines[3])
298
- if m:
299
- start = int(m.group(1)) # 起始坐标
300
- strand = m.group(2) # '+' 或 '-'
301
300
  contig = ":".join(lines[0].split(":")[:2])
302
301
  if strand == '+':
303
302
  self.gene_rank[contig][0].append(start)
304
303
  else:
305
304
  self.gene_rank[contig][1].append(start)
306
305
  self.annot.update({gene_index: {'len': gene_len, 'id': gene_id,
307
- 'name': gene_name, 'product': gene_product}})
306
+ 'name': gene_name, 'product': gene_product,
307
+ 'strand': strand}})
308
308
  bar.close()
309
309
  else:
310
310
  logger.error(f'Cannot find the annot file {flat_annot_file}')
@@ -45,7 +45,7 @@ def set_logger(logger_):
45
45
 
46
46
 
47
47
  class Phylogeny():
48
- def __init__(self, basic, outdir, threads, disable, msa_method, tree_method, fastbaps_levels=None, fastbaps_prior=None, add_paras=[]) -> None:
48
+ def __init__(self, basic, outdir, threads, disable, msa_method, tree_method, fastbaps_levels=None, fastbaps_prior=None, add_paras=[], no_partition=False) -> None:
49
49
  self.basic: Basic = basic
50
50
  self.outdir = outdir
51
51
  self.threads = threads
@@ -54,6 +54,7 @@ class Phylogeny():
54
54
  self.tree_method = tree_method
55
55
  self.fastbaps_levels = fastbaps_levels
56
56
  self.fastbaps_prior = fastbaps_prior
57
+ self.no_partition = no_partition
57
58
  self.add_paras_dict = {2: '', 4: '',
58
59
  6: '', 7: '', 8: '', 9: '', 10: ''}
59
60
  self.load_additional_paras(add_paras)
@@ -359,6 +360,7 @@ class Phylogeny():
359
360
  concatenated_gb.annotations['molecule_type'] = 'DNA'
360
361
 
361
362
  position_offset = 0 # Track the concatenated sequence length
363
+ partition_entries = [] # RAxML-style partition definitions
362
364
 
363
365
  real_involved_clusters = {
364
366
  strain: 0 for strain in self.basic.strain_dict}
@@ -400,8 +402,23 @@ class Phylogeny():
400
402
  type="region",
401
403
  qualifiers={"note": core_cluster_path, "locus_tag": core_cluster})
402
404
  concatenated_gb.features.append(feature)
405
+ # Record partition range (1-based) for RAxML/IQ-TREE partition model
406
+ partition_entries.append(
407
+ f'GTR+G, {core_cluster} = {position_offset + 1}-{position_offset + example_length}')
403
408
  position_offset += example_length # Update position for the next cluster
404
409
 
410
+ # Save partition file (RAxML-style, also accepted by IQ-TREE with -p)
411
+ if not self.no_partition:
412
+ partition_path = os.path.join(
413
+ cat_align_outdir, 'core_gene_alignment.partitions')
414
+ with open(partition_path, 'w') as fh:
415
+ fh.write('\n'.join(partition_entries) + '\n')
416
+ logger.info(
417
+ f'Partition file: {partition_path} ({len(partition_entries)} partitions)')
418
+ else:
419
+ partition_path = None
420
+ logger.info('Partition file generation skipped (--no-partition)')
421
+
405
422
  # Save the concatenated record to a GenBank file
406
423
  SeqIO.write(concatenated_gb, os.path.join(
407
424
  cat_align_outdir, 'core_gene_alignment.gb'), 'genbank')
@@ -424,6 +441,9 @@ class Phylogeny():
424
441
  self.results_file.append(f'{cat_align_outdir}/core_gene_alignment.aln')
425
442
  self.results_file.append(f'{cat_align_outdir}/core_gene_alignment.gb')
426
443
  self.results_file.append(f'{cat_align_outdir}/core_gene_alignment.txt')
444
+ if partition_path is not None:
445
+ self.results_file.append(partition_path)
446
+ self.partition_file = partition_path
427
447
  return f'{cat_align_outdir}/core_gene_alignment.aln'
428
448
 
429
449
  def construct_tree(self, wd):
@@ -433,18 +453,29 @@ class Phylogeny():
433
453
  os.makedirs(tree_outdir, exist_ok=True)
434
454
  tree_path = os.path.join(tree_outdir, 'core_gene_alignment')
435
455
  add_paras = self.add_paras_dict[6]
456
+ extra = (add_paras or "").strip()
457
+ model_arg = 'GTR+G'
458
+
436
459
  if self.tree_method == 'raxml':
437
- cmd = f'{sfw.raxml} --msa {self.first_aln} --model GTR+G --prefix {tree_path} --all --bs-trees 100 --redo {add_paras}'
438
- best_tree = os.path.join(
439
- tree_outdir, 'core_gene_alignment'+'.raxml.bestTree')
460
+ cmd = (
461
+ f'{sfw.raxml} --msa {self.first_aln} --model {model_arg} '
462
+ f'--prefix {tree_path} --all --bs-trees 100 --redo {extra}'
463
+ ).strip()
464
+ best_tree = f'{tree_path}.raxml.support'
465
+
440
466
  elif self.tree_method == 'fasttree':
441
- cmd = f'{sfw.fasttree} -gtr -gamma -nt -out {tree_path}.treefile {self.first_aln} {add_paras} 2> {tree_path}.log'
442
- best_tree = os.path.join(
443
- tree_outdir, 'core_gene_alignment'+'.treefile')
467
+ cmd = (
468
+ f'{sfw.fasttree} -nt -gtr -gamma {extra} '
469
+ f'-log {tree_path}.log {self.first_aln} > {tree_path}.treefile'
470
+ ).strip()
471
+ best_tree = f'{tree_path}.treefile'
472
+
444
473
  elif self.tree_method == 'iqtree':
445
- cmd = f'{sfw.iqtree} -s {self.first_aln} -m GTR+G -pre {tree_path} -bb 1000 -redo -nt AUTO {add_paras}'
446
- best_tree = os.path.join(
447
- tree_outdir, 'core_gene_alignment'+'.treefile')
474
+ cmd = (
475
+ f'{sfw.iqtree} -s {self.first_aln} -m {model_arg} '
476
+ f'-pre {tree_path} -bb 1000 -bnni -redo -nt AUTO {extra}'
477
+ ).strip()
478
+ best_tree = f'{tree_path}.treefile'
448
479
 
449
480
  self.results_file.append(best_tree)
450
481
 
@@ -529,19 +560,40 @@ class Phylogeny():
529
560
  os.makedirs(tree_outdir, exist_ok=True)
530
561
  tree_path = os.path.join(tree_outdir, 'reconstructed_tree')
531
562
  add_paras = self.add_paras_dict[9]
563
+ extra = (add_paras or "").strip()
564
+
565
+ # Use partition model when partition file is available
566
+ has_partition = hasattr(
567
+ self, 'partition_file') and os.path.exists(self.partition_file)
532
568
 
533
569
  if self.tree_method == 'raxml':
534
- cmd = f'{sfw.raxml} --msa {self.maskrc_aln} --model GTR+G --prefix {tree_path} --all --bs-trees 100 --redo {add_paras}'
535
- best_tree = os.path.join(
536
- tree_outdir, 'reconstructed_tree'+'.raxml.bestTree')
570
+ model_arg = self.partition_file if has_partition else 'GTR+G'
571
+ cmd = (
572
+ f'{sfw.raxml} --msa {self.maskrc_aln} --model {model_arg} '
573
+ f'--prefix {tree_path} --all --bs-trees 100 --redo {extra}'
574
+ ).strip()
575
+ best_tree = f'{tree_path}.raxml.support' # 带 bootstrap 支持值的最终树
576
+
537
577
  elif self.tree_method == 'fasttree':
538
- cmd = f'{sfw.fasttree} -gtr -gamma -nt -out {tree_path}.treefile {self.maskrc_aln} {add_paras} 2> {tree_path}.log'
539
- best_tree = os.path.join(
540
- tree_outdir, 'reconstructed_tree'+'.treefile')
578
+ # FastTree does not support partition models
579
+ cmd = (
580
+ f'{sfw.fasttree} -nt -gtr -gamma {extra} '
581
+ f'-log {tree_path}.log {self.maskrc_aln} > {tree_path}.treefile'
582
+ ).strip()
583
+ best_tree = f'{tree_path}.treefile'
584
+
541
585
  elif self.tree_method == 'iqtree':
542
- cmd = f'{sfw.iqtree} -s {self.maskrc_aln} -m GTR+G -pre {tree_path} -bb 1000 -redo -nt AUTO {add_paras}'
543
- best_tree = os.path.join(
544
- tree_outdir, 'reconstructed_tree'+'.treefile')
586
+ if has_partition:
587
+ cmd = (
588
+ f'{sfw.iqtree} -s {self.maskrc_aln} -p {self.partition_file} '
589
+ f'-pre {tree_path} -m MFP+MERGE -bb 1000 -bnni -redo -nt AUTO {extra}'
590
+ ).strip()
591
+ else:
592
+ cmd = (
593
+ f'{sfw.iqtree} -s {self.maskrc_aln} -m GTR+G '
594
+ f'-pre {tree_path} -bb 1000 -bnni -redo -nt AUTO {extra}'
595
+ ).strip()
596
+ best_tree = f'{tree_path}.treefile'
545
597
 
546
598
  self.results_file.append(best_tree)
547
599
  finished_status = self.check_before_run(cmd, tree_outdir)