pytransdecoder 0.2.1__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.
Files changed (63) hide show
  1. pytransdecoder/__init__.py +7 -0
  2. pytransdecoder/__main__.py +6 -0
  3. pytransdecoder/cli.py +781 -0
  4. pytransdecoder/core/__init__.py +1 -0
  5. pytransdecoder/core/base_freqs.py +71 -0
  6. pytransdecoder/core/decoys.py +113 -0
  7. pytransdecoder/core/fasta.py +55 -0
  8. pytransdecoder/core/gene_model.py +143 -0
  9. pytransdecoder/core/gff3_parser.py +58 -0
  10. pytransdecoder/core/homology_starts.py +54 -0
  11. pytransdecoder/core/kozak.py +180 -0
  12. pytransdecoder/core/models.py +123 -0
  13. pytransdecoder/core/orf_finder.py +317 -0
  14. pytransdecoder/core/presets.py +152 -0
  15. pytransdecoder/core/prng.py +54 -0
  16. pytransdecoder/core/pwm.py +174 -0
  17. pytransdecoder/core/sequence.py +19 -0
  18. pytransdecoder/core/translator.py +143 -0
  19. pytransdecoder/core/writers.py +69 -0
  20. pytransdecoder/genome.py +338 -0
  21. pytransdecoder/longorfs.py +180 -0
  22. pytransdecoder/predict.py +1621 -0
  23. pytransdecoder/presets/apicomplexan/hexamer.scores.gz +0 -0
  24. pytransdecoder/presets/apicomplexan/kozak.tsv +9 -0
  25. pytransdecoder/presets/apicomplexan/preset.json +20 -0
  26. pytransdecoder/presets/apicomplexan/thresholds.tsv.gz +0 -0
  27. pytransdecoder/presets/apicomplexan/training_orfs.cds.gz +0 -0
  28. pytransdecoder/presets/ciliate/hexamer.scores.gz +0 -0
  29. pytransdecoder/presets/ciliate/kozak.tsv +9 -0
  30. pytransdecoder/presets/ciliate/preset.json +20 -0
  31. pytransdecoder/presets/ciliate/thresholds.tsv.gz +0 -0
  32. pytransdecoder/presets/ciliate/training_orfs.cds.gz +0 -0
  33. pytransdecoder/presets/fungus/hexamer.scores.gz +0 -0
  34. pytransdecoder/presets/fungus/kozak.tsv +9 -0
  35. pytransdecoder/presets/fungus/preset.json +20 -0
  36. pytransdecoder/presets/fungus/thresholds.tsv.gz +0 -0
  37. pytransdecoder/presets/fungus/training_orfs.cds.gz +0 -0
  38. pytransdecoder/presets/green_alga/hexamer.scores.gz +0 -0
  39. pytransdecoder/presets/green_alga/kozak.tsv +9 -0
  40. pytransdecoder/presets/green_alga/preset.json +20 -0
  41. pytransdecoder/presets/green_alga/thresholds.tsv.gz +0 -0
  42. pytransdecoder/presets/green_alga/training_orfs.cds.gz +0 -0
  43. pytransdecoder/presets/insect/hexamer.scores.gz +0 -0
  44. pytransdecoder/presets/insect/kozak.tsv +9 -0
  45. pytransdecoder/presets/insect/preset.json +20 -0
  46. pytransdecoder/presets/insect/thresholds.tsv.gz +0 -0
  47. pytransdecoder/presets/insect/training_orfs.cds.gz +0 -0
  48. pytransdecoder/presets/mammal/hexamer.scores.gz +0 -0
  49. pytransdecoder/presets/mammal/kozak.tsv +9 -0
  50. pytransdecoder/presets/mammal/preset.json +20 -0
  51. pytransdecoder/presets/mammal/thresholds.tsv.gz +0 -0
  52. pytransdecoder/presets/mammal/training_orfs.cds.gz +0 -0
  53. pytransdecoder/presets/plant/hexamer.scores.gz +0 -0
  54. pytransdecoder/presets/plant/kozak.tsv +9 -0
  55. pytransdecoder/presets/plant/preset.json +20 -0
  56. pytransdecoder/presets/plant/thresholds.tsv.gz +0 -0
  57. pytransdecoder/presets/plant/training_orfs.cds.gz +0 -0
  58. pytransdecoder-0.2.1.dist-info/METADATA +307 -0
  59. pytransdecoder-0.2.1.dist-info/RECORD +63 -0
  60. pytransdecoder-0.2.1.dist-info/WHEEL +5 -0
  61. pytransdecoder-0.2.1.dist-info/entry_points.txt +3 -0
  62. pytransdecoder-0.2.1.dist-info/licenses/LICENSE +28 -0
  63. pytransdecoder-0.2.1.dist-info/top_level.txt +1 -0
pytransdecoder/cli.py ADDED
@@ -0,0 +1,781 @@
1
+ """
2
+ Command-line interface for PyTransDecoder
3
+ """
4
+
5
+ import sys
6
+ import argparse
7
+ from pathlib import Path
8
+ from . import __version__
9
+ from .longorfs import run_longorfs, DEFAULT_MIN_PROTEIN_LENGTH
10
+ from .predict import (DEFAULT_START_CONTRAST_ALPHA, DEFAULT_START_KOZAK_WEIGHT,
11
+ DEFAULT_DECOY_Q, PRECISE_DECOY_Q, SENSITIVE_DECOY_Q, DEFAULT_DECOY_SEED)
12
+
13
+
14
+ def create_longorfs_parser(subparsers):
15
+ """Create parser for longorfs command"""
16
+ parser = subparsers.add_parser(
17
+ 'longorfs',
18
+ help='Extract long ORFs from transcripts (Phase 1)',
19
+ description='''Extract long ORFs from transcripts (Phase 1)
20
+
21
+ This command identifies all potential ORFs in the input transcripts and
22
+ outputs them in GFF3 format along with CDS and protein sequences.
23
+
24
+ Example:
25
+ pytransdecoder longorfs -t transcripts.fasta
26
+ ''',
27
+ formatter_class=argparse.RawDescriptionHelpFormatter
28
+ )
29
+
30
+ parser.add_argument(
31
+ '-t', '--transcripts',
32
+ required=True,
33
+ type=Path,
34
+ help='Transcripts FASTA file'
35
+ )
36
+ parser.add_argument(
37
+ '-m', '--min-protein-length',
38
+ type=int,
39
+ default=DEFAULT_MIN_PROTEIN_LENGTH,
40
+ help="Minimum protein length in amino acids; ORFs under 100 codons must also pass Predict's "
41
+ f"short-ORF frame-contrast test (default: {DEFAULT_MIN_PROTEIN_LENGTH})"
42
+ )
43
+ parser.add_argument(
44
+ '-G', '--genetic-code',
45
+ default='universal',
46
+ help='Genetic code (universal, Euplotid, Tetrahymena, Candida, etc.) (default: universal)'
47
+ )
48
+ parser.add_argument(
49
+ '-S', '--strand-specific',
50
+ action='store_true',
51
+ help='Only analyze top strand'
52
+ )
53
+ parser.add_argument(
54
+ '-O', '--output-dir', '--output_dir',
55
+ dest='output_dir',
56
+ type=Path,
57
+ default=None,
58
+ help='Output directory (default: current directory)'
59
+ )
60
+ parser.add_argument(
61
+ '--gene-trans-map', '--gene_trans_map',
62
+ dest='gene_trans_map',
63
+ type=Path,
64
+ help='Gene-to-transcript mapping file (tab-delimited: gene_id<tab>trans_id)'
65
+ )
66
+ parser.add_argument(
67
+ '--complete-orfs-only', '--complete_orfs_only',
68
+ dest='complete_orfs_only',
69
+ action='store_true',
70
+ help='Only output complete ORFs (with start and stop codons)'
71
+ )
72
+ parser.add_argument(
73
+ '-v', '--verbose',
74
+ action='store_true',
75
+ help='Verbose output'
76
+ )
77
+ parser.add_argument(
78
+ '--version',
79
+ action='store_true',
80
+ help='Show version and exit'
81
+ )
82
+
83
+ parser.set_defaults(func=longorfs_cmd)
84
+ return parser
85
+
86
+
87
+ def _add_qvalue_args(parser):
88
+ """q-value cutoff for ORFs without homology support: a number or a named setting."""
89
+ g = parser.add_mutually_exclusive_group()
90
+ g.add_argument('--max-qvalue', '--decoy-q', type=float, default=None, dest='decoy_q',
91
+ help="Keep ORFs (without homology support) whose target-decoy q-value is <= this "
92
+ f"(default: {DEFAULT_DECOY_Q:g})")
93
+ g.add_argument('--precise', action='store_true',
94
+ help=f"Fewer calls on noncoding sequence, at some loss of short and partial ORFs "
95
+ f"(--max-qvalue {PRECISE_DECOY_Q:g})")
96
+ g.add_argument('--sensitive', action='store_true',
97
+ help=f"More short and partial ORFs, at more calls on noncoding sequence "
98
+ f"(--max-qvalue {SENSITIVE_DECOY_Q:g})")
99
+
100
+
101
+ def _add_selection_args(parser):
102
+ """ORF acceptance: q-value cutoff, decoy settings and presets."""
103
+ _add_qvalue_args(parser)
104
+ parser.add_argument('--decoy-shuffles', type=int, default=0, dest='decoy_shuffles',
105
+ help="Shuffled copies of each transcript for decoy ORFs; 0 = automatic "
106
+ "(~20 Mb of decoy sequence, at most 50)")
107
+ parser.add_argument('--decoy-seed', type=int, default=DEFAULT_DECOY_SEED, dest='decoy_seed',
108
+ help="Seed of the decoy shuffles (default: %(default)s)")
109
+ from .core.presets import bundled_presets
110
+ parser.add_argument('--preset', default=None,
111
+ help="Use a preset's coding model, start model and q-value thresholds instead of "
112
+ "learning them from the input; recommended for fewer than ~1,000 transcripts "
113
+ "or sets of mostly partial transcripts. A bundled name (" +
114
+ (", ".join(bundled_presets()) or "none installed") +
115
+ ") or a directory from build-preset")
116
+
117
+
118
+ def _qvalue_cutoff(args) -> float:
119
+ if args.precise:
120
+ return PRECISE_DECOY_Q
121
+ if args.sensitive:
122
+ return SENSITIVE_DECOY_Q
123
+ return DEFAULT_DECOY_Q if args.decoy_q is None else args.decoy_q
124
+
125
+
126
+ def create_predict_parser(subparsers):
127
+ """Create parser for predict command"""
128
+ parser = subparsers.add_parser(
129
+ 'predict',
130
+ help='Predict likely coding regions (Phase 2)',
131
+ description='''Predict likely coding regions (Phase 2)
132
+
133
+ This command takes the output from TransDecoder.LongOrfs and identifies
134
+ the most likely coding regions using:
135
+ - Hexamer composition scoring
136
+ - Homology support (BLAST and Pfam)
137
+ - ORF length
138
+ - Start codon refinement
139
+
140
+ Example:
141
+ pytransdecoder predict -t transcripts.fasta --retain_pfam_hits pfam.domtblout
142
+ ''',
143
+ formatter_class=argparse.RawDescriptionHelpFormatter
144
+ )
145
+
146
+ parser.add_argument(
147
+ '-t', '--transcripts',
148
+ required=True,
149
+ type=Path,
150
+ help='Transcripts FASTA file'
151
+ )
152
+ parser.add_argument(
153
+ '-O', '--output-dir', '--output_dir',
154
+ dest='output_dir',
155
+ type=Path,
156
+ help='Output directory (default: current directory)'
157
+ )
158
+ parser.add_argument(
159
+ '-T', '--top-orfs-train', '--top_orfs_train',
160
+ dest='top_orfs_train',
161
+ type=int,
162
+ default=500,
163
+ help='Number of top ORFs to use for training Markov model (default: 500)'
164
+ )
165
+ parser.add_argument(
166
+ '--retain-long-orfs-mode', '--retain_long_orfs_mode',
167
+ type=str,
168
+ choices=['dynamic', 'strict'],
169
+ default='dynamic',
170
+ dest='retain_long_orfs_mode',
171
+ help='Mode for retaining long ORFs (default: dynamic)'
172
+ )
173
+ parser.add_argument(
174
+ '--retain-long-orfs-length', '--retain_long_orfs_length',
175
+ type=int,
176
+ default=1000000,
177
+ dest='retain_long_orfs_length',
178
+ help='Under strict mode, minimum length to auto-retain (default: 1000000)'
179
+ )
180
+ parser.add_argument(
181
+ '--retain-pfam-hits', '--retain_pfam_hits',
182
+ type=Path,
183
+ dest='retain_pfam_hits',
184
+ help='Pfam domain hits file from hmmscan'
185
+ )
186
+ parser.add_argument(
187
+ '--retain-blastp-hits', '--retain_blastp_hits',
188
+ type=Path,
189
+ dest='retain_blastp_hits',
190
+ help='BLASTP hits file in outfmt 6 format'
191
+ )
192
+ parser.add_argument(
193
+ '--retain-multiple-orfs', '--retain_multiple_orfs',
194
+ action='store_false',
195
+ dest='single_best_only',
196
+ help='Report every non-overlapping ORF that passes selection, not only the best per '
197
+ 'transcript (the default reports one ORF per transcript)'
198
+ )
199
+ parser.add_argument(
200
+ '--single-best-only', '--single_best_only',
201
+ action='store_true',
202
+ dest='single_best_only',
203
+ help=argparse.SUPPRESS # now the default; accepted for compatibility
204
+ )
205
+ parser.add_argument(
206
+ '--no-refine-starts', '--no_refine_starts',
207
+ action='store_true',
208
+ dest='no_refine_starts',
209
+ help='Skip start codon refinement'
210
+ )
211
+ parser.add_argument(
212
+ '--no-homology-start-assist', '--no_homology_start_assist',
213
+ action='store_false',
214
+ dest='homology_start_assist',
215
+ help="Start refinement: do not use the --retain-blastp-hits / --retain-pfam-hits evidence to "
216
+ "decide whether a 5'-open ORF keeps its open end or is trimmed to its first ATG (by "
217
+ "default, when hits are supplied: trim if the best BLASTP hit's subject N-terminus aligns "
218
+ "within 5 aa of the ATG; keep open if the alignment starts >= 5 aa upstream of the ATG with "
219
+ "more subject upstream, or a Pfam domain envelope starts upstream of the ATG; otherwise, "
220
+ "or when the calls conflict, the ab initio rule decides)"
221
+ )
222
+ parser.add_argument(
223
+ '--start-contrast-alpha', type=float, default=DEFAULT_START_CONTRAST_ALPHA,
224
+ dest='start_contrast_alpha',
225
+ help="Start refinement: UTR score per base of the segment before an open ORF's first in-frame "
226
+ "ATG, i.e. the fraction of typical coding signal the segment may show and still be "
227
+ "called UTR. Higher values trim more open 5' ends (suits full-length/long-read "
228
+ "transcripts); lower values keep more 5' partial (suits fragmentary assemblies) "
229
+ f"(default: {DEFAULT_START_CONTRAST_ALPHA})"
230
+ )
231
+ parser.add_argument(
232
+ '--start-kozak-weight', type=float, default=DEFAULT_START_KOZAK_WEIGHT,
233
+ dest='start_kozak_weight',
234
+ help="Start refinement: bases of coding signal the first ATG may overrule per bit of its "
235
+ "Kozak score (log2-odds vs internal ATGs); 0 ignores start context "
236
+ f"(default: {DEFAULT_START_KOZAK_WEIGHT:g})"
237
+ )
238
+ _add_selection_args(parser)
239
+ parser.add_argument(
240
+ '-G', '--genetic-code',
241
+ default='Standard',
242
+ help='Genetic code (default: Standard)'
243
+ )
244
+ parser.add_argument(
245
+ '-v', '--verbose',
246
+ action='store_true',
247
+ help='Verbose output'
248
+ )
249
+
250
+ parser.set_defaults(func=predict_cmd)
251
+ return parser
252
+
253
+
254
+ def longorfs_cmd(args):
255
+ """Execute longorfs command"""
256
+ if args.version:
257
+ print(f"TransDecoder.LongOrfs {__version__}")
258
+ sys.exit(0)
259
+
260
+ # Check that transcripts file exists
261
+ if not args.transcripts.exists():
262
+ print(f"Error: Transcripts file not found: {args.transcripts}", file=sys.stderr)
263
+ sys.exit(1)
264
+
265
+ # Check gene_trans_map if provided
266
+ if args.gene_trans_map and not args.gene_trans_map.exists():
267
+ print(f"Error: Gene-transcript map file not found: {args.gene_trans_map}", file=sys.stderr)
268
+ sys.exit(1)
269
+
270
+ try:
271
+ run_longorfs(
272
+ transcripts_file=args.transcripts,
273
+ min_protein_length=args.min_protein_length,
274
+ genetic_code=args.genetic_code,
275
+ strand_specific=args.strand_specific,
276
+ output_dir=args.output_dir,
277
+ gene_trans_map_file=args.gene_trans_map,
278
+ complete_orfs_only=args.complete_orfs_only,
279
+ verbose=args.verbose
280
+ )
281
+ except Exception as e:
282
+ print(f"Error: {e}", file=sys.stderr)
283
+ if args.verbose:
284
+ import traceback
285
+ traceback.print_exc()
286
+ sys.exit(1)
287
+
288
+
289
+ def predict_cmd(args):
290
+ """Execute predict command"""
291
+ from .predict import TransDecoderPredict
292
+ import logging
293
+
294
+ # Setup logging
295
+ logging.basicConfig(
296
+ level=logging.DEBUG if args.verbose else logging.INFO,
297
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
298
+ )
299
+
300
+ # Check that transcripts file exists
301
+ if not args.transcripts.exists():
302
+ print(f"Error: Transcripts file not found: {args.transcripts}", file=sys.stderr)
303
+ sys.exit(1)
304
+
305
+ # Check optional input files
306
+ if args.retain_pfam_hits and not args.retain_pfam_hits.exists():
307
+ print(f"Error: Pfam hits file not found: {args.retain_pfam_hits}", file=sys.stderr)
308
+ sys.exit(1)
309
+
310
+ if args.retain_blastp_hits and not args.retain_blastp_hits.exists():
311
+ print(f"Error: BLASTP hits file not found: {args.retain_blastp_hits}", file=sys.stderr)
312
+ sys.exit(1)
313
+
314
+ try:
315
+ predictor = TransDecoderPredict(
316
+ transcripts_file=str(args.transcripts),
317
+ output_dir=str(args.output_dir) if args.output_dir else None,
318
+ top_orfs_train=args.top_orfs_train,
319
+ retain_long_orfs_mode=args.retain_long_orfs_mode,
320
+ retain_long_orfs_length=args.retain_long_orfs_length,
321
+ retain_pfam_hits=str(args.retain_pfam_hits) if args.retain_pfam_hits else None,
322
+ retain_blastp_hits=str(args.retain_blastp_hits) if args.retain_blastp_hits else None,
323
+ single_best_only=args.single_best_only,
324
+ no_refine_starts=args.no_refine_starts,
325
+ genetic_code=args.genetic_code,
326
+ start_contrast_alpha=args.start_contrast_alpha,
327
+ start_kozak_weight=args.start_kozak_weight,
328
+ decoy_q=_qvalue_cutoff(args),
329
+ decoy_shuffles=args.decoy_shuffles,
330
+ decoy_seed=args.decoy_seed,
331
+ preset=args.preset,
332
+ homology_start_assist=args.homology_start_assist,
333
+ )
334
+ predictor.run()
335
+ except Exception as e:
336
+ print(f"Error: {e}", file=sys.stderr)
337
+ if args.verbose:
338
+ import traceback
339
+ traceback.print_exc()
340
+ sys.exit(1)
341
+
342
+
343
+ def create_pipeline_parser():
344
+ """Create standalone parser for pyTransdecoder pipeline command"""
345
+ parser = argparse.ArgumentParser(
346
+ prog='pyTransdecoder',
347
+ description='pyTransdecoder: Full pipeline - run LongOrfs, optional BLAST/Pfam searches, then Predict',
348
+ epilog='Python port of TransDecoder (https://github.com/TransDecoder/TransDecoder)',
349
+ formatter_class=argparse.RawDescriptionHelpFormatter
350
+ )
351
+
352
+ parser.add_argument('-t', '--transcripts', required=False, default=None, type=Path,
353
+ help='Transcripts FASTA file (required unless --genome and --gtf are provided)')
354
+ parser.add_argument('--genome', type=Path, default=None, dest='genome',
355
+ help='Genome FASTA file (use with --gtf to extract cDNA sequences via gffread)')
356
+ parser.add_argument('--gtf', type=Path, default=None, dest='gtf',
357
+ help='Genome annotation GTF file (use with --genome to extract cDNA sequences via gffread)')
358
+ parser.add_argument('--structures', type=Path, default=None, dest='structures',
359
+ help='Genome structures of the -t transcripts for annotating the genome: GTF, alignment '
360
+ 'GFF3 (GMAP/PASA, Target=), or SAM/BAM spliced alignments (minimap2). Writes '
361
+ '<transcripts>.transdecoder.genome.gff3')
362
+ parser.add_argument('-m', '--min-protein-length', type=int, default=DEFAULT_MIN_PROTEIN_LENGTH,
363
+ dest='min_protein_length',
364
+ help='Minimum protein length in amino acids (default: %(default)s)')
365
+ parser.add_argument('-G', '--genetic-code', default='universal', dest='genetic_code',
366
+ help='Genetic code (default: universal)')
367
+ parser.add_argument('-S', '--strand-specific', action='store_true', dest='strand_specific',
368
+ help='Only analyze top strand')
369
+ parser.add_argument('-O', '--output-dir', type=Path, default=None, dest='output_dir',
370
+ help='Output directory (default: current directory)')
371
+ parser.add_argument('--gene-trans-map', type=Path, default=None, dest='gene_trans_map',
372
+ help='Gene-to-transcript mapping file (tab-delimited: gene_id<tab>trans_id)')
373
+ parser.add_argument('--complete-orfs-only', action='store_true', dest='complete_orfs_only',
374
+ help='Only output complete ORFs (with start and stop codons)')
375
+
376
+ # BLAST args
377
+ parser.add_argument('--blast-search-pep', type=Path, default=None, dest='blast_search_pep',
378
+ help='Protein FASTA to search against; triggers homology search step '
379
+ '(database will be built automatically for the selected --blast-tool)')
380
+ parser.add_argument('--blast-tool', choices=['diamond', 'blastp'], default='diamond',
381
+ dest='blast_tool',
382
+ help='Homology search tool: diamond or blastp (default: diamond)')
383
+ parser.add_argument('--blast-evalue', type=float, default=1e-5, dest='blast_evalue',
384
+ help='E-value cutoff for homology search (default: 1e-5)')
385
+ parser.add_argument('--blast-threads', type=int, default=1, dest='blast_threads',
386
+ help='Number of threads for homology search (default: 1)')
387
+ parser.add_argument('--pfam-search-db', type=Path, default=None, dest='pfam_search_db',
388
+ help='Pfam HMM database to search with hmmsearch; '
389
+ 'hmmpress will be run automatically if needed')
390
+
391
+ # Predict args
392
+ parser.add_argument('-T', '--top-orfs-train', type=int, default=500, dest='top_orfs_train',
393
+ help='Number of top ORFs to use for training Markov model (default: 500)')
394
+ parser.add_argument('--retain-long-orfs-mode', type=str, choices=['dynamic', 'strict'],
395
+ default='dynamic', dest='retain_long_orfs_mode',
396
+ help='Mode for retaining long ORFs (default: dynamic)')
397
+ parser.add_argument('--retain-long-orfs-length', type=int, default=1000000,
398
+ dest='retain_long_orfs_length',
399
+ help='Under strict mode, minimum length to auto-retain (default: 1000000)')
400
+ parser.add_argument('--retain-multiple-orfs', action='store_false', dest='single_best_only',
401
+ help='Report every non-overlapping ORF that passes selection (default: '
402
+ 'one ORF per transcript)')
403
+ parser.add_argument('--single-best-only', action='store_true', dest='single_best_only',
404
+ help=argparse.SUPPRESS) # now the default; accepted for compatibility
405
+ parser.add_argument('--no-refine-starts', action='store_true', dest='no_refine_starts',
406
+ help='Skip start codon refinement')
407
+ parser.add_argument('--no-homology-start-assist', '--no_homology_start_assist', action='store_false',
408
+ dest='homology_start_assist',
409
+ help="Do not use BLAST/Pfam hits to decide 5'-open vs trimmed starts "
410
+ "(default: used whenever hits are available)")
411
+ parser.add_argument('--start-contrast-alpha', type=float, default=DEFAULT_START_CONTRAST_ALPHA,
412
+ dest='start_contrast_alpha',
413
+ help="Start refinement UTR score per base of segment (default: %(default)s)")
414
+ parser.add_argument('--start-kozak-weight', type=float, default=DEFAULT_START_KOZAK_WEIGHT,
415
+ dest='start_kozak_weight',
416
+ help="Start refinement bases per bit of Kozak score (default: %(default)s)")
417
+ _add_selection_args(parser)
418
+ parser.add_argument('-v', '--verbose', action='store_true',
419
+ help='Verbose output')
420
+ parser.add_argument('--version', action='version', version=f'%(prog)s {__version__}')
421
+
422
+ return parser
423
+
424
+
425
+ def pipeline_cmd(args):
426
+ """Orchestrate the full pyTransdecoder pipeline"""
427
+ import subprocess
428
+ import logging
429
+ from .predict import TransDecoderPredict
430
+
431
+ logging.basicConfig(
432
+ level=logging.DEBUG if args.verbose else logging.INFO,
433
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
434
+ )
435
+
436
+ # Validate inputs and resolve transcripts file
437
+ genome_mode = args.genome is not None or args.gtf is not None
438
+ if genome_mode and args.structures is not None:
439
+ print("Error: use --structures with -t, not with --genome/--gtf", file=sys.stderr)
440
+ sys.exit(1)
441
+ if args.structures is not None and not args.structures.exists():
442
+ print(f"Error: structures file not found: {args.structures}", file=sys.stderr)
443
+ sys.exit(1)
444
+ if genome_mode:
445
+ if args.genome is None or args.gtf is None:
446
+ print("Error: --genome and --gtf must be provided together", file=sys.stderr)
447
+ sys.exit(1)
448
+ if not args.genome.exists():
449
+ print(f"Error: Genome FASTA not found: {args.genome}", file=sys.stderr)
450
+ sys.exit(1)
451
+ if not args.gtf.exists():
452
+ print(f"Error: GTF file not found: {args.gtf}", file=sys.stderr)
453
+ sys.exit(1)
454
+ # Extract cDNA sequences with gffread
455
+ out_dir = args.output_dir if args.output_dir else Path('.')
456
+ out_dir.mkdir(parents=True, exist_ok=True)
457
+ cdna_fasta = out_dir / (args.gtf.stem + ".cDNA.fasta")
458
+ gffread_cmd = ['gffread', str(args.gtf), '-g', str(args.genome), '-w', str(cdna_fasta)]
459
+ print(f"Extracting cDNA sequences: {' '.join(gffread_cmd)}", file=sys.stderr)
460
+ result = subprocess.run(gffread_cmd)
461
+ if result.returncode != 0:
462
+ print(f"Error: gffread failed with return code {result.returncode}", file=sys.stderr)
463
+ sys.exit(result.returncode)
464
+ args.transcripts = cdna_fasta
465
+ elif args.transcripts is None:
466
+ print("Error: provide -t/--transcripts or both --genome and --gtf", file=sys.stderr)
467
+ sys.exit(1)
468
+
469
+ if not args.transcripts.exists():
470
+ print(f"Error: Transcripts file not found: {args.transcripts}", file=sys.stderr)
471
+ sys.exit(1)
472
+
473
+ if args.blast_search_pep and not args.blast_search_pep.exists():
474
+ print(f"Error: Protein FASTA not found: {args.blast_search_pep}", file=sys.stderr)
475
+ sys.exit(1)
476
+
477
+ if args.pfam_search_db and not args.pfam_search_db.exists():
478
+ print(f"Error: Pfam search database not found: {args.pfam_search_db}", file=sys.stderr)
479
+ sys.exit(1)
480
+
481
+ if args.gene_trans_map and not args.gene_trans_map.exists():
482
+ print(f"Error: Gene-transcript map file not found: {args.gene_trans_map}", file=sys.stderr)
483
+ sys.exit(1)
484
+
485
+ # Phase 1: LongOrfs
486
+ try:
487
+ run_longorfs(
488
+ transcripts_file=args.transcripts,
489
+ min_protein_length=args.min_protein_length,
490
+ genetic_code=args.genetic_code,
491
+ strand_specific=args.strand_specific,
492
+ output_dir=args.output_dir,
493
+ gene_trans_map_file=args.gene_trans_map,
494
+ complete_orfs_only=args.complete_orfs_only,
495
+ verbose=args.verbose
496
+ )
497
+ except Exception as e:
498
+ print(f"Error during LongOrfs phase: {e}", file=sys.stderr)
499
+ if args.verbose:
500
+ import traceback
501
+ traceback.print_exc()
502
+ sys.exit(1)
503
+
504
+ # Derive workdir path (mirrors longorfs.py logic)
505
+ output_dir = args.output_dir if args.output_dir else Path('.')
506
+ workdir = output_dir / f"{args.transcripts.name}.transdecoder_dir"
507
+
508
+ # Phase 1.5: Optional homology search (diamond or blastp)
509
+ retain_blastp_hits = None
510
+ if args.blast_search_pep:
511
+ pep_file = workdir / "longest_orfs.pep"
512
+ blast_out = workdir / "blastp.outfmt6"
513
+
514
+ # Build the database
515
+ if args.blast_tool == 'diamond':
516
+ db_path = workdir / "blast_db"
517
+ makedb_cmd = [
518
+ 'diamond', 'makedb',
519
+ '--in', str(args.blast_search_pep),
520
+ '-d', str(db_path),
521
+ '-p', str(args.blast_threads)
522
+ ]
523
+ else:
524
+ db_path = workdir / "blast_db"
525
+ makedb_cmd = [
526
+ 'makeblastdb',
527
+ '-in', str(args.blast_search_pep),
528
+ '-dbtype', 'prot',
529
+ '-out', str(db_path)
530
+ ]
531
+ print(f"Building {args.blast_tool} database: {' '.join(makedb_cmd)}", file=sys.stderr)
532
+ result = subprocess.run(makedb_cmd)
533
+ if result.returncode != 0:
534
+ print(f"Error: database build failed with return code {result.returncode}", file=sys.stderr)
535
+ sys.exit(result.returncode)
536
+
537
+ # Run the search
538
+ if args.blast_tool == 'diamond':
539
+ blast_cmd = [
540
+ 'diamond', 'blastp',
541
+ '-q', str(pep_file),
542
+ '-d', str(db_path),
543
+ '-k', '1',
544
+ '-f', '6',
545
+ '-e', str(args.blast_evalue),
546
+ '-p', str(args.blast_threads),
547
+ '-o', str(blast_out)
548
+ ]
549
+ else:
550
+ blast_cmd = [
551
+ 'blastp',
552
+ '-query', str(pep_file),
553
+ '-db', str(db_path),
554
+ '-max_target_seqs', '1',
555
+ '-outfmt', '6',
556
+ '-evalue', str(args.blast_evalue),
557
+ '-num_threads', str(args.blast_threads),
558
+ '-out', str(blast_out)
559
+ ]
560
+ print(f"Running homology search ({args.blast_tool}): {' '.join(blast_cmd)}", file=sys.stderr)
561
+ result = subprocess.run(blast_cmd)
562
+ if result.returncode != 0:
563
+ print(f"Error: {args.blast_tool} failed with return code {result.returncode}", file=sys.stderr)
564
+ sys.exit(result.returncode)
565
+ retain_blastp_hits = str(blast_out)
566
+
567
+ retain_pfam_hits = None
568
+ if args.pfam_search_db:
569
+ pep_file = workdir / "longest_orfs.pep"
570
+ pfam_out = workdir / "pfam.domtblout"
571
+ pfam_db = args.pfam_search_db
572
+ hmmpress_outputs = [Path(str(pfam_db) + ext) for ext in ('.h3f', '.h3i', '.h3m', '.h3p')]
573
+
574
+ if not all(path.exists() for path in hmmpress_outputs):
575
+ hmmpress_cmd = ['hmmpress', '-f', str(pfam_db)]
576
+ print(f"Preparing Pfam database: {' '.join(hmmpress_cmd)}", file=sys.stderr)
577
+ result = subprocess.run(hmmpress_cmd)
578
+ if result.returncode != 0:
579
+ print(f"Error: hmmpress failed with return code {result.returncode}", file=sys.stderr)
580
+ sys.exit(result.returncode)
581
+
582
+ hmmsearch_cmd = ['hmmsearch', '--domtblout', str(pfam_out), str(pfam_db), str(pep_file)]
583
+ print(f"Running Pfam search: {' '.join(hmmsearch_cmd)}", file=sys.stderr)
584
+ result = subprocess.run(hmmsearch_cmd)
585
+ if result.returncode != 0:
586
+ print(f"Error: hmmsearch failed with return code {result.returncode}", file=sys.stderr)
587
+ sys.exit(result.returncode)
588
+ retain_pfam_hits = str(pfam_out)
589
+
590
+ # Phase 2: Predict
591
+ try:
592
+ predictor = TransDecoderPredict(
593
+ transcripts_file=str(args.transcripts),
594
+ output_dir=str(args.output_dir) if args.output_dir else None,
595
+ top_orfs_train=args.top_orfs_train,
596
+ retain_long_orfs_mode=args.retain_long_orfs_mode,
597
+ retain_long_orfs_length=args.retain_long_orfs_length,
598
+ retain_pfam_hits=retain_pfam_hits,
599
+ retain_blastp_hits=retain_blastp_hits,
600
+ single_best_only=args.single_best_only,
601
+ no_refine_starts=args.no_refine_starts,
602
+ genetic_code=args.genetic_code,
603
+ start_contrast_alpha=args.start_contrast_alpha,
604
+ start_kozak_weight=args.start_kozak_weight,
605
+ decoy_q=_qvalue_cutoff(args),
606
+ decoy_shuffles=args.decoy_shuffles,
607
+ decoy_seed=args.decoy_seed,
608
+ preset=args.preset,
609
+ homology_start_assist=args.homology_start_assist,
610
+ )
611
+ predictor.run()
612
+ except Exception as e:
613
+ print(f"Error during Predict phase: {e}", file=sys.stderr)
614
+ if args.verbose:
615
+ import traceback
616
+ traceback.print_exc()
617
+ sys.exit(1)
618
+
619
+ # Phase 3: Propagate ORFs to genome coordinates (with --genome/--gtf or --structures)
620
+ structures = args.gtf if genome_mode else args.structures
621
+ if structures is not None:
622
+ from .genome import run as genome_run
623
+ td_gff3 = output_dir / f"{args.transcripts.name}.transdecoder.gff3"
624
+ genome_gff3 = output_dir / f"{args.transcripts.name}.transdecoder.genome.gff3"
625
+ n_done, n_total = genome_run(td_gff3, structures, args.transcripts, genome_gff3)
626
+ print(f"{n_done} / {n_total} transcript ORFs propagated to the genome; "
627
+ f"genome-coordinate annotations written to: {genome_gff3} "
628
+ f"and {genome_gff3.with_suffix('.bed')}", file=sys.stderr)
629
+
630
+
631
+ def pipeline_main():
632
+ """Entry point for pyTransdecoder pipeline command"""
633
+ parser = create_pipeline_parser()
634
+ args = parser.parse_args()
635
+ pipeline_cmd(args)
636
+
637
+
638
+ def create_genome_map_parser(subparsers):
639
+ """Create parser for genome-map command"""
640
+ parser = subparsers.add_parser(
641
+ 'genome-map',
642
+ help='Propagate predicted ORFs to the genome through transcript structures',
643
+ description="""Propagate ORFs predicted on transcripts to genome coordinates, writing gene / mRNA /
644
+ exon / CDS / five_prime_UTR / three_prime_UTR models (GFF3).
645
+
646
+ Transcript structures: GTF (StringTie, LRAA, Cufflinks, GENCODE ...), alignment GFF3 (GMAP, PASA;
647
+ Target=<transcript> <start> <end>) or SAM/BAM spliced alignments (minimap2; primary alignments).
648
+
649
+ Example:
650
+ pytransdecoder genome-map --orfs transcripts.fa.transdecoder.gff3 --structures transcripts.gtf \\
651
+ -t transcripts.fa -o transcripts.fa.transdecoder.genome.gff3
652
+ """,
653
+ formatter_class=argparse.RawDescriptionHelpFormatter
654
+ )
655
+ parser.add_argument('--orfs', required=True, type=Path,
656
+ help='TransDecoder transcript-level GFF3 (<transcripts>.transdecoder.gff3)')
657
+ parser.add_argument('--structures', required=True, type=Path,
658
+ help='transcript genome structures: .gtf, alignment .gff3, .sam or .bam')
659
+ parser.add_argument('--format', choices=['gtf', 'gff3', 'sam'], default=None,
660
+ help='structures format (default: from the file extension)')
661
+ parser.add_argument('-t', '--transcripts', required=True, type=Path, help='transcripts FASTA')
662
+ parser.add_argument('--unstranded-as', choices=['plus', 'minus'], default='plus', dest='unstranded_as',
663
+ help="strand of GTF transcripts with strand '.' (the strand their sequence was "
664
+ "extracted from): plus for gffread (default), minus for TransDecoder's "
665
+ "util/gtf_genome_to_cdna_fasta.pl")
666
+ parser.add_argument('-o', '--output', type=Path, default=None,
667
+ help='output GFF3 (default: <orfs without .gff3>.genome.gff3); a BED12 file is written '
668
+ 'next to it (.gff3 replaced by .bed)')
669
+ parser.add_argument('-v', '--verbose', action='store_true', help='Verbose output')
670
+ parser.set_defaults(func=genome_map_cmd)
671
+ return parser
672
+
673
+
674
+ def genome_map_cmd(args):
675
+ """Execute genome-map command"""
676
+ import logging
677
+ from .genome import run as genome_run
678
+ logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO,
679
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
680
+ for f in (args.orfs, args.structures, args.transcripts):
681
+ if not f.exists():
682
+ print(f"Error: file not found: {f}", file=sys.stderr)
683
+ sys.exit(1)
684
+ out = args.output or Path(str(args.orfs).removesuffix('.gff3') + '.genome.gff3')
685
+ n_done, n_total = genome_run(args.orfs, args.structures, args.transcripts, out, args.format,
686
+ unstranded_as='+' if args.unstranded_as == 'plus' else '-')
687
+ print(f"{n_done} / {n_total} transcript ORFs propagated to the genome: {out} "
688
+ f"(BED: {str(out).removesuffix('.gff3') + '.bed'})", file=sys.stderr)
689
+
690
+
691
+ def create_build_preset_parser(subparsers):
692
+ """Create parser for build-preset command"""
693
+ parser = subparsers.add_parser(
694
+ 'build-preset',
695
+ help='Build a preset (coding model, start model, q-value thresholds) from a Predict run on a '
696
+ 'large reference transcriptome, ideally complete transcripts plus fragments of them')
697
+ parser.add_argument('-t', '--transcripts', required=True, type=Path,
698
+ help='the reference transcripts FASTA the run was made on')
699
+ parser.add_argument('--run-dir', type=Path, default=None,
700
+ help='directory holding <transcripts>.transdecoder_dir (default: current directory)')
701
+ parser.add_argument('--name', required=True, help='preset name')
702
+ parser.add_argument('--source', default='', help='free-text source description (species, release)')
703
+ parser.add_argument('-o', '--output', required=True, type=Path, help='preset directory to write')
704
+ parser.set_defaults(func=build_preset_cmd)
705
+ return parser
706
+
707
+
708
+ def build_preset_cmd(args):
709
+ """Execute build-preset command"""
710
+ import json
711
+ from .core.presets import build_preset
712
+ from .predict import TransDecoderPredict
713
+ run_dir = args.run_dir or Path.cwd()
714
+ workdir = run_dir / f"{args.transcripts.name}.transdecoder_dir"
715
+ params = json.loads((workdir / "longorfs.params.json").read_text())
716
+ pred = TransDecoderPredict(transcripts_file=str(args.transcripts), output_dir=str(run_dir),
717
+ genetic_code=params["genetic_code"])
718
+ auto = pred._min_length_auto_accept()
719
+ gc = 0.0
720
+ with open(args.transcripts) as fh:
721
+ from .core.fasta import read_fasta
722
+ n_gc = n = 0
723
+ for r in read_fasta(fh):
724
+ s = str(r.seq).upper()
725
+ n_gc += s.count('G') + s.count('C')
726
+ n += sum(s.count(b) for b in 'ACGT')
727
+ gc = 100.0 * n_gc / n if n else 50.0
728
+ strata = 'len_tx_type'
729
+ for line in open(workdir / "decoy_qvalues.tsv"):
730
+ if not line.startswith('#'):
731
+ strata = {1: 'len', 2: 'len_tx', 3: 'len_tx_type'}[len(line.split('\t')[1].split(','))]
732
+ break
733
+ out = build_preset(workdir, args.output, args.name, gc, auto, params["genetic_code"], strata, args.source)
734
+ print(f"preset written: {out}", file=sys.stderr)
735
+
736
+
737
+ def main():
738
+ """Main entry point for CLI"""
739
+ parser = argparse.ArgumentParser(
740
+ prog='pytransdecoder',
741
+ description='PyTransDecoder: Identify candidate coding regions within transcript sequences',
742
+ epilog='Python port of TransDecoder (https://github.com/TransDecoder/TransDecoder)'
743
+ )
744
+
745
+ parser.add_argument(
746
+ '--version',
747
+ action='version',
748
+ version=f'%(prog)s {__version__}'
749
+ )
750
+
751
+ subparsers = parser.add_subparsers(
752
+ title='commands',
753
+ description='Valid commands',
754
+ help='Use "pytransdecoder <command> -h" for command-specific help',
755
+ dest='command'
756
+ )
757
+
758
+ # Create subcommands
759
+ create_longorfs_parser(subparsers)
760
+ create_predict_parser(subparsers)
761
+ create_genome_map_parser(subparsers)
762
+ create_build_preset_parser(subparsers)
763
+
764
+ # Parse arguments
765
+ args = parser.parse_args()
766
+
767
+ # Execute command
768
+ if hasattr(args, 'func'):
769
+ args.func(args)
770
+ else:
771
+ parser.print_help()
772
+ sys.exit(1)
773
+
774
+
775
+ def cli():
776
+ """Alias for main() to maintain compatibility"""
777
+ main()
778
+
779
+
780
+ if __name__ == '__main__':
781
+ main()