Skip to main content

polydat_core/numeric/
vector.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! The f32 vector bodies: the scalar references, the SIMD dispatch,
5//! and the element-wise and reducing operations the `vec_*` nodes and
6//! their native lowerings share, so both produce the same bytes.
7
8// ── Scalar reference implementations ──────────────────────────
9// Used directly on non-jit builds and as the equivalence oracle
10// in tests.
11
12/// The scalar dot product: the reference the SIMD kernel is checked against.
13pub fn dot_scalar(a: &[f32], b: &[f32]) -> f32 {
14    a.iter().zip(b).map(|(x, y)| x * y).sum()
15}
16
17/// The scalar squared L2 distance: the reference the SIMD kernel is checked against.
18pub fn l2sq_scalar(a: &[f32], b: &[f32]) -> f32 {
19    a.iter().zip(b).map(|(x, y)| (x - y) * (x - y)).sum()
20}
21
22/// Panics with both lengths named when two operands differ in length.
23pub fn check_lens(name: &str, a: usize, b: usize) {
24    if a != b {
25        panic!("{name}: operand lengths differ ({a} vs {b})");
26    }
27}
28
29// ── Kernel dispatch ────────────────────────────────────────────
30//
31// One body per operation, shared by the node (which allocates its
32// result) and the native helper (which writes into the step's own
33// scratch entry, compiled_handles.md §6), so both produce the same
34// bytes: the same SIMD kernel or the same scalar loop, in the same
35// order.
36
37/// The dot product, through the SIMD kernel where the host has one.
38pub fn dot_f32(a: &[f32], b: &[f32]) -> f32 {
39    #[cfg(feature = "jit")]
40    if let Some(k) = crate::compile::jit::simd::kernels() {
41        // SAFETY: both slices live for the call; len is the
42        // (equal) element count.
43        return unsafe { (k.dot_f32)(a.as_ptr(), b.as_ptr(), a.len() as u64) };
44    }
45    dot_scalar(a, b)
46}
47
48/// The squared L2 distance, through the SIMD kernel where the host has one.
49pub fn l2sq_f32(a: &[f32], b: &[f32]) -> f32 {
50    #[cfg(feature = "jit")]
51    if let Some(k) = crate::compile::jit::simd::kernels() {
52        return unsafe { (k.l2sq_f32)(a.as_ptr(), b.as_ptr(), a.len() as u64) };
53    }
54    l2sq_scalar(a, b)
55}
56
57/// `a + b` element-wise into `out`, which is cleared first.
58pub fn add_f32_into(a: &[f32], b: &[f32], out: &mut Vec<f32>) {
59    out.clear();
60    out.resize(a.len(), 0.0);
61    #[cfg(feature = "jit")]
62    if let Some(k) = crate::compile::jit::simd::kernels() {
63        // SAFETY: out holds a.len() elements.
64        unsafe { (k.add_f32)(a.as_ptr(), b.as_ptr(), out.as_mut_ptr(), a.len() as u64) };
65        return;
66    }
67    for i in 0..a.len() {
68        out[i] = a[i] + b[i];
69    }
70}
71
72/// `a * k` element-wise into `out`, which is cleared first.
73pub fn scale_f32_into(a: &[f32], k_val: f32, out: &mut Vec<f32>) {
74    out.clear();
75    out.resize(a.len(), 0.0);
76    #[cfg(feature = "jit")]
77    if let Some(k) = crate::compile::jit::simd::kernels() {
78        unsafe { (k.scale_f32)(a.as_ptr(), k_val, out.as_mut_ptr(), a.len() as u64) };
79        return;
80    }
81    for i in 0..a.len() {
82        out[i] = a[i] * k_val;
83    }
84}
85
86/// `a + b` element-wise, as a new vector.
87pub fn add_f32(a: &[f32], b: &[f32]) -> Vec<f32> {
88    let mut out = Vec::new();
89    add_f32_into(a, b, &mut out);
90    out
91}
92
93/// `a * k` element-wise, as a new vector.
94pub fn scale_f32(a: &[f32], k_val: f32) -> Vec<f32> {
95    let mut out = Vec::new();
96    scale_f32_into(a, k_val, &mut out);
97    out
98}
99
100/// `a` scaled to unit L2 magnitude into `out`; `a` itself when its
101/// magnitude is zero.
102pub fn norm_f32_into(a: &[f32], out: &mut Vec<f32>) {
103    let mag = (dot_f32(a, a) as f64).sqrt();
104    if mag == 0.0 {
105        out.clear();
106        out.extend_from_slice(a);
107    } else {
108        scale_f32_into(a, (1.0 / mag) as f32, out);
109    }
110}
111
112/// The cosine similarity of `vec_cosine`.
113pub fn cosine_f32(a: &[f32], b: &[f32]) -> f64 {
114    let dot = dot_f32(a, b) as f64;
115    let na = (dot_f32(a, a) as f64).sqrt();
116    let nb = (dot_f32(b, b) as f64).sqrt();
117    if na == 0.0 || nb == 0.0 {
118        0.0
119    } else {
120        dot / (na * nb)
121    }
122}
123
124/// The estimate of `lid_mle`.
125pub fn lid_mle_of(distances: &[f32], k: f64) -> f64 {
126    let k = (k as usize).min(distances.len());
127    if k < 2 {
128        return 0.0;
129    }
130    let r_k = distances[k - 1] as f64;
131    if r_k <= 0.0 {
132        return 0.0;
133    }
134    let ln_rk = r_k.ln();
135    let mut logsum = 0.0_f64;
136    let mut terms = 0u32;
137    for &r_j in &distances[..k - 1] {
138        let r_j = r_j as f64;
139        if r_j > 0.0 {
140            logsum += ln_rk - r_j.ln(); // ln(r_k / r_j) ≥ 0 since r_j ≤ r_k
141            terms += 1;
142        }
143    }
144    if terms == 0 || logsum <= 0.0 {
145        0.0
146    } else {
147        terms as f64 / logsum
148    }
149}
150
151/// The vector of `hash_vec` into `out`, which is cleared first.
152pub fn hash_vec_into(seed: u64, dim: u64, out: &mut Vec<f32>) {
153    let dim = dim as usize;
154    out.clear();
155    out.reserve(dim);
156    for i in 0..dim {
157        let h = crate::numeric::hash::splitmix64_u64(
158            seed.wrapping_add((i as u64).wrapping_mul(0x9e3779b97f4a7c15)),
159        );
160        out.push((h as f64 / u64::MAX as f64 * 2.0 - 1.0) as f32);
161    }
162}
163
164/// The vector of `xxhash3_vec` into `out`, which is cleared first.
165pub fn xxhash3_vec_into(seed: u64, dim: u64, out: &mut Vec<f32>) {
166    let dim = dim as usize;
167    out.clear();
168    out.reserve(dim);
169    for i in 0..dim {
170        let mut key = [0u8; 16];
171        key[..8].copy_from_slice(&seed.to_le_bytes());
172        key[8..].copy_from_slice(&(i as u64).to_le_bytes());
173        let h = xxhash_rust::xxh3::xxh3_64(&key);
174        out.push((h as f64 / u64::MAX as f64 * 2.0 - 1.0) as f32);
175    }
176}