bioforge 2.3.0__py3-none-win_amd64.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.
- bioforge-2.3.0.data/purelib/bioforge/__init__.py +74 -0
- bioforge-2.3.0.data/purelib/bioforge/aligner.py +953 -0
- bioforge-2.3.0.data/purelib/bioforge/analyze.py +385 -0
- bioforge-2.3.0.data/purelib/bioforge/bgzf.py +119 -0
- bioforge-2.3.0.data/purelib/bioforge/biocore.py +2092 -0
- bioforge-2.3.0.data/purelib/bioforge/engine/__init__.py +1 -0
- bioforge-2.3.0.data/purelib/bioforge/engine/_loader.py +543 -0
- bioforge-2.3.0.data/purelib/bioforge/engine/build.py +231 -0
- bioforge-2.3.0.data/purelib/bioforge/engine/engine.c +1538 -0
- bioforge-2.3.0.data/purelib/bioforge/engine/engine.dll +0 -0
- bioforge-2.3.0.data/purelib/bioforge/qcreport.py +298 -0
- bioforge-2.3.0.data/purelib/bioforge/smart_translator.py +601 -0
- bioforge-2.3.0.dist-info/METADATA +736 -0
- bioforge-2.3.0.dist-info/RECORD +18 -0
- bioforge-2.3.0.dist-info/WHEEL +5 -0
- bioforge-2.3.0.dist-info/entry_points.txt +4 -0
- bioforge-2.3.0.dist-info/licenses/LICENSE +280 -0
- bioforge-2.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
"""
|
|
2
|
+
smart_translator.py
|
|
3
|
+
═══════════════════════════════════════════════════════════════════════
|
|
4
|
+
Standard Genetic Code translator for the biocore 5-bit engine.
|
|
5
|
+
|
|
6
|
+
Integrates with biocore.py (A=0, C=1, G=2, T/U=3, AA codes 4–23,
|
|
7
|
+
STOP=24) without any Python-level loops in the hot path.
|
|
8
|
+
|
|
9
|
+
Pipeline (fully vectorised — zero Python loops at call time)
|
|
10
|
+
─────────────────────────────────────────────────────────────
|
|
11
|
+
① Decode PackedSequence(NUC) → uint8 codes [0–3]
|
|
12
|
+
② Locate first ATG/AUG via sliding_window_view (Fail-Fast)
|
|
13
|
+
③ Extract ORF from ATG; reshape into (N, 3) codon matrix
|
|
14
|
+
④ Compute LUT index: idx = n₁×16 + n₂×4 + n₃ (base-4, vectorised)
|
|
15
|
+
⑤ CODON_LUT[indices] → amino-acid BioCode array (single fancy-index)
|
|
16
|
+
⑥ Truncate at first STOP codon (BioCode.STOP = 24)
|
|
17
|
+
⑦ Raise UserWarning if result < 50 aa (possible ncRNA / broken fragment)
|
|
18
|
+
⑧ BitPacker.pack(aa_codes) → new PackedSequence(PROTEIN)
|
|
19
|
+
|
|
20
|
+
CODON_LUT (64 entries, Standard Genetic Code NCBI table #1)
|
|
21
|
+
─────────────────────────────────────────────────────────────
|
|
22
|
+
Index = n₁×16 + n₂×4 + n₃ │ A=0 C=1 G=2 T/U=3
|
|
23
|
+
|
|
24
|
+
Idx Codon AA │ Idx Codon AA │ Idx Codon AA │ Idx Codon AA
|
|
25
|
+
──────────────── │ ──────────────── │ ──────────────── │ ────────────────
|
|
26
|
+
0 AAA K │ 16 CAA Q │ 32 GAA E │ 48 UAA STP
|
|
27
|
+
1 AAC N │ 17 CAC H │ 33 GAC D │ 49 UAC Y
|
|
28
|
+
2 AAG K │ 18 CAG Q │ 34 GAG E │ 50 UAG STP
|
|
29
|
+
3 AAU N │ 19 CAU H │ 35 GAU D │ 51 UAU Y
|
|
30
|
+
4 ACA T │ 20 CCA P │ 36 GCA A │ 52 UCA S
|
|
31
|
+
5 ACC T │ 21 CCC P │ 37 GCC A │ 53 UCC S
|
|
32
|
+
6 ACG T │ 22 CCG P │ 38 GCG A │ 54 UCG S
|
|
33
|
+
7 ACU T │ 23 CCU P │ 39 GCU A │ 55 UCU S
|
|
34
|
+
8 AGA R │ 24 CGA R │ 40 GGA G │ 56 UGA STP
|
|
35
|
+
9 AGC S │ 25 CGC R │ 41 GGC G │ 57 UGC C
|
|
36
|
+
10 AGG R │ 26 CGG R │ 42 GGG G │ 58 UGG W
|
|
37
|
+
11 AGU S │ 27 CGU R │ 43 GGU G │ 59 UGU C
|
|
38
|
+
12 AUA I │ 28 CUA L │ 44 GUA V │ 60 UUA L
|
|
39
|
+
13 AUC I │ 29 CUC L │ 45 GUC V │ 61 UUC F
|
|
40
|
+
14 AUG M │ 30 CUG L │ 46 GUG V │ 62 UUG L
|
|
41
|
+
15 AUU I │ 31 CUU L │ 47 GUU V │ 63 UUU F
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import warnings
|
|
47
|
+
|
|
48
|
+
import numpy as np
|
|
49
|
+
from numpy.lib.stride_tricks import sliding_window_view
|
|
50
|
+
|
|
51
|
+
# ── biocore integration ────────────────────────────────────────────────────────
|
|
52
|
+
from .biocore import (
|
|
53
|
+
BioCode,
|
|
54
|
+
BitPacker,
|
|
55
|
+
PackedSequence,
|
|
56
|
+
SeqType,
|
|
57
|
+
SequenceTypeError,
|
|
58
|
+
SequenceValueError,
|
|
59
|
+
TranslationError,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# ── motor C (opcional) ────────────────────────────────────────────────────────
|
|
63
|
+
try:
|
|
64
|
+
from .engine._loader import C_AVAILABLE as _C_AVAILABLE
|
|
65
|
+
from .engine._loader import c_find_atg as _c_find_atg
|
|
66
|
+
from .engine._loader import c_translate as _c_translate
|
|
67
|
+
except ImportError:
|
|
68
|
+
_C_AVAILABLE = False
|
|
69
|
+
|
|
70
|
+
__all__: list[str] = ["SmartTranslator"]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
74
|
+
# §1 CODON LUT — Standard Genetic Code, built once at module load
|
|
75
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
76
|
+
|
|
77
|
+
def _build_codon_lut() -> np.ndarray:
|
|
78
|
+
"""
|
|
79
|
+
Build the 64-entry Standard Genetic Code lookup array.
|
|
80
|
+
|
|
81
|
+
Encoding: A=0, C=1, G=2, U=3 (T and U share the same slot as in biocore).
|
|
82
|
+
LUT index = n₁×16 + n₂×4 + n₃ → [0, 63].
|
|
83
|
+
Stored values: BioCode.AA_* (4–23), BioCode.STOP (24), BioCode.UNK (31).
|
|
84
|
+
|
|
85
|
+
This function runs **once** at module import time and is never called again.
|
|
86
|
+
|
|
87
|
+
Returns
|
|
88
|
+
-------
|
|
89
|
+
np.ndarray, dtype=uint8, shape=(64,)
|
|
90
|
+
"""
|
|
91
|
+
# RNA base → biocore nucleotide code
|
|
92
|
+
_B: dict[str, int] = {"A": 0, "C": 1, "G": 2, "U": 3}
|
|
93
|
+
|
|
94
|
+
# Standard Genetic Code (NCBI table #1)
|
|
95
|
+
# RNA codon string → BioCode integer value
|
|
96
|
+
_TABLE: dict[str, int] = {
|
|
97
|
+
# ── Phenylalanine F = 8 ─────────────────────────────────────────────
|
|
98
|
+
"UUU": BioCode.AA_F, "UUC": BioCode.AA_F,
|
|
99
|
+
# ── Leucine L = 13 ──────────────────────────────────────────────────
|
|
100
|
+
"UUA": BioCode.AA_L, "UUG": BioCode.AA_L,
|
|
101
|
+
"CUU": BioCode.AA_L, "CUC": BioCode.AA_L,
|
|
102
|
+
"CUA": BioCode.AA_L, "CUG": BioCode.AA_L,
|
|
103
|
+
# ── Isoleucine I = 11 ───────────────────────────────────────────────
|
|
104
|
+
"AUU": BioCode.AA_I, "AUC": BioCode.AA_I, "AUA": BioCode.AA_I,
|
|
105
|
+
# ── Methionine / START M = 14 ───────────────────────────────────────
|
|
106
|
+
"AUG": BioCode.AA_M,
|
|
107
|
+
# ── Valine V = 21 ───────────────────────────────────────────────────
|
|
108
|
+
"GUU": BioCode.AA_V, "GUC": BioCode.AA_V,
|
|
109
|
+
"GUA": BioCode.AA_V, "GUG": BioCode.AA_V,
|
|
110
|
+
# ── Serine S = 19 ───────────────────────────────────────────────────
|
|
111
|
+
"UCU": BioCode.AA_S, "UCC": BioCode.AA_S,
|
|
112
|
+
"UCA": BioCode.AA_S, "UCG": BioCode.AA_S,
|
|
113
|
+
"AGU": BioCode.AA_S, "AGC": BioCode.AA_S,
|
|
114
|
+
# ── Proline P = 16 ──────────────────────────────────────────────────
|
|
115
|
+
"CCU": BioCode.AA_P, "CCC": BioCode.AA_P,
|
|
116
|
+
"CCA": BioCode.AA_P, "CCG": BioCode.AA_P,
|
|
117
|
+
# ── Threonine T = 20 ────────────────────────────────────────────────
|
|
118
|
+
"ACU": BioCode.AA_T, "ACC": BioCode.AA_T,
|
|
119
|
+
"ACA": BioCode.AA_T, "ACG": BioCode.AA_T,
|
|
120
|
+
# ── Alanine A = 4 ───────────────────────────────────────────────────
|
|
121
|
+
"GCU": BioCode.AA_A, "GCC": BioCode.AA_A,
|
|
122
|
+
"GCA": BioCode.AA_A, "GCG": BioCode.AA_A,
|
|
123
|
+
# ── Tyrosine Y = 23 ─────────────────────────────────────────────────
|
|
124
|
+
"UAU": BioCode.AA_Y, "UAC": BioCode.AA_Y,
|
|
125
|
+
# ── STOP codons (24) ────────────────────────────────────────────────
|
|
126
|
+
"UAA": BioCode.STOP, "UAG": BioCode.STOP, "UGA": BioCode.STOP,
|
|
127
|
+
# ── Histidine H = 10 ────────────────────────────────────────────────
|
|
128
|
+
"CAU": BioCode.AA_H, "CAC": BioCode.AA_H,
|
|
129
|
+
# ── Glutamine Q = 17 ────────────────────────────────────────────────
|
|
130
|
+
"CAA": BioCode.AA_Q, "CAG": BioCode.AA_Q,
|
|
131
|
+
# ── Asparagine N = 15 ───────────────────────────────────────────────
|
|
132
|
+
"AAU": BioCode.AA_N, "AAC": BioCode.AA_N,
|
|
133
|
+
# ── Lysine K = 12 ───────────────────────────────────────────────────
|
|
134
|
+
"AAA": BioCode.AA_K, "AAG": BioCode.AA_K,
|
|
135
|
+
# ── Aspartic acid D = 6 ─────────────────────────────────────────────
|
|
136
|
+
"GAU": BioCode.AA_D, "GAC": BioCode.AA_D,
|
|
137
|
+
# ── Glutamic acid E = 7 ─────────────────────────────────────────────
|
|
138
|
+
"GAA": BioCode.AA_E, "GAG": BioCode.AA_E,
|
|
139
|
+
# ── Cysteine C = 5 ──────────────────────────────────────────────────
|
|
140
|
+
"UGU": BioCode.AA_C, "UGC": BioCode.AA_C,
|
|
141
|
+
# ── Tryptophan W = 22 ───────────────────────────────────────────────
|
|
142
|
+
"UGG": BioCode.AA_W,
|
|
143
|
+
# ── Arginine R = 18 ─────────────────────────────────────────────────
|
|
144
|
+
"CGU": BioCode.AA_R, "CGC": BioCode.AA_R,
|
|
145
|
+
"CGA": BioCode.AA_R, "CGG": BioCode.AA_R,
|
|
146
|
+
"AGA": BioCode.AA_R, "AGG": BioCode.AA_R,
|
|
147
|
+
# ── Glycine G = 9 ───────────────────────────────────────────────────
|
|
148
|
+
"GGU": BioCode.AA_G, "GGC": BioCode.AA_G,
|
|
149
|
+
"GGA": BioCode.AA_G, "GGG": BioCode.AA_G,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
assert len(_TABLE) == 64, f"Genetic code table must have 64 entries, got {len(_TABLE)}"
|
|
153
|
+
|
|
154
|
+
lut = np.full(64, BioCode.UNK, dtype=np.uint8)
|
|
155
|
+
for codon, aa_code in _TABLE.items():
|
|
156
|
+
n1, n2, n3 = _B[codon[0]], _B[codon[1]], _B[codon[2]]
|
|
157
|
+
lut[n1 * 16 + n2 * 4 + n3] = np.uint8(aa_code)
|
|
158
|
+
|
|
159
|
+
return lut
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
163
|
+
# §2 SMART TRANSLATOR
|
|
164
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
165
|
+
|
|
166
|
+
class SmartTranslator:
|
|
167
|
+
"""
|
|
168
|
+
Vectorised Standard Genetic Code translator for biocore PackedSequences.
|
|
169
|
+
|
|
170
|
+
Accepts a ``PackedSequence(NUCLEOTIDE)``, locates the first ATG/AUG
|
|
171
|
+
codon via a zero-copy sliding-window view, translates the ORF using
|
|
172
|
+
a single numpy fancy-index lookup, truncates at the first STOP codon,
|
|
173
|
+
and returns a ``PackedSequence(PROTEIN)`` — all without a single
|
|
174
|
+
Python-level loop in the call path.
|
|
175
|
+
|
|
176
|
+
Class constants
|
|
177
|
+
───────────────
|
|
178
|
+
CODON_LUT : np.ndarray, shape (64,) — Standard Genetic Code LUT.
|
|
179
|
+
_START_CODON : np.ndarray [0, 3, 2] — ATG/AUG in 5-bit encoding.
|
|
180
|
+
_CODON_WEIGHTS: np.ndarray [16, 4, 1] — base-4 index weights (uint16).
|
|
181
|
+
_MIN_AA_LEN : int = 50 — biological plausibility floor.
|
|
182
|
+
|
|
183
|
+
Quick start
|
|
184
|
+
───────────
|
|
185
|
+
>>> from bioforge import SmartImporter, SeqType
|
|
186
|
+
>>> nuc_seq = SmartImporter.from_string(fasta)[0]
|
|
187
|
+
>>> prot_seq = SmartTranslator.translate(nuc_seq)
|
|
188
|
+
>>> print(prot_seq.to_string())
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
# ── Class-level constants (allocated once at class definition) ─────────────
|
|
192
|
+
|
|
193
|
+
# Standard Genetic Code LUT — 64 entries, computed at class load time.
|
|
194
|
+
# Index formula: n₁×16 + n₂×4 + n₃ (A=0 C=1 G=2 T/U=3)
|
|
195
|
+
# Values: BioCode.AA_* (4–23), BioCode.STOP (24), BioCode.UNK (31)
|
|
196
|
+
CODON_LUT: np.ndarray = _build_codon_lut()
|
|
197
|
+
|
|
198
|
+
# ATG / AUG start codon in biocore 5-bit encoding (A=0, T/U=3, G=2)
|
|
199
|
+
_START_CODON: np.ndarray = np.array([0, 3, 2], dtype=np.uint8)
|
|
200
|
+
|
|
201
|
+
# Base-4 weights for codon index arithmetic.
|
|
202
|
+
# uint16 prevents overflow when ambiguous/gap codes (values > 3) appear:
|
|
203
|
+
# worst case: UNK=31 → 31×16 + 31×4 + 31 = 651 → out of [0,63] → masked
|
|
204
|
+
_CODON_WEIGHTS: np.ndarray = np.array([16, 4, 1], dtype=np.uint16)
|
|
205
|
+
|
|
206
|
+
# Proteins shorter than this trigger a UserWarning
|
|
207
|
+
_MIN_AA_LEN: int = 50
|
|
208
|
+
|
|
209
|
+
# ── Public API ─────────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
@classmethod
|
|
212
|
+
def translate(
|
|
213
|
+
cls,
|
|
214
|
+
seq: PackedSequence,
|
|
215
|
+
warn_short: bool = True,
|
|
216
|
+
) -> PackedSequence:
|
|
217
|
+
"""
|
|
218
|
+
Translate a nucleotide ``PackedSequence`` into a protein ``PackedSequence``.
|
|
219
|
+
|
|
220
|
+
Translation starts at the first ATG/AUG codon and stops at the
|
|
221
|
+
first in-frame STOP codon. The STOP codon itself is excluded from
|
|
222
|
+
the result.
|
|
223
|
+
|
|
224
|
+
Parameters
|
|
225
|
+
----------
|
|
226
|
+
seq : PackedSequence
|
|
227
|
+
A nucleotide-type packed sequence (``SeqType.NUCLEOTIDE``).
|
|
228
|
+
warn_short : bool, default True
|
|
229
|
+
If ``True``, emit a ``UserWarning`` when the translated protein
|
|
230
|
+
is shorter than 50 amino acids.
|
|
231
|
+
|
|
232
|
+
Returns
|
|
233
|
+
-------
|
|
234
|
+
PackedSequence
|
|
235
|
+
A protein-type packed sequence (``SeqType.PROTEIN``).
|
|
236
|
+
Header is prefixed with ``[PROT | ORF@<start>]``.
|
|
237
|
+
|
|
238
|
+
Raises
|
|
239
|
+
------
|
|
240
|
+
TypeError
|
|
241
|
+
If *seq* is not a ``NUCLEOTIDE`` sequence.
|
|
242
|
+
ValueError
|
|
243
|
+
If the sequence is too short, or no ATG/AUG codon is found,
|
|
244
|
+
or the ORF yields no complete codon after the start.
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
# ── Guard clauses ─────────────────────────────────────────────────────
|
|
248
|
+
if not isinstance(seq, PackedSequence):
|
|
249
|
+
raise SequenceTypeError(
|
|
250
|
+
f"seq debe ser PackedSequence, se recibió {type(seq).__name__!r}. "
|
|
251
|
+
"Crea la secuencia con SmartImporter.from_string() o SmartImporter.from_file()."
|
|
252
|
+
)
|
|
253
|
+
if seq.seq_type != SeqType.NUCLEOTIDE:
|
|
254
|
+
raise SequenceTypeError(
|
|
255
|
+
f"Se esperaba SeqType.NUCLEOTIDE, se recibió {seq.seq_type.name}. "
|
|
256
|
+
"SmartTranslator solo traduce ADN/ARN. "
|
|
257
|
+
"Para alinear proteínas usa SequenceAligner directamente."
|
|
258
|
+
)
|
|
259
|
+
if seq.n_symbols < 3:
|
|
260
|
+
raise SequenceValueError(
|
|
261
|
+
f"Secuencia demasiado corta ({seq.n_symbols} nt): "
|
|
262
|
+
"se necesitan al menos 3 nucleótidos para un codón."
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# ① Unpack to flat uint8 nucleotide code array
|
|
266
|
+
nuc_codes: np.ndarray = seq.decode() # shape (n,)
|
|
267
|
+
|
|
268
|
+
# ② Vectorised ORF search — locate first ATG/AUG (Fail-Fast)
|
|
269
|
+
orf_start: int = cls._find_orf_start(nuc_codes)
|
|
270
|
+
|
|
271
|
+
# ③ Extract ORF from start codon; discard any incomplete trailing codon
|
|
272
|
+
orf: np.ndarray = nuc_codes[orf_start:]
|
|
273
|
+
n_codons: int = len(orf) // 3
|
|
274
|
+
if n_codons == 0:
|
|
275
|
+
raise TranslationError(
|
|
276
|
+
f"El ORF en la posición {orf_start} no tiene ningún codón "
|
|
277
|
+
"completo tras el ATG de inicio. "
|
|
278
|
+
"La secuencia termina justo en el ATG sin residuos posteriores."
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# ④+⑤ Traducir codones → AAs (C si disponible, NumPy si no)
|
|
282
|
+
if _C_AVAILABLE:
|
|
283
|
+
aa_codes: np.ndarray = _c_translate(cls.CODON_LUT, orf, n_codons)
|
|
284
|
+
else:
|
|
285
|
+
codons: np.ndarray = orf[: n_codons * 3].reshape(n_codons, 3)
|
|
286
|
+
indices_u16: np.ndarray = (
|
|
287
|
+
codons.astype(np.uint16) * cls._CODON_WEIGHTS
|
|
288
|
+
).sum(axis=1)
|
|
289
|
+
valid_codon: np.ndarray = indices_u16 < np.uint16(64)
|
|
290
|
+
safe_idx: np.ndarray = np.where(
|
|
291
|
+
valid_codon, indices_u16, np.uint16(0)
|
|
292
|
+
).astype(np.uint8)
|
|
293
|
+
aa_codes = np.where(
|
|
294
|
+
valid_codon,
|
|
295
|
+
cls.CODON_LUT[safe_idx],
|
|
296
|
+
np.uint8(BioCode.UNK),
|
|
297
|
+
).astype(np.uint8)
|
|
298
|
+
|
|
299
|
+
# ⑥ Truncate at first in-frame STOP codon (BioCode.STOP = 24).
|
|
300
|
+
stop_mask: np.ndarray = aa_codes == np.uint8(BioCode.STOP)
|
|
301
|
+
if stop_mask.any():
|
|
302
|
+
aa_codes = aa_codes[: int(np.argmax(stop_mask))] # exclude STOP itself
|
|
303
|
+
|
|
304
|
+
# ⑦ Biological plausibility check
|
|
305
|
+
n_aa: int = int(len(aa_codes))
|
|
306
|
+
if warn_short and n_aa < cls._MIN_AA_LEN:
|
|
307
|
+
warnings.warn(
|
|
308
|
+
f"Secuencia sospechosamente corta ({n_aa} aa < {cls._MIN_AA_LEN} aa). "
|
|
309
|
+
"Podría ser un ARNt, micro-ARN o un fragmento roto.",
|
|
310
|
+
UserWarning,
|
|
311
|
+
stacklevel=2,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# ⑧ Pack and return as a new PROTEIN PackedSequence
|
|
315
|
+
return PackedSequence(
|
|
316
|
+
header = f"[PROT | ORF@{orf_start}] {seq.header}",
|
|
317
|
+
seq_type = SeqType.PROTEIN,
|
|
318
|
+
n_symbols = n_aa,
|
|
319
|
+
data = BitPacker.pack(aa_codes),
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
@classmethod
|
|
323
|
+
def translate_all_frames(
|
|
324
|
+
cls,
|
|
325
|
+
seq: PackedSequence,
|
|
326
|
+
warn_short: bool = False,
|
|
327
|
+
) -> list[PackedSequence]:
|
|
328
|
+
"""
|
|
329
|
+
Translate all 6 reading frames of a nucleotide sequence.
|
|
330
|
+
|
|
331
|
+
The 6 frames are:
|
|
332
|
+
+1, +2, +3 — forward strand, offsets 0, 1, 2.
|
|
333
|
+
-1, -2, -3 — reverse complement strand, offsets 0, 1, 2.
|
|
334
|
+
|
|
335
|
+
Only frames that contain at least one ATG codon are included.
|
|
336
|
+
Frames with no ATG are silently skipped, so the result may have
|
|
337
|
+
fewer than 6 entries (or be empty for non-coding sequences).
|
|
338
|
+
|
|
339
|
+
Parameters
|
|
340
|
+
----------
|
|
341
|
+
seq : PackedSequence
|
|
342
|
+
A nucleotide-type packed sequence (``SeqType.NUCLEOTIDE``).
|
|
343
|
+
warn_short : bool, default False
|
|
344
|
+
If ``True``, emit a ``UserWarning`` for proteins shorter than
|
|
345
|
+
50 amino acids. Off by default because short ORFs are common
|
|
346
|
+
in non-primary frames.
|
|
347
|
+
|
|
348
|
+
Returns
|
|
349
|
+
-------
|
|
350
|
+
list[PackedSequence]
|
|
351
|
+
Up to 6 protein PackedSequences, one per frame with an ORF.
|
|
352
|
+
Each header: ``[PROT | frame +/-N | ORF@<pos>] <original>``.
|
|
353
|
+
Empty list if no ORF is found in any frame.
|
|
354
|
+
|
|
355
|
+
Raises
|
|
356
|
+
------
|
|
357
|
+
SequenceTypeError
|
|
358
|
+
If *seq* is not a ``NUCLEOTIDE`` sequence.
|
|
359
|
+
SequenceValueError
|
|
360
|
+
If the sequence is shorter than 3 nucleotides.
|
|
361
|
+
"""
|
|
362
|
+
if not isinstance(seq, PackedSequence):
|
|
363
|
+
raise SequenceTypeError(
|
|
364
|
+
f"seq debe ser PackedSequence, se recibió {type(seq).__name__!r}. "
|
|
365
|
+
"Crea la secuencia con SmartImporter.from_string() o SmartImporter.from_file()."
|
|
366
|
+
)
|
|
367
|
+
if seq.seq_type != SeqType.NUCLEOTIDE:
|
|
368
|
+
raise SequenceTypeError(
|
|
369
|
+
f"Se esperaba SeqType.NUCLEOTIDE, se recibió {seq.seq_type.name}. "
|
|
370
|
+
"translate_all_frames() solo traduce ADN/ARN."
|
|
371
|
+
)
|
|
372
|
+
if seq.n_symbols < 3:
|
|
373
|
+
raise SequenceValueError(
|
|
374
|
+
f"Secuencia demasiado corta ({seq.n_symbols} nt): "
|
|
375
|
+
"se necesitan al menos 3 nucleótidos para un codón."
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
rc_seq = seq.reverse_complement()
|
|
379
|
+
results: list[PackedSequence] = []
|
|
380
|
+
|
|
381
|
+
for strand_codes, strand_sign in [
|
|
382
|
+
(seq.decode(), '+'),
|
|
383
|
+
(rc_seq.decode(), '-'),
|
|
384
|
+
]:
|
|
385
|
+
for offset in range(3):
|
|
386
|
+
frame_codes = strand_codes[offset:]
|
|
387
|
+
if len(frame_codes) < 3:
|
|
388
|
+
continue
|
|
389
|
+
frame_label = f"{strand_sign}{offset + 1}"
|
|
390
|
+
|
|
391
|
+
try:
|
|
392
|
+
orf_start = cls._find_orf_start(frame_codes)
|
|
393
|
+
except (TranslationError, ValueError):
|
|
394
|
+
continue # no ATG in this frame — skip silently
|
|
395
|
+
|
|
396
|
+
orf = frame_codes[orf_start:]
|
|
397
|
+
n_codons = len(orf) // 3
|
|
398
|
+
if n_codons == 0:
|
|
399
|
+
continue
|
|
400
|
+
|
|
401
|
+
# Translate codons (C engine or NumPy)
|
|
402
|
+
if _C_AVAILABLE:
|
|
403
|
+
aa_codes: np.ndarray = _c_translate(cls.CODON_LUT, orf, n_codons)
|
|
404
|
+
else:
|
|
405
|
+
codons = orf[: n_codons * 3].reshape(n_codons, 3)
|
|
406
|
+
idx_u16 = (codons.astype(np.uint16) * cls._CODON_WEIGHTS).sum(axis=1)
|
|
407
|
+
valid = idx_u16 < np.uint16(64)
|
|
408
|
+
safe_idx = np.where(valid, idx_u16, np.uint16(0)).astype(np.uint8)
|
|
409
|
+
aa_codes = np.where(
|
|
410
|
+
valid, cls.CODON_LUT[safe_idx], np.uint8(BioCode.UNK)
|
|
411
|
+
).astype(np.uint8)
|
|
412
|
+
|
|
413
|
+
# Truncate at first in-frame STOP codon
|
|
414
|
+
stop_mask = aa_codes == np.uint8(BioCode.STOP)
|
|
415
|
+
if stop_mask.any():
|
|
416
|
+
aa_codes = aa_codes[: int(np.argmax(stop_mask))]
|
|
417
|
+
|
|
418
|
+
n_aa = int(len(aa_codes))
|
|
419
|
+
if n_aa == 0:
|
|
420
|
+
continue
|
|
421
|
+
|
|
422
|
+
if warn_short and n_aa < cls._MIN_AA_LEN:
|
|
423
|
+
warnings.warn(
|
|
424
|
+
f"Frame {frame_label}: proteína corta "
|
|
425
|
+
f"({n_aa} aa < {cls._MIN_AA_LEN} aa).",
|
|
426
|
+
UserWarning,
|
|
427
|
+
stacklevel=2,
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
abs_pos = offset + orf_start
|
|
431
|
+
results.append(PackedSequence(
|
|
432
|
+
header = f"[PROT | frame {frame_label} | ORF@{abs_pos}] {seq.header}",
|
|
433
|
+
seq_type = SeqType.PROTEIN,
|
|
434
|
+
n_symbols = n_aa,
|
|
435
|
+
data = BitPacker.pack(aa_codes),
|
|
436
|
+
))
|
|
437
|
+
|
|
438
|
+
return results
|
|
439
|
+
|
|
440
|
+
# ── Private helpers ────────────────────────────────────────────────────────
|
|
441
|
+
|
|
442
|
+
@classmethod
|
|
443
|
+
def _find_orf_start(cls, codes: np.ndarray) -> int:
|
|
444
|
+
"""
|
|
445
|
+
Locate the first ATG/AUG codon using a vectorised sliding-window view.
|
|
446
|
+
|
|
447
|
+
Uses ``numpy.lib.stride_tricks.sliding_window_view`` to create a
|
|
448
|
+
zero-copy ``(n-2, 3)`` view over *codes*, then compares every
|
|
449
|
+
3-mer against ``[0, 3, 2]`` (= A, T/U, G) in one broadcast op.
|
|
450
|
+
``np.argmax`` performs a C-level scan and returns the first hit index.
|
|
451
|
+
|
|
452
|
+
Parameters
|
|
453
|
+
----------
|
|
454
|
+
codes : np.ndarray, dtype uint8
|
|
455
|
+
Decoded nucleotide code array (values 0–3 for ACGTU).
|
|
456
|
+
|
|
457
|
+
Returns
|
|
458
|
+
-------
|
|
459
|
+
int
|
|
460
|
+
Zero-based index of the first nucleotide of the ATG codon.
|
|
461
|
+
|
|
462
|
+
Raises
|
|
463
|
+
------
|
|
464
|
+
ValueError
|
|
465
|
+
If no ATG/AUG codon is found (Fail-Fast).
|
|
466
|
+
"""
|
|
467
|
+
if _C_AVAILABLE:
|
|
468
|
+
pos = _c_find_atg(codes)
|
|
469
|
+
if pos == -1:
|
|
470
|
+
raise TranslationError(
|
|
471
|
+
"No se encontró ningún codón de inicio ATG/AUG en la secuencia. "
|
|
472
|
+
"Comprueba que sea una secuencia codificante (CDS) completa. "
|
|
473
|
+
"Si la auto-detección de tipo falla, importa con force_type=SeqType.NUCLEOTIDE."
|
|
474
|
+
)
|
|
475
|
+
return pos
|
|
476
|
+
|
|
477
|
+
# fallback NumPy: sliding-window vectorizado
|
|
478
|
+
windows: np.ndarray = sliding_window_view(codes, window_shape=3)
|
|
479
|
+
hit_mask: np.ndarray = np.all(windows == cls._START_CODON, axis=1)
|
|
480
|
+
first_hit: int = int(np.argmax(hit_mask))
|
|
481
|
+
if not hit_mask[first_hit]:
|
|
482
|
+
raise TranslationError(
|
|
483
|
+
"No se encontró ningún codón de inicio ATG/AUG en la secuencia. "
|
|
484
|
+
"Asegúrate de que sea una secuencia codificante (CDS) válida."
|
|
485
|
+
)
|
|
486
|
+
return first_hit
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
490
|
+
# §3 DEMO / SELF-TEST (python smart_translator.py)
|
|
491
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
492
|
+
|
|
493
|
+
if __name__ == "__main__":
|
|
494
|
+
import sys
|
|
495
|
+
import time
|
|
496
|
+
sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parent.parent))
|
|
497
|
+
from bioforge import SmartImporter, compute_stats
|
|
498
|
+
|
|
499
|
+
W = 65
|
|
500
|
+
print("═" * W)
|
|
501
|
+
print(" smart_translator.py — Vectorised Genetic Code demo")
|
|
502
|
+
print("═" * W)
|
|
503
|
+
|
|
504
|
+
# ── Inspect LUT ────────────────────────────────────────────────────────────
|
|
505
|
+
lut = SmartTranslator.CODON_LUT
|
|
506
|
+
n_stop = int((lut == BioCode.STOP).sum())
|
|
507
|
+
n_aa = int(((lut >= 4) & (lut <= 23)).sum())
|
|
508
|
+
print(f"\n CODON_LUT ({len(lut)} entries)")
|
|
509
|
+
print(f" ├─ Sense codons (AA 4–23) : {n_aa}")
|
|
510
|
+
print(f" ├─ STOP codons (24) : {n_stop}")
|
|
511
|
+
print(f" └─ Filled / UNK entries : {n_aa + n_stop} / {64 - n_aa - n_stop}")
|
|
512
|
+
|
|
513
|
+
# ── Test 1: HBB_HUMAN — long CDS with upstream noise ──────────────────────
|
|
514
|
+
# First 48 codons of human haemoglobin beta-chain mRNA (NM_000518),
|
|
515
|
+
# prefixed with 8 nt of 5′ UTR noise to exercise the ORF finder.
|
|
516
|
+
HBB_FASTA = """\
|
|
517
|
+
>NM_000518.5|HBB_HUMAN|partial CDS with 5-UTR noise
|
|
518
|
+
NNNAACCCATGGTGCACCTGACTCCTGAGGAGAAGTCTGCCGTTACTGCCCTGTGGGGCAAGGTGAACGT
|
|
519
|
+
GGATGAAGTTGGTGGTGAGGCCCTGGGCAGGCTGCTGGTGGTCTACCCTTGGACCCAGAGGTTCTTTGAG
|
|
520
|
+
TCCTTTGGGGATCTGTCCACTCCTGATGCTGTTATGGGCAACCCTAAGGTGAAGGCTCATGGCAAGAAAG
|
|
521
|
+
TGCTCGGTGCCTTTAGTGATGGCCTGGCTCACCTGGACAACCTCAAGGGCACCTTTGCCACACTGAGTGA
|
|
522
|
+
GCTGCACTGTGACAAGCTGCACGTGGATCCTGAGAACTTCAGGCTCCTGGGCAACGTGCTGGTCTGTGTG
|
|
523
|
+
CTGGCCCATCACTTTGGCAAAGAATTCACCCCACCAGTGCAGGCTGCCTATCAGAAAGTGGTGGCTGGTGT
|
|
524
|
+
GGCTAATGCCCTGGCCCACAAGTATCACTAA
|
|
525
|
+
"""
|
|
526
|
+
|
|
527
|
+
# Test 2: short synthetic ORF to trigger UserWarning
|
|
528
|
+
SHORT_FASTA = """\
|
|
529
|
+
>synthetic|short_ORF|triggers_warning
|
|
530
|
+
ATGAAACGCATTAGCACCACCATTACCACCACCATCACCATTACCACAGGTAACGGTGCGGGCTGA
|
|
531
|
+
"""
|
|
532
|
+
|
|
533
|
+
test_cases = [
|
|
534
|
+
("HBB_HUMAN (full CDS)", HBB_FASTA, False),
|
|
535
|
+
("Synthetic short ORF", SHORT_FASTA, True),
|
|
536
|
+
]
|
|
537
|
+
|
|
538
|
+
for title, fasta, expect_warn in test_cases:
|
|
539
|
+
print(f"\n {'─'*55}")
|
|
540
|
+
print(f" {title}")
|
|
541
|
+
print(f" {'─'*55}")
|
|
542
|
+
|
|
543
|
+
nuc_seq = SmartImporter.from_string(fasta, force_type=SeqType.NUCLEOTIDE)[0]
|
|
544
|
+
print(f" Input : {repr(nuc_seq)}")
|
|
545
|
+
|
|
546
|
+
import warnings as _w
|
|
547
|
+
with _w.catch_warnings(record=True) as caught:
|
|
548
|
+
_w.simplefilter("always")
|
|
549
|
+
t0 = time.perf_counter()
|
|
550
|
+
prot_seq = SmartTranslator.translate(nuc_seq)
|
|
551
|
+
elapsed_us = (time.perf_counter() - t0) * 1e6
|
|
552
|
+
|
|
553
|
+
prot_str = prot_seq.to_string()
|
|
554
|
+
stats = compute_stats(prot_seq)
|
|
555
|
+
|
|
556
|
+
print(f" Output : {repr(prot_seq)}")
|
|
557
|
+
print(f" Protein : {prot_str[:60]}{'…' if len(prot_str)>60 else ''}")
|
|
558
|
+
print(f" Elapsed : {elapsed_us:.1f} µs")
|
|
559
|
+
print(f" Mem save: {stats.compression_pct:.1f}% "
|
|
560
|
+
f"({stats.n_packed_bytes} B packed vs {stats.n_symbols} B naive)")
|
|
561
|
+
|
|
562
|
+
if caught:
|
|
563
|
+
print(f" ⚠ UserWarning: {caught[0].message}")
|
|
564
|
+
if expect_warn:
|
|
565
|
+
print(" UserWarning raised as expected ✅")
|
|
566
|
+
else:
|
|
567
|
+
print(" No warning (sequence ≥ 50 aa) ✅")
|
|
568
|
+
|
|
569
|
+
# Round-trip integrity
|
|
570
|
+
aa_codes = prot_seq.decode()
|
|
571
|
+
repacked = BitPacker.pack(aa_codes)
|
|
572
|
+
rt_ok = np.array_equal(repacked, prot_seq.data)
|
|
573
|
+
print(f" Round-trip: {'✅' if rt_ok else '❌'} "
|
|
574
|
+
f"({prot_seq.n_symbols} aa → {prot_seq.packed_bytes} B → {'OK' if rt_ok else 'FAIL'})")
|
|
575
|
+
|
|
576
|
+
# ── Test 3: Error paths ────────────────────────────────────────────────────
|
|
577
|
+
print(f"\n {'─'*55}")
|
|
578
|
+
print(" Error-path validation")
|
|
579
|
+
print(f" {'─'*55}")
|
|
580
|
+
|
|
581
|
+
# TypeError: protein input
|
|
582
|
+
prot_input = PackedSequence(
|
|
583
|
+
header="fake_prot", seq_type=SeqType.PROTEIN, n_symbols=3,
|
|
584
|
+
data=BitPacker.pack(np.array([4, 5, 6], dtype=np.uint8)),
|
|
585
|
+
)
|
|
586
|
+
try:
|
|
587
|
+
SmartTranslator.translate(prot_input)
|
|
588
|
+
print(" TypeError not raised ❌")
|
|
589
|
+
except TypeError as exc:
|
|
590
|
+
print(f" TypeError ✅ → {exc}")
|
|
591
|
+
|
|
592
|
+
# ValueError: no ATG
|
|
593
|
+
no_atg_fasta = ">noatg\nCCCGGGTTTACCCACC\n"
|
|
594
|
+
no_atg_seq = SmartImporter.from_string(no_atg_fasta, force_type=SeqType.NUCLEOTIDE)[0]
|
|
595
|
+
try:
|
|
596
|
+
SmartTranslator.translate(no_atg_seq)
|
|
597
|
+
print(" ValueError (no ATG) not raised ❌")
|
|
598
|
+
except ValueError as exc:
|
|
599
|
+
print(f" ValueError (no ATG) ✅ → {exc}")
|
|
600
|
+
|
|
601
|
+
print(f"\n{'═' * W}\n")
|