bioforge 3.0.0__tar.gz → 3.2.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.
- {bioforge-3.0.0 → bioforge-3.2.0}/PKG-INFO +1 -1
- {bioforge-3.0.0 → bioforge-3.2.0}/bioforge/__init__.py +1 -1
- {bioforge-3.0.0 → bioforge-3.2.0}/bioforge/aligner.py +9 -4
- {bioforge-3.0.0 → bioforge-3.2.0}/bioforge/genomemap.py +47 -1
- {bioforge-3.0.0 → bioforge-3.2.0}/bioforge.egg-info/PKG-INFO +1 -1
- {bioforge-3.0.0 → bioforge-3.2.0}/tests/test_aligner.py +17 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/tests/test_genomemap.py +17 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/LICENSE +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/README.md +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/bioforge/analyze.py +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/bioforge/bgzf.py +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/bioforge/biocore.py +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/bioforge/engine/__init__.py +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/bioforge/engine/_loader.py +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/bioforge/engine/build.py +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/bioforge/engine/engine.c +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/bioforge/engine/engine.dll +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/bioforge/minimizers.py +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/bioforge/qcreport.py +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/bioforge/refindex.py +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/bioforge/smart_translator.py +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/bioforge.egg-info/SOURCES.txt +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/bioforge.egg-info/dependency_links.txt +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/bioforge.egg-info/entry_points.txt +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/bioforge.egg-info/requires.txt +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/bioforge.egg-info/top_level.txt +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/pyproject.toml +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/setup.cfg +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/setup.py +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/tests/test_analyze.py +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/tests/test_bgzf.py +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/tests/test_biocore.py +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/tests/test_errors.py +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/tests/test_minimizers.py +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/tests/test_qcreport.py +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/tests/test_refindex.py +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/tests/test_streaming.py +0 -0
- {bioforge-3.0.0 → bioforge-3.2.0}/tests/test_translator.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bioforge
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Summary: High-performance bioinformatics engine for Edge Computing — 5-bit encoding, vectorised NumPy core, optional C backend
|
|
5
5
|
Author-email: Aarón Aranda Torrijos <noe9601@gmail.com>
|
|
6
6
|
License: # PolyForm Noncommercial License 1.0.0
|
|
@@ -179,6 +179,7 @@ class SequenceAligner:
|
|
|
179
179
|
seq_b: PackedSequence,
|
|
180
180
|
mode: Literal['global', 'semi-global'] = 'global',
|
|
181
181
|
band: int | None = None,
|
|
182
|
+
detect_mutations: bool = True,
|
|
182
183
|
) -> AlignmentResult:
|
|
183
184
|
"""
|
|
184
185
|
Align seq_a (reference) against seq_b (query) — Needleman-Wunsch.
|
|
@@ -238,7 +239,8 @@ class SequenceAligner:
|
|
|
238
239
|
raise AlignmentError(f"band debe ser ≥ 1, se recibió {band}.")
|
|
239
240
|
if C_AVAILABLE:
|
|
240
241
|
return cls._align_banded_c(
|
|
241
|
-
codes_a, codes_b, m, n, band, mode, seq_a.seq_type
|
|
242
|
+
codes_a, codes_b, m, n, band, mode, seq_a.seq_type,
|
|
243
|
+
detect_mutations,
|
|
242
244
|
)
|
|
243
245
|
# NumPy fallback: banded fill sobre matriz completa
|
|
244
246
|
if m > cls._MAX_SAFE_LEN or n > cls._MAX_SAFE_LEN:
|
|
@@ -261,7 +263,8 @@ class SequenceAligner:
|
|
|
261
263
|
)
|
|
262
264
|
|
|
263
265
|
if C_AVAILABLE:
|
|
264
|
-
return cls._align_c(codes_a, codes_b, m, n, mode, seq_a.seq_type
|
|
266
|
+
return cls._align_c(codes_a, codes_b, m, n, mode, seq_a.seq_type,
|
|
267
|
+
detect_mutations)
|
|
265
268
|
|
|
266
269
|
H = cls._fill_matrix(codes_a, codes_b, m, n, mode)
|
|
267
270
|
return cls._traceback(H, codes_a, codes_b, m, n, mode, seq_a.seq_type)
|
|
@@ -361,6 +364,7 @@ class SequenceAligner:
|
|
|
361
364
|
band: int,
|
|
362
365
|
mode: str,
|
|
363
366
|
seq_type: SeqType,
|
|
367
|
+
detect_mutations: bool = True,
|
|
364
368
|
) -> AlignmentResult:
|
|
365
369
|
decode_bytes = (
|
|
366
370
|
_NUC_DECODE_BYTES if seq_type == SeqType.NUCLEOTIDE else _AA_DECODE_BYTES
|
|
@@ -370,7 +374,7 @@ class SequenceAligner:
|
|
|
370
374
|
int(cls.MATCH), int(cls.MISMATCH), int(cls.GAP),
|
|
371
375
|
band, mode,
|
|
372
376
|
)
|
|
373
|
-
mutations = cls._detect_mutations(aligned_a, aligned_b)
|
|
377
|
+
mutations = cls._detect_mutations(aligned_a, aligned_b) if detect_mutations else []
|
|
374
378
|
aln_len = n_matches + n_mismatches + n_gaps
|
|
375
379
|
identity = n_matches / aln_len if aln_len else 0.0
|
|
376
380
|
return AlignmentResult(
|
|
@@ -389,6 +393,7 @@ class SequenceAligner:
|
|
|
389
393
|
n: int,
|
|
390
394
|
mode: str,
|
|
391
395
|
seq_type: SeqType,
|
|
396
|
+
detect_mutations: bool = True,
|
|
392
397
|
) -> AlignmentResult:
|
|
393
398
|
decode_bytes = (
|
|
394
399
|
_NUC_DECODE_BYTES if seq_type == SeqType.NUCLEOTIDE else _AA_DECODE_BYTES
|
|
@@ -398,7 +403,7 @@ class SequenceAligner:
|
|
|
398
403
|
int(cls.MATCH), int(cls.MISMATCH), int(cls.GAP),
|
|
399
404
|
mode,
|
|
400
405
|
)
|
|
401
|
-
mutations = cls._detect_mutations(aligned_a, aligned_b)
|
|
406
|
+
mutations = cls._detect_mutations(aligned_a, aligned_b) if detect_mutations else []
|
|
402
407
|
aln_len = n_matches + n_mismatches + n_gaps
|
|
403
408
|
identity = n_matches / aln_len if aln_len else 0.0
|
|
404
409
|
return AlignmentResult(
|
|
@@ -31,6 +31,8 @@ en Python ya es correcto y rápido para el nº de anclas habitual.
|
|
|
31
31
|
|
|
32
32
|
from __future__ import annotations
|
|
33
33
|
|
|
34
|
+
import os
|
|
35
|
+
from multiprocessing import Pool
|
|
34
36
|
from typing import NamedTuple, Optional
|
|
35
37
|
|
|
36
38
|
import numpy as np
|
|
@@ -308,7 +310,8 @@ def _extend(ref: str, read: str, ch: Chain) -> Optional[Mapping]:
|
|
|
308
310
|
band = min(256, abs(len(ref_sub) - len(read_sub)) + 32)
|
|
309
311
|
try:
|
|
310
312
|
res = SequenceAligner.align(_pack(ref_sub), _pack(read_sub),
|
|
311
|
-
mode="global", band=band
|
|
313
|
+
mode="global", band=band,
|
|
314
|
+
detect_mutations=False) # el mapeo no las usa
|
|
312
315
|
cigar, n_match, block = _cigar(res.aligned_a, res.aligned_b)
|
|
313
316
|
identity = res.identity
|
|
314
317
|
except Exception: # noqa: BLE001 — extensión best-effort
|
|
@@ -336,6 +339,21 @@ def _mapq(chains: list[Chain], i: int) -> int:
|
|
|
336
339
|
return int(max(0, min(60, round(60 * (1.0 - ratio)))))
|
|
337
340
|
|
|
338
341
|
|
|
342
|
+
# ── Workers de multiprocessing (a nivel de módulo → picklables) ─────────────────
|
|
343
|
+
_MP_ALIGNER: "Optional[GenomeAligner]" = None
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _mp_init(aligner: "GenomeAligner") -> None:
|
|
347
|
+
"""Inicializador de cada proceso: guarda el índice (se pasa una vez)."""
|
|
348
|
+
global _MP_ALIGNER
|
|
349
|
+
_MP_ALIGNER = aligner
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _mp_worker(args) -> "list[Mapping]":
|
|
353
|
+
read, min_chain_score, max_hits = args
|
|
354
|
+
return _MP_ALIGNER.map(read, min_chain_score, max_hits)
|
|
355
|
+
|
|
356
|
+
|
|
339
357
|
class GenomeAligner:
|
|
340
358
|
"""Mapeador de reads contra una referencia (seed-chain-align)."""
|
|
341
359
|
|
|
@@ -362,3 +380,31 @@ class GenomeAligner:
|
|
|
362
380
|
if mp is not None:
|
|
363
381
|
mappings.append(mp._replace(mapq=_mapq(chains, i)))
|
|
364
382
|
return mappings
|
|
383
|
+
|
|
384
|
+
def map_batch(self, reads, n_processes: int = 0,
|
|
385
|
+
min_chain_score: float = 40.0,
|
|
386
|
+
max_hits: int = 5) -> list[list[Mapping]]:
|
|
387
|
+
"""Mapea muchos reads en PARALELO → lista de listas de Mapping.
|
|
388
|
+
|
|
389
|
+
Los reads son independientes (paralelismo perfecto). Usa procesos
|
|
390
|
+
(multiprocessing) porque el GIL impide que los hilos escalen: el trabajo
|
|
391
|
+
por read es mayoritariamente Python. El índice se pasa una vez a cada
|
|
392
|
+
proceso; el orden de salida coincide con el de ``reads``.
|
|
393
|
+
|
|
394
|
+
``n_processes``: 0 = todos los núcleos · 1 = secuencial · N = N procesos.
|
|
395
|
+
|
|
396
|
+
Nota (Windows/macOS): el script que lo llame debe estar protegido por
|
|
397
|
+
``if __name__ == "__main__":`` (requisito de multiprocessing). Si el
|
|
398
|
+
arranque de procesos falla, cae a secuencial con gracia.
|
|
399
|
+
"""
|
|
400
|
+
reads = list(reads)
|
|
401
|
+
if n_processes == 1 or len(reads) <= 1:
|
|
402
|
+
return [self.map(r, min_chain_score, max_hits) for r in reads]
|
|
403
|
+
nt = (os.cpu_count() or 1) if n_processes <= 0 else n_processes
|
|
404
|
+
args = [(r, min_chain_score, max_hits) for r in reads]
|
|
405
|
+
chunk = max(1, len(reads) // (nt * 4))
|
|
406
|
+
try:
|
|
407
|
+
with Pool(processes=nt, initializer=_mp_init, initargs=(self,)) as pool:
|
|
408
|
+
return pool.map(_mp_worker, args, chunksize=chunk)
|
|
409
|
+
except Exception: # noqa: BLE001 — fallback secuencial
|
|
410
|
+
return [self.map(r, min_chain_score, max_hits) for r in reads]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bioforge
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.0
|
|
4
4
|
Summary: High-performance bioinformatics engine for Edge Computing — 5-bit encoding, vectorised NumPy core, optional C backend
|
|
5
5
|
Author-email: Aarón Aranda Torrijos <noe9601@gmail.com>
|
|
6
6
|
License: # PolyForm Noncommercial License 1.0.0
|
|
@@ -44,6 +44,23 @@ def _prot(seq: str, header: str = "h") -> PackedSequence:
|
|
|
44
44
|
# §1 PROPIEDADES MATEMÁTICAS
|
|
45
45
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
46
46
|
|
|
47
|
+
def test_detect_mutations_flag():
|
|
48
|
+
"""detect_mutations=False omite la lista de mutaciones sin tocar el resto."""
|
|
49
|
+
a = "ACGTACGTACGTACGTACGT"
|
|
50
|
+
b = "ACGTACGTTCGTACGTACGT" # una sustitución
|
|
51
|
+
full = SequenceAligner.align(_nuc(a), _nuc(b))
|
|
52
|
+
fast = SequenceAligner.align(_nuc(a), _nuc(b), detect_mutations=False)
|
|
53
|
+
assert len(full.mutations) == 1
|
|
54
|
+
assert fast.mutations == []
|
|
55
|
+
# todo lo demás idéntico (identidad, score, matches)
|
|
56
|
+
assert fast.identity == full.identity
|
|
57
|
+
assert fast.score == full.score
|
|
58
|
+
assert fast.n_matches == full.n_matches
|
|
59
|
+
# y también en la ruta banded (la que usa el mapeador)
|
|
60
|
+
fastb = SequenceAligner.align(_nuc(a), _nuc(b), band=6, detect_mutations=False)
|
|
61
|
+
assert fastb.mutations == []
|
|
62
|
+
|
|
63
|
+
|
|
47
64
|
def test_identical_sequences_perfect_score():
|
|
48
65
|
"""Dos secuencias idénticas → score máximo, identidad 1.0, 0 mutaciones."""
|
|
49
66
|
seq = "ATGGTGCACCTGACTCCTGAGGAGAAGTCTGCC"
|
|
@@ -129,6 +129,23 @@ def test_copias_multiples():
|
|
|
129
129
|
|
|
130
130
|
# ── Salida PAF ──────────────────────────────────────────────────────────────────
|
|
131
131
|
|
|
132
|
+
def test_map_batch_igual_que_secuencial():
|
|
133
|
+
genoma = _rng_seq(30_000, 20)
|
|
134
|
+
ga = GenomeAligner(genoma, k=15, w=10)
|
|
135
|
+
reads = [genoma[o : o + 400] for o in (1000, 5000, 12000, 20000)]
|
|
136
|
+
seq = [ga.map(r) for r in reads]
|
|
137
|
+
|
|
138
|
+
# ruta secuencial de la API (n_processes=1)
|
|
139
|
+
b1 = ga.map_batch(reads, n_processes=1)
|
|
140
|
+
assert [len(x) for x in b1] == [len(x) for x in seq]
|
|
141
|
+
|
|
142
|
+
# ruta paralela (procesos): mismo resultado y mismo orden
|
|
143
|
+
b2 = ga.map_batch(reads, n_processes=2)
|
|
144
|
+
assert len(b2) == len(reads)
|
|
145
|
+
for got, exp in zip(b2, seq, strict=True):
|
|
146
|
+
assert [m.target_start for m in got] == [m.target_start for m in exp]
|
|
147
|
+
|
|
148
|
+
|
|
132
149
|
def test_formato_paf():
|
|
133
150
|
genoma = _rng_seq(30_000, 16)
|
|
134
151
|
ga = GenomeAligner(genoma, k=15, w=10)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|