Skip to main content

presentar_core/
simd.rs

1#![allow(clippy::unwrap_used, clippy::disallowed_methods)]
2//! SIMD-accelerated operations using Trueno.
3//!
4//! This module provides hardware-accelerated vector and matrix operations
5//! for the Presentar rendering pipeline.
6//!
7//! When the `simd` feature is disabled, operations fall back to scalar
8//! implementations.
9//!
10//! # Example
11//!
12//! ```
13//! use presentar_core::simd::{Vec4, Mat4, batch_transform_points};
14//! use presentar_core::Point;
15//!
16//! let transform = Mat4::identity();
17//! let points = vec![Point::new(0.0, 0.0), Point::new(100.0, 100.0)];
18//! let transformed = batch_transform_points(&points, &transform);
19//! ```
20
21use crate::{Point, Rect};
22
23/// 4-component vector for SIMD operations.
24#[derive(Debug, Clone, Copy, PartialEq)]
25#[repr(C)]
26pub struct Vec4 {
27    pub x: f32,
28    pub y: f32,
29    pub z: f32,
30    pub w: f32,
31}
32
33impl Vec4 {
34    /// Create a new Vec4.
35    #[inline]
36    #[must_use]
37    pub const fn new(x: f32, y: f32, z: f32, w: f32) -> Self {
38        Self { x, y, z, w }
39    }
40
41    /// Create a zero vector.
42    #[inline]
43    #[must_use]
44    pub const fn zero() -> Self {
45        Self::new(0.0, 0.0, 0.0, 0.0)
46    }
47
48    /// Create from a Point (z=0, w=1).
49    #[inline]
50    #[must_use]
51    pub const fn from_point(p: Point) -> Self {
52        Self::new(p.x, p.y, 0.0, 1.0)
53    }
54
55    /// Convert to Point (ignoring z and w).
56    #[inline]
57    #[must_use]
58    pub const fn to_point(self) -> Point {
59        Point {
60            x: self.x,
61            y: self.y,
62        }
63    }
64
65    /// Dot product.
66    #[inline]
67    #[must_use]
68    pub fn dot(self, other: Self) -> f32 {
69        self.w.mul_add(
70            other.w,
71            self.z
72                .mul_add(other.z, self.x.mul_add(other.x, self.y * other.y)),
73        )
74    }
75
76    /// Component-wise addition.
77    #[inline]
78    #[must_use]
79    pub fn add(self, other: Self) -> Self {
80        Self::new(
81            self.x + other.x,
82            self.y + other.y,
83            self.z + other.z,
84            self.w + other.w,
85        )
86    }
87
88    /// Component-wise subtraction.
89    #[inline]
90    #[must_use]
91    pub fn sub(self, other: Self) -> Self {
92        Self::new(
93            self.x - other.x,
94            self.y - other.y,
95            self.z - other.z,
96            self.w - other.w,
97        )
98    }
99
100    /// Scalar multiplication.
101    #[inline]
102    #[must_use]
103    pub fn scale(self, s: f32) -> Self {
104        Self::new(self.x * s, self.y * s, self.z * s, self.w * s)
105    }
106
107    /// Component-wise multiplication.
108    #[inline]
109    #[must_use]
110    pub fn mul(self, other: Self) -> Self {
111        Self::new(
112            self.x * other.x,
113            self.y * other.y,
114            self.z * other.z,
115            self.w * other.w,
116        )
117    }
118
119    /// Linear interpolation.
120    #[inline]
121    #[must_use]
122    pub fn lerp(self, other: Self, t: f32) -> Self {
123        self.add(other.sub(self).scale(t))
124    }
125
126    /// Length (magnitude).
127    #[inline]
128    #[must_use]
129    pub fn length(self) -> f32 {
130        self.dot(self).sqrt()
131    }
132
133    /// Normalize to unit length.
134    #[inline]
135    #[must_use]
136    pub fn normalize(self) -> Self {
137        let len = self.length();
138        if len > 0.0 {
139            self.scale(1.0 / len)
140        } else {
141            self
142        }
143    }
144}
145
146impl Default for Vec4 {
147    fn default() -> Self {
148        Self::zero()
149    }
150}
151
152impl From<Point> for Vec4 {
153    fn from(p: Point) -> Self {
154        Self::from_point(p)
155    }
156}
157
158impl From<Vec4> for Point {
159    fn from(v: Vec4) -> Self {
160        v.to_point()
161    }
162}
163
164/// 4x4 matrix for transforms.
165#[derive(Debug, Clone, Copy, PartialEq)]
166#[repr(C)]
167pub struct Mat4 {
168    /// Row-major matrix data [row][col]
169    pub data: [[f32; 4]; 4],
170}
171
172impl Mat4 {
173    /// Create from raw data (row-major).
174    #[inline]
175    #[must_use]
176    pub const fn from_data(data: [[f32; 4]; 4]) -> Self {
177        Self { data }
178    }
179
180    /// Create identity matrix.
181    #[inline]
182    #[must_use]
183    pub const fn identity() -> Self {
184        Self::from_data([
185            [1.0, 0.0, 0.0, 0.0],
186            [0.0, 1.0, 0.0, 0.0],
187            [0.0, 0.0, 1.0, 0.0],
188            [0.0, 0.0, 0.0, 1.0],
189        ])
190    }
191
192    /// Create zero matrix.
193    #[inline]
194    #[must_use]
195    pub const fn zero() -> Self {
196        Self::from_data([
197            [0.0, 0.0, 0.0, 0.0],
198            [0.0, 0.0, 0.0, 0.0],
199            [0.0, 0.0, 0.0, 0.0],
200            [0.0, 0.0, 0.0, 0.0],
201        ])
202    }
203
204    /// Create translation matrix.
205    #[inline]
206    #[must_use]
207    pub const fn translation(x: f32, y: f32, z: f32) -> Self {
208        Self::from_data([
209            [1.0, 0.0, 0.0, x],
210            [0.0, 1.0, 0.0, y],
211            [0.0, 0.0, 1.0, z],
212            [0.0, 0.0, 0.0, 1.0],
213        ])
214    }
215
216    /// Create 2D translation matrix.
217    #[inline]
218    #[must_use]
219    pub const fn translation_2d(x: f32, y: f32) -> Self {
220        Self::translation(x, y, 0.0)
221    }
222
223    /// Create scale matrix.
224    #[inline]
225    #[must_use]
226    pub const fn scale(x: f32, y: f32, z: f32) -> Self {
227        Self::from_data([
228            [x, 0.0, 0.0, 0.0],
229            [0.0, y, 0.0, 0.0],
230            [0.0, 0.0, z, 0.0],
231            [0.0, 0.0, 0.0, 1.0],
232        ])
233    }
234
235    /// Create 2D scale matrix.
236    #[inline]
237    #[must_use]
238    pub const fn scale_2d(x: f32, y: f32) -> Self {
239        Self::scale(x, y, 1.0)
240    }
241
242    /// Create uniform scale matrix.
243    #[inline]
244    #[must_use]
245    pub const fn scale_uniform(s: f32) -> Self {
246        Self::scale(s, s, s)
247    }
248
249    /// Create rotation around Z axis (2D rotation).
250    #[inline]
251    #[must_use]
252    pub fn rotation_z(angle_rad: f32) -> Self {
253        let (sin, cos) = angle_rad.sin_cos();
254        Self::from_data([
255            [cos, -sin, 0.0, 0.0],
256            [sin, cos, 0.0, 0.0],
257            [0.0, 0.0, 1.0, 0.0],
258            [0.0, 0.0, 0.0, 1.0],
259        ])
260    }
261
262    /// Create orthographic projection matrix.
263    #[inline]
264    #[must_use]
265    pub fn ortho(left: f32, right: f32, bottom: f32, top: f32, near: f32, far: f32) -> Self {
266        let width = right - left;
267        let height = top - bottom;
268        let depth = far - near;
269
270        Self::from_data([
271            [2.0 / width, 0.0, 0.0, -(right + left) / width],
272            [0.0, 2.0 / height, 0.0, -(top + bottom) / height],
273            [0.0, 0.0, -2.0 / depth, -(far + near) / depth],
274            [0.0, 0.0, 0.0, 1.0],
275        ])
276    }
277
278    /// Create orthographic projection for screen coordinates (Y down).
279    #[inline]
280    #[must_use]
281    pub fn ortho_screen(width: f32, height: f32) -> Self {
282        Self::ortho(0.0, width, height, 0.0, -1.0, 1.0)
283    }
284
285    /// Matrix multiplication.
286    #[inline]
287    #[must_use]
288    pub fn mul(&self, other: &Self) -> Self {
289        let mut result = Self::zero();
290        for i in 0..4 {
291            for j in 0..4 {
292                for k in 0..4 {
293                    result.data[i][j] += self.data[i][k] * other.data[k][j];
294                }
295            }
296        }
297        result
298    }
299
300    /// Transform a Vec4.
301    #[inline]
302    #[must_use]
303    pub fn transform_vec4(&self, v: Vec4) -> Vec4 {
304        Vec4::new(
305            self.data[0][3].mul_add(
306                v.w,
307                self.data[0][2].mul_add(v.z, self.data[0][0].mul_add(v.x, self.data[0][1] * v.y)),
308            ),
309            self.data[1][3].mul_add(
310                v.w,
311                self.data[1][2].mul_add(v.z, self.data[1][0].mul_add(v.x, self.data[1][1] * v.y)),
312            ),
313            self.data[2][3].mul_add(
314                v.w,
315                self.data[2][2].mul_add(v.z, self.data[2][0].mul_add(v.x, self.data[2][1] * v.y)),
316            ),
317            self.data[3][3].mul_add(
318                v.w,
319                self.data[3][2].mul_add(v.z, self.data[3][0].mul_add(v.x, self.data[3][1] * v.y)),
320            ),
321        )
322    }
323
324    /// Transform a 2D point (assumes z=0, w=1).
325    #[inline]
326    #[must_use]
327    pub fn transform_point(&self, p: Point) -> Point {
328        let v = self.transform_vec4(Vec4::from_point(p));
329        Point::new(v.x, v.y)
330    }
331
332    /// Transform a rectangle.
333    #[inline]
334    #[must_use]
335    pub fn transform_rect(&self, rect: &Rect) -> Rect {
336        let corners = [
337            Point::new(rect.x, rect.y),
338            Point::new(rect.x + rect.width, rect.y),
339            Point::new(rect.x + rect.width, rect.y + rect.height),
340            Point::new(rect.x, rect.y + rect.height),
341        ];
342
343        let transformed: Vec<Point> = corners.iter().map(|&p| self.transform_point(p)).collect();
344
345        let min_x = transformed
346            .iter()
347            .map(|p| p.x)
348            .fold(f32::INFINITY, f32::min);
349        let max_x = transformed
350            .iter()
351            .map(|p| p.x)
352            .fold(f32::NEG_INFINITY, f32::max);
353        let min_y = transformed
354            .iter()
355            .map(|p| p.y)
356            .fold(f32::INFINITY, f32::min);
357        let max_y = transformed
358            .iter()
359            .map(|p| p.y)
360            .fold(f32::NEG_INFINITY, f32::max);
361
362        Rect::new(min_x, min_y, max_x - min_x, max_y - min_y)
363    }
364
365    /// Get column as Vec4.
366    #[inline]
367    #[must_use]
368    pub const fn column(&self, idx: usize) -> Vec4 {
369        Vec4::new(
370            self.data[0][idx],
371            self.data[1][idx],
372            self.data[2][idx],
373            self.data[3][idx],
374        )
375    }
376
377    /// Get row as Vec4.
378    #[inline]
379    #[must_use]
380    pub const fn row(&self, idx: usize) -> Vec4 {
381        Vec4::new(
382            self.data[idx][0],
383            self.data[idx][1],
384            self.data[idx][2],
385            self.data[idx][3],
386        )
387    }
388
389    /// Transpose the matrix.
390    #[inline]
391    #[must_use]
392    pub const fn transpose(&self) -> Self {
393        Self::from_data([
394            [
395                self.data[0][0],
396                self.data[1][0],
397                self.data[2][0],
398                self.data[3][0],
399            ],
400            [
401                self.data[0][1],
402                self.data[1][1],
403                self.data[2][1],
404                self.data[3][1],
405            ],
406            [
407                self.data[0][2],
408                self.data[1][2],
409                self.data[2][2],
410                self.data[3][2],
411            ],
412            [
413                self.data[0][3],
414                self.data[1][3],
415                self.data[2][3],
416                self.data[3][3],
417            ],
418        ])
419    }
420}
421
422impl Default for Mat4 {
423    fn default() -> Self {
424        Self::identity()
425    }
426}
427
428impl std::ops::Mul for Mat4 {
429    type Output = Self;
430    fn mul(self, rhs: Self) -> Self {
431        Self::mul(&self, &rhs)
432    }
433}
434
435impl std::ops::Mul<Vec4> for Mat4 {
436    type Output = Vec4;
437    fn mul(self, rhs: Vec4) -> Vec4 {
438        self.transform_vec4(rhs)
439    }
440}
441
442/// Batch transform multiple points.
443///
444/// When SIMD is enabled, this uses vectorized operations for better performance.
445#[inline]
446#[must_use]
447pub fn batch_transform_points(points: &[Point], transform: &Mat4) -> Vec<Point> {
448    points
449        .iter()
450        .map(|&p| transform.transform_point(p))
451        .collect()
452}
453
454/// Batch transform multiple Vec4s.
455#[inline]
456#[must_use]
457pub fn batch_transform_vec4(vecs: &[Vec4], transform: &Mat4) -> Vec<Vec4> {
458    vecs.iter().map(|&v| transform.transform_vec4(v)).collect()
459}
460
461/// Batch linear interpolation.
462#[inline]
463#[must_use]
464pub fn batch_lerp_points(from: &[Point], to: &[Point], t: f32) -> Vec<Point> {
465    debug_assert_eq!(from.len(), to.len());
466    from.iter()
467        .zip(to.iter())
468        .map(|(a, b)| a.lerp(b, t))
469        .collect()
470}
471
472/// Axis-aligned bounding box from points.
473#[inline]
474#[must_use]
475pub fn bounding_box(points: &[Point]) -> Option<Rect> {
476    if points.is_empty() {
477        return None;
478    }
479
480    let mut min_x = f32::INFINITY;
481    let mut max_x = f32::NEG_INFINITY;
482    let mut min_y = f32::INFINITY;
483    let mut max_y = f32::NEG_INFINITY;
484
485    for p in points {
486        min_x = min_x.min(p.x);
487        max_x = max_x.max(p.x);
488        min_y = min_y.min(p.y);
489        max_y = max_y.max(p.y);
490    }
491
492    Some(Rect::new(min_x, min_y, max_x - min_x, max_y - min_y))
493}
494
495/// Calculate the centroid of points.
496#[inline]
497#[must_use]
498pub fn centroid(points: &[Point]) -> Option<Point> {
499    if points.is_empty() {
500        return None;
501    }
502
503    let sum: (f32, f32) = points
504        .iter()
505        .fold((0.0, 0.0), |acc, p| (acc.0 + p.x, acc.1 + p.y));
506    let n = points.len() as f32;
507    Some(Point::new(sum.0 / n, sum.1 / n))
508}
509
510/// Check if a point is inside a convex polygon.
511#[must_use]
512pub fn point_in_convex_polygon(point: Point, polygon: &[Point]) -> bool {
513    if polygon.len() < 3 {
514        return false;
515    }
516
517    let mut positive = false;
518    let mut negative = false;
519
520    for i in 0..polygon.len() {
521        let a = polygon[i];
522        let b = polygon[(i + 1) % polygon.len()];
523
524        let cross = (point.x - a.x).mul_add(b.y - a.y, -((point.y - a.y) * (b.x - a.x)));
525
526        if cross > 0.0 {
527            positive = true;
528        } else if cross < 0.0 {
529            negative = true;
530        }
531
532        if positive && negative {
533            return false;
534        }
535    }
536
537    true
538}
539
540/// Compute the area of a polygon using the shoelace formula.
541#[must_use]
542pub fn polygon_area(polygon: &[Point]) -> f32 {
543    if polygon.len() < 3 {
544        return 0.0;
545    }
546
547    let mut area = 0.0;
548    for i in 0..polygon.len() {
549        let j = (i + 1) % polygon.len();
550        area += polygon[i].x * polygon[j].y;
551        area -= polygon[j].x * polygon[i].y;
552    }
553
554    (area / 2.0).abs()
555}
556
557// =============================================================================
558// SIMD-accelerated implementations when trueno is available
559// =============================================================================
560
561// =============================================================================
562// ComputeBlocks: Auto-Accelerated Aggregation Primitives
563// =============================================================================
564//
565// ComputeBlocks are data processing primitives with automatic acceleration.
566// They select the optimal execution path at compile time:
567//
568// 1. **Auto-vectorization**: Tight 4-way loops enable compiler SIMD codegen
569// 2. **ILP (Instruction-Level Parallelism)**: Multiple accumulators hide latency
570// 3. **Cache-friendly**: Sequential access patterns for prefetcher efficiency
571//
572// These primitives form the foundation for widget data processing:
573// - Charts: range calculation, normalization, statistics
574// - Heatmaps: binning, histogram generation
575// - Tables: aggregation, sorting support
576//
577// Reference: PROBAR-SPEC-009 (ComputeBlock Architecture)
578// Reference: docs/specifications/computeblocks-refactor.md Section 2
579
580/// SIMD-friendly sum of f64 values.
581///
582/// Uses 4-way accumulator for instruction-level parallelism and auto-vectorization.
583/// For small slices (<4), falls back to direct sum.
584///
585/// # Example
586/// ```
587/// use presentar_core::simd::batch_sum_f64;
588/// let values = vec![1.0, 2.0, 3.0, 4.0];
589/// assert_eq!(batch_sum_f64(&values), 10.0);
590/// ```
591#[inline]
592#[must_use]
593pub fn batch_sum_f64(values: &[f64]) -> f64 {
594    if values.len() < 4 {
595        return values.iter().sum();
596    }
597
598    // 4-way accumulator for ILP and auto-vectorization.
599    // `as_chunks::<4>()` is `chunks_exact(4)` plus its remainder, as one
600    // destructuring, and yields `&[f64; 4]` so the fixed indexing below is
601    // bounds-checked at compile time. clippy::chunks_exact_to_as_chunks (1.98).
602    let (chunks, remainder) = values.as_chunks::<4>();
603
604    let mut acc = [0.0f64; 4];
605    for chunk in chunks {
606        acc[0] += chunk[0];
607        acc[1] += chunk[1];
608        acc[2] += chunk[2];
609        acc[3] += chunk[3];
610    }
611
612    let mut sum = acc[0] + acc[1] + acc[2] + acc[3];
613    for &v in remainder {
614        sum += v;
615    }
616    sum
617}
618
619/// SIMD-friendly mean of f64 values.
620///
621/// Returns 0.0 for empty slices.
622///
623/// # Example
624/// ```
625/// use presentar_core::simd::batch_mean_f64;
626/// let values = vec![2.0, 4.0, 6.0, 8.0];
627/// assert_eq!(batch_mean_f64(&values), 5.0);
628/// ```
629#[inline]
630#[must_use]
631pub fn batch_mean_f64(values: &[f64]) -> f64 {
632    if values.is_empty() {
633        return 0.0;
634    }
635    batch_sum_f64(values) / values.len() as f64
636}
637
638/// SIMD-friendly min/max of f64 values.
639///
640/// Returns `None` for empty slices.
641/// Uses 4-way comparison for auto-vectorization.
642///
643/// # Example
644/// ```
645/// use presentar_core::simd::batch_min_max_f64;
646/// let values = vec![3.0, 1.0, 4.0, 1.0, 5.0, 9.0];
647/// let (min, max) = batch_min_max_f64(&values).unwrap();
648/// assert_eq!(min, 1.0);
649/// assert_eq!(max, 9.0);
650/// ```
651#[inline]
652#[must_use]
653pub fn batch_min_max_f64(values: &[f64]) -> Option<(f64, f64)> {
654    if values.is_empty() {
655        return None;
656    }
657
658    if values.len() < 4 {
659        let mut min = values[0];
660        let mut max = values[0];
661        for &v in &values[1..] {
662            min = min.min(v);
663            max = max.max(v);
664        }
665        return Some((min, max));
666    }
667
668    // 4-way min/max for auto-vectorization. See batch_sum_f64 on as_chunks.
669    let (chunks, remainder) = values.as_chunks::<4>();
670
671    let mut min_acc = [f64::INFINITY; 4];
672    let mut max_acc = [f64::NEG_INFINITY; 4];
673
674    for chunk in chunks {
675        min_acc[0] = min_acc[0].min(chunk[0]);
676        min_acc[1] = min_acc[1].min(chunk[1]);
677        min_acc[2] = min_acc[2].min(chunk[2]);
678        min_acc[3] = min_acc[3].min(chunk[3]);
679
680        max_acc[0] = max_acc[0].max(chunk[0]);
681        max_acc[1] = max_acc[1].max(chunk[1]);
682        max_acc[2] = max_acc[2].max(chunk[2]);
683        max_acc[3] = max_acc[3].max(chunk[3]);
684    }
685
686    let mut min = min_acc[0].min(min_acc[1]).min(min_acc[2]).min(min_acc[3]);
687    let mut max = max_acc[0].max(max_acc[1]).max(max_acc[2]).max(max_acc[3]);
688
689    for &v in remainder {
690        min = min.min(v);
691        max = max.max(v);
692    }
693
694    Some((min, max))
695}
696
697/// SIMD-friendly normalization to [0, 1] range.
698///
699/// Normalizes values using `(v - min) / (max - min)`.
700/// Returns empty vec for empty input, all zeros if min == max.
701///
702/// # Example
703/// ```
704/// use presentar_core::simd::normalize_f64;
705/// let values = vec![0.0, 50.0, 100.0];
706/// let normalized = normalize_f64(&values);
707/// assert_eq!(normalized, vec![0.0, 0.5, 1.0]);
708/// ```
709#[inline]
710#[must_use]
711pub fn normalize_f64(values: &[f64]) -> Vec<f64> {
712    if values.is_empty() {
713        return Vec::new();
714    }
715
716    let Some((min, max)) = batch_min_max_f64(values) else {
717        return Vec::new();
718    };
719
720    let range = max - min;
721    if range == 0.0 {
722        return vec![0.0; values.len()];
723    }
724
725    let inv_range = 1.0 / range;
726    values.iter().map(|&v| (v - min) * inv_range).collect()
727}
728
729/// SIMD-friendly normalization with provided range.
730///
731/// Normalizes values using `(v - min) / (max - min)` with caller-provided bounds.
732/// More efficient when min/max are already known.
733///
734/// # Example
735/// ```
736/// use presentar_core::simd::normalize_with_range_f64;
737/// let values = vec![25.0, 50.0, 75.0];
738/// let normalized = normalize_with_range_f64(&values, 0.0, 100.0);
739/// assert_eq!(normalized, vec![0.25, 0.5, 0.75]);
740/// ```
741#[inline]
742#[must_use]
743pub fn normalize_with_range_f64(values: &[f64], min: f64, max: f64) -> Vec<f64> {
744    let range = max - min;
745    if range == 0.0 {
746        return vec![0.0; values.len()];
747    }
748
749    let inv_range = 1.0 / range;
750    values.iter().map(|&v| (v - min) * inv_range).collect()
751}
752
753/// SIMD-friendly scale operation.
754///
755/// Multiplies all values by a scalar.
756///
757/// # Example
758/// ```
759/// use presentar_core::simd::batch_scale_f64;
760/// let values = vec![1.0, 2.0, 3.0];
761/// let scaled = batch_scale_f64(&values, 2.0);
762/// assert_eq!(scaled, vec![2.0, 4.0, 6.0]);
763/// ```
764#[inline]
765#[must_use]
766pub fn batch_scale_f64(values: &[f64], scale: f64) -> Vec<f64> {
767    values.iter().map(|&v| v * scale).collect()
768}
769
770/// SIMD-friendly scale and offset operation.
771///
772/// Applies `v * scale + offset` to all values.
773/// Common operation for data transformation.
774///
775/// # Example
776/// ```
777/// use presentar_core::simd::batch_scale_offset_f64;
778/// let values = vec![0.0, 0.5, 1.0];
779/// // Map [0, 1] to [100, 200]
780/// let transformed = batch_scale_offset_f64(&values, 100.0, 100.0);
781/// assert_eq!(transformed, vec![100.0, 150.0, 200.0]);
782/// ```
783#[inline]
784#[must_use]
785pub fn batch_scale_offset_f64(values: &[f64], scale: f64, offset: f64) -> Vec<f64> {
786    values.iter().map(|&v| v.mul_add(scale, offset)).collect()
787}
788
789/// SIMD-friendly variance calculation (population variance).
790///
791/// Uses two-pass algorithm: first calculates mean, then variance.
792/// Returns 0.0 for empty or single-element slices.
793///
794/// # Example
795/// ```
796/// use presentar_core::simd::batch_variance_f64;
797/// let values = vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
798/// let variance = batch_variance_f64(&values);
799/// assert!((variance - 4.0).abs() < 0.001);
800/// ```
801#[inline]
802#[must_use]
803pub fn batch_variance_f64(values: &[f64]) -> f64 {
804    if values.len() < 2 {
805        return 0.0;
806    }
807
808    let mean = batch_mean_f64(values);
809    let sum_sq: f64 = values.iter().map(|&v| (v - mean) * (v - mean)).sum();
810    sum_sq / values.len() as f64
811}
812
813/// SIMD-friendly standard deviation.
814///
815/// Returns square root of population variance.
816///
817/// # Example
818/// ```
819/// use presentar_core::simd::batch_stddev_f64;
820/// let values = vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
821/// let stddev = batch_stddev_f64(&values);
822/// assert!((stddev - 2.0).abs() < 0.001);
823/// ```
824#[inline]
825#[must_use]
826pub fn batch_stddev_f64(values: &[f64]) -> f64 {
827    batch_variance_f64(values).sqrt()
828}
829
830/// SIMD-friendly weighted sum.
831///
832/// Computes sum(values[i] * weights[i]).
833/// Panics in debug mode if lengths differ.
834///
835/// # Example
836/// ```
837/// use presentar_core::simd::weighted_sum_f64;
838/// let values = vec![1.0, 2.0, 3.0];
839/// let weights = vec![0.5, 0.3, 0.2];
840/// let result = weighted_sum_f64(&values, &weights);
841/// assert!((result - 1.7).abs() < 0.001);
842/// ```
843#[inline]
844#[must_use]
845pub fn weighted_sum_f64(values: &[f64], weights: &[f64]) -> f64 {
846    debug_assert_eq!(values.len(), weights.len());
847
848    if values.len() < 4 {
849        return values
850            .iter()
851            .zip(weights.iter())
852            .map(|(&v, &w)| v * w)
853            .sum();
854    }
855
856    // 4-way accumulator. See batch_sum_f64 on as_chunks.
857    let (v_chunks, v_rem) = values.as_chunks::<4>();
858    let (w_chunks, w_rem) = weights.as_chunks::<4>();
859
860    let mut acc = [0.0f64; 4];
861    for (vc, wc) in v_chunks.iter().zip(w_chunks.iter()) {
862        acc[0] = vc[0].mul_add(wc[0], acc[0]);
863        acc[1] = vc[1].mul_add(wc[1], acc[1]);
864        acc[2] = vc[2].mul_add(wc[2], acc[2]);
865        acc[3] = vc[3].mul_add(wc[3], acc[3]);
866    }
867
868    let mut sum = acc[0] + acc[1] + acc[2] + acc[3];
869    for (&v, &w) in v_rem.iter().zip(w_rem.iter()) {
870        sum = v.mul_add(w, sum);
871    }
872    sum
873}
874
875/// SIMD-friendly percentile calculation.
876///
877/// Finds the value at the given percentile (0.0-1.0).
878/// Uses linear interpolation for non-integer indices.
879/// Values must be sorted in ascending order.
880///
881/// # Example
882/// ```
883/// use presentar_core::simd::percentile_sorted_f64;
884/// let sorted = vec![1.0, 2.0, 3.0, 4.0, 5.0];
885/// assert_eq!(percentile_sorted_f64(&sorted, 0.5), 3.0); // median
886/// ```
887#[inline]
888#[must_use]
889pub fn percentile_sorted_f64(sorted_values: &[f64], p: f64) -> f64 {
890    if sorted_values.is_empty() {
891        return 0.0;
892    }
893    if sorted_values.len() == 1 {
894        return sorted_values[0];
895    }
896
897    let p = p.clamp(0.0, 1.0);
898    let n = sorted_values.len() as f64;
899    let idx = p * (n - 1.0);
900    let lower = idx.floor() as usize;
901    let upper = idx.ceil() as usize;
902
903    if lower == upper {
904        sorted_values[lower]
905    } else {
906        let frac = idx - lower as f64;
907        sorted_values[lower].mul_add(1.0 - frac, sorted_values[upper] * frac)
908    }
909}
910
911/// Compute histogram bin counts for f64 data.
912///
913/// Assigns values to bins and returns counts per bin.
914/// Efficient for visualization (heatmaps, histograms).
915///
916/// # Example
917/// ```
918/// use presentar_core::simd::histogram_f64;
919/// let values = vec![0.1, 0.5, 0.9, 1.5, 2.5, 3.5, 4.5];
920/// let counts = histogram_f64(&values, 0.0, 5.0, 5);
921/// assert_eq!(counts, vec![3, 1, 1, 1, 1]); // bins: [0-1), [1-2), [2-3), [3-4), [4-5]
922/// ```
923#[inline]
924#[must_use]
925pub fn histogram_f64(values: &[f64], min: f64, max: f64, num_bins: usize) -> Vec<usize> {
926    if num_bins == 0 || values.is_empty() {
927        return vec![0; num_bins];
928    }
929
930    let range = max - min;
931    if range <= 0.0 {
932        // All values in first bin
933        return {
934            let mut counts = vec![0; num_bins];
935            counts[0] = values.len();
936            counts
937        };
938    }
939
940    let bin_width = range / num_bins as f64;
941    let mut counts = vec![0usize; num_bins];
942
943    for &v in values {
944        let bin = ((v - min) / bin_width) as usize;
945        let bin = bin.min(num_bins - 1); // Clamp to last bin
946        counts[bin] += 1;
947    }
948
949    counts
950}
951
952#[cfg(test)]
953mod tests {
954    use super::*;
955
956    // =========================================================================
957    // Vec4 Tests
958    // =========================================================================
959
960    #[test]
961    fn test_vec4_new() {
962        let v = Vec4::new(1.0, 2.0, 3.0, 4.0);
963        assert_eq!(v.x, 1.0);
964        assert_eq!(v.y, 2.0);
965        assert_eq!(v.z, 3.0);
966        assert_eq!(v.w, 4.0);
967    }
968
969    #[test]
970    fn test_vec4_zero() {
971        let v = Vec4::zero();
972        assert_eq!(v, Vec4::new(0.0, 0.0, 0.0, 0.0));
973    }
974
975    #[test]
976    fn test_vec4_default() {
977        let v = Vec4::default();
978        assert_eq!(v, Vec4::zero());
979    }
980
981    #[test]
982    fn test_vec4_from_point() {
983        let p = Point::new(10.0, 20.0);
984        let v = Vec4::from_point(p);
985        assert_eq!(v, Vec4::new(10.0, 20.0, 0.0, 1.0));
986    }
987
988    #[test]
989    fn test_vec4_to_point() {
990        let v = Vec4::new(5.0, 15.0, 25.0, 35.0);
991        let p = v.to_point();
992        assert_eq!(p, Point::new(5.0, 15.0));
993    }
994
995    #[test]
996    fn test_vec4_dot() {
997        let a = Vec4::new(1.0, 2.0, 3.0, 4.0);
998        let b = Vec4::new(2.0, 3.0, 4.0, 5.0);
999        assert_eq!(
1000            a.dot(b),
1001            4.0f32.mul_add(5.0, 3.0f32.mul_add(4.0, 1.0f32.mul_add(2.0, 2.0 * 3.0)))
1002        );
1003    }
1004
1005    #[test]
1006    fn test_vec4_add() {
1007        let a = Vec4::new(1.0, 2.0, 3.0, 4.0);
1008        let b = Vec4::new(5.0, 6.0, 7.0, 8.0);
1009        let c = a.add(b);
1010        assert_eq!(c, Vec4::new(6.0, 8.0, 10.0, 12.0));
1011    }
1012
1013    #[test]
1014    fn test_vec4_sub() {
1015        let a = Vec4::new(5.0, 6.0, 7.0, 8.0);
1016        let b = Vec4::new(1.0, 2.0, 3.0, 4.0);
1017        let c = a.sub(b);
1018        assert_eq!(c, Vec4::new(4.0, 4.0, 4.0, 4.0));
1019    }
1020
1021    #[test]
1022    fn test_vec4_scale() {
1023        let v = Vec4::new(1.0, 2.0, 3.0, 4.0);
1024        let s = v.scale(2.0);
1025        assert_eq!(s, Vec4::new(2.0, 4.0, 6.0, 8.0));
1026    }
1027
1028    #[test]
1029    fn test_vec4_mul() {
1030        let a = Vec4::new(1.0, 2.0, 3.0, 4.0);
1031        let b = Vec4::new(2.0, 2.0, 2.0, 2.0);
1032        let c = a.mul(b);
1033        assert_eq!(c, Vec4::new(2.0, 4.0, 6.0, 8.0));
1034    }
1035
1036    #[test]
1037    fn test_vec4_lerp() {
1038        let a = Vec4::new(0.0, 0.0, 0.0, 0.0);
1039        let b = Vec4::new(10.0, 10.0, 10.0, 10.0);
1040        let c = a.lerp(b, 0.5);
1041        assert_eq!(c, Vec4::new(5.0, 5.0, 5.0, 5.0));
1042    }
1043
1044    #[test]
1045    fn test_vec4_length() {
1046        let v = Vec4::new(3.0, 4.0, 0.0, 0.0);
1047        assert!((v.length() - 5.0).abs() < 0.0001);
1048    }
1049
1050    #[test]
1051    fn test_vec4_normalize() {
1052        let v = Vec4::new(3.0, 4.0, 0.0, 0.0);
1053        let n = v.normalize();
1054        assert!((n.length() - 1.0).abs() < 0.0001);
1055    }
1056
1057    #[test]
1058    fn test_vec4_from_impl() {
1059        let p = Point::new(1.0, 2.0);
1060        let v: Vec4 = p.into();
1061        assert_eq!(v, Vec4::new(1.0, 2.0, 0.0, 1.0));
1062    }
1063
1064    // =========================================================================
1065    // Mat4 Tests
1066    // =========================================================================
1067
1068    #[test]
1069    fn test_mat4_identity() {
1070        let m = Mat4::identity();
1071        assert_eq!(m.data[0][0], 1.0);
1072        assert_eq!(m.data[1][1], 1.0);
1073        assert_eq!(m.data[2][2], 1.0);
1074        assert_eq!(m.data[3][3], 1.0);
1075        assert_eq!(m.data[0][1], 0.0);
1076    }
1077
1078    #[test]
1079    fn test_mat4_zero() {
1080        let m = Mat4::zero();
1081        for i in 0..4 {
1082            for j in 0..4 {
1083                assert_eq!(m.data[i][j], 0.0);
1084            }
1085        }
1086    }
1087
1088    #[test]
1089    fn test_mat4_default() {
1090        let m = Mat4::default();
1091        assert_eq!(m, Mat4::identity());
1092    }
1093
1094    #[test]
1095    fn test_mat4_translation() {
1096        let m = Mat4::translation(10.0, 20.0, 30.0);
1097        let p = Point::new(0.0, 0.0);
1098        let t = m.transform_point(p);
1099        assert_eq!(t, Point::new(10.0, 20.0));
1100    }
1101
1102    #[test]
1103    fn test_mat4_translation_2d() {
1104        let m = Mat4::translation_2d(5.0, 15.0);
1105        let p = Point::new(10.0, 10.0);
1106        let t = m.transform_point(p);
1107        assert_eq!(t, Point::new(15.0, 25.0));
1108    }
1109
1110    #[test]
1111    fn test_mat4_scale() {
1112        let m = Mat4::scale(2.0, 3.0, 4.0);
1113        let p = Point::new(10.0, 10.0);
1114        let t = m.transform_point(p);
1115        assert_eq!(t, Point::new(20.0, 30.0));
1116    }
1117
1118    #[test]
1119    fn test_mat4_scale_2d() {
1120        let m = Mat4::scale_2d(0.5, 2.0);
1121        let p = Point::new(10.0, 10.0);
1122        let t = m.transform_point(p);
1123        assert_eq!(t, Point::new(5.0, 20.0));
1124    }
1125
1126    #[test]
1127    fn test_mat4_scale_uniform() {
1128        let m = Mat4::scale_uniform(2.0);
1129        let p = Point::new(5.0, 5.0);
1130        let t = m.transform_point(p);
1131        assert_eq!(t, Point::new(10.0, 10.0));
1132    }
1133
1134    #[test]
1135    fn test_mat4_rotation_z() {
1136        use std::f32::consts::PI;
1137        let m = Mat4::rotation_z(PI / 2.0); // 90 degrees
1138        let p = Point::new(1.0, 0.0);
1139        let t = m.transform_point(p);
1140        // Should rotate (1, 0) to approximately (0, 1)
1141        assert!((t.x - 0.0).abs() < 0.0001);
1142        assert!((t.y - 1.0).abs() < 0.0001);
1143    }
1144
1145    #[test]
1146    fn test_mat4_mul_identity() {
1147        let a = Mat4::identity();
1148        let b = Mat4::identity();
1149        let c = a.mul(&b);
1150        assert_eq!(c, Mat4::identity());
1151    }
1152
1153    #[test]
1154    fn test_mat4_mul_combined_transform() {
1155        let translate = Mat4::translation_2d(10.0, 0.0);
1156        let scale = Mat4::scale_2d(2.0, 2.0);
1157        let combined = translate.mul(&scale);
1158
1159        let p = Point::new(5.0, 5.0);
1160        let t = combined.transform_point(p);
1161        // First scale (5,5) -> (10, 10), then translate -> (20, 10)
1162        assert_eq!(t, Point::new(20.0, 10.0));
1163    }
1164
1165    #[test]
1166    fn test_mat4_transform_rect() {
1167        let m = Mat4::scale_2d(2.0, 2.0);
1168        let rect = Rect::new(10.0, 10.0, 20.0, 30.0);
1169        let t = m.transform_rect(&rect);
1170        assert_eq!(t.x, 20.0);
1171        assert_eq!(t.y, 20.0);
1172        assert_eq!(t.width, 40.0);
1173        assert_eq!(t.height, 60.0);
1174    }
1175
1176    #[test]
1177    fn test_mat4_transpose() {
1178        let m = Mat4::from_data([
1179            [1.0, 2.0, 3.0, 4.0],
1180            [5.0, 6.0, 7.0, 8.0],
1181            [9.0, 10.0, 11.0, 12.0],
1182            [13.0, 14.0, 15.0, 16.0],
1183        ]);
1184        let t = m.transpose();
1185        assert_eq!(t.data[0][1], 5.0);
1186        assert_eq!(t.data[1][0], 2.0);
1187    }
1188
1189    #[test]
1190    fn test_mat4_column() {
1191        let m = Mat4::identity();
1192        let col = m.column(0);
1193        assert_eq!(col, Vec4::new(1.0, 0.0, 0.0, 0.0));
1194    }
1195
1196    #[test]
1197    fn test_mat4_row() {
1198        let m = Mat4::identity();
1199        let row = m.row(0);
1200        assert_eq!(row, Vec4::new(1.0, 0.0, 0.0, 0.0));
1201    }
1202
1203    #[test]
1204    fn test_mat4_ortho_screen() {
1205        let m = Mat4::ortho_screen(800.0, 600.0);
1206        // Point at top-left should map to (-1, 1) in NDC
1207        let p = m.transform_vec4(Vec4::new(0.0, 0.0, 0.0, 1.0));
1208        assert!((p.x - (-1.0)).abs() < 0.001);
1209        assert!((p.y - 1.0).abs() < 0.001);
1210    }
1211
1212    #[test]
1213    fn test_mat4_mul_operator() {
1214        let a = Mat4::translation_2d(10.0, 20.0);
1215        let b = Mat4::scale_2d(2.0, 2.0);
1216        let c = a * b;
1217        assert_eq!(c, a.mul(&b));
1218    }
1219
1220    #[test]
1221    fn test_mat4_mul_vec4_operator() {
1222        let m = Mat4::translation_2d(10.0, 20.0);
1223        let v = Vec4::new(0.0, 0.0, 0.0, 1.0);
1224        let r = m * v;
1225        assert_eq!(r, Vec4::new(10.0, 20.0, 0.0, 1.0));
1226    }
1227
1228    // =========================================================================
1229    // Batch Operation Tests
1230    // =========================================================================
1231
1232    #[test]
1233    fn test_batch_transform_points() {
1234        let m = Mat4::translation_2d(10.0, 10.0);
1235        let points = vec![Point::new(0.0, 0.0), Point::new(5.0, 5.0)];
1236        let result = batch_transform_points(&points, &m);
1237        assert_eq!(result[0], Point::new(10.0, 10.0));
1238        assert_eq!(result[1], Point::new(15.0, 15.0));
1239    }
1240
1241    #[test]
1242    fn test_batch_transform_vec4() {
1243        let m = Mat4::scale_uniform(2.0);
1244        let vecs = vec![Vec4::new(1.0, 1.0, 1.0, 0.0), Vec4::new(2.0, 2.0, 2.0, 0.0)];
1245        let result = batch_transform_vec4(&vecs, &m);
1246        assert_eq!(result[0], Vec4::new(2.0, 2.0, 2.0, 0.0));
1247        assert_eq!(result[1], Vec4::new(4.0, 4.0, 4.0, 0.0));
1248    }
1249
1250    #[test]
1251    fn test_batch_lerp_points() {
1252        let from = vec![Point::new(0.0, 0.0), Point::new(10.0, 10.0)];
1253        let to = vec![Point::new(10.0, 10.0), Point::new(20.0, 20.0)];
1254        let result = batch_lerp_points(&from, &to, 0.5);
1255        assert_eq!(result[0], Point::new(5.0, 5.0));
1256        assert_eq!(result[1], Point::new(15.0, 15.0));
1257    }
1258
1259    // =========================================================================
1260    // Geometry Tests
1261    // =========================================================================
1262
1263    #[test]
1264    fn test_bounding_box() {
1265        let points = vec![
1266            Point::new(0.0, 0.0),
1267            Point::new(10.0, 5.0),
1268            Point::new(5.0, 15.0),
1269        ];
1270        let bbox = bounding_box(&points).unwrap();
1271        assert_eq!(bbox.x, 0.0);
1272        assert_eq!(bbox.y, 0.0);
1273        assert_eq!(bbox.width, 10.0);
1274        assert_eq!(bbox.height, 15.0);
1275    }
1276
1277    #[test]
1278    fn test_bounding_box_empty() {
1279        let points: Vec<Point> = vec![];
1280        assert!(bounding_box(&points).is_none());
1281    }
1282
1283    #[test]
1284    fn test_centroid() {
1285        let points = vec![
1286            Point::new(0.0, 0.0),
1287            Point::new(10.0, 0.0),
1288            Point::new(10.0, 10.0),
1289            Point::new(0.0, 10.0),
1290        ];
1291        let c = centroid(&points).unwrap();
1292        assert_eq!(c, Point::new(5.0, 5.0));
1293    }
1294
1295    #[test]
1296    fn test_centroid_empty() {
1297        let points: Vec<Point> = vec![];
1298        assert!(centroid(&points).is_none());
1299    }
1300
1301    #[test]
1302    fn test_point_in_convex_polygon() {
1303        let square = vec![
1304            Point::new(0.0, 0.0),
1305            Point::new(10.0, 0.0),
1306            Point::new(10.0, 10.0),
1307            Point::new(0.0, 10.0),
1308        ];
1309
1310        assert!(point_in_convex_polygon(Point::new(5.0, 5.0), &square));
1311        assert!(!point_in_convex_polygon(Point::new(15.0, 5.0), &square));
1312    }
1313
1314    #[test]
1315    fn test_point_in_convex_polygon_edge() {
1316        let triangle = vec![
1317            Point::new(0.0, 0.0),
1318            Point::new(10.0, 0.0),
1319            Point::new(5.0, 10.0),
1320        ];
1321
1322        // On the edge
1323        assert!(point_in_convex_polygon(Point::new(5.0, 0.0), &triangle));
1324    }
1325
1326    #[test]
1327    fn test_polygon_area_square() {
1328        let square = vec![
1329            Point::new(0.0, 0.0),
1330            Point::new(10.0, 0.0),
1331            Point::new(10.0, 10.0),
1332            Point::new(0.0, 10.0),
1333        ];
1334        let area = polygon_area(&square);
1335        assert!((area - 100.0).abs() < 0.0001);
1336    }
1337
1338    #[test]
1339    fn test_polygon_area_triangle() {
1340        let triangle = vec![
1341            Point::new(0.0, 0.0),
1342            Point::new(10.0, 0.0),
1343            Point::new(5.0, 10.0),
1344        ];
1345        let area = polygon_area(&triangle);
1346        assert!((area - 50.0).abs() < 0.0001);
1347    }
1348
1349    #[test]
1350    fn test_polygon_area_too_few_points() {
1351        assert_eq!(polygon_area(&[]), 0.0);
1352        assert_eq!(polygon_area(&[Point::new(0.0, 0.0)]), 0.0);
1353        assert_eq!(
1354            polygon_area(&[Point::new(0.0, 0.0), Point::new(1.0, 1.0)]),
1355            0.0
1356        );
1357    }
1358
1359    // =========================================================================
1360    // SIMD Tests (when feature enabled)
1361    // =========================================================================
1362
1363    // =========================================================================
1364    // ComputeBlock Tests (f64 Aggregation)
1365    // =========================================================================
1366
1367    mod compute_block_tests {
1368        use super::*;
1369
1370        #[test]
1371        fn test_batch_sum_f64_empty() {
1372            assert_eq!(batch_sum_f64(&[]), 0.0);
1373        }
1374
1375        #[test]
1376        fn test_batch_sum_f64_small() {
1377            assert_eq!(batch_sum_f64(&[1.0, 2.0, 3.0]), 6.0);
1378        }
1379
1380        #[test]
1381        fn test_batch_sum_f64_large() {
1382            let values: Vec<f64> = (1..=100).map(f64::from).collect();
1383            assert_eq!(batch_sum_f64(&values), 5050.0);
1384        }
1385
1386        #[test]
1387        fn test_batch_sum_f64_exact_chunks() {
1388            // 8 elements = 2 exact chunks of 4
1389            let values = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
1390            assert_eq!(batch_sum_f64(&values), 36.0);
1391        }
1392
1393        #[test]
1394        fn test_batch_mean_f64_empty() {
1395            assert_eq!(batch_mean_f64(&[]), 0.0);
1396        }
1397
1398        #[test]
1399        fn test_batch_mean_f64() {
1400            assert_eq!(batch_mean_f64(&[2.0, 4.0, 6.0, 8.0]), 5.0);
1401        }
1402
1403        #[test]
1404        fn test_batch_min_max_f64_empty() {
1405            assert!(batch_min_max_f64(&[]).is_none());
1406        }
1407
1408        #[test]
1409        fn test_batch_min_max_f64_single() {
1410            assert_eq!(batch_min_max_f64(&[42.0]), Some((42.0, 42.0)));
1411        }
1412
1413        #[test]
1414        fn test_batch_min_max_f64_small() {
1415            assert_eq!(batch_min_max_f64(&[3.0, 1.0, 2.0]), Some((1.0, 3.0)));
1416        }
1417
1418        #[test]
1419        fn test_batch_min_max_f64_large() {
1420            let values: Vec<f64> = (1..=256).map(f64::from).collect();
1421            assert_eq!(batch_min_max_f64(&values), Some((1.0, 256.0)));
1422        }
1423
1424        #[test]
1425        fn test_normalize_f64_empty() {
1426            assert!(normalize_f64(&[]).is_empty());
1427        }
1428
1429        #[test]
1430        fn test_normalize_f64_constant() {
1431            let result = normalize_f64(&[5.0, 5.0, 5.0]);
1432            assert_eq!(result, vec![0.0, 0.0, 0.0]);
1433        }
1434
1435        #[test]
1436        fn test_normalize_f64() {
1437            let result = normalize_f64(&[0.0, 50.0, 100.0]);
1438            assert_eq!(result, vec![0.0, 0.5, 1.0]);
1439        }
1440
1441        #[test]
1442        fn test_normalize_with_range_f64() {
1443            let result = normalize_with_range_f64(&[25.0, 50.0, 75.0], 0.0, 100.0);
1444            assert_eq!(result, vec![0.25, 0.5, 0.75]);
1445        }
1446
1447        #[test]
1448        fn test_batch_scale_f64() {
1449            let result = batch_scale_f64(&[1.0, 2.0, 3.0], 2.0);
1450            assert_eq!(result, vec![2.0, 4.0, 6.0]);
1451        }
1452
1453        #[test]
1454        fn test_batch_scale_offset_f64() {
1455            let result = batch_scale_offset_f64(&[0.0, 0.5, 1.0], 100.0, 100.0);
1456            assert_eq!(result, vec![100.0, 150.0, 200.0]);
1457        }
1458
1459        #[test]
1460        fn test_batch_variance_f64_empty() {
1461            assert_eq!(batch_variance_f64(&[]), 0.0);
1462        }
1463
1464        #[test]
1465        fn test_batch_variance_f64_single() {
1466            assert_eq!(batch_variance_f64(&[42.0]), 0.0);
1467        }
1468
1469        #[test]
1470        fn test_batch_variance_f64() {
1471            let values = vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
1472            let variance = batch_variance_f64(&values);
1473            assert!((variance - 4.0).abs() < 0.001);
1474        }
1475
1476        #[test]
1477        fn test_batch_stddev_f64() {
1478            let values = vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
1479            let stddev = batch_stddev_f64(&values);
1480            assert!((stddev - 2.0).abs() < 0.001);
1481        }
1482
1483        #[test]
1484        fn test_weighted_sum_f64_small() {
1485            let values = vec![1.0, 2.0];
1486            let weights = vec![0.5, 0.5];
1487            assert_eq!(weighted_sum_f64(&values, &weights), 1.5);
1488        }
1489
1490        #[test]
1491        fn test_weighted_sum_f64_large() {
1492            let values = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
1493            let weights = vec![1.0; 8];
1494            assert_eq!(weighted_sum_f64(&values, &weights), 36.0);
1495        }
1496
1497        #[test]
1498        fn test_percentile_sorted_f64_empty() {
1499            assert_eq!(percentile_sorted_f64(&[], 0.5), 0.0);
1500        }
1501
1502        #[test]
1503        fn test_percentile_sorted_f64_single() {
1504            assert_eq!(percentile_sorted_f64(&[42.0], 0.5), 42.0);
1505        }
1506
1507        #[test]
1508        fn test_percentile_sorted_f64_median() {
1509            let sorted = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1510            assert_eq!(percentile_sorted_f64(&sorted, 0.5), 3.0);
1511        }
1512
1513        #[test]
1514        fn test_percentile_sorted_f64_quartiles() {
1515            let sorted = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1516            assert_eq!(percentile_sorted_f64(&sorted, 0.0), 1.0);
1517            assert_eq!(percentile_sorted_f64(&sorted, 1.0), 5.0);
1518            assert_eq!(percentile_sorted_f64(&sorted, 0.25), 2.0);
1519            assert_eq!(percentile_sorted_f64(&sorted, 0.75), 4.0);
1520        }
1521
1522        #[test]
1523        fn test_histogram_f64_empty() {
1524            assert_eq!(histogram_f64(&[], 0.0, 10.0, 5), vec![0; 5]);
1525        }
1526
1527        #[test]
1528        fn test_histogram_f64() {
1529            let values = vec![0.1, 0.5, 0.9, 1.5, 2.5, 3.5, 4.5];
1530            let counts = histogram_f64(&values, 0.0, 5.0, 5);
1531            assert_eq!(counts, vec![3, 1, 1, 1, 1]);
1532        }
1533
1534        #[test]
1535        fn test_histogram_f64_edge_values() {
1536            // Values exactly at bin boundaries
1537            let values = vec![0.0, 1.0, 2.0, 3.0, 4.0, 4.999];
1538            let counts = histogram_f64(&values, 0.0, 5.0, 5);
1539            assert_eq!(counts, vec![1, 1, 1, 1, 2]); // 4.0 and 4.999 in last bin
1540        }
1541
1542        #[test]
1543        fn test_histogram_f64_uniform() {
1544            // Uniform distribution across bins
1545            let values: Vec<f64> = (0..100).map(f64::from).collect();
1546            let counts = histogram_f64(&values, 0.0, 100.0, 10);
1547            // Each bin should have 10 values
1548            assert!(counts.iter().all(|&c| c == 10));
1549        }
1550    }
1551}