pytransdecoder 0.2.1__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pytransdecoder-0.2.1/pytransdecoder.egg-info → pytransdecoder-0.3.0}/PKG-INFO +5 -1
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/README.md +3 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pyproject.toml +2 -1
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/__init__.py +1 -1
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/cli.py +5 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/core/base_freqs.py +6 -5
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/core/decoys.py +3 -5
- pytransdecoder-0.3.0/pytransdecoder/core/markov_fast.py +107 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/core/orf_finder.py +39 -21
- pytransdecoder-0.3.0/pytransdecoder/core/parallel.py +133 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/core/prng.py +24 -1
- pytransdecoder-0.3.0/pytransdecoder/core/sequence.py +32 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/core/translator.py +25 -3
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/predict.py +73 -130
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0/pytransdecoder.egg-info}/PKG-INFO +5 -1
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder.egg-info/SOURCES.txt +6 -1
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder.egg-info/requires.txt +1 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/tests/test_decoys.py +14 -0
- pytransdecoder-0.3.0/tests/test_markov_fast.py +51 -0
- pytransdecoder-0.3.0/tests/test_parallel.py +51 -0
- pytransdecoder-0.3.0/tests/test_translator_memo.py +31 -0
- pytransdecoder-0.2.1/pytransdecoder/core/sequence.py +0 -19
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/LICENSE +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/__main__.py +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/core/__init__.py +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/core/fasta.py +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/core/gene_model.py +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/core/gff3_parser.py +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/core/homology_starts.py +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/core/kozak.py +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/core/models.py +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/core/presets.py +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/core/pwm.py +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/core/writers.py +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/genome.py +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/longorfs.py +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/apicomplexan/hexamer.scores.gz +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/apicomplexan/kozak.tsv +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/apicomplexan/preset.json +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/apicomplexan/thresholds.tsv.gz +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/apicomplexan/training_orfs.cds.gz +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/ciliate/hexamer.scores.gz +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/ciliate/kozak.tsv +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/ciliate/preset.json +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/ciliate/thresholds.tsv.gz +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/ciliate/training_orfs.cds.gz +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/fungus/hexamer.scores.gz +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/fungus/kozak.tsv +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/fungus/preset.json +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/fungus/thresholds.tsv.gz +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/fungus/training_orfs.cds.gz +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/green_alga/hexamer.scores.gz +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/green_alga/kozak.tsv +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/green_alga/preset.json +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/green_alga/thresholds.tsv.gz +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/green_alga/training_orfs.cds.gz +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/insect/hexamer.scores.gz +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/insect/kozak.tsv +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/insect/preset.json +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/insect/thresholds.tsv.gz +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/insect/training_orfs.cds.gz +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/mammal/hexamer.scores.gz +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/mammal/kozak.tsv +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/mammal/preset.json +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/mammal/thresholds.tsv.gz +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/mammal/training_orfs.cds.gz +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/plant/hexamer.scores.gz +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/plant/kozak.tsv +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/plant/preset.json +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/plant/thresholds.tsv.gz +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder/presets/plant/training_orfs.cds.gz +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder.egg-info/dependency_links.txt +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder.egg-info/entry_points.txt +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/pytransdecoder.egg-info/top_level.txt +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/setup.cfg +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/setup.py +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/tests/test_basic.py +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/tests/test_fasta.py +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/tests/test_genome_bed.py +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/tests/test_homology_starts.py +0 -0
- {pytransdecoder-0.2.1 → pytransdecoder-0.3.0}/tests/test_qvalue_modes.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytransdecoder
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: TransDecoder, the original Perl version ported to Python by AI/LLMs with new features integrated
|
|
5
5
|
Author: Brian Haas, ported to python by AI/LLMs
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -10,6 +10,7 @@ Requires-Python: >=3.8
|
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
11
|
License-File: LICENSE
|
|
12
12
|
Requires-Dist: biopython>=1.81
|
|
13
|
+
Requires-Dist: numpy>=1.21
|
|
13
14
|
Requires-Dist: tqdm>=4.65
|
|
14
15
|
Provides-Extra: dev
|
|
15
16
|
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
@@ -178,6 +179,8 @@ pyTransdecoder -t transcripts.fasta --pfam-search-db Pfam-A.hmm
|
|
|
178
179
|
--decoy-seed INT Seed of the decoy shuffles (default: 42)
|
|
179
180
|
--preset NAME|DIR Models and thresholds from a preset instead of the input
|
|
180
181
|
(recommended below ~1,000 transcripts or for mostly partial sets)
|
|
182
|
+
--cpu INT Worker processes for scoring candidate and decoy ORFs
|
|
183
|
+
(default: 1); the output does not depend on it
|
|
181
184
|
-G, --genetic-code TEXT Genetic code (default: Standard)
|
|
182
185
|
-v, --verbose Verbose output
|
|
183
186
|
```
|
|
@@ -302,6 +305,7 @@ diff ../TransDecoder/test.fasta.transdecoder_dir/longest_orfs.pep \
|
|
|
302
305
|
|
|
303
306
|
- Python 3.8+
|
|
304
307
|
- BioPython >= 1.81
|
|
308
|
+
- NumPy >= 1.21
|
|
305
309
|
- tqdm >= 4.65
|
|
306
310
|
|
|
307
311
|
**Note**: Ported by Claude.io Sonnet 4.5 under guidance by bhaas. Jan 24, 2026.
|
|
@@ -158,6 +158,8 @@ pyTransdecoder -t transcripts.fasta --pfam-search-db Pfam-A.hmm
|
|
|
158
158
|
--decoy-seed INT Seed of the decoy shuffles (default: 42)
|
|
159
159
|
--preset NAME|DIR Models and thresholds from a preset instead of the input
|
|
160
160
|
(recommended below ~1,000 transcripts or for mostly partial sets)
|
|
161
|
+
--cpu INT Worker processes for scoring candidate and decoy ORFs
|
|
162
|
+
(default: 1); the output does not depend on it
|
|
161
163
|
-G, --genetic-code TEXT Genetic code (default: Standard)
|
|
162
164
|
-v, --verbose Verbose output
|
|
163
165
|
```
|
|
@@ -282,6 +284,7 @@ diff ../TransDecoder/test.fasta.transdecoder_dir/longest_orfs.pep \
|
|
|
282
284
|
|
|
283
285
|
- Python 3.8+
|
|
284
286
|
- BioPython >= 1.81
|
|
287
|
+
- NumPy >= 1.21
|
|
285
288
|
- tqdm >= 4.65
|
|
286
289
|
|
|
287
290
|
**Note**: Ported by Claude.io Sonnet 4.5 under guidance by bhaas. Jan 24, 2026.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pytransdecoder"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "TransDecoder, the original Perl version ported to Python by AI/LLMs with new features integrated"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -14,6 +14,7 @@ authors = [
|
|
|
14
14
|
]
|
|
15
15
|
dependencies = [
|
|
16
16
|
"biopython>=1.81",
|
|
17
|
+
"numpy>=1.21",
|
|
17
18
|
"tqdm>=4.65",
|
|
18
19
|
]
|
|
19
20
|
|
|
@@ -106,6 +106,9 @@ def _add_selection_args(parser):
|
|
|
106
106
|
"(~20 Mb of decoy sequence, at most 50)")
|
|
107
107
|
parser.add_argument('--decoy-seed', type=int, default=DEFAULT_DECOY_SEED, dest='decoy_seed',
|
|
108
108
|
help="Seed of the decoy shuffles (default: %(default)s)")
|
|
109
|
+
parser.add_argument('--cpu', type=int, default=1, dest='cpu',
|
|
110
|
+
help="Worker processes for scoring candidate and decoy ORFs; results do not depend "
|
|
111
|
+
"on it (default: %(default)s)")
|
|
109
112
|
from .core.presets import bundled_presets
|
|
110
113
|
parser.add_argument('--preset', default=None,
|
|
111
114
|
help="Use a preset's coding model, start model and q-value thresholds instead of "
|
|
@@ -330,6 +333,7 @@ def predict_cmd(args):
|
|
|
330
333
|
decoy_seed=args.decoy_seed,
|
|
331
334
|
preset=args.preset,
|
|
332
335
|
homology_start_assist=args.homology_start_assist,
|
|
336
|
+
cpu=args.cpu,
|
|
333
337
|
)
|
|
334
338
|
predictor.run()
|
|
335
339
|
except Exception as e:
|
|
@@ -607,6 +611,7 @@ def pipeline_cmd(args):
|
|
|
607
611
|
decoy_seed=args.decoy_seed,
|
|
608
612
|
preset=args.preset,
|
|
609
613
|
homology_start_assist=args.homology_start_assist,
|
|
614
|
+
cpu=args.cpu,
|
|
610
615
|
)
|
|
611
616
|
predictor.run()
|
|
612
617
|
except Exception as e:
|
|
@@ -19,11 +19,12 @@ def compute_base_counts(fasta_file: str, top_strand_only: bool = False) -> Dict[
|
|
|
19
19
|
complement = {'A': 'T', 'T': 'A', 'G': 'C', 'C': 'G'}
|
|
20
20
|
counts = {base: 0 for base in 'ACGT'}
|
|
21
21
|
for record in read_fasta(fasta_file):
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
seq = str(record.seq).upper()
|
|
23
|
+
for base in 'ACGT':
|
|
24
|
+
k = seq.count(base)
|
|
25
|
+
counts[base] += k
|
|
26
|
+
if not top_strand_only:
|
|
27
|
+
counts[complement[base]] += k
|
|
27
28
|
return counts
|
|
28
29
|
|
|
29
30
|
|
|
@@ -55,13 +55,11 @@ def dinucleotide_shuffle(seq: str, rng: Xoshiro128ss) -> str:
|
|
|
55
55
|
if v in last:
|
|
56
56
|
out.append(last[v])
|
|
57
57
|
order[v] = out
|
|
58
|
-
|
|
58
|
+
nxt_of = {v: iter(o).__next__ for v, o in order.items()} # each edge list consumed in order
|
|
59
59
|
res, u = [seq[0]], seq[0]
|
|
60
60
|
for _ in range(n - 1):
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
res.append(w)
|
|
64
|
-
u = w
|
|
61
|
+
u = nxt_of[u]()
|
|
62
|
+
res.append(u)
|
|
65
63
|
return "".join(res)
|
|
66
64
|
|
|
67
65
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Framed 5th-order Markov scoring with NumPy, bit-identical to the reference loop
|
|
3
|
+
(core/parallel.score_markov).
|
|
4
|
+
|
|
5
|
+
The reference adds, for i = 0 .. n-1, the score of the framed k-mer ending at i
|
|
6
|
+
(k = min(i, 5) + 1, frame i % 3) to a running total that starts at 0.0, and stops
|
|
7
|
+
before a terminal in-frame stop codon. Here the per-position values are gathered
|
|
8
|
+
with array lookups, and the total is formed by np.add.accumulate over
|
|
9
|
+
[0.0, v0, v1, ...], which performs the same IEEE additions in the same order.
|
|
10
|
+
Positions 0-4 (k-mers shorter than 6) use the dict, as in the reference.
|
|
11
|
+
|
|
12
|
+
Only score tables whose keys are all ^[ACGT]{1,6}-[012]$ can be compiled; for any
|
|
13
|
+
other table, compile() returns None and callers use the reference loop.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import re
|
|
17
|
+
from typing import Dict, Optional, Sequence, Tuple
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
|
|
21
|
+
_KEY = re.compile(r"^[ACGT]{1,6}-[012]$")
|
|
22
|
+
_CODE = np.full(256, 4, dtype=np.int64)
|
|
23
|
+
for _i, _b in enumerate("ACGT"):
|
|
24
|
+
_CODE[ord(_b)] = _i
|
|
25
|
+
_POW = (1024, 256, 64, 16, 4, 1)
|
|
26
|
+
INVALID = 4096 # index of the zero slot: 6-mers with a non-ACGT base
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MarkovTable:
|
|
30
|
+
"""Hexamer scores of one model, as a (3, 4097) array plus the dict for short k-mers."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, scores: Dict[str, float], stop_codons):
|
|
33
|
+
self.scores = scores
|
|
34
|
+
self.stop_codons = stop_codons
|
|
35
|
+
tbl = np.zeros((3, INVALID + 1), dtype=np.float64)
|
|
36
|
+
for key, v in scores.items():
|
|
37
|
+
kmer, fr = key[:-2], int(key[-1])
|
|
38
|
+
if len(kmer) == 6:
|
|
39
|
+
idx = 0
|
|
40
|
+
for b, p in zip(kmer, _POW):
|
|
41
|
+
idx += "ACGT".index(b) * p
|
|
42
|
+
tbl[fr, idx] = v
|
|
43
|
+
self.tbl = tbl
|
|
44
|
+
|
|
45
|
+
# -- helpers -------------------------------------------------------------------
|
|
46
|
+
@staticmethod
|
|
47
|
+
def _hexamer_index(seq: str) -> np.ndarray:
|
|
48
|
+
"""idx[t] = base-4 index of seq[t:t+6], or INVALID if it contains a non-ACGT base."""
|
|
49
|
+
c = _CODE[np.frombuffer(seq.encode("latin-1"), dtype=np.uint8)]
|
|
50
|
+
n = len(c)
|
|
51
|
+
bad = c == 4
|
|
52
|
+
cc = np.where(bad, 0, c)
|
|
53
|
+
m = n - 5
|
|
54
|
+
idx = cc[0:m] * 1024
|
|
55
|
+
badw = bad[0:m].copy()
|
|
56
|
+
for j in range(1, 6):
|
|
57
|
+
idx += cc[j:m + j] * _POW[j]
|
|
58
|
+
badw |= bad[j:m + j]
|
|
59
|
+
idx[badw] = INVALID
|
|
60
|
+
return idx
|
|
61
|
+
|
|
62
|
+
def _score_offsets(self, seq: str, idx: Optional[np.ndarray], offsets: Sequence[int]) -> Tuple[float, ...]:
|
|
63
|
+
"""Reference score of seq[k:] for each k in offsets (idx: _hexamer_index(seq) or None)."""
|
|
64
|
+
n_full = len(seq)
|
|
65
|
+
get = self.scores.get
|
|
66
|
+
tail_stop = seq[n_full - 3:] in self.stop_codons
|
|
67
|
+
out = []
|
|
68
|
+
for k in offsets:
|
|
69
|
+
n = n_full - k
|
|
70
|
+
if n < 6:
|
|
71
|
+
out.append(0.0)
|
|
72
|
+
continue
|
|
73
|
+
m = n - 3 if (n % 3 == 0 and tail_stop) else n # positions scored: 0 .. m-1
|
|
74
|
+
ms = min(m, 5)
|
|
75
|
+
vals = np.empty(m + 1, dtype=np.float64)
|
|
76
|
+
vals[0] = 0.0
|
|
77
|
+
for i in range(ms):
|
|
78
|
+
vals[i + 1] = get(f"{seq[k:k + i + 1]}-{i % 3}", 0.0)
|
|
79
|
+
if m > 5:
|
|
80
|
+
# positions i = 5 .. m-1 of seq[k:], i.e. 6-mers seq[k+i-5 : k+i+1] = idx[k+i-5]
|
|
81
|
+
t = idx[k:k + m - 5]
|
|
82
|
+
fr = (np.arange(5, m)) % 3
|
|
83
|
+
vals[6:] = self.tbl[fr, t]
|
|
84
|
+
out.append(float(np.add.accumulate(vals)[-1]))
|
|
85
|
+
return tuple(out)
|
|
86
|
+
|
|
87
|
+
# -- public --------------------------------------------------------------------
|
|
88
|
+
def score(self, seq: str) -> float:
|
|
89
|
+
if len(seq) < 6:
|
|
90
|
+
return 0.0
|
|
91
|
+
return self._score_offsets(seq, self._hexamer_index(seq), (0,))[0]
|
|
92
|
+
|
|
93
|
+
def forward_frames(self, seq: str) -> Tuple[float, float, float]:
|
|
94
|
+
"""Scores of seq, seq[1:], seq[2:]."""
|
|
95
|
+
idx = self._hexamer_index(seq) if len(seq) >= 6 else None
|
|
96
|
+
return self._score_offsets(seq, idx, (0, 1, 2))
|
|
97
|
+
|
|
98
|
+
def six_frames(self, seq: str, rev: str) -> Tuple[float, ...]:
|
|
99
|
+
"""Scores of seq, seq[1:], seq[2:], rev, rev[1:], rev[2:] (rev = reverse complement)."""
|
|
100
|
+
return self.forward_frames(seq) + self.forward_frames(rev)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def compile_table(scores: Dict[str, float], stop_codons) -> Optional[MarkovTable]:
|
|
104
|
+
"""MarkovTable for a score dict, or None if any key is outside ^[ACGT]{1,6}-[012]$."""
|
|
105
|
+
if not all(_KEY.match(k) for k in scores):
|
|
106
|
+
return None
|
|
107
|
+
return MarkovTable(scores, stop_codons)
|
|
@@ -4,6 +4,8 @@ ORF (Open Reading Frame) finder
|
|
|
4
4
|
Replicates the core algorithm from Longest_orf.pm
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import bisect
|
|
8
|
+
import re
|
|
7
9
|
from typing import List, Dict, Tuple
|
|
8
10
|
from .models import ORF
|
|
9
11
|
from .translator import Translator
|
|
@@ -54,6 +56,9 @@ class ORFFinder:
|
|
|
54
56
|
self.translator = Translator(genetic_code)
|
|
55
57
|
self.stop_codons = set(self.translator.get_stop_codons())
|
|
56
58
|
self.start_codons = set(self.translator.get_start_codons(allow_non_met_starts))
|
|
59
|
+
# overlapping matches of any stop / start codon (positions in all three frames)
|
|
60
|
+
self._stop_re = re.compile("(?=(" + "|".join(sorted(map(re.escape, self.stop_codons))) + "))")
|
|
61
|
+
self._start_re = re.compile("(?=(" + "|".join(sorted(map(re.escape, self.start_codons))) + "))")
|
|
57
62
|
|
|
58
63
|
def find_all_orfs(self, sequence: str, seq_id: str) -> List[ORF]:
|
|
59
64
|
"""
|
|
@@ -91,13 +96,21 @@ class ORFFinder:
|
|
|
91
96
|
sequence = sequence.upper()
|
|
92
97
|
orfs = []
|
|
93
98
|
|
|
99
|
+
# Codon positions in all frames from one scan each (ascending), split by frame
|
|
100
|
+
stop_pos = [[], [], []]
|
|
101
|
+
for m in self._stop_re.finditer(sequence):
|
|
102
|
+
stop_pos[m.start() % 3].append(m.start())
|
|
103
|
+
start_pos = [[], [], []]
|
|
104
|
+
for m in self._start_re.finditer(sequence):
|
|
105
|
+
start_pos[m.start() % 3].append(m.start())
|
|
106
|
+
|
|
94
107
|
# Find stops and starts for each frame
|
|
95
108
|
for frame in range(3):
|
|
96
109
|
# Find all stop codons in this frame
|
|
97
|
-
stops, real_stops = self._find_stops_in_frame(sequence, frame)
|
|
110
|
+
stops, real_stops = self._find_stops_in_frame(sequence, frame, stop_pos[frame])
|
|
98
111
|
|
|
99
112
|
# Find all start codons in this frame (passing real_stops to avoid starts at stop positions)
|
|
100
|
-
starts = self._find_starts_in_frame(sequence, frame, real_stops)
|
|
113
|
+
starts = self._find_starts_in_frame(sequence, frame, real_stops, start_pos[frame])
|
|
101
114
|
|
|
102
115
|
# Build ORFs from start-stop pairs
|
|
103
116
|
orfs.extend(
|
|
@@ -108,7 +121,7 @@ class ORFFinder:
|
|
|
108
121
|
|
|
109
122
|
return orfs
|
|
110
123
|
|
|
111
|
-
def _find_stops_in_frame(self, sequence: str, frame: int) -> Tuple[List[int], set]:
|
|
124
|
+
def _find_stops_in_frame(self, sequence: str, frame: int, positions: List[int] = None) -> Tuple[List[int], set]:
|
|
112
125
|
"""
|
|
113
126
|
Find all stop codon positions in a specific frame.
|
|
114
127
|
|
|
@@ -117,14 +130,10 @@ class ORFFinder:
|
|
|
117
130
|
- all_stops: List of all stop positions (real + implicit)
|
|
118
131
|
- real_stop_set: Set of positions that are actual stop codons
|
|
119
132
|
"""
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
codon = sequence[i:i+3]
|
|
125
|
-
if codon in self.stop_codons:
|
|
126
|
-
stops.append(i)
|
|
127
|
-
real_stops.add(i)
|
|
133
|
+
if positions is None: # stop codons at frame, frame+3, ... (ascending)
|
|
134
|
+
positions = [i for i in range(frame, len(sequence) - 2, 3) if sequence[i:i+3] in self.stop_codons]
|
|
135
|
+
stops = list(positions)
|
|
136
|
+
real_stops = set(positions)
|
|
128
137
|
|
|
129
138
|
# Add implicit stops at end for 3' partials (NOT real stop codons)
|
|
130
139
|
# Perl adds seq_length, seq_length-1, seq_length-2 as implicit stops
|
|
@@ -137,7 +146,8 @@ class ORFFinder:
|
|
|
137
146
|
|
|
138
147
|
return stops, real_stops
|
|
139
148
|
|
|
140
|
-
def _find_starts_in_frame(self, sequence: str, frame: int, real_stops: set
|
|
149
|
+
def _find_starts_in_frame(self, sequence: str, frame: int, real_stops: set,
|
|
150
|
+
positions: List[int] = None) -> List[int]:
|
|
141
151
|
"""
|
|
142
152
|
Find all start codon positions in a specific frame.
|
|
143
153
|
|
|
@@ -157,10 +167,9 @@ class ORFFinder:
|
|
|
157
167
|
starts.append(frame)
|
|
158
168
|
|
|
159
169
|
# Find explicit start codons
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
starts.append(i)
|
|
170
|
+
if positions is None:
|
|
171
|
+
positions = [i for i in range(frame, len(sequence) - 2, 3) if sequence[i:i+3] in self.start_codons]
|
|
172
|
+
starts.extend(positions)
|
|
164
173
|
|
|
165
174
|
return starts
|
|
166
175
|
|
|
@@ -189,6 +198,11 @@ class ORFFinder:
|
|
|
189
198
|
"""
|
|
190
199
|
orfs = []
|
|
191
200
|
last_used_stop = -1 # Track the last stop position used in this frame
|
|
201
|
+
# stops = real stop positions (ascending, all in this frame) + implicit end positions;
|
|
202
|
+
# every start is in this frame too, so the first listed stop after a start is the first
|
|
203
|
+
# real stop after it (bisect), else the first matching implicit position
|
|
204
|
+
n_real = len(real_stops)
|
|
205
|
+
real_sorted = stops[:n_real]
|
|
192
206
|
|
|
193
207
|
# Iterate through starts in order
|
|
194
208
|
for start_pos in starts:
|
|
@@ -199,11 +213,15 @@ class ORFFinder:
|
|
|
199
213
|
|
|
200
214
|
# Find the first valid stop after this start (Perl's algorithm)
|
|
201
215
|
valid_stop = None
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
216
|
+
k = bisect.bisect_right(real_sorted, start_pos)
|
|
217
|
+
if k < n_real and (real_sorted[k] - start_pos) % 3 == 0:
|
|
218
|
+
valid_stop = real_sorted[k]
|
|
219
|
+
else: # the original scan; stops before index k are <= start_pos and cannot match
|
|
220
|
+
for stop_pos in stops[k:]:
|
|
221
|
+
if (stop_pos > start_pos and
|
|
222
|
+
(stop_pos - start_pos) % 3 == 0):
|
|
223
|
+
valid_stop = stop_pos
|
|
224
|
+
break # Take first valid stop (replicates Perl's 'last')
|
|
207
225
|
|
|
208
226
|
if valid_stop is None:
|
|
209
227
|
continue
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Multi-process scoring of candidate and decoy ORFs.
|
|
3
|
+
|
|
4
|
+
Both loops are CPU-bound pure Python, so they use worker processes (threads would be serialised by the
|
|
5
|
+
GIL). Each worker runs the same per-ORF arithmetic as the single-process code. Work is split into chunks
|
|
6
|
+
in input order, and results are put back together in that order, so the output does not depend on the
|
|
7
|
+
number of workers.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
11
|
+
import multiprocessing as mp
|
|
12
|
+
from typing import Dict, FrozenSet, List, Sequence, Tuple
|
|
13
|
+
|
|
14
|
+
from pytransdecoder.core.sequence import reverse_complement
|
|
15
|
+
|
|
16
|
+
MARKOV_ORDER = 5
|
|
17
|
+
|
|
18
|
+
# per-worker state, set by _init_worker (inherited on fork, or passed on spawn)
|
|
19
|
+
_HEX: Dict[str, float] = {}
|
|
20
|
+
_STOPS: FrozenSet[str] = frozenset()
|
|
21
|
+
_FINDER_ARGS: dict = {}
|
|
22
|
+
_TABLE = None # core/markov_fast.MarkovTable of _HEX, or None (reference loop)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def score_markov(sequence: str, markov_order: int, scores: Dict[str, float], stop_codons) -> float:
|
|
26
|
+
"""Sum of framed k-mer log-likelihoods; a terminal in-frame stop codon is not scored."""
|
|
27
|
+
seq_length = len(sequence)
|
|
28
|
+
if seq_length < markov_order + 1:
|
|
29
|
+
return 0.0
|
|
30
|
+
total_score = 0.0
|
|
31
|
+
for i in range(seq_length):
|
|
32
|
+
frame = i % 3
|
|
33
|
+
markov_use = min(i, markov_order)
|
|
34
|
+
kmer = sequence[i - markov_use:i + 1]
|
|
35
|
+
if i == seq_length - 3 and frame == 0:
|
|
36
|
+
if sequence[i:i + 3] in stop_codons:
|
|
37
|
+
break
|
|
38
|
+
total_score += scores.get(f"{kmer}-{frame}", 0.0)
|
|
39
|
+
return total_score
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _init_worker(hex_scores, stop_codons, finder_args):
|
|
43
|
+
global _HEX, _STOPS, _FINDER_ARGS, _TABLE
|
|
44
|
+
from pytransdecoder.core.markov_fast import compile_table
|
|
45
|
+
_HEX, _STOPS, _FINDER_ARGS = hex_scores, stop_codons, finder_args
|
|
46
|
+
_TABLE = compile_table(hex_scores, stop_codons)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _score_six_frames(sequence: str) -> Tuple[float, ...]:
|
|
50
|
+
rev = reverse_complement(sequence)
|
|
51
|
+
if _TABLE is not None:
|
|
52
|
+
return _TABLE.six_frames(sequence, rev)
|
|
53
|
+
return tuple(score_markov(s, MARKOV_ORDER, _HEX, _STOPS)
|
|
54
|
+
for s in (sequence, sequence[1:], sequence[2:], rev, rev[1:], rev[2:]))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _six_frame_chunk(seqs: Sequence[str]) -> List[Tuple[float, ...]]:
|
|
58
|
+
return [_score_six_frames(s) for s in seqs]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _decoy_chunk(args):
|
|
62
|
+
"""Decoy ORFs of a chunk of (tid, seq) in the given order: (n_decoy, [(stratum, contrast), ...])."""
|
|
63
|
+
from pytransdecoder.core import decoys
|
|
64
|
+
from pytransdecoder.core.prng import Xoshiro128ss
|
|
65
|
+
from pytransdecoder.core.orf_finder import ORFFinder
|
|
66
|
+
items, n_shuf, seed = args
|
|
67
|
+
finder = ORFFinder(**_FINDER_ARGS)
|
|
68
|
+
n_decoy, out = 0, []
|
|
69
|
+
for tid, seq in items:
|
|
70
|
+
for k in range(n_shuf):
|
|
71
|
+
shuf = decoys.dinucleotide_shuffle(seq, Xoshiro128ss(f"{seed}:{tid}:{k}"))
|
|
72
|
+
for orf in finder.find_all_orfs(shuf, f"decoy{k}_{tid}"):
|
|
73
|
+
n_decoy += 1
|
|
74
|
+
cds = orf.sequence.upper()
|
|
75
|
+
if _TABLE is not None:
|
|
76
|
+
s0, s1, s2 = (round(x, 2) for x in _TABLE.forward_frames(cds))
|
|
77
|
+
else:
|
|
78
|
+
s0 = round(score_markov(cds, MARKOV_ORDER, _HEX, _STOPS), 2)
|
|
79
|
+
s1 = round(score_markov(cds[1:], MARKOV_ORDER, _HEX, _STOPS), 2)
|
|
80
|
+
s2 = round(score_markov(cds[2:], MARKOV_ORDER, _HEX, _STOPS), 2)
|
|
81
|
+
c = s0 - max(s1, s2)
|
|
82
|
+
if s0 > 0 and c > 0:
|
|
83
|
+
out.append((decoys.stratum(len(cds) // 3, len(seq), orf.orf_type == 'complete',
|
|
84
|
+
'len_tx_type'), c))
|
|
85
|
+
return n_decoy, out
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _chunks_by_size(items, sizes, n_chunks):
|
|
89
|
+
"""Consecutive chunks of roughly equal total size (order preserved)."""
|
|
90
|
+
total = sum(sizes) or 1
|
|
91
|
+
target = total / n_chunks
|
|
92
|
+
chunks, cur, acc = [], [], 0
|
|
93
|
+
for it, sz in zip(items, sizes):
|
|
94
|
+
cur.append(it)
|
|
95
|
+
acc += sz
|
|
96
|
+
if acc >= target:
|
|
97
|
+
chunks.append(cur)
|
|
98
|
+
cur, acc = [], 0
|
|
99
|
+
if cur:
|
|
100
|
+
chunks.append(cur)
|
|
101
|
+
return chunks
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _pool(cpu, hex_scores, stop_codons, finder_args=None):
|
|
105
|
+
ctx = mp.get_context("fork") if "fork" in mp.get_all_start_methods() else None
|
|
106
|
+
return ProcessPoolExecutor(max_workers=cpu, mp_context=ctx, initializer=_init_worker,
|
|
107
|
+
initargs=(hex_scores, stop_codons, finder_args or {}))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def score_six_frames(seqs: List[str], hex_scores, stop_codons, cpu: int) -> List[Tuple[float, ...]]:
|
|
111
|
+
"""Six-frame scores of each sequence, in input order."""
|
|
112
|
+
if cpu <= 1 or len(seqs) < 2:
|
|
113
|
+
_init_worker(hex_scores, stop_codons, {})
|
|
114
|
+
return _six_frame_chunk(seqs)
|
|
115
|
+
# many small chunks so slow chunks (long ORFs) do not leave workers idle
|
|
116
|
+
chunks = _chunks_by_size(seqs, [len(s) for s in seqs], cpu * 16)
|
|
117
|
+
with _pool(cpu, hex_scores, stop_codons) as ex:
|
|
118
|
+
return [r for part in ex.map(_six_frame_chunk, chunks) for r in part]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def decoy_scores(transcripts: List[Tuple[str, str]], n_shuf: int, seed: int, hex_scores, stop_codons,
|
|
122
|
+
finder_args: dict, cpu: int):
|
|
123
|
+
"""(n_decoy, eligible decoy (stratum, contrast) list) over transcripts in the given order."""
|
|
124
|
+
if cpu <= 1:
|
|
125
|
+
_init_worker(hex_scores, stop_codons, finder_args)
|
|
126
|
+
return _decoy_chunk((transcripts, n_shuf, seed))
|
|
127
|
+
chunks = _chunks_by_size(transcripts, [len(s) for _, s in transcripts], cpu * 16)
|
|
128
|
+
n_decoy, out = 0, []
|
|
129
|
+
with _pool(cpu, hex_scores, stop_codons, finder_args) as ex:
|
|
130
|
+
for n, part in ex.map(_decoy_chunk, [(c, n_shuf, seed) for c in chunks]):
|
|
131
|
+
n_decoy += n
|
|
132
|
+
out.extend(part)
|
|
133
|
+
return n_decoy, out
|
|
@@ -48,7 +48,30 @@ class Xoshiro128ss:
|
|
|
48
48
|
return r % n
|
|
49
49
|
|
|
50
50
|
def shuffle(self, items: list) -> None:
|
|
51
|
-
"""Fisher-Yates, in place
|
|
51
|
+
"""Fisher-Yates, in place: j = below(i + 1) for i = len-1 .. 1, with next32/below inlined
|
|
52
|
+
(same integer arithmetic, so the same stream as shuffle_reference)."""
|
|
53
|
+
s0, s1, s2, s3 = self.s
|
|
54
|
+
for i in range(len(items) - 1, 0, -1):
|
|
55
|
+
n = i + 1
|
|
56
|
+
limit = (4294967296 // n) * n
|
|
57
|
+
while True:
|
|
58
|
+
x = (s1 * 5) & M32
|
|
59
|
+
r = ((((x << 7) | (x >> 25)) & M32) * 9) & M32
|
|
60
|
+
t = (s1 << 9) & M32
|
|
61
|
+
s2 ^= s0
|
|
62
|
+
s3 ^= s1
|
|
63
|
+
s1 ^= s2
|
|
64
|
+
s0 ^= s3
|
|
65
|
+
s2 ^= t
|
|
66
|
+
s3 = ((s3 << 11) | (s3 >> 21)) & M32
|
|
67
|
+
if r < limit:
|
|
68
|
+
break
|
|
69
|
+
j = r % n
|
|
70
|
+
items[i], items[j] = items[j], items[i]
|
|
71
|
+
self.s[:] = [s0, s1, s2, s3]
|
|
72
|
+
|
|
73
|
+
def shuffle_reference(self, items: list) -> None:
|
|
74
|
+
"""Fisher-Yates, in place (the form shuffle() inlines)."""
|
|
52
75
|
for i in range(len(items) - 1, 0, -1):
|
|
53
76
|
j = self.below(i + 1)
|
|
54
77
|
items[i], items[j] = items[j], items[i]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sequence utilities
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from Bio.Seq import Seq
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def reverse_complement(sequence: str) -> str:
|
|
9
|
+
"""
|
|
10
|
+
Return reverse complement of a DNA/RNA sequence.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
sequence: DNA or RNA sequence
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
Reverse complement sequence
|
|
17
|
+
"""
|
|
18
|
+
if sequence.isascii():
|
|
19
|
+
return sequence.translate(_rc_table())[::-1]
|
|
20
|
+
return str(Seq(sequence).reverse_complement())
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_RC = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _rc_table():
|
|
27
|
+
"""Per-character complement of ASCII, taken from Biopython (whose reverse complement maps each
|
|
28
|
+
character independently), so translate + reverse equals Seq.reverse_complement."""
|
|
29
|
+
global _RC
|
|
30
|
+
if _RC is None:
|
|
31
|
+
_RC = str.maketrans({chr(o): str(Seq(chr(o)).reverse_complement()) for o in range(128)})
|
|
32
|
+
return _RC
|
|
@@ -78,6 +78,9 @@ class Translator:
|
|
|
78
78
|
self.genetic_code_name = genetic_code
|
|
79
79
|
self.table_id = self.GENETIC_CODE_MAP[code_name]
|
|
80
80
|
self.codon_table = CodonTable.unambiguous_dna_by_id[self.table_id]
|
|
81
|
+
# codon -> amino acid, filled from Biopython one codon at a time (Biopython translates
|
|
82
|
+
# each codon independently, so joining memoised codons gives the same protein)
|
|
83
|
+
self._codon_memo = {}
|
|
81
84
|
|
|
82
85
|
def get_stop_codons(self) -> List[str]:
|
|
83
86
|
"""
|
|
@@ -108,15 +111,34 @@ class Translator:
|
|
|
108
111
|
|
|
109
112
|
def translate(self, sequence: str, frame: int = 0) -> str:
|
|
110
113
|
"""
|
|
111
|
-
Translate nucleotide sequence to protein.
|
|
112
|
-
|
|
114
|
+
Translate nucleotide sequence to protein (as Biopython's Seq.translate, to_stop=False;
|
|
115
|
+
a trailing partial codon is dropped).
|
|
116
|
+
|
|
113
117
|
Args:
|
|
114
118
|
sequence: DNA/RNA sequence
|
|
115
119
|
frame: Reading frame offset (0, 1, or 2)
|
|
116
|
-
|
|
120
|
+
|
|
117
121
|
Returns:
|
|
118
122
|
Protein sequence (stops are represented as *)
|
|
119
123
|
"""
|
|
124
|
+
seq = sequence[frame:].upper()
|
|
125
|
+
n = len(seq) - len(seq) % 3
|
|
126
|
+
memo = self._codon_memo
|
|
127
|
+
try:
|
|
128
|
+
return ''.join([memo[seq[i:i + 3]] for i in range(0, n, 3)])
|
|
129
|
+
except KeyError:
|
|
130
|
+
pass
|
|
131
|
+
for i in range(0, n, 3):
|
|
132
|
+
codon = seq[i:i + 3]
|
|
133
|
+
if codon not in memo:
|
|
134
|
+
try:
|
|
135
|
+
memo[codon] = str(Seq(codon).translate(table=self.table_id, to_stop=False))
|
|
136
|
+
except Exception:
|
|
137
|
+
return self._translate_reference(sequence, frame)
|
|
138
|
+
return ''.join([memo[seq[i:i + 3]] for i in range(0, n, 3)])
|
|
139
|
+
|
|
140
|
+
def _translate_reference(self, sequence: str, frame: int = 0) -> str:
|
|
141
|
+
"""Whole-sequence Biopython translation (the form translate() reproduces codon by codon)."""
|
|
120
142
|
# Adjust for frame
|
|
121
143
|
seq = sequence[frame:].upper()
|
|
122
144
|
|