Skip to main content

embedded_stats_f32/
lib.rs

1// Copyright (C) 2026 Jorge Andre Castro
2//
3// Ce programme est un logiciel libre : vous pouvez le redistribuer et/ou le modifier
4// selon les termes de la Licence Publique Générale GNU telle que publiée par la
5// Free Software Foundation, soit la version 2 de la licence, soit (à votre convention)
6// n'importe quelle version ultérieure.
7
8//! # embedded-stats-f32
9//! [`StreamingStats`] moyenne + variance + écart type en streaming (Welford)
10//!
11//! Statistiques `f32` pour systèmes embarqués `no_std`.
12//!
13//! Sans dépendance, sans `unsafe`, sans FPU requise.
14//!
15//! Fournit :
16//! - [`mean`]           moyenne arithmétique sur une tranche
17//! - [`variance`]       variance (population, non corrigée)
18//! - [`std_dev`]        écart type (= √variance)
19//! - [`StreamingStats`]  moyenne en ligne (Welford), O(1) mémoire
20//!
21//! Toutes les fonctions rejettent les valeurs non-finies (`NaN`, `±inf`)
22//! via [`StatsError::NonFiniteValue`].
23//!
24//! ## Exemple rapide
25//!
26//! ```rust
27//! use embedded_stats_f32::{mean, variance, std_dev, StreamingStats};
28//!
29//! let data = [2.0_f32, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
30//!
31//! assert!((mean(&data).unwrap()     - 5.0).abs() < 1e-5);
32//! assert!((variance(&data).unwrap() - 4.0).abs() < 1e-4);
33//! assert!((std_dev(&data).unwrap()  - 2.0).abs() < 1e-4);
34//!
35//! let mut s = StreamingStats::new();
36//! for &x in &data { s.update(x).unwrap(); }
37//! assert!((s.mean().unwrap() - 5.0).abs() < 1e-5);
38//! ```
39
40#![no_std]
41#![forbid(unsafe_code)]
42#![warn(missing_docs)]
43
44use embedded_f32_sqrt::sqrt;
45
46///  Erreurs EmptySlice et NonFiniteValue pour la roboustesse du systeme 
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum StatsError {
49    /// La tranche (ou l'accumulateur) est vide : le calcul est impossible.
50    EmptySlice,
51    /// Une valeur `NaN` ou infinie a été détectée dans les données.
52    NonFiniteValue,
53}
54
55// Garde centrale
56
57/// Vérifie qu'une valeur `f32` est finie (`!NaN`, `!±inf`).
58///
59/// Point unique de validation  évite de dupliquer la logique dans chaque fonction.
60#[inline]
61fn ensure_finite(x: f32) -> Result<f32, StatsError> {
62    if x.is_finite() {
63        Ok(x)
64    } else {
65        Err(StatsError::NonFiniteValue)
66    }
67}
68
69//Moyenne 
70
71/// Calcule la moyenne arithmétique d'une tranche `f32`.
72///
73/// Utilise la sommation compensée de Kahan pour minimiser l'erreur
74/// d'arrondi sur les grandes tranches.
75///
76/// # Erreurs
77///
78/// - [`StatsError::EmptySlice`]      tranche vide
79/// - [`StatsError::NonFiniteValue`] `NaN` ou `±inf` dans les données
80///
81/// # Exemples
82///
83/// ```rust
84/// use embedded_stats_f32::{mean, StatsError};
85///
86/// assert!((mean(&[1.0, 2.0, 3.0]).unwrap() - 2.0).abs() < 1e-6);
87/// assert_eq!(mean(&[] as &[f32]),            Err(StatsError::EmptySlice));
88/// assert_eq!(mean(&[f32::NAN]),              Err(StatsError::NonFiniteValue));
89/// assert_eq!(mean(&[f32::INFINITY]),         Err(StatsError::NonFiniteValue));
90/// ```
91pub fn mean(data: &[f32]) -> Result<f32, StatsError> {
92    if data.is_empty() {
93        return Err(StatsError::EmptySlice);
94    }
95    let sum = kahan_sum_checked(data)?;
96    Ok(sum / data.len() as f32)
97}
98
99//  Variance 
100
101/// Calcule la variance de population (non corrigée, diviseur N) d'une tranche `f32`.
102///
103/// # Erreurs
104///
105/// - [`StatsError::EmptySlice`]     tranche vide
106/// - [`StatsError::NonFiniteValue`] `NaN` ou `±inf` dans les données ou dans le résultat
107///
108/// # Exemples
109///
110/// ```rust
111/// use embedded_stats_f32::{variance, StatsError};
112///
113/// let data = [2.0_f32, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
114/// assert!((variance(&data).unwrap() - 4.0).abs() < 1e-4);
115/// assert_eq!(variance(&[f32::NAN]), Err(StatsError::NonFiniteValue));
116/// ```
117pub fn variance(data: &[f32]) -> Result<f32, StatsError> {
118    if data.is_empty() {
119        return Err(StatsError::EmptySlice);
120    }
121
122    let m = kahan_sum_checked(data)? / data.len() as f32;
123
124    let mut sum  = 0.0_f32;
125    let mut comp = 0.0_f32;
126
127    for &x in data {
128        let x = ensure_finite(x)?;
129        let d = x - m;
130        let y = d * d - comp;
131        let t = sum + y;
132        comp  = (t - sum) - y;
133        sum   = t;
134    }
135
136    let result = sum / data.len() as f32;
137    ensure_finite(result)
138}
139
140//  Écart type
141/// Calcule l'écart type de population (= √variance) d'une tranche `f32`.
142///
143/// Utilise [`embedded_f32_sqrt::sqrt`] (Newton-Raphson, pas de FPU requise).
144///
145/// # Erreurs
146///
147/// - [`StatsError::EmptySlice`]      tranche vide
148/// - [`StatsError::NonFiniteValue`]  `NaN` ou `±inf` dans les données ou le résultat
149///
150/// # Exemples
151///
152/// ```rust
153/// use embedded_stats_f32::{std_dev, StatsError};
154///
155/// let data = [2.0_f32, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
156/// assert!((std_dev(&data).unwrap() - 2.0).abs() < 1e-4);
157/// assert_eq!(std_dev(&[f32::NAN]), Err(StatsError::NonFiniteValue));
158/// ```
159pub fn std_dev(data: &[f32]) -> Result<f32, StatsError> {
160    let v = variance(data)?;
161
162    // clamp défensif
163    let v = if v < 0.0 { 0.0 } else { v };
164    // Clamp défensif : peut être légèrement négatif à cause des erreurs flottantes
165    let s = sqrt(v).map_err(|_| StatsError::NonFiniteValue)?;
166    ensure_finite(s)
167}
168
169// Moyenne streaming (Welford) 
170
171/// Accumulateur de moyenne en ligne, O(1) mémoire.
172///
173/// Implémente la mise à jour incrémentale de Welford :
174/// ```text
175/// mean_n = mean_{n-1} + (x_n − mean_{n-1}) / n
176/// ```
177/// Stable numériquement même pour des millions de points.
178/// Rejette les valeurs non-finies sans corrompre l'état interne.
179///
180/// # Exemples
181///
182/// ```rust
183/// use embedded_stats_f32::{StreamingStats, StatsError};
184///
185/// let mut s = StreamingStats::new();
186/// for x in [1.0_f32, 2.0, 3.0, 4.0, 5.0] { s.update(x).unwrap(); }
187///
188/// assert!((s.mean().unwrap() - 3.0).abs() < 1e-6);
189/// assert_eq!(s.count(), 5);
190///
191/// // NaN rejeté, état interne préservé
192/// assert_eq!(s.update(f32::NAN), Err(StatsError::NonFiniteValue));
193/// assert_eq!(s.count(), 5);
194/// ```
195#[derive(Debug, Clone, Copy)]
196pub struct StreamingStats {
197    count: u32,
198    mean:  f32,
199    m2:    f32,
200}
201
202impl StreamingStats {
203    /// Crée un accumulateur vide.
204    #[inline]
205    pub const fn new() -> Self {
206        Self { count: 0, mean: 0.0 , m2: 0.0 }
207    }
208
209    /// Intègre une nouvelle observation.
210    ///
211    /// Retourne [`StatsError::NonFiniteValue`] si `x` est `NaN` ou `±inf`.
212    /// En cas d'erreur, l'état interne est **inchangé**.
213    #[inline]
214    pub fn update(&mut self, x: f32) -> Result<(), StatsError> {
215       let x = ensure_finite(x)?;
216
217       self.count += 1;
218
219       let delta  = x - self.mean;
220       self.mean += delta / self.count as f32;
221       let delta2 = x - self.mean;
222
223       self.m2 += delta * delta2;
224
225       Ok(())
226    }    
227
228    /// Retourne la moyenne courante.
229    ///
230    /// # Erreurs
231    ///
232    /// - [`StatsError::EmptySlice`]     : aucune observation intégrée
233    /// - [`StatsError::NonFiniteValue`] : état interne corrompu (théoriquement impossible
234    ///   via [`update`](StreamingStats::update), garde de sécurité défensive)
235    #[inline]
236    pub fn mean(&self) -> Result<f32, StatsError> {
237        if self.count == 0 {
238            return Err(StatsError::EmptySlice);
239        }
240        self.check_state()?;
241        Ok(self.mean)
242    }
243
244    /// Retourne le nombre d'observations intégrées.
245    #[inline]
246    pub const fn count(&self) -> u32 {
247        self.count
248    }
249
250    /// Remet l'accumulateur à zéro.
251    #[inline]
252    pub fn reset(&mut self) {
253        self.count = 0;
254        self.mean  = 0.0;
255        // Remise à zéro de m2 pour éviter les incohérences si on continue à utiliser l'accumulateur après reset.
256        self.m2    = 0.0;
257    }
258
259    /// Garde défensive : vérifie que l'état interne est cohérent.
260    #[inline]
261   fn check_state(&self) -> Result<(), StatsError> {
262    if self.mean.is_finite() && self.m2.is_finite() {
263        Ok(())
264    } else {
265        Err(StatsError::NonFiniteValue)
266    }
267   }
268  /// Variance de population (diviseur N) en streaming.
269  #[inline]
270  pub fn running_variance(&self) -> Result<f32, StatsError> {
271    if self.count == 0 {
272        return Err(StatsError::EmptySlice);
273    }
274
275    self.check_state()?;
276
277    let v = self.m2 / self.count as f32;
278    let v = if v < 0.0 { 0.0 } else { v };
279
280    ensure_finite(v)
281  }
282
283/// Écart type en streaming (= √variance).
284#[inline]
285pub fn running_std_dev(&self) -> Result<f32, StatsError> {
286    let v = self.running_variance()?;
287    let s = sqrt(v).map_err(|_| StatsError::NonFiniteValue)?;
288    ensure_finite(s)
289}
290
291
292}
293
294impl Default for StreamingStats {
295    fn default() -> Self {
296        Self::new()
297    }
298}
299
300// Utilitaire interne 
301
302/// Sommation compensée de Kahan avec validation  fail-fast sur NaN/inf.
303///
304/// Réduit l'erreur d'arrondi de O(Nε) à O(ε).
305/// Retourne [`StatsError::NonFiniteValue`] dès la première valeur non-finie.
306#[inline]
307fn kahan_sum_checked(data: &[f32]) -> Result<f32, StatsError> {
308    let mut sum  = 0.0_f32;
309    let mut comp = 0.0_f32;
310
311    for &x in data {
312        let x = ensure_finite(x)?;
313        let y = x - comp;
314        let t = sum + y;
315        comp  = (t - sum) - y;
316        sum   = t;
317    }
318
319    Ok(sum)
320}
321
322
323// Tests 
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328
329    // Dataset de référence Wikipedia (variance = 4.0, std = 2.0, mean = 5.0)
330    const DATA: [f32; 8] = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
331
332    //  mean 
333
334    #[test]
335    fn test_mean_reference() {
336        let m = mean(&DATA).unwrap();
337        assert!((m - 5.0).abs() < 1e-5, "mean = {m}");
338    }
339
340    #[test]
341    fn test_mean_single() {
342        assert!((mean(&[42.0_f32]).unwrap() - 42.0).abs() < 1e-6);
343    }
344
345    #[test]
346    fn test_mean_empty() {
347        assert_eq!(mean(&[] as &[f32]), Err(StatsError::EmptySlice));
348    }
349
350    #[test]
351    fn test_mean_nan() {
352        assert_eq!(mean(&[1.0, f32::NAN, 3.0]), Err(StatsError::NonFiniteValue));
353    }
354
355    #[test]
356    fn test_mean_inf() {
357        assert_eq!(mean(&[1.0, f32::INFINITY]), Err(StatsError::NonFiniteValue));
358    }
359
360    #[test]
361    fn test_mean_neg_inf() {
362        assert_eq!(mean(&[f32::NEG_INFINITY]), Err(StatsError::NonFiniteValue));
363    }
364
365    //  variance 
366
367    #[test]
368    fn test_variance_reference() {
369        let v = variance(&DATA).unwrap();
370        assert!((v - 4.0).abs() < 1e-4, "variance = {v}");
371    }
372
373    #[test]
374    fn test_variance_constant() {
375        let v = variance(&[3.0_f32; 100]).unwrap();
376        assert!(v.abs() < 1e-5, "variance constante = {v}");
377    }
378
379    #[test]
380    fn test_variance_empty() {
381        assert_eq!(variance(&[] as &[f32]), Err(StatsError::EmptySlice));
382    }
383
384    #[test]
385    fn test_variance_nan() {
386        assert_eq!(variance(&[1.0, f32::NAN]), Err(StatsError::NonFiniteValue));
387    }
388
389    #[test]
390    fn test_variance_inf() {
391        assert_eq!(variance(&[f32::INFINITY, 1.0]), Err(StatsError::NonFiniteValue));
392    }
393
394    //  std_dev 
395
396    #[test]
397    fn test_std_dev_reference() {
398        let s = std_dev(&DATA).unwrap();
399        assert!((s - 2.0).abs() < 1e-4, "std_dev = {s}");
400    }
401
402    #[test]
403    fn test_std_dev_empty() {
404        assert_eq!(std_dev(&[] as &[f32]), Err(StatsError::EmptySlice));
405    }
406
407    #[test]
408    fn test_std_dev_nan() {
409        assert_eq!(std_dev(&[f32::NAN]), Err(StatsError::NonFiniteValue));
410    }
411
412    //  StreamingStats
413
414    #[test]
415    fn test_streaming_mean_reference() {
416        let mut acc = StreamingStats::new();
417        for &x in &DATA { acc.update(x).unwrap(); }
418        let m = acc.mean().unwrap();
419        assert!((m - 5.0).abs() < 1e-5, "streaming mean = {m}");
420        assert_eq!(acc.count(), 8);
421    }
422
423    #[test]
424    fn test_streaming_mean_empty() {
425        assert_eq!(StreamingStats::new().mean(), Err(StatsError::EmptySlice));
426    }
427
428    #[test]
429    fn test_streaming_nan_rejected_state_preserved() {
430        let mut acc = StreamingStats::new();
431        acc.update(1.0).unwrap();
432        acc.update(2.0).unwrap();
433        // NaN rejeté
434        assert_eq!(acc.update(f32::NAN), Err(StatsError::NonFiniteValue));
435        // count et mean inchangés
436        assert_eq!(acc.count(), 2);
437        assert!((acc.mean().unwrap() - 1.5).abs() < 1e-6);
438    }
439
440    #[test]
441    fn test_streaming_inf_rejected() {
442        let mut acc = StreamingStats::new();
443        acc.update(5.0).unwrap();
444        assert_eq!(acc.update(f32::INFINITY), Err(StatsError::NonFiniteValue));
445        assert_eq!(acc.count(), 1);
446    }
447
448    #[test]
449    fn test_streaming_reset() {
450        let mut acc = StreamingStats::new();
451        for &x in &DATA { acc.update(x).unwrap(); }
452        acc.reset();
453        assert_eq!(acc.count(), 0);
454        assert_eq!(acc.mean(), Err(StatsError::EmptySlice));
455    }
456
457    #[test]
458    fn test_streaming_matches_batch() {
459        let mut acc = StreamingStats::new();
460        for &x in &DATA { acc.update(x).unwrap(); }
461        let batch  = mean(&DATA).unwrap();
462        let stream = acc.mean().unwrap();
463        assert!((batch - stream).abs() < 1e-5, "batch={batch} stream={stream}");
464    }
465
466
467    #[test]
468    fn test_streaming_variance_matches_batch() {
469      let mut acc = StreamingStats::new();
470      for &x in &DATA {
471        acc.update(x).unwrap();
472    }
473
474      let batch  = variance(&DATA).unwrap();
475      let stream = acc.running_variance().unwrap();
476
477    assert!((batch - stream).abs() < 1e-4,
478        "batch={batch}, stream={stream}");
479   }
480
481   #[test]
482   fn test_streaming_std_dev_matches_batch() {
483    let mut acc = StreamingStats::new();
484    for &x in &DATA {
485        acc.update(x).unwrap();
486    }
487
488    let batch  = std_dev(&DATA).unwrap();
489    let stream = acc.running_std_dev().unwrap();
490
491    assert!((batch - stream).abs() < 1e-4,
492        "batch={batch}, stream={stream}");
493   }
494
495  #[test]
496  fn test_streaming_large_stability() {
497    let mut acc = StreamingStats::new();
498
499    for i in 0..1_000 {
500        acc.update(i as f32).unwrap();
501    }
502
503    let mean = acc.mean().unwrap();
504
505    // moyenne attendue ≈ 499.5
506    assert!((mean - 499.5).abs() < 1e-2);
507  }
508
509
510  #[test]
511  fn test_nan_does_not_corrupt_state() {
512    let mut acc = StreamingStats::new();
513
514    acc.update(10.0).unwrap();
515    acc.update(20.0).unwrap();
516
517    let before = acc.mean().unwrap();
518
519    assert_eq!(acc.update(f32::NAN), Err(StatsError::NonFiniteValue));
520
521    let after = acc.mean().unwrap();
522
523    assert!((before - after).abs() < 1e-6);
524    assert_eq!(acc.count(), 2);
525  }
526
527
528  #[test]
529  fn test_count_monotonic() {
530    let mut acc = StreamingStats::new();
531
532    for i in 1..100 {
533        acc.update(i as f32).unwrap();
534        assert_eq!(acc.count(), i);
535    }
536  }
537
538
539
540  #[test]
541  fn test_constant_values_zero_variance() {
542    let mut acc = StreamingStats::new();
543
544    for _ in 0..100 {
545        acc.update(5.0).unwrap();
546    }
547
548    let v = acc.running_variance().unwrap();
549    assert!(v.abs() < 1e-6);
550   }
551
552
553  #[test]
554  fn test_inf_inputs_rejected() {
555    let mut acc = StreamingStats::new();
556
557    assert_eq!(acc.update(f32::INFINITY), Err(StatsError::NonFiniteValue));
558    assert_eq!(acc.update(f32::NEG_INFINITY), Err(StatsError::NonFiniteValue));
559
560    assert_eq!(acc.count(), 0);
561   }
562
563
564  #[test]
565  fn test_streaming_long_run_stability() {
566    let mut acc = StreamingStats::new();
567
568    let mut sum = 0.0;
569    for i in 1..10_000 {
570        let x = (i as f32).sin() * 100.0;
571        acc.update(x).unwrap();
572        sum += x;
573    }
574
575    let mean_manual = sum / 10_000.0;
576    let mean_stream = acc.mean().unwrap();
577
578    assert!((mean_manual - mean_stream).abs() < 1e-3);
579  }
580}