Skip to main content

ailake_vec/
distance.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2use ailake_core::{Centroid, VectorMetric};
3use half::f16;
4
5// ── Public API ────────────────────────────────────────────────────────────────
6
7pub fn dot_product(a: &[f32], b: &[f32]) -> f32 {
8    debug_assert_eq!(
9        a.len(),
10        b.len(),
11        "dot_product: dimension mismatch {} vs {}",
12        a.len(),
13        b.len()
14    );
15    #[cfg(target_arch = "x86_64")]
16    {
17        #[cfg(feature = "avx512")]
18        if is_x86_feature_detected!("avx512f") {
19            return unsafe { avx512::dot(a, b) };
20        }
21        if is_x86_feature_detected!("avx2") {
22            return unsafe { avx2::dot(a, b) };
23        }
24    }
25    #[cfg(target_arch = "aarch64")]
26    if std::arch::is_aarch64_feature_detected!("neon") {
27        return unsafe { neon_impl::dot(a, b) };
28    }
29    dot_scalar(a, b)
30}
31
32pub fn euclidean_distance(a: &[f32], b: &[f32]) -> f32 {
33    debug_assert_eq!(
34        a.len(),
35        b.len(),
36        "euclidean_distance: dimension mismatch {} vs {}",
37        a.len(),
38        b.len()
39    );
40    #[cfg(target_arch = "x86_64")]
41    {
42        #[cfg(feature = "avx512")]
43        if is_x86_feature_detected!("avx512f") {
44            return unsafe { avx512::euclidean(a, b) };
45        }
46        if is_x86_feature_detected!("avx2") {
47            return unsafe { avx2::euclidean(a, b) };
48        }
49    }
50    #[cfg(target_arch = "aarch64")]
51    if std::arch::is_aarch64_feature_detected!("neon") {
52        return unsafe { neon_impl::euclidean(a, b) };
53    }
54    euclidean_scalar(a, b)
55}
56
57pub fn cosine_distance(a: &[f32], b: &[f32]) -> f32 {
58    debug_assert_eq!(
59        a.len(),
60        b.len(),
61        "cosine_distance: dimension mismatch {} vs {}",
62        a.len(),
63        b.len()
64    );
65    #[cfg(target_arch = "x86_64")]
66    {
67        #[cfg(feature = "avx512")]
68        if is_x86_feature_detected!("avx512f") {
69            return unsafe { avx512::cosine(a, b) };
70        }
71        if is_x86_feature_detected!("avx2") {
72            return unsafe { avx2::cosine(a, b) };
73        }
74    }
75    #[cfg(target_arch = "aarch64")]
76    if std::arch::is_aarch64_feature_detected!("neon") {
77        return unsafe { neon_impl::cosine(a, b) };
78    }
79    cosine_scalar(a, b)
80}
81
82pub fn exact_distance(metric: VectorMetric, a: &[f32], b: &[f32]) -> f32 {
83    match metric {
84        VectorMetric::Cosine => cosine_distance(a, b),
85        VectorMetric::Euclidean => euclidean_distance(a, b),
86        VectorMetric::DotProduct => -dot_product(a, b),
87        VectorMetric::NormalizedCosine => normalized_cosine_distance(a, b),
88    }
89}
90
91// ── F16 distance functions ────────────────────────────────────────────────────
92//
93// Query `a` stays F32 (one vector, lives in registers).
94// Database vector `b` is F16 (dequantized inline — no allocation).
95//
96// Fast path: F16C converts 8 F16 values to F32 in one instruction via
97// _mm256_cvtph_ps, then FMA accumulates. Eliminates scalar half::to_f32()
98// loop that dominates HNSW graph traversal on dim=128 vectors.
99
100pub fn cosine_distance_f16(a: &[f32], b: &[f16]) -> f32 {
101    debug_assert_eq!(
102        a.len(),
103        b.len(),
104        "cosine_distance_f16: dimension mismatch {} vs {}",
105        a.len(),
106        b.len()
107    );
108    #[cfg(target_arch = "x86_64")]
109    {
110        #[cfg(feature = "avx512")]
111        if is_x86_feature_detected!("avx512f") && is_x86_feature_detected!("f16c") {
112            return unsafe { avx512::cosine_f16(a, b) };
113        }
114        if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("f16c") {
115            return unsafe { avx2_f16c::cosine(a, b) };
116        }
117    }
118    cosine_f16_scalar(a, b)
119}
120
121pub fn euclidean_distance_f16(a: &[f32], b: &[f16]) -> f32 {
122    debug_assert_eq!(
123        a.len(),
124        b.len(),
125        "euclidean_distance_f16: dimension mismatch {} vs {}",
126        a.len(),
127        b.len()
128    );
129    #[cfg(target_arch = "x86_64")]
130    {
131        #[cfg(feature = "avx512")]
132        if is_x86_feature_detected!("avx512f") && is_x86_feature_detected!("f16c") {
133            return unsafe { avx512::euclidean_f16(a, b) };
134        }
135        if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("f16c") {
136            return unsafe { avx2_f16c::euclidean(a, b) };
137        }
138    }
139    euclidean_f16_scalar(a, b)
140}
141
142pub fn dot_product_f16(a: &[f32], b: &[f16]) -> f32 {
143    debug_assert_eq!(
144        a.len(),
145        b.len(),
146        "dot_product_f16: dimension mismatch {} vs {}",
147        a.len(),
148        b.len()
149    );
150    #[cfg(target_arch = "x86_64")]
151    {
152        #[cfg(feature = "avx512")]
153        if is_x86_feature_detected!("avx512f") && is_x86_feature_detected!("f16c") {
154            return unsafe { avx512::dot_f16(a, b) };
155        }
156        if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("f16c") {
157            return unsafe { avx2_f16c::dot(a, b) };
158        }
159    }
160    dot_f16_scalar(a, b)
161}
162
163/// Normalize a vector to unit L2 length. Returns a zero vector unchanged.
164pub fn normalize_l2(v: &[f32]) -> Vec<f32> {
165    let norm_sq: f32 = v.iter().map(|x| x * x).sum();
166    if norm_sq < 1e-12 {
167        return v.to_vec();
168    }
169    let inv = 1.0 / norm_sq.sqrt();
170    v.iter().map(|x| x * inv).collect()
171}
172
173/// 1 - dot(a, b) for pre-normalized unit vectors — no sqrt, no norm computation.
174/// Equivalent to cosine distance but ~2× faster in the HNSW hot loop.
175pub fn normalized_cosine_distance(a: &[f32], b: &[f32]) -> f32 {
176    1.0 - dot_product(a, b)
177}
178
179pub fn normalized_cosine_distance_f16(a: &[f32], b: &[f16]) -> f32 {
180    1.0 - dot_product_f16(a, b)
181}
182
183pub fn compute_centroid_and_radius(vectors: &[Vec<f32>], metric: VectorMetric) -> Centroid {
184    // Exclude non-finite vectors up front — a single NaN/Infinity-poisoned embedding
185    // would otherwise poison the shared centroid's sum/n average for every dimension,
186    // silently corrupting the geometric bound for every OTHER, healthy vector in the
187    // file too (radius would collapse toward 0.0 once nearly every distance to the
188    // poisoned centroid comes out non-finite and gets filtered below).
189    let finite: Vec<&Vec<f32>> = vectors
190        .iter()
191        .filter(|v| v.iter().all(|x| x.is_finite()))
192        .collect();
193    if finite.is_empty() {
194        return Centroid {
195            values: vec![],
196            radius: 0.0,
197            metric,
198        };
199    }
200    let dim = finite[0].len();
201    let n = finite.len() as f32;
202    let centroid: Vec<f32> = (0..dim)
203        .map(|i| finite.iter().map(|v| v[i]).sum::<f32>() / n)
204        .collect();
205    // Exclude non-finite per-vector distances (e.g. from numeric overflow in the
206    // distance computation itself) rather than letting one such value produce a
207    // non-finite radius for the whole file — a non-finite radius can't round-trip
208    // through the manifest's JSON-encoded key_metadata (serde_json serializes it as
209    // `null`). With the centroid and inputs already finite, this is now a rare
210    // defensive fallback rather than the primary guard.
211    let radius = finite
212        .iter()
213        .map(|v| exact_distance(metric, &centroid, v))
214        .filter(|d| d.is_finite())
215        .fold(0.0_f32, f32::max);
216    Centroid {
217        values: centroid,
218        radius,
219        metric,
220    }
221}
222
223// ── Scalar fallbacks ──────────────────────────────────────────────────────────
224
225#[inline(always)]
226fn dot_scalar(a: &[f32], b: &[f32]) -> f32 {
227    debug_assert_eq!(
228        a.len(),
229        b.len(),
230        "dot_scalar: dimension mismatch {} vs {}",
231        a.len(),
232        b.len()
233    );
234    a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
235}
236
237#[inline(always)]
238fn euclidean_scalar(a: &[f32], b: &[f32]) -> f32 {
239    debug_assert_eq!(
240        a.len(),
241        b.len(),
242        "euclidean_scalar: dimension mismatch {} vs {}",
243        a.len(),
244        b.len()
245    );
246    a.iter()
247        .zip(b.iter())
248        .map(|(x, y)| (x - y) * (x - y))
249        .sum::<f32>()
250        .sqrt()
251}
252
253#[inline(always)]
254fn cosine_scalar(a: &[f32], b: &[f32]) -> f32 {
255    debug_assert_eq!(
256        a.len(),
257        b.len(),
258        "cosine_scalar: dimension mismatch {} vs {}",
259        a.len(),
260        b.len()
261    );
262    let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
263    let na: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
264    let nb: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
265    if na == 0.0 || nb == 0.0 {
266        return 1.0;
267    }
268    1.0 - dot / (na * nb)
269}
270
271#[inline(always)]
272fn cosine_f16_scalar(a: &[f32], b: &[f16]) -> f32 {
273    let n = a.len().min(b.len());
274    let mut dot = 0.0f32;
275    let mut norm_a = 0.0f32;
276    let mut norm_b = 0.0f32;
277    for i in 0..n {
278        let ai = a[i];
279        let bi = b[i].to_f32();
280        dot += ai * bi;
281        norm_a += ai * ai;
282        norm_b += bi * bi;
283    }
284    let denom = (norm_a * norm_b).sqrt();
285    if denom < 1e-8 {
286        1.0
287    } else {
288        1.0 - dot / denom
289    }
290}
291
292#[inline(always)]
293fn euclidean_f16_scalar(a: &[f32], b: &[f16]) -> f32 {
294    let n = a.len().min(b.len());
295    let mut sum = 0.0f32;
296    for i in 0..n {
297        let diff = a[i] - b[i].to_f32();
298        sum += diff * diff;
299    }
300    sum.sqrt()
301}
302
303#[inline(always)]
304fn dot_f16_scalar(a: &[f32], b: &[f16]) -> f32 {
305    let n = a.len().min(b.len());
306    let mut acc = 0.0f32;
307    for i in 0..n {
308        acc += a[i] * b[i].to_f32();
309    }
310    acc
311}
312
313// ── x86_64 AVX2 + FMA ────────────────────────────────────────────────────────
314//
315// Compiled with target_feature = "avx2,fma". The compiler emits vfmadd231ps
316// instead of separate vmulps + vaddps, cutting inner-loop instruction count
317// by ~33% and reducing latency via fused operations.
318
319#[cfg(target_arch = "x86_64")]
320mod avx2 {
321    use std::arch::x86_64::*;
322
323    #[inline(always)]
324    pub unsafe fn hsum256(v: __m256) -> f32 {
325        let hi = _mm256_extractf128_ps(v, 1);
326        let lo = _mm256_castps256_ps128(v);
327        let s = _mm_add_ps(lo, hi);
328        let shuf = _mm_movehdup_ps(s);
329        let sums = _mm_add_ps(s, shuf);
330        let shuf = _mm_movehl_ps(shuf, sums);
331        _mm_cvtss_f32(_mm_add_ss(sums, shuf))
332    }
333
334    /// dot(a, b) — AVX2+FMA, 2× unrolled (16 f32/iter).
335    #[target_feature(enable = "avx2,fma")]
336    pub unsafe fn dot(a: &[f32], b: &[f32]) -> f32 {
337        let n = a.len().min(b.len());
338        let ap = a.as_ptr();
339        let bp = b.as_ptr();
340
341        let mut acc0 = _mm256_setzero_ps();
342        let mut acc1 = _mm256_setzero_ps();
343
344        let chunks16 = n / 16;
345        for i in 0..chunks16 {
346            let base = i * 16;
347            let a0 = _mm256_loadu_ps(ap.add(base));
348            let b0 = _mm256_loadu_ps(bp.add(base));
349            let a1 = _mm256_loadu_ps(ap.add(base + 8));
350            let b1 = _mm256_loadu_ps(bp.add(base + 8));
351            acc0 = _mm256_fmadd_ps(a0, b0, acc0);
352            acc1 = _mm256_fmadd_ps(a1, b1, acc1);
353        }
354
355        let chunks8 = n / 8;
356        if chunks8 > chunks16 * 2 {
357            let base = chunks16 * 16;
358            let a0 = _mm256_loadu_ps(ap.add(base));
359            let b0 = _mm256_loadu_ps(bp.add(base));
360            acc0 = _mm256_fmadd_ps(a0, b0, acc0);
361        }
362
363        let mut sum = hsum256(_mm256_add_ps(acc0, acc1));
364        for i in (chunks8 * 8)..n {
365            sum += *ap.add(i) * *bp.add(i);
366        }
367        sum
368    }
369
370    /// ||a - b||² — AVX2+FMA, 2× unrolled.
371    #[target_feature(enable = "avx2,fma")]
372    pub unsafe fn euclidean(a: &[f32], b: &[f32]) -> f32 {
373        let n = a.len().min(b.len());
374        let ap = a.as_ptr();
375        let bp = b.as_ptr();
376
377        let mut acc0 = _mm256_setzero_ps();
378        let mut acc1 = _mm256_setzero_ps();
379
380        let chunks16 = n / 16;
381        for i in 0..chunks16 {
382            let base = i * 16;
383            let d0 = _mm256_sub_ps(_mm256_loadu_ps(ap.add(base)), _mm256_loadu_ps(bp.add(base)));
384            let d1 = _mm256_sub_ps(
385                _mm256_loadu_ps(ap.add(base + 8)),
386                _mm256_loadu_ps(bp.add(base + 8)),
387            );
388            acc0 = _mm256_fmadd_ps(d0, d0, acc0);
389            acc1 = _mm256_fmadd_ps(d1, d1, acc1);
390        }
391
392        let chunks8 = n / 8;
393        if chunks8 > chunks16 * 2 {
394            let base = chunks16 * 16;
395            let d0 = _mm256_sub_ps(_mm256_loadu_ps(ap.add(base)), _mm256_loadu_ps(bp.add(base)));
396            acc0 = _mm256_fmadd_ps(d0, d0, acc0);
397        }
398
399        let mut sum = hsum256(_mm256_add_ps(acc0, acc1));
400        for i in (chunks8 * 8)..n {
401            let d = *ap.add(i) - *bp.add(i);
402            sum += d * d;
403        }
404        sum.sqrt()
405    }
406
407    /// 1 - cos(a, b) — AVX2+FMA, single pass for dot + norms².
408    #[target_feature(enable = "avx2,fma")]
409    pub unsafe fn cosine(a: &[f32], b: &[f32]) -> f32 {
410        let n = a.len().min(b.len());
411        let ap = a.as_ptr();
412        let bp = b.as_ptr();
413
414        let mut dot_acc = _mm256_setzero_ps();
415        let mut na_acc = _mm256_setzero_ps();
416        let mut nb_acc = _mm256_setzero_ps();
417
418        let chunks8 = n / 8;
419        for i in 0..chunks8 {
420            let base = i * 8;
421            let av = _mm256_loadu_ps(ap.add(base));
422            let bv = _mm256_loadu_ps(bp.add(base));
423            dot_acc = _mm256_fmadd_ps(av, bv, dot_acc);
424            na_acc = _mm256_fmadd_ps(av, av, na_acc);
425            nb_acc = _mm256_fmadd_ps(bv, bv, nb_acc);
426        }
427
428        let mut dot = hsum256(dot_acc);
429        let mut na2 = hsum256(na_acc);
430        let mut nb2 = hsum256(nb_acc);
431
432        for i in (chunks8 * 8)..n {
433            let ai = *ap.add(i);
434            let bi = *bp.add(i);
435            dot += ai * bi;
436            na2 += ai * ai;
437            nb2 += bi * bi;
438        }
439
440        let na = na2.sqrt();
441        let nb = nb2.sqrt();
442        if na == 0.0 || nb == 0.0 {
443            return 1.0;
444        }
445        1.0 - dot / (na * nb)
446    }
447}
448
449// ── x86_64 AVX2 + F16C — F16 hot path ────────────────────────────────────────
450//
451// _mm256_cvtph_ps converts 8 packed F16 (as __m128i) to 8 F32 in one cycle.
452// Combined with FMA, this replaces 8 scalar half::to_f32() calls per iteration.
453// Critical hot path: every HNSW edge traversal calls one of these functions.
454
455#[cfg(target_arch = "x86_64")]
456mod avx2_f16c {
457    use half::f16;
458    use std::arch::x86_64::*;
459
460    use super::avx2::hsum256;
461
462    /// dot(a_f32, b_f16) — AVX2+F16C+FMA, 16 F16/iter.
463    #[target_feature(enable = "avx2,f16c,fma")]
464    pub unsafe fn dot(a: &[f32], b: &[f16]) -> f32 {
465        let n = a.len().min(b.len());
466        let ap = a.as_ptr();
467        let bp = b.as_ptr() as *const u16;
468
469        let mut acc0 = _mm256_setzero_ps();
470        let mut acc1 = _mm256_setzero_ps();
471
472        let chunks16 = n / 16;
473        for i in 0..chunks16 {
474            let base = i * 16;
475            let b0 = _mm256_cvtph_ps(_mm_loadu_si128(bp.add(base) as *const __m128i));
476            let b1 = _mm256_cvtph_ps(_mm_loadu_si128(bp.add(base + 8) as *const __m128i));
477            let a0 = _mm256_loadu_ps(ap.add(base));
478            let a1 = _mm256_loadu_ps(ap.add(base + 8));
479            acc0 = _mm256_fmadd_ps(a0, b0, acc0);
480            acc1 = _mm256_fmadd_ps(a1, b1, acc1);
481        }
482
483        let chunks8 = n / 8;
484        if chunks8 > chunks16 * 2 {
485            let base = chunks16 * 16;
486            let b0 = _mm256_cvtph_ps(_mm_loadu_si128(bp.add(base) as *const __m128i));
487            let a0 = _mm256_loadu_ps(ap.add(base));
488            acc0 = _mm256_fmadd_ps(a0, b0, acc0);
489        }
490
491        let mut sum = hsum256(_mm256_add_ps(acc0, acc1));
492        for i in (chunks8 * 8)..n {
493            sum += *ap.add(i) * f16::from_bits(*bp.add(i)).to_f32();
494        }
495        sum
496    }
497
498    /// ||a_f32 - b_f16||² — AVX2+F16C+FMA, 16 F16/iter.
499    #[target_feature(enable = "avx2,f16c,fma")]
500    pub unsafe fn euclidean(a: &[f32], b: &[f16]) -> f32 {
501        let n = a.len().min(b.len());
502        let ap = a.as_ptr();
503        let bp = b.as_ptr() as *const u16;
504
505        let mut acc0 = _mm256_setzero_ps();
506        let mut acc1 = _mm256_setzero_ps();
507
508        let chunks16 = n / 16;
509        for i in 0..chunks16 {
510            let base = i * 16;
511            let b0 = _mm256_cvtph_ps(_mm_loadu_si128(bp.add(base) as *const __m128i));
512            let b1 = _mm256_cvtph_ps(_mm_loadu_si128(bp.add(base + 8) as *const __m128i));
513            let d0 = _mm256_sub_ps(_mm256_loadu_ps(ap.add(base)), b0);
514            let d1 = _mm256_sub_ps(_mm256_loadu_ps(ap.add(base + 8)), b1);
515            acc0 = _mm256_fmadd_ps(d0, d0, acc0);
516            acc1 = _mm256_fmadd_ps(d1, d1, acc1);
517        }
518
519        let chunks8 = n / 8;
520        if chunks8 > chunks16 * 2 {
521            let base = chunks16 * 16;
522            let b0 = _mm256_cvtph_ps(_mm_loadu_si128(bp.add(base) as *const __m128i));
523            let d0 = _mm256_sub_ps(_mm256_loadu_ps(ap.add(base)), b0);
524            acc0 = _mm256_fmadd_ps(d0, d0, acc0);
525        }
526
527        let mut sum = hsum256(_mm256_add_ps(acc0, acc1));
528        for i in (chunks8 * 8)..n {
529            let diff = *ap.add(i) - f16::from_bits(*bp.add(i)).to_f32();
530            sum += diff * diff;
531        }
532        sum.sqrt()
533    }
534
535    /// 1 - cos(a_f32, b_f16) — AVX2+F16C+FMA, single pass.
536    #[target_feature(enable = "avx2,f16c,fma")]
537    pub unsafe fn cosine(a: &[f32], b: &[f16]) -> f32 {
538        let n = a.len().min(b.len());
539        let ap = a.as_ptr();
540        let bp = b.as_ptr() as *const u16;
541
542        let mut dot_acc = _mm256_setzero_ps();
543        let mut na_acc = _mm256_setzero_ps();
544        let mut nb_acc = _mm256_setzero_ps();
545
546        let chunks8 = n / 8;
547        for i in 0..chunks8 {
548            let base = i * 8;
549            let av = _mm256_loadu_ps(ap.add(base));
550            let bv = _mm256_cvtph_ps(_mm_loadu_si128(bp.add(base) as *const __m128i));
551            dot_acc = _mm256_fmadd_ps(av, bv, dot_acc);
552            na_acc = _mm256_fmadd_ps(av, av, na_acc);
553            nb_acc = _mm256_fmadd_ps(bv, bv, nb_acc);
554        }
555
556        let mut dot = hsum256(dot_acc);
557        let mut na2 = hsum256(na_acc);
558        let mut nb2 = hsum256(nb_acc);
559
560        for i in (chunks8 * 8)..n {
561            let ai = *ap.add(i);
562            let bi = f16::from_bits(*bp.add(i)).to_f32();
563            dot += ai * bi;
564            na2 += ai * ai;
565            nb2 += bi * bi;
566        }
567
568        let denom = (na2 * nb2).sqrt();
569        if denom < 1e-8 {
570            1.0
571        } else {
572            1.0 - dot / denom
573        }
574    }
575}
576
577// ── x86_64 AVX-512F — forward compatibility ───────────────────────────────────
578//
579// 16 f32/iter (vs 8 for AVX2). Runtime-detected — skipped on this machine
580// (no avx512f), active on Xeon Scalable, Zen 4+, and Intel Core 12th gen+.
581// Requires Rust ≥ 1.89 (AVX-512 intrinsics stabilised there). Gated behind
582// the `avx512` feature so the default/manylinux build always succeeds.
583
584#[cfg(all(target_arch = "x86_64", feature = "avx512"))]
585mod avx512 {
586    use half::f16;
587    use std::arch::x86_64::*;
588
589    #[inline(always)]
590    unsafe fn hsum512(v: __m512) -> f32 {
591        // _mm512_reduce_add_ps stabilized Rust 1.89; _mm512_extractf32x8_ps needs avx512dq.
592        // Store all 16 lanes to stack (avx512f), reload as two __m256 (avx), then reduce.
593        let mut buf = [0.0f32; 16];
594        _mm512_storeu_ps(buf.as_mut_ptr(), v);
595        let lo = _mm256_loadu_ps(buf.as_ptr());
596        let hi = _mm256_loadu_ps(buf.as_ptr().add(8));
597        let sum256 = _mm256_add_ps(lo, hi);
598        let hi128 = _mm256_extractf128_ps(sum256, 1);
599        let lo128 = _mm256_castps256_ps128(sum256);
600        let sum128 = _mm_add_ps(lo128, hi128);
601        let shuf = _mm_movehdup_ps(sum128);
602        let sums = _mm_add_ps(sum128, shuf);
603        let shuf2 = _mm_movehl_ps(shuf, sums);
604        _mm_cvtss_f32(_mm_add_ss(sums, shuf2))
605    }
606
607    #[target_feature(enable = "avx512f,fma")]
608    pub unsafe fn dot(a: &[f32], b: &[f32]) -> f32 {
609        let n = a.len().min(b.len());
610        let ap = a.as_ptr();
611        let bp = b.as_ptr();
612        let mut acc = _mm512_setzero_ps();
613        let chunks16 = n / 16;
614        for i in 0..chunks16 {
615            let base = i * 16;
616            acc = _mm512_fmadd_ps(
617                _mm512_loadu_ps(ap.add(base)),
618                _mm512_loadu_ps(bp.add(base)),
619                acc,
620            );
621        }
622        let mut sum = hsum512(acc);
623        for i in (chunks16 * 16)..n {
624            sum += *ap.add(i) * *bp.add(i);
625        }
626        sum
627    }
628
629    #[target_feature(enable = "avx512f,fma")]
630    pub unsafe fn euclidean(a: &[f32], b: &[f32]) -> f32 {
631        let n = a.len().min(b.len());
632        let ap = a.as_ptr();
633        let bp = b.as_ptr();
634        let mut acc = _mm512_setzero_ps();
635        let chunks16 = n / 16;
636        for i in 0..chunks16 {
637            let base = i * 16;
638            let d = _mm512_sub_ps(_mm512_loadu_ps(ap.add(base)), _mm512_loadu_ps(bp.add(base)));
639            acc = _mm512_fmadd_ps(d, d, acc);
640        }
641        let mut sum = hsum512(acc);
642        for i in (chunks16 * 16)..n {
643            let d = *ap.add(i) - *bp.add(i);
644            sum += d * d;
645        }
646        sum.sqrt()
647    }
648
649    #[target_feature(enable = "avx512f,fma")]
650    pub unsafe fn cosine(a: &[f32], b: &[f32]) -> f32 {
651        let n = a.len().min(b.len());
652        let ap = a.as_ptr();
653        let bp = b.as_ptr();
654        let mut dot_acc = _mm512_setzero_ps();
655        let mut na_acc = _mm512_setzero_ps();
656        let mut nb_acc = _mm512_setzero_ps();
657        let chunks16 = n / 16;
658        for i in 0..chunks16 {
659            let base = i * 16;
660            let av = _mm512_loadu_ps(ap.add(base));
661            let bv = _mm512_loadu_ps(bp.add(base));
662            dot_acc = _mm512_fmadd_ps(av, bv, dot_acc);
663            na_acc = _mm512_fmadd_ps(av, av, na_acc);
664            nb_acc = _mm512_fmadd_ps(bv, bv, nb_acc);
665        }
666        let mut dot = hsum512(dot_acc);
667        let mut na2 = hsum512(na_acc);
668        let mut nb2 = hsum512(nb_acc);
669        for i in (chunks16 * 16)..n {
670            let ai = *ap.add(i);
671            let bi = *bp.add(i);
672            dot += ai * bi;
673            na2 += ai * ai;
674            nb2 += bi * bi;
675        }
676        let (na, nb) = (na2.sqrt(), nb2.sqrt());
677        if na == 0.0 || nb == 0.0 {
678            return 1.0;
679        }
680        1.0 - dot / (na * nb)
681    }
682
683    /// dot(a_f32, b_f16) — AVX-512F+F16C+FMA, 16 F16/iter.
684    #[target_feature(enable = "avx512f,f16c,fma")]
685    pub unsafe fn dot_f16(a: &[f32], b: &[f16]) -> f32 {
686        let n = a.len().min(b.len());
687        let ap = a.as_ptr();
688        let bp = b.as_ptr() as *const u16;
689        let mut acc = _mm512_setzero_ps();
690        let chunks16 = n / 16;
691        for i in 0..chunks16 {
692            let base = i * 16;
693            let b_lo = _mm256_cvtph_ps(_mm_loadu_si128(bp.add(base) as *const __m128i));
694            let b_hi = _mm256_cvtph_ps(_mm_loadu_si128(bp.add(base + 8) as *const __m128i));
695            let bv = _mm512_insertf32x8(_mm512_castps256_ps512(b_lo), b_hi, 1);
696            acc = _mm512_fmadd_ps(_mm512_loadu_ps(ap.add(base)), bv, acc);
697        }
698        let mut sum = hsum512(acc);
699        for i in (chunks16 * 16)..n {
700            sum += *ap.add(i) * f16::from_bits(*bp.add(i)).to_f32();
701        }
702        sum
703    }
704
705    #[target_feature(enable = "avx512f,f16c,fma")]
706    pub unsafe fn euclidean_f16(a: &[f32], b: &[f16]) -> f32 {
707        let n = a.len().min(b.len());
708        let ap = a.as_ptr();
709        let bp = b.as_ptr() as *const u16;
710        let mut acc = _mm512_setzero_ps();
711        let chunks16 = n / 16;
712        for i in 0..chunks16 {
713            let base = i * 16;
714            let b_lo = _mm256_cvtph_ps(_mm_loadu_si128(bp.add(base) as *const __m128i));
715            let b_hi = _mm256_cvtph_ps(_mm_loadu_si128(bp.add(base + 8) as *const __m128i));
716            let bv = _mm512_insertf32x8(_mm512_castps256_ps512(b_lo), b_hi, 1);
717            let d = _mm512_sub_ps(_mm512_loadu_ps(ap.add(base)), bv);
718            acc = _mm512_fmadd_ps(d, d, acc);
719        }
720        let mut sum = hsum512(acc);
721        for i in (chunks16 * 16)..n {
722            let d = *ap.add(i) - f16::from_bits(*bp.add(i)).to_f32();
723            sum += d * d;
724        }
725        sum.sqrt()
726    }
727
728    #[target_feature(enable = "avx512f,f16c,fma")]
729    pub unsafe fn cosine_f16(a: &[f32], b: &[f16]) -> f32 {
730        let n = a.len().min(b.len());
731        let ap = a.as_ptr();
732        let bp = b.as_ptr() as *const u16;
733        let mut dot_acc = _mm512_setzero_ps();
734        let mut na_acc = _mm512_setzero_ps();
735        let mut nb_acc = _mm512_setzero_ps();
736        let chunks16 = n / 16;
737        for i in 0..chunks16 {
738            let base = i * 16;
739            let b_lo = _mm256_cvtph_ps(_mm_loadu_si128(bp.add(base) as *const __m128i));
740            let b_hi = _mm256_cvtph_ps(_mm_loadu_si128(bp.add(base + 8) as *const __m128i));
741            let bv = _mm512_insertf32x8(_mm512_castps256_ps512(b_lo), b_hi, 1);
742            let av = _mm512_loadu_ps(ap.add(base));
743            dot_acc = _mm512_fmadd_ps(av, bv, dot_acc);
744            na_acc = _mm512_fmadd_ps(av, av, na_acc);
745            nb_acc = _mm512_fmadd_ps(bv, bv, nb_acc);
746        }
747        let mut dot = hsum512(dot_acc);
748        let mut na2 = hsum512(na_acc);
749        let mut nb2 = hsum512(nb_acc);
750        for i in (chunks16 * 16)..n {
751            let ai = *ap.add(i);
752            let bi = f16::from_bits(*bp.add(i)).to_f32();
753            dot += ai * bi;
754            na2 += ai * ai;
755            nb2 += bi * bi;
756        }
757        let denom = (na2 * nb2).sqrt();
758        if denom < 1e-8 {
759            1.0
760        } else {
761            1.0 - dot / denom
762        }
763    }
764}
765
766// ── aarch64 NEON ──────────────────────────────────────────────────────────────
767
768#[cfg(target_arch = "aarch64")]
769mod neon_impl {
770    use std::arch::aarch64::*;
771
772    #[target_feature(enable = "neon")]
773    pub unsafe fn dot(a: &[f32], b: &[f32]) -> f32 {
774        let n = a.len().min(b.len());
775        let mut acc = vdupq_n_f32(0.0);
776        let chunks = n / 4;
777        for i in 0..chunks {
778            let base = i * 4;
779            let av = vld1q_f32(a.as_ptr().add(base));
780            let bv = vld1q_f32(b.as_ptr().add(base));
781            acc = vmlaq_f32(acc, av, bv);
782        }
783        let mut sum = vaddvq_f32(acc);
784        for i in (chunks * 4)..n {
785            sum += a[i] * b[i];
786        }
787        sum
788    }
789
790    #[target_feature(enable = "neon")]
791    pub unsafe fn euclidean(a: &[f32], b: &[f32]) -> f32 {
792        let n = a.len().min(b.len());
793        let mut acc = vdupq_n_f32(0.0);
794        let chunks = n / 4;
795        for i in 0..chunks {
796            let base = i * 4;
797            let d = vsubq_f32(
798                vld1q_f32(a.as_ptr().add(base)),
799                vld1q_f32(b.as_ptr().add(base)),
800            );
801            acc = vmlaq_f32(acc, d, d);
802        }
803        let mut sum = vaddvq_f32(acc);
804        for i in (chunks * 4)..n {
805            let d = a[i] - b[i];
806            sum += d * d;
807        }
808        sum.sqrt()
809    }
810
811    #[target_feature(enable = "neon")]
812    pub unsafe fn cosine(a: &[f32], b: &[f32]) -> f32 {
813        let n = a.len().min(b.len());
814        let mut dot_acc = vdupq_n_f32(0.0);
815        let mut na_acc = vdupq_n_f32(0.0);
816        let mut nb_acc = vdupq_n_f32(0.0);
817        let chunks = n / 4;
818        for i in 0..chunks {
819            let base = i * 4;
820            let av = vld1q_f32(a.as_ptr().add(base));
821            let bv = vld1q_f32(b.as_ptr().add(base));
822            dot_acc = vmlaq_f32(dot_acc, av, bv);
823            na_acc = vmlaq_f32(na_acc, av, av);
824            nb_acc = vmlaq_f32(nb_acc, bv, bv);
825        }
826        let mut dot = vaddvq_f32(dot_acc);
827        let mut na2 = vaddvq_f32(na_acc);
828        let mut nb2 = vaddvq_f32(nb_acc);
829        for i in (chunks * 4)..n {
830            dot += a[i] * b[i];
831            na2 += a[i] * a[i];
832            nb2 += b[i] * b[i];
833        }
834        let (na, nb) = (na2.sqrt(), nb2.sqrt());
835        if na == 0.0 || nb == 0.0 {
836            return 1.0;
837        }
838        1.0 - dot / (na * nb)
839    }
840}
841
842// ── Tests ─────────────────────────────────────────────────────────────────────
843
844#[cfg(test)]
845mod tests {
846    use super::*;
847    use proptest::prelude::*;
848
849    // ── Proptest: SIMD kernels match scalar fallback ──────────────────────
850
851    fn arb_vec(dim: usize) -> impl Strategy<Value = Vec<f32>> {
852        proptest::collection::vec(proptest::num::f32::ANY, dim)
853    }
854
855    fn arb_vecs() -> impl Strategy<Value = (Vec<f32>, Vec<f32>)> {
856        (1usize..2048).prop_flat_map(|dim| (arb_vec(dim), arb_vec(dim)))
857    }
858
859    fn arb_f16_vec(dim: usize) -> impl Strategy<Value = Vec<f16>> {
860        proptest::collection::vec(proptest::num::f32::ANY, dim)
861            .prop_map(|v| v.into_iter().map(f16::from_f32).collect())
862    }
863
864    fn arb_f16_vecs() -> impl Strategy<Value = (Vec<f32>, Vec<f16>)> {
865        (1usize..2048).prop_flat_map(|dim| (arb_vec(dim), arb_f16_vec(dim)))
866    }
867
868    proptest! {
869        #[test]
870        fn prop_dot_matches_scalar((a, b) in arb_vecs()) {
871            let expected = dot_scalar(&a, &b);
872            let actual = dot_product(&a, &b);
873            let max_err = (expected.abs().max(1.0)) * 1e-4;
874            if expected.is_finite() && actual.is_finite() {
875                prop_assert!(
876                    (actual - expected).abs() <= max_err,
877                    "dot simd={actual} scalar={expected} dim={}",
878                    a.len()
879                );
880            }
881        }
882
883        #[test]
884        fn prop_euclidean_matches_scalar((a, b) in arb_vecs()) {
885            let expected = euclidean_scalar(&a, &b);
886            let actual = euclidean_distance(&a, &b);
887            let max_err = (expected.abs().max(1.0)) * 1e-4;
888            if expected.is_finite() && actual.is_finite() {
889                prop_assert!(
890                    (actual - expected).abs() <= max_err,
891                    "euclidean simd={actual} scalar={expected} dim={}",
892                    a.len()
893                );
894            }
895        }
896
897        #[test]
898        fn prop_cosine_matches_scalar((a, b) in arb_vecs()) {
899            let expected = cosine_scalar(&a, &b);
900            let actual = cosine_distance(&a, &b);
901            let max_err = (expected.abs().max(1.0)) * 1e-4;
902            if expected.is_finite() && actual.is_finite() {
903                prop_assert!(
904                    (actual - expected).abs() <= max_err,
905                    "cosine simd={actual} scalar={expected} dim={}",
906                    a.len()
907                );
908            }
909        }
910
911        #[test]
912        fn prop_dot_f16_matches_scalar((a, b) in arb_f16_vecs()) {
913            let expected = dot_f16_scalar(&a, &b);
914            let actual = dot_product_f16(&a, &b);
915            let max_err = (expected.abs().max(1.0)) * 1e-3;
916            if expected.is_finite() && actual.is_finite() {
917                prop_assert!(
918                    (actual - expected).abs() <= max_err,
919                    "f16 dot simd={actual} scalar={expected} dim={}",
920                    a.len()
921                );
922            }
923        }
924
925        #[test]
926        fn prop_euclidean_f16_matches_scalar((a, b) in arb_f16_vecs()) {
927            let expected = euclidean_f16_scalar(&a, &b);
928            let actual = euclidean_distance_f16(&a, &b);
929            let max_err = (expected.abs().max(1.0)) * 1e-3;
930            if expected.is_finite() && actual.is_finite() {
931                prop_assert!(
932                    (actual - expected).abs() <= max_err,
933                    "f16 euclidean simd={actual} scalar={expected} dim={}",
934                    a.len()
935                );
936            }
937        }
938
939        #[test]
940        fn prop_cosine_f16_matches_scalar((a, b) in arb_f16_vecs()) {
941            let expected = cosine_f16_scalar(&a, &b);
942            let actual = cosine_distance_f16(&a, &b);
943            let max_err = (expected.abs().max(1.0)) * 1e-3;
944            if expected.is_finite() && actual.is_finite() {
945                prop_assert!(
946                    (actual - expected).abs() <= max_err,
947                    "f16 cosine simd={actual} scalar={expected} dim={}",
948                    a.len()
949                );
950            }
951        }
952    }
953
954    // ── Deterministic edge cases ──────────────────────────────────────────
955
956    #[test]
957    fn cosine_identical() {
958        let v = vec![1.0f32, 0.0, 0.0];
959        assert!(cosine_distance(&v, &v).abs() < 1e-5);
960    }
961
962    #[test]
963    fn cosine_orthogonal() {
964        assert!((cosine_distance(&[1.0f32, 0.0], &[0.0f32, 1.0]) - 1.0).abs() < 1e-5);
965    }
966
967    #[test]
968    fn euclidean_basic() {
969        assert!((euclidean_distance(&[0.0f32, 0.0], &[3.0f32, 4.0]) - 5.0).abs() < 1e-5);
970    }
971
972    #[test]
973    fn dot_basic() {
974        assert!((dot_product(&[1.0f32, 2.0, 3.0], &[4.0f32, 5.0, 6.0]) - 32.0).abs() < 1e-5);
975    }
976
977    #[test]
978    fn simd_matches_scalar_dim128() {
979        use rand::{rngs::StdRng, Rng, SeedableRng};
980        let mut rng = StdRng::seed_from_u64(99);
981        let a: Vec<f32> = (0..128).map(|_| rng.gen::<f32>() * 2.0 - 1.0).collect();
982        let b: Vec<f32> = (0..128).map(|_| rng.gen::<f32>() * 2.0 - 1.0).collect();
983
984        let dot_s = dot_scalar(&a, &b);
985        let euclid_s = euclidean_scalar(&a, &b);
986        let cos_s = cosine_scalar(&a, &b);
987
988        let dot_f = dot_product(&a, &b);
989        let euclid_f = euclidean_distance(&a, &b);
990        let cos_f = cosine_distance(&a, &b);
991
992        assert!(
993            (dot_f - dot_s).abs() < 1e-4,
994            "dot mismatch: {dot_f} vs {dot_s}"
995        );
996        assert!(
997            (euclid_f - euclid_s).abs() < 1e-4,
998            "euclidean mismatch: {euclid_f} vs {euclid_s}"
999        );
1000        assert!(
1001            (cos_f - cos_s).abs() < 1e-4,
1002            "cosine mismatch: {cos_f} vs {cos_s}"
1003        );
1004    }
1005
1006    #[test]
1007    fn f16_simd_matches_scalar() {
1008        use rand::{rngs::StdRng, Rng, SeedableRng};
1009        let mut rng = StdRng::seed_from_u64(42);
1010        let a: Vec<f32> = (0..128).map(|_| rng.gen::<f32>() * 2.0 - 1.0).collect();
1011        let b_f32: Vec<f32> = (0..128).map(|_| rng.gen::<f32>() * 2.0 - 1.0).collect();
1012        let b: Vec<f16> = b_f32.iter().map(|&x| f16::from_f32(x)).collect();
1013
1014        let dot_s = dot_f16_scalar(&a, &b);
1015        let euclid_s = euclidean_f16_scalar(&a, &b);
1016        let cos_s = cosine_f16_scalar(&a, &b);
1017
1018        let dot_f = dot_product_f16(&a, &b);
1019        let euclid_f = euclidean_distance_f16(&a, &b);
1020        let cos_f = cosine_distance_f16(&a, &b);
1021
1022        // F16 rounding introduces small error — tolerate 1e-3
1023        assert!(
1024            (dot_f - dot_s).abs() < 1e-3,
1025            "f16 dot mismatch: {dot_f} vs {dot_s}"
1026        );
1027        assert!(
1028            (euclid_f - euclid_s).abs() < 1e-3,
1029            "f16 euclidean mismatch: {euclid_f} vs {euclid_s}"
1030        );
1031        assert!(
1032            (cos_f - cos_s).abs() < 1e-3,
1033            "f16 cosine mismatch: {cos_f} vs {cos_s}"
1034        );
1035    }
1036
1037    #[test]
1038    fn normalize_l2_unit() {
1039        let v = vec![3.0f32, 4.0];
1040        let n = normalize_l2(&v);
1041        let norm: f32 = n.iter().map(|x| x * x).sum::<f32>().sqrt();
1042        assert!((norm - 1.0).abs() < 1e-6, "norm={norm}");
1043        assert!((n[0] - 0.6).abs() < 1e-6);
1044        assert!((n[1] - 0.8).abs() < 1e-6);
1045    }
1046
1047    #[test]
1048    fn normalized_cosine_matches_cosine_on_unit_vecs() {
1049        let a = normalize_l2(&[1.0f32, 1.0, 0.0]);
1050        let b = normalize_l2(&[1.0f32, 0.0, 1.0]);
1051        let cos = cosine_distance(&a, &b);
1052        let ncos = normalized_cosine_distance(&a, &b);
1053        assert!((cos - ncos).abs() < 1e-5, "cos={cos} ncos={ncos}");
1054    }
1055
1056    #[test]
1057    fn centroid_single() {
1058        let v = vec![vec![1.0f32, 2.0, 3.0]];
1059        let c = compute_centroid_and_radius(&v, VectorMetric::Cosine);
1060        assert_eq!(c.values, vec![1.0, 2.0, 3.0]);
1061        assert!(c.radius < 1e-6, "radius={}", c.radius);
1062    }
1063
1064    #[test]
1065    fn centroid_two_points() {
1066        let vs = vec![vec![0.0f32, 0.0], vec![2.0f32, 2.0]];
1067        let c = compute_centroid_and_radius(&vs, VectorMetric::Euclidean);
1068        assert!((c.values[0] - 1.0).abs() < 1e-6);
1069        assert!(c.radius > 0.0);
1070    }
1071
1072    #[test]
1073    fn centroid_excludes_nan_poisoned_vector_instead_of_collapsing_radius() {
1074        // One NaN-poisoned embedding mixed in with healthy vectors clustered around
1075        // (10, 10) — before the fix, the poisoned vector alone corrupted every dimension
1076        // of the shared centroid average, driving nearly every distance non-finite and
1077        // radius silently to 0.0. The centroid/radius should now reflect only the
1078        // healthy vectors.
1079        let vs = vec![
1080            vec![10.0f32, 10.0],
1081            vec![11.0f32, 9.0],
1082            vec![9.0f32, 11.0],
1083            vec![f32::NAN, 10.0],
1084        ];
1085        let c = compute_centroid_and_radius(&vs, VectorMetric::Euclidean);
1086        assert!(c.values.iter().all(|x| x.is_finite()), "{:?}", c.values);
1087        assert!((c.values[0] - 10.0).abs() < 1e-6, "{:?}", c.values);
1088        assert!((c.values[1] - 10.0).abs() < 1e-6, "{:?}", c.values);
1089        assert!(c.radius > 0.5, "radius collapsed to {}", c.radius);
1090    }
1091
1092    // ── Edge cases: NaN, Inf, zero vectors ───────────────────────────────
1093
1094    #[test]
1095    fn dot_product_nan_inputs() {
1096        let a = vec![f32::NAN, 1.0];
1097        let b = vec![1.0, 2.0];
1098        let result = dot_product(&a, &b);
1099        assert!(
1100            result.is_nan(),
1101            "dot with NaN should produce NaN, got {result}"
1102        );
1103    }
1104
1105    #[test]
1106    fn dot_product_inf_inputs() {
1107        let a = vec![f32::INFINITY, 1.0];
1108        let b = vec![1.0, 2.0];
1109        let result = dot_product(&a, &b);
1110        assert!(
1111            result.is_infinite(),
1112            "dot with Inf should produce Inf, got {result}"
1113        );
1114    }
1115
1116    #[test]
1117    fn dot_product_zero_vector() {
1118        let a = vec![0.0f32; 4];
1119        let b = vec![1.0, 2.0, 3.0, 4.0];
1120        let result = dot_product(&a, &b);
1121        assert_eq!(
1122            result, 0.0,
1123            "dot with zero vector should be 0, got {result}"
1124        );
1125    }
1126
1127    #[test]
1128    fn euclidean_nan_input() {
1129        let a = vec![f32::NAN, 1.0];
1130        let b = vec![1.0, 2.0];
1131        let result = euclidean_distance(&a, &b);
1132        assert!(
1133            result.is_nan(),
1134            "euclidean with NaN should produce NaN, got {result}"
1135        );
1136    }
1137
1138    #[test]
1139    fn euclidean_zero_vector() {
1140        let a = vec![0.0f32; 3];
1141        let b = vec![3.0, 4.0, 0.0];
1142        let result = euclidean_distance(&a, &b);
1143        assert!((result - 5.0).abs() < 1e-5, "expected 5.0, got {result}");
1144    }
1145
1146    #[test]
1147    fn cosine_zero_vector_handles_gracefully() {
1148        let zero = vec![0.0f32; 4];
1149        let other = vec![1.0, 0.0, 0.0, 0.0];
1150        // Cosine with a zero vector: ||zero|| = 0 → division by zero
1151        // Should return a finite value (1.0 = max distance) rather than panicking
1152        let result = cosine_distance(&zero, &other);
1153        assert!(
1154            result.is_finite(),
1155            "cosine with zero vector should return finite value, got {result}"
1156        );
1157    }
1158
1159    #[test]
1160    fn cosine_both_zero_vectors() {
1161        let zero = vec![0.0f32; 4];
1162        let result = cosine_distance(&zero, &zero);
1163        assert!(
1164            result.is_finite(),
1165            "cosine with both zero should return finite value, got {result}"
1166        );
1167    }
1168
1169    #[test]
1170    fn normalize_l2_zero_vector() {
1171        let v = vec![0.0f32; 4];
1172        let n = normalize_l2(&v);
1173        // Should return zero vector (not NaN)
1174        for x in &n {
1175            assert!(x.is_finite(), "normalized zero vector has non-finite {x}");
1176        }
1177        let norm: f32 = n.iter().map(|x| x * x).sum::<f32>().sqrt();
1178        assert!(norm.abs() < 1e-6, "norm should be 0, got {norm}");
1179    }
1180
1181    // ── Dimension mismatch ───────────────────────────────────────────────
1182
1183    #[test]
1184    #[should_panic(expected = "dimension mismatch")]
1185    fn dot_dimension_mismatch_panics() {
1186        let a = vec![1.0f32; 3];
1187        let b = vec![2.0f32; 5];
1188        dot_product(&a, &b);
1189    }
1190
1191    #[test]
1192    #[should_panic(expected = "dimension mismatch")]
1193    fn euclidean_dimension_mismatch_panics() {
1194        euclidean_distance(&[1.0f32; 3], &[2.0f32; 5]);
1195    }
1196
1197    #[test]
1198    #[should_panic(expected = "dimension mismatch")]
1199    fn cosine_dimension_mismatch_panics() {
1200        cosine_distance(&[1.0f32; 3], &[2.0f32; 5]);
1201    }
1202
1203    #[cfg(miri)]
1204    mod miri_tests {
1205        use super::*;
1206
1207        /// Scalar dot product under Miri — exercita o path safe (não-SIMD).
1208        #[test]
1209        fn miri_scalar_dot_product() {
1210            let a = vec![1.0f32; 100];
1211            let b = vec![2.0f32; 100];
1212            let r = dot_scalar(&a, &b);
1213            assert!((r - 200.0).abs() < 1e-5);
1214        }
1215
1216        /// Cosine com vetor zero — divisão por zero, deve retornar finito.
1217        #[test]
1218        fn miri_scalar_cosine_zero_vectors() {
1219            let zero = vec![0.0f32; 64];
1220            let r = cosine_scalar(&zero, &zero);
1221            assert!(r.is_finite());
1222        }
1223
1224        /// Dimension mismatch — deve panic via assert! na função scalar.
1225        #[test]
1226        #[should_panic(expected = "dimension mismatch")]
1227        fn miri_scalar_dimension_mismatch() {
1228            dot_scalar(&[1.0f32; 3], &[2.0f32; 5]);
1229        }
1230
1231        /// Normalize L2 — edge case de vetor zero.
1232        #[test]
1233        fn miri_normalize_l2_zero() {
1234            let v = vec![0.0f32; 32];
1235            let n = normalize_l2(&v);
1236            for x in &n {
1237                assert!(x.is_finite(), "non-finite {x}");
1238            }
1239        }
1240    }
1241
1242    // ── NormalizedCosine edge cases ──────────────────────────────────────
1243
1244    #[test]
1245    fn normalized_cosine_identity() {
1246        let v = vec![0.5f32, 0.5, 0.5, 0.5];
1247        let d = normalized_cosine_distance(&v, &v);
1248        assert!(d.abs() < 1e-6, "identity distance should be 0, got {d}");
1249    }
1250
1251    #[test]
1252    fn normalized_cosine_orthogonal() {
1253        let a = normalize_l2(&[1.0f32, 0.0]);
1254        let b = normalize_l2(&[0.0f32, 1.0]);
1255        let d = normalized_cosine_distance(&a, &b);
1256        assert!(
1257            (d - 1.0).abs() < 1e-5,
1258            "orthogonal distance should be 1, got {d}"
1259        );
1260    }
1261}