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