simd_kernels/kernels/
aggregate.rs

1// Copyright Peter Bower 2025. All Rights Reserved.
2// Licensed under Mozilla Public License (MPL) 2.0.
3
4//! # **Aggregation Kernels Module** - *High-Performance Statistical Reduction Operations*
5//!
6//! Null-aware aggregation kernels implementing statistical reductions with SIMD acceleration.
7//! Core building blocks for analytical workloads requiring high-throughput data summarisation.
8//!
9//! ## Core Operations
10//! - **Sum aggregation**: Optimised summation with overflow handling and numerical stability
11//! - **Min/Max operations**: Efficient extrema detection with proper null handling
12//! - **Count operations**: Fast counting with distinct value detection capabilities
13//! - **Statistical moments**: Mean, variance, and standard deviation calculations
14//! - **Percentile calculations**: Quantile computation with interpolation support
15//! - **String aggregation**: Concatenation and string-based reduction operations
16include!(concat!(env!("OUT_DIR"), "/simd_lanes.rs"));
17
18#[cfg(not(feature = "fast_hash"))]
19use std::collections::HashMap;
20use std::collections::HashSet;
21use std::hash::Hash;
22use std::ops::{Mul, Sub};
23#[cfg(feature = "simd")]
24use std::simd::{
25    Mask, Simd, SimdElement,
26    cmp::SimdOrd,
27    num::{SimdFloat, SimdInt, SimdUint},
28};
29
30#[cfg(feature = "fast_hash")]
31use ahash::AHashMap;
32use minarrow::traits::type_unions::Float;
33use minarrow::{Bitmask, Vec64};
34use num_traits::{NumCast, One, ToPrimitive, Zero};
35
36use crate::kernels::sort::{sort_float, total_cmp_f};
37use crate::traits::to_bits::ToBits;
38use crate::utils::has_nulls;
39
40#[cfg(feature = "simd")]
41use crate::utils::{bitmask_to_simd_mask, is_simd_aligned};
42
43/// Iterator over non-null values in a slice, using an optional Bitmask.
44struct ValidIter<'a, T> {
45    slice: &'a [T],
46    idx: usize,
47    mask: Option<&'a Bitmask>,
48    len: usize,
49}
50impl<'a, T: Copy> ValidIter<'a, T> {
51    #[inline(always)]
52    fn new(slice: &'a [T], mask: Option<&'a Bitmask>) -> Self {
53        let len = slice.len();
54        Self {
55            slice,
56            idx: 0,
57            mask,
58            len,
59        }
60    }
61}
62impl<'a, T: Copy> Iterator for ValidIter<'a, T> {
63    type Item = T;
64    #[inline(always)]
65    fn next(&mut self) -> Option<Self::Item> {
66        match self.mask {
67            None => {
68                if self.idx >= self.len {
69                    None
70                } else {
71                    let v = self.slice[self.idx];
72                    self.idx += 1;
73                    Some(v)
74                }
75            }
76            Some(m) => {
77                while self.idx < self.len {
78                    let i = self.idx;
79                    self.idx += 1;
80                    if unsafe { m.get_unchecked(i) } {
81                        return Some(self.slice[i]);
82                    }
83                }
84                None
85            }
86        }
87    }
88}
89
90/// Collects valid (non-null) values from a slice into a Vec64.
91#[inline(always)]
92fn collect_valid<T: Copy>(d: &[T], m: Option<&Bitmask>) -> Vec64<T> {
93    ValidIter::new(d, m).collect()
94}
95
96// --- SIMD/stat-moments helpers -----------------------------------------------
97
98/// Generates stat‐moment aggregation (sum, sum², count)
99/// for float types using SIMD or scalar fallback.
100macro_rules! impl_stat_moments_float {
101    ($name:ident, $ty:ty, $LANES:expr) => {
102        #[doc = concat!("Computes sum, sum-of-squares, and count for a `", stringify!($ty), "` slice.")]
103        #[doc = "Skips nulls if present. Uses SIMD if available."]
104        #[inline(always)]
105        pub fn $name(
106            data: &[$ty],
107            mask: Option<&Bitmask>,
108            null_count: Option<usize>,
109        ) -> (f64, f64, usize) {
110            let has_nulls = has_nulls(null_count, mask);
111
112            #[cfg(feature = "simd")]
113            {
114                // Check if both arrays are 64-byte aligned for SIMD
115                if is_simd_aligned(data) {
116                    type V<const L: usize> = Simd<$ty, L>;
117                    // The mask‐type for `Simd<$ty, N>` is `<$ty as SimdElement>::Mask`.
118                    type M<const L: usize> = <$ty as SimdElement>::Mask;
119
120                    const N: usize = $LANES;
121                    let len = data.len();
122                    let mut i = 0;
123                    let (mut sum, mut sum2) = (0.0_f64, 0.0_f64);
124                    let mut cnt = 0_usize;
125
126                    if !has_nulls {
127                        // dense path
128                        while i + N <= len {
129                            let v = V::<N>::from_slice(&data[i..i + N]);
130                            let dv = v.cast::<f64>();
131                            cnt += N;
132                            sum += dv.reduce_sum();
133                            sum2 += (dv * dv).reduce_sum();
134                            i += N;
135                        }
136                        // tail
137                        for &v in &data[i..] {
138                            let x = v as f64;
139                            sum += x;
140                            sum2 += x * x;
141                            cnt += 1;
142                        }
143                    } else {
144                        // null‐aware SIMD path
145                        let mb = mask.expect("Mask must be Some if nulls are present");
146                        let mask_bytes = mb.as_bytes();
147
148                        while i + N <= len {
149                            let v = V::<N>::from_slice(&data[i..i + N]);
150                            // extract a SIMD mask of valid lanes
151                            let lane_mask: Mask<M<N>, N> =
152                                bitmask_to_simd_mask::<N, M<N>>(mask_bytes, i, len);
153                            // zero‐out invalid lanes
154                            let zeros = V::<N>::splat(0.0 as $ty);
155                            let vv = lane_mask.select(v, zeros);
156
157                            // count valid lanes
158                            cnt += lane_mask.to_bitmask().count_ones() as usize;
159
160                            let dv = vv.cast::<f64>();
161                            sum += dv.reduce_sum();
162                            sum2 += (dv * dv).reduce_sum();
163
164                            i += N;
165                        }
166                        // scalar tail
167                        for j in i..len {
168                            if unsafe { mb.get_unchecked(j) } {
169                                let x = data[j] as f64;
170                                sum += x;
171                                sum2 += x * x;
172                                cnt += 1;
173                            }
174                        }
175                    }
176                    return (sum, sum2, cnt);
177                }
178                // Fall through to scalar path if alignment check failed
179            }
180
181            // Scalar fallback - alignment check failed
182            let (mut sum, mut sum2, mut cnt) = (0.0_f64, 0.0_f64, 0_usize);
183            if !has_nulls {
184                for &v in data {
185                    let x = v as f64;
186                    sum += x;
187                    sum2 += x * x;
188                    cnt += 1;
189                }
190            } else {
191                let mb = mask.expect("Mask must be Some if nulls are present");
192                for (i, &v) in data.iter().enumerate() {
193                    if unsafe { mb.get_unchecked(i) } {
194                        let x = v as f64;
195                        sum += x;
196                        sum2 += x * x;
197                        cnt += 1;
198                    }
199                }
200            }
201            (sum, sum2, cnt)
202        }
203    };
204}
205
206/// Generates stat-moment aggregation (sum, sum², count)
207/// for integer types using SIMD or scalar fallback.
208macro_rules! impl_stat_moments_int {
209    ($name:ident, $ty:ty, $acc:ty, $LANES:expr, $mask_ty:ty) => {
210        #[doc = concat!("Computes sum, sum-of-squares, and count for `", stringify!($ty), "` integers.")]
211        #[doc = concat!(" Uses `", stringify!($acc), "` for accumulation. Skips nulls if present.")]
212        #[inline(always)]
213        pub fn $name(
214            data: &[$ty],
215            mask: Option<&Bitmask>,
216            null_count: Option<usize>
217        ) -> (f64, f64, usize) {
218            let has_nulls = has_nulls(null_count, mask);
219
220            #[cfg(feature = "simd")]
221            {
222                // Check if both arrays are 64-byte aligned for SIMD
223                if is_simd_aligned(data) {
224                const N: usize = $LANES;
225                let mut i = 0;
226                let mut sum: $acc = 0;
227                let mut sum2: $acc = 0;
228                let mut cnt = 0usize;
229
230                if !has_nulls {
231                    while i + N <= data.len() {
232                        let v = Simd::<$ty, N>::from_slice(&data[i..i + N]);
233                        sum += v.reduce_sum() as $acc;
234                        sum2 += (v * v).reduce_sum() as $acc;
235                        cnt += N;
236                        i += N;
237                    }
238                    for &x in &data[i..] {
239                        let x = x as $acc;
240                        sum += x;
241                        sum2 += x * x;
242                        cnt += 1;
243                    }
244                } else {
245                    let mb = mask.expect("Mask must be Some if nulls are present or mask was supplied");
246                    let mask_bytes = &mb.bits;
247                    while i + N <= data.len() {
248                        let v = Simd::<$ty, N>::from_slice(&data[i..i + N]);
249                        // use signed type for mask to satisfy trait bound
250                        let lane_mask = bitmask_to_simd_mask::<N, $mask_ty>(mask_bytes, i, data.len());
251                        let vv = lane_mask.select(v, Simd::<$ty, N>::splat(0 as $ty));
252                        sum += vv.reduce_sum() as $acc;
253                        sum2 += (vv * vv).reduce_sum() as $acc;
254                        cnt += lane_mask.to_bitmask().count_ones() as usize;
255                        i += N;
256                    }
257                    for j in i..data.len() {
258                        if unsafe { mb.get_unchecked(j) } {
259                            let x = data[j] as $acc;
260                            sum += x;
261                            sum2 += x * x;
262                            cnt += 1;
263                        }
264                    }
265                }
266                    return (sum as f64, sum2 as f64, cnt);
267                }
268                // Fall through to scalar path if alignment check failed
269            }
270
271            // Scalar fallback - alignment check failed
272            let mut sum: $acc = 0;
273            let mut sum2: $acc = 0;
274            let mut cnt = 0usize;
275            if !has_nulls {
276                for &x in data.iter() {
277                    let xi = x as $acc;
278                    sum += xi;
279                    sum2 += xi * xi;
280                    cnt += 1;
281                }
282            } else {
283                let mb = mask.expect("Mask must be Some if nulls are present or mask was supplied");
284                for (i, &x) in data.iter().enumerate() {
285                    if unsafe { mb.get_unchecked(i) } {
286                        let xi = x as $acc;
287                        sum += xi;
288                        sum2 += xi * xi;
289                        cnt += 1;
290                    }
291                }
292            }
293            (sum as f64, sum2 as f64, cnt)
294        }
295    };
296}
297
298/// Implements SIMD-enabled min/max reducers
299/// for numeric slices with or without nulls.
300macro_rules! impl_reduce_min_max {
301    ($name:ident, $ty:ty, $LANES:expr, $minval:expr, $maxval:expr) => {
302        /// Finds the minimum and maximum values in a numeric slice.
303        /// 
304        /// Uses SIMD operations when available for optimal performance.
305        /// Handles null values through optional bitmask.
306        /// 
307        /// # Arguments
308        /// 
309        /// * `data` - The slice of numeric values to process
310        /// * `mask` - Optional bitmask indicating valid/invalid elements
311        /// * `null_count` - Optional count of null values for optimisation
312        /// 
313        /// # Returns
314        /// 
315        /// `Some((min, max))` if any valid values exist, `None` if all values are null
316        #[inline(always)]
317        pub fn $name(
318            data: &[$ty],
319            mask: Option<&Bitmask>,
320            null_count: Option<usize>,
321        ) -> Option<($ty, $ty)> {
322            let has_nulls = has_nulls(null_count, mask);
323            let len = data.len();
324            if len == 0 {
325                return None;
326            }
327
328            #[cfg(feature = "simd")]
329            {
330                // Check if both arrays are 64-byte aligned for SIMD
331                if is_simd_aligned(data) {
332                    type V<const N: usize> = Simd<$ty, N>;
333                    type M<const N: usize> = <$ty as SimdElement>::Mask;
334                    const N: usize = $LANES;
335
336                    let mut first_valid = None;
337                    if !has_nulls {
338                        // Find first value
339                        if len == 0 {
340                            return None;
341                        }
342                        first_valid = Some((0, data[0]));
343                    } else {
344                        let mb = mask.expect("Mask must be Some if nulls are present");
345                        for (idx, &x) in data.iter().enumerate() {
346                            if unsafe { mb.get_unchecked(idx) } {
347                                first_valid = Some((idx, x));
348                                break;
349                            }
350                        }
351                    }
352                    let (mut i, x0) = match first_valid {
353                        Some(v) => v,
354                        None => return None,
355                    };
356                    let mut min_s = x0;
357                    let mut max_s = x0;
358                    i += 1;
359
360                    // SIMD alignment: process until next SIMD-aligned index
361                    while i < len && (i % N != 0) {
362                        let x = data[i];
363                        if !has_nulls
364                            || (has_nulls && {
365                                let mb = mask.unwrap();
366                                unsafe { mb.get_unchecked(i) }
367                            })
368                        {
369                            min_s = min_s.min(x);
370                            max_s = max_s.max(x);
371                        }
372                        i += 1;
373                    }
374
375                    let mut min_v = V::<N>::splat(min_s);
376                    let mut max_v = V::<N>::splat(max_s);
377
378                    if !has_nulls {
379                        while i + N <= len {
380                            let v = V::<N>::from_slice(&data[i..i + N]);
381                            min_v = min_v.simd_min(v);
382                            max_v = max_v.simd_max(v);
383                            i += N;
384                        }
385                    } else {
386                        let mb = mask.unwrap();
387                        let mask_bytes = mb.as_bytes();
388                        while i + N <= len {
389                            let v = V::<N>::from_slice(&data[i..i + N]);
390                            let lane_m: Mask<M<N>, N> =
391                                bitmask_to_simd_mask::<N, M<N>>(mask_bytes, i, len);
392                            // Null entries are replaced by previous running min/max
393                            let v_min = lane_m.select(v, V::<N>::splat(min_s));
394                            let v_max = lane_m.select(v, V::<N>::splat(max_s));
395                            min_v = min_v.simd_min(v_min);
396                            max_v = max_v.simd_max(v_max);
397                            i += N;
398                        }
399                    }
400
401                    for idx in i..len {
402                        let x = data[idx];
403                        if !has_nulls
404                            || (has_nulls && {
405                                let mb = mask.unwrap();
406                                unsafe { mb.get_unchecked(idx) }
407                            })
408                        {
409                            min_s = min_s.min(x);
410                            max_s = max_s.max(x);
411                        }
412                    }
413
414                    min_s = min_s.min(min_v.reduce_min());
415                    max_s = max_s.max(max_v.reduce_max());
416                    return Some((min_s, max_s));
417                }
418                // Fall through to scalar path if alignment check failed
419            }
420
421            // Scalar fallback - alignment check failed
422            let mut min = None;
423            let mut max = None;
424            if !has_nulls {
425                for &x in data {
426                    match min {
427                        None => {
428                            min = Some(x);
429                            max = Some(x);
430                        }
431                        Some(m) => {
432                            min = Some(m.min(x));
433                            max = Some(max.unwrap().max(x));
434                        }
435                    }
436                }
437            } else {
438                let mb = mask.expect("Mask must be Some if nulls are present");
439                for (i, &x) in data.iter().enumerate() {
440                    if unsafe { mb.get_unchecked(i) } {
441                        match min {
442                            None => {
443                                min = Some(x);
444                                max = Some(x);
445                            }
446                            Some(m) => {
447                                min = Some(m.min(x));
448                                max = Some(max.unwrap().max(x));
449                            }
450                        }
451                    }
452                }
453            }
454            min.zip(max)
455        }
456    };
457}
458
459/// Implements SIMD-enabled floating-point min/max reducers
460/// for numeric slices with or without nulls, handling NaN values.
461macro_rules! impl_reduce_min_max_float {
462    ($fn_name:ident, $ty:ty, $LANES:ident, $SIMD:ident, $MASK:ident) => {
463        /// Finds the minimum and maximum values in a floating-point slice.
464        /// 
465        /// Handles NaN values correctly by treating them as missing data.
466        /// Uses SIMD operations when available for optimal performance.
467        /// 
468        /// # Arguments
469        /// 
470        /// * `data` - The slice of floating-point values to process
471        /// * `mask` - Optional bitmask indicating valid/invalid elements
472        /// * `null_count` - Optional count of null values for optimisation
473        /// 
474        /// # Returns
475        /// 
476        /// `Some((min, max))` if any valid values exist, `None` if all values are null/NaN
477        #[inline(always)]
478        pub fn $fn_name(
479            data: &[$ty],
480            mask: Option<&Bitmask>,
481            null_count: Option<usize>,
482        ) -> Option<($ty, $ty)> {
483            let has_nulls = has_nulls(null_count, mask);
484            let len = data.len();
485            if len == 0 {
486                return None;
487            }
488
489            #[cfg(feature = "simd")]
490            {
491                // Check if both arrays are 64-byte aligned for SIMD
492                if is_simd_aligned(data) {
493                    const LANES: usize = $LANES;
494                    type V = $SIMD<$ty, LANES>;
495                    type M = <$ty as SimdElement>::Mask;
496
497                    // Find the first valid value (not null and not NaN)
498                    let mut first_valid = None;
499                    if !has_nulls {
500                        for (idx, &x) in data.iter().enumerate() {
501                            if !x.is_nan() {
502                                first_valid = Some((idx, x));
503                                break;
504                            }
505                        }
506                    } else {
507                        let mb = mask.expect("Bitmask must be Some if nulls are present");
508                        for (idx, &x) in data.iter().enumerate() {
509                            if unsafe { mb.get_unchecked(idx) } && !x.is_nan() {
510                                first_valid = Some((idx, x));
511                                break;
512                            }
513                        }
514                    }
515                    let (mut i, x0) = match first_valid {
516                        Some(v) => v,
517                        None => return None,
518                    };
519                    let mut min_s = x0;
520                    let mut max_s = x0;
521                    i += 1;
522
523                    // SIMD alignment for tail
524                    while i < len && (i % LANES != 0) {
525                        let x = data[i];
526                        if (!has_nulls && !x.is_nan())
527                            || (has_nulls && {
528                                let mb = mask.unwrap();
529                                unsafe { mb.get_unchecked(i) && !x.is_nan() }
530                            })
531                        {
532                            min_s = min_s.min(x);
533                            max_s = max_s.max(x);
534                        }
535                        i += 1;
536                    }
537
538                    let mut min_v = V::splat(min_s);
539                    let mut max_v = V::splat(max_s);
540
541                    if !has_nulls {
542                        while i + LANES <= len {
543                            let v = V::from_slice(&data[i..i + LANES]);
544                            let valid = !v.is_nan();
545                            let v_min = valid.select(v, V::splat(<$ty>::INFINITY));
546                            let v_max = valid.select(v, V::splat(<$ty>::NEG_INFINITY));
547                            min_v = min_v.simd_min(v_min);
548                            max_v = max_v.simd_max(v_max);
549                            i += LANES;
550                        }
551                    } else {
552                        let mb = mask.unwrap();
553                        let mask_bytes = mb.as_bytes();
554                        while i + LANES <= len {
555                            let v = V::from_slice(&data[i..i + LANES]);
556                            let valid_mask =
557                                bitmask_to_simd_mask::<LANES, M>(mask_bytes, i, len) & !v.is_nan();
558                            let v_min = valid_mask.select(v, V::splat(<$ty>::INFINITY));
559                            let v_max = valid_mask.select(v, V::splat(<$ty>::NEG_INFINITY));
560                            min_v = min_v.simd_min(v_min);
561                            max_v = max_v.simd_max(v_max);
562                            i += LANES;
563                        }
564                    }
565
566                    for idx in i..len {
567                        let x = data[idx];
568                        if (!has_nulls && !x.is_nan())
569                            || (has_nulls && {
570                                let mb = mask.unwrap();
571                                unsafe { mb.get_unchecked(idx) && !x.is_nan() }
572                            })
573                        {
574                            min_s = min_s.min(x);
575                            max_s = max_s.max(x);
576                        }
577                    }
578
579                    min_s = min_s.min(min_v.reduce_min());
580                    max_s = max_s.max(max_v.reduce_max());
581                    return Some((min_s, max_s));
582                }
583                // Fall through to scalar path if alignment check failed
584            }
585
586            // Scalar fallback - alignment check failed
587            let mut min = None;
588            let mut max = None;
589            if !has_nulls {
590                for &x in data {
591                    if !x.is_nan() {
592                        match min {
593                            None => {
594                                min = Some(x);
595                                max = Some(x);
596                            }
597                            Some(m) => {
598                                min = Some(m.min(x));
599                                max = Some(max.unwrap().max(x));
600                            }
601                        }
602                    }
603                }
604            } else {
605                let mb = mask.expect("Bitmask must be Some if nulls are present");
606                for (i, &x) in data.iter().enumerate() {
607                    if unsafe { mb.get_unchecked(i) } && !x.is_nan() {
608                        match min {
609                            None => {
610                                min = Some(x);
611                                max = Some(x);
612                            }
613                            Some(m) => {
614                                min = Some(m.min(x));
615                                max = Some(max.unwrap().max(x));
616                            }
617                        }
618                    }
619                }
620            }
621            min.zip(max)
622        }
623    };
624}
625
626/// Dispatches to the right stat-moment function by type.
627macro_rules! stat_moments {
628    (f64 => $d:expr, $m:expr, $hn:expr) => {
629        stat_moments_f64($d, $m, $hn)
630    };
631    (f32 => $d:expr, $m:expr, $hn:expr) => {
632        stat_moments_f32($d, $m, $hn)
633    };
634    (i64 => $d:expr, $m:expr, $hn:expr) => {
635        stat_moments_i64($d, $m, $hn)
636    };
637    (u64 => $d:expr, $m:expr, $hn:expr) => {
638        stat_moments_u64($d, $m, $hn)
639    };
640    (i32 => $d:expr, $m:expr, $hn:expr) => {
641        stat_moments_i32($d, $m, $hn)
642    };
643    (u32 => $d:expr, $m:expr, $hn:expr) => {
644        stat_moments_u32($d, $m, $hn)
645    };
646}
647
648/// Generates a variance function using stat moments.
649/// Generates both population and sample variance for a numeric type.
650macro_rules! impl_variance {
651    ($ty:ident, $fn_var:ident) => {
652        #[doc = concat!("Returns the variance for `", stringify!($ty), "`; if `sample` is true, uses Bessel's correction (n-1).")]
653        #[inline(always)]
654        pub fn $fn_var(
655            data: &[$ty],
656            mask: Option<&Bitmask>,
657            null_count: Option<usize>,
658            sample: bool,
659        ) -> Option<f64> {
660            let (s, s2, n) = stat_moments!($ty => data, mask, null_count);
661            if n == 0 || (sample && n < 2) {
662                None
663            } else if sample {
664                Some((s2 - s * s / n as f64) / (n as f64 - 1.0))
665            } else {
666                Some((s2 - s * s / n as f64) / n as f64)
667            }
668        }
669    };
670}
671
672/// Generates min, max, and (min, max) range extractors from a reduce.
673macro_rules! impl_min_max_range {
674    ($ty:ty, $reduce_fn:ident, $min_fn:ident, $max_fn:ident, $range_fn:ident) => {
675        #[doc = concat!("Returns the minimum of a `", stringify!($ty), "` slice.")]
676        #[inline(always)]
677        pub fn $min_fn(
678            data: &[$ty],
679            mask: Option<&Bitmask>,
680            null_count: Option<usize>,
681        ) -> Option<$ty> {
682            $reduce_fn(data, mask, null_count).map(|(min, _)| min)
683        }
684
685        #[doc = concat!("Returns the maximum of a `", stringify!($ty), "` slice.")]
686        #[inline(always)]
687        pub fn $max_fn(
688            data: &[$ty],
689            mask: Option<&Bitmask>,
690            null_count: Option<usize>,
691        ) -> Option<$ty> {
692            $reduce_fn(data, mask, null_count).map(|(_, max)| max)
693        }
694
695        #[doc = concat!("Returns the (min, max) of a `", stringify!($ty), "` slice.")]
696        #[inline(always)]
697        pub fn $range_fn(
698            data: &[$ty],
699            mask: Option<&Bitmask>,
700            null_count: Option<usize>,
701        ) -> Option<($ty, $ty)> {
702            $reduce_fn(data, mask, null_count)
703        }
704    };
705}
706
707// ---------- 1. SIMD helpers: SUM / COUNT / SUM² ------------------------
708
709/// Implements raw (sum, count) aggregation for float types.
710macro_rules! impl_sum_float {
711    ($name:ident, $ty:ty, $LANES:expr) => {
712        #[doc = concat!("Returns (sum, count) for non-null `", stringify!($ty), "` values.")]
713        #[doc = " Uses SIMD where available."]
714        #[inline(always)]
715        pub fn $name(
716            data: &[$ty],
717            mask: Option<&Bitmask>,
718            null_count: Option<usize>,
719        ) -> (f64, usize) {
720            let has_nulls = has_nulls(null_count, mask);
721
722            #[cfg(feature = "simd")]
723            {
724                // Check if both arrays are 64-byte aligned for SIMD
725                if is_simd_aligned(data) {
726                    const N: usize = $LANES;
727                    type V<const L: usize> = Simd<$ty, L>;
728                    type M<const L: usize> = <$ty as SimdElement>::Mask;
729
730                    let len = data.len();
731                    let mut i = 0;
732                    let mut sum = 0.0f64;
733                    let mut cnt = 0usize;
734
735                    if !has_nulls {
736                        // Dense path
737                        while i + N <= len {
738                            let v = V::<N>::from_slice(&data[i..i + N]);
739                            sum += v.cast::<f64>().reduce_sum();
740                            cnt += N;
741                            i += N;
742                        }
743                        for &x in &data[i..] {
744                            sum += x as f64;
745                            cnt += 1;
746                        }
747                    } else {
748                        // Null‐aware path
749                        let mb = mask.expect(
750                            "Bitmask must be Some if nulls are present or mask is supplied",
751                        );
752                        let bytes = mb.as_bytes();
753
754                        while i + N <= len {
755                            let v = V::<N>::from_slice(&data[i..i + N]);
756                            // extract a SIMD‐mask for these N lanes
757                            let lane_m: Mask<M<N>, N> =
758                                bitmask_to_simd_mask::<N, M<N>>(bytes, i, len);
759                            // zero out the null lanes
760                            let vv = lane_m.select(v, V::<N>::splat(0.0 as $ty));
761                            sum += vv.cast::<f64>().reduce_sum();
762                            // count the 1‐bits in lane_m
763                            cnt += lane_m.to_bitmask().count_ones() as usize;
764                            i += N;
765                        }
766                        for idx in i..len {
767                            if unsafe { mb.get_unchecked(idx) } {
768                                sum += data[idx] as f64;
769                                cnt += 1;
770                            }
771                        }
772                    }
773
774                    return (sum, cnt);
775                }
776                // Fall through to scalar path if alignment check failed
777            }
778
779            // Scalar fallback - alignment check failed
780            let mut sum = 0.0f64;
781            let mut cnt = 0usize;
782            let len = data.len();
783
784            if !has_nulls {
785                for &x in data {
786                    sum += x as f64;
787                    cnt += 1;
788                }
789            } else {
790                let mb =
791                    mask.expect("Bitmask must be Some if nulls are present or mask is supplied");
792                for i in 0..len {
793                    if unsafe { mb.get_unchecked(i) } {
794                        sum += data[i] as f64;
795                        cnt += 1;
796                    }
797                }
798            }
799
800            (sum, cnt)
801        }
802    };
803}
804
805/// Macro to implement raw sum/count aggregation for integer types.
806macro_rules! impl_sum_int {
807    ($name:ident, $ty:ty, $acc:ty, $LANES:expr) => {
808        #[doc = concat!("Returns (sum, count) for non-null `", stringify!($ty), "` values.")]
809        #[doc = concat!(" Accumulates using `", stringify!($acc), "`. Uses SIMD if available.")]
810        #[inline(always)]
811        pub fn $name(
812            data: &[$ty],
813            mask: Option<&Bitmask>,
814            null_count: Option<usize>,
815        ) -> ($acc, usize) {
816            let has_nulls = has_nulls(null_count, mask);
817
818            #[cfg(feature = "simd")]
819            {
820                // Check if both arrays are 64-byte aligned for SIMD
821                if is_simd_aligned(data) {
822                    const N: usize = $LANES;
823                    type V<const L: usize> = Simd<$ty, L>;
824                    type M<const L: usize> = <$ty as SimdElement>::Mask;
825
826                    let len = data.len();
827                    let mut i = 0;
828                    let mut sum: $acc = 0;
829                    let mut cnt = 0_usize;
830
831                    if !has_nulls {
832                        // dense path
833                        while i + N <= len {
834                            let v = V::<N>::from_slice(&data[i..i + N]);
835                            sum += v.reduce_sum() as $acc;
836                            cnt += N;
837                            i += N;
838                        }
839                        // tail
840                        for &x in &data[i..] {
841                            sum += x as $acc;
842                            cnt += 1;
843                        }
844                    } else {
845                        // null‐aware path
846                        let mb = mask.expect(
847                            "Bitmask must be Some if nulls are present or mask is supplied",
848                        );
849                        let bytes = mb.as_bytes();
850
851                        while i + N <= len {
852                            let v = V::<N>::from_slice(&data[i..i + N]);
853                            // extract the SIMD validity mask
854                            let lane_m: Mask<M<N>, N> =
855                                bitmask_to_simd_mask::<N, M<N>>(bytes, i, len);
856                            // zero‐out invalid lanes
857                            let vv = lane_m.select(v, V::<N>::splat(0 as $ty));
858                            sum += vv.reduce_sum() as $acc;
859                            // count ones in the mask
860                            cnt += lane_m.to_bitmask().count_ones() as usize;
861                            i += N;
862                        }
863                        // scalar tail
864                        for idx in i..len {
865                            if unsafe { mb.get_unchecked(idx) } {
866                                sum += data[idx] as $acc;
867                                cnt += 1;
868                            }
869                        }
870                    }
871
872                    return (sum, cnt);
873                }
874                // Fall through to scalar path if alignment check failed
875            }
876
877            // Scalar fallback - alignment check failed
878            let mut sum: $acc = 0;
879            let mut cnt = 0_usize;
880            let len = data.len();
881
882            if !has_nulls {
883                for &x in data {
884                    sum += x as $acc;
885                    cnt += 1;
886                }
887            } else {
888                let mb =
889                    mask.expect("Bitmask must be Some if nulls are present or mask is supplied");
890                for i in 0..len {
891                    if unsafe { mb.get_unchecked(i) } {
892                        sum += data[i] as $acc;
893                        cnt += 1;
894                    }
895                }
896            }
897
898            (sum, cnt)
899        }
900    };
901}
902
903/// Computes Σx² for `$ty` values, skipping nulls.  
904/// SIMD‐accelerated when available.
905macro_rules! impl_sum2_float {
906    ($fn_name:ident, $ty:ty, $LANES:expr) => {
907        #[doc = concat!("Computes sum-of-squares for `", stringify!($ty), "` (Σx²).")]
908        #[doc = " Skips nulls if present. SIMD‐enabled."]
909        #[inline(always)]
910        pub fn $fn_name(data: &[$ty], mask: Option<&Bitmask>, null_count: Option<usize>) -> f64 {
911            let has_nulls = has_nulls(null_count, mask);
912
913            #[cfg(feature = "simd")]
914            {
915                // Check if both arrays are 64-byte aligned for SIMD
916                if is_simd_aligned(data) {
917                    // Vector type for the original element type
918                    type V<const N: usize> = Simd<$ty, N>;
919                    // Mask type comes from the element’s SimdElement impl
920                    type M<const N: usize> = <$ty as SimdElement>::Mask;
921
922                    const N: usize = $LANES;
923                    let len = data.len();
924                    let mut i = 0;
925                    let mut s2 = 0.0f64;
926
927                    if !has_nulls {
928                        // Dense path: no nulls
929                        while i + N <= len {
930                            // load N lanes, cast to f64, square & sum
931                            let dv = V::<N>::from_slice(&data[i..i + N]).cast::<f64>();
932                            s2 += (dv * dv).reduce_sum();
933                            i += N;
934                        }
935                        // tail
936                        for &x in &data[i..] {
937                            let y = x as f64;
938                            s2 += y * y;
939                        }
940                    } else {
941                        // Null‐aware path
942                        let mb = mask.expect("mask must be Some if nulls present");
943                        let bytes = mb.as_bytes();
944                        while i + N <= len {
945                            // load original‐type vector
946                            let v = V::<N>::from_slice(&data[i..i + N]);
947                            // extract the same‐type mask
948                            let lane_m: Mask<M<N>, N> =
949                                bitmask_to_simd_mask::<N, M<N>>(bytes, i, len);
950                            // zero out null lanes *before* the cast
951                            let vv = lane_m.select(v, V::<N>::splat(0 as $ty));
952                            // now safe to cast to f64 and sum squares
953                            let dv = vv.cast::<f64>();
954                            s2 += (dv * dv).reduce_sum();
955                            i += N;
956                        }
957                        // tail
958                        for idx in i..len {
959                            if unsafe { mb.get_unchecked(idx) } {
960                                let y = data[idx] as f64;
961                                s2 += y * y;
962                            }
963                        }
964                    }
965                    return s2;
966                }
967                // Fall through to scalar path if alignment check failed
968            }
969
970            // Scalar fallback - alignment check failed
971            let mut s2 = 0.0f64;
972            if !has_nulls {
973                for &x in data {
974                    let y = x as f64;
975                    s2 += y * y;
976                }
977            } else {
978                let mb = mask.expect("mask must be Some if nulls present");
979                for (i, &x) in data.iter().enumerate() {
980                    if unsafe { mb.get_unchecked(i) } {
981                        let y = x as f64;
982                        s2 += y * y;
983                    }
984                }
985            }
986            s2
987        }
988    };
989}
990
991/// Macro to implement null-aware element counting.
992macro_rules! impl_count {
993    ($fn_name:ident) => {
994        #[doc = "Returns the number of valid (non-null) elements."]
995        #[inline(always)]
996        pub fn $fn_name(len: usize, mask: Option<&Bitmask>, null_count: Option<usize>) -> usize {
997            match null_count {
998                Some(n) => len - n,
999                None => match mask {
1000                    Some(m) => m.count_ones(),
1001                    None => len,
1002                },
1003            }
1004        }
1005    };
1006}
1007
1008/// Macro to implement sum and average aggregations for float/int types.
1009/// Also handles raw (sum, count) cases internally.
1010macro_rules! impl_sum_avg {
1011    // floats ---------------------------------------------------
1012    (float $ty:ident, $sum_fn:ident, $avg_fn:ident, $raw:ident) => {
1013        #[doc = concat!("Returns the sum of `", stringify!($ty), "` values, or None if all-null.")]
1014        #[inline(always)]
1015        pub fn $sum_fn(d: &[$ty], m: Option<&Bitmask>, null_count: Option<usize>) -> Option<f64> {
1016            let (s, n) = $raw(d, m, null_count);
1017            if n == 0 { None } else { Some(s) }
1018        }
1019
1020        #[doc = concat!("Returns the average of `", stringify!($ty), "` values, or None if all-null.")]
1021        #[inline(always)]
1022        pub fn $avg_fn(d: &[$ty], m: Option<&Bitmask>, null_count: Option<usize>) -> Option<f64> {
1023            let (s, n) = $raw(d, m, null_count);
1024            if n == 0 { None } else { Some(s / n as f64) }
1025        }
1026    };
1027
1028    // ints -----------------------------------------------------
1029    (int $ty:ident, $sum_fn:ident, $avg_fn:ident, $acc:ty, $raw:ident) => {
1030        #[doc = concat!("Returns the sum of `", stringify!($ty), "` values, or None if all-null.")]
1031        #[inline(always)]
1032        pub fn $sum_fn(d: &[$ty], m: Option<&Bitmask>, null_count: Option<usize>) -> Option<$acc> {
1033            let (s, n) = $raw(d, m, null_count);
1034            if n == 0 { None } else { Some(s) }
1035        }
1036
1037        #[doc = concat!("Returns the average of `", stringify!($ty), "` values, or None if all-null.")]
1038        #[inline(always)]
1039        pub fn $avg_fn(d: &[$ty], m: Option<&Bitmask>, null_count: Option<usize>) -> Option<f64> {
1040            let (s, n) = $raw(d, m, null_count);
1041            if n == 0 { None } else { Some(s as f64 / n as f64) }
1042        }
1043    };
1044}
1045
1046macro_rules! impl_stat_moments4_float {
1047    ($fn_name:ident, $ty:ty, $LANES:expr) => {
1048        #[doc = concat!(
1049                                    "SIMD-accelerated (∑x … ∑x⁴) for `", stringify!($ty), "`.\n",
1050                                    "Returns `(s1,s2,s3,s4,n)` as `(f64,f64,f64,f64,usize)`."
1051                                )]
1052        #[inline(always)]
1053        pub fn $fn_name(
1054            data: &[$ty],
1055            mask: Option<&Bitmask>,
1056            null_count: Option<usize>,
1057        ) -> (f64, f64, f64, f64, usize) {
1058            let has_nulls = has_nulls(null_count, mask);
1059
1060            #[cfg(feature = "simd")]
1061            {
1062                // Check if both arrays are 64-byte aligned for SIMD
1063                if is_simd_aligned(data) {
1064                    type V<const L: usize> = Simd<$ty, L>;
1065                    type M<const L: usize> = <$ty as SimdElement>::Mask;
1066
1067                    const N: usize = $LANES;
1068                    let len = data.len();
1069                    let mut i = 0usize;
1070
1071                    let mut s1 = 0.0;
1072                    let mut s2 = 0.0;
1073                    let mut s3 = 0.0;
1074                    let mut s4 = 0.0;
1075                    let mut n = 0usize;
1076
1077                    if !has_nulls {
1078                        // Dense, no nulls
1079                        while i + N <= len {
1080                            let v = V::<N>::from_slice(&data[i..i + N]).cast::<f64>();
1081                            let v2 = v * v;
1082                            s1 += v.reduce_sum();
1083                            s2 += v2.reduce_sum();
1084                            s3 += (v2 * v).reduce_sum();
1085                            s4 += (v2 * v2).reduce_sum();
1086                            n += N;
1087                            i += N;
1088                        }
1089                        for &x in &data[i..] {
1090                            let xf = x as f64;
1091                            s1 += xf;
1092                            let x2 = xf * xf;
1093                            s2 += x2;
1094                            s3 += x2 * xf;
1095                            s4 += x2 * x2;
1096                            n += 1;
1097                        }
1098                    } else {
1099                        // Null-aware SIMD
1100                        let mb = mask.expect("mask must be Some when nulls present");
1101                        let mask_bytes = mb.as_bytes();
1102                        while i + N <= len {
1103                            let v_raw = V::<N>::from_slice(&data[i..i + N]);
1104                            let lane_m: Mask<M<N>, N> =
1105                                bitmask_to_simd_mask::<N, M<N>>(mask_bytes, i, len);
1106                            if !lane_m.any() {
1107                                i += N;
1108                                continue;
1109                            }
1110                            // Zero out nulls
1111                            let v = lane_m.select(v_raw, V::<N>::splat(0 as $ty)).cast::<f64>();
1112                            let v2 = v * v;
1113                            s1 += v.reduce_sum();
1114                            s2 += v2.reduce_sum();
1115                            s3 += (v2 * v).reduce_sum();
1116                            s4 += (v2 * v2).reduce_sum();
1117                            n += lane_m.to_bitmask().count_ones() as usize;
1118                            i += N;
1119                        }
1120                        for j in i..len {
1121                            if unsafe { mb.get_unchecked(j) } {
1122                                let xf = data[j] as f64;
1123                                s1 += xf;
1124                                let x2 = xf * xf;
1125                                s2 += x2;
1126                                s3 += x2 * xf;
1127                                s4 += x2 * x2;
1128                                n += 1;
1129                            }
1130                        }
1131                    }
1132                    return (s1, s2, s3, s4, n);
1133                }
1134                // Fall through to scalar path if alignment check failed
1135            }
1136
1137            // Scalar fallback - alignment check failed
1138            let mut s1 = 0.0;
1139            let mut s2 = 0.0;
1140            let mut s3 = 0.0;
1141            let mut s4 = 0.0;
1142            let mut n = 0usize;
1143
1144            if !has_nulls {
1145                for &x in data {
1146                    let xf = x as f64;
1147                    s1 += xf;
1148                    let x2 = xf * xf;
1149                    s2 += x2;
1150                    s3 += x2 * xf;
1151                    s4 += x2 * x2;
1152                    n += 1;
1153                }
1154            } else {
1155                let mb = mask.expect("mask must be Some when nulls present");
1156                for (idx, &x) in data.iter().enumerate() {
1157                    if unsafe { mb.get_unchecked(idx) } {
1158                        let xf = x as f64;
1159                        s1 += xf;
1160                        let x2 = xf * xf;
1161                        s2 += x2;
1162                        s3 += x2 * xf;
1163                        s4 += x2 * x2;
1164                        n += 1;
1165                    }
1166                }
1167            }
1168            (s1, s2, s3, s4, n)
1169        }
1170    };
1171}
1172
1173macro_rules! impl_stat_moments4_int {
1174    ($fn_name:ident, $ty:ty, $acc:ty, $LANES:expr) => {
1175        #[doc = concat!(
1176                    "SIMD-accelerated (∑x … ∑x⁴) for `", stringify!($ty), "` (promoted to f64).\n",
1177                    "Accumulator type for scalar paths: `", stringify!($acc), "`."
1178                )]
1179        #[inline(always)]
1180        pub fn $fn_name(
1181            data: &[$ty],
1182            mask: Option<&Bitmask>,
1183            null_count: Option<usize>
1184        ) -> (f64, f64, f64, f64, usize) {
1185            let has_nulls = has_nulls(null_count, mask);
1186
1187            #[cfg(feature = "simd")]
1188            {
1189                // Check if both arrays are 64-byte aligned for SIMD
1190                if is_simd_aligned(data) {
1191                type V<const L: usize> = Simd<$ty, L>;
1192                type M<const L: usize> = <$ty as SimdElement>::Mask;
1193
1194                const N: usize = $LANES;
1195                let len = data.len();
1196                let mut i = 0usize;
1197
1198                let mut s1 = 0.0;
1199                let mut s2 = 0.0;
1200                let mut s3 = 0.0;
1201                let mut s4 = 0.0;
1202                let mut n = 0usize;
1203
1204                if !has_nulls {
1205                    while i + N <= len {
1206                        let v_i = V::<N>::from_slice(&data[i..i + N]);
1207                        let v_f = v_i.cast::<f64>();
1208                        let v2 = v_f * v_f;
1209                        s1 += v_f.reduce_sum();
1210                        s2 += v2.reduce_sum();
1211                        s3 += (v2 * v_f).reduce_sum();
1212                        s4 += (v2 * v2).reduce_sum();
1213                        n += N;
1214                        i += N;
1215                    }
1216                    for &x in &data[i..] {
1217                        let xf = x as f64;
1218                        s1 += xf;
1219                        let x2 = xf * xf;
1220                        s2 += x2;
1221                        s3 += x2 * xf;
1222                        s4 += x2 * x2;
1223                        n += 1;
1224                    }
1225                } else {
1226                    let mb = mask.expect("mask must be Some when nulls present");
1227                    let mask_bytes = mb.as_bytes();
1228                    while i + N <= len {
1229                        let v_i = V::<N>::from_slice(&data[i..i + N]);
1230                        let lane_m: Mask<M<N>, N> =
1231                            bitmask_to_simd_mask::<N, M<N>>(mask_bytes, i, len);
1232                        if !lane_m.any() {
1233                            i += N;
1234                            continue;
1235                        }
1236                        let v_masked = lane_m.select(v_i, V::<N>::splat(0));
1237                        let v_f = v_masked.cast::<f64>();
1238                        let v2 = v_f * v_f;
1239                        s1 += v_f.reduce_sum();
1240                        s2 += v2.reduce_sum();
1241                        s3 += (v2 * v_f).reduce_sum();
1242                        s4 += (v2 * v2).reduce_sum();
1243                        n += lane_m.to_bitmask().count_ones() as usize;
1244                        i += N;
1245                    }
1246                    for j in i..len {
1247                        if unsafe { mb.get_unchecked(j) } {
1248                            let xf = data[j] as f64;
1249                            let x2 = xf * xf;
1250                            s1 += xf;
1251                            s2 += x2;
1252                            s3 += x2 * xf;
1253                            s4 += x2 * x2;
1254                            n += 1;
1255                        }
1256                    }
1257                }
1258                return (s1, s2, s3, s4, n);
1259                }
1260                // Fall through to scalar path if alignment check failed
1261            }
1262
1263            // Scalar fallback - alignment check failed
1264            // For int types, accumulate in a wide accumulator, then cast
1265            let mut s1: $acc = 0;
1266            let mut s2: $acc = 0;
1267            let mut s3: $acc = 0;
1268            let mut s4: $acc = 0;
1269            let mut n = 0usize;
1270
1271            if !has_nulls {
1272                for &x in data {
1273                    let xi: $acc = x as $acc;
1274                    let x2 = xi * xi;
1275                    s1 += xi;
1276                    s2 += x2;
1277                    s3 += x2 * xi;
1278                    s4 += x2 * x2;
1279                    n += 1;
1280                }
1281            } else {
1282                let mb = mask.expect("mask must be Some when nulls present");
1283                for (idx, &x) in data.iter().enumerate() {
1284                    if unsafe { mb.get_unchecked(idx) } {
1285                        let xi: $acc = x as $acc;
1286                        let x2 = xi * xi;
1287                        s1 += xi;
1288                        s2 += x2;
1289                        s3 += x2 * xi;
1290                        s4 += x2 * x2;
1291                        n += 1;
1292                    }
1293                }
1294            }
1295            (s1 as f64, s2 as f64, s3 as f64, s4 as f64, n)
1296        }
1297    };
1298}
1299
1300// Weighted-Sum-2  &  Pair-Stats kernels
1301macro_rules! impl_weighted_sum2_float {
1302    ($fn:ident, $ty:ty, $LANES:expr) => {
1303        #[doc = concat!(
1304                                    "Returns `(∑w·x, ∑w, ∑w·x², rows)` for `", stringify!($ty),
1305                                    "` slices `vals`, `wts` with optional null mask."
1306                                )]
1307        #[inline(always)]
1308        pub fn $fn(
1309            vals: &[$ty],
1310            wts: &[$ty],
1311            mask: Option<&Bitmask>,
1312            null_count: Option<usize>,
1313        ) -> (f64, f64, f64, usize) {
1314            debug_assert_eq!(vals.len(), wts.len(), "weighted_sum2x: len mismatch");
1315            let has_nulls = has_nulls(null_count, mask);
1316
1317            #[cfg(feature = "simd")]
1318            {
1319                // Check if both arrays are 64-byte aligned for SIMD
1320                if is_simd_aligned(vals) && is_simd_aligned(wts) {
1321                    type V<const L: usize> = Simd<$ty, L>;
1322                    type M<const L: usize> = <$ty as SimdElement>::Mask;
1323                    const N: usize = $LANES;
1324
1325                    let len = vals.len();
1326                    let mut i = 0usize;
1327                    let mut swx = 0.0;
1328                    let mut sw = 0.0;
1329                    let mut swx2 = 0.0;
1330                    let mut n = 0usize;
1331
1332                    if !has_nulls {
1333                        while i + N <= len {
1334                            let v = V::<N>::from_slice(&vals[i..i + N]).cast::<f64>();
1335                            let w = V::<N>::from_slice(&wts[i..i + N]).cast::<f64>();
1336                            swx += (v * w).reduce_sum();
1337                            sw += w.reduce_sum();
1338                            swx2 += (w * v * v).reduce_sum();
1339                            n += N;
1340                            i += N;
1341                        }
1342                        for j in i..len {
1343                            let vf = vals[j] as f64;
1344                            let wf = wts[j] as f64;
1345                            swx += wf * vf;
1346                            sw += wf;
1347                            swx2 += wf * vf * vf;
1348                            n += 1;
1349                        }
1350                    } else {
1351                        let mb = mask.expect("mask = Some when nulls present");
1352                        let mask_bytes = mb.as_bytes();
1353                        while i + N <= len {
1354                            let v_raw = V::<N>::from_slice(&vals[i..i + N]);
1355                            let w_raw = V::<N>::from_slice(&wts[i..i + N]);
1356                            let lane_m: Mask<M<N>, N> =
1357                                bitmask_to_simd_mask::<N, M<N>>(mask_bytes, i, len);
1358                            if !lane_m.any() {
1359                                i += N;
1360                                continue;
1361                            }
1362                            let v = lane_m.select(v_raw, V::<N>::splat(0.0)).cast::<f64>();
1363                            let w = lane_m.select(w_raw, V::<N>::splat(0.0)).cast::<f64>();
1364                            swx += (v * w).reduce_sum();
1365                            sw += w.reduce_sum();
1366                            swx2 += (w * v * v).reduce_sum();
1367                            n += lane_m.to_bitmask().count_ones() as usize;
1368                            i += N;
1369                        }
1370                        for j in i..len {
1371                            if unsafe { mb.get_unchecked(j) } {
1372                                let vf = vals[j] as f64;
1373                                let wf = wts[j] as f64;
1374                                swx += wf * vf;
1375                                sw += wf;
1376                                swx2 += wf * vf * vf;
1377                                n += 1;
1378                            }
1379                        }
1380                    }
1381                    return (swx, sw, swx2, n);
1382                }
1383                // Fall through to scalar path if alignment check failed
1384            }
1385
1386            // Scalar fallback - alignment check failed
1387            let mut swx = 0.0;
1388            let mut sw = 0.0;
1389            let mut swx2 = 0.0;
1390            let mut n = 0usize;
1391
1392            if !has_nulls {
1393                for i in 0..vals.len() {
1394                    let vf = vals[i] as f64;
1395                    let wf = wts[i] as f64;
1396                    swx += wf * vf;
1397                    sw += wf;
1398                    swx2 += wf * vf * vf;
1399                    n += 1;
1400                }
1401            } else {
1402                let mb = mask.expect("mask = Some when nulls present");
1403                for i in 0..vals.len() {
1404                    if unsafe { mb.get_unchecked(i) } {
1405                        let vf = vals[i] as f64;
1406                        let wf = wts[i] as f64;
1407                        swx += wf * vf;
1408                        sw += wf;
1409                        swx2 += wf * vf * vf;
1410                        n += 1;
1411                    }
1412                }
1413            }
1414            (swx, sw, swx2, n)
1415        }
1416    };
1417}
1418
1419/// Statistical summary for paired data analysis.
1420///
1421/// Accumulates fundamental statistical quantities for bivariate data pairs,
1422/// providing the building blocks for correlation analysis, regression coefficients,
1423/// and covariance calculations with SIMD-accelerated computation.
1424///
1425/// # Fields
1426/// - **`n`**: Count of valid (non-null) data pairs
1427/// - **`sx`**: Sum of x-values (Σx)
1428/// - **`sy`**: Sum of y-values (Σy)
1429/// - **`sxy`**: Sum of products (Σxy) for covariance calculation
1430/// - **`sx2`**: Sum of x-squared values (Σx²) for variance calculation
1431/// - **`sy2`**: Sum of y-squared values (Σy²) for variance calculation
1432///
1433/// # Applications
1434/// These statistics enable efficient calculation of:
1435/// - **Pearson correlation coefficient**: r = (n⋅Σxy - Σx⋅Σy) / √[(n⋅Σx² - (Σx)²)(n⋅Σy² - (Σy)²)]
1436/// - **Linear regression slope**: β₁ = (n⋅Σxy - Σx⋅Σy) / (n⋅Σx² - (Σx)²)
1437/// - **Sample covariance**: cov(x,y) = (Σxy - n⋅x̄⋅ȳ) / (n-1)
1438/// - **Coefficient of determination**: R² for regression analysis
1439#[derive(Default)]
1440pub struct PairStats {
1441    /// Count of valid (non-null) paired observations
1442    pub n: usize,
1443    /// Sum of x-values (Σx)
1444    pub sx: f64,
1445    /// Sum of y-values (Σy)
1446    pub sy: f64,
1447    /// Sum of cross-products (Σxy) for covariance computation
1448    pub sxy: f64,
1449    /// Sum of squared x-values (Σx²) for variance computation
1450    pub sx2: f64,
1451    /// Sum of squared y-values (Σy²) for variance computation
1452    pub sy2: f64,
1453}
1454
1455// PAIR - Σ stats (x,y)  (float)
1456macro_rules! impl_pair_stats_float {
1457    ($fn:ident, $ty:ty, $LANES:expr) => {
1458        /// Returns PairStats (n, sum_x, sum_y, sum_xy, sum_x2, sum_y2) for floating point types.
1459        #[inline(always)]
1460        pub fn $fn(
1461            xs: &[$ty],
1462            ys: &[$ty],
1463            mask: Option<&Bitmask>,
1464            null_count: Option<usize>
1465        ) -> PairStats {
1466            debug_assert_eq!(xs.len(), ys.len(), "pair_stats: len mismatch ");
1467            let has_nulls = has_nulls(null_count, mask);
1468
1469            #[cfg(feature = "simd")]
1470            {
1471                // Check if both arrays are 64-byte aligned for SIMD
1472                if is_simd_aligned(xs) && is_simd_aligned(ys) {
1473
1474                type V<const L: usize> = Simd<$ty, L>;
1475                type M<const L: usize> = <$ty as SimdElement>::Mask;
1476                const N: usize = $LANES;
1477                let len = xs.len();
1478
1479                let mut i = 0usize;
1480                let mut sx = 0.0;
1481                let mut sy = 0.0;
1482                let mut sxy = 0.0;
1483                let mut sx2 = 0.0;
1484                let mut sy2 = 0.0;
1485                let mut n = 0usize;
1486
1487                if !has_nulls {
1488                    while i + N <= len {
1489                        let vx = V::<N>::from_slice(&xs[i..i + N]).cast::<f64>();
1490                        let vy = V::<N>::from_slice(&ys[i..i + N]).cast::<f64>();
1491
1492                        sx += vx.reduce_sum();
1493                        sy += vy.reduce_sum();
1494                        sxy += (vx * vy).reduce_sum();
1495                        sx2 += (vx * vx).reduce_sum();
1496                        sy2 += (vy * vy).reduce_sum();
1497                        n += N;
1498                        i += N;
1499                    }
1500                    for j in i..len {
1501                        let xf = xs[j] as f64;
1502                        let yf = ys[j] as f64;
1503                        sx += xf;
1504                        sy += yf;
1505                        sxy += xf * yf;
1506                        sx2 += xf * xf;
1507                        sy2 += yf * yf;
1508                        n += 1;
1509                    }
1510                } else {
1511                    let mb = mask.expect("mask Some when nulls present");
1512                    let mask_bytes = mb.as_bytes();
1513
1514                    while i + N <= len {
1515                        let vx_raw = V::<N>::from_slice(&xs[i..i + N]);
1516                        let vy_raw = V::<N>::from_slice(&ys[i..i + N]);
1517                        let lane_m: Mask<M<N>, N> =
1518                            bitmask_to_simd_mask::<N, M<N>>(mask_bytes, i, len);
1519                        if !lane_m.any() {
1520                            i += N;
1521                            continue;
1522                        }
1523
1524                        let vx = lane_m.select(vx_raw, V::<N>::splat(0.0)).cast::<f64>();
1525                        let vy = lane_m.select(vy_raw, V::<N>::splat(0.0)).cast::<f64>();
1526
1527                        sx += vx.reduce_sum();
1528                        sy += vy.reduce_sum();
1529                        sxy += (vx * vy).reduce_sum();
1530                        sx2 += (vx * vx).reduce_sum();
1531                        sy2 += (vy * vy).reduce_sum();
1532                        n += lane_m.to_bitmask().count_ones() as usize;
1533                        i += N;
1534                    }
1535                    for j in i..len {
1536                        if unsafe { mb.get_unchecked(j) } {
1537                            let xf = xs[j] as f64;
1538                            let yf = ys[j] as f64;
1539                            sx += xf;
1540                            sy += yf;
1541                            sxy += xf * yf;
1542                            sx2 += xf * xf;
1543                            sy2 += yf * yf;
1544                            n += 1;
1545                        }
1546                    }
1547                }
1548                return PairStats { n, sx, sy, sxy, sx2, sy2 };
1549                }
1550                // Fall through to scalar path if alignment check failed
1551            }
1552
1553            // Scalar fallback - alignment check failed
1554            let mut sx = 0.0;
1555            let mut sy = 0.0;
1556            let mut sxy = 0.0;
1557            let mut sx2 = 0.0;
1558            let mut sy2 = 0.0;
1559            let mut n = 0usize;
1560
1561            if !has_nulls {
1562                for i in 0..xs.len() {
1563                    let xf = xs[i] as f64;
1564                    let yf = ys[i] as f64;
1565                    sx += xf;
1566                    sy += yf;
1567                    sxy += xf * yf;
1568                    sx2 += xf * xf;
1569                    sy2 += yf * yf;
1570                    n += 1;
1571                }
1572            } else {
1573                let mb = mask.expect("mask Some when nulls present");
1574                for i in 0..xs.len() {
1575                    if unsafe { mb.get_unchecked(i) } {
1576                        let xf = xs[i] as f64;
1577                        let yf = ys[i] as f64;
1578                        sx += xf;
1579                        sy += yf;
1580                        sxy += xf * yf;
1581                        sx2 += xf * xf;
1582                        sy2 += yf * yf;
1583                        n += 1;
1584                    }
1585                }
1586            }
1587            PairStats { n, sx, sy, sxy, sx2, sy2 }
1588        }
1589    };
1590}
1591
1592// 4-moment running-update helper (scalar). Works for any Into<f64> value.
1593#[inline(always)]
1594fn moments4_scalar<I>(iter: I) -> (usize, f64, f64, f64, f64)
1595where
1596    I: IntoIterator<Item = f64>,
1597{
1598    let mut n = 0usize;
1599    let mut m1 = 0.0; // mean
1600    let mut m2 = 0.0; // Σ(x-μ)²
1601    let mut m3 = 0.0; // Σ(x-μ)³
1602    let mut m4 = 0.0; // Σ(x-μ)⁴
1603
1604    for x in iter {
1605        n += 1;
1606        let n_f = n as f64;
1607        let delta = x - m1;
1608        let delta_n = delta / n_f;
1609        let delta_n2 = delta_n * delta_n;
1610        let term1 = delta * delta_n * (n_f - 1.0);
1611
1612        m4 += term1 * delta_n2 * (n_f * n_f - 3.0 * n_f + 3.0) + 6.0 * delta_n2 * m2
1613            - 4.0 * delta_n * m3;
1614
1615        m3 += term1 * delta_n * (n_f - 2.0) - 3.0 * delta_n * m2;
1616        m2 += term1;
1617        m1 += delta_n;
1618    }
1619    (n, m1, m2, m3, m4)
1620}
1621
1622//  Skewness & Kurtosis — float
1623/// Implements skewness and kurtosis calculation functions for floating-point types.
1624macro_rules! impl_skew_kurt_float {
1625    ($ty:ident, $skew_fn:ident, $kurt_fn:ident) => {
1626        /// Calculates the skewness of a floating-point dataset.
1627        /// 
1628        /// Skewness measures the asymmetry of the probability distribution.
1629        /// Positive skewness indicates a longer tail on the right side,
1630        /// negative skewness indicates a longer tail on the left side.
1631        /// 
1632        /// # Arguments
1633        /// 
1634        /// * `d` - The slice of floating-point values
1635        /// * `m` - Optional bitmask for null value handling
1636        /// * `null_count` - Optional count of null values
1637        /// * `adjust_sample_bias` - Whether to apply sample bias correction
1638        /// 
1639        /// # Returns
1640        /// 
1641        /// `Some(skewness)` if calculation is possible, `None` if insufficient data
1642        #[inline(always)]
1643        pub fn $skew_fn(
1644            d: &[$ty],
1645            m: Option<&Bitmask>,
1646            null_count: Option<usize>,
1647            adjust_sample_bias: bool,
1648        ) -> Option<f64> {
1649            let vals: Vec<f64> = if has_nulls(null_count, m) {
1650                collect_valid(d, m).into_iter().map(|x| x as f64).collect()
1651            } else {
1652                d.iter().map(|&x| x as f64).collect()
1653            };
1654
1655            let (n, _m1, m2, m3, _) = moments4_scalar(vals);
1656            if n < 3 || m2 == 0.0 {
1657                return None;
1658            }
1659            let n_f = n as f64;
1660
1661            // population skewness (fisher-style)
1662            let pop = n_f.sqrt() * m3 / m2.powf(1.5);
1663            if !adjust_sample_bias {
1664                return Some(pop);
1665            }
1666
1667            // unbiased sample skewness (Joanes & Gill)
1668            Some(n_f * (n_f - 1.0).sqrt() * m3 / ((n_f - 2.0) * m2.powf(1.5)))
1669        }
1670
1671        /// Calculates the kurtosis of a floating-point dataset.
1672        /// 
1673        /// Kurtosis measures the "tailedness" of the probability distribution.
1674        /// Higher kurtosis indicates more extreme outliers, lower kurtosis
1675        /// indicates a distribution with lighter tails.
1676        /// 
1677        /// # Arguments
1678        /// 
1679        /// * `d` - The slice of floating-point values
1680        /// * `m` - Optional bitmask for null value handling
1681        /// * `null_count` - Optional count of null values
1682        /// * `adjust_sample_bias` - Whether to apply sample bias correction
1683        /// 
1684        /// # Returns
1685        /// 
1686        /// `Some(kurtosis)` if calculation is possible, `None` if insufficient data
1687        #[inline(always)]
1688        pub fn $kurt_fn(
1689            d: &[$ty],
1690            m: Option<&Bitmask>,
1691            null_count: Option<usize>,
1692            adjust_sample_bias: bool,
1693        ) -> Option<f64> {
1694            let vals: Vec<f64> = if has_nulls(null_count, m) {
1695                collect_valid(d, m).into_iter().map(|x| x as f64).collect()
1696            } else {
1697                d.iter().map(|&x| x as f64).collect()
1698            };
1699
1700            let (n, _m1, m2, _m3, m4) = moments4_scalar(vals);
1701            if n < 2 || m2 == 0.0 {
1702                return None;
1703            }
1704            let n_f = n as f64;
1705            let pop_excess = n_f * m4 / (m2 * m2) - 3.0;
1706            if !adjust_sample_bias {
1707                return Some(pop_excess);
1708            }
1709
1710            // n < 4 → unbiased formula undefined; fall back to population value
1711            if n < 4 {
1712                return Some(pop_excess);
1713            }
1714
1715            // unbiased sample excess kurtosis (Joanes & Gill)
1716            let numerator = (n_f + 1.0) * n_f * (m4 / (m2 * m2)) - 3.0 * (n_f - 1.0);
1717            let denominator = (n_f - 2.0) * (n_f - 3.0);
1718            Some(numerator * (n_f - 1.0) / denominator)
1719        }
1720    };
1721}
1722
1723//  Skewness & Kurtosis — Integer
1724/// Implements skewness and kurtosis calculation functions for integer types.
1725macro_rules! impl_skew_kurt_int {
1726    ($ty:ident, $skew_fn:ident, $kurt_fn:ident) => {
1727        /// Calculates the skewness of an integer dataset.
1728        /// 
1729        /// Skewness measures the asymmetry of the probability distribution.
1730        /// Values are converted to f64 for calculation to maintain precision.
1731        /// 
1732        /// # Arguments
1733        /// 
1734        /// * `d` - The slice of integer values
1735        /// * `m` - Optional bitmask for null value handling
1736        /// * `null_count` - Optional count of null values
1737        /// * `adjust_sample_bias` - Whether to apply sample bias correction
1738        /// 
1739        /// # Returns
1740        /// 
1741        /// `Some(skewness)` if calculation is possible, `None` if insufficient data
1742        #[inline(always)]
1743        pub fn $skew_fn(
1744            d: &[$ty],
1745            m: Option<&Bitmask>,
1746            null_count: Option<usize>,
1747            adjust_sample_bias: bool,
1748        ) -> Option<f64> {
1749            let vals: Vec<f64> = if has_nulls(null_count, m) {
1750                collect_valid(d, m).into_iter().map(|x| x as f64).collect()
1751            } else {
1752                d.iter().map(|&x| x as f64).collect()
1753            };
1754
1755            let (n, _m1, m2, m3, _) = moments4_scalar(vals);
1756            if n < 3 || m2 == 0.0 {
1757                return None;
1758            }
1759            let n_f = n as f64;
1760
1761            let pop = n_f.sqrt() * m3 / m2.powf(1.5);
1762            if !adjust_sample_bias {
1763                return Some(pop);
1764            }
1765
1766            Some(n_f * (n_f - 1.0).sqrt() * m3 / ((n_f - 2.0) * m2.powf(1.5)))
1767        }
1768
1769        /// Calculates the kurtosis of an integer dataset.
1770        /// 
1771        /// Kurtosis measures the "tailedness" of the probability distribution.
1772        /// Values are converted to f64 for calculation to maintain precision.
1773        /// 
1774        /// # Arguments
1775        /// 
1776        /// * `d` - The slice of integer values
1777        /// * `m` - Optional bitmask for null value handling
1778        /// * `null_count` - Optional count of null values
1779        /// * `adjust_sample_bias` - Whether to apply sample bias correction
1780        /// 
1781        /// # Returns
1782        /// 
1783        /// `Some(kurtosis)` if calculation is possible, `None` if insufficient data
1784        #[inline(always)]
1785        pub fn $kurt_fn(
1786            d: &[$ty],
1787            m: Option<&Bitmask>,
1788            null_count: Option<usize>,
1789            adjust_sample_bias: bool,
1790        ) -> Option<f64> {
1791            let vals: Vec<f64> = if has_nulls(null_count, m) {
1792                collect_valid(d, m).into_iter().map(|x| x as f64).collect()
1793            } else {
1794                d.iter().map(|&x| x as f64).collect()
1795            };
1796
1797            let (n, _m1, m2, _m3, m4) = moments4_scalar(vals);
1798            if n < 2 || m2 == 0.0 {
1799                return None;
1800            }
1801            let n_f = n as f64;
1802
1803            let pop_excess = n_f * m4 / (m2 * m2) - 3.0;
1804            if !adjust_sample_bias {
1805                return Some(pop_excess);
1806            }
1807            if n < 4 {
1808                return Some(pop_excess);
1809            }
1810
1811            let numerator = (n_f + 1.0) * n_f * (m4 / (m2 * m2)) - 3.0 * (n_f - 1.0);
1812            let denominator = (n_f - 2.0) * (n_f - 3.0);
1813            Some(numerator * (n_f - 1.0) / denominator)
1814        }
1815    };
1816}
1817
1818/// Computes the sum of squares (Σx²).
1819/// Kernel: SumP2 (L1-2)
1820#[inline(always)]
1821pub fn sum_squares(v: &[f64], mask: Option<&Bitmask>, null_count: Option<usize>) -> f64 {
1822    let has_nulls = has_nulls(null_count, mask);
1823
1824    #[cfg(feature = "simd")]
1825    if is_simd_aligned(v) {
1826        type V<const L: usize> = Simd<f64, L>;
1827        type M<const L: usize> = <f64 as SimdElement>::Mask;
1828        const N: usize = W64;
1829        let len = v.len();
1830        let mut i = 0;
1831        let mut acc = 0.0f64;
1832        if !has_nulls {
1833            while i + N <= len {
1834                let simd_v = V::<N>::from_slice(&v[i..i + N]);
1835                acc += (simd_v * simd_v).reduce_sum();
1836                i += N;
1837            }
1838            for &x in &v[i..] {
1839                acc += x * x;
1840            }
1841        } else {
1842            let mb = mask.expect("sum_squares: mask must be Some when nulls are present");
1843            let mask_bytes = mb.as_bytes();
1844            while i + N <= len {
1845                let simd_v = V::<N>::from_slice(&v[i..i + N]);
1846                let lane_m: Mask<M<N>, N> = bitmask_to_simd_mask::<N, M<N>>(mask_bytes, i, len);
1847                let simd_v_masked = lane_m.select(simd_v, V::<N>::splat(0.0));
1848                acc += (simd_v_masked * simd_v_masked).reduce_sum();
1849                i += N;
1850            }
1851            for j in i..len {
1852                if unsafe { mb.get_unchecked(j) } {
1853                    acc += v[j] * v[j];
1854                }
1855            }
1856        }
1857        return acc;
1858    }
1859
1860    // Scalar fallback - alignment check failed or no simd flag
1861    let mut acc = 0.0f64;
1862    if !has_nulls {
1863        for &x in v {
1864            acc += x * x;
1865        }
1866    } else {
1867        let mb = mask.expect("sum_squares: mask must be Some when nulls are present");
1868        for (j, &x) in v.iter().enumerate() {
1869            if unsafe { mb.get_unchecked(j) } {
1870                acc += x * x;
1871            }
1872        }
1873    }
1874    acc
1875}
1876
1877/// Computes the sum of cubes (Σx³).
1878/// Kernel: SumP3 (L1-3)
1879#[inline(always)]
1880pub fn sum_cubes(v: &[f64], mask: Option<&Bitmask>, null_count: Option<usize>) -> f64 {
1881    let has_nulls = has_nulls(null_count, mask);
1882
1883    #[cfg(feature = "simd")]
1884    if is_simd_aligned(v) {
1885        type V<const L: usize> = Simd<f64, L>;
1886        type M<const L: usize> = <f64 as SimdElement>::Mask;
1887        const N: usize = W64;
1888        let len = v.len();
1889        let mut i = 0;
1890        let mut acc = 0.0f64;
1891        let mut found_infinite = false;
1892
1893        if !has_nulls {
1894            while i + N <= len {
1895                let simd_v = V::<N>::from_slice(&v[i..i + N]);
1896                let simd_v2 = simd_v * simd_v;
1897                let simd_cubes = simd_v2 * simd_v;
1898                // SIMD horizontal check for infinities
1899                let infinite_mask = simd_cubes.is_infinite();
1900                if infinite_mask.any() {
1901                    found_infinite = true;
1902                }
1903                acc += simd_cubes.reduce_sum();
1904                i += N;
1905            }
1906            for &x in &v[i..] {
1907                let cube = x * x * x;
1908                if cube.is_infinite() {
1909                    found_infinite = true;
1910                }
1911                acc += cube;
1912            }
1913        } else {
1914            let mb = mask.expect("sum_cubes: mask must be Some when nulls are present");
1915            let mask_bytes = mb.as_bytes();
1916            while i + N <= len {
1917                let simd_v = V::<N>::from_slice(&v[i..i + N]);
1918                let lane_m: Mask<M<N>, N> = bitmask_to_simd_mask::<N, M<N>>(mask_bytes, i, len);
1919                let simd_v_masked = lane_m.select(simd_v, V::<N>::splat(0.0));
1920                let simd_v2 = simd_v_masked * simd_v_masked;
1921                let simd_cubes = simd_v2 * simd_v_masked;
1922                let infinite_mask = simd_cubes.is_infinite();
1923                if infinite_mask.any() {
1924                    found_infinite = true;
1925                }
1926                acc += simd_cubes.reduce_sum();
1927                i += N;
1928            }
1929            for j in i..len {
1930                if unsafe { mb.get_unchecked(j) } {
1931                    let cube = v[j] * v[j] * v[j];
1932                    if cube.is_infinite() {
1933                        found_infinite = true;
1934                    }
1935                    acc += cube;
1936                }
1937            }
1938        }
1939        return if acc.is_nan() && found_infinite {
1940            f64::INFINITY
1941        } else {
1942            acc
1943        };
1944    }
1945
1946    // Scalar fallback - alignment check failed or no simd flag
1947    let mut acc = 0.0f64;
1948    let mut found_infinite = false;
1949    if !has_nulls {
1950        for &x in v {
1951            let cube = x * x * x;
1952            if cube.is_infinite() {
1953                found_infinite = true;
1954            }
1955            acc += cube;
1956        }
1957    } else {
1958        let mb = mask.expect("sum_cubes: mask must be Some when nulls are present");
1959        for (j, &x) in v.iter().enumerate() {
1960            if unsafe { mb.get_unchecked(j) } {
1961                let cube = x * x * x;
1962                if cube.is_infinite() {
1963                    found_infinite = true;
1964                }
1965                acc += cube;
1966            }
1967        }
1968    }
1969    if acc.is_nan() && found_infinite {
1970        f64::INFINITY
1971    } else {
1972        acc
1973    }
1974}
1975
1976/// Computes the sum of fourth powers (Σx⁴).
1977/// Kernel: SumP4 (L1-4)
1978#[inline(always)]
1979pub fn sum_quartics(v: &[f64], mask: Option<&Bitmask>, null_count: Option<usize>) -> f64 {
1980    let has_nulls = has_nulls(null_count, mask);
1981
1982    #[cfg(feature = "simd")]
1983    if is_simd_aligned(v) {
1984        type V<const L: usize> = Simd<f64, L>;
1985        type M<const L: usize> = <f64 as SimdElement>::Mask;
1986        const N: usize = W64;
1987        let len = v.len();
1988        let mut i = 0;
1989        let mut acc = 0.0f64;
1990        if !has_nulls {
1991            while i + N <= len {
1992                let simd_v = V::<N>::from_slice(&v[i..i + N]);
1993                let simd_v2 = simd_v * simd_v;
1994                acc += (simd_v2 * simd_v2).reduce_sum();
1995                i += N;
1996            }
1997            for &x in &v[i..] {
1998                let x2 = x * x;
1999                acc += x2 * x2;
2000            }
2001        } else {
2002            let mb = mask.expect("sum_quartics: mask must be Some when nulls are present");
2003            let mask_bytes = mb.as_bytes();
2004            while i + N <= len {
2005                let simd_v = V::<N>::from_slice(&v[i..i + N]);
2006                let lane_m: Mask<M<N>, N> = bitmask_to_simd_mask::<N, M<N>>(mask_bytes, i, len);
2007                let simd_v_masked = lane_m.select(simd_v, V::<N>::splat(0.0));
2008                let simd_v2 = simd_v_masked * simd_v_masked;
2009                acc += (simd_v2 * simd_v2).reduce_sum();
2010                i += N;
2011            }
2012            for j in i..len {
2013                if unsafe { mb.get_unchecked(j) } {
2014                    let x2 = v[j] * v[j];
2015                    acc += x2 * x2;
2016                }
2017            }
2018        }
2019        return acc;
2020    }
2021
2022    // Scalar fallback - alignment check failed or no simd flag
2023    let mut acc = 0.0f64;
2024    if !has_nulls {
2025        for &x in v {
2026            let x2 = x * x;
2027            acc += x2 * x2;
2028        }
2029    } else {
2030        let mb = mask.expect("sum_quartics: mask must be Some when nulls are present");
2031        for (j, &x) in v.iter().enumerate() {
2032            if unsafe { mb.get_unchecked(j) } {
2033                let x2 = x * x;
2034                acc += x2 * x2;
2035            }
2036        }
2037    }
2038    acc
2039}
2040
2041// Macro impls per type
2042
2043impl_weighted_sum2_float!(weighted_sum2_f64, f64, W64);
2044impl_weighted_sum2_float!(weighted_sum2_f32, f32, W32);
2045
2046impl_pair_stats_float!(pair_stats_f64, f64, W64);
2047impl_pair_stats_float!(pair_stats_f32, f32, W32);
2048
2049impl_stat_moments4_float!(stat_moments4_f64, f64, W64);
2050impl_stat_moments4_float!(stat_moments4_f32, f32, W32);
2051
2052impl_stat_moments4_int!(stat_moments4_i64, i64, i128, W64);
2053impl_stat_moments4_int!(stat_moments4_u64, u64, u128, W64);
2054impl_stat_moments4_int!(stat_moments4_i32, i32, i128, W32);
2055impl_stat_moments4_int!(stat_moments4_u32, u32, u128, W32);
2056
2057impl_stat_moments_float!(stat_moments_f64, f64, W64);
2058impl_stat_moments_float!(stat_moments_f32, f32, W32);
2059impl_stat_moments_int!(stat_moments_i64, i64, i64, W64, i64);
2060impl_stat_moments_int!(stat_moments_u64, u64, u64, W64, i64);
2061impl_stat_moments_int!(stat_moments_i32, i32, i64, W32, i32);
2062impl_stat_moments_int!(stat_moments_u32, u32, u64, W32, i32);
2063impl_reduce_min_max!(reduce_min_max_i64, i64, W64, i64::MAX, i64::MIN);
2064impl_reduce_min_max!(reduce_min_max_u64, u64, W64, u64::MAX, u64::MIN);
2065impl_reduce_min_max!(reduce_min_max_i32, i32, W32, i32::MAX, i32::MIN);
2066impl_reduce_min_max!(reduce_min_max_u32, u32, W32, u32::MAX, u32::MIN);
2067impl_reduce_min_max_float!(reduce_min_max_f64, f64, W64, Simd, Mask);
2068impl_reduce_min_max_float!(reduce_min_max_f32, f32, W32, Simd, Mask);
2069
2070impl_min_max_range!(f64, reduce_min_max_f64, min_f64, max_f64, range_f64);
2071impl_min_max_range!(f32, reduce_min_max_f32, min_f32, max_f32, range_f32);
2072impl_min_max_range!(i64, reduce_min_max_i64, min_i64, max_i64, range_i64);
2073impl_min_max_range!(u64, reduce_min_max_u64, min_u64, max_u64, range_u64);
2074impl_min_max_range!(i32, reduce_min_max_i32, min_i32, max_i32, range_i32);
2075impl_min_max_range!(u32, reduce_min_max_u32, min_u32, max_u32, range_u32);
2076
2077impl_sum_avg!(float f64, sum_f64, average_f64, sum_f64_raw);
2078impl_sum_avg!(float f32, sum_f32, average_f32, sum_f32_raw);
2079
2080impl_sum_avg!(int  i64, sum_i64, average_i64, i64, sum_i64_raw);
2081impl_sum_avg!(int  u64, sum_u64, average_u64, u64, sum_u64_raw);
2082impl_sum_avg!(int  i32, sum_i32, average_i32, i64 , sum_i32_raw);
2083impl_sum_avg!(int  u32, sum_u32, average_u32, u64 , sum_u32_raw);
2084
2085impl_variance!(f64, variance_f64);
2086impl_variance!(f32, variance_f32);
2087impl_variance!(i64, variance_i64);
2088impl_variance!(u64, variance_u64);
2089impl_variance!(i32, variance_i32);
2090impl_variance!(u32, variance_u32);
2091
2092impl_sum_float!(sum_f64_raw, f64, W64);
2093impl_sum_float!(sum_f32_raw, f32, W32);
2094impl_sum_int!(sum_i64_raw, i64, i64, W64);
2095impl_sum_int!(sum_u64_raw, u64, u64, W64);
2096impl_sum_int!(sum_i32_raw, i32, i64, W32);
2097impl_sum_int!(sum_u32_raw, u32, u64, W32);
2098
2099impl_sum2_float!(sum2_f64_raw, f64, W64);
2100impl_sum2_float!(sum2_f32_raw, f32, W32);
2101
2102impl_count!(count_generic_raw);
2103
2104impl_skew_kurt_float!(f64, skewness_f64, kurtosis_f64);
2105impl_skew_kurt_float!(f32, skewness_f32, kurtosis_f32);
2106
2107impl_skew_kurt_int!(i64, skewness_i64, kurtosis_i64);
2108impl_skew_kurt_int!(u64, skewness_u64, kurtosis_u64);
2109impl_skew_kurt_int!(i32, skewness_i32, kurtosis_i32);
2110impl_skew_kurt_int!(u32, skewness_u32, kurtosis_u32);
2111
2112/// Computes median of a sorted/unsorted slice with optional nulls.
2113#[inline(always)]
2114pub fn median<T: Ord + Copy>(
2115    d: &[T],
2116    m: Option<&Bitmask>,
2117    null_count: Option<usize>,
2118    sort: bool,
2119) -> Option<T> {
2120    let has_nulls = match null_count {
2121        Some(n) => n > 0,
2122        None => m.is_some(),
2123    };
2124
2125    // collect valid values
2126    let mut v = if !has_nulls {
2127        Vec64::from_slice(d)
2128    } else {
2129        collect_valid(d, m)
2130    };
2131
2132    if v.is_empty() {
2133        return None;
2134    }
2135
2136    let len = v.len();
2137    let mid = len / 2;
2138
2139    if len % 2 == 1 {
2140        // odd: middle element
2141        if sort {
2142            let (_, nth, _) = v.select_nth_unstable(mid);
2143            Some(*nth)
2144        } else {
2145            Some(v[mid])
2146        }
2147    } else {
2148        // even: keep the *lower* median when sorted,
2149        // but for the unsorted case pick the element at index mid
2150        if sort {
2151            let (_, nth, _) = v.select_nth_unstable(mid - 1);
2152            Some(*nth)
2153        } else {
2154            Some(v[mid])
2155        }
2156    }
2157}
2158
2159/// Computes median for float types, with optional nulls and sorting.
2160#[inline(always)]
2161pub fn median_f<T: Float + Copy>(
2162    d: &[T],
2163    m: Option<&Bitmask>,
2164    null_count: Option<usize>,
2165    sort: bool,
2166) -> Option<T> {
2167    let has_nulls = match null_count {
2168        Some(n) => n > 0,
2169        None => m.is_some(),
2170    };
2171
2172    let mut v = if !has_nulls {
2173        Vec64::from_slice(d)
2174    } else {
2175        collect_valid(d, m)
2176    };
2177
2178    if v.is_empty() {
2179        return None;
2180    }
2181
2182    let len = v.len();
2183    let mid = len / 2;
2184
2185    if !sort {
2186        return Some(if len & 1 == 0 {
2187            (v[mid - 1] + v[mid]) / T::from(2.0).unwrap()
2188        } else {
2189            v[mid]
2190        });
2191    }
2192
2193    if len & 1 == 1 {
2194        let (_, nth, _) = v.select_nth_unstable_by(mid, total_cmp_f);
2195        Some(*nth)
2196    } else {
2197        let (_, nth_hi, _) = v.select_nth_unstable_by(mid, total_cmp_f);
2198        let hi = *nth_hi;
2199        let (_, nth_lo, _) = v.select_nth_unstable_by(mid - 1, total_cmp_f);
2200        let lo = *nth_lo;
2201        Some((lo + hi) / T::from(2.0).unwrap())
2202    }
2203}
2204/// Computes percentile for ordinal types, optionally sorted.
2205#[inline(always)]
2206pub fn percentile_ord<T: Ord + Copy>(
2207    d: &[T],
2208    m: Option<&Bitmask>,
2209    null_count: Option<usize>,
2210    p: f64,
2211    sort: bool,
2212) -> Option<T> {
2213    let has_nulls = match null_count {
2214        Some(n) => n > 0,
2215        None => m.is_some(),
2216    };
2217
2218    let mut v = if !has_nulls {
2219        Vec64::from_slice(d)
2220    } else {
2221        collect_valid(d, m)
2222    };
2223
2224    if v.is_empty() {
2225        return None;
2226    }
2227
2228    let idx = ((p / 100.0) * (v.len() as f64 - 1.0)).round() as usize;
2229
2230    if !sort {
2231        return v.get(idx).copied();
2232    }
2233
2234    let (_, nth, _) = v.select_nth_unstable(idx);
2235    Some(*nth)
2236}
2237
2238/// Computes percentile for float types, optionally sorted.
2239#[inline(always)]
2240pub fn percentile_f<T: Float + Copy>(
2241    d: &[T],
2242    m: Option<&Bitmask>,
2243    null_count: Option<usize>,
2244    p: f64,
2245    sort: bool,
2246) -> Option<T> {
2247    let has_nulls = match null_count {
2248        Some(n) => n > 0,
2249        None => m.is_some(),
2250    };
2251
2252    let mut v = if !has_nulls {
2253        Vec64::from_slice(d)
2254    } else {
2255        collect_valid(d, m)
2256    };
2257
2258    if v.is_empty() {
2259        return None;
2260    }
2261
2262    let idx = (p.clamp(0.0, 1.0) * (v.len() - 1) as f64).round() as usize;
2263
2264    if !sort {
2265        return v.get(idx).copied();
2266    }
2267
2268    let (_, nth, _) = v.select_nth_unstable_by(idx, total_cmp_f);
2269    Some(*nth)
2270}
2271
2272/// Computes `q` quantiles for ordinal types, optionally sorted.
2273#[inline(always)]
2274pub fn quantile<T: Ord + Copy>(
2275    d: &[T],
2276    m: Option<&Bitmask>,
2277    null_count: Option<usize>,
2278    q: usize,
2279    sort: bool,
2280) -> Option<Vec64<T>> {
2281    if q < 2 {
2282        return None;
2283    }
2284
2285    let has_nulls = match null_count {
2286        Some(n) => n > 0,
2287        None => m.is_some(),
2288    };
2289
2290    let mut v = if !has_nulls {
2291        Vec64::from_slice(d)
2292    } else {
2293        collect_valid(d, m)
2294    };
2295
2296    let n = v.len();
2297    if n == 0 {
2298        return None;
2299    }
2300
2301    if sort {
2302        v.sort_unstable();
2303    }
2304
2305    let mut out = Vec64::with_capacity(q - 1);
2306    unsafe {
2307        out.set_len(q - 1);
2308    }
2309
2310    for k in 1..q {
2311        let p = k as f64 / q as f64;
2312        let h = 1.0 + (n as f64 - 1.0) * p;
2313        let idx = h.floor() as usize;
2314        let idx = idx.saturating_sub(1).min(n - 1);
2315        unsafe {
2316            *out.get_unchecked_mut(k - 1) = v[idx];
2317        }
2318    }
2319
2320    Some(out)
2321}
2322
2323/// Computes `q` quantiles for float types, optionally sorted.
2324#[inline(always)]
2325pub fn quantile_f<T: Float + Copy>(
2326    d: &[T],
2327    m: Option<&Bitmask>,
2328    null_count: Option<usize>,
2329    q: usize,
2330    sort: bool,
2331) -> Option<Vec64<T>> {
2332    if q < 2 {
2333        return None;
2334    }
2335
2336    let has_nulls = match null_count {
2337        Some(n) => n > 0,
2338        None => m.is_some(),
2339    };
2340
2341    let mut v = if !has_nulls {
2342        Vec64::from_slice(d)
2343    } else {
2344        collect_valid(d, m)
2345    };
2346
2347    let n = v.len();
2348    if n == 0 {
2349        return None;
2350    }
2351
2352    if sort {
2353        sort_float(&mut v);
2354    }
2355
2356    let mut out = Vec64::with_capacity(q - 1);
2357    unsafe {
2358        out.set_len(q - 1);
2359    }
2360
2361    for k in 1..q {
2362        let p = k as f64 / q as f64;
2363        let h = 1.0 + (n as f64 - 1.0) * p;
2364        let hf = h.floor();
2365        let hc = h.ceil();
2366
2367        let idx_lo = (hf as usize).saturating_sub(1).min(n - 1);
2368        let idx_hi = (hc as usize).saturating_sub(1).min(n - 1);
2369
2370        let value = if idx_lo == idx_hi {
2371            v[idx_lo]
2372        } else {
2373            let weight = T::from(h - hf).unwrap();
2374            let v_lo = v[idx_lo];
2375            let v_hi = v[idx_hi];
2376            v_lo + (v_hi - v_lo) * weight
2377        };
2378
2379        unsafe {
2380            *out.get_unchecked_mut(k - 1) = value;
2381        }
2382    }
2383
2384    Some(out)
2385}
2386
2387/// Computes interquartile range for ordinal types.
2388#[inline(always)]
2389pub fn iqr<T>(d: &[T], m: Option<&Bitmask>, null_count: Option<usize>, s: bool) -> Option<T>
2390where
2391    T: Ord + Copy + Sub<Output = T>,
2392{
2393    Some(percentile_ord(d, m, null_count, 75.0, s)? - percentile_ord(d, m, null_count, 25.0, s)?)
2394}
2395
2396/// Computes interquartile range for float types.
2397#[inline(always)]
2398pub fn iqr_f<T: Float + Copy>(
2399    d: &[T],
2400    m: Option<&Bitmask>,
2401    null_count: Option<usize>,
2402    s: bool,
2403) -> Option<T> {
2404    Some(percentile_f(d, m, null_count, 0.75, s)? - percentile_f(d, m, null_count, 0.25, s)?)
2405}
2406
2407/// Computes mode (most frequent value) for ordinal types.
2408#[inline(always)]
2409pub fn mode<T: Eq + Hash + Copy>(
2410    d: &[T],
2411    m: Option<&Bitmask>,
2412    null_count: Option<usize>,
2413) -> Option<T> {
2414    let has_nulls = match null_count {
2415        Some(n) => n > 0,
2416        None => m.is_some(),
2417    };
2418
2419    #[cfg(feature = "fast_hash")]
2420    let mut f = AHashMap::with_capacity(d.len());
2421    #[cfg(not(feature = "fast_hash"))]
2422    let mut f = HashMap::with_capacity(d.len());
2423
2424    if !has_nulls {
2425        for &v in d {
2426            *f.entry(v).or_insert(0usize) += 1;
2427        }
2428    } else {
2429        let mb = m.expect("mode: nulls present but mask is None");
2430        for (i, &v) in d.iter().enumerate() {
2431            if unsafe { mb.get_unchecked(i) } {
2432                *f.entry(v).or_insert(0usize) += 1;
2433            }
2434        }
2435    }
2436
2437    f.into_iter().max_by_key(|e| e.1).map(|e| e.0)
2438}
2439
2440/// Computes mode for float types via bit-level equivalence.
2441#[inline(always)]
2442pub fn mode_f<T: ToBits + Copy>(
2443    d: &[T],
2444    m: Option<&Bitmask>,
2445    null_count: Option<usize>,
2446) -> Option<T> {
2447    let has_nulls = match null_count {
2448        Some(n) => n > 0,
2449        None => m.is_some(),
2450    };
2451
2452    #[cfg(feature = "fast_hash")]
2453    let mut f: AHashMap<T::Bits, (usize, T)> = AHashMap::with_capacity(d.len());
2454    #[cfg(not(feature = "fast_hash"))]
2455    let mut f: HashMap<T::Bits, (usize, T)> = HashMap::with_capacity(d.len());
2456
2457    if !has_nulls {
2458        for &v in d {
2459            let e = f.entry(v.to_bits()).or_insert((0, v));
2460            e.0 += 1;
2461        }
2462    } else {
2463        let mb = m.expect("mode_f: nulls present but mask is None");
2464        for (i, &v) in d.iter().enumerate() {
2465            if unsafe { mb.get_unchecked(i) } {
2466                let e = f.entry(v.to_bits()).or_insert((0, v));
2467                e.0 += 1;
2468            }
2469        }
2470    }
2471
2472    f.into_values().max_by_key(|x| x.0).map(|x| x.1)
2473}
2474
2475/// Counts distinct values in an ordinal slice, with optional nulls.
2476#[inline(always)]
2477pub fn count_distinct<T: Eq + Hash + Copy>(
2478    d: &[T],
2479    m: Option<&Bitmask>,
2480    null_count: Option<usize>,
2481) -> usize {
2482    let has_nulls = match null_count {
2483        Some(n) => n > 0,
2484        None => m.is_some(),
2485    };
2486
2487    if !has_nulls {
2488        d.iter().collect::<HashSet<_>>().len()
2489    } else {
2490        let mb = m.expect("count_distinct: nulls present but mask is None");
2491        d.iter()
2492            .enumerate()
2493            .filter(|(i, _)| unsafe { mb.get_unchecked(*i) })
2494            .map(|(_, v)| v)
2495            .collect::<HashSet<_>>()
2496            .len()
2497    }
2498}
2499
2500/// Counts distinct values in a float slice using bitwise equality.
2501#[inline(always)]
2502pub fn count_distinct_f<T: Float + ToBits + Copy>(
2503    d: &[T],
2504    m: Option<&Bitmask>,
2505    null_count: Option<usize>,
2506) -> usize {
2507    let has_nulls = match null_count {
2508        Some(n) => n > 0,
2509        None => m.is_some(),
2510    };
2511
2512    if !has_nulls {
2513        d.iter().map(|v| v.to_bits()).collect::<HashSet<_>>().len()
2514    } else {
2515        let mb = m.expect("count_distinct_f: nulls present but mask is None");
2516        d.iter()
2517            .enumerate()
2518            .filter(|(i, _)| unsafe { mb.get_unchecked(*i) })
2519            .map(|(_, v)| v.to_bits())
2520            .collect::<HashSet<_>>()
2521            .len()
2522    }
2523}
2524
2525/// Computes harmonic mean for integer types, returns `AggResult`.
2526#[inline(always)]
2527pub fn harmonic_mean_int<T: NumCast + Copy + PartialOrd>(
2528    d: &[T],
2529    m: Option<&Bitmask>,
2530    null_count: Option<usize>,
2531) -> Option<f64> {
2532    let has_nulls = match null_count {
2533        Some(n) => n > 0,
2534        None => m.is_some(),
2535    };
2536
2537    let v: Vec<f64> = if !has_nulls {
2538        d.iter().map(|&x| NumCast::from(x).unwrap()).collect()
2539    } else {
2540        collect_valid(d, m)
2541            .into_iter()
2542            .map(|x| NumCast::from(x).unwrap())
2543            .collect()
2544    };
2545
2546    if v.is_empty() || v.iter().any(|&x| x <= 0.0) {
2547        return None;
2548    }
2549
2550    let n = v.len() as f64;
2551    let denom: f64 = v.iter().map(|&x| 1.0 / x).sum();
2552
2553    if denom == 0.0 {
2554        return None;
2555    }
2556
2557    Some(n / denom)
2558}
2559
2560/// Computes harmonic mean for unsigned integers.
2561#[inline(always)]
2562pub fn harmonic_mean_uint<T: NumCast + Copy + PartialOrd>(
2563    d: &[T],
2564    m: Option<&Bitmask>,
2565    null_count: Option<usize>,
2566) -> Option<f64> {
2567    harmonic_mean_int(d, m, null_count)
2568}
2569
2570/// Computes harmonic mean for float types.
2571#[inline(always)]
2572pub fn harmonic_mean_f<T: Float + Into<f64> + Copy>(
2573    d: &[T],
2574    m: Option<&Bitmask>,
2575    null_count: Option<usize>,
2576) -> Option<f64> {
2577    let has_nulls = match null_count {
2578        Some(n) => n > 0,
2579        None => m.is_some(),
2580    };
2581
2582    let (mut s, mut n) = (0.0, 0usize);
2583
2584    if !has_nulls {
2585        for &v in d {
2586            let x = v.into();
2587            if x <= 0.0 {
2588                return None;
2589            }
2590            s += 1.0 / x;
2591            n += 1;
2592        }
2593    } else {
2594        let mb = m.expect("harmonic_mean_f: nulls present but mask is None");
2595        for (i, &v) in d.iter().enumerate() {
2596            if unsafe { mb.get_unchecked(i) } {
2597                let x = v.into();
2598                if x <= 0.0 {
2599                    return None;
2600                }
2601                s += 1.0 / x;
2602                n += 1;
2603            }
2604        }
2605    }
2606
2607    if n == 0 || s == 0.0 {
2608        None
2609    } else {
2610        Some(n as f64 / s)
2611    }
2612}
2613
2614/// Computes geometric mean for integer types.
2615#[inline(always)]
2616pub fn geometric_mean_int<T: ToPrimitive + Copy>(
2617    d: &[T],
2618    m: Option<&Bitmask>,
2619    null_count: Option<usize>,
2620) -> Option<f64> {
2621    let has_nulls = match null_count {
2622        Some(n) => n > 0,
2623        None => m.is_some(),
2624    };
2625
2626    let (mut s, mut n) = (0.0, 0usize);
2627    if !has_nulls {
2628        for &v in d.iter() {
2629            s += v.to_f64().unwrap().ln();
2630            n += 1;
2631        }
2632    } else {
2633        let mb = m.expect("geometric_mean_int: nulls present but mask is None");
2634        for (i, &v) in d.iter().enumerate() {
2635            if unsafe { mb.get_unchecked(i) } {
2636                s += v.to_f64().unwrap().ln();
2637                n += 1;
2638            }
2639        }
2640    }
2641
2642    if n == 0 {
2643        None
2644    } else {
2645        Some((s / n as f64).exp())
2646    }
2647}
2648
2649/// Computes geometric mean for unsigned integer types.
2650#[inline(always)]
2651pub fn geometric_mean_uint<T: ToPrimitive + Copy>(
2652    d: &[T],
2653    m: Option<&Bitmask>,
2654    null_count: Option<usize>,
2655) -> Option<f64> {
2656    geometric_mean_int(d, m, null_count)
2657}
2658
2659/// Computes geometric mean for float types.
2660#[inline(always)]
2661pub fn geometric_mean_f<T: Float + Into<f64> + Copy>(
2662    d: &[T],
2663    m: Option<&Bitmask>,
2664    null_count: Option<usize>,
2665) -> Option<f64> {
2666    let has_nulls = match null_count {
2667        Some(n) => n > 0,
2668        None => m.is_some(),
2669    };
2670
2671    let (mut s, mut n) = (0.0, 0usize);
2672    if !has_nulls {
2673        for &v in d.iter() {
2674            s += v.into().ln();
2675            n += 1;
2676        }
2677    } else {
2678        let mb = m.expect("geometric_mean_f: nulls present but mask is None");
2679        for (i, &v) in d.iter().enumerate() {
2680            if unsafe { mb.get_unchecked(i) } {
2681                s += v.into().ln();
2682                n += 1;
2683            }
2684        }
2685    }
2686
2687    if n == 0 {
2688        None
2689    } else {
2690        Some((s / n as f64).exp())
2691    }
2692}
2693
2694/// Returns the accumulating product of all values
2695#[inline(always)]
2696pub fn agg_product<T: Copy + One + Mul<Output = T> + Zero + PartialEq + 'static>(
2697    data: &[T],
2698    mask: Option<&Bitmask>,
2699    offset: usize,
2700    len: usize,
2701) -> T {
2702    let mut prod = T::one();
2703    match mask {
2704        Some(mask) => {
2705            let mask = mask.slice_clone(offset, len);
2706            for (i, &x) in data[offset..offset + len].iter().enumerate() {
2707                if unsafe { mask.get_unchecked(i) } {
2708                    prod = prod * x;
2709                    if prod == T::zero() {
2710                        break;
2711                    }
2712                }
2713            }
2714        }
2715        None => {
2716            for &x in &data[offset..offset + len] {
2717                prod = prod * x;
2718                if prod == T::zero() {
2719                    break;
2720                }
2721            }
2722        }
2723    }
2724    prod
2725}
2726
2727#[cfg(test)]
2728mod tests {
2729    use minarrow::{Vec64, vec64};
2730    use num_traits::Float;
2731
2732    use super::*;
2733
2734    // Build Arrow-style validity mask from bools (least significant bit is index 0)
2735    fn mask_from_bools(bits: &[bool]) -> Bitmask {
2736        Bitmask::from_bools(bits)
2737    }
2738
2739    // Compare floats robustly (for variance etc)
2740    fn approx_eq(a: f64, b: f64, eps: f64) -> bool {
2741        (a - b).abs() <= eps
2742    }
2743
2744    #[test]
2745    fn test_stat_moments_f64_dense() {
2746        let data = vec64![1.0, 2.0, 3.0, 4.0];
2747        let (sum, sum2, count) = stat_moments_f64(&data, None, None);
2748        assert_eq!(sum, 10.0);
2749        assert_eq!(sum2, 30.0);
2750        assert_eq!(count, 4);
2751    }
2752
2753    #[test]
2754    fn test_stat_moments_f64_masked() {
2755        let data = vec64![1.0, 2.0, 3.0, 4.0];
2756        let mask = mask_from_bools(&[true, false, true, false]);
2757        let (sum, sum2, count) = stat_moments_f64(&data, Some(&mask), Some(2));
2758        assert_eq!(sum, 4.0);
2759        assert_eq!(sum2, 10.0);
2760        assert_eq!(count, 2);
2761    }
2762
2763    #[test]
2764    fn test_stat_moments_i32_dense() {
2765        let data = vec64![1i32, 2, 3, 4];
2766        let (sum, sum2, count) = stat_moments_i32(&data, None, None);
2767        assert_eq!(sum, 10.0);
2768        assert_eq!(sum2, 30.0);
2769        assert_eq!(count, 4);
2770    }
2771
2772    #[test]
2773    fn test_stat_moments_i32_masked() {
2774        let data = vec64![1i32, 2, 3, 4];
2775        let mask = mask_from_bools(&[true, false, false, true]);
2776        let (sum, sum2, count) = stat_moments_i32(&data, Some(&mask), Some(2));
2777        assert_eq!(sum, 5.0);
2778        assert_eq!(sum2, 17.0);
2779        assert_eq!(count, 2);
2780    }
2781
2782    #[test]
2783    fn test_min_max_range_f64_dense() {
2784        let data = vec64![5.0, 3.0, 1.0, 7.0];
2785        assert_eq!(min_f64(&data, None, None), Some(1.0));
2786        assert_eq!(max_f64(&data, None, None), Some(7.0));
2787        assert_eq!(range_f64(&data, None, None), Some((1.0, 7.0)));
2788    }
2789
2790    #[test]
2791    fn test_min_max_range_f64_masked() {
2792        let data = vec64![5.0, 3.0, 1.0, 7.0];
2793        let mask = mask_from_bools(&[false, true, true, false]);
2794        assert_eq!(min_f64(&data, Some(&mask), Some(2)), Some(1.0));
2795        assert_eq!(max_f64(&data, Some(&mask), Some(2)), Some(3.0));
2796        assert_eq!(range_f64(&data, Some(&mask), Some(2)), Some((1.0, 3.0)));
2797    }
2798
2799    #[test]
2800    fn test_sum_and_avg_i32() {
2801        let data = vec64![1i32, 2, 3, 4];
2802        assert_eq!(sum_i32(&data, None, None), Some(10));
2803        assert_eq!(average_i32(&data, None, None), Some(2.5));
2804        let mask = mask_from_bools(&[true, false, true, true]);
2805        assert_eq!(sum_i32(&data, Some(&mask), Some(1)), Some(8));
2806        assert_eq!(average_i32(&data, Some(&mask), Some(1)), Some(8.0 / 3.0));
2807    }
2808
2809    #[test]
2810    fn test_sum_and_avg_f32() {
2811        let data = vec64![1.0f32, 2.0, 3.0, 4.0];
2812        assert_eq!(sum_f32(&data, None, None), Some(10.0));
2813        assert_eq!(average_f32(&data, None, None), Some(2.5));
2814        let mask = mask_from_bools(&[false, true, true, false]);
2815        assert_eq!(sum_f32(&data, Some(&mask), Some(2)), Some(5.0));
2816        assert_eq!(average_f32(&data, Some(&mask), Some(2)), Some(2.5));
2817    }
2818
2819    #[test]
2820    fn test_variance_f64() {
2821        let data = vec64![1.0, 2.0, 3.0, 4.0];
2822        let v = variance_f64(&data, None, None, false);
2823        assert!(approx_eq(v.unwrap(), 1.25, 1e-9));
2824        let mask = mask_from_bools(&[true, true, false, false]);
2825        let v2 = variance_f64(&data, Some(&mask), Some(2), false);
2826        assert!(approx_eq(v2.unwrap(), 0.25, 1e-9));
2827    }
2828
2829    #[test]
2830    fn test_count_generic_raw() {
2831        let n = 100;
2832        let mask = mask_from_bools(&[true; 100]);
2833        assert_eq!(count_generic_raw(n, None, None), 100);
2834        assert_eq!(count_generic_raw(n, Some(&mask), Some(0)), 100);
2835
2836        let mask = mask_from_bools(&(0..n).map(|i| i % 2 == 0).collect::<Vec<_>>());
2837        assert_eq!(count_generic_raw(n, Some(&mask), Some(50)), 50);
2838    }
2839
2840    #[test]
2841    fn test_median_ord_sorted() {
2842        let data = vec64![1i32, 4, 2, 3];
2843        assert_eq!(median(&data, None, None, true), Some(2));
2844    }
2845
2846    #[test]
2847    fn test_median_ord_unsorted() {
2848        let data = vec64![1i32, 4, 2, 3];
2849        // unsorted, even-length ⇒ index mid = 2 ⇒ value 2
2850        assert_eq!(median(&data, None, None, false), Some(2));
2851    }
2852
2853    #[test]
2854    fn test_median_ord_masked() {
2855        let data = vec64![1i32, 4, 2, 3];
2856        let mask = mask_from_bools(&[true, true, false, false]);
2857        assert_eq!(median(&data, Some(&mask), Some(2), true), Some(1));
2858    }
2859
2860    #[test]
2861    fn test_median_f_sorted() {
2862        let dataf = vec64![1.0f64, 2.0, 3.0, 4.0];
2863        assert_eq!(median_f(&dataf, None, None, true), Some(2.5));
2864    }
2865
2866    #[test]
2867    fn test_median_f_masked() {
2868        let dataf = vec64![1.0f64, 2.0, 3.0, 4.0];
2869        let mask = mask_from_bools(&[true, false, true, false]);
2870        assert_eq!(median_f(&dataf, Some(&mask), Some(2), true), Some(2.0));
2871    }
2872
2873    #[test]
2874    fn test_percentile_ord_and_f() {
2875        let data = vec64![10i32, 20, 30, 40, 50];
2876        assert_eq!(percentile_ord(&data, None, None, 0.0, true), Some(10));
2877        assert_eq!(percentile_ord(&data, None, None, 100.0, true), Some(50));
2878        assert_eq!(percentile_ord(&data, None, None, 50.0, true), Some(30));
2879        let mask = mask_from_bools(&[false, true, true, false, true]);
2880        assert_eq!(
2881            percentile_ord(&data, Some(&mask), Some(2), 50.0, true),
2882            Some(30)
2883        );
2884
2885        let dataf = vec64![1.0f64, 2.0, 3.0, 4.0, 5.0];
2886        assert_eq!(percentile_f(&dataf, None, None, 0.0, true), Some(1.0));
2887        assert_eq!(percentile_f(&dataf, None, None, 1.0, true), Some(5.0));
2888        assert_eq!(percentile_f(&dataf, None, None, 0.5, true), Some(3.0));
2889    }
2890
2891    #[test]
2892    fn test_quantile_ord_and_f() {
2893        let data = vec64![1i32, 2, 3, 4, 5];
2894        let out = quantile(&data, None, None, 4, true).unwrap();
2895        assert_eq!(out.len(), 3);
2896        assert_eq!(out, Vec64::from_slice(&[2, 3, 4]));
2897
2898        let mask = mask_from_bools(&[true, false, true, true, false]);
2899        let out2 = quantile(&data, Some(&mask), Some(2), 3, true).unwrap();
2900        assert_eq!(out2, Vec64::from_slice(&[1, 3]));
2901
2902        let dataf = vec64![10.0f64, 20.0, 30.0, 40.0, 50.0];
2903        let out = quantile_f(&dataf, None, None, 4, true).unwrap();
2904        assert_eq!(out.len(), 3);
2905        assert!(approx_eq(out[1], 30.0, 1e-9));
2906    }
2907
2908    #[test]
2909    fn test_iqr_ord() {
2910        let data = vec64![1i32, 2, 3, 4, 5];
2911        assert_eq!(iqr(&data, None, None, true), Some(2));
2912    }
2913
2914    #[test]
2915    fn test_iqr_f64() {
2916        let dataf = vec64![10.0f64, 20.0, 30.0, 40.0, 50.0];
2917        // p*(n−1) rounding definition, Q3=40, Q1=20 → IQR=20
2918        assert_eq!(iqr_f(&dataf, None, None, true), Some(20.0));
2919    }
2920    #[test]
2921    fn test_mode_ord_and_f() {
2922        let data = vec64![1i32, 2, 3, 2, 2, 4, 5, 1];
2923        assert_eq!(mode(&data, None, None), Some(2));
2924        let dataf = vec64![1.0f32, 2.0, 2.0, 3.0, 3.0, 3.0];
2925        assert_eq!(mode_f(&dataf, None, None), Some(3.0));
2926        let data = vec64![1i32, 2, 3, 3, 2, 1];
2927        let m = mode(&data, None, None);
2928        assert!(m == Some(1) || m == Some(2) || m == Some(3));
2929    }
2930
2931    #[test]
2932    fn test_count_distinct_and_f() {
2933        let data = vec64![1i32, 2, 2, 3, 3, 4];
2934        assert_eq!(count_distinct(&data, None, None), 4);
2935        let dataf = vec64![1.0f32, 2.0, 2.0, 3.0, 3.0, 4.0];
2936        assert_eq!(count_distinct_f(&dataf, None, None), 4);
2937    }
2938
2939    #[test]
2940    fn test_harmonic_and_geometric_mean() {
2941        let data = vec64![1.0f64, 2.0, 4.0];
2942        if let Some(hm) = harmonic_mean_f(&data, None, None) {
2943            assert!(approx_eq(hm, 1.714285714, 1e-6));
2944        } else {
2945            panic!("Expected F64");
2946        }
2947
2948        if let Some(gm) = geometric_mean_f(&data, None, None) {
2949            assert!(approx_eq(gm, 2.0, 1e-6));
2950        } else {
2951            panic!("Expected F64");
2952        }
2953
2954        let data = vec64![1i32, 2, 4];
2955        if let Some(hm) = harmonic_mean_int(&data, None, None) {
2956            assert!(approx_eq(hm, 1.714285714, 1e-6));
2957        }
2958        if let Some(gm) = geometric_mean_int(&data, None, None) {
2959            assert!(approx_eq(gm, 2.0, 1e-6));
2960        }
2961    }
2962
2963    #[test]
2964    fn test_empty_arrays() {
2965        assert_eq!(stat_moments_f64(&[], None, None), (0.0, 0.0, 0));
2966        assert_eq!(stat_moments_i32(&[], None, None), (0.0, 0.0, 0));
2967        assert_eq!(sum_f64(&[], None, None), None);
2968        assert_eq!(average_i32(&[], None, None), None);
2969        assert_eq!(variance_f64(&[], None, None, false), None);
2970        assert_eq!(median::<i32>(&[], None, None, true), None);
2971        assert_eq!(median_f::<f64>(&[], None, None, true), None);
2972        assert_eq!(mode::<i32>(&[], None, None), None);
2973        assert_eq!(mode_f::<f32>(&[], None, None), None);
2974        assert_eq!(quantile::<i32>(&[], None, None, 3, true), None);
2975        assert_eq!(quantile_f::<f64>(&[], None, None, 3, true), None);
2976        assert_eq!(iqr::<i32>(&[], None, None, true), None);
2977        assert_eq!(iqr_f::<f64>(&[], None, None, true), None);
2978        assert_eq!(count_distinct::<i32>(&[], None, None), 0);
2979        assert_eq!(count_distinct_f::<f32>(&[], None, None), 0);
2980    }
2981
2982    // --- i64, u64, f32, u32: moments and basic stats (dense/masked) ---
2983    #[test]
2984    fn test_stat_moments_all_types() {
2985        let i64_data = vec64![-8i64, 2, 8, -2, 3];
2986        let mask = mask_from_bools(&[true, false, true, true, true]);
2987        let (sum, sum2, count) = stat_moments_i64(&i64_data, Some(&mask), Some(1));
2988        assert_eq!(sum, (-8.0) + 8.0 + (-2.0) + 3.0);
2989        assert_eq!(sum2, 64.0 + 64.0 + 4.0 + 9.0);
2990        assert_eq!(count, 4);
2991
2992        let u64_data = vec64![1u64, 2, 3, 4, 5];
2993        let mask = mask_from_bools(&[false, true, true, true, false]);
2994        let (sum, sum2, count) = stat_moments_u64(&u64_data, Some(&mask), Some(2));
2995        assert_eq!(sum, 2.0 + 3.0 + 4.0);
2996        assert_eq!(sum2, 4.0 + 9.0 + 16.0);
2997        assert_eq!(count, 3);
2998
2999        let f32_data = vec64![0.5f32, 2.0, 4.5, -2.0, 3.0];
3000        let mask = mask_from_bools(&[true, true, false, false, true]);
3001        let (sum, sum2, count) = stat_moments_f32(&f32_data, Some(&mask), Some(2));
3002        assert!(approx_eq(sum, 0.5 + 2.0 + 3.0, 1e-6));
3003        assert!(approx_eq(sum2, 0.25 + 4.0 + 9.0, 1e-6));
3004        assert_eq!(count, 3);
3005
3006        let u32_data = vec64![100u32, 200, 300, 400, 500];
3007        let mask = mask_from_bools(&[true, false, false, true, true]);
3008        let (sum, sum2, count) = stat_moments_u32(&u32_data, Some(&mask), Some(2));
3009        assert_eq!(sum, 100.0 + 400.0 + 500.0);
3010        assert_eq!(sum2, 10000.0 + 160000.0 + 250000.0);
3011        assert_eq!(count, 3);
3012    }
3013
3014    #[test]
3015    fn test_reduce_min_max_for_all_types() {
3016        let i64_data = vec64![-8i64, 2, 8, -2, 3];
3017        assert_eq!(reduce_min_max_i64(&i64_data, None, None), Some((-8, 8)));
3018        let mask = mask_from_bools(&[true, false, false, false, true]);
3019        assert_eq!(
3020            reduce_min_max_i64(&i64_data, Some(&mask), Some(3)),
3021            Some((-8, 3))
3022        );
3023
3024        let u64_data = vec64![1u64, 2, 3, 4, 5];
3025        assert_eq!(reduce_min_max_u64(&u64_data, None, None), Some((1, 5)));
3026        let mask = mask_from_bools(&[false, false, true, true, false]);
3027        assert_eq!(
3028            reduce_min_max_u64(&u64_data, Some(&mask), Some(3)),
3029            Some((3, 4))
3030        );
3031
3032        let i32_data = vec64![9i32, -2, 5, -2];
3033        assert_eq!(reduce_min_max_i32(&i32_data, None, None), Some((-2, 9)));
3034        let mask = mask_from_bools(&[false, false, true, false]);
3035        assert_eq!(
3036            reduce_min_max_i32(&i32_data, Some(&mask), Some(3)),
3037            Some((5, 5))
3038        );
3039
3040        let u32_data = vec64![7u32, 2, 5, 11];
3041        assert_eq!(reduce_min_max_u32(&u32_data, None, None), Some((2, 11)));
3042        let mask = mask_from_bools(&[true, false, false, true]);
3043        assert_eq!(
3044            reduce_min_max_u32(&u32_data, Some(&mask), Some(2)),
3045            Some((7, 11))
3046        );
3047
3048        let f64_data = vec64![f64::NAN, 3.0, 1.0, 7.0];
3049        assert_eq!(reduce_min_max_f64(&f64_data, None, None), Some((1.0, 7.0))); // NaN ignored
3050    }
3051
3052    #[test]
3053    fn test_empty_and_all_null_min_max() {
3054        let empty: [i32; 0] = [];
3055        assert_eq!(reduce_min_max_i32(&empty, None, None), None);
3056
3057        let all_nulls = vec64![5i32, 10, 15];
3058        let mask = mask_from_bools(&[false, false, false]);
3059        assert_eq!(reduce_min_max_i32(&all_nulls, Some(&mask), Some(3)), None);
3060    }
3061
3062    // --- sum/avg: all types, empty and all-null ---
3063    #[test]
3064    fn test_sum_avg_and_variance_all_types() {
3065        // u32
3066        let data = vec64![100u32, 200, 0, 400];
3067        assert_eq!(sum_u32(&data, None, None), Some(700));
3068        assert_eq!(average_u32(&data, None, None), Some(175.0));
3069        let mask = mask_from_bools(&[false, true, false, true]);
3070        assert_eq!(sum_u32(&data, Some(&mask), Some(2)), Some(600));
3071        assert_eq!(average_u32(&data, Some(&mask), Some(2)), Some(300.0));
3072        assert_eq!(
3073            variance_u32(&data, None, None, false).unwrap(),
3074            ((100f64.powi(2) + 200f64.powi(2) + 400f64.powi(2)) / 4.0 - (700.0 / 4.0).powi(2))
3075        );
3076
3077        // i64
3078        let data = vec64![5i64, -10, 25, 5];
3079        assert_eq!(sum_i64(&data, None, None), Some(25));
3080        assert_eq!(average_i64(&data, None, None), Some(6.25));
3081        let mask = mask_from_bools(&[true, false, false, false]);
3082        assert_eq!(sum_i64(&data, Some(&mask), Some(3)), Some(5));
3083        assert_eq!(average_i64(&data, Some(&mask), Some(3)), Some(5.0));
3084        assert!(variance_i64(&data, None, None, false).unwrap() > 0.0);
3085
3086        // u64
3087        let data = vec64![10u64, 0, 10, 10];
3088        assert_eq!(sum_u64(&data, None, None), Some(30));
3089        assert_eq!(average_u64(&data, None, None), Some(7.5));
3090        let mask = mask_from_bools(&[false, false, false, false]);
3091        assert_eq!(sum_u64(&data, Some(&mask), Some(4)), None);
3092        assert_eq!(average_u64(&data, Some(&mask), Some(4)), None);
3093
3094        // f32
3095        let data = vec64![2.0f32, 4.0, 6.0];
3096        assert_eq!(sum_f32(&data, None, None), Some(12.0));
3097        assert_eq!(average_f32(&data, None, None), Some(4.0));
3098        let mask = mask_from_bools(&[true, false, true]);
3099        assert_eq!(sum_f32(&data, Some(&mask), Some(1)), Some(8.0));
3100        assert_eq!(average_f32(&data, Some(&mask), Some(1)), Some(4.0));
3101        assert!(variance_f32(&data, None, None, false).unwrap() > 0.0);
3102
3103        // f64
3104        let data = vec64![1.0f64, f64::INFINITY, f64::NAN];
3105        assert!(
3106            sum_f64(&data, None, None).unwrap().is_nan()
3107                || sum_f64(&data, None, None).unwrap().is_infinite()
3108        );
3109    }
3110
3111    // --- min/max/range: singleton, duplicates, negative, NaN, INF ---
3112    #[test]
3113    fn test_min_max_range_i32_singleton() {
3114        let d = vec64![1i32];
3115        assert_eq!(min_i32(&d, None, None), Some(1));
3116        assert_eq!(max_i32(&d, None, None), Some(1));
3117        assert_eq!(range_i32(&d, None, None), Some((1, 1)));
3118    }
3119
3120    #[test]
3121    fn test_min_f64_nan() {
3122        let d = vec64![5.0f64, f64::NAN];
3123        // we ignore NaNs, so the min of [5.0, NaN] is 5.0
3124        assert_eq!(min_f64(&d, None, None), Some(5.0));
3125    }
3126
3127    #[test]
3128    fn test_max_f64_nan() {
3129        let d = vec64![5.0f64, f64::NAN];
3130        // we ignore NaNs, so the max of [5.0, NaN] is 5.0
3131        assert_eq!(max_f64(&d, None, None), Some(5.0));
3132    }
3133
3134    #[test]
3135    fn test_min_f32_neg_inf() {
3136        let d = vec64![f32::NEG_INFINITY, 0.0f32, 5.0f32];
3137        assert_eq!(min_f32(&d, None, None), Some(f32::NEG_INFINITY));
3138    }
3139
3140    #[test]
3141    fn test_max_f32_regular() {
3142        let d = vec64![f32::NEG_INFINITY, 0.0f32, 5.0f32];
3143        assert_eq!(max_f32(&d, None, None), Some(5.0));
3144    }
3145
3146    #[test]
3147    fn test_median_ord_even() {
3148        let d = vec64![2i64, 4, 6, 8];
3149        // sorted ⇒ [2,4,6,8], lower median is 4
3150        // Keep integers as integers - with users able
3151        // to cast to floats as needed
3152        assert_eq!(median(&d, None, None, true), Some(4));
3153    }
3154
3155    #[test]
3156    fn test_median_ord_odd() {
3157        let d = vec64![7i32, 3, 5];
3158        assert_eq!(median(&d, None, None, true), Some(5));
3159    }
3160
3161    #[test]
3162    fn test_median_ord_with_mask() {
3163        let d = vec64![7i32, 3, 5];
3164        let mask = mask_from_bools(&[false, true, false]);
3165        assert_eq!(median(&d, Some(&mask), Some(2), true), Some(3));
3166    }
3167
3168    #[test]
3169    fn test_quantile_ord() {
3170        let d = vec64![10u32, 20, 30, 40, 50, 60];
3171        let q = quantile(&d, None, None, 3, true).unwrap();
3172        assert_eq!(q.len(), 2);
3173        assert!(q[0] >= 20 && q[1] >= 40);
3174    }
3175
3176    #[test]
3177    fn test_median_f64() {
3178        let df = vec64![1.0f64, 5.0, 3.0, 2.0];
3179        assert!(approx_eq(
3180            median_f(&df, None, None, true).unwrap(),
3181            2.5,
3182            1e-9
3183        ));
3184    }
3185
3186    #[test]
3187    fn test_quantile_f64() {
3188        let df = vec64![1.0f64, 5.0, 3.0, 2.0];
3189        let qf = quantile_f(&df, None, None, 3, true).unwrap();
3190        // we produce exactly q−1 = 2 interior tertile values
3191        assert_eq!(qf.len(), 2);
3192        assert!(qf.iter().all(|&v| v >= 1.0 && v <= 5.0));
3193    }
3194
3195    #[test]
3196    fn test_iqr_i32() {
3197        let d = vec64![1i32, 3, 2, 8, 4, 5];
3198        assert!(iqr(&d, None, None, true).unwrap() >= 2);
3199    }
3200
3201    #[test]
3202    fn test_iqr_f32() {
3203        let d = vec64![10.0f32, 100.0, 30.0, 50.0];
3204        assert!(iqr_f(&d, None, None, true).unwrap() > 0.0);
3205    }
3206
3207    // --- mode & count_distinct: ties, all unique, all equal, NaN/Inf ---
3208    #[test]
3209    fn test_mode_and_count_distinct_exhaustive() {
3210        // Mode: all unique
3211        let d = vec64![7u64, 5, 3, 1];
3212        assert!(d.contains(&mode(&d, None, None).unwrap()));
3213
3214        // Mode: all equal
3215        let d = vec64![2i64; 6];
3216        assert_eq!(mode(&d, None, None), Some(2));
3217
3218        // Mode: tie
3219        let d = vec64![3i32, 5, 3, 5];
3220        let m = mode(&d, None, None);
3221        assert!(m == Some(3) || m == Some(5));
3222
3223        // Mode: NaN in floats, inf, repeated
3224        let d = vec64![f32::NAN, f32::INFINITY, 5.0, 5.0, f32::NAN];
3225        let m = mode_f(&d, None, None);
3226        assert!(m == Some(5.0) || m.unwrap().is_nan() || m.unwrap().is_infinite());
3227
3228        // Count distinct: all equal, all unique, mask
3229        let d = vec64![1u64, 1, 1, 1];
3230        assert_eq!(count_distinct(&d, None, None), 1);
3231
3232        let d = vec64![1i32, 2, 3, 4, 5];
3233        assert_eq!(count_distinct(&d, None, None), 5);
3234
3235        let mask = mask_from_bools(&[true, false, false, true, true]);
3236        assert_eq!(count_distinct(&d, Some(&mask), Some(2)), 3);
3237
3238        let d = vec64![f64::NAN, f64::INFINITY, 0.0];
3239        assert_eq!(count_distinct_f(&d, None, None), 3);
3240    }
3241
3242    // --- harmonic & geometric means: all types, zero/negative/NaN ---
3243    #[test]
3244    fn test_harmonic_mean_f64() {
3245        let data = vec64![0.5f64, 2.0, 1.0];
3246        if let Some(hm) = harmonic_mean_f(&data, None, None) {
3247            // the true harmonic mean is 3/(2 + 0.5 + 1) ≈ 0.8571
3248            assert!(approx_eq(hm, 0.857142857, 1e-2));
3249        } else {
3250            panic!("Expected F64 result for harmonic mean f64");
3251        }
3252    }
3253
3254    #[test]
3255    fn test_geometric_mean_f64() {
3256        let data = vec64![0.5f64, 2.0, 1.0];
3257        if let Some(gm) = geometric_mean_f(&data, None, None) {
3258            assert!(approx_eq(gm, 1.0, 1e-9));
3259        } else {
3260            panic!("Expected F64 result for geometric mean f64");
3261        }
3262    }
3263
3264    #[test]
3265    fn test_harmonic_mean_int_all_zeros() {
3266        let d = vec64![0i32, 0, 0];
3267        assert_eq!(harmonic_mean_int(&d, None, None), None);
3268    }
3269
3270    #[test]
3271    fn test_geometric_mean_uint() {
3272        let data = vec64![2u64, 8, 4];
3273        if let Some(gm) = geometric_mean_uint(&data, None, None) {
3274            assert!(approx_eq(gm, 4.0, 1e-9));
3275        } else {
3276            panic!("Expected F64 result for geometric mean uint");
3277        }
3278    }
3279
3280    #[test]
3281    fn test_geometric_mean_int_negative() {
3282        // Should not panic on negative input, even if result is complex
3283        let d = vec64![-2i64, -4, -8];
3284        let _ = geometric_mean_int(&d, None, None);
3285    }
3286
3287    // --- percentile: boundary and empty, floats with mask, p out of range ---
3288    #[test]
3289    fn test_percentile_ord_and_f_all_cases() {
3290        let d = vec64![1i32, 4, 6, 8, 10];
3291        assert_eq!(percentile_ord(&d, None, None, 0.0, true), Some(1));
3292        assert_eq!(percentile_ord(&d, None, None, 100.0, true), Some(10));
3293        let mask = mask_from_bools(&[false, false, false, false, false]);
3294        assert_eq!(percentile_ord(&d, Some(&mask), Some(5), 50.0, true), None);
3295
3296        let df = vec64![1.0f64, 2.0, 3.0, 4.0, 5.0];
3297        assert_eq!(percentile_f(&df, None, None, 0.0, true), Some(1.0));
3298        assert_eq!(percentile_f(&df, None, None, 1.0, true), Some(5.0));
3299        let mask = mask_from_bools(&[false, false, false, false, false]);
3300        assert_eq!(percentile_f(&df, Some(&mask), Some(5), 0.5, true), None);
3301
3302        // Out of range p (should clamp)
3303        assert_eq!(percentile_f(&df, None, None, 10.0, true), Some(5.0));
3304        assert_eq!(percentile_f(&df, None, None, -5.0, true), Some(1.0));
3305    }
3306
3307    // --- quantile: small q, empty, all-null, mask with only one valid ---
3308    #[test]
3309    fn test_quantile_f_and_ord_special_cases() {
3310        let d = vec64![2.0f32, 4.0, 6.0, 8.0];
3311        // q < 2 (should return None)
3312        assert_eq!(quantile_f(&d, None, None, 1, true), None);
3313        // all nulls
3314        let mask = mask_from_bools(&[false, false, false, false]);
3315        assert_eq!(quantile_f(&d, Some(&mask), Some(4), 2, true), None);
3316
3317        // mask: only one valid
3318        let mask = mask_from_bools(&[false, false, true, false]);
3319        let out = quantile_f(&d, Some(&mask), Some(3), 2, true).unwrap();
3320        assert_eq!(out.len(), 1);
3321        assert!(out.iter().all(|&x| x == 6.0));
3322    }
3323
3324    // --- Test for proper handling of singletons, all-nulls, degenerate input for all aggregation APIs ---
3325    #[test]
3326    fn test_all_apis_singleton_and_nulls() {
3327        let d = vec64![7u32];
3328        assert_eq!(sum_u32(&d, None, None), Some(7));
3329        assert_eq!(average_u32(&d, None, None), Some(7.0));
3330        assert_eq!(variance_u32(&d, None, None, false).unwrap(), 0.0);
3331        assert_eq!(mode(&d, None, None), Some(7));
3332        assert_eq!(median(&d, None, None, true), Some(7));
3333        assert_eq!(
3334            quantile(&d, None, None, 3, true).unwrap(),
3335            Vec64::from_slice(&[7, 7])
3336        );
3337        assert_eq!(iqr(&d, None, None, true), Some(0));
3338
3339        let mask = mask_from_bools(&[false]);
3340        assert_eq!(sum_u32(&d, Some(&mask), Some(1)), None);
3341        assert_eq!(median(&d, Some(&mask), Some(1), true), None);
3342        assert_eq!(quantile(&d, Some(&mask), Some(1), 3, true), None);
3343        assert_eq!(mode(&d, Some(&mask), Some(1)), None);
3344        assert_eq!(iqr(&d, Some(&mask), Some(1), true), None);
3345    }
3346
3347    #[test]
3348    fn test_skewness_kurtosis_dense() {
3349        // Data 1..=5 → mean 3, symmetric
3350        // population skewness = 0, population excess kurtosis = -1.3
3351        // sample skewness = 0, sample excess kurtosis = -1.2
3352        let d_f64 = vec64![1.0f64, 2.0, 3.0, 4.0, 5.0];
3353        // population
3354        assert!(approx_eq(
3355            skewness_f64(&d_f64, None, None, false).unwrap(),
3356            0.0,
3357            1e-12
3358        ));
3359        assert!(approx_eq(
3360            kurtosis_f64(&d_f64, None, None, false).unwrap(),
3361            -1.3,
3362            1e-12
3363        ));
3364        // sample
3365        assert!(approx_eq(
3366            skewness_f64(&d_f64, None, None, true).unwrap(),
3367            0.0,
3368            1e-12
3369        ));
3370        assert!(approx_eq(
3371            kurtosis_f64(&d_f64, None, None, true).unwrap(),
3372            -1.2,
3373            1e-12
3374        ));
3375
3376        let d_f32 = vec64![1.0f32, 2.0, 3.0, 4.0, 5.0];
3377        // population
3378        assert!(approx_eq(
3379            skewness_f32(&d_f32, None, None, false).unwrap(),
3380            0.0,
3381            1e-6
3382        ));
3383        assert!(approx_eq(
3384            kurtosis_f32(&d_f32, None, None, false).unwrap(),
3385            -1.3,
3386            1e-6
3387        ));
3388        // sample
3389        assert!(approx_eq(
3390            skewness_f32(&d_f32, None, None, true).unwrap(),
3391            0.0,
3392            1e-6
3393        ));
3394        assert!(approx_eq(
3395            kurtosis_f32(&d_f32, None, None, true).unwrap(),
3396            -1.2,
3397            1e-6
3398        ));
3399
3400        let d_i32 = vec64![1i32, 2, 3, 4, 5];
3401        assert!(approx_eq(
3402            skewness_i32(&d_i32, None, None, false).unwrap(),
3403            0.0,
3404            1e-12
3405        ));
3406        assert!(approx_eq(
3407            kurtosis_i32(&d_i32, None, None, false).unwrap(),
3408            -1.3,
3409            1e-12
3410        ));
3411        assert!(approx_eq(
3412            skewness_i32(&d_i32, None, None, true).unwrap(),
3413            0.0,
3414            1e-12
3415        ));
3416        assert!(approx_eq(
3417            kurtosis_i32(&d_i32, None, None, true).unwrap(),
3418            -1.2,
3419            1e-12
3420        ));
3421    }
3422
3423    #[test]
3424    fn test_skewness_kurtosis_masked() {
3425        // Mask out 1 & 5 → [2,3,4]
3426        // population skewness = 0, population & sample excess kurtosis = -1.5
3427        let data = vec64![1i32, 2, 3, 4, 5];
3428        let mask = mask_from_bools(&[false, true, true, true, false]);
3429        // population
3430        assert!(approx_eq(
3431            skewness_i32(&data, Some(&mask), Some(2), false).unwrap(),
3432            0.0,
3433            1e-12
3434        ));
3435        assert!(approx_eq(
3436            kurtosis_i32(&data, Some(&mask), Some(2), false).unwrap(),
3437            -1.5,
3438            1e-12
3439        ));
3440        // sample
3441        assert!(approx_eq(
3442            skewness_i32(&data, Some(&mask), Some(2), true).unwrap(),
3443            0.0,
3444            1e-12
3445        ));
3446        assert!(approx_eq(
3447            kurtosis_i32(&data, Some(&mask), Some(2), true).unwrap(),
3448            -1.5,
3449            1e-12
3450        ));
3451    }
3452
3453    #[test]
3454    fn test_skewness_kurtosis_constant_none() {
3455        // Variance zero → expect None
3456        let d = vec64![7u32; 5];
3457        assert_eq!(skewness_u32(&d, None, None, false), None);
3458        assert_eq!(kurtosis_u32(&d, None, None, false), None);
3459        assert_eq!(skewness_u32(&d, None, None, true), None);
3460        assert_eq!(kurtosis_u32(&d, None, None, true), None);
3461    }
3462
3463    #[test]
3464    fn test_kurtosis_skewness_large_f32() {
3465        // Mixed negative/positive of length 11:
3466        // population: skew ≈ -0.20081265, kurt ≈ -1.28414694
3467        // sample:     skew ≈ -0.23401565, kurt ≈ -1.30691156
3468        let data = vec64![-2.0, -1.0, -1.0, 0.0, 0.0, 1.0, 2.0, 2.0, 2.0, 3.0, 3.0];
3469        // population
3470        assert!(approx_eq(
3471            skewness_f32(&data, None, None, false).unwrap(),
3472            -0.20081265422848577_f64,
3473            1e-12
3474        ));
3475        assert!(approx_eq(
3476            kurtosis_f32(&data, None, None, false).unwrap(),
3477            -1.2841469381610144_f64,
3478            1e-12
3479        ));
3480        // sample
3481        assert!(approx_eq(
3482            skewness_f32(&data, None, None, true).unwrap(),
3483            -0.23401565397707677_f64,
3484            1e-12
3485        ));
3486        assert!(approx_eq(
3487            kurtosis_f32(&data, None, None, true).unwrap(),
3488            -1.3069115636016906_f64,
3489            1e-12
3490        ));
3491    }
3492
3493    fn bitmask_from_bools(bs: &[bool]) -> Bitmask {
3494        let mut mask = Bitmask::with_capacity(bs.len());
3495        for (i, &b) in bs.iter().enumerate() {
3496            mask.set(i, b);
3497        }
3498        mask
3499    }
3500
3501    #[test]
3502    fn test_kurtosis_skewness_large_masked_i32() {
3503        // Drop 4th element (4), leaving 9 values:
3504        // population: skew ≈ 2.43067998, kurt ≈ 3.99120344
3505        // sample:     skew ≈ 2.94642909, kurt ≈ 8.74514940
3506        let data = vec64![1, 2, 3, 4, 5, 6, 7, 100, 1000, 10000];
3507        let mask = mask_from_bools(&[true, true, true, false, true, true, true, true, true, true]);
3508        // population
3509        assert!(approx_eq(
3510            skewness_i32(&data, Some(&mask), None, false).unwrap(),
3511            2.4306799843134046_f64,
3512            1e-12
3513        ));
3514        assert!(approx_eq(
3515            kurtosis_i32(&data, Some(&mask), None, false).unwrap(),
3516            3.991203437112363_f64,
3517            1e-12
3518        ));
3519        // sample
3520        assert!(approx_eq(
3521            skewness_i32(&data, Some(&mask), None, true).unwrap(),
3522            2.946429085375575_f64,
3523            1e-12
3524        ));
3525        assert!(approx_eq(
3526            kurtosis_i32(&data, Some(&mask), None, true).unwrap(),
3527            8.745149404023547_f64,
3528            1e-12
3529        ));
3530    }
3531
3532    #[test]
3533    fn test_kurtosis_skewness_no_valids() {
3534        let data: [f64; 3] = [0.0, 0.0, 0.0];
3535        let mask = bitmask_from_bools(&[false, false, false]);
3536        assert_eq!(kurtosis_f64(&data, Some(&mask), None, false), None);
3537        assert_eq!(skewness_f64(&data, Some(&mask), None, false), None);
3538        assert_eq!(kurtosis_f64(&data, Some(&mask), None, true), None);
3539        assert_eq!(skewness_f64(&data, Some(&mask), None, true), None);
3540    }
3541
3542    #[test]
3543    fn test_kurtosis_skewness_single_value() {
3544        let data = vec64![1u32];
3545        assert_eq!(kurtosis_u32(&data, None, None, false), None);
3546        assert_eq!(skewness_u32(&data, None, None, false), None);
3547        assert_eq!(kurtosis_u32(&data, None, None, true), None);
3548        assert_eq!(skewness_u32(&data, None, None, true), None);
3549    }
3550
3551    #[test]
3552    fn test_kurtosis_skewness_all_equal() {
3553        let data = vec64![7.0, 7.0, 7.0, 7.0];
3554        assert_eq!(kurtosis_f64(&data, None, None, false), None);
3555        assert_eq!(skewness_f64(&data, None, None, false), None);
3556        assert_eq!(kurtosis_f64(&data, None, None, true), None);
3557        assert_eq!(skewness_f64(&data, None, None, true), None);
3558    }
3559
3560    #[test]
3561    fn test_kurtosis_skewness_mixed_nulls() {
3562        // Valid [2.0,7.0,10.0] (length 3):
3563        // population: skew ≈ -0.29479962, kurt = -1.5
3564        // sample:     skew ≈ -0.72210865, kurt = -1.5
3565        let data = vec64![2.0, 5.0, 7.0, 3.0, 10.0];
3566        let mask = mask_from_bools(&[true, false, true, false, true]);
3567        // population
3568        assert!(approx_eq(
3569            skewness_f64(&data, Some(&mask), None, false).unwrap(),
3570            -0.29479962014482897_f64,
3571            1e-12
3572        ));
3573        assert!(approx_eq(
3574            kurtosis_f64(&data, Some(&mask), None, false).unwrap(),
3575            -1.5_f64,
3576            1e-12
3577        ));
3578        // sample
3579        assert!(approx_eq(
3580            skewness_f64(&data, Some(&mask), None, true).unwrap(),
3581            -0.7221086457211346_f64,
3582            1e-12
3583        ));
3584        assert!(approx_eq(
3585            kurtosis_f64(&data, Some(&mask), None, true).unwrap(),
3586            -1.5_f64,
3587            1e-12
3588        ));
3589    }
3590
3591    #[test]
3592    fn test_kurtosis_skewness_integers_large() {
3593        let data = vec64![1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9];
3594        assert!(kurtosis_i32(&data, None, None, false).unwrap().abs() < 1.5);
3595        assert!(skewness_i32(&data, None, None, false).unwrap().abs() < 1e-6);
3596        // Sample
3597        assert!(kurtosis_i32(&data, None, None, true).unwrap().abs() < 2.0);
3598        assert!(skewness_i32(&data, None, None, true).unwrap().abs() < 1e-6);
3599    }
3600
3601    #[test]
3602    fn test_agg_product_dense() {
3603        // No mask, all values included
3604        let data = vec64![2, 3, 4, 5];
3605        let prod = agg_product(&data, None, 0, data.len());
3606        assert_eq!(prod, 120);
3607
3608        // Offset/len window
3609        let prod = agg_product(&data, None, 1, 2);
3610        assert_eq!(prod, 12); // 3*4
3611    }
3612
3613    #[test]
3614    fn test_agg_product_with_mask_all_valid() {
3615        let data = vec64![2, 3, 4, 5];
3616        let mask = Bitmask::new_set_all(4, true);
3617        let prod = agg_product(&data, Some(&mask), 0, 4);
3618        assert_eq!(prod, 120);
3619    }
3620
3621    #[test]
3622    fn test_agg_product_with_mask_some_nulls() {
3623        let data = vec64![2, 3, 4, 5];
3624        let mut mask = Bitmask::new_set_all(4, false);
3625        mask.set(0, true);
3626        mask.set(2, true);
3627        // Only data[0] and data[2] included: 2*4=8
3628        let prod = agg_product(&data, Some(&mask), 0, 4);
3629        assert_eq!(prod, 8);
3630    }
3631
3632    #[test]
3633    fn test_agg_product_zero_short_circuit() {
3634        let data = vec64![2, 0, 4, 5];
3635        let prod = agg_product(&data, None, 0, 4);
3636        assert_eq!(prod, 0);
3637    }
3638
3639    #[test]
3640    fn test_agg_product_empty() {
3641        let data: [i32; 0] = [];
3642        let prod = agg_product(&data, None, 0, 0);
3643        assert_eq!(prod, 1); // One by convention
3644    }
3645
3646    #[test]
3647    fn test_agg_product_mask_all_null() {
3648        let data = vec64![2, 3, 4, 5];
3649        let mask = Bitmask::new_set_all(4, false);
3650        let prod = agg_product(&data, Some(&mask), 0, 4);
3651        assert_eq!(prod, 1); // No values included: returns one
3652    }
3653
3654    #[test]
3655    fn test_agg_product_offset_and_mask() {
3656        let data = vec64![2, 3, 4, 5, 6];
3657        let mut mask = Bitmask::new_set_all(5, false);
3658        mask.set(1, true);
3659        mask.set(3, true);
3660        let prod = agg_product(&data, Some(&mask), 1, 3);
3661        assert_eq!(prod, 15);
3662    }
3663
3664    #[test]
3665    fn test_sum_squares_empty() {
3666        let v: [f64; 0] = [];
3667        assert_eq!(sum_squares(&v, None, None), 0.0);
3668    }
3669
3670    #[test]
3671    fn test_sum_squares_basic() {
3672        let v = vec64![1.0, 2.0, 3.0];
3673        assert_eq!(sum_squares(&v, None, None), 1.0 + 4.0 + 9.0);
3674    }
3675
3676    #[test]
3677    fn test_sum_squares_neg() {
3678        let v = vec64![-1.0, -2.0, -3.0];
3679        assert_eq!(sum_squares(&v, None, None), 1.0 + 4.0 + 9.0);
3680    }
3681
3682    #[test]
3683    fn test_sum_squares_large() {
3684        let v: Vec<f64> = (0..1000).map(|x| x as f64).collect();
3685        let expect: f64 = v.iter().map(|x| x * x).sum();
3686        assert!((sum_squares(&v, None, None) - expect).abs() < 1e-9);
3687    }
3688
3689    #[test]
3690    fn test_sum_squares_masked() {
3691        let v = vec64![1.0, 2.0, 3.0, 4.0];
3692        let mask = Bitmask::from_bools(&[true, false, true, false]);
3693        assert_eq!(sum_squares(&v, Some(&mask), Some(2)), 1.0 + 9.0);
3694    }
3695
3696    #[test]
3697    fn test_sum_cubes_empty() {
3698        let v: [f64; 0] = [];
3699        assert_eq!(sum_cubes(&v, None, None), 0.0);
3700    }
3701
3702    #[test]
3703    fn test_sum_cubes_basic() {
3704        let v = vec64![1.0, 2.0, 3.0];
3705        assert_eq!(sum_cubes(&v, None, None), 1.0 + 8.0 + 27.0);
3706    }
3707
3708    #[test]
3709    fn test_sum_cubes_neg() {
3710        let v = vec64![-1.0, -2.0, -3.0];
3711        assert_eq!(sum_cubes(&v, None, None), -1.0 - 8.0 - 27.0);
3712    }
3713
3714    #[test]
3715    fn test_sum_cubes_large() {
3716        let v: Vec<f64> = (0..1000).map(|x| x as f64).collect();
3717        let expect: f64 = v.iter().map(|x| x * x * x).sum();
3718        assert!((sum_cubes(&v, None, None) - expect).abs() < 1e-6);
3719    }
3720
3721    #[test]
3722    fn test_sum_cubes_masked() {
3723        let v = vec64![1.0, 2.0, 3.0, 4.0];
3724        let mask = Bitmask::from_bools(&[true, true, false, true]);
3725        assert_eq!(sum_cubes(&v, Some(&mask), Some(1)), 1.0 + 8.0 + 64.0);
3726    }
3727
3728    #[test]
3729    fn test_sum_quartics_empty() {
3730        let v: [f64; 0] = [];
3731        assert_eq!(sum_quartics(&v, None, None), 0.0);
3732    }
3733
3734    #[test]
3735    fn test_sum_quartics_basic() {
3736        let v = vec64![1.0, 2.0, 3.0];
3737        assert_eq!(sum_quartics(&v, None, None), 1.0 + 16.0 + 81.0);
3738    }
3739
3740    #[test]
3741    fn test_sum_quartics_neg() {
3742        let v = vec64![-1.0, -2.0, -3.0];
3743        assert_eq!(sum_quartics(&v, None, None), 1.0 + 16.0 + 81.0);
3744    }
3745
3746    #[test]
3747    fn test_sum_quartics_large() {
3748        let v: Vec<f64> = (0..1000).map(|x| x as f64).collect();
3749        let expect: f64 = v
3750            .iter()
3751            .map(|x| {
3752                let x2 = x * x;
3753                x2 * x2
3754            })
3755            .sum();
3756        assert!((sum_quartics(&v, None, None) - expect).abs() < 1e-3);
3757    }
3758
3759    #[test]
3760    fn test_sum_quartics_masked() {
3761        let v = vec64![1.0, 2.0, 3.0, 4.0];
3762        let mask = Bitmask::from_bools(&[false, true, false, true]);
3763        assert_eq!(sum_quartics(&v, Some(&mask), Some(2)), 16.0 + 256.0);
3764    }
3765
3766    #[test]
3767    fn test_sum_powers_singleton() {
3768        let v = vec64![4.2];
3769        assert!((sum_squares(&v, None, None) - 4.2 * 4.2).abs() < 1e-12);
3770        assert!((sum_cubes(&v, None, None) - 4.2 * 4.2 * 4.2).abs() < 1e-12);
3771        assert!((sum_quartics(&v, None, None) - (4.2 * 4.2 * 4.2 * 4.2)).abs() < 1e-12);
3772    }
3773
3774    #[test]
3775    fn test_sum_powers_f64_extremes() {
3776        let v = vec64![f64::MAX, -f64::MAX, 0.0, f64::MIN_POSITIVE];
3777        let ss = sum_squares(&v, None, None).is_infinite();
3778        let sc = sum_cubes(&v, None, None).is_infinite();
3779        let sq = sum_quartics(&v, None, None).is_infinite();
3780        println!("{:?}", ss);
3781        println!("{:?}", sc);
3782        println!("{:?}", sq);
3783        // f64::MAX * f64::MAX is inf, so expect inf for all powers >1
3784        assert!(sum_squares(&v, None, None).is_infinite());
3785        assert!(sum_cubes(&v, None, None).is_infinite());
3786        assert!(sum_quartics(&v, None, None).is_infinite());
3787    }
3788}