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.
- pytransdecoder/__init__.py +7 -0
- pytransdecoder/__main__.py +6 -0
- pytransdecoder/cli.py +781 -0
- pytransdecoder/core/__init__.py +1 -0
- pytransdecoder/core/base_freqs.py +71 -0
- pytransdecoder/core/decoys.py +113 -0
- pytransdecoder/core/fasta.py +55 -0
- pytransdecoder/core/gene_model.py +143 -0
- pytransdecoder/core/gff3_parser.py +58 -0
- pytransdecoder/core/homology_starts.py +54 -0
- pytransdecoder/core/kozak.py +180 -0
- pytransdecoder/core/models.py +123 -0
- pytransdecoder/core/orf_finder.py +317 -0
- pytransdecoder/core/presets.py +152 -0
- pytransdecoder/core/prng.py +54 -0
- pytransdecoder/core/pwm.py +174 -0
- pytransdecoder/core/sequence.py +19 -0
- pytransdecoder/core/translator.py +143 -0
- pytransdecoder/core/writers.py +69 -0
- pytransdecoder/genome.py +338 -0
- pytransdecoder/longorfs.py +180 -0
- pytransdecoder/predict.py +1621 -0
- pytransdecoder/presets/apicomplexan/hexamer.scores.gz +0 -0
- pytransdecoder/presets/apicomplexan/kozak.tsv +9 -0
- pytransdecoder/presets/apicomplexan/preset.json +20 -0
- pytransdecoder/presets/apicomplexan/thresholds.tsv.gz +0 -0
- pytransdecoder/presets/apicomplexan/training_orfs.cds.gz +0 -0
- pytransdecoder/presets/ciliate/hexamer.scores.gz +0 -0
- pytransdecoder/presets/ciliate/kozak.tsv +9 -0
- pytransdecoder/presets/ciliate/preset.json +20 -0
- pytransdecoder/presets/ciliate/thresholds.tsv.gz +0 -0
- pytransdecoder/presets/ciliate/training_orfs.cds.gz +0 -0
- pytransdecoder/presets/fungus/hexamer.scores.gz +0 -0
- pytransdecoder/presets/fungus/kozak.tsv +9 -0
- pytransdecoder/presets/fungus/preset.json +20 -0
- pytransdecoder/presets/fungus/thresholds.tsv.gz +0 -0
- pytransdecoder/presets/fungus/training_orfs.cds.gz +0 -0
- pytransdecoder/presets/green_alga/hexamer.scores.gz +0 -0
- pytransdecoder/presets/green_alga/kozak.tsv +9 -0
- pytransdecoder/presets/green_alga/preset.json +20 -0
- pytransdecoder/presets/green_alga/thresholds.tsv.gz +0 -0
- pytransdecoder/presets/green_alga/training_orfs.cds.gz +0 -0
- pytransdecoder/presets/insect/hexamer.scores.gz +0 -0
- pytransdecoder/presets/insect/kozak.tsv +9 -0
- pytransdecoder/presets/insect/preset.json +20 -0
- pytransdecoder/presets/insect/thresholds.tsv.gz +0 -0
- pytransdecoder/presets/insect/training_orfs.cds.gz +0 -0
- pytransdecoder/presets/mammal/hexamer.scores.gz +0 -0
- pytransdecoder/presets/mammal/kozak.tsv +9 -0
- pytransdecoder/presets/mammal/preset.json +20 -0
- pytransdecoder/presets/mammal/thresholds.tsv.gz +0 -0
- pytransdecoder/presets/mammal/training_orfs.cds.gz +0 -0
- pytransdecoder/presets/plant/hexamer.scores.gz +0 -0
- pytransdecoder/presets/plant/kozak.tsv +9 -0
- pytransdecoder/presets/plant/preset.json +20 -0
- pytransdecoder/presets/plant/thresholds.tsv.gz +0 -0
- pytransdecoder/presets/plant/training_orfs.cds.gz +0 -0
- pytransdecoder-0.2.1.dist-info/METADATA +307 -0
- pytransdecoder-0.2.1.dist-info/RECORD +63 -0
- pytransdecoder-0.2.1.dist-info/WHEEL +5 -0
- pytransdecoder-0.2.1.dist-info/entry_points.txt +3 -0
- pytransdecoder-0.2.1.dist-info/licenses/LICENSE +28 -0
- 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()
|