bioforge 2.3.0__tar.gz → 3.1.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-2.3.0 → bioforge-3.1.0}/PKG-INFO +44 -1
  2. {bioforge-2.3.0 → bioforge-3.1.0}/README.md +43 -0
  3. {bioforge-2.3.0 → bioforge-3.1.0}/bioforge/__init__.py +5 -1
  4. {bioforge-2.3.0 → bioforge-3.1.0}/bioforge/aligner.py +9 -4
  5. {bioforge-2.3.0 → bioforge-3.1.0}/bioforge/engine/_loader.py +40 -0
  6. {bioforge-2.3.0 → bioforge-3.1.0}/bioforge/engine/engine.c +43 -0
  7. {bioforge-2.3.0 → bioforge-3.1.0}/bioforge/engine/engine.dll +0 -0
  8. bioforge-3.1.0/bioforge/genomemap.py +365 -0
  9. bioforge-3.1.0/bioforge/minimizers.py +148 -0
  10. bioforge-3.1.0/bioforge/refindex.py +111 -0
  11. {bioforge-2.3.0 → bioforge-3.1.0}/bioforge.egg-info/PKG-INFO +44 -1
  12. {bioforge-2.3.0 → bioforge-3.1.0}/bioforge.egg-info/SOURCES.txt +6 -0
  13. {bioforge-2.3.0 → bioforge-3.1.0}/pyproject.toml +7 -3
  14. {bioforge-2.3.0 → bioforge-3.1.0}/tests/test_aligner.py +17 -0
  15. bioforge-3.1.0/tests/test_genomemap.py +144 -0
  16. bioforge-3.1.0/tests/test_minimizers.py +133 -0
  17. bioforge-3.1.0/tests/test_refindex.py +98 -0
  18. {bioforge-2.3.0 → bioforge-3.1.0}/LICENSE +0 -0
  19. {bioforge-2.3.0 → bioforge-3.1.0}/bioforge/analyze.py +0 -0
  20. {bioforge-2.3.0 → bioforge-3.1.0}/bioforge/bgzf.py +0 -0
  21. {bioforge-2.3.0 → bioforge-3.1.0}/bioforge/biocore.py +0 -0
  22. {bioforge-2.3.0 → bioforge-3.1.0}/bioforge/engine/__init__.py +0 -0
  23. {bioforge-2.3.0 → bioforge-3.1.0}/bioforge/engine/build.py +0 -0
  24. {bioforge-2.3.0 → bioforge-3.1.0}/bioforge/qcreport.py +0 -0
  25. {bioforge-2.3.0 → bioforge-3.1.0}/bioforge/smart_translator.py +0 -0
  26. {bioforge-2.3.0 → bioforge-3.1.0}/bioforge.egg-info/dependency_links.txt +0 -0
  27. {bioforge-2.3.0 → bioforge-3.1.0}/bioforge.egg-info/entry_points.txt +0 -0
  28. {bioforge-2.3.0 → bioforge-3.1.0}/bioforge.egg-info/requires.txt +0 -0
  29. {bioforge-2.3.0 → bioforge-3.1.0}/bioforge.egg-info/top_level.txt +0 -0
  30. {bioforge-2.3.0 → bioforge-3.1.0}/setup.cfg +0 -0
  31. {bioforge-2.3.0 → bioforge-3.1.0}/setup.py +0 -0
  32. {bioforge-2.3.0 → bioforge-3.1.0}/tests/test_analyze.py +0 -0
  33. {bioforge-2.3.0 → bioforge-3.1.0}/tests/test_bgzf.py +0 -0
  34. {bioforge-2.3.0 → bioforge-3.1.0}/tests/test_biocore.py +0 -0
  35. {bioforge-2.3.0 → bioforge-3.1.0}/tests/test_errors.py +0 -0
  36. {bioforge-2.3.0 → bioforge-3.1.0}/tests/test_qcreport.py +0 -0
  37. {bioforge-2.3.0 → bioforge-3.1.0}/tests/test_streaming.py +0 -0
  38. {bioforge-2.3.0 → bioforge-3.1.0}/tests/test_translator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bioforge
3
- Version: 2.3.0
3
+ Version: 3.1.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
@@ -554,6 +554,26 @@ for mut in result.mutations:
554
554
  # Mutation(kind='substitution', pos_a=18, pos_b=18, sym_a='A', sym_b='T')
555
555
  ```
556
556
 
557
+ ### Map long reads to a genome (Level 4 — seed-chain-align)
558
+
559
+ Locate reads in a reference far beyond what the O(m·n) aligner can handle,
560
+ minimap2-style: minimizer seeding → chaining (C DP) → banded extension.
561
+
562
+ ```python
563
+ from bioforge import GenomeAligner
564
+
565
+ mapper = GenomeAligner(reference_sequence, k=15, w=10) # builds the index once
566
+
567
+ for m in mapper.map(read):
568
+ 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%}")
571
+ ```
572
+
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
+
557
577
  ### Full mutation analysis pipeline (DNA + protein)
558
578
 
559
579
  ```python
@@ -722,6 +742,29 @@ python check.py
722
742
 
723
743
  ---
724
744
 
745
+ ## References & inspiration
746
+
747
+ BioForge's genome mapper (Level 4) is an **independent, from-scratch
748
+ implementation** of well-established, published algorithms. No third-party
749
+ source code is included or copied — only the *ideas* from the scientific
750
+ literature, which is what publishing them is for. With gratitude to:
751
+
752
+ - **Minimap2** — Li, H. (2018). *Minimap2: pairwise alignment for nucleotide
753
+ sequences.* Bioinformatics, 34(18), 3094–3100. The seed-chain-align strategy
754
+ and the chaining dynamic program that inspired `genomemap.py`.
755
+ ([paper](https://doi.org/10.1093/bioinformatics/bty191) ·
756
+ [MIT-licensed source](https://github.com/lh3/minimap2))
757
+ - **Minimizers** — Roberts, M., Hayes, W., Hunt, B. R., Mount, S. M., &
758
+ Yorke, J. A. (2004). *Reducing storage requirements for biological sequence
759
+ comparison.* Bioinformatics, 20(18), 3363–3369. The (w, k) minimizer sampling
760
+ behind `minimizers.py`.
761
+ - **Needleman–Wunsch** (1970) and **Smith–Waterman** (1981) — the classic
762
+ dynamic-programming alignments behind Level 3.
763
+
764
+ BioForge is not affiliated with or endorsed by the authors of the above.
765
+
766
+ ---
767
+
725
768
  ## Author
726
769
 
727
770
  **Aarón Aranda Torrijos** — [github.com/erlanders177](https://github.com/erlanders177)
@@ -234,6 +234,26 @@ for mut in result.mutations:
234
234
  # Mutation(kind='substitution', pos_a=18, pos_b=18, sym_a='A', sym_b='T')
235
235
  ```
236
236
 
237
+ ### Map long reads to a genome (Level 4 — seed-chain-align)
238
+
239
+ Locate reads in a reference far beyond what the O(m·n) aligner can handle,
240
+ minimap2-style: minimizer seeding → chaining (C DP) → banded extension.
241
+
242
+ ```python
243
+ from bioforge import GenomeAligner
244
+
245
+ mapper = GenomeAligner(reference_sequence, k=15, w=10) # builds the index once
246
+
247
+ for m in mapper.map(read):
248
+ 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%}")
251
+ ```
252
+
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
+
237
257
  ### Full mutation analysis pipeline (DNA + protein)
238
258
 
239
259
  ```python
@@ -402,6 +422,29 @@ python check.py
402
422
 
403
423
  ---
404
424
 
425
+ ## References & inspiration
426
+
427
+ BioForge's genome mapper (Level 4) is an **independent, from-scratch
428
+ implementation** of well-established, published algorithms. No third-party
429
+ source code is included or copied — only the *ideas* from the scientific
430
+ literature, which is what publishing them is for. With gratitude to:
431
+
432
+ - **Minimap2** — Li, H. (2018). *Minimap2: pairwise alignment for nucleotide
433
+ sequences.* Bioinformatics, 34(18), 3094–3100. The seed-chain-align strategy
434
+ and the chaining dynamic program that inspired `genomemap.py`.
435
+ ([paper](https://doi.org/10.1093/bioinformatics/bty191) ·
436
+ [MIT-licensed source](https://github.com/lh3/minimap2))
437
+ - **Minimizers** — Roberts, M., Hayes, W., Hunt, B. R., Mount, S. M., &
438
+ Yorke, J. A. (2004). *Reducing storage requirements for biological sequence
439
+ comparison.* Bioinformatics, 20(18), 3363–3369. The (w, k) minimizer sampling
440
+ behind `minimizers.py`.
441
+ - **Needleman–Wunsch** (1970) and **Smith–Waterman** (1981) — the classic
442
+ dynamic-programming alignments behind Level 3.
443
+
444
+ BioForge is not affiliated with or endorsed by the authors of the above.
445
+
446
+ ---
447
+
405
448
  ## Author
406
449
 
407
450
  **Aarón Aranda Torrijos** — [github.com/erlanders177](https://github.com/erlanders177)
@@ -33,9 +33,10 @@ from .biocore import (
33
33
  TranslationError,
34
34
  compute_stats,
35
35
  )
36
+ from .genomemap import GenomeAligner, Mapping
36
37
  from .smart_translator import SmartTranslator
37
38
 
38
- __version__ = "2.3.0"
39
+ __version__ = "3.1.0"
39
40
  __author__ = "Aarón Aranda Torrijos"
40
41
 
41
42
  __all__ = [
@@ -67,6 +68,9 @@ __all__ = [
67
68
  "format_alignment",
68
69
  "Mutation",
69
70
  "AlignmentResult",
71
+ # Genome mapper (v3.0)
72
+ "GenomeAligner",
73
+ "Mapping",
70
74
  # Pipeline
71
75
  "run",
72
76
  "build_report",
@@ -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(
@@ -17,8 +17,11 @@ _DLL_PATH = _ENGINE_DIR / ("engine.dll" if sys.platform == "win32" else "engin
17
17
  # ── Tipos ctypes frecuentes ────────────────────────────────────────────────────
18
18
  _U8P = ctypes.POINTER(ctypes.c_uint8)
19
19
  _I32P = ctypes.POINTER(ctypes.c_int32)
20
+ _I64P = ctypes.POINTER(ctypes.c_int64)
21
+ _F64P = ctypes.POINTER(ctypes.c_double)
20
22
  _I32 = ctypes.c_int32
21
23
  _I64 = ctypes.c_int64
24
+ _F64 = ctypes.c_double
22
25
  _CHARP = ctypes.c_char_p
23
26
 
24
27
  # ── Carga del DLL ──────────────────────────────────────────────────────────────
@@ -200,10 +203,30 @@ def _check_libdeflate() -> None:
200
203
  pass
201
204
 
202
205
 
206
+ C_CHAIN_AVAILABLE: bool = False
207
+
208
+ def _check_chain() -> None:
209
+ """DP de chaining del alineador de genomas (v3). Opcional."""
210
+ global C_CHAIN_AVAILABLE
211
+ if not C_AVAILABLE or _lib is None:
212
+ return
213
+ try:
214
+ _lib.bio_chain_dp.restype = None
215
+ _lib.bio_chain_dp.argtypes = [
216
+ _I64P, _I64P, _I32, # x, y, n
217
+ _I32, _I64, _I32, _F64, # k, max_gap, window, gap_w
218
+ _F64P, _I32P, # f (out), prev (out)
219
+ ]
220
+ C_CHAIN_AVAILABLE = True
221
+ except (AttributeError, OSError):
222
+ pass
223
+
224
+
203
225
  _check_parser()
204
226
  _check_batch()
205
227
  _check_parallel()
206
228
  _check_libdeflate()
229
+ _check_chain()
207
230
 
208
231
 
209
232
  # ── Wrappers Python ────────────────────────────────────────────────────────────
@@ -324,6 +347,23 @@ def c_nw_banded(
324
347
  )
325
348
 
326
349
 
350
+ def c_chain_dp(xs: np.ndarray, ys: np.ndarray, k: int,
351
+ max_gap: int, window: int, gap_w: float):
352
+ """DP de chaining en C. Rellena y devuelve (f, prev).
353
+
354
+ ``xs``/``ys`` deben ser int64 contiguos y estar ordenados por (x, y).
355
+ """
356
+ n = int(xs.size)
357
+ f = np.empty(n, dtype=np.float64)
358
+ prev = np.empty(n, dtype=np.int32)
359
+ _lib.bio_chain_dp(
360
+ xs.ctypes.data_as(_I64P), ys.ctypes.data_as(_I64P), _I32(n),
361
+ _I32(int(k)), _I64(int(max_gap)), _I32(int(window)), _F64(float(gap_w)),
362
+ f.ctypes.data_as(_F64P), prev.ctypes.data_as(_I32P),
363
+ )
364
+ return f, prev
365
+
366
+
327
367
  def c_parser_open(path: str) -> int:
328
368
  """Abre un archivo FASTA/FASTQ y devuelve un handle opaco (c_void_p)."""
329
369
  raw = path.encode("utf-8") if isinstance(path, str) else path
@@ -21,6 +21,7 @@
21
21
  #include <stdlib.h>
22
22
  #include <string.h>
23
23
  #include <stdint.h>
24
+ #include <math.h>
24
25
 
25
26
  /* ── Lectura de archivos: gzip transparente si se compila con -DBIO_USE_ZLIB ──
26
27
  zlib gzopen/gzread leen tanto archivos planos como .gz (autodetección del
@@ -1536,3 +1537,45 @@ EXPORT int32_t bio_parse_mem_parallel(
1536
1537
  free(outs);
1537
1538
  return (rc != 0) ? rc : total_rec;
1538
1539
  }
1540
+
1541
+ /* ── Chaining DP (alineador v3) ───────────────────────────────────────────────
1542
+ Rellena f[] (mejor score de cadena que TERMINA en i) y prev[] (predecesor)
1543
+ para anclas ordenadas ascendentemente por (x, y). El backtracking y la
1544
+ supresión de solapamientos se hacen en Python (baratos, O(n)).
1545
+
1546
+ x, y : posiciones de las anclas (ref y coord. de chaining), int64.
1547
+ n : nº de anclas. k : peso del ancla (longitud del k-mer).
1548
+ max_gap : distancia máxima entre anclas encadenables.
1549
+ window : nº de predecesores examinados por ancla.
1550
+ gap_w : peso lineal de la penalización por hueco.
1551
+
1552
+ Réplica exacta del DP vectorizado de genomemap.py (misma fórmula y mismo
1553
+ desempate: al iterar j de i-1 hacia abajo con `>` estricto, en empate gana
1554
+ el predecesor MÁS CERCANO). Así el resultado C == fallback NumPy. */
1555
+ EXPORT void bio_chain_dp(const int64_t* x, const int64_t* y, int32_t n,
1556
+ int32_t k, int64_t max_gap, int32_t window,
1557
+ double gap_w, double* f, int32_t* prev) {
1558
+ for (int32_t i = 0; i < n; i++) {
1559
+ double best = (double)k;
1560
+ int32_t bp = -1;
1561
+ int32_t lo = i - window;
1562
+ if (lo < 0) lo = 0;
1563
+ for (int32_t j = i - 1; j >= lo; j--) {
1564
+ int64_t dx = x[i] - x[j];
1565
+ if (dx > max_gap) break; /* ordenado → resto aún más lejos */
1566
+ int64_t dy = y[i] - y[j];
1567
+ if (dy <= 0 || dx <= 0 || dy > max_gap) continue;
1568
+ int64_t gap = dx - dy;
1569
+ if (gap < 0) gap = -gap;
1570
+ if (gap > max_gap) continue;
1571
+ int64_t mn = (dx < dy) ? dx : dy;
1572
+ double match = (double)((mn < (int64_t)k) ? mn : (int64_t)k);
1573
+ double cost = gap_w * (double)gap;
1574
+ if (gap > 0) cost += log2((double)gap + 1.0);
1575
+ double sc = f[j] + match - cost;
1576
+ if (sc > best) { best = sc; bp = j; }
1577
+ }
1578
+ f[i] = best;
1579
+ prev[i] = bp;
1580
+ }
1581
+ }