Skip to main content

trueno/vector/ops/reductions/
stats.rs

1//! Statistical and numerically stable reduction operations for Vector<f32>
2//!
3//! Provides: `sum_kahan`, `sum_of_squares`, `mean`, `variance`, `stddev`,
4//! `covariance`, `correlation`.
5
6#[cfg(target_arch = "x86_64")]
7use crate::backends::avx2::Avx2Backend;
8#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
9use crate::backends::neon::NeonBackend;
10use crate::backends::scalar::ScalarBackend;
11#[cfg(target_arch = "x86_64")]
12use crate::backends::sse2::Sse2Backend;
13#[cfg(target_arch = "wasm32")]
14use crate::backends::wasm::WasmBackend;
15use crate::backends::VectorBackend;
16use crate::vector::Vector;
17use crate::{Backend, Result, TruenoError};
18
19impl Vector<f32> {
20    /// Kahan summation (numerically stable sum)
21    ///
22    /// Uses the Kahan summation algorithm to reduce floating-point rounding errors
23    /// when summing many numbers. This is more accurate than the standard sum() method
24    /// for vectors with many elements or elements of vastly different magnitudes.
25    ///
26    /// # Performance
27    ///
28    /// Note: Kahan summation is inherently sequential and cannot be effectively
29    /// parallelized with SIMD. All backends use the scalar implementation.
30    ///
31    /// # Examples
32    ///
33    /// ```
34    /// use trueno::Vector;
35    ///
36    /// let v = Vector::from_slice(&[1.0, 2.0, 3.0, 4.0]);
37    /// assert_eq!(v.sum_kahan()?, 10.0);
38    /// # Ok::<(), trueno::TruenoError>(())
39    /// ```
40    pub fn sum_kahan(&self) -> Result<f32> {
41        if self.data.is_empty() {
42            return Ok(0.0);
43        }
44
45        // SAFETY: Unsafe block delegates to backend implementation which maintains safety invariants
46        let result = unsafe {
47            match self.backend {
48                Backend::Scalar => ScalarBackend::sum_kahan(&self.data),
49                #[cfg(target_arch = "x86_64")]
50                Backend::SSE2 | Backend::AVX => Sse2Backend::sum_kahan(&self.data),
51                #[cfg(target_arch = "x86_64")]
52                Backend::AVX2 | Backend::AVX512 => Avx2Backend::sum_kahan(&self.data),
53                #[cfg(not(target_arch = "x86_64"))]
54                Backend::SSE2 | Backend::AVX | Backend::AVX2 | Backend::AVX512 => {
55                    ScalarBackend::sum_kahan(&self.data)
56                }
57                #[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
58                Backend::NEON => NeonBackend::sum_kahan(&self.data),
59                #[cfg(not(any(target_arch = "aarch64", target_arch = "arm")))]
60                Backend::NEON => ScalarBackend::sum_kahan(&self.data),
61                #[cfg(target_arch = "wasm32")]
62                Backend::WasmSIMD => WasmBackend::sum_kahan(&self.data),
63                #[cfg(not(target_arch = "wasm32"))]
64                Backend::WasmSIMD => ScalarBackend::sum_kahan(&self.data),
65                Backend::GPU | Backend::Auto => ScalarBackend::sum_kahan(&self.data),
66            }
67        };
68
69        Ok(result)
70    }
71
72    /// Sum of squared elements
73    ///
74    /// Computes the sum of squares: sum(a\[i\]^2).
75    /// This is the building block for computing L2 norm and variance.
76    ///
77    /// # Examples
78    ///
79    /// ```
80    /// use trueno::Vector;
81    ///
82    /// let v = Vector::from_slice(&[1.0, 2.0, 3.0]);
83    /// let sum_sq = v.sum_of_squares()?;
84    /// assert_eq!(sum_sq, 14.0); // 1^2 + 2^2 + 3^2 = 1 + 4 + 9 = 14
85    /// # Ok::<(), trueno::TruenoError>(())
86    /// ```
87    ///
88    /// # Empty vectors
89    ///
90    /// Returns 0.0 for empty vectors.
91    pub fn sum_of_squares(&self) -> Result<f32> {
92        if self.data.is_empty() {
93            return Ok(0.0);
94        }
95
96        // Use dot product with self: dot(self, self) = sum(a[i]^2)
97        self.dot(self)
98    }
99
100    /// Arithmetic mean (average)
101    ///
102    /// Computes the arithmetic mean of all elements: sum(a\[i\]) / n.
103    ///
104    /// # Performance
105    ///
106    /// Uses optimized SIMD sum() implementation, then divides by length.
107    ///
108    /// # Examples
109    ///
110    /// ```
111    /// use trueno::Vector;
112    ///
113    /// let v = Vector::from_slice(&[1.0, 2.0, 3.0, 4.0]);
114    /// let avg = v.mean()?;
115    /// assert!((avg - 2.5).abs() < 1e-5); // (1+2+3+4)/4 = 2.5
116    /// # Ok::<(), trueno::TruenoError>(())
117    /// ```
118    ///
119    /// # Empty vectors
120    ///
121    /// Returns an error for empty vectors (division by zero).
122    ///
123    /// ```
124    /// use trueno::{Vector, TruenoError};
125    ///
126    /// let v: Vector<f32> = Vector::from_slice(&[]);
127    /// assert!(matches!(v.mean(), Err(TruenoError::EmptyVector)));
128    /// ```
129    pub fn mean(&self) -> Result<f32> {
130        if self.data.is_empty() {
131            return Err(TruenoError::EmptyVector);
132        }
133
134        let total = self.sum()?;
135        Ok(total / self.len() as f32)
136    }
137
138    /// Population variance
139    ///
140    /// Computes the population variance: Var(X) = E\[(X - μ)²\].
141    /// Uses the two-pass centered formula (subtract the mean, then sum squared
142    /// deviations) for numerical stability — the naive E\[X²\] - μ² loses precision via
143    /// catastrophic cancellation when the values are large (e.g. after a translation).
144    ///
145    /// # Performance
146    ///
147    /// `mean()` is SIMD-accelerated; the centered-deviation sum is a single scalar pass.
148    ///
149    /// # Examples
150    ///
151    /// ```
152    /// use trueno::Vector;
153    ///
154    /// let v = Vector::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0]);
155    /// let var = v.variance()?;
156    /// assert!((var - 2.0).abs() < 1e-5); // Population variance
157    /// # Ok::<(), trueno::TruenoError>(())
158    /// ```
159    ///
160    /// # Empty vectors
161    ///
162    /// Returns an error for empty vectors.
163    ///
164    /// ```
165    /// use trueno::{Vector, TruenoError};
166    ///
167    /// let v: Vector<f32> = Vector::from_slice(&[]);
168    /// assert!(matches!(v.variance(), Err(TruenoError::EmptyVector)));
169    /// ```
170    pub fn variance(&self) -> Result<f32> {
171        if self.data.is_empty() {
172            return Err(TruenoError::EmptyVector);
173        }
174
175        let mean_val = self.mean()?;
176
177        // Two-pass (centered) formula: Var(X) = E[(X − μ)²].
178        // The naive Var(X) = E[X²] − μ² suffers catastrophic cancellation when E[X²]
179        // and μ² are both large (e.g. after a translation x + c), which broke
180        // translation-invariance for large offsets and made the stddev property tests
181        // flaky. Subtracting the mean first is numerically stable and exactly
182        // translation-invariant.
183        let sum_sq_dev: f32 = self
184            .data
185            .iter()
186            .map(|&x| {
187                let d = x - mean_val;
188                d * d
189            })
190            .sum();
191        Ok(sum_sq_dev / self.len() as f32)
192    }
193
194    /// Population standard deviation
195    ///
196    /// Computes the population standard deviation: σ = sqrt(Var(X)).
197    /// This is the square root of the variance.
198    ///
199    /// # Performance
200    ///
201    /// Uses optimized SIMD implementations via variance().
202    ///
203    /// # Examples
204    ///
205    /// ```
206    /// use trueno::Vector;
207    ///
208    /// let v = Vector::from_slice(&[1.0, 2.0, 3.0, 4.0, 5.0]);
209    /// let sd = v.stddev()?;
210    /// assert!((sd - 1.4142135).abs() < 1e-5); // sqrt(2) ≈ 1.414
211    /// # Ok::<(), trueno::TruenoError>(())
212    /// ```
213    ///
214    /// # Empty vectors
215    ///
216    /// Returns an error for empty vectors.
217    ///
218    /// ```
219    /// use trueno::{Vector, TruenoError};
220    ///
221    /// let v: Vector<f32> = Vector::from_slice(&[]);
222    /// assert!(matches!(v.stddev(), Err(TruenoError::EmptyVector)));
223    /// ```
224    pub fn stddev(&self) -> Result<f32> {
225        let var = self.variance()?;
226        Ok(var.sqrt())
227    }
228
229    /// Population covariance between two vectors
230    ///
231    /// Computes the population covariance: Cov(X,Y) = E[(X - μx)(Y - μy)]
232    /// Uses the computational formula: Cov(X,Y) = E\[XY\] - μx·μy
233    ///
234    /// # Performance
235    ///
236    /// Uses optimized SIMD implementations via dot() and mean().
237    ///
238    /// # Examples
239    ///
240    /// ```
241    /// use trueno::Vector;
242    ///
243    /// let x = Vector::from_slice(&[1.0, 2.0, 3.0]);
244    /// let y = Vector::from_slice(&[2.0, 4.0, 6.0]);
245    /// let cov = x.covariance(&y)?;
246    /// assert!((cov - 1.333).abs() < 0.01); // Perfect positive covariance
247    /// # Ok::<(), trueno::TruenoError>(())
248    /// ```
249    ///
250    /// # Size mismatch
251    ///
252    /// Returns an error if vectors have different lengths.
253    ///
254    /// ```
255    /// use trueno::{Vector, TruenoError};
256    ///
257    /// let x = Vector::from_slice(&[1.0, 2.0]);
258    /// let y = Vector::from_slice(&[1.0, 2.0, 3.0]);
259    /// assert!(matches!(x.covariance(&y), Err(TruenoError::SizeMismatch { .. })));
260    /// ```
261    ///
262    /// # Empty vectors
263    ///
264    /// Returns an error for empty vectors.
265    ///
266    /// ```
267    /// use trueno::{Vector, TruenoError};
268    ///
269    /// let x: Vector<f32> = Vector::from_slice(&[]);
270    /// let y: Vector<f32> = Vector::from_slice(&[]);
271    /// assert!(matches!(x.covariance(&y), Err(TruenoError::EmptyVector)));
272    /// ```
273    pub fn covariance(&self, other: &Self) -> Result<f32> {
274        if self.data.is_empty() {
275            return Err(TruenoError::EmptyVector);
276        }
277        if self.len() != other.len() {
278            return Err(TruenoError::SizeMismatch { expected: self.len(), actual: other.len() });
279        }
280
281        let mean_x = self.mean()?;
282        let mean_y = other.mean()?;
283
284        // Two-pass centered formula: Cov(X,Y) = E[(X - μx)(Y - μy)].
285        // This MUST match variance()'s two-pass formula so that Cov(X,X) ==
286        // Var(X) exactly — otherwise correlation(X,X) drifts off 1.0 (the naive
287        // E[XY] - μxμy form suffers catastrophic cancellation for large means
288        // and is inconsistent with the centered variance, breaking
289        // FALSIFY: rho(X,X) == 1).
290        let sum_cross_dev: f32 = self
291            .data
292            .iter()
293            .zip(other.data.iter())
294            .map(|(&x, &y)| (x - mean_x) * (y - mean_y))
295            .sum();
296        Ok(sum_cross_dev / self.len() as f32)
297    }
298
299    /// Pearson correlation coefficient
300    ///
301    /// Computes the Pearson correlation coefficient: ρ(X,Y) = Cov(X,Y) / (σx·σy)
302    /// Normalized covariance in range [-1, 1].
303    ///
304    /// # Performance
305    ///
306    /// Uses optimized SIMD implementations via covariance() and stddev().
307    ///
308    /// # Examples
309    ///
310    /// ```
311    /// use trueno::Vector;
312    ///
313    /// let x = Vector::from_slice(&[1.0, 2.0, 3.0]);
314    /// let y = Vector::from_slice(&[2.0, 4.0, 6.0]);
315    /// let corr = x.correlation(&y)?;
316    /// assert!((corr - 1.0).abs() < 1e-5); // Perfect positive correlation
317    /// # Ok::<(), trueno::TruenoError>(())
318    /// ```
319    ///
320    /// # Size mismatch
321    ///
322    /// Returns an error if vectors have different lengths.
323    ///
324    /// # Division by zero
325    ///
326    /// Returns DivisionByZero error if either vector has zero standard deviation
327    /// (i.e., is constant).
328    ///
329    /// ```
330    /// use trueno::{Vector, TruenoError};
331    ///
332    /// let x = Vector::from_slice(&[5.0, 5.0, 5.0]); // Constant
333    /// let y = Vector::from_slice(&[1.0, 2.0, 3.0]);
334    /// assert!(matches!(x.correlation(&y), Err(TruenoError::DivisionByZero)));
335    /// ```
336    pub fn correlation(&self, other: &Self) -> Result<f32> {
337        let cov = self.covariance(other)?;
338        let std_x = self.stddev()?;
339        let std_y = other.stddev()?;
340
341        // Check for zero standard deviation (constant vectors)
342        if std_x.abs() < 1e-10 || std_y.abs() < 1e-10 {
343            return Err(TruenoError::DivisionByZero);
344        }
345
346        // ρ(X,Y) = Cov(X,Y) / (σx·σy)
347        // Clamp to [-1, 1] to handle floating-point precision errors
348        let corr = cov / (std_x * std_y);
349        Ok(corr.clamp(-1.0, 1.0))
350    }
351}