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}