bioforge 3.3.0__tar.gz → 4.0.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.3.0 → bioforge-4.0.0}/PKG-INFO +16 -7
- {bioforge-3.3.0 → bioforge-4.0.0}/README.md +15 -6
- {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/__init__.py +1 -1
- {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/engine/engine.c +2 -1
- {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/engine/engine.dll +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/genomemap.py +119 -27
- {bioforge-3.3.0 → bioforge-4.0.0}/bioforge.egg-info/PKG-INFO +16 -7
- {bioforge-3.3.0 → bioforge-4.0.0}/tests/test_genomemap.py +50 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/LICENSE +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/aligner.py +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/analyze.py +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/bgzf.py +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/biocore.py +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/engine/__init__.py +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/engine/_loader.py +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/engine/build.py +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/minimizers.py +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/qcreport.py +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/refindex.py +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/smart_translator.py +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/bioforge.egg-info/SOURCES.txt +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/bioforge.egg-info/dependency_links.txt +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/bioforge.egg-info/entry_points.txt +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/bioforge.egg-info/requires.txt +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/bioforge.egg-info/top_level.txt +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/pyproject.toml +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/setup.cfg +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/setup.py +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/tests/test_aligner.py +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/tests/test_analyze.py +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/tests/test_bgzf.py +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/tests/test_biocore.py +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/tests/test_errors.py +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/tests/test_minimizers.py +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/tests/test_qcreport.py +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/tests/test_refindex.py +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/tests/test_streaming.py +0 -0
- {bioforge-3.3.0 → bioforge-4.0.0}/tests/test_translator.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bioforge
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.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
|
|
@@ -562,17 +562,26 @@ minimap2-style: minimizer seeding → chaining (C DP) → banded extension.
|
|
|
562
562
|
```python
|
|
563
563
|
from bioforge import GenomeAligner
|
|
564
564
|
|
|
565
|
-
|
|
565
|
+
# Single sequence, or a whole multi-contig genome:
|
|
566
|
+
mapper = GenomeAligner({"chr1": chr1_seq, "chr2": chr2_seq, "plasmid": p_seq})
|
|
566
567
|
|
|
567
568
|
for m in mapper.map(read):
|
|
568
569
|
print(m.to_paf()) # standard PAF, one line per mapping
|
|
569
|
-
|
|
570
|
-
|
|
570
|
+
print(m.target_name, m.strand, m.target_start, f"{m.identity:.1%}")
|
|
571
|
+
|
|
572
|
+
# Map many reads in parallel (uses processes):
|
|
573
|
+
results = mapper.map_batch(reads, n_processes=0) # 0 = all cores
|
|
571
574
|
```
|
|
572
575
|
|
|
573
|
-
Handles
|
|
574
|
-
|
|
575
|
-
|
|
576
|
+
Handles multi-chromosome references (reports the contig + local coordinates),
|
|
577
|
+
both strands, aligns the full read, tolerates mismatches/indels, and reports a
|
|
578
|
+
mapping quality. The minimizer and chaining kernels run in the C engine (with
|
|
579
|
+
NumPy fallbacks).
|
|
580
|
+
|
|
581
|
+
> **Speed, honestly:** indexing is fast, but read mapping is not yet
|
|
582
|
+
> competitive with hand-tuned C mappers like minimap2 — the mapping pipeline is
|
|
583
|
+
> being moved into the C engine. Use BioForge's mapper where its strengths fit:
|
|
584
|
+
> pure-Python/NumPy core, tiny footprint, `pip install` and go, no C toolchain.
|
|
576
585
|
|
|
577
586
|
### Full mutation analysis pipeline (DNA + protein)
|
|
578
587
|
|
|
@@ -242,17 +242,26 @@ minimap2-style: minimizer seeding → chaining (C DP) → banded extension.
|
|
|
242
242
|
```python
|
|
243
243
|
from bioforge import GenomeAligner
|
|
244
244
|
|
|
245
|
-
|
|
245
|
+
# Single sequence, or a whole multi-contig genome:
|
|
246
|
+
mapper = GenomeAligner({"chr1": chr1_seq, "chr2": chr2_seq, "plasmid": p_seq})
|
|
246
247
|
|
|
247
248
|
for m in mapper.map(read):
|
|
248
249
|
print(m.to_paf()) # standard PAF, one line per mapping
|
|
249
|
-
|
|
250
|
-
|
|
250
|
+
print(m.target_name, m.strand, m.target_start, f"{m.identity:.1%}")
|
|
251
|
+
|
|
252
|
+
# Map many reads in parallel (uses processes):
|
|
253
|
+
results = mapper.map_batch(reads, n_processes=0) # 0 = all cores
|
|
251
254
|
```
|
|
252
255
|
|
|
253
|
-
Handles
|
|
254
|
-
|
|
255
|
-
|
|
256
|
+
Handles multi-chromosome references (reports the contig + local coordinates),
|
|
257
|
+
both strands, aligns the full read, tolerates mismatches/indels, and reports a
|
|
258
|
+
mapping quality. The minimizer and chaining kernels run in the C engine (with
|
|
259
|
+
NumPy fallbacks).
|
|
260
|
+
|
|
261
|
+
> **Speed, honestly:** indexing is fast, but read mapping is not yet
|
|
262
|
+
> competitive with hand-tuned C mappers like minimap2 — the mapping pipeline is
|
|
263
|
+
> being moved into the C engine. Use BioForge's mapper where its strengths fit:
|
|
264
|
+
> pure-Python/NumPy core, tiny footprint, `pip install` and go, no C toolchain.
|
|
256
265
|
|
|
257
266
|
### Full mutation analysis pipeline (DNA + protein)
|
|
258
267
|
|
|
@@ -1570,8 +1570,9 @@ EXPORT void bio_chain_dp(const int64_t* x, const int64_t* y, int32_t n,
|
|
|
1570
1570
|
if (gap > max_gap) continue;
|
|
1571
1571
|
int64_t mn = (dx < dy) ? dx : dy;
|
|
1572
1572
|
double match = (double)((mn < (int64_t)k) ? mn : (int64_t)k);
|
|
1573
|
+
/* coste de hueco estilo minimap2: gap_w·|l| + 0.5·log2|l| (l≠0) */
|
|
1573
1574
|
double cost = gap_w * (double)gap;
|
|
1574
|
-
if (gap > 0) cost += log2((double)gap
|
|
1575
|
+
if (gap > 0) cost += 0.5 * log2((double)gap);
|
|
1575
1576
|
double sc = f[j] + match - cost;
|
|
1576
1577
|
if (sc > best) { best = sc; bp = j; }
|
|
1577
1578
|
}
|
|
Binary file
|
|
@@ -101,16 +101,17 @@ class Chain(NamedTuple):
|
|
|
101
101
|
# Parámetros de chaining (razonables; ajustables).
|
|
102
102
|
_MAX_GAP = 5000 # distancia máxima entre anclas consecutivas
|
|
103
103
|
_WINDOW = 64 # nº de predecesores que se examinan por ancla
|
|
104
|
-
|
|
104
|
+
_GAP_COEF = 0.01 # coste lineal de hueco = 0.01·k (fórmula de minimap2)
|
|
105
105
|
_MIN_ANCHORS = 2
|
|
106
106
|
|
|
107
107
|
|
|
108
|
-
def _chain_fill_numpy(xs: np.ndarray, ys: np.ndarray, k: int):
|
|
108
|
+
def _chain_fill_numpy(xs: np.ndarray, ys: np.ndarray, k: int, gap_w: float):
|
|
109
109
|
"""Fallback NumPy del DP de chaining (idéntico al C bio_chain_dp).
|
|
110
110
|
|
|
111
111
|
Bucle EXTERNO secuencial (f[i] depende de f[j<i]); bucle INTERNO sobre la
|
|
112
112
|
ventana de predecesores vectorizado. En empate gana el predecesor más
|
|
113
|
-
cercano (mismo desempate que el C).
|
|
113
|
+
cercano (mismo desempate que el C). Coste de hueco estilo minimap2:
|
|
114
|
+
``gap_w·|l| + 0.5·log2|l|``.
|
|
114
115
|
"""
|
|
115
116
|
n = xs.size
|
|
116
117
|
f = np.full(n, float(k), dtype=np.float64)
|
|
@@ -130,8 +131,8 @@ def _chain_fill_numpy(xs: np.ndarray, ys: np.ndarray, k: int):
|
|
|
130
131
|
if not valid.any():
|
|
131
132
|
continue
|
|
132
133
|
match = np.minimum(np.minimum(dx, dy), k).astype(np.float64)
|
|
133
|
-
logterm = np.where(gap > 0, np.log2(gap
|
|
134
|
-
sc = np.where(valid, f[lo:i] + match - (
|
|
134
|
+
logterm = np.where(gap > 0, 0.5 * np.log2(np.maximum(gap, 1)), 0.0)
|
|
135
|
+
sc = np.where(valid, f[lo:i] + match - (gap_w * gap + logterm), -np.inf)
|
|
135
136
|
b = sc.size - 1 - int(sc[::-1].argmax()) # empate → predecesor cercano
|
|
136
137
|
if sc[b] > kf:
|
|
137
138
|
f[i], prev[i] = float(sc[b]), lo + b
|
|
@@ -153,11 +154,12 @@ def _chain_one(x: np.ndarray, y: np.ndarray, k: int,
|
|
|
153
154
|
ys = np.ascontiguousarray(y[order], dtype=np.int64)
|
|
154
155
|
|
|
155
156
|
# Relleno del DP: C si está disponible (10-50× más rápido), si no NumPy.
|
|
157
|
+
gap_w = _GAP_COEF * k # 0.01·k (coste de hueco minimap2)
|
|
156
158
|
if C_CHAIN_AVAILABLE:
|
|
157
|
-
f, prev = c_chain_dp(xs, ys, k, _MAX_GAP, _WINDOW,
|
|
159
|
+
f, prev = c_chain_dp(xs, ys, k, _MAX_GAP, _WINDOW, gap_w)
|
|
158
160
|
prev = prev.astype(np.int64)
|
|
159
161
|
else:
|
|
160
|
-
f, prev = _chain_fill_numpy(xs, ys, k)
|
|
162
|
+
f, prev = _chain_fill_numpy(xs, ys, k, gap_w)
|
|
161
163
|
|
|
162
164
|
# Backtrack de cadenas no solapadas, por score descendente.
|
|
163
165
|
used = np.zeros(n, dtype=bool)
|
|
@@ -253,12 +255,19 @@ class Mapping(NamedTuple):
|
|
|
253
255
|
identity: float
|
|
254
256
|
chain_score: float
|
|
255
257
|
cigar: Optional[str]
|
|
258
|
+
target_name: str = "ref" # nombre del contig/cromosoma al que mapea
|
|
256
259
|
|
|
257
|
-
def to_paf(self, query_name: str = "query",
|
|
258
|
-
|
|
260
|
+
def to_paf(self, query_name: str = "query",
|
|
261
|
+
target_name: Optional[str] = None) -> str:
|
|
262
|
+
"""Serializa a una línea PAF (el formato de minimap2).
|
|
263
|
+
|
|
264
|
+
Si no se pasa ``target_name``, se usa el del propio mapeo (el contig al
|
|
265
|
+
que mapeó, útil con referencias multi-cromosoma).
|
|
266
|
+
"""
|
|
267
|
+
tname = target_name if target_name is not None else self.target_name
|
|
259
268
|
fields = [
|
|
260
269
|
query_name, self.query_len, self.query_start, self.query_end,
|
|
261
|
-
self.strand,
|
|
270
|
+
self.strand, tname, self.target_len,
|
|
262
271
|
self.target_start, self.target_end,
|
|
263
272
|
self.num_matches, self.block_len, self.mapq,
|
|
264
273
|
]
|
|
@@ -298,12 +307,31 @@ def _cigar(aln_ref: str, aln_read: str) -> tuple[str, int, int]:
|
|
|
298
307
|
return cigar, n_match, int(op.size)
|
|
299
308
|
|
|
300
309
|
|
|
301
|
-
def _extend(ref: str, read: str, ch: Chain
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
310
|
+
def _extend(ref: str, read: str, ch: Chain, k: int,
|
|
311
|
+
cstart: int, cend: int) -> Optional[Mapping]:
|
|
312
|
+
"""Fase 5: alinea el READ COMPLETO contra la referencia y arma el Mapping.
|
|
313
|
+
|
|
314
|
+
La ventana de referencia se sitúa por la diagonal de la cadena
|
|
315
|
+
(``d`` = posición ref donde cae el inicio del read) y se recorta al contig
|
|
316
|
+
``[cstart, cend)``. Así la alineación cubre todo el read (no solo la región
|
|
317
|
+
de la cadena); si el read sobresale por un borde del contig, esa parte queda
|
|
318
|
+
recortada (soft-clip) de forma natural.
|
|
319
|
+
"""
|
|
320
|
+
Lr = len(read)
|
|
321
|
+
if ch.strand == 0:
|
|
322
|
+
d = int(np.median(ch.anchor_ref - ch.anchor_read))
|
|
323
|
+
oriented = read
|
|
324
|
+
else:
|
|
325
|
+
# hebra inversa: se alinea revcomp(read) hacia delante; diagonal:
|
|
326
|
+
d = int(np.median(ch.anchor_ref + ch.anchor_read)) - (Lr - k)
|
|
327
|
+
oriented = _revcomp(read)
|
|
328
|
+
|
|
329
|
+
rs = max(cstart, d)
|
|
330
|
+
re = min(cend, d + Lr)
|
|
331
|
+
if re - rs < 1:
|
|
332
|
+
return None
|
|
333
|
+
ref_sub = ref[rs:re]
|
|
334
|
+
read_sub = oriented[rs - d:re - d] # porción del read dentro de la ventana
|
|
307
335
|
if not ref_sub or not read_sub:
|
|
308
336
|
return None
|
|
309
337
|
|
|
@@ -315,15 +343,20 @@ def _extend(ref: str, read: str, ch: Chain) -> Optional[Mapping]:
|
|
|
315
343
|
cigar, n_match, block = _cigar(res.aligned_a, res.aligned_b)
|
|
316
344
|
identity = res.identity
|
|
317
345
|
except Exception: # noqa: BLE001 — extensión best-effort
|
|
318
|
-
cigar, n_match, block, identity = None, ch.n_anchors *
|
|
319
|
-
|
|
346
|
+
cigar, n_match, block, identity = None, ch.n_anchors * k, re - rs, 1.0
|
|
347
|
+
|
|
348
|
+
# coords de query en el read ORIGINAL (hacia delante), aun en hebra inversa
|
|
349
|
+
if ch.strand == 0:
|
|
350
|
+
q_start, q_end = rs - d, re - d
|
|
351
|
+
else:
|
|
352
|
+
q_start, q_end = Lr - (re - d), Lr - (rs - d)
|
|
320
353
|
|
|
321
354
|
return Mapping(
|
|
322
|
-
query_len=
|
|
323
|
-
query_start=
|
|
355
|
+
query_len=Lr,
|
|
356
|
+
query_start=q_start, query_end=q_end,
|
|
324
357
|
strand="+" if ch.strand == 0 else "-",
|
|
325
358
|
target_len=len(ref),
|
|
326
|
-
target_start=
|
|
359
|
+
target_start=rs, target_end=re,
|
|
327
360
|
num_matches=n_match, block_len=block,
|
|
328
361
|
mapq=0, identity=identity, chain_score=ch.score, cigar=cigar,
|
|
329
362
|
)
|
|
@@ -355,19 +388,74 @@ def _mp_worker(args) -> "list[Mapping]":
|
|
|
355
388
|
|
|
356
389
|
|
|
357
390
|
class GenomeAligner:
|
|
358
|
-
"""Mapeador de reads contra una referencia (seed-chain-align).
|
|
391
|
+
"""Mapeador de reads contra una referencia (seed-chain-align).
|
|
392
|
+
|
|
393
|
+
La referencia puede ser:
|
|
394
|
+
• una cadena (un solo contig), o
|
|
395
|
+
• un dict ``{nombre: secuencia}`` o iterable de ``(nombre, secuencia)``
|
|
396
|
+
para varios cromosomas/contigs (genomas reales).
|
|
397
|
+
|
|
398
|
+
Multi-contig: los contigs se concatenan con separadores de ``N`` (que los
|
|
399
|
+
minimizers excluyen → ningún k-mer cruza fronteras). Se indexa el conjunto y
|
|
400
|
+
al mapear la posición global se traduce a (contig, posición local).
|
|
401
|
+
"""
|
|
359
402
|
|
|
360
|
-
def __init__(self, reference
|
|
403
|
+
def __init__(self, reference, k: int = 15, w: int = 10,
|
|
361
404
|
max_occ: Optional[int] = 50, name: str = "ref"):
|
|
362
|
-
self.reference
|
|
363
|
-
|
|
405
|
+
contigs = self._normalize(reference, name) # [(nombre, seq), ...]
|
|
406
|
+
sep = "N" * k # rompe k-meros de frontera
|
|
407
|
+
parts: list[str] = []
|
|
408
|
+
self._names: list[str] = []
|
|
409
|
+
self._starts_list: list[int] = []
|
|
410
|
+
self._lengths: list[int] = []
|
|
411
|
+
g = 0
|
|
412
|
+
for i, (nm, seq) in enumerate(contigs):
|
|
413
|
+
seq = seq.upper()
|
|
414
|
+
if i > 0:
|
|
415
|
+
parts.append(sep); g += len(sep)
|
|
416
|
+
self._names.append(str(nm))
|
|
417
|
+
self._starts_list.append(g)
|
|
418
|
+
self._lengths.append(len(seq))
|
|
419
|
+
parts.append(seq); g += len(seq)
|
|
420
|
+
|
|
421
|
+
self.reference = "".join(parts) # concatenado (con N)
|
|
422
|
+
self._starts = np.array(self._starts_list, dtype=np.int64)
|
|
423
|
+
self.name = self._names[0] if len(self._names) == 1 else "multi"
|
|
364
424
|
self.index = ReferenceIndex.from_sequence(self.reference, k=k, w=w,
|
|
365
425
|
max_occ=max_occ)
|
|
366
426
|
|
|
427
|
+
@staticmethod
|
|
428
|
+
def _normalize(reference, name: str) -> "list[tuple[str, str]]":
|
|
429
|
+
if isinstance(reference, str):
|
|
430
|
+
return [(name, reference)]
|
|
431
|
+
if isinstance(reference, dict):
|
|
432
|
+
return list(reference.items())
|
|
433
|
+
return [(n, s) for n, s in reference] # iterable de pares
|
|
434
|
+
|
|
367
435
|
@property
|
|
368
436
|
def k(self) -> int:
|
|
369
437
|
return self.index.k
|
|
370
438
|
|
|
439
|
+
@property
|
|
440
|
+
def n_contigs(self) -> int:
|
|
441
|
+
return len(self._names)
|
|
442
|
+
|
|
443
|
+
def _contig_of(self, gpos: int) -> int:
|
|
444
|
+
"""Índice del contig que contiene la posición global ``gpos``."""
|
|
445
|
+
i = int(np.searchsorted(self._starts, gpos, side="right")) - 1
|
|
446
|
+
return max(0, i)
|
|
447
|
+
|
|
448
|
+
def _localize(self, mp: Mapping) -> Mapping:
|
|
449
|
+
"""Traduce coords globales de un mapeo a (contig, coords locales)."""
|
|
450
|
+
ci = self._contig_of(mp.target_start)
|
|
451
|
+
cstart, clen = self._starts_list[ci], self._lengths[ci]
|
|
452
|
+
return mp._replace(
|
|
453
|
+
target_name=self._names[ci],
|
|
454
|
+
target_len=clen,
|
|
455
|
+
target_start=mp.target_start - cstart,
|
|
456
|
+
target_end=min(mp.target_end - cstart, clen),
|
|
457
|
+
)
|
|
458
|
+
|
|
371
459
|
def map(self, read: str, min_chain_score: float = 40.0,
|
|
372
460
|
max_hits: int = 5) -> list[Mapping]:
|
|
373
461
|
"""Mapea un read → lista de Mapping (primaria primero)."""
|
|
@@ -376,9 +464,13 @@ class GenomeAligner:
|
|
|
376
464
|
chains = chain(anchors, min_score=min_chain_score)[:max_hits]
|
|
377
465
|
mappings: list[Mapping] = []
|
|
378
466
|
for i, ch in enumerate(chains):
|
|
379
|
-
|
|
467
|
+
# Contig de la cadena → sus límites acotan la ventana de extensión.
|
|
468
|
+
ci = self._contig_of(ch.ref_start)
|
|
469
|
+
cstart = self._starts_list[ci]
|
|
470
|
+
cend = cstart + self._lengths[ci]
|
|
471
|
+
mp = _extend(self.reference, read, ch, self.k, cstart, cend)
|
|
380
472
|
if mp is not None:
|
|
381
|
-
mappings.append(mp._replace(mapq=_mapq(chains, i)))
|
|
473
|
+
mappings.append(self._localize(mp._replace(mapq=_mapq(chains, i))))
|
|
382
474
|
return mappings
|
|
383
475
|
|
|
384
476
|
def map_batch(self, reads, n_processes: int = 0,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bioforge
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.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
|
|
@@ -562,17 +562,26 @@ minimap2-style: minimizer seeding → chaining (C DP) → banded extension.
|
|
|
562
562
|
```python
|
|
563
563
|
from bioforge import GenomeAligner
|
|
564
564
|
|
|
565
|
-
|
|
565
|
+
# Single sequence, or a whole multi-contig genome:
|
|
566
|
+
mapper = GenomeAligner({"chr1": chr1_seq, "chr2": chr2_seq, "plasmid": p_seq})
|
|
566
567
|
|
|
567
568
|
for m in mapper.map(read):
|
|
568
569
|
print(m.to_paf()) # standard PAF, one line per mapping
|
|
569
|
-
|
|
570
|
-
|
|
570
|
+
print(m.target_name, m.strand, m.target_start, f"{m.identity:.1%}")
|
|
571
|
+
|
|
572
|
+
# Map many reads in parallel (uses processes):
|
|
573
|
+
results = mapper.map_batch(reads, n_processes=0) # 0 = all cores
|
|
571
574
|
```
|
|
572
575
|
|
|
573
|
-
Handles
|
|
574
|
-
|
|
575
|
-
|
|
576
|
+
Handles multi-chromosome references (reports the contig + local coordinates),
|
|
577
|
+
both strands, aligns the full read, tolerates mismatches/indels, and reports a
|
|
578
|
+
mapping quality. The minimizer and chaining kernels run in the C engine (with
|
|
579
|
+
NumPy fallbacks).
|
|
580
|
+
|
|
581
|
+
> **Speed, honestly:** indexing is fast, but read mapping is not yet
|
|
582
|
+
> competitive with hand-tuned C mappers like minimap2 — the mapping pipeline is
|
|
583
|
+
> being moved into the C engine. Use BioForge's mapper where its strengths fit:
|
|
584
|
+
> pure-Python/NumPy core, tiny footprint, `pip install` and go, no C toolchain.
|
|
576
585
|
|
|
577
586
|
### Full mutation analysis pipeline (DNA + protein)
|
|
578
587
|
|
|
@@ -146,6 +146,56 @@ def test_map_batch_igual_que_secuencial():
|
|
|
146
146
|
assert [m.target_start for m in got] == [m.target_start for m in exp]
|
|
147
147
|
|
|
148
148
|
|
|
149
|
+
def test_multicontig_mapea_al_contig_correcto():
|
|
150
|
+
rng = np.random.default_rng(30)
|
|
151
|
+
chr1 = "".join("ACGT"[i] for i in rng.integers(0, 4, 40_000))
|
|
152
|
+
chr2 = "".join("ACGT"[i] for i in rng.integers(0, 4, 25_000))
|
|
153
|
+
plas = "".join("ACGT"[i] for i in rng.integers(0, 4, 6_000))
|
|
154
|
+
ga = GenomeAligner({"chr1": chr1, "chr2": chr2, "plasmid": plas}, k=15, w=10)
|
|
155
|
+
assert ga.n_contigs == 3
|
|
156
|
+
|
|
157
|
+
# read de chr2: contig y coords LOCALES correctas
|
|
158
|
+
mp = ga.map(chr2[12_000:12_500])[0]
|
|
159
|
+
assert mp.target_name == "chr2"
|
|
160
|
+
assert mp.target_len == len(chr2)
|
|
161
|
+
assert mp.target_start - mp.query_start == 12_000 # diagonal local
|
|
162
|
+
assert mp.identity > 0.99
|
|
163
|
+
|
|
164
|
+
# read del plasmid en hebra inversa
|
|
165
|
+
mp = ga.map(_revcomp(plas[2_000:2_400]))[0]
|
|
166
|
+
assert mp.target_name == "plasmid"
|
|
167
|
+
assert mp.strand == "-"
|
|
168
|
+
assert mp.target_start < 2_400 and mp.target_end > 0
|
|
169
|
+
|
|
170
|
+
# el PAF lleva el contig correcto sin pasar target_name
|
|
171
|
+
assert ga.map(chr1[5_000:5_500])[0].to_paf().split("\t")[5] == "chr1"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_extension_cubre_el_read_entero():
|
|
175
|
+
# La extensión alinea el read COMPLETO (no solo la región de la cadena).
|
|
176
|
+
genoma = _rng_seq(60_000, 33)
|
|
177
|
+
ga = GenomeAligner(genoma, k=15, w=10)
|
|
178
|
+
o, Lr = 25_000, 500
|
|
179
|
+
mp = ga.map(genoma[o : o + Lr])[0]
|
|
180
|
+
assert mp.query_start == 0 # empieza en el inicio del read…
|
|
181
|
+
assert mp.query_end == Lr # …y llega al final
|
|
182
|
+
assert mp.target_start == o # posición exacta (no + offset de minimizer)
|
|
183
|
+
assert mp.identity > 0.99
|
|
184
|
+
# con mutaciones también cubre el read entero
|
|
185
|
+
read = _mutate(genoma[o : o + 600], n_mut=12, seed_=2)
|
|
186
|
+
mp = ga.map(read)[0]
|
|
187
|
+
assert mp.query_start == 0 and mp.query_end == 600
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_multicontig_lista_de_pares():
|
|
191
|
+
# también acepta un iterable de (nombre, secuencia)
|
|
192
|
+
a = _rng_seq(8000, 31)
|
|
193
|
+
b = _rng_seq(8000, 32)
|
|
194
|
+
ga = GenomeAligner([("A", a), ("B", b)], k=15, w=10)
|
|
195
|
+
assert ga.n_contigs == 2
|
|
196
|
+
assert ga.map(b[3000:3400])[0].target_name == "B"
|
|
197
|
+
|
|
198
|
+
|
|
149
199
|
def test_formato_paf():
|
|
150
200
|
genoma = _rng_seq(30_000, 16)
|
|
151
201
|
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
|