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/genome.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Propagate ORFs predicted on transcript sequences to genome coordinates through the
|
|
3
|
+
transcripts' genome structures, producing gene / mRNA / exon / CDS / UTR models.
|
|
4
|
+
|
|
5
|
+
Transcript structures can come from
|
|
6
|
+
- GTF (exon features with gene_id / transcript_id: StringTie, LRAA, Cufflinks, GENCODE ...),
|
|
7
|
+
- alignment GFF3 (cDNA_match / match lines with Target=<transcript> <start> <end>: GMAP,
|
|
8
|
+
PASA, util/gtf_to_alignment_gff3.pl),
|
|
9
|
+
- SAM / BAM spliced alignments (minimap2 -ax splice; primary alignments; BAM read via samtools).
|
|
10
|
+
|
|
11
|
+
For GTF and alignment GFF3 the result is the same as Perl TransDecoder's
|
|
12
|
+
util/cdna_alignment_orf_to_genome_orf.pl, except that each mRNA is named by its own ORF.
|
|
13
|
+
Transcripts of unknown strand ('.', e.g. StringTie single-exon transcripts) are taken to be
|
|
14
|
+
on the strand their sequence was extracted from: '+' for gffread (default), '-' for
|
|
15
|
+
TransDecoder's util/gtf_genome_to_cdna_fasta.pl (unstranded_as='-'). Alignment blocks carry transcript coordinates, so
|
|
16
|
+
transcripts with indels or clipped ends are mapped base-accurately; an ORF whose CDS is not
|
|
17
|
+
entirely within the aligned part of its transcript is not propagated.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
import re
|
|
24
|
+
import subprocess
|
|
25
|
+
from collections import OrderedDict, defaultdict
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Dict, Iterable, List, Optional, Tuple
|
|
29
|
+
|
|
30
|
+
from pytransdecoder.core.fasta import read_fasta
|
|
31
|
+
from pytransdecoder.core.gene_model import CodingGene, CodingModel
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
# (genome lend, genome rend, transcript start, transcript end); transcript coordinates run in
|
|
36
|
+
# the transcribed direction: on '-' structures they decrease with genome position
|
|
37
|
+
Block = Tuple[int, int, int, int]
|
|
38
|
+
|
|
39
|
+
# alignment blocks separated by at most this many genome bases (an alignment deletion) belong
|
|
40
|
+
# to the same exon; larger gaps are introns
|
|
41
|
+
MAX_INDEL = 20
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class TranscriptStructure:
|
|
46
|
+
transcript_id: str
|
|
47
|
+
seqid: str
|
|
48
|
+
strand: str # orientation of the transcript sequence on the genome
|
|
49
|
+
blocks: List[Block] = field(default_factory=list)
|
|
50
|
+
gene_id: str = ""
|
|
51
|
+
exons: List[Tuple[int, int]] = field(default_factory=list) # merged across indels
|
|
52
|
+
|
|
53
|
+
def finalize(self, transcript_length: Optional[int] = None) -> None:
|
|
54
|
+
"""Order blocks; assign transcript coordinates when the source has none (GTF)."""
|
|
55
|
+
self.blocks.sort()
|
|
56
|
+
if self.blocks and self.blocks[0][2] == 0:
|
|
57
|
+
# contiguous transcript: cumulative exon lengths in the transcribed direction
|
|
58
|
+
ordered = self.blocks if self.strand != "-" else self.blocks[::-1]
|
|
59
|
+
pos, blocks = 1, []
|
|
60
|
+
for g0, g1, _, _ in ordered:
|
|
61
|
+
n = g1 - g0 + 1
|
|
62
|
+
blocks.append((g0, g1, pos, pos + n - 1) if self.strand != "-" else (g0, g1, pos + n - 1, pos))
|
|
63
|
+
pos += n
|
|
64
|
+
self.blocks = sorted(blocks)
|
|
65
|
+
if not self.exons:
|
|
66
|
+
self.exons = _merge(((g0, g1) for g0, g1, _, _ in self.blocks), max_gap=MAX_INDEL)
|
|
67
|
+
self.exons = _merge(self.exons)
|
|
68
|
+
|
|
69
|
+
def to_genome(self, tpos: int) -> Optional[int]:
|
|
70
|
+
"""Genome position of transcript position tpos (1-based), None if not aligned."""
|
|
71
|
+
for g0, g1, t0, t1 in self.blocks:
|
|
72
|
+
lo, hi = min(t0, t1), max(t0, t1)
|
|
73
|
+
if lo <= tpos <= hi:
|
|
74
|
+
return g0 + (tpos - t0) if t1 >= t0 else g0 + (t0 - tpos)
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _merge(segments: Iterable[Tuple[int, int]], max_gap: int = 0) -> List[Tuple[int, int]]:
|
|
79
|
+
merged: List[List[int]] = []
|
|
80
|
+
for a, b in sorted(segments):
|
|
81
|
+
if merged and a <= merged[-1][1] + 1 + max_gap:
|
|
82
|
+
merged[-1][1] = max(merged[-1][1], b)
|
|
83
|
+
else:
|
|
84
|
+
merged.append([a, b])
|
|
85
|
+
return [(a, b) for a, b in merged]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# --------------------------------------------------------------------------------------
|
|
89
|
+
# readers
|
|
90
|
+
# --------------------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
def _gtf_attrs(text: str) -> Dict[str, str]:
|
|
93
|
+
attrs = {}
|
|
94
|
+
for part in text.strip().split(";"):
|
|
95
|
+
part = part.strip()
|
|
96
|
+
if not part:
|
|
97
|
+
continue
|
|
98
|
+
key, _, val = part.partition(" ")
|
|
99
|
+
attrs.setdefault(key, val.strip().strip('"'))
|
|
100
|
+
return attrs
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def read_gtf(path: Path, unstranded_as: str = "+") -> Dict[str, TranscriptStructure]:
|
|
104
|
+
structs: Dict[str, TranscriptStructure] = OrderedDict()
|
|
105
|
+
with open(path) as fh:
|
|
106
|
+
for line in fh:
|
|
107
|
+
if line.startswith("#"):
|
|
108
|
+
continue
|
|
109
|
+
x = line.rstrip("\n").split("\t")
|
|
110
|
+
if len(x) < 9 or x[2] != "exon":
|
|
111
|
+
continue
|
|
112
|
+
a = _gtf_attrs(x[8])
|
|
113
|
+
tid = a.get("transcript_id")
|
|
114
|
+
if not tid:
|
|
115
|
+
raise ValueError(f"GTF exon without transcript_id: {line.strip()}")
|
|
116
|
+
s = structs.get(tid)
|
|
117
|
+
if s is None:
|
|
118
|
+
strand = x[6] if x[6] in ("+", "-") else unstranded_as
|
|
119
|
+
s = structs[tid] = TranscriptStructure(tid, x[0], strand, gene_id=a.get("gene_id", ""))
|
|
120
|
+
s.blocks.append((int(x[3]), int(x[4]), 0, 0))
|
|
121
|
+
return structs
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def read_alignment_gff3(path: Path) -> Dict[str, TranscriptStructure]:
|
|
125
|
+
structs: Dict[str, TranscriptStructure] = OrderedDict()
|
|
126
|
+
target_re = re.compile(r"Target=([^;\s]+)(?:\s+(\d+)\s+(\d+))?")
|
|
127
|
+
with open(path) as fh:
|
|
128
|
+
for line in fh:
|
|
129
|
+
if line.startswith("#") or not line.strip():
|
|
130
|
+
continue
|
|
131
|
+
x = line.rstrip("\n").split("\t")
|
|
132
|
+
if len(x) < 9:
|
|
133
|
+
continue
|
|
134
|
+
m = target_re.search(x[8])
|
|
135
|
+
if not m:
|
|
136
|
+
raise ValueError(f"alignment GFF3 line without Target=: {line.strip()}")
|
|
137
|
+
tid, gene_id = m.group(1), ""
|
|
138
|
+
gt = re.match(r"GENE\^(\S+),TRANS\^(\S+)", tid) # util/gtf_to_alignment_gff3.pl
|
|
139
|
+
if gt:
|
|
140
|
+
gene_id, tid = gt.group(1), gt.group(2)
|
|
141
|
+
s = structs.get(tid)
|
|
142
|
+
if s is None:
|
|
143
|
+
s = structs[tid] = TranscriptStructure(tid, x[0], x[6], gene_id=gene_id)
|
|
144
|
+
g0, g1 = int(x[3]), int(x[4])
|
|
145
|
+
if m.group(2):
|
|
146
|
+
t0, t1 = int(m.group(2)), int(m.group(3))
|
|
147
|
+
t0, t1 = (min(t0, t1), max(t0, t1)) if x[6] != "-" else (max(t0, t1), min(t0, t1))
|
|
148
|
+
s.blocks.append((g0, g1, t0, t1))
|
|
149
|
+
else:
|
|
150
|
+
s.blocks.append((g0, g1, 0, 0))
|
|
151
|
+
return structs
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
_CIGAR_RE = re.compile(r"(\d+)([MIDNSHP=X])")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def read_sam(path: Path) -> Dict[str, TranscriptStructure]:
|
|
158
|
+
"""Primary spliced alignments; BAM input is read through `samtools view`."""
|
|
159
|
+
structs: Dict[str, TranscriptStructure] = OrderedDict()
|
|
160
|
+
if str(path).endswith(".bam"):
|
|
161
|
+
proc = subprocess.Popen(["samtools", "view", "-F", "0x904", str(path)],
|
|
162
|
+
stdout=subprocess.PIPE, text=True)
|
|
163
|
+
lines = proc.stdout
|
|
164
|
+
else:
|
|
165
|
+
proc, lines = None, open(path)
|
|
166
|
+
try:
|
|
167
|
+
for line in lines:
|
|
168
|
+
if line.startswith("@"):
|
|
169
|
+
continue
|
|
170
|
+
x = line.split("\t", 11)
|
|
171
|
+
flag = int(x[1])
|
|
172
|
+
if flag & 0x904 or x[2] == "*":
|
|
173
|
+
continue
|
|
174
|
+
tid, seqid, gpos, cigar = x[0], x[2], int(x[3]), x[5]
|
|
175
|
+
ops = [(int(n), op) for n, op in _CIGAR_RE.findall(cigar)]
|
|
176
|
+
qlen = sum(n for n, op in ops if op in "MIS=XH")
|
|
177
|
+
minus = bool(flag & 16)
|
|
178
|
+
strand = "-" if minus else "+"
|
|
179
|
+
q, g = 0, gpos
|
|
180
|
+
blocks, exons, exon_start = [], [], gpos
|
|
181
|
+
for n, op in ops:
|
|
182
|
+
if op in "M=X":
|
|
183
|
+
t0, t1 = (q + 1, q + n) if not minus else (qlen - q, qlen - q - n + 1)
|
|
184
|
+
blocks.append((g, g + n - 1, t0, t1))
|
|
185
|
+
q += n
|
|
186
|
+
g += n
|
|
187
|
+
elif op in "ISH":
|
|
188
|
+
q += n
|
|
189
|
+
elif op == "D":
|
|
190
|
+
g += n
|
|
191
|
+
elif op == "N":
|
|
192
|
+
exons.append((exon_start, g - 1))
|
|
193
|
+
g += n
|
|
194
|
+
exon_start = g
|
|
195
|
+
exons.append((exon_start, g - 1))
|
|
196
|
+
s = TranscriptStructure(tid, seqid, strand, blocks=blocks, exons=exons)
|
|
197
|
+
structs[tid] = s
|
|
198
|
+
finally:
|
|
199
|
+
if proc is not None:
|
|
200
|
+
proc.stdout.close()
|
|
201
|
+
if proc.wait() != 0:
|
|
202
|
+
raise RuntimeError(f"samtools view {path} failed")
|
|
203
|
+
else:
|
|
204
|
+
lines.close()
|
|
205
|
+
return structs
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def read_structures(path: Path, fmt: Optional[str] = None,
|
|
209
|
+
unstranded_as: str = "+") -> Dict[str, TranscriptStructure]:
|
|
210
|
+
path = Path(path)
|
|
211
|
+
if fmt is None:
|
|
212
|
+
name = path.name.lower()
|
|
213
|
+
if name.endswith((".bam", ".sam")):
|
|
214
|
+
fmt = "sam"
|
|
215
|
+
elif name.endswith(".gtf"):
|
|
216
|
+
fmt = "gtf"
|
|
217
|
+
else:
|
|
218
|
+
fmt = "gff3"
|
|
219
|
+
structs = read_gtf(path, unstranded_as) if fmt == "gtf" else \
|
|
220
|
+
{"gff3": read_alignment_gff3, "sam": read_sam}[fmt](path)
|
|
221
|
+
for s in structs.values():
|
|
222
|
+
s.finalize()
|
|
223
|
+
return structs
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@dataclass
|
|
227
|
+
class TranscriptOrf:
|
|
228
|
+
transcript_id: str
|
|
229
|
+
model_id: str
|
|
230
|
+
gene_id: str
|
|
231
|
+
name: str
|
|
232
|
+
strand: str
|
|
233
|
+
cds_lend: int
|
|
234
|
+
cds_rend: int
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def read_orfs(td_gff3: Path) -> "OrderedDict[str, List[TranscriptOrf]]":
|
|
238
|
+
"""ORFs of a TransDecoder transcript-level GFF3 (Perl or PyTransDecoder), by transcript."""
|
|
239
|
+
gene_names: Dict[str, str] = {}
|
|
240
|
+
mrnas: "OrderedDict[str, dict]" = OrderedDict()
|
|
241
|
+
cds: Dict[str, List[int]] = defaultdict(list)
|
|
242
|
+
with open(td_gff3) as fh:
|
|
243
|
+
for line in fh:
|
|
244
|
+
x = line.rstrip("\n").split("\t")
|
|
245
|
+
if line.startswith("#") or len(x) != 9:
|
|
246
|
+
continue
|
|
247
|
+
attrs = dict(kv.split("=", 1) for kv in x[8].split(";") if "=" in kv)
|
|
248
|
+
if x[2] == "gene":
|
|
249
|
+
gene_names[attrs["ID"]] = attrs.get("Name", "").strip('"')
|
|
250
|
+
elif x[2] == "mRNA":
|
|
251
|
+
mrnas[attrs["ID"]] = {"seqid": x[0], "gene": attrs.get("Parent", ""), "strand": x[6]}
|
|
252
|
+
elif x[2] == "CDS":
|
|
253
|
+
cds[attrs["Parent"]] += [int(x[3]), int(x[4])]
|
|
254
|
+
orfs: "OrderedDict[str, List[TranscriptOrf]]" = OrderedDict()
|
|
255
|
+
for model_id, m in mrnas.items():
|
|
256
|
+
if not cds.get(model_id):
|
|
257
|
+
continue
|
|
258
|
+
orf = TranscriptOrf(m["seqid"], model_id, m["gene"], gene_names.get(m["gene"], ""),
|
|
259
|
+
m["strand"], min(cds[model_id]), max(cds[model_id]))
|
|
260
|
+
orfs.setdefault(m["seqid"], []).append(orf)
|
|
261
|
+
return orfs
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def read_fasta_lengths(path: Path) -> Dict[str, int]:
|
|
265
|
+
return {record.id: len(record.seq) for record in read_fasta(path)}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# --------------------------------------------------------------------------------------
|
|
269
|
+
# propagation
|
|
270
|
+
# --------------------------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
def place_orf(orf: TranscriptOrf, struct: TranscriptStructure, transcript_length: int) -> Optional[CodingModel]:
|
|
273
|
+
"""Genome model of one ORF, or None (logged) if it cannot be placed."""
|
|
274
|
+
cds_lend, cds_rend = orf.cds_lend, min(orf.cds_rend, transcript_length)
|
|
275
|
+
tstrand = struct.strand if struct.strand in "+-" else "+"
|
|
276
|
+
if orf.strand == "+":
|
|
277
|
+
strand = tstrand
|
|
278
|
+
else:
|
|
279
|
+
if len(struct.exons) > 1:
|
|
280
|
+
logger.warning("%s: ORF on the opposite strand of spliced transcript %s; skipped",
|
|
281
|
+
orf.model_id, struct.transcript_id)
|
|
282
|
+
return None
|
|
283
|
+
strand = "-" if tstrand == "+" else "+" # single exon: orient by the ORF
|
|
284
|
+
g_a, g_b = struct.to_genome(cds_lend), struct.to_genome(cds_rend)
|
|
285
|
+
if g_a is None or g_b is None:
|
|
286
|
+
logger.warning("%s: CDS %d-%d of %s is not entirely in its aligned part; skipped",
|
|
287
|
+
orf.model_id, cds_lend, cds_rend, struct.transcript_id)
|
|
288
|
+
return None
|
|
289
|
+
return CodingModel(seqid=struct.seqid, strand=strand, exons=list(struct.exons),
|
|
290
|
+
cds_lend=min(g_a, g_b), cds_rend=max(g_a, g_b), model_id=orf.model_id, name=orf.name)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def propagate(orfs_by_transcript, structures: Dict[str, TranscriptStructure],
|
|
294
|
+
transcript_lengths: Dict[str, int]) -> Tuple[List[CodingGene], int, int]:
|
|
295
|
+
"""Returns (genes sorted by id, number propagated, total ORFs)."""
|
|
296
|
+
genes: Dict[str, CodingGene] = {}
|
|
297
|
+
n_done = n_total = 0
|
|
298
|
+
for tid in sorted(orfs_by_transcript):
|
|
299
|
+
for orf in orfs_by_transcript[tid]:
|
|
300
|
+
n_total += 1
|
|
301
|
+
struct = structures.get(tid)
|
|
302
|
+
if struct is None:
|
|
303
|
+
logger.warning("%s has no genome structure; skipped", tid)
|
|
304
|
+
continue
|
|
305
|
+
if tid not in transcript_lengths:
|
|
306
|
+
raise ValueError(f"no sequence length for {tid}: provide the transcript FASTA")
|
|
307
|
+
model = place_orf(orf, struct, transcript_lengths[tid])
|
|
308
|
+
if model is None:
|
|
309
|
+
continue
|
|
310
|
+
base_gene = struct.gene_id or re.split(r"~~|::", orf.gene_id)[0]
|
|
311
|
+
# gene ids can repeat at different loci or strands
|
|
312
|
+
gene_key = f"{base_gene}^{model.seqid}^{model.strand}"
|
|
313
|
+
genes.setdefault(gene_key, CodingGene(gene_key)).models.append(model)
|
|
314
|
+
n_done += 1
|
|
315
|
+
return [genes[k] for k in sorted(genes)], n_done, n_total
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def run(td_gff3: Path, structures_file: Path, transcripts_fasta: Path, out_gff3: Path,
|
|
319
|
+
structures_format: Optional[str] = None, source: str = "transdecoder",
|
|
320
|
+
unstranded_as: str = "+", out_bed: Optional[Path] = None) -> Tuple[int, int]:
|
|
321
|
+
"""Propagate transcript ORFs to the genome; writes GFF3 and BED12 (default: the GFF3 path
|
|
322
|
+
with .gff3 replaced by .bed, as Perl TransDecoder's genome mode)."""
|
|
323
|
+
structures = read_structures(structures_file, structures_format, unstranded_as)
|
|
324
|
+
genes, n_done, n_total = propagate(read_orfs(td_gff3), structures, read_fasta_lengths(transcripts_fasta))
|
|
325
|
+
with open(out_gff3, "w") as out:
|
|
326
|
+
for gene in genes:
|
|
327
|
+
out.write(gene.gff3_text(source))
|
|
328
|
+
if out_bed is None:
|
|
329
|
+
out_bed = Path(str(out_gff3).removesuffix(".gff3") + ".bed")
|
|
330
|
+
with open(out_bed, "w") as out:
|
|
331
|
+
for gene in genes:
|
|
332
|
+
# name as Perl's Gene_obj::to_BED_format: ID=<transcript>;<gene>;<gene Name>
|
|
333
|
+
gene_name = (gene.name or gene.models[0].name).strip("'\"")
|
|
334
|
+
for model in gene.models:
|
|
335
|
+
name = f"ID={model.model_id};{gene.gene_id};{gene_name or model.model_id}"
|
|
336
|
+
out.write(model.bed12(name.replace(" ", "_")) + "\n")
|
|
337
|
+
logger.info("%d / %d transcript ORFs propagated to the genome", n_done, n_total)
|
|
338
|
+
return n_done, n_total
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TransDecoder.LongOrfs - Extract long ORFs from transcript sequences
|
|
3
|
+
|
|
4
|
+
Phase 1 of TransDecoder pipeline.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional, Dict
|
|
10
|
+
from collections import defaultdict
|
|
11
|
+
from .core.fasta import read_fasta
|
|
12
|
+
from tqdm import tqdm
|
|
13
|
+
|
|
14
|
+
from .core.orf_finder import ORFFinder
|
|
15
|
+
from .core.models import ORF
|
|
16
|
+
from .core.writers import GFF3Writer, FastaWriter
|
|
17
|
+
from .core.base_freqs import compute_base_counts, write_base_frequencies
|
|
18
|
+
|
|
19
|
+
# Minimum ORF length (aa). Predict's decoy q-values (stratified by ORF length) decide how much coding
|
|
20
|
+
# evidence short ORFs need.
|
|
21
|
+
DEFAULT_MIN_PROTEIN_LENGTH = 50
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run_longorfs(
|
|
25
|
+
transcripts_file: Path,
|
|
26
|
+
min_protein_length: int = DEFAULT_MIN_PROTEIN_LENGTH,
|
|
27
|
+
genetic_code: str = "universal",
|
|
28
|
+
strand_specific: bool = False,
|
|
29
|
+
output_dir: Optional[Path] = None,
|
|
30
|
+
gene_trans_map_file: Optional[Path] = None,
|
|
31
|
+
complete_orfs_only: bool = False,
|
|
32
|
+
verbose: bool = False
|
|
33
|
+
):
|
|
34
|
+
"""
|
|
35
|
+
Extract long ORFs from transcripts (Phase 1).
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
transcripts_file: Input FASTA file
|
|
39
|
+
min_protein_length: Minimum protein length in amino acids
|
|
40
|
+
genetic_code: Genetic code name
|
|
41
|
+
strand_specific: Only analyze top strand
|
|
42
|
+
output_dir: Output directory
|
|
43
|
+
gene_trans_map_file: Gene to transcript mapping file
|
|
44
|
+
complete_orfs_only: Only output complete ORFs
|
|
45
|
+
verbose: Verbose output
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
# Set up output directory
|
|
49
|
+
if output_dir is None:
|
|
50
|
+
output_dir = Path.cwd()
|
|
51
|
+
output_dir = Path(output_dir)
|
|
52
|
+
|
|
53
|
+
# Create transdecoder_dir
|
|
54
|
+
workdir = output_dir / f"{transcripts_file.name}.transdecoder_dir"
|
|
55
|
+
workdir.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
|
|
57
|
+
if verbose:
|
|
58
|
+
print(f"Output directory: {workdir}", file=sys.stderr)
|
|
59
|
+
|
|
60
|
+
# Load gene-transcript mapping if provided
|
|
61
|
+
gene_trans_map = {}
|
|
62
|
+
if gene_trans_map_file:
|
|
63
|
+
with open(gene_trans_map_file) as f:
|
|
64
|
+
for line in f:
|
|
65
|
+
parts = line.strip().split('\t')
|
|
66
|
+
if len(parts) >= 2:
|
|
67
|
+
gene_id, trans_id = parts[0], parts[1]
|
|
68
|
+
gene_trans_map[trans_id] = gene_id
|
|
69
|
+
|
|
70
|
+
# LongOrfs settings, for Predict's decoy ORFs (same finder settings on shuffled transcripts)
|
|
71
|
+
import json
|
|
72
|
+
with open(workdir / "longorfs.params.json", "w") as fh:
|
|
73
|
+
json.dump({"min_protein_length": min_protein_length, "genetic_code": genetic_code,
|
|
74
|
+
"strand_specific": bool(strand_specific),
|
|
75
|
+
"complete_orfs_only": bool(complete_orfs_only)}, fh, indent=1)
|
|
76
|
+
|
|
77
|
+
# Step 1: Compute base frequencies
|
|
78
|
+
if verbose:
|
|
79
|
+
print("\n\n-first extracting base frequencies, we'll need them later.\n", file=sys.stderr)
|
|
80
|
+
|
|
81
|
+
base_freqs_file = workdir / "base_freqs.dat"
|
|
82
|
+
base_counts = compute_base_counts(str(transcripts_file), strand_specific)
|
|
83
|
+
write_base_frequencies(base_counts, str(base_freqs_file))
|
|
84
|
+
|
|
85
|
+
# Step 2: Extract ORFs
|
|
86
|
+
if verbose:
|
|
87
|
+
print("\n\n- extracting ORFs from transcripts.\n", file=sys.stderr)
|
|
88
|
+
|
|
89
|
+
# Initialize ORF finder
|
|
90
|
+
orf_finder = ORFFinder(
|
|
91
|
+
min_protein_length=min_protein_length,
|
|
92
|
+
allow_5prime_partial=not complete_orfs_only,
|
|
93
|
+
allow_3prime_partial=not complete_orfs_only,
|
|
94
|
+
forward_strand=True,
|
|
95
|
+
reverse_strand=not strand_specific,
|
|
96
|
+
genetic_code=genetic_code,
|
|
97
|
+
complete_orfs_only=complete_orfs_only
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Output files
|
|
101
|
+
prefix = workdir / "longest_orfs"
|
|
102
|
+
cds_file = prefix.with_suffix('.cds')
|
|
103
|
+
gff3_file = prefix.with_suffix('.gff3')
|
|
104
|
+
pep_file = prefix.with_suffix('.pep')
|
|
105
|
+
|
|
106
|
+
# Count total transcripts for progress bar
|
|
107
|
+
total_transcripts = sum(1 for _ in read_fasta(transcripts_file))
|
|
108
|
+
|
|
109
|
+
if verbose:
|
|
110
|
+
print(f"-total transcripts to examine: {total_transcripts}\n", file=sys.stderr)
|
|
111
|
+
|
|
112
|
+
# Process transcripts
|
|
113
|
+
model_counter = 0
|
|
114
|
+
trans_counter = 0
|
|
115
|
+
seen_prot_ids = set()
|
|
116
|
+
|
|
117
|
+
with GFF3Writer(gff3_file) as gff_writer, \
|
|
118
|
+
FastaWriter(cds_file) as cds_writer, \
|
|
119
|
+
FastaWriter(pep_file) as pep_writer:
|
|
120
|
+
|
|
121
|
+
progress = tqdm(
|
|
122
|
+
read_fasta(transcripts_file),
|
|
123
|
+
total=total_transcripts,
|
|
124
|
+
desc="Processing transcripts",
|
|
125
|
+
disable=not verbose,
|
|
126
|
+
file=sys.stderr
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
for record in progress:
|
|
130
|
+
trans_counter += 1
|
|
131
|
+
|
|
132
|
+
acc = record.id
|
|
133
|
+
sequence = str(record.seq)
|
|
134
|
+
|
|
135
|
+
# Find all ORFs
|
|
136
|
+
orfs = orf_finder.find_all_orfs(sequence, acc)
|
|
137
|
+
|
|
138
|
+
# Process each ORF
|
|
139
|
+
for orf in orfs:
|
|
140
|
+
model_counter += 1
|
|
141
|
+
|
|
142
|
+
# Generate unique model ID
|
|
143
|
+
pcounter = 1
|
|
144
|
+
model_id = f"{acc}.p{pcounter}"
|
|
145
|
+
while model_id in seen_prot_ids:
|
|
146
|
+
pcounter += 1
|
|
147
|
+
model_id = f"{acc}.p{pcounter}"
|
|
148
|
+
seen_prot_ids.add(model_id)
|
|
149
|
+
|
|
150
|
+
# Generate gene ID
|
|
151
|
+
if acc in gene_trans_map:
|
|
152
|
+
gene_id = gene_trans_map[acc]
|
|
153
|
+
else:
|
|
154
|
+
gene_id = f"GENE.{acc}"
|
|
155
|
+
|
|
156
|
+
gene_id = f"{gene_id}~~{model_id}"
|
|
157
|
+
|
|
158
|
+
# Update ORF with IDs
|
|
159
|
+
orf.model_id = model_id
|
|
160
|
+
orf.gene_id = gene_id
|
|
161
|
+
orf.description = f"ORF type:{orf.orf_type} ({orf.strand})"
|
|
162
|
+
|
|
163
|
+
# Write outputs
|
|
164
|
+
gff_writer.write_orf(orf)
|
|
165
|
+
|
|
166
|
+
# Write CDS
|
|
167
|
+
cds_header = f">{model_id} type:{orf.orf_type} {acc}:{orf.start}-{orf.end}({orf.strand})"
|
|
168
|
+
cds_writer.write_sequence(cds_header, orf.sequence)
|
|
169
|
+
|
|
170
|
+
# Write protein
|
|
171
|
+
pep_header = f">{model_id} type:{orf.orf_type} gc:{genetic_code} {acc}:{orf.start}-{orf.end}({orf.strand})"
|
|
172
|
+
pep_writer.write_sequence(pep_header, orf.protein)
|
|
173
|
+
|
|
174
|
+
# Print summary
|
|
175
|
+
if verbose:
|
|
176
|
+
print(f"\n\n#################################", file=sys.stderr)
|
|
177
|
+
print(f"### Done preparing long ORFs ###", file=sys.stderr)
|
|
178
|
+
print(f"##################################", file=sys.stderr)
|
|
179
|
+
print(f"\n\tUse file: {pep_file} for Pfam and/or BlastP searches to enable homology-based coding region identification.\n\n", file=sys.stderr)
|
|
180
|
+
print(f"\tThen, run TransDecoder.Predict\n\n", file=sys.stderr)
|