//! Compiled kernels for SimurgArray (plan §18 P9, §19). //! //! Every kernel is a drop-in replacement for a pure-Python/NumPy reference in the `simurg` package and returns //! bit-identical results (checked by `tests/unit/test_core_kernels.py`): //! //! * `exp`, `log10`, `log2`, `pow` — element-wise glibc libm calls with the error semantics of CPython's `math` //! module as used by `simurg.genotype.gencall._c_call` (NaN input returned as is, a domain error gives the //! canonical +NaN, `log10(0)` = −inf, overflow = +inf). The libm symbols are the ones CPython's `math` calls, so //! results equal the C reference implementations (gtc2vcf, MIT) that the Python port follows. //! * `closest_binned` — the per-bin candidate scan of the vendor's binned nearest-neighbour search //! (`simurg.signal.normalization.fit._closest_binned`): float32 squared distances, first minimum wins. //! //! * `call_genotypes`, `baf_lrr` — fused per-locus GenCall calling/scoring and BAF/LRR (`gencall.rs`). //! * `bpm_locus_entries` — the BPM LocusEntry parser (`simurg.formats.bpm._parse_locus_entries`). //! //! No floating-point contraction (FMA) happens: Rust never fuses `a*b+c` implicitly. mod bpm; mod gencall; use numpy::{IntoPyArray, PyArray1, PyReadonlyArray1}; use pyo3::exceptions::PyValueError; use pyo3::prelude::*; #[link(name = "m")] unsafe extern "C" { #[link_name = "exp"] fn c_exp(x: f64) -> f64; #[link_name = "log10"] fn c_log10(x: f64) -> f64; #[link_name = "log2"] fn c_log2(x: f64) -> f64; #[link_name = "pow"] fn c_pow(x: f64, y: f64) -> f64; } /// CPython's `float('nan')`. const PY_NAN: f64 = f64::from_bits(0x7ff8_0000_0000_0000); #[inline] pub(crate) fn math_exp(v: f64) -> f64 { if v.is_nan() { return v; } let r = unsafe { c_exp(v) }; if r.is_nan() { PY_NAN } else if r.is_infinite() && v.is_finite() { f64::INFINITY } else { r } } /// CPython `m_log10`/`m_log2` + `_c_call`: x > 0 → libm; 0 → −inf (log10) or NaN (log2); x < 0, −inf → NaN. #[inline] pub(crate) fn math_log(v: f64, ten: bool) -> f64 { if v.is_nan() { return v; } if v.is_infinite() { return if v > 0.0 { v } else { PY_NAN }; } if v > 0.0 { return unsafe { if ten { c_log10(v) } else { c_log2(v) } }; } if v == 0.0 && ten { f64::NEG_INFINITY } else { PY_NAN } } /// CPython `math.pow(v, e)` for a finite exponent e > 0 that is not an integer, + `_c_call`. #[inline] pub(crate) fn math_pow(v: f64, e: f64) -> f64 { if v.is_nan() { return v; } if v.is_infinite() { return f64::INFINITY; } if v == 0.0 { return 0.0; } let r = unsafe { c_pow(v, e) }; if r.is_nan() { PY_NAN } else if r.is_infinite() { f64::INFINITY } else { r } } fn map_f64<'py>(py: Python<'py>, x: PyReadonlyArray1<'py, f64>, f: impl Fn(f64) -> f64) -> PyResult>> { let s = x.as_slice()?; let out: Vec = s.iter().map(|&v| f(v)).collect(); Ok(out.into_pyarray(py)) } #[pyfunction] fn exp<'py>(py: Python<'py>, x: PyReadonlyArray1<'py, f64>) -> PyResult>> { map_f64(py, x, math_exp) } #[pyfunction] fn log10<'py>(py: Python<'py>, x: PyReadonlyArray1<'py, f64>) -> PyResult>> { map_f64(py, x, |v| math_log(v, true)) } #[pyfunction] fn log2<'py>(py: Python<'py>, x: PyReadonlyArray1<'py, f64>) -> PyResult>> { map_f64(py, x, |v| math_log(v, false)) } #[pyfunction] fn pow<'py>(py: Python<'py>, x: PyReadonlyArray1<'py, f64>, e: f64) -> PyResult>> { if !(e.is_finite() && e > 0.0 && e.fract() != 0.0) { return Err(PyValueError::new_err("pow kernel: exponent must be finite, positive and not an integer")); } map_f64(py, x, |v| math_pow(v, e)) } /// For each sweep point `p` searching bin `choice[p]` (0..11 regular, 12..22 shifted; any other value: none), the /// index of the closest candidate among `eligible[i]` with `b1[i] == choice` (regular) or `b2[i] == choice − 12` /// (shifted), in ascending order; squared distance in float32; −1 if no candidate or the best is not < 1e20. #[pyfunction] #[allow(clippy::too_many_arguments)] fn closest_binned<'py>( py: Python<'py>, raw_x: PyReadonlyArray1<'py, f32>, raw_y: PyReadonlyArray1<'py, f32>, axis_x: PyReadonlyArray1<'py, f32>, axis_y: PyReadonlyArray1<'py, f32>, choice: PyReadonlyArray1<'py, i64>, eligible: PyReadonlyArray1<'py, i64>, b1: PyReadonlyArray1<'py, i64>, b2: PyReadonlyArray1<'py, i64>, ) -> PyResult>> { let (rx, ry) = (raw_x.as_slice()?, raw_y.as_slice()?); let (ax, ay, ch) = (axis_x.as_slice()?, axis_y.as_slice()?, choice.as_slice()?); let (el, b1, b2) = (eligible.as_slice()?, b1.as_slice()?, b2.as_slice()?); if ax.len() != ay.len() || ax.len() != ch.len() || el.len() != b1.len() || el.len() != b2.len() || rx.len() != ry.len() { return Err(PyValueError::new_err("closest_binned: length mismatch")); } let mut groups: Vec> = vec![Vec::new(); 23]; for (i, &e) in el.iter().enumerate() { let e = usize::try_from(e).map_err(|_| PyValueError::new_err("closest_binned: negative index"))?; if e >= rx.len() { return Err(PyValueError::new_err("closest_binned: index out of range")); } if (0..12).contains(&b1[i]) { groups[b1[i] as usize].push(e); } if (0..11).contains(&b2[i]) { groups[12 + b2[i] as usize].push(e); } } let out: Vec = (0..ax.len()) .map(|p| { let c = ch[p]; if !(0..23).contains(&c) { return -1; } let cand = &groups[c as usize]; let mut best_j = usize::MAX; let mut best = f32::INFINITY; for (j, &e) in cand.iter().enumerate() { let dx = rx[e] - ax[p]; let dy = ry[e] - ay[p]; let d = dx * dx + dy * dy; // numpy argmin: the first NaN wins, otherwise the first strict minimum if d.is_nan() { best_j = j; best = d; break; } if best_j == usize::MAX || d < best { best_j = j; best = d; } } if best_j == usize::MAX || !((best as f64) < 1e20) { -1 } else { cand[best_j] as i64 } }) .collect(); Ok(out.into_pyarray(py)) } /// Exact nearest neighbour (`simurg.signal.normalization.fit._closest_slow`): for each sweep point (x, y) the /// index of the reference point with the smallest float32 squared distance `(x-xr)²+(y-yr)²`; first minimum wins, /// a NaN distance wins as in `numpy.argmin`. `xref` must not be empty. #[pyfunction] fn closest_exact<'py>( py: Python<'py>, xref: PyReadonlyArray1<'py, f32>, yref: PyReadonlyArray1<'py, f32>, x: PyReadonlyArray1<'py, f32>, y: PyReadonlyArray1<'py, f32>, ) -> PyResult>> { let (xr, yr, xs, ys) = (xref.as_slice()?, yref.as_slice()?, x.as_slice()?, y.as_slice()?); if xr.is_empty() || xr.len() != yr.len() || xs.len() != ys.len() { return Err(PyValueError::new_err("closest_exact: empty reference or length mismatch")); } let out: Vec = xs .iter() .zip(ys) .map(|(&px, &py_)| { let mut best_j = 0usize; let mut best = f32::INFINITY; for (j, (&rx, &ry)) in xr.iter().zip(yr).enumerate() { let dx = px - rx; let dy = py_ - ry; let d = dx * dx + dy * dy; if d.is_nan() { return j as i64; } if j == 0 || d < best { best_j = j; best = d; } } best_j as i64 }) .collect(); Ok(out.into_pyarray(py)) } #[pymodule] fn simurg_core(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add("__version__", env!("CARGO_PKG_VERSION"))?; m.add_function(wrap_pyfunction!(exp, m)?)?; m.add_function(wrap_pyfunction!(log10, m)?)?; m.add_function(wrap_pyfunction!(log2, m)?)?; m.add_function(wrap_pyfunction!(pow, m)?)?; m.add_function(wrap_pyfunction!(closest_binned, m)?)?; m.add_function(wrap_pyfunction!(closest_exact, m)?)?; m.add_function(wrap_pyfunction!(bpm::bpm_locus_entries, m)?)?; m.add_function(wrap_pyfunction!(gencall::call_genotypes, m)?)?; m.add_function(wrap_pyfunction!(gencall::baf_lrr, m)?)?; Ok(()) }