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.
Files changed (38) hide show
  1. {bioforge-3.3.0 → bioforge-4.0.0}/PKG-INFO +16 -7
  2. {bioforge-3.3.0 → bioforge-4.0.0}/README.md +15 -6
  3. {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/__init__.py +1 -1
  4. {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/engine/engine.c +2 -1
  5. {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/engine/engine.dll +0 -0
  6. {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/genomemap.py +119 -27
  7. {bioforge-3.3.0 → bioforge-4.0.0}/bioforge.egg-info/PKG-INFO +16 -7
  8. {bioforge-3.3.0 → bioforge-4.0.0}/tests/test_genomemap.py +50 -0
  9. {bioforge-3.3.0 → bioforge-4.0.0}/LICENSE +0 -0
  10. {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/aligner.py +0 -0
  11. {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/analyze.py +0 -0
  12. {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/bgzf.py +0 -0
  13. {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/biocore.py +0 -0
  14. {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/engine/__init__.py +0 -0
  15. {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/engine/_loader.py +0 -0
  16. {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/engine/build.py +0 -0
  17. {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/minimizers.py +0 -0
  18. {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/qcreport.py +0 -0
  19. {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/refindex.py +0 -0
  20. {bioforge-3.3.0 → bioforge-4.0.0}/bioforge/smart_translator.py +0 -0
  21. {bioforge-3.3.0 → bioforge-4.0.0}/bioforge.egg-info/SOURCES.txt +0 -0
  22. {bioforge-3.3.0 → bioforge-4.0.0}/bioforge.egg-info/dependency_links.txt +0 -0
  23. {bioforge-3.3.0 → bioforge-4.0.0}/bioforge.egg-info/entry_points.txt +0 -0
  24. {bioforge-3.3.0 → bioforge-4.0.0}/bioforge.egg-info/requires.txt +0 -0
  25. {bioforge-3.3.0 → bioforge-4.0.0}/bioforge.egg-info/top_level.txt +0 -0
  26. {bioforge-3.3.0 → bioforge-4.0.0}/pyproject.toml +0 -0
  27. {bioforge-3.3.0 → bioforge-4.0.0}/setup.cfg +0 -0
  28. {bioforge-3.3.0 → bioforge-4.0.0}/setup.py +0 -0
  29. {bioforge-3.3.0 → bioforge-4.0.0}/tests/test_aligner.py +0 -0
  30. {bioforge-3.3.0 → bioforge-4.0.0}/tests/test_analyze.py +0 -0
  31. {bioforge-3.3.0 → bioforge-4.0.0}/tests/test_bgzf.py +0 -0
  32. {bioforge-3.3.0 → bioforge-4.0.0}/tests/test_biocore.py +0 -0
  33. {bioforge-3.3.0 → bioforge-4.0.0}/tests/test_errors.py +0 -0
  34. {bioforge-3.3.0 → bioforge-4.0.0}/tests/test_minimizers.py +0 -0
  35. {bioforge-3.3.0 → bioforge-4.0.0}/tests/test_qcreport.py +0 -0
  36. {bioforge-3.3.0 → bioforge-4.0.0}/tests/test_refindex.py +0 -0
  37. {bioforge-3.3.0 → bioforge-4.0.0}/tests/test_streaming.py +0 -0
  38. {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.3.0
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
- mapper = GenomeAligner(reference_sequence, k=15, w=10) # builds the index once
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
- # read1 1000 0 1000 + chr1 4600000 733120 734118 980 998 60 ...
570
- print(m.strand, m.target_start, f"{m.identity:.1%}")
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 both strands, tolerates mismatches/indels, and reports a mapping
574
- quality. The chaining dynamic program runs in the C engine (with a NumPy
575
- fallback).
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
- mapper = GenomeAligner(reference_sequence, k=15, w=10) # builds the index once
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
- # read1 1000 0 1000 + chr1 4600000 733120 734118 980 998 60 ...
250
- print(m.strand, m.target_start, f"{m.identity:.1%}")
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 both strands, tolerates mismatches/indels, and reports a mapping
254
- quality. The chaining dynamic program runs in the C engine (with a NumPy
255
- fallback).
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
 
@@ -36,7 +36,7 @@ from .biocore import (
36
36
  from .genomemap import GenomeAligner, Mapping
37
37
  from .smart_translator import SmartTranslator
38
38
 
39
- __version__ = "3.3.0"
39
+ __version__ = "4.0.0"
40
40
  __author__ = "Aarón Aranda Torrijos"
41
41
 
42
42
  __all__ = [
@@ -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 + 1.0);
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
  }
@@ -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
- _GAP_W = 0.2 # peso lineal de la penalización por hueco
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 + 1.0), 0.0)
134
- sc = np.where(valid, f[lo:i] + match - (_GAP_W * gap + logterm), -np.inf)
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, _GAP_W)
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", target_name: str = "ref") -> str:
258
- """Serializa a una línea PAF (el formato de minimap2)."""
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, target_name, self.target_len,
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) -> Optional[Mapping]:
302
- """Fase 5: alinea (banded) la región de la cadena y arma el Mapping."""
303
- ref_sub = ref[ch.ref_start:ch.ref_end]
304
- read_sub = read[ch.read_start:ch.read_end]
305
- if ch.strand == 1:
306
- read_sub = _revcomp(read_sub)
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 * ch.k, \
319
- ch.ref_end - ch.ref_start, 1.0
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=len(read),
323
- query_start=ch.read_start, query_end=ch.read_end,
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=ch.ref_start, target_end=ch.ref_end,
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: str, k: int = 15, w: int = 10,
403
+ def __init__(self, reference, k: int = 15, w: int = 10,
361
404
  max_occ: Optional[int] = 50, name: str = "ref"):
362
- self.reference = reference.upper()
363
- self.name = name
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
- mp = _extend(self.reference, read, ch)
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.3.0
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
- mapper = GenomeAligner(reference_sequence, k=15, w=10) # builds the index once
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
- # read1 1000 0 1000 + chr1 4600000 733120 734118 980 998 60 ...
570
- print(m.strand, m.target_start, f"{m.identity:.1%}")
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 both strands, tolerates mismatches/indels, and reports a mapping
574
- quality. The chaining dynamic program runs in the C engine (with a NumPy
575
- fallback).
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