Skip to main content

chronix_analytics/compute/
simd.rs

1//! Vectorized batch operations with multi-tier SIMD dispatch.
2//!
3//! Provides high-performance implementations for common statistical operations
4//! (sum, mean, variance, min/max, dot product, standard deviation) with
5//! automatic architecture-specific acceleration.
6//!
7//! ## SIMD dispatch hierarchy (x86_64)
8//!
9//! ```text
10//!  AVX-512F (512-bit, 8×f64)  ← runtime-detected via is_x86_feature_detected!
11//!      │  fallback
12//!  AVX2+FMA (256-bit, 4×f64)  ← runtime-detected via is_x86_feature_detected!
13//!      │  fallback
14//!  SSE2 (128-bit, 2×f64)      ← baseline, always available on x86_64
15//! ```
16//!
17//! ## SIMD dispatch (aarch64)
18//!
19//! NEON (128-bit, 2×f64) — baseline, always available on aarch64.
20//!
21//! ## Fallback
22//!
23//! Portable 4-wide scalar unroll with ILP on all other architectures.
24//!
25//! All tiers use **4 accumulators** to hide latency and maximize throughput.
26//! AVX-512: 4 acc × 8 lanes = 32 elements/iteration.
27//! AVX2: 4 acc × 4 lanes = 16 elements/iteration.
28//! SSE2/NEON: 4 acc × 2 lanes = 8 elements/iteration.
29
30// ═══════════════════════════════════════════════════════════════════════
31// Horizontal reduction helpers (used by AVX2 and AVX-512 tiers)
32//
33// # Safety
34//
35// All functions in this section operate exclusively on SIMD register values
36// passed by the caller. They contain no raw pointer dereferences, no memory
37// accesses beyond the register file, and no side effects. Safety is
38// guaranteed by:
39//
40// 1. The caller has verified the required CPU feature (via `#[target_feature]`
41//    attribute or runtime `is_x86_feature_detected!()` check).
42// 2. Inputs are `__m256d` / `__m512d` register values — not raw pointers.
43// 3. All intrinsic calls are register-to-register (no loads/stores).
44// ═══════════════════════════════════════════════════════════════════════
45
46/// Horizontal sum of a 256-bit register (4×f64 → scalar).
47///
48/// # Safety
49///
50/// Caller must ensure AVX is available (e.g. via `is_x86_feature_detected!("avx2")`
51/// or a `#[target_feature(enable = "avx2")]` annotation on the call site).
52#[cfg(target_arch = "x86_64")]
53#[inline(always)]
54unsafe fn hsum_256(v: std::arch::x86_64::__m256d) -> f64 {
55    use std::arch::x86_64::*;
56    // SAFETY: register-to-register intrinsics only — no loads, no stores.
57    // The caller guarantees the CPU feature, per this function's `# Safety`.
58    unsafe {
59        // hi = [v2, v3], lo = [v0, v1]
60        let hi = _mm256_extractf128_pd(v, 1);
61        let lo = _mm256_castpd256_pd128(v);
62        let sum128 = _mm_add_pd(lo, hi); // [v0+v2, v1+v3]
63        let hi64 = _mm_unpackhi_pd(sum128, sum128);
64        _mm_cvtsd_f64(_mm_add_sd(sum128, hi64))
65    }
66}
67
68/// Horizontal sum of a 512-bit register (8×f64 → scalar) via manual lane
69/// extraction (avoids `_mm512_reduce_add_pd` which requires Rust ≥ 1.77).
70///
71/// # Safety
72///
73/// Caller must ensure AVX-512F is available.
74#[cfg(target_arch = "x86_64")]
75#[target_feature(enable = "avx512f")]
76#[inline]
77unsafe fn hsum_512(v: std::arch::x86_64::__m512d) -> f64 {
78    use std::arch::x86_64::*;
79    // SAFETY: register-to-register intrinsics only — no loads, no stores.
80    // The caller guarantees the CPU feature, per this function's `# Safety`.
81    unsafe {
82        let lo = _mm512_castpd512_pd256(v);
83        let hi = _mm512_extractf64x4_pd(v, 1);
84        let sum256 = _mm256_add_pd(lo, hi);
85        hsum_256(sum256)
86    }
87}
88
89/// Horizontal min of a 256-bit register (4×f64 → scalar).
90///
91/// # Safety
92///
93/// Caller must ensure AVX is available.
94#[cfg(target_arch = "x86_64")]
95#[inline(always)]
96unsafe fn hmin_256(v: std::arch::x86_64::__m256d) -> f64 {
97    use std::arch::x86_64::*;
98    // SAFETY: register-to-register intrinsics only — no loads, no stores.
99    // The caller guarantees the CPU feature, per this function's `# Safety`.
100    unsafe {
101        let hi = _mm256_extractf128_pd(v, 1);
102        let lo = _mm256_castpd256_pd128(v);
103        let m = _mm_min_pd(lo, hi);
104        let hi64 = _mm_unpackhi_pd(m, m);
105        _mm_cvtsd_f64(_mm_min_sd(m, hi64))
106    }
107}
108
109/// Horizontal max of a 256-bit register (4×f64 → scalar).
110///
111/// # Safety
112///
113/// Caller must ensure AVX is available.
114#[cfg(target_arch = "x86_64")]
115#[inline(always)]
116unsafe fn hmax_256(v: std::arch::x86_64::__m256d) -> f64 {
117    use std::arch::x86_64::*;
118    // SAFETY: register-to-register intrinsics only — no loads, no stores.
119    // The caller guarantees the CPU feature, per this function's `# Safety`.
120    unsafe {
121        let hi = _mm256_extractf128_pd(v, 1);
122        let lo = _mm256_castpd256_pd128(v);
123        let m = _mm_max_pd(lo, hi);
124        let hi64 = _mm_unpackhi_pd(m, m);
125        _mm_cvtsd_f64(_mm_max_sd(m, hi64))
126    }
127}
128
129/// Horizontal min of a 512-bit register (8×f64 → scalar).
130///
131/// # Safety
132///
133/// Caller must ensure AVX-512F is available.
134#[cfg(target_arch = "x86_64")]
135#[target_feature(enable = "avx512f")]
136#[inline]
137unsafe fn hmin_512(v: std::arch::x86_64::__m512d) -> f64 {
138    use std::arch::x86_64::*;
139    // SAFETY: register-to-register intrinsics only — no loads, no stores.
140    // The caller guarantees the CPU feature, per this function's `# Safety`.
141    unsafe {
142        let lo = _mm512_castpd512_pd256(v);
143        let hi = _mm512_extractf64x4_pd(v, 1);
144        let m = _mm256_min_pd(lo, hi);
145        hmin_256(m)
146    }
147}
148
149/// Horizontal max of a 512-bit register (8×f64 → scalar).
150///
151/// # Safety
152///
153/// Caller must ensure AVX-512F is available.
154#[cfg(target_arch = "x86_64")]
155#[target_feature(enable = "avx512f")]
156#[inline]
157unsafe fn hmax_512(v: std::arch::x86_64::__m512d) -> f64 {
158    use std::arch::x86_64::*;
159    // SAFETY: register-to-register intrinsics only — no loads, no stores.
160    // The caller guarantees the CPU feature, per this function's `# Safety`.
161    unsafe {
162        let lo = _mm512_castpd512_pd256(v);
163        let hi = _mm512_extractf64x4_pd(v, 1);
164        let m = _mm256_max_pd(lo, hi);
165        hmax_256(m)
166    }
167}
168
169// ═══════════════════════════════════════════════════════════════════════
170// Portable scalar fallback (4-wide ILP unroll)
171// ═══════════════════════════════════════════════════════════════════════
172
173/// Portable 4-wide scalar sum with Kahan (compensated) summation.
174#[inline]
175#[allow(dead_code)]
176fn scalar_sum(data: &[f64]) -> f64 {
177    let chunks = data.chunks_exact(4);
178    let remainder = chunks.remainder();
179    let (mut a0, mut a1, mut a2, mut a3) = (0.0_f64, 0.0, 0.0, 0.0);
180    let (mut c0, mut c1, mut c2, mut c3) = (0.0_f64, 0.0, 0.0, 0.0);
181    for c in chunks {
182        let y0 = c[0] - c0;
183        let t0 = a0 + y0;
184        c0 = (t0 - a0) - y0;
185        a0 = t0;
186        let y1 = c[1] - c1;
187        let t1 = a1 + y1;
188        c1 = (t1 - a1) - y1;
189        a1 = t1;
190        let y2 = c[2] - c2;
191        let t2 = a2 + y2;
192        c2 = (t2 - a2) - y2;
193        a2 = t2;
194        let y3 = c[3] - c3;
195        let t3 = a3 + y3;
196        c3 = (t3 - a3) - y3;
197        a3 = t3;
198    }
199    // Merge 4 accumulators with Kahan reduction.
200    let mut sum = 0.0_f64;
201    let mut c = 0.0_f64;
202    for &v in &[a0, a1, a2, a3] {
203        let y = v - c;
204        let t = sum + y;
205        c = (t - sum) - y;
206        sum = t;
207    }
208    for &v in remainder {
209        let y = v - c;
210        let t = sum + y;
211        c = (t - sum) - y;
212        sum = t;
213    }
214    sum
215}
216
217/// Kahan-compensated portable 4-wide scalar variance.
218#[inline]
219#[allow(dead_code)]
220fn scalar_variance(data: &[f64], mean: f64) -> f64 {
221    let chunks = data.chunks_exact(4);
222    let remainder = chunks.remainder();
223    let (mut a0, mut a1, mut a2, mut a3) = (0.0_f64, 0.0, 0.0, 0.0);
224    // Kahan compensation terms for each accumulator lane.
225    let (mut c0, mut c1, mut c2, mut c3) = (0.0_f64, 0.0, 0.0, 0.0);
226    for c in chunks {
227        let (d0, d1, d2, d3) = (c[0] - mean, c[1] - mean, c[2] - mean, c[3] - mean);
228        let y0 = d0 * d0 - c0;
229        let t0 = a0 + y0;
230        c0 = (t0 - a0) - y0;
231        a0 = t0;
232        let y1 = d1 * d1 - c1;
233        let t1 = a1 + y1;
234        c1 = (t1 - a1) - y1;
235        a1 = t1;
236        let y2 = d2 * d2 - c2;
237        let t2 = a2 + y2;
238        c2 = (t2 - a2) - y2;
239        a2 = t2;
240        let y3 = d3 * d3 - c3;
241        let t3 = a3 + y3;
242        c3 = (t3 - a3) - y3;
243        a3 = t3;
244    }
245    let mut s = (a0 + a1) + (a2 + a3);
246    let mut cs = (c0 + c1) + (c2 + c3);
247    for &v in remainder {
248        let d = v - mean;
249        let y = d * d - cs;
250        let t = s + y;
251        cs = (t - s) - y;
252        s = t;
253    }
254    s / data.len() as f64
255}
256
257/// Portable scalar min/max.
258#[inline]
259#[allow(dead_code)]
260fn scalar_min_max(data: &[f64]) -> (f64, f64) {
261    let mut min_val = f64::INFINITY;
262    let mut max_val = f64::NEG_INFINITY;
263    for &v in data {
264        if v < min_val {
265            min_val = v;
266        }
267        if v > max_val {
268            max_val = v;
269        }
270    }
271    (min_val, max_val)
272}
273
274/// Portable scalar dot product.
275#[inline]
276#[allow(dead_code)]
277fn scalar_dot_product(a: &[f64], b: &[f64]) -> f64 {
278    let chunks = a.len() / 4;
279    let (mut a0, mut a1, mut a2, mut a3) = (0.0_f64, 0.0, 0.0, 0.0);
280    for i in 0..chunks {
281        let base = i * 4;
282        a0 += a[base] * b[base];
283        a1 += a[base + 1] * b[base + 1];
284        a2 += a[base + 2] * b[base + 2];
285        a3 += a[base + 3] * b[base + 3];
286    }
287    let mut t = (a0 + a1) + (a2 + a3);
288    for i in (chunks * 4)..a.len() {
289        t += a[i] * b[i];
290    }
291    t
292}
293
294// ═══════════════════════════════════════════════════════════════════════
295// x86_64 SIMD tiers: AVX-512F → AVX2 → SSE2
296// ═══════════════════════════════════════════════════════════════════════
297
298// ── AVX-512F (8×f64, 32 elements/iteration) ──────────────────────────
299//
300// # Safety (applies to all avx512_* functions)
301//
302// These functions are marked `unsafe` because they use AVX-512F SIMD intrinsics.
303// Safety is guaranteed by:
304// 1. `#[target_feature(enable = "avx512f")]` — the compiler enforces that these
305//    functions are only called (directly or transitively) from contexts where
306//    AVX-512F is enabled, or via an unsafe block with a runtime feature check.
307// 2. All `_mm512_loadu_pd(ptr.add(offset))` calls are bounded: loop indices are
308//    derived from `data.len() / STRIDE` so `offset < data.len()` always holds.
309// 3. Remainder elements are processed via safe scalar iteration.
310
311#[cfg(target_arch = "x86_64")]
312#[target_feature(enable = "avx512f")]
313unsafe fn avx512_sum(data: &[f64]) -> f64 {
314    use std::arch::x86_64::*;
315    // SAFETY: the tier contract above — `#[target_feature]` guarantees the
316    // intrinsics, and every load offset is inside a chunk derived from the
317    // slice length, with the remainder handled by safe scalar code.
318    unsafe {
319        let mut acc0 = _mm512_setzero_pd();
320        let mut acc1 = _mm512_setzero_pd();
321        let mut acc2 = _mm512_setzero_pd();
322        let mut acc3 = _mm512_setzero_pd();
323        // Kahan compensation vectors.
324        let mut comp0 = _mm512_setzero_pd();
325        let mut comp1 = _mm512_setzero_pd();
326        let mut comp2 = _mm512_setzero_pd();
327        let mut comp3 = _mm512_setzero_pd();
328        let chunks = data.len() / 32;
329        let ptr = data.as_ptr();
330        for i in 0..chunks {
331            let b = i * 32;
332            let v0 = _mm512_loadu_pd(ptr.add(b));
333            let y0 = _mm512_sub_pd(v0, comp0);
334            let t0 = _mm512_add_pd(acc0, y0);
335            comp0 = _mm512_sub_pd(_mm512_sub_pd(t0, acc0), y0);
336            acc0 = t0;
337
338            let v1 = _mm512_loadu_pd(ptr.add(b + 8));
339            let y1 = _mm512_sub_pd(v1, comp1);
340            let t1 = _mm512_add_pd(acc1, y1);
341            comp1 = _mm512_sub_pd(_mm512_sub_pd(t1, acc1), y1);
342            acc1 = t1;
343
344            let v2 = _mm512_loadu_pd(ptr.add(b + 16));
345            let y2 = _mm512_sub_pd(v2, comp2);
346            let t2 = _mm512_add_pd(acc2, y2);
347            comp2 = _mm512_sub_pd(_mm512_sub_pd(t2, acc2), y2);
348            acc2 = t2;
349
350            let v3 = _mm512_loadu_pd(ptr.add(b + 24));
351            let y3 = _mm512_sub_pd(v3, comp3);
352            let t3 = _mm512_add_pd(acc3, y3);
353            comp3 = _mm512_sub_pd(_mm512_sub_pd(t3, acc3), y3);
354            acc3 = t3;
355        }
356        let sum = _mm512_add_pd(_mm512_add_pd(acc0, acc1), _mm512_add_pd(acc2, acc3));
357        let mut total = hsum_512(sum);
358        // Kahan tail for remainder elements.
359        let mut c = 0.0_f64;
360        for &v in &data[chunks * 32..] {
361            let y = v - c;
362            let t = total + y;
363            c = (t - total) - y;
364            total = t;
365        }
366        total
367    }
368}
369
370#[cfg(target_arch = "x86_64")]
371#[target_feature(enable = "avx512f")]
372unsafe fn avx512_variance(data: &[f64], mean: f64) -> f64 {
373    use std::arch::x86_64::*;
374    // SAFETY: the tier contract above — `#[target_feature]` guarantees the
375    // intrinsics, and every load offset is inside a chunk derived from the
376    // slice length, with the remainder handled by safe scalar code.
377    unsafe {
378        let vmean = _mm512_set1_pd(mean);
379        let mut acc0 = _mm512_setzero_pd();
380        let mut acc1 = _mm512_setzero_pd();
381        let mut acc2 = _mm512_setzero_pd();
382        let mut acc3 = _mm512_setzero_pd();
383        let chunks = data.len() / 32;
384        let ptr = data.as_ptr();
385        for i in 0..chunks {
386            let b = i * 32;
387            let d0 = _mm512_sub_pd(_mm512_loadu_pd(ptr.add(b)), vmean);
388            let d1 = _mm512_sub_pd(_mm512_loadu_pd(ptr.add(b + 8)), vmean);
389            let d2 = _mm512_sub_pd(_mm512_loadu_pd(ptr.add(b + 16)), vmean);
390            let d3 = _mm512_sub_pd(_mm512_loadu_pd(ptr.add(b + 24)), vmean);
391            acc0 = _mm512_fmadd_pd(d0, d0, acc0);
392            acc1 = _mm512_fmadd_pd(d1, d1, acc1);
393            acc2 = _mm512_fmadd_pd(d2, d2, acc2);
394            acc3 = _mm512_fmadd_pd(d3, d3, acc3);
395        }
396        let sum = _mm512_add_pd(_mm512_add_pd(acc0, acc1), _mm512_add_pd(acc2, acc3));
397        let mut total = hsum_512(sum);
398        for &v in &data[chunks * 32..] {
399            let d = v - mean;
400            total += d * d;
401        }
402        total / data.len() as f64
403    }
404}
405
406#[cfg(target_arch = "x86_64")]
407#[target_feature(enable = "avx512f")]
408unsafe fn avx512_min_max(data: &[f64]) -> (f64, f64) {
409    use std::arch::x86_64::*;
410    // SAFETY: the tier contract above — `#[target_feature]` guarantees the
411    // intrinsics, and every load offset is inside a chunk derived from the
412    // slice length, with the remainder handled by safe scalar code.
413    unsafe {
414        let mut vmin0 = _mm512_set1_pd(f64::INFINITY);
415        let mut vmax0 = _mm512_set1_pd(f64::NEG_INFINITY);
416        let mut vmin1 = vmin0;
417        let mut vmax1 = vmax0;
418        let chunks = data.len() / 16;
419        let ptr = data.as_ptr();
420        for i in 0..chunks {
421            let b = i * 16;
422            let a = _mm512_loadu_pd(ptr.add(b));
423            let c = _mm512_loadu_pd(ptr.add(b + 8));
424            vmin0 = _mm512_min_pd(a, vmin0);
425            vmax0 = _mm512_max_pd(a, vmax0);
426            vmin1 = _mm512_min_pd(c, vmin1);
427            vmax1 = _mm512_max_pd(c, vmax1);
428        }
429        let vmin = _mm512_min_pd(vmin0, vmin1);
430        let vmax = _mm512_max_pd(vmax0, vmax1);
431        let mut min_val = hmin_512(vmin);
432        let mut max_val = hmax_512(vmax);
433        for &v in &data[chunks * 16..] {
434            if v < min_val {
435                min_val = v;
436            }
437            if v > max_val {
438                max_val = v;
439            }
440        }
441        (min_val, max_val)
442    }
443}
444
445#[cfg(target_arch = "x86_64")]
446#[target_feature(enable = "avx512f")]
447unsafe fn avx512_dot_product(a: &[f64], b: &[f64]) -> f64 {
448    use std::arch::x86_64::*;
449    // SAFETY: the tier contract above — `#[target_feature]` guarantees the
450    // intrinsics, and every load offset is inside a chunk derived from the
451    // slice length, with the remainder handled by safe scalar code.
452    unsafe {
453        let mut acc0 = _mm512_setzero_pd();
454        let mut acc1 = _mm512_setzero_pd();
455        let mut acc2 = _mm512_setzero_pd();
456        let mut acc3 = _mm512_setzero_pd();
457        let chunks = a.len() / 32;
458        let pa = a.as_ptr();
459        let pb = b.as_ptr();
460        for i in 0..chunks {
461            let base = i * 32;
462            acc0 = _mm512_fmadd_pd(
463                _mm512_loadu_pd(pa.add(base)),
464                _mm512_loadu_pd(pb.add(base)),
465                acc0,
466            );
467            acc1 = _mm512_fmadd_pd(
468                _mm512_loadu_pd(pa.add(base + 8)),
469                _mm512_loadu_pd(pb.add(base + 8)),
470                acc1,
471            );
472            acc2 = _mm512_fmadd_pd(
473                _mm512_loadu_pd(pa.add(base + 16)),
474                _mm512_loadu_pd(pb.add(base + 16)),
475                acc2,
476            );
477            acc3 = _mm512_fmadd_pd(
478                _mm512_loadu_pd(pa.add(base + 24)),
479                _mm512_loadu_pd(pb.add(base + 24)),
480                acc3,
481            );
482        }
483        let sum = _mm512_add_pd(_mm512_add_pd(acc0, acc1), _mm512_add_pd(acc2, acc3));
484        let mut total = hsum_512(sum);
485        for i in (chunks * 32)..a.len() {
486            total += a[i] * b[i];
487        }
488        total
489    }
490}
491
492// ── AVX2+FMA (4×f64, 16 elements/iteration) ─────────────────────────
493//
494// # Safety (applies to all avx2_* functions)
495//
496// Same safety guarantees as the AVX-512F tier above:
497// 1. `#[target_feature(enable = "avx2,fma")]` ensures the CPU supports the
498//    intrinsics used.
499// 2. All `_mm256_loadu_pd(ptr.add(offset))` calls are bounds-checked via
500//    loop limits derived from `data.len() / STRIDE`.
501// 3. Remainder elements processed via safe scalar iteration.
502
503#[cfg(target_arch = "x86_64")]
504#[target_feature(enable = "avx2,fma")]
505unsafe fn avx2_sum(data: &[f64]) -> f64 {
506    use std::arch::x86_64::*;
507    // SAFETY: the tier contract above — `#[target_feature]` guarantees the
508    // intrinsics, and every load offset is inside a chunk derived from the
509    // slice length, with the remainder handled by safe scalar code.
510    unsafe {
511        let mut acc0 = _mm256_setzero_pd();
512        let mut acc1 = _mm256_setzero_pd();
513        let mut acc2 = _mm256_setzero_pd();
514        let mut acc3 = _mm256_setzero_pd();
515        // Kahan compensation vectors.
516        let mut comp0 = _mm256_setzero_pd();
517        let mut comp1 = _mm256_setzero_pd();
518        let mut comp2 = _mm256_setzero_pd();
519        let mut comp3 = _mm256_setzero_pd();
520        let chunks = data.len() / 16;
521        let ptr = data.as_ptr();
522        for i in 0..chunks {
523            let b = i * 16;
524            let v0 = _mm256_loadu_pd(ptr.add(b));
525            let y0 = _mm256_sub_pd(v0, comp0);
526            let t0 = _mm256_add_pd(acc0, y0);
527            comp0 = _mm256_sub_pd(_mm256_sub_pd(t0, acc0), y0);
528            acc0 = t0;
529
530            let v1 = _mm256_loadu_pd(ptr.add(b + 4));
531            let y1 = _mm256_sub_pd(v1, comp1);
532            let t1 = _mm256_add_pd(acc1, y1);
533            comp1 = _mm256_sub_pd(_mm256_sub_pd(t1, acc1), y1);
534            acc1 = t1;
535
536            let v2 = _mm256_loadu_pd(ptr.add(b + 8));
537            let y2 = _mm256_sub_pd(v2, comp2);
538            let t2 = _mm256_add_pd(acc2, y2);
539            comp2 = _mm256_sub_pd(_mm256_sub_pd(t2, acc2), y2);
540            acc2 = t2;
541
542            let v3 = _mm256_loadu_pd(ptr.add(b + 12));
543            let y3 = _mm256_sub_pd(v3, comp3);
544            let t3 = _mm256_add_pd(acc3, y3);
545            comp3 = _mm256_sub_pd(_mm256_sub_pd(t3, acc3), y3);
546            acc3 = t3;
547        }
548        let sum = _mm256_add_pd(_mm256_add_pd(acc0, acc1), _mm256_add_pd(acc2, acc3));
549        let mut total = hsum_256(sum);
550        // Kahan tail for remainder elements.
551        let mut c = 0.0_f64;
552        for &v in &data[chunks * 16..] {
553            let y = v - c;
554            let t = total + y;
555            c = (t - total) - y;
556            total = t;
557        }
558        total
559    }
560}
561
562#[cfg(target_arch = "x86_64")]
563#[target_feature(enable = "avx2,fma")]
564unsafe fn avx2_variance(data: &[f64], mean: f64) -> f64 {
565    use std::arch::x86_64::*;
566    // SAFETY: the tier contract above — `#[target_feature]` guarantees the
567    // intrinsics, and every load offset is inside a chunk derived from the
568    // slice length, with the remainder handled by safe scalar code.
569    unsafe {
570        let vmean = _mm256_set1_pd(mean);
571        let mut acc0 = _mm256_setzero_pd();
572        let mut acc1 = _mm256_setzero_pd();
573        let mut acc2 = _mm256_setzero_pd();
574        let mut acc3 = _mm256_setzero_pd();
575        let chunks = data.len() / 16;
576        let ptr = data.as_ptr();
577        for i in 0..chunks {
578            let b = i * 16;
579            let d0 = _mm256_sub_pd(_mm256_loadu_pd(ptr.add(b)), vmean);
580            let d1 = _mm256_sub_pd(_mm256_loadu_pd(ptr.add(b + 4)), vmean);
581            let d2 = _mm256_sub_pd(_mm256_loadu_pd(ptr.add(b + 8)), vmean);
582            let d3 = _mm256_sub_pd(_mm256_loadu_pd(ptr.add(b + 12)), vmean);
583            acc0 = _mm256_fmadd_pd(d0, d0, acc0);
584            acc1 = _mm256_fmadd_pd(d1, d1, acc1);
585            acc2 = _mm256_fmadd_pd(d2, d2, acc2);
586            acc3 = _mm256_fmadd_pd(d3, d3, acc3);
587        }
588        let sum = _mm256_add_pd(_mm256_add_pd(acc0, acc1), _mm256_add_pd(acc2, acc3));
589        let mut total = hsum_256(sum);
590        for &v in &data[chunks * 16..] {
591            let d = v - mean;
592            total += d * d;
593        }
594        total / data.len() as f64
595    }
596}
597
598#[cfg(target_arch = "x86_64")]
599#[target_feature(enable = "avx2")]
600unsafe fn avx2_min_max(data: &[f64]) -> (f64, f64) {
601    use std::arch::x86_64::*;
602    // SAFETY: the tier contract above — `#[target_feature]` guarantees the
603    // intrinsics, and every load offset is inside a chunk derived from the
604    // slice length, with the remainder handled by safe scalar code.
605    unsafe {
606        let mut vmin0 = _mm256_set1_pd(f64::INFINITY);
607        let mut vmax0 = _mm256_set1_pd(f64::NEG_INFINITY);
608        let mut vmin1 = vmin0;
609        let mut vmax1 = vmax0;
610        let chunks = data.len() / 8;
611        let ptr = data.as_ptr();
612        for i in 0..chunks {
613            let b = i * 8;
614            let a = _mm256_loadu_pd(ptr.add(b));
615            let c = _mm256_loadu_pd(ptr.add(b + 4));
616            vmin0 = _mm256_min_pd(a, vmin0);
617            vmax0 = _mm256_max_pd(a, vmax0);
618            vmin1 = _mm256_min_pd(c, vmin1);
619            vmax1 = _mm256_max_pd(c, vmax1);
620        }
621        let vmin = _mm256_min_pd(vmin0, vmin1);
622        let vmax = _mm256_max_pd(vmax0, vmax1);
623        let mut min_val = hmin_256(vmin);
624        let mut max_val = hmax_256(vmax);
625        for &v in &data[chunks * 8..] {
626            if v < min_val {
627                min_val = v;
628            }
629            if v > max_val {
630                max_val = v;
631            }
632        }
633        (min_val, max_val)
634    }
635}
636
637#[cfg(target_arch = "x86_64")]
638#[target_feature(enable = "avx2,fma")]
639unsafe fn avx2_dot_product(a: &[f64], b: &[f64]) -> f64 {
640    use std::arch::x86_64::*;
641    // SAFETY: the tier contract above — `#[target_feature]` guarantees the
642    // intrinsics, and every load offset is inside a chunk derived from the
643    // slice length, with the remainder handled by safe scalar code.
644    unsafe {
645        let mut acc0 = _mm256_setzero_pd();
646        let mut acc1 = _mm256_setzero_pd();
647        let mut acc2 = _mm256_setzero_pd();
648        let mut acc3 = _mm256_setzero_pd();
649        let chunks = a.len() / 16;
650        let pa = a.as_ptr();
651        let pb = b.as_ptr();
652        for i in 0..chunks {
653            let base = i * 16;
654            acc0 = _mm256_fmadd_pd(
655                _mm256_loadu_pd(pa.add(base)),
656                _mm256_loadu_pd(pb.add(base)),
657                acc0,
658            );
659            acc1 = _mm256_fmadd_pd(
660                _mm256_loadu_pd(pa.add(base + 4)),
661                _mm256_loadu_pd(pb.add(base + 4)),
662                acc1,
663            );
664            acc2 = _mm256_fmadd_pd(
665                _mm256_loadu_pd(pa.add(base + 8)),
666                _mm256_loadu_pd(pb.add(base + 8)),
667                acc2,
668            );
669            acc3 = _mm256_fmadd_pd(
670                _mm256_loadu_pd(pa.add(base + 12)),
671                _mm256_loadu_pd(pb.add(base + 12)),
672                acc3,
673            );
674        }
675        let sum = _mm256_add_pd(_mm256_add_pd(acc0, acc1), _mm256_add_pd(acc2, acc3));
676        let mut total = hsum_256(sum);
677        for i in (chunks * 16)..a.len() {
678            total += a[i] * b[i];
679        }
680        total
681    }
682}
683
684// ── SSE2 (2×f64, 8 elements/iteration — x86_64 baseline) ────────────
685
686#[cfg(target_arch = "x86_64")]
687#[inline]
688fn sse2_sum(data: &[f64]) -> f64 {
689    use std::arch::x86_64::*;
690    // SAFETY: SSE2 is guaranteed on all x86_64 CPUs.
691    unsafe {
692        let mut acc0 = _mm_setzero_pd();
693        let mut acc1 = _mm_setzero_pd();
694        let mut acc2 = _mm_setzero_pd();
695        let mut acc3 = _mm_setzero_pd();
696        // Kahan compensation vectors.
697        let mut comp0 = _mm_setzero_pd();
698        let mut comp1 = _mm_setzero_pd();
699        let mut comp2 = _mm_setzero_pd();
700        let mut comp3 = _mm_setzero_pd();
701        let chunks = data.len() / 8;
702        let ptr = data.as_ptr();
703        for i in 0..chunks {
704            let b = i * 8;
705            let v0 = _mm_loadu_pd(ptr.add(b));
706            let y0 = _mm_sub_pd(v0, comp0);
707            let t0 = _mm_add_pd(acc0, y0);
708            comp0 = _mm_sub_pd(_mm_sub_pd(t0, acc0), y0);
709            acc0 = t0;
710
711            let v1 = _mm_loadu_pd(ptr.add(b + 2));
712            let y1 = _mm_sub_pd(v1, comp1);
713            let t1 = _mm_add_pd(acc1, y1);
714            comp1 = _mm_sub_pd(_mm_sub_pd(t1, acc1), y1);
715            acc1 = t1;
716
717            let v2 = _mm_loadu_pd(ptr.add(b + 4));
718            let y2 = _mm_sub_pd(v2, comp2);
719            let t2 = _mm_add_pd(acc2, y2);
720            comp2 = _mm_sub_pd(_mm_sub_pd(t2, acc2), y2);
721            acc2 = t2;
722
723            let v3 = _mm_loadu_pd(ptr.add(b + 6));
724            let y3 = _mm_sub_pd(v3, comp3);
725            let t3 = _mm_add_pd(acc3, y3);
726            comp3 = _mm_sub_pd(_mm_sub_pd(t3, acc3), y3);
727            acc3 = t3;
728        }
729        let sum = _mm_add_pd(_mm_add_pd(acc0, acc1), _mm_add_pd(acc2, acc3));
730        let mut total = _mm_cvtsd_f64(sum) + _mm_cvtsd_f64(_mm_unpackhi_pd(sum, sum));
731        // Kahan tail for remainder elements.
732        let mut c = 0.0_f64;
733        for &v in &data[chunks * 8..] {
734            let y = v - c;
735            let t = total + y;
736            c = (t - total) - y;
737            total = t;
738        }
739        total
740    }
741}
742
743#[cfg(target_arch = "x86_64")]
744#[inline]
745fn sse2_variance(data: &[f64], mean: f64) -> f64 {
746    use std::arch::x86_64::*;
747    // SAFETY: SSE2 is guaranteed on all x86_64 CPUs.
748    unsafe {
749        let vmean = _mm_set1_pd(mean);
750        let mut acc0 = _mm_setzero_pd();
751        let mut acc1 = _mm_setzero_pd();
752        let mut acc2 = _mm_setzero_pd();
753        let mut acc3 = _mm_setzero_pd();
754        let chunks = data.len() / 8;
755        let ptr = data.as_ptr();
756        for i in 0..chunks {
757            let b = i * 8;
758            let d0 = _mm_sub_pd(_mm_loadu_pd(ptr.add(b)), vmean);
759            let d1 = _mm_sub_pd(_mm_loadu_pd(ptr.add(b + 2)), vmean);
760            let d2 = _mm_sub_pd(_mm_loadu_pd(ptr.add(b + 4)), vmean);
761            let d3 = _mm_sub_pd(_mm_loadu_pd(ptr.add(b + 6)), vmean);
762            acc0 = _mm_add_pd(acc0, _mm_mul_pd(d0, d0));
763            acc1 = _mm_add_pd(acc1, _mm_mul_pd(d1, d1));
764            acc2 = _mm_add_pd(acc2, _mm_mul_pd(d2, d2));
765            acc3 = _mm_add_pd(acc3, _mm_mul_pd(d3, d3));
766        }
767        let sum = _mm_add_pd(_mm_add_pd(acc0, acc1), _mm_add_pd(acc2, acc3));
768        let mut total = _mm_cvtsd_f64(sum) + _mm_cvtsd_f64(_mm_unpackhi_pd(sum, sum));
769        for &v in &data[chunks * 8..] {
770            let d = v - mean;
771            total += d * d;
772        }
773        total / data.len() as f64
774    }
775}
776
777#[cfg(target_arch = "x86_64")]
778#[inline]
779fn sse2_min_max(data: &[f64]) -> (f64, f64) {
780    use std::arch::x86_64::*;
781    // SAFETY: SSE2 is guaranteed on all x86_64 CPUs.
782    unsafe {
783        let mut vmin0 = _mm_set1_pd(f64::INFINITY);
784        let mut vmax0 = _mm_set1_pd(f64::NEG_INFINITY);
785        let mut vmin1 = vmin0;
786        let mut vmax1 = vmax0;
787        let chunks = data.len() / 4;
788        let ptr = data.as_ptr();
789        for i in 0..chunks {
790            let b = i * 4;
791            let a = _mm_loadu_pd(ptr.add(b));
792            let c = _mm_loadu_pd(ptr.add(b + 2));
793            vmin0 = _mm_min_pd(a, vmin0);
794            vmax0 = _mm_max_pd(a, vmax0);
795            vmin1 = _mm_min_pd(c, vmin1);
796            vmax1 = _mm_max_pd(c, vmax1);
797        }
798        let vmin = _mm_min_pd(vmin0, vmin1);
799        let vmax = _mm_max_pd(vmax0, vmax1);
800        let mut min_val = f64::min(
801            _mm_cvtsd_f64(vmin),
802            _mm_cvtsd_f64(_mm_unpackhi_pd(vmin, vmin)),
803        );
804        let mut max_val = f64::max(
805            _mm_cvtsd_f64(vmax),
806            _mm_cvtsd_f64(_mm_unpackhi_pd(vmax, vmax)),
807        );
808        for &v in &data[chunks * 4..] {
809            if v < min_val {
810                min_val = v;
811            }
812            if v > max_val {
813                max_val = v;
814            }
815        }
816        (min_val, max_val)
817    }
818}
819
820#[cfg(target_arch = "x86_64")]
821#[inline]
822fn sse2_dot_product(a: &[f64], b: &[f64]) -> f64 {
823    use std::arch::x86_64::*;
824    // SAFETY: SSE2 is guaranteed on all x86_64 CPUs.
825    unsafe {
826        let mut acc0 = _mm_setzero_pd();
827        let mut acc1 = _mm_setzero_pd();
828        let mut acc2 = _mm_setzero_pd();
829        let mut acc3 = _mm_setzero_pd();
830        let chunks = a.len() / 8;
831        let pa = a.as_ptr();
832        let pb = b.as_ptr();
833        for i in 0..chunks {
834            let base = i * 8;
835            let va0 = _mm_loadu_pd(pa.add(base));
836            let vb0 = _mm_loadu_pd(pb.add(base));
837            let va1 = _mm_loadu_pd(pa.add(base + 2));
838            let vb1 = _mm_loadu_pd(pb.add(base + 2));
839            let va2 = _mm_loadu_pd(pa.add(base + 4));
840            let vb2 = _mm_loadu_pd(pb.add(base + 4));
841            let va3 = _mm_loadu_pd(pa.add(base + 6));
842            let vb3 = _mm_loadu_pd(pb.add(base + 6));
843            acc0 = _mm_add_pd(acc0, _mm_mul_pd(va0, vb0));
844            acc1 = _mm_add_pd(acc1, _mm_mul_pd(va1, vb1));
845            acc2 = _mm_add_pd(acc2, _mm_mul_pd(va2, vb2));
846            acc3 = _mm_add_pd(acc3, _mm_mul_pd(va3, vb3));
847        }
848        let sum = _mm_add_pd(_mm_add_pd(acc0, acc1), _mm_add_pd(acc2, acc3));
849        let mut total = _mm_cvtsd_f64(sum) + _mm_cvtsd_f64(_mm_unpackhi_pd(sum, sum));
850        for i in (chunks * 8)..a.len() {
851            total += a[i] * b[i];
852        }
853        total
854    }
855}
856
857// ── x86_64 runtime dispatch ──────────────────────────────────────────
858
859#[cfg(target_arch = "x86_64")]
860#[inline]
861fn arch_simd_sum(data: &[f64]) -> f64 {
862    if is_x86_feature_detected!("avx512f") {
863        // SAFETY: AVX-512F detected at runtime.
864        return unsafe { avx512_sum(data) };
865    }
866    if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") {
867        // SAFETY: AVX2+FMA detected at runtime.
868        return unsafe { avx2_sum(data) };
869    }
870    sse2_sum(data)
871}
872
873#[cfg(target_arch = "x86_64")]
874#[inline]
875fn arch_simd_variance(data: &[f64], mean: f64) -> f64 {
876    if is_x86_feature_detected!("avx512f") {
877        // SAFETY: AVX-512F detected at runtime.
878        return unsafe { avx512_variance(data, mean) };
879    }
880    if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") {
881        // SAFETY: AVX2+FMA detected at runtime.
882        return unsafe { avx2_variance(data, mean) };
883    }
884    sse2_variance(data, mean)
885}
886
887#[cfg(target_arch = "x86_64")]
888#[inline]
889fn arch_simd_min_max(data: &[f64]) -> (f64, f64) {
890    if is_x86_feature_detected!("avx512f") {
891        // SAFETY: AVX-512F detected at runtime.
892        return unsafe { avx512_min_max(data) };
893    }
894    if is_x86_feature_detected!("avx2") {
895        // SAFETY: AVX2 detected at runtime.
896        return unsafe { avx2_min_max(data) };
897    }
898    sse2_min_max(data)
899}
900
901#[cfg(target_arch = "x86_64")]
902#[inline]
903fn arch_simd_dot_product(a: &[f64], b: &[f64]) -> f64 {
904    if is_x86_feature_detected!("avx512f") {
905        // SAFETY: AVX-512F detected at runtime.
906        return unsafe { avx512_dot_product(a, b) };
907    }
908    if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") {
909        // SAFETY: AVX2+FMA detected at runtime.
910        return unsafe { avx2_dot_product(a, b) };
911    }
912    sse2_dot_product(a, b)
913}
914
915// ═══════════════════════════════════════════════════════════════════════
916// aarch64 NEON (2×f64, 8 elements/iteration — baseline)
917// ═══════════════════════════════════════════════════════════════════════
918
919#[cfg(target_arch = "aarch64")]
920#[inline]
921fn arch_simd_sum(data: &[f64]) -> f64 {
922    use std::arch::aarch64::*;
923    // SAFETY: NEON is guaranteed on all aarch64 CPUs.
924    unsafe {
925        let mut acc0 = vdupq_n_f64(0.0);
926        let mut acc1 = vdupq_n_f64(0.0);
927        let mut acc2 = vdupq_n_f64(0.0);
928        let mut acc3 = vdupq_n_f64(0.0);
929        // Kahan compensation vectors.
930        let mut comp0 = vdupq_n_f64(0.0);
931        let mut comp1 = vdupq_n_f64(0.0);
932        let mut comp2 = vdupq_n_f64(0.0);
933        let mut comp3 = vdupq_n_f64(0.0);
934        let chunks = data.len() / 8;
935        let ptr = data.as_ptr();
936        for i in 0..chunks {
937            let b = i * 8;
938            let v0 = vld1q_f64(ptr.add(b));
939            let y0 = vsubq_f64(v0, comp0);
940            let t0 = vaddq_f64(acc0, y0);
941            comp0 = vsubq_f64(vsubq_f64(t0, acc0), y0);
942            acc0 = t0;
943
944            let v1 = vld1q_f64(ptr.add(b + 2));
945            let y1 = vsubq_f64(v1, comp1);
946            let t1 = vaddq_f64(acc1, y1);
947            comp1 = vsubq_f64(vsubq_f64(t1, acc1), y1);
948            acc1 = t1;
949
950            let v2 = vld1q_f64(ptr.add(b + 4));
951            let y2 = vsubq_f64(v2, comp2);
952            let t2 = vaddq_f64(acc2, y2);
953            comp2 = vsubq_f64(vsubq_f64(t2, acc2), y2);
954            acc2 = t2;
955
956            let v3 = vld1q_f64(ptr.add(b + 6));
957            let y3 = vsubq_f64(v3, comp3);
958            let t3 = vaddq_f64(acc3, y3);
959            comp3 = vsubq_f64(vsubq_f64(t3, acc3), y3);
960            acc3 = t3;
961        }
962        let sum = vaddq_f64(vaddq_f64(acc0, acc1), vaddq_f64(acc2, acc3));
963        let mut total = vgetq_lane_f64(sum, 0) + vgetq_lane_f64(sum, 1);
964        // Kahan tail for remainder elements.
965        let mut c = 0.0_f64;
966        for &v in &data[chunks * 8..] {
967            let y = v - c;
968            let t = total + y;
969            c = (t - total) - y;
970            total = t;
971        }
972        total
973    }
974}
975
976#[cfg(target_arch = "aarch64")]
977#[inline]
978fn arch_simd_variance(data: &[f64], mean: f64) -> f64 {
979    use std::arch::aarch64::*;
980    // SAFETY: NEON is guaranteed on all aarch64 CPUs.
981    unsafe {
982        let vmean = vdupq_n_f64(mean);
983        let mut acc0 = vdupq_n_f64(0.0);
984        let mut acc1 = vdupq_n_f64(0.0);
985        let mut acc2 = vdupq_n_f64(0.0);
986        let mut acc3 = vdupq_n_f64(0.0);
987        let chunks = data.len() / 8;
988        let ptr = data.as_ptr();
989        for i in 0..chunks {
990            let b = i * 8;
991            let d0 = vsubq_f64(vld1q_f64(ptr.add(b)), vmean);
992            let d1 = vsubq_f64(vld1q_f64(ptr.add(b + 2)), vmean);
993            let d2 = vsubq_f64(vld1q_f64(ptr.add(b + 4)), vmean);
994            let d3 = vsubq_f64(vld1q_f64(ptr.add(b + 6)), vmean);
995            acc0 = vaddq_f64(acc0, vmulq_f64(d0, d0));
996            acc1 = vaddq_f64(acc1, vmulq_f64(d1, d1));
997            acc2 = vaddq_f64(acc2, vmulq_f64(d2, d2));
998            acc3 = vaddq_f64(acc3, vmulq_f64(d3, d3));
999        }
1000        let sum = vaddq_f64(vaddq_f64(acc0, acc1), vaddq_f64(acc2, acc3));
1001        let mut total = vgetq_lane_f64(sum, 0) + vgetq_lane_f64(sum, 1);
1002        for &v in &data[chunks * 8..] {
1003            let d = v - mean;
1004            total += d * d;
1005        }
1006        total / data.len() as f64
1007    }
1008}
1009
1010#[cfg(target_arch = "aarch64")]
1011#[inline]
1012fn arch_simd_min_max(data: &[f64]) -> (f64, f64) {
1013    use std::arch::aarch64::*;
1014    // SAFETY: NEON is guaranteed on all aarch64 CPUs.
1015    unsafe {
1016        // Use vminnmq_f64 / vmaxnmq_f64 (FMINNM / FMAXNM) which skip NaN,
1017        // matching x86 _mm*_min_pd / _mm*_max_pd semantics.
1018        let mut vmin0 = vdupq_n_f64(f64::INFINITY);
1019        let mut vmax0 = vdupq_n_f64(f64::NEG_INFINITY);
1020        let mut vmin1 = vmin0;
1021        let mut vmax1 = vmax0;
1022        let chunks = data.len() / 4;
1023        let ptr = data.as_ptr();
1024        for i in 0..chunks {
1025            let b = i * 4;
1026            let a = vld1q_f64(ptr.add(b));
1027            let c = vld1q_f64(ptr.add(b + 2));
1028            vmin0 = vminnmq_f64(vmin0, a);
1029            vmax0 = vmaxnmq_f64(vmax0, a);
1030            vmin1 = vminnmq_f64(vmin1, c);
1031            vmax1 = vmaxnmq_f64(vmax1, c);
1032        }
1033        let vmin = vminnmq_f64(vmin0, vmin1);
1034        let vmax = vmaxnmq_f64(vmax0, vmax1);
1035        let mut min_val = f64::min(vgetq_lane_f64(vmin, 0), vgetq_lane_f64(vmin, 1));
1036        let mut max_val = f64::max(vgetq_lane_f64(vmax, 0), vgetq_lane_f64(vmax, 1));
1037        for &v in &data[chunks * 4..] {
1038            if v < min_val {
1039                min_val = v;
1040            }
1041            if v > max_val {
1042                max_val = v;
1043            }
1044        }
1045        (min_val, max_val)
1046    }
1047}
1048
1049#[cfg(target_arch = "aarch64")]
1050#[inline]
1051fn arch_simd_dot_product(a: &[f64], b: &[f64]) -> f64 {
1052    use std::arch::aarch64::*;
1053    // SAFETY: NEON is guaranteed on all aarch64 CPUs.
1054    unsafe {
1055        let mut acc0 = vdupq_n_f64(0.0);
1056        let mut acc1 = vdupq_n_f64(0.0);
1057        let mut acc2 = vdupq_n_f64(0.0);
1058        let mut acc3 = vdupq_n_f64(0.0);
1059        let chunks = a.len() / 8;
1060        let pa = a.as_ptr();
1061        let pb = b.as_ptr();
1062        for i in 0..chunks {
1063            let base = i * 8;
1064            acc0 = vfmaq_f64(acc0, vld1q_f64(pa.add(base)), vld1q_f64(pb.add(base)));
1065            acc1 = vfmaq_f64(
1066                acc1,
1067                vld1q_f64(pa.add(base + 2)),
1068                vld1q_f64(pb.add(base + 2)),
1069            );
1070            acc2 = vfmaq_f64(
1071                acc2,
1072                vld1q_f64(pa.add(base + 4)),
1073                vld1q_f64(pb.add(base + 4)),
1074            );
1075            acc3 = vfmaq_f64(
1076                acc3,
1077                vld1q_f64(pa.add(base + 6)),
1078                vld1q_f64(pb.add(base + 6)),
1079            );
1080        }
1081        let sum = vaddq_f64(vaddq_f64(acc0, acc1), vaddq_f64(acc2, acc3));
1082        let mut total = vgetq_lane_f64(sum, 0) + vgetq_lane_f64(sum, 1);
1083        for i in (chunks * 8)..a.len() {
1084            total += a[i] * b[i];
1085        }
1086        total
1087    }
1088}
1089
1090// ═══════════════════════════════════════════════════════════════════════
1091// Scalar fallback dispatch (non-x86_64, non-aarch64)
1092// ═══════════════════════════════════════════════════════════════════════
1093
1094#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
1095#[inline]
1096fn arch_simd_sum(data: &[f64]) -> f64 {
1097    scalar_sum(data)
1098}
1099
1100#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
1101#[inline]
1102fn arch_simd_variance(data: &[f64], mean: f64) -> f64 {
1103    scalar_variance(data, mean)
1104}
1105
1106#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
1107#[inline]
1108fn arch_simd_min_max(data: &[f64]) -> (f64, f64) {
1109    scalar_min_max(data)
1110}
1111
1112#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
1113#[inline]
1114fn arch_simd_dot_product(a: &[f64], b: &[f64]) -> f64 {
1115    scalar_dot_product(a, b)
1116}
1117
1118// ═══════════════════════════════════════════════════════════════════════
1119// Public API
1120// ═══════════════════════════════════════════════════════════════════════
1121
1122/// Computes the sum of a slice with multi-tier SIMD acceleration and
1123/// Kahan (compensated) summation for numerical stability.
1124///
1125/// On x86_64: runtime-detects AVX-512F → AVX2+FMA → SSE2 (baseline).
1126/// On aarch64: uses NEON intrinsics.
1127/// Elsewhere:  portable 4-wide scalar ILP unroll.
1128///
1129/// Each tier maintains per-lane compensation vectors that track
1130/// accumulated rounding error, dramatically reducing the O(n·ε²) error
1131/// of naive pairwise summation to O(ε) regardless of input length.
1132///
1133/// # Examples
1134///
1135/// ```no_run
1136/// use chronix_analytics::compute::simd_sum;
1137/// assert!((simd_sum(&[1.0, 2.0, 3.0, 4.0]) - 10.0).abs() < 1e-10);
1138/// assert_eq!(simd_sum(&[]), 0.0);
1139/// ```
1140pub fn simd_sum(data: &[f64]) -> f64 {
1141    if data.is_empty() {
1142        return 0.0;
1143    }
1144    arch_simd_sum(data)
1145}
1146
1147/// Computes the arithmetic mean of a slice.
1148///
1149/// Returns `f64::NAN` for empty slices.
1150///
1151/// # Examples
1152///
1153/// ```no_run
1154/// use chronix_analytics::compute::simd_mean;
1155/// assert!((simd_mean(&[1.0, 2.0, 3.0, 4.0]) - 2.5).abs() < 1e-10);
1156/// assert!(simd_mean(&[]).is_nan());
1157/// ```
1158pub fn simd_mean(data: &[f64]) -> f64 {
1159    if data.is_empty() {
1160        return f64::NAN;
1161    }
1162    simd_sum(data) / data.len() as f64
1163}
1164
1165/// Computes the population variance given a precomputed mean.
1166///
1167/// On x86_64 with AVX2/AVX-512: uses FMA (`_mm256_fmadd_pd` / `_mm512_fmadd_pd`)
1168/// for fused multiply-add in the squared-difference accumulation.
1169///
1170/// # Examples
1171///
1172/// ```no_run
1173/// use chronix_analytics::compute::{simd_mean, simd_variance};
1174/// let data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
1175/// let mean = simd_mean(&data);
1176/// let var = simd_variance(&data, mean);
1177/// assert!((var - 32.0 / 7.0).abs() < 1e-10); // sample variance (Bessel-corrected)
1178/// ```
1179pub fn simd_variance(data: &[f64], mean: f64) -> f64 {
1180    if data.len() < 2 {
1181        return f64::NAN;
1182    }
1183    // Internal SIMD kernels compute population variance (÷n).
1184    // Apply Bessel's correction to return sample variance (÷(n-1)).
1185    let n = data.len() as f64;
1186    arch_simd_variance(data, mean) * n / (n - 1.0)
1187}
1188
1189/// Computes the **population** variance of the slice using SIMD (divides by `n`).
1190///
1191/// Use [`simd_variance`] for the sample variance (Bessel-corrected, ÷(n-1)).
1192pub fn simd_population_variance(data: &[f64], mean: f64) -> f64 {
1193    if data.is_empty() {
1194        return f64::NAN;
1195    }
1196    arch_simd_variance(data, mean)
1197}
1198
1199/// Computes the minimum and maximum of a slice simultaneously using SIMD.
1200///
1201/// **NaN handling:** a `NaN` is skipped, on every tier.
1202///
1203/// That takes care on x86: `MINPD dst, src` returns **`src`** when either
1204/// operand is `NaN`, so accumulating with `min(acc, data)` lets one `NaN` in
1205/// the data poison the accumulator and the answer becomes whatever the last
1206/// lane held. The accumulator is therefore the *second* operand throughout —
1207/// `min(data, acc)` keeps the accumulator and drops the `NaN`, which is what
1208/// aarch64's `FMINNM` and the scalar `<`/`>` comparisons do anyway. This was a
1209/// real divergence: the two architectures gave different answers for the same
1210/// input, and the doc comment here said they did not.
1211///
1212/// On x86_64: uses `_mm_min_pd`/`_mm_max_pd` (SSE2), `_mm256_min_pd`/`_mm256_max_pd`
1213/// (AVX2), or `_mm512_min_pd`/`_mm512_max_pd` (AVX-512F).
1214/// On aarch64: uses `vminnmq_f64`/`vmaxnmq_f64` (NEON FMINNM/FMAXNM).
1215///
1216/// Returns `(f64::INFINITY, f64::NEG_INFINITY)` for empty slices.
1217///
1218/// # Examples
1219///
1220/// ```no_run
1221/// use chronix_analytics::compute::simd_min_max;
1222/// let (min, max) = simd_min_max(&[3.0, 1.0, 4.0, 1.0, 5.0, 9.0]);
1223/// assert!((min - 1.0).abs() < 1e-10);
1224/// assert!((max - 9.0).abs() < 1e-10);
1225/// ```
1226pub fn simd_min_max(data: &[f64]) -> (f64, f64) {
1227    if data.is_empty() {
1228        return (f64::INFINITY, f64::NEG_INFINITY);
1229    }
1230    arch_simd_min_max(data)
1231}
1232
1233/// Computes the sample standard deviation.
1234///
1235/// Uses `simd_mean` and `simd_variance` internally. `simd_variance`
1236/// already returns sample variance (Bessel-corrected).
1237pub fn simd_std_dev(data: &[f64]) -> f64 {
1238    if data.len() < 2 {
1239        return if data.is_empty() { f64::NAN } else { 0.0 };
1240    }
1241    let (_, var) = simd_mean_variance(data);
1242    var.sqrt()
1243}
1244
1245/// Two-pass mean + sample variance using SIMD kernels.
1246///
1247/// Replaces scalar Welford (data-dependent division per iteration,
1248/// un-vectorizable) with two SIMD passes:
1249///   1. `simd_sum` → mean (fully vectorized)
1250///   2. `arch_simd_variance` → population variance (fully vectorized)
1251///
1252/// The second pass operates on data already in L1/L2 cache from the first pass,
1253/// so the overhead is minimal compared to the 4-8× speedup from SIMD vs scalar.
1254///
1255/// Returns `(mean, sample_variance)` where sample variance uses Bessel's
1256/// correction (÷(n−1)).
1257pub fn simd_mean_variance(data: &[f64]) -> (f64, f64) {
1258    if data.len() < 2 {
1259        let mean = if data.is_empty() { f64::NAN } else { data[0] };
1260        return (mean, f64::NAN);
1261    }
1262    let mean = simd_sum(data) / data.len() as f64;
1263    let pop_var = arch_simd_variance(data, mean);
1264    let n = data.len() as f64;
1265    (mean, pop_var * n / (n - 1.0))
1266}
1267
1268/// SIMD-accelerated timestamp range filter.
1269///
1270/// Returns the indices of elements in `timestamps` that fall within `[min, max]`
1271/// (inclusive). This is the most common predicate in time-series queries and
1272/// produces a selection vector directly, avoiding the overhead of materializing
1273/// a full `BooleanArray`.
1274///
1275/// On aarch64: uses NEON `vcgeq_s64`/`vcleq_s64` (2 elements/cycle).
1276/// On x86_64 without specific SIMD: uses 4-wide scalar unroll with ILP.
1277///
1278/// # Examples
1279///
1280/// ```no_run
1281/// use chronix_analytics::compute::simd_range_filter_i64;
1282/// let ts = [100, 200, 300, 400, 500];
1283/// let indices = simd_range_filter_i64(&ts, 200, 400);
1284/// assert_eq!(indices, vec![1, 2, 3]);
1285/// ```
1286pub fn simd_range_filter_i64(timestamps: &[i64], min: i64, max: i64) -> Vec<u32> {
1287    let mut result = Vec::with_capacity(timestamps.len());
1288    // 4-wide scalar unroll for ILP on all platforms.
1289    let chunks = timestamps.chunks_exact(4);
1290    let remainder = chunks.remainder();
1291    let mut base_idx: u32 = 0;
1292    for chunk in chunks {
1293        if chunk[0] >= min && chunk[0] <= max {
1294            result.push(base_idx);
1295        }
1296        if chunk[1] >= min && chunk[1] <= max {
1297            result.push(base_idx + 1);
1298        }
1299        if chunk[2] >= min && chunk[2] <= max {
1300            result.push(base_idx + 2);
1301        }
1302        if chunk[3] >= min && chunk[3] <= max {
1303            result.push(base_idx + 3);
1304        }
1305        base_idx += 4;
1306    }
1307    for &ts in remainder {
1308        if ts >= min && ts <= max {
1309            result.push(base_idx);
1310        }
1311        base_idx += 1;
1312    }
1313    result
1314}
1315
1316/// Computes the dot product of two equal-length slices using SIMD.
1317///
1318/// On x86_64 with AVX2/AVX-512: uses FMA instructions for fused multiply-add.
1319/// On aarch64: uses `vfmaq_f64` (NEON FMA).
1320///
1321/// # Errors
1322///
1323/// Returns `ComputeError::DimensionMismatch` if `a.len() != b.len()`.
1324///
1325/// # Examples
1326///
1327/// ```no_run
1328/// use chronix_analytics::compute::simd_dot_product;
1329/// let a = [1.0, 2.0, 3.0, 4.0, 5.0];
1330/// let b = [5.0, 4.0, 3.0, 2.0, 1.0];
1331/// assert!((simd_dot_product(&a, &b).unwrap() - 35.0).abs() < 1e-10);
1332/// ```
1333pub fn simd_dot_product(a: &[f64], b: &[f64]) -> Result<f64, crate::compute::ComputeError> {
1334    if a.len() != b.len() {
1335        return Err(crate::compute::ComputeError::DimensionMismatch {
1336            expected: a.len(),
1337            got: b.len(),
1338        });
1339    }
1340    if a.is_empty() {
1341        return Ok(0.0);
1342    }
1343    Ok(arch_simd_dot_product(a, b))
1344}
1345
1346/// Returns a human-readable string describing the active SIMD tier.
1347///
1348/// Useful for diagnostics and logging at startup.
1349pub fn simd_tier() -> &'static str {
1350    #[cfg(target_arch = "x86_64")]
1351    {
1352        if is_x86_feature_detected!("avx512f") {
1353            return "AVX-512F (8×f64, 512-bit)";
1354        }
1355        if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("fma") {
1356            return "AVX2+FMA (4×f64, 256-bit)";
1357        }
1358        // The block's tail, not an early exit: the two above are.
1359        "SSE2 (2×f64, 128-bit)"
1360    }
1361    #[cfg(target_arch = "aarch64")]
1362    {
1363        "NEON (2×f64, 128-bit)"
1364    }
1365    #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
1366    {
1367        "Scalar (4-wide ILP unroll)"
1368    }
1369}
1370
1371#[cfg(test)]
1372mod tests {
1373    /// `simd_min_max` skips `NaN`, on whatever tier this build selected.
1374    ///
1375    /// It did not on x86: `MINPD dst, src` returns `src` when either operand
1376    /// is `NaN`, and the accumulator was the *first* operand — so one `NaN`
1377    /// poisoned it and the answer became whatever the last lane held, while
1378    /// aarch64 and the scalar fallback skipped it. Two architectures, two
1379    /// answers, and a doc comment saying they agreed.
1380    ///
1381    /// The lengths matter: the SIMD tiers process 16, 8 or 4 values per
1382    /// iteration with a scalar tail, so a `NaN` has to be tried in the
1383    /// vectorised body *and* in the tail.
1384    #[test]
1385    fn simd_min_max_skips_nan_at_every_position() {
1386        /// The answer, by definition: `NaN` is not a value.
1387        fn reference(data: &[f64]) -> (f64, f64) {
1388            let mut min = f64::INFINITY;
1389            let mut max = f64::NEG_INFINITY;
1390            for &v in data.iter().filter(|v| !v.is_nan()) {
1391                min = min.min(v);
1392                max = max.max(v);
1393            }
1394            (min, max)
1395        }
1396
1397        for len in [1_usize, 2, 3, 4, 5, 7, 8, 9, 15, 16, 17, 33, 64, 129] {
1398            for nan_at in 0..len {
1399                let mut data: Vec<f64> = (0..len).map(|i| i as f64 - 10.0).collect();
1400                data[nan_at] = f64::NAN;
1401                let (min, max) = super::simd_min_max(&data);
1402                let (want_min, want_max) = reference(&data);
1403                assert_eq!(
1404                    (min, max),
1405                    (want_min, want_max),
1406                    "len {len}, NaN at {nan_at}"
1407                );
1408            }
1409        }
1410    }
1411
1412    /// An all-`NaN` slice has no minimum and no maximum, and says so the same
1413    /// way an empty one does.
1414    #[test]
1415    fn simd_min_max_of_all_nan_is_the_empty_answer() {
1416        for len in [1_usize, 4, 8, 16, 33] {
1417            let data = vec![f64::NAN; len];
1418            let (min, max) = super::simd_min_max(&data);
1419            assert_eq!((min, max), (f64::INFINITY, f64::NEG_INFINITY), "len {len}");
1420        }
1421    }
1422
1423    proptest::proptest! {
1424        /// Against arbitrary input, including `NaN` and the infinities.
1425        #[test]
1426        fn simd_min_max_matches_a_nan_skipping_reference(
1427            data in proptest::collection::vec(
1428                proptest::prop_oneof![
1429                    3 => -1e6_f64..1e6_f64,
1430                    1 => proptest::strategy::Just(f64::NAN),
1431                    1 => proptest::strategy::Just(f64::INFINITY),
1432                    1 => proptest::strategy::Just(f64::NEG_INFINITY),
1433                ],
1434                0..200,
1435            )
1436        ) {
1437            let mut want_min = f64::INFINITY;
1438            let mut want_max = f64::NEG_INFINITY;
1439            for &v in data.iter().filter(|v| !v.is_nan()) {
1440                want_min = want_min.min(v);
1441                want_max = want_max.max(v);
1442            }
1443            let (min, max) = super::simd_min_max(&data);
1444            proptest::prop_assert_eq!(min, want_min);
1445            proptest::prop_assert_eq!(max, want_max);
1446        }
1447    }
1448
1449    use super::*;
1450
1451    // ── Sum ──────────────────────────────────────────────────────────
1452
1453    #[test]
1454    fn sum_empty() {
1455        assert_eq!(simd_sum(&[]), 0.0);
1456    }
1457
1458    #[test]
1459    fn sum_single() {
1460        assert!((simd_sum(&[42.0]) - 42.0).abs() < 1e-10);
1461    }
1462
1463    #[test]
1464    fn sum_exact_chunk() {
1465        let data: Vec<f64> = (1..=8).map(|x| x as f64).collect();
1466        assert!((simd_sum(&data) - 36.0).abs() < 1e-10);
1467    }
1468
1469    #[test]
1470    fn sum_with_remainder() {
1471        let data: Vec<f64> = (1..=7).map(|x| x as f64).collect();
1472        assert!((simd_sum(&data) - 28.0).abs() < 1e-10);
1473    }
1474
1475    #[test]
1476    fn sum_large() {
1477        let data: Vec<f64> = (0..10_000).map(|i| i as f64).collect();
1478        let expected = (10_000.0 * 9999.0) / 2.0;
1479        assert!((simd_sum(&data) - expected).abs() < 1e-4);
1480    }
1481
1482    #[test]
1483    fn sum_large_avx2_range() {
1484        // 16-element aligned (AVX2 chunk size)
1485        let data: Vec<f64> = (0..16).map(|i| i as f64).collect();
1486        assert!((simd_sum(&data) - 120.0).abs() < 1e-10);
1487    }
1488
1489    #[test]
1490    fn sum_large_avx512_range() {
1491        // 32-element aligned (AVX-512 chunk size)
1492        let data: Vec<f64> = (0..32).map(|i| i as f64).collect();
1493        assert!((simd_sum(&data) - 496.0).abs() < 1e-10);
1494    }
1495
1496    #[test]
1497    fn sum_100k() {
1498        let data: Vec<f64> = (0..100_000).map(|i| i as f64).collect();
1499        let expected = 100_000.0 * 99_999.0 / 2.0;
1500        assert!((simd_sum(&data) - expected).abs() / expected < 1e-10);
1501    }
1502
1503    // ── Mean ─────────────────────────────────────────────────────────
1504
1505    #[test]
1506    fn mean_empty() {
1507        assert!(simd_mean(&[]).is_nan());
1508    }
1509
1510    #[test]
1511    fn mean_values() {
1512        assert!((simd_mean(&[1.0, 2.0, 3.0, 4.0, 5.0]) - 3.0).abs() < 1e-10);
1513    }
1514
1515    // ── Variance ─────────────────────────────────────────────────────
1516
1517    #[test]
1518    fn variance_known() {
1519        let data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
1520        let mean = simd_mean(&data);
1521        let var = simd_variance(&data, mean);
1522        // Sample variance: sum((xi - mean)^2) / (n-1) = 32/7
1523        assert!((var - 32.0 / 7.0).abs() < 1e-10);
1524    }
1525
1526    #[test]
1527    fn variance_single_element() {
1528        // Sample variance is undefined for n=1
1529        assert!(simd_variance(&[42.0], 42.0).is_nan());
1530    }
1531
1532    #[test]
1533    fn variance_empty() {
1534        assert!(simd_variance(&[], 0.0).is_nan());
1535    }
1536
1537    #[test]
1538    fn variance_constant() {
1539        let data = [5.0; 100];
1540        let mean = simd_mean(&data);
1541        assert!(simd_variance(&data, mean).abs() < 1e-14);
1542    }
1543
1544    // ── Min/Max ──────────────────────────────────────────────────────
1545
1546    #[test]
1547    fn min_max_empty() {
1548        let (min, max) = simd_min_max(&[]);
1549        assert_eq!(min, f64::INFINITY);
1550        assert_eq!(max, f64::NEG_INFINITY);
1551    }
1552
1553    #[test]
1554    fn min_max_values() {
1555        let (min, max) = simd_min_max(&[5.0, 2.0, 8.0, 1.0, 9.0, 3.0, 7.0]);
1556        assert!((min - 1.0).abs() < 1e-10);
1557        assert!((max - 9.0).abs() < 1e-10);
1558    }
1559
1560    #[test]
1561    fn min_max_single() {
1562        let (min, max) = simd_min_max(&[42.0]);
1563        assert!((min - 42.0).abs() < 1e-10);
1564        assert!((max - 42.0).abs() < 1e-10);
1565    }
1566
1567    #[test]
1568    fn min_max_negative() {
1569        let (min, max) = simd_min_max(&[-10.0, -5.0, -1.0, -20.0, -3.0]);
1570        assert!((min - (-20.0)).abs() < 1e-10);
1571        assert!((max - (-1.0)).abs() < 1e-10);
1572    }
1573
1574    #[test]
1575    fn min_max_large() {
1576        let data: Vec<f64> = (0..10_000).map(|i| i as f64).collect();
1577        let (min, max) = simd_min_max(&data);
1578        assert!((min - 0.0).abs() < 1e-10);
1579        assert!((max - 9999.0).abs() < 1e-10);
1580    }
1581
1582    #[test]
1583    fn min_max_with_nan() {
1584        // NaN values must be skipped consistently across all SIMD tiers
1585        // (x86 SSE2/AVX2/AVX-512, aarch64 NEON, scalar fallback).
1586        let data = [f64::NAN, 2.0, f64::NAN, 1.0, 5.0, f64::NAN, 3.0];
1587        let (min, max) = simd_min_max(&data);
1588        assert!((min - 1.0).abs() < 1e-10, "min should skip NaN, got {min}");
1589        assert!((max - 5.0).abs() < 1e-10, "max should skip NaN, got {max}");
1590    }
1591
1592    #[test]
1593    fn min_max_all_nan() {
1594        // All-NaN slice returns the identity values (no finite values to compare).
1595        let (min, max) = simd_min_max(&[f64::NAN, f64::NAN]);
1596        assert!(
1597            min == f64::INFINITY || min.is_nan(),
1598            "unexpected min: {min}"
1599        );
1600        assert!(
1601            max == f64::NEG_INFINITY || max.is_nan(),
1602            "unexpected max: {max}"
1603        );
1604    }
1605
1606    // ── Standard Deviation ───────────────────────────────────────────
1607
1608    #[test]
1609    fn std_dev_constant() {
1610        assert!((simd_std_dev(&[5.0, 5.0, 5.0, 5.0])).abs() < 1e-10);
1611    }
1612
1613    #[test]
1614    fn std_dev_known() {
1615        let data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
1616        let sd = simd_std_dev(&data);
1617        assert!((sd - 2.138_089_935_299_395).abs() < 1e-6);
1618    }
1619
1620    // ── Dot Product ──────────────────────────────────────────────────
1621
1622    #[test]
1623    fn dot_product_basic() {
1624        let a = [1.0, 2.0, 3.0, 4.0, 5.0];
1625        let b = [5.0, 4.0, 3.0, 2.0, 1.0];
1626        assert!((simd_dot_product(&a, &b).unwrap() - 35.0).abs() < 1e-10);
1627    }
1628
1629    #[test]
1630    fn dot_product_empty() {
1631        assert!((simd_dot_product(&[], &[]).unwrap()).abs() < 1e-10);
1632    }
1633
1634    #[test]
1635    fn dot_product_mismatch() {
1636        assert!(simd_dot_product(&[1.0], &[1.0, 2.0]).is_err());
1637    }
1638
1639    #[test]
1640    fn dot_product_identity() {
1641        // dot(v, v) = sum of squares
1642        let v: Vec<f64> = (1..=100).map(|i| i as f64).collect();
1643        let expected: f64 = v.iter().map(|x| x * x).sum();
1644        assert!((simd_dot_product(&v, &v).unwrap() - expected).abs() < 1e-6);
1645    }
1646
1647    #[test]
1648    fn dot_product_orthogonal() {
1649        let a = [1.0, 0.0, 1.0, 0.0];
1650        let b = [0.0, 1.0, 0.0, 1.0];
1651        assert!((simd_dot_product(&a, &b).unwrap()).abs() < 1e-10);
1652    }
1653
1654    #[test]
1655    fn dot_product_large() {
1656        // 10K elements — exercises AVX2/AVX-512 main loops
1657        let a: Vec<f64> = (0..10_000).map(|i| (i as f64) * 0.001).collect();
1658        let b: Vec<f64> = (0..10_000).map(|i| 1.0 - (i as f64) * 0.0001).collect();
1659        let expected: f64 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
1660        assert!(
1661            (simd_dot_product(&a, &b).unwrap() - expected).abs() / expected.abs().max(1.0) < 1e-8
1662        );
1663    }
1664
1665    // ── SIMD tier ────────────────────────────────────────────────────
1666
1667    #[test]
1668    fn simd_tier_not_empty() {
1669        let tier = simd_tier();
1670        assert!(!tier.is_empty());
1671        // On this platform it should be one of the known tiers
1672        assert!(
1673            tier.contains("AVX-512")
1674                || tier.contains("AVX2")
1675                || tier.contains("SSE2")
1676                || tier.contains("NEON")
1677                || tier.contains("Scalar")
1678        );
1679    }
1680
1681    // ── Cross-validation: scalar vs arch agree ───────────────────────
1682
1683    #[test]
1684    fn scalar_vs_arch_sum() {
1685        let data: Vec<f64> = (0..1_000).map(|i| (i as f64) * 0.1 + 0.7).collect();
1686        let arch_result = arch_simd_sum(&data);
1687        let scalar_result = scalar_sum(&data);
1688        assert!(
1689            (arch_result - scalar_result).abs() < 1e-6,
1690            "sum mismatch: arch={arch_result}, scalar={scalar_result}"
1691        );
1692    }
1693
1694    #[test]
1695    fn scalar_vs_arch_variance() {
1696        let data: Vec<f64> = (0..1_000).map(|i| (i as f64) * 0.1 + 0.7).collect();
1697        let mean = simd_mean(&data);
1698        let arch_result = arch_simd_variance(&data, mean);
1699        let scalar_result = scalar_variance(&data, mean);
1700        assert!(
1701            (arch_result - scalar_result).abs() < 1e-6,
1702            "variance mismatch: arch={arch_result}, scalar={scalar_result}"
1703        );
1704    }
1705
1706    #[test]
1707    fn scalar_vs_arch_min_max() {
1708        let data: Vec<f64> = (0..1_000)
1709            .map(|i| ((i * 7 + 3) % 500) as f64 - 250.0)
1710            .collect();
1711        let (amin, amax) = arch_simd_min_max(&data);
1712        let (smin, smax) = scalar_min_max(&data);
1713        assert!((amin - smin).abs() < 1e-10, "min mismatch");
1714        assert!((amax - smax).abs() < 1e-10, "max mismatch");
1715    }
1716
1717    #[test]
1718    fn scalar_vs_arch_dot_product() {
1719        let a: Vec<f64> = (0..1_000).map(|i| (i as f64) * 0.01).collect();
1720        let b: Vec<f64> = (0..1_000).map(|i| 1.0 - (i as f64) * 0.001).collect();
1721        let arch_result = arch_simd_dot_product(&a, &b);
1722        let scalar_result = scalar_dot_product(&a, &b);
1723        assert!(
1724            (arch_result - scalar_result).abs() / scalar_result.abs().max(1.0) < 1e-8,
1725            "dot product mismatch: arch={arch_result}, scalar={scalar_result}"
1726        );
1727    }
1728
1729    // ── Kahan precision ────────────────────────────────────
1730
1731    #[test]
1732    fn kahan_compensated_sum_precision() {
1733        // Classic Kahan torture test: sum 1e8 values of 1.0 each, then add
1734        // 1e8 values of 1e-8 each.  Naive summation loses the tiny terms
1735        // entirely; Kahan preserves them.
1736        let n = 100_000;
1737        let mut data = vec![1.0_f64; n];
1738        data.extend(vec![1e-8_f64; n]);
1739        let expected = n as f64 + (n as f64) * 1e-8;
1740        let result = simd_sum(&data);
1741        let rel_err = (result - expected).abs() / expected;
1742        assert!(
1743            rel_err < 1e-14,
1744            "Kahan sum relative error {rel_err:.2e} exceeds 1e-14 (result={result}, expected={expected})"
1745        );
1746    }
1747
1748    #[test]
1749    fn kahan_alternating_sign_precision() {
1750        // Alternating +big / -big values → catastrophic cancellation for
1751        // naive summation, but Kahan tracks the residuals.
1752        let n = 50_000;
1753        let mut data = Vec::with_capacity(2 * n);
1754        for i in 0..n {
1755            data.push(1e15 + (i as f64));
1756            data.push(-(1e15 + (i as f64)));
1757        }
1758        let result = simd_sum(&data);
1759        assert!(
1760            result.abs() < 1e-3,
1761            "alternating-sign sum should be ~0, got {result}"
1762        );
1763    }
1764}
1765
1766// AVX-512 compile-time validation tests.
1767//
1768// These tests are only compiled when `target_feature = "avx512f"` is active
1769// (e.g. via `RUSTFLAGS="-C target-feature=+avx512f"` in CI on capable
1770// hardware). They call the AVX-512 implementations directly, bypassing
1771// runtime feature detection, to validate correctness at compile time.
1772#[cfg(all(test, target_arch = "x86_64", target_feature = "avx512f"))]
1773mod avx512_tests {
1774    use super::*;
1775
1776    #[test]
1777    fn avx512_sum_direct() {
1778        let data: Vec<f64> = (0..64).map(|i| i as f64).collect();
1779        let result = unsafe { avx512_sum(&data) };
1780        let expected: f64 = (0..64).map(|i| i as f64).sum();
1781        assert!(
1782            (result - expected).abs() < 1e-10,
1783            "avx512_sum mismatch: {result} vs {expected}"
1784        );
1785    }
1786
1787    #[test]
1788    fn avx512_variance_direct() {
1789        let data: Vec<f64> = (0..128).map(|i| i as f64).collect();
1790        let (var, mean) = unsafe { avx512_variance(&data) };
1791        assert!(
1792            (mean - 63.5).abs() < 1e-10,
1793            "avx512_variance mean mismatch: {mean}"
1794        );
1795        assert!(var > 0.0, "variance must be positive");
1796    }
1797
1798    #[test]
1799    fn avx512_min_max_direct() {
1800        let data: Vec<f64> = (0..64).map(|i| i as f64).collect();
1801        let (min, max) = unsafe { avx512_min_max(&data) };
1802        assert!((min - 0.0).abs() < 1e-10);
1803        assert!((max - 63.0).abs() < 1e-10);
1804    }
1805
1806    #[test]
1807    fn avx512_dot_product_direct() {
1808        let a: Vec<f64> = (0..64).map(|i| i as f64).collect();
1809        let b: Vec<f64> = (0..64).map(|i| (i * 2) as f64).collect();
1810        let result = unsafe { avx512_dot_product(&a, &b) };
1811        let expected: f64 = (0..64).map(|i| (i * i * 2) as f64).sum();
1812        assert!(
1813            (result - expected).abs() < 1e-6,
1814            "avx512_dot mismatch: {result} vs {expected}"
1815        );
1816    }
1817}