bioforge 3.2.0__tar.gz → 3.3.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.2.0 → bioforge-3.3.0}/PKG-INFO +1 -1
  2. {bioforge-3.2.0 → bioforge-3.3.0}/bioforge/__init__.py +1 -1
  3. {bioforge-3.2.0 → bioforge-3.3.0}/bioforge/engine/_loader.py +38 -0
  4. {bioforge-3.2.0 → bioforge-3.3.0}/bioforge/engine/engine.c +84 -0
  5. {bioforge-3.2.0 → bioforge-3.3.0}/bioforge/engine/engine.dll +0 -0
  6. {bioforge-3.2.0 → bioforge-3.3.0}/bioforge/minimizers.py +11 -0
  7. {bioforge-3.2.0 → bioforge-3.3.0}/bioforge.egg-info/PKG-INFO +1 -1
  8. {bioforge-3.2.0 → bioforge-3.3.0}/tests/test_minimizers.py +23 -1
  9. {bioforge-3.2.0 → bioforge-3.3.0}/LICENSE +0 -0
  10. {bioforge-3.2.0 → bioforge-3.3.0}/README.md +0 -0
  11. {bioforge-3.2.0 → bioforge-3.3.0}/bioforge/aligner.py +0 -0
  12. {bioforge-3.2.0 → bioforge-3.3.0}/bioforge/analyze.py +0 -0
  13. {bioforge-3.2.0 → bioforge-3.3.0}/bioforge/bgzf.py +0 -0
  14. {bioforge-3.2.0 → bioforge-3.3.0}/bioforge/biocore.py +0 -0
  15. {bioforge-3.2.0 → bioforge-3.3.0}/bioforge/engine/__init__.py +0 -0
  16. {bioforge-3.2.0 → bioforge-3.3.0}/bioforge/engine/build.py +0 -0
  17. {bioforge-3.2.0 → bioforge-3.3.0}/bioforge/genomemap.py +0 -0
  18. {bioforge-3.2.0 → bioforge-3.3.0}/bioforge/qcreport.py +0 -0
  19. {bioforge-3.2.0 → bioforge-3.3.0}/bioforge/refindex.py +0 -0
  20. {bioforge-3.2.0 → bioforge-3.3.0}/bioforge/smart_translator.py +0 -0
  21. {bioforge-3.2.0 → bioforge-3.3.0}/bioforge.egg-info/SOURCES.txt +0 -0
  22. {bioforge-3.2.0 → bioforge-3.3.0}/bioforge.egg-info/dependency_links.txt +0 -0
  23. {bioforge-3.2.0 → bioforge-3.3.0}/bioforge.egg-info/entry_points.txt +0 -0
  24. {bioforge-3.2.0 → bioforge-3.3.0}/bioforge.egg-info/requires.txt +0 -0
  25. {bioforge-3.2.0 → bioforge-3.3.0}/bioforge.egg-info/top_level.txt +0 -0
  26. {bioforge-3.2.0 → bioforge-3.3.0}/pyproject.toml +0 -0
  27. {bioforge-3.2.0 → bioforge-3.3.0}/setup.cfg +0 -0
  28. {bioforge-3.2.0 → bioforge-3.3.0}/setup.py +0 -0
  29. {bioforge-3.2.0 → bioforge-3.3.0}/tests/test_aligner.py +0 -0
  30. {bioforge-3.2.0 → bioforge-3.3.0}/tests/test_analyze.py +0 -0
  31. {bioforge-3.2.0 → bioforge-3.3.0}/tests/test_bgzf.py +0 -0
  32. {bioforge-3.2.0 → bioforge-3.3.0}/tests/test_biocore.py +0 -0
  33. {bioforge-3.2.0 → bioforge-3.3.0}/tests/test_errors.py +0 -0
  34. {bioforge-3.2.0 → bioforge-3.3.0}/tests/test_genomemap.py +0 -0
  35. {bioforge-3.2.0 → bioforge-3.3.0}/tests/test_qcreport.py +0 -0
  36. {bioforge-3.2.0 → bioforge-3.3.0}/tests/test_refindex.py +0 -0
  37. {bioforge-3.2.0 → bioforge-3.3.0}/tests/test_streaming.py +0 -0
  38. {bioforge-3.2.0 → bioforge-3.3.0}/tests/test_translator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bioforge
3
- Version: 3.2.0
3
+ Version: 3.3.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
@@ -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.2.0"
39
+ __version__ = "3.3.0"
40
40
  __author__ = "Aarón Aranda Torrijos"
41
41
 
42
42
  __all__ = [
@@ -18,6 +18,7 @@ _DLL_PATH = _ENGINE_DIR / ("engine.dll" if sys.platform == "win32" else "engin
18
18
  _U8P = ctypes.POINTER(ctypes.c_uint8)
19
19
  _I32P = ctypes.POINTER(ctypes.c_int32)
20
20
  _I64P = ctypes.POINTER(ctypes.c_int64)
21
+ _U64P = ctypes.POINTER(ctypes.c_uint64)
21
22
  _F64P = ctypes.POINTER(ctypes.c_double)
22
23
  _I32 = ctypes.c_int32
23
24
  _I64 = ctypes.c_int64
@@ -222,11 +223,30 @@ def _check_chain() -> None:
222
223
  pass
223
224
 
224
225
 
226
+ C_MINIMIZERS_AVAILABLE: bool = False
227
+
228
+ def _check_minimizers() -> None:
229
+ """Minimizers (w,k) del alineador de genomas (v3). Opcional."""
230
+ global C_MINIMIZERS_AVAILABLE
231
+ if not C_AVAILABLE or _lib is None:
232
+ return
233
+ try:
234
+ _lib.bio_minimizers.restype = _I64
235
+ _lib.bio_minimizers.argtypes = [
236
+ _U8P, _I64, _I32, _I32, # codes, n, k, w
237
+ _U64P, _I64P, _U8P, # out_hash, out_pos, out_strand
238
+ ]
239
+ C_MINIMIZERS_AVAILABLE = True
240
+ except (AttributeError, OSError):
241
+ pass
242
+
243
+
225
244
  _check_parser()
226
245
  _check_batch()
227
246
  _check_parallel()
228
247
  _check_libdeflate()
229
248
  _check_chain()
249
+ _check_minimizers()
230
250
 
231
251
 
232
252
  # ── Wrappers Python ────────────────────────────────────────────────────────────
@@ -347,6 +367,24 @@ def c_nw_banded(
347
367
  )
348
368
 
349
369
 
370
+ def c_minimizers(codes: np.ndarray, k: int, w: int):
371
+ """Minimizers (w,k) canónicos en C. Devuelve (hashes, positions, strands)."""
372
+ codes = np.ascontiguousarray(codes, dtype=np.uint8)
373
+ n = int(codes.size)
374
+ nk = max(0, n - k + 1)
375
+ out_hash = np.empty(nk, dtype=np.uint64)
376
+ out_pos = np.empty(nk, dtype=np.int64)
377
+ out_strand = np.empty(nk, dtype=np.uint8)
378
+ cnt = _lib.bio_minimizers(
379
+ codes.ctypes.data_as(_U8P), _I64(n), _I32(int(k)), _I32(int(w)),
380
+ out_hash.ctypes.data_as(_U64P), out_pos.ctypes.data_as(_I64P),
381
+ out_strand.ctypes.data_as(_U8P),
382
+ )
383
+ if cnt < 0:
384
+ raise MemoryError("Motor C: fallo de memoria en minimizers")
385
+ return out_hash[:cnt], out_pos[:cnt], out_strand[:cnt]
386
+
387
+
350
388
  def c_chain_dp(xs: np.ndarray, ys: np.ndarray, k: int,
351
389
  max_gap: int, window: int, gap_w: float):
352
390
  """DP de chaining en C. Rellena y devuelve (f, prev).
@@ -1579,3 +1579,87 @@ EXPORT void bio_chain_dp(const int64_t* x, const int64_t* y, int32_t n,
1579
1579
  prev[i] = bp;
1580
1580
  }
1581
1581
  }
1582
+
1583
+ /* ── Minimizers (w,k) canónicos en C (alineador v3) ───────────────────────────
1584
+ Réplica exacta del cálculo vectorizado de minimizers.py:
1585
+ - k-mer directo/inverso por hash RODANTE (idéntico al Horner de NumPy),
1586
+ - hash invertible _hash64 (mismo esquema), canónico = menor(hf,hr),
1587
+ - mínimo por ventana deslizante de w k-mers (argmin más a la IZQUIERDA),
1588
+ - dedup de posiciones consecutivas (== np.unique sobre pos no-decrecientes).
1589
+ Escala a genomas donde sliding_window_view de NumPy se ahoga.
1590
+
1591
+ codes: uint8 2-bit (>=4 = base inválida). Salidas (tamaño ≥ n-k+1):
1592
+ out_hash/out_pos/out_strand. Devuelve el nº de minimizers, o -1 si malloc. */
1593
+ static uint64_t _hash64_c(uint64_t x, uint64_t mask) {
1594
+ x = x & mask;
1595
+ x = (~x + (x << 21)) & mask;
1596
+ x = x ^ (x >> 24);
1597
+ x = ((x + (x << 3)) + (x << 8)) & mask;
1598
+ x = x ^ (x >> 14);
1599
+ x = ((x + (x << 2)) + (x << 4)) & mask;
1600
+ x = x ^ (x >> 28);
1601
+ x = (x + (x << 31)) & mask;
1602
+ return x;
1603
+ }
1604
+
1605
+ EXPORT int64_t bio_minimizers(const uint8_t* codes, int64_t n, int32_t k, int32_t w,
1606
+ uint64_t* out_hash, int64_t* out_pos,
1607
+ uint8_t* out_strand) {
1608
+ int64_t nk = n - (int64_t)k + 1;
1609
+ if (nk <= 0) return 0;
1610
+
1611
+ uint64_t* h = (uint64_t*)malloc((size_t)nk * sizeof(uint64_t));
1612
+ uint8_t* st = (uint8_t*) malloc((size_t)nk * sizeof(uint8_t));
1613
+ if (!h || !st) { free(h); free(st); return -1; }
1614
+
1615
+ const uint64_t SENT = ~0ULL;
1616
+ uint64_t mask = (k >= 32) ? ~0ULL : ((1ULL << (2 * k)) - 1);
1617
+ uint64_t shift = (uint64_t)(2 * (k - 1));
1618
+ uint64_t fwd = 0, rev = 0;
1619
+ int32_t l = 0; /* bases válidas consecutivas */
1620
+
1621
+ for (int64_t i = 0; i < n; i++) {
1622
+ uint8_t c = codes[i];
1623
+ if (c < 4) {
1624
+ uint64_t cc = c;
1625
+ fwd = ((fwd << 2) | cc) & mask;
1626
+ rev = (rev >> 2) | ((3ULL - cc) << shift);
1627
+ l++;
1628
+ } else {
1629
+ l = 0; fwd = 0; rev = 0;
1630
+ }
1631
+ int64_t p = i - (int64_t)k + 1; /* k-mer que termina en i */
1632
+ if (p >= 0) {
1633
+ if (l >= k) {
1634
+ uint64_t hf = _hash64_c(fwd, mask);
1635
+ uint64_t hr = _hash64_c(rev, mask);
1636
+ if (hr < hf) { h[p] = hr; st[p] = 1; }
1637
+ else { h[p] = hf; st[p] = 0; }
1638
+ } else {
1639
+ h[p] = SENT; st[p] = 0;
1640
+ }
1641
+ }
1642
+ }
1643
+
1644
+ int64_t count = 0, last = -1;
1645
+ if (nk < w) { /* una sola ventana */
1646
+ int64_t bi = 0;
1647
+ for (int64_t j = 1; j < nk; j++) if (h[j] < h[bi]) bi = j;
1648
+ if (h[bi] != SENT) {
1649
+ out_hash[0] = h[bi]; out_pos[0] = bi; out_strand[0] = st[bi];
1650
+ count = 1;
1651
+ }
1652
+ } else {
1653
+ for (int64_t i = 0; i + w <= nk; i++) {
1654
+ int64_t bi = i; /* argmin más a la izquierda */
1655
+ for (int64_t j = i + 1; j < i + w; j++) if (h[j] < h[bi]) bi = j;
1656
+ if (bi != last && h[bi] != SENT) {
1657
+ out_hash[count] = h[bi]; out_pos[count] = bi;
1658
+ out_strand[count] = st[bi];
1659
+ count++; last = bi;
1660
+ }
1661
+ }
1662
+ }
1663
+ free(h); free(st);
1664
+ return count;
1665
+ }
@@ -34,6 +34,7 @@ import numpy as np
34
34
  from numpy.lib.stride_tricks import sliding_window_view
35
35
 
36
36
  from .biocore import SequenceValueError
37
+ from .engine._loader import C_MINIMIZERS_AVAILABLE, c_minimizers
37
38
 
38
39
  # ── LUT ASCII → código 2-bit (A=0, C=1, G=2, T=3; cualquier otra cosa = 4) ──────
39
40
  _BASE_LUT = np.full(256, 4, dtype=np.uint8)
@@ -103,6 +104,16 @@ def minimizers(codes: np.ndarray, k: int = 15, w: int = 10) -> MinimizerSketch:
103
104
  raise SequenceValueError(f"w debe ser ≥ 1 (se recibió {w}).")
104
105
 
105
106
  codes = np.ascontiguousarray(codes, dtype=np.uint8)
107
+ # Motor C si está disponible (mucho más rápido en genomas grandes); si no,
108
+ # el fallback NumPy, que produce EXACTAMENTE lo mismo (verificado en tests).
109
+ if C_MINIMIZERS_AVAILABLE:
110
+ h, pos, strand = c_minimizers(codes, k, w)
111
+ return MinimizerSketch(h, pos, strand)
112
+ return _minimizers_numpy(codes, k, w)
113
+
114
+
115
+ def _minimizers_numpy(codes: np.ndarray, k: int, w: int) -> MinimizerSketch:
116
+ """Fallback NumPy de :func:`minimizers` (idéntico al C bio_minimizers)."""
106
117
  L = codes.size
107
118
  n = L - k + 1 # nº de k-mers en la secuencia
108
119
  if n <= 0:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bioforge
3
- Version: 3.2.0
3
+ Version: 3.3.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
@@ -9,7 +9,13 @@ import numpy as np
9
9
  import pytest
10
10
 
11
11
  from bioforge.biocore import SequenceValueError
12
- from bioforge.minimizers import MinimizerSketch, encode_bases, minimizers
12
+ from bioforge.engine._loader import C_MINIMIZERS_AVAILABLE
13
+ from bioforge.minimizers import (
14
+ MinimizerSketch,
15
+ _minimizers_numpy,
16
+ encode_bases,
17
+ minimizers,
18
+ )
13
19
 
14
20
  _COMP = str.maketrans("ACGTacgt", "TGCAtgca")
15
21
 
@@ -122,6 +128,22 @@ def test_hash_canonico_es_el_menor():
122
128
 
123
129
  # ── Validación de argumentos ────────────────────────────────────────────────────
124
130
 
131
+ @pytest.mark.skipif(not C_MINIMIZERS_AVAILABLE, reason="motor C no disponible")
132
+ @pytest.mark.parametrize("seed", [0, 1, 2, 3])
133
+ def test_c_igual_que_numpy(seed):
134
+ # El camino C y el fallback NumPy deben dar EXACTAMENTE lo mismo.
135
+ rng = np.random.default_rng(seed)
136
+ s = list("".join("ACGT"[i] for i in rng.integers(0, 4, 2000)))
137
+ for p in rng.integers(0, 2000, 25): # unas cuantas N
138
+ s[p] = "N"
139
+ codes = encode_bases("".join(s))
140
+ c = minimizers(codes, k=15, w=10) # ruta C (activa)
141
+ n = _minimizers_numpy(codes, 15, 10) # fallback NumPy
142
+ assert np.array_equal(c.hashes, n.hashes)
143
+ assert np.array_equal(c.positions, n.positions)
144
+ assert np.array_equal(c.strands, n.strands)
145
+
146
+
125
147
  @pytest.mark.parametrize("k", [0, 32, -1])
126
148
  def test_k_invalido(k):
127
149
  with pytest.raises(SequenceValueError):
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