Skip to main content

kalman_fixed_agnostic/
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//! # kalman-fixed-agnostic
9//!
10//! Filtre de Kalman adaptatif en virgule fixe pour systèmes embarqués (`no_std`).
11//!
12//! ## Caractéristiques
13//!
14//! - `#![no_std]` — aucune dépendance à la bibliothèque standard
15//! - Arithmétique entière pure (pas de flottants, pas de `libm`)
16//! - Compatible RP2040 (Cortex-M0+), RP2350 (Cortex-M33) et architectures RISC-V
17//! - Algorithme : Filtre de Kalman scalaire à modèle constant
18//! - Bruit de mesure **R adaptatif** via la formule de König : `Var(X) = E[X²] − E[X]²`
19//! - Temps d'exécution **constant** (déterministe) — idéal pour les noyaux temps réel
20//!
21//! ## Représentation interne Q16.16
22//!
23//! Toutes les grandeurs internes (`x`, `p`, `q`, `r`) sont stockées en format
24//! virgule fixe Q16.16 sur `i64` :
25//! ```text
26//! valeur_réelle = valeur_i64 / 65536.0
27//! ```
28//! L'interface publique (entrée/sortie) reste en `i32` (entiers bruts de capteur).
29//!
30//! ## Algorithme
31//!
32//! À chaque appel à [`AgnosticKalman::update`], le filtre effectue dans l'ordre :
33//!
34//! 1. **Mise à jour du buffer circulaire** — la nouvelle mesure remplace la plus ancienne
35//! 2. **Calcul de R dynamique** — variance de la fenêtre via la formule de König
36//! 3. **Prédiction** — `P ← P + Q`
37//! 4. **Gain de Kalman** — `K = P / (P + R)`
38//! 5. **Correction de l'état** — `x ← x + K·(z − x)`
39//! 6. **Correction de la covariance** — `P ← (1 − K)·P`
40//!
41//! ## Formule de König
42//!
43//! La variance est calculée sans passe double grâce à l'identité :
44//! ```text
45//! Var(X) = E[X²] − E[X]²
46//! ```
47//! Cette formulation est numériquement stable pour des capteurs i32 typiques
48//! (ADC 12 bits, capteurs de température, accéléromètres) et s'exécute en O(N)
49//! avec N = 10 (taille fixe du buffer).
50//!
51//! ## Exemple
52//!
53//! ```rust
54//! use kalman_fixed_agnostic::AgnosticKalman;
55//!
56//! // Signal bruité autour de 1000 avec Q = 1 (filtre très lisse)
57//! let mut k = AgnosticKalman::new(1000, 1);
58//!
59//! // Convergence progressive vers la vraie valeur
60//! let mesures = [1005, 998, 1002, 1010, 995, 1003, 999, 1007, 1001, 1004];
61//! let mut sortie = 0i32;
62//! for &m in &mesures {
63//!     sortie = k.update(m);
64//! }
65//!
66//! // La sortie doit être proche de 1000
67//! assert!((sortie - 1000).abs() < 15);
68//! ```
69
70#![no_std]
71#![forbid(unsafe_code)]
72
73/// Taille de la fenêtre glissante pour le calcul de la variance dynamique.
74 pub const WINDOW: usize = 10;
75/// Filtre de Kalman adaptatif en virgule fixe Q16.16.
76///
77/// Le paramètre de bruit de mesure R est calculé dynamiquement à partir
78/// de la variance des dernières mesures (formule de König), ce qui permet
79/// au filtre de s'auto-ajuster à la qualité du signal du capteur.
80pub struct AgnosticKalman {
81    /// Valeur estimée en Q16.16 (valeur_réelle = x >> 16)
82    x: i64,
83    /// Covariance d'erreur en Q16.16
84    p: i64,
85    /// Bruit de processus Q en Q16.16 (confiance dans le modèle)
86    q: i64,
87    /// Fenêtre glissante pour le calcul de la variance dynamique
88    buffer: [i32; WINDOW],
89    /// Index courant dans le buffer circulaire
90    buf_idx: usize,
91    /// Indique si le buffer a été rempli au moins une fois
92    is_warm: bool,
93}
94
95impl AgnosticKalman {
96    /// Initialise un nouveau filtre avec une valeur de départ et un bruit de processus.
97    ///
98    /// # Arguments
99    ///
100    /// * `initial_value` Valeur initiale de l'état (en unités capteur, `i32`)
101    /// * `q` — Bruit de processus : confiance dans la stabilité du système.
102    ///   Un `q` faible (ex. `1`) produit un filtre très lisse (réponse lente aux
103    ///   changements). Un `q` élevé (ex. `100`) suit plus agressivement les mesures.
104    ///
105    /// # Exemple
106    ///
107    /// ```rust
108    /// use kalman_fixed_agnostic::AgnosticKalman;
109    /// let mut filtre = AgnosticKalman::new(512, 5);
110    /// ```
111    pub fn new(initial_value: i32, q: i32) -> Self {
112        Self {
113            x: (initial_value as i64) << 16,
114            p: 1i64 << 16,
115            q: (q as i64) << 16,
116            buffer: [initial_value; WINDOW],
117            buf_idx: 0,
118            is_warm: false,
119        }
120    }
121
122    /// Calcule la variance dynamique via la formule de König : `E[X²] − E[X]²`.
123    ///
124    /// Cette valeur devient le paramètre R (bruit de mesure) du filtre.
125    /// Saturée à 1 (en Q16.16) pour éviter toute singularité mathématique
126    /// (division par zéro dans le calcul du gain de Kalman).
127    fn calculate_dynamic_r(&self) -> i64 {
128        let mut sum: i64 = 0;
129        let mut sum_sq: i64 = 0;
130
131        for &val in self.buffer.iter() {
132            let v = val as i64;
133            sum += v;
134            sum_sq += v * v;
135        }
136
137        let n = WINDOW as i64;
138        let mean = sum / n;
139        let mean_sq = sum_sq / n;
140        // Var(X) = E[X²] − E[X]²
141        let variance = mean_sq - (mean * mean);
142
143        // Saturation basse à 1 ULP Q16.16 pour éviter K = 1 exact
144        if variance > 1 {
145            variance << 16
146        } else {
147            1i64 << 16
148        }
149    }
150
151    /// Met à jour le filtre avec une nouvelle mesure brute.
152    ///
153    /// Exécute un cycle complet : prédiction → calcul de R → gain → correction.
154    /// Le temps d'exécution est **constant** et **déterministe**.
155    ///
156    /// # Arguments
157    ///
158    /// * `measurement`  Valeur brute du capteur (`i32`)
159    ///
160    /// # Retour
161    ///
162    /// Valeur filtrée en `i32` (même échelle que l'entrée).
163    ///
164    /// # Exemple
165    ///
166    /// ```rust
167    /// use kalman_fixed_agnostic::AgnosticKalman;
168    /// let mut k = AgnosticKalman::new(0, 10);
169    /// let v = k.update(42);
170    /// assert!(v >= 0);
171    /// ```
172    pub fn update(&mut self, measurement: i32) -> i32 {
173        // --- Mise à jour du buffer circulaire ---
174        self.buffer[self.buf_idx] = measurement;
175        self.buf_idx = (self.buf_idx + 1) % WINDOW;
176        if self.buf_idx == 0 {
177            self.is_warm = true;
178        }
179
180        // Calcul du paramètre R adaptatif 
181        let r = self.calculate_dynamic_r();
182
183        // ÉTAPE 1 : PRÉDICTION  modèle à vitesse constante nulle
184        // P ← P + Q
185        self.p = self.p.saturating_add(self.q);
186
187        // ÉTAPE 2 : GAIN DE KALMAN
188        // K = P / (P + R), résultat en Q16.16
189        let p_plus_r = self.p.saturating_add(r);
190        let k = if p_plus_r != 0 {
191            (self.p << 16) / p_plus_r
192        } else {
193            0
194        };
195
196        // ÉTAPE 3 : CORRECTION DE L'ÉTAT
197        // x ← x + K · (z − x)
198        let z = (measurement as i64) << 16;
199        let innovation = z.saturating_sub(self.x);
200        self.x = self.x.saturating_add((k * innovation) >> 16);
201
202        // ÉTAPE 4 : CORRECTION DE LA COVARIANCE
203        // P ← (1 − K) · P
204        let one_q = 1i64 << 16;
205        self.p = (one_q.saturating_sub(k).saturating_mul(self.p)) >> 16;
206        // Saturation basse de P pour éviter la dégénérescence numérique
207        if self.p < 1 {
208            self.p = 1;
209        }
210
211        // Déséchélonnage vers i32
212        (self.x >> 16) as i32
213    }
214
215    /// Indique si le buffer est rempli (au moins [`WINDOW`] mesures reçues).
216    ///
217    /// Avant le premier remplissage, la variance R est calculée sur des copies
218    /// de `initial_value`, ce qui peut produire un R très bas (signal "parfait").
219    /// Les applications critiques peuvent attendre `is_warm()` avant d'utiliser
220    /// la sortie filtrée.
221    pub fn is_warm(&self) -> bool {
222        self.is_warm
223    }
224
225    /// Réinitialise l'état interne sans réallouer.
226    ///
227    /// Utile pour un redémarrage logiciel en place (watchdog, reprise après erreur).
228    pub fn reset(&mut self, initial_value: i32) {
229        self.x = (initial_value as i64) << 16;
230        self.p = 1i64 << 16;
231        self.buffer = [initial_value; WINDOW];
232        self.buf_idx = 0;
233        self.is_warm = false;
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    #[test]
242    fn test_signal_constant_converge() {
243        // Un signal parfaitement constant doit être reproduit exactement après convergence
244        let mut k = AgnosticKalman::new(500, 1);
245        let mut out = 0i32;
246        for _ in 0..50 {
247            out = k.update(500);
248        }
249        assert_eq!(out, 500, "Signal constant : attendu 500, reçu {}", out);
250    }
251
252    #[test]
253    fn test_signal_bruite_lisse() {
254        // Signal bruité autour de 1000 : la sortie doit rester proche de 1000
255        let mut k = AgnosticKalman::new(1000, 1);
256        let bruit = [1005i32, 998, 1002, 1010, 995, 1003, 999, 1007, 1001, 1004];
257        let mut out = 0i32;
258        for _ in 0..5 {
259            for &m in &bruit {
260                out = k.update(m);
261            }
262        }
263        assert!(
264            (out - 1000).abs() < 15,
265            "Signal bruité 1000 : attendu ≈1000, reçu {}",
266            out
267        );
268    }
269
270    #[test]
271    fn test_sortie_strictement_positive_signal_positif() {
272        let mut k = AgnosticKalman::new(100, 10);
273        for i in 0..20 {
274            let out = k.update(100 + i);
275            assert!(out > 0, "Sortie doit être positive, reçu {} à i={}", out, i);
276        }
277    }
278
279    #[test]
280    fn test_monotone_apres_saut() {
281        // Après un saut de valeur, le filtre doit converger progressivement (sans oscillation)
282        let mut k = AgnosticKalman::new(0, 50);
283        // Phase de chauffe à 0
284        for _ in 0..10 {
285            k.update(0);
286        }
287        // Saut à 1000
288        let mut prev = k.update(1000);
289        let mut monotone = true;
290        for _ in 0..20 {
291            let cur = k.update(1000);
292            if cur < prev {
293                monotone = false;
294                break;
295            }
296            prev = cur;
297        }
298        assert!(monotone, "La convergence post-saut doit être monotone croissante");
299    }
300
301    #[test]
302    fn test_is_warm_false_initialement() {
303        let k = AgnosticKalman::new(0, 1);
304        assert!(!k.is_warm(), "Le filtre ne doit pas être chaud à l'initialisation");
305    }
306
307    #[test]
308    fn test_is_warm_apres_window_mesures() {
309        let mut k = AgnosticKalman::new(0, 1);
310        for i in 0..WINDOW {
311            // Pas encore chaud après i mesures (sauf la dernière qui complète le tour)
312            k.update(i as i32);
313        }
314        assert!(k.is_warm(), "Le filtre doit être chaud après {} mesures", WINDOW);
315    }
316
317    #[test]
318    fn test_reset_reinitialise_etat() {
319        let mut k = AgnosticKalman::new(1000, 5);
320        for _ in 0..20 {
321            k.update(2000);
322        }
323        k.reset(0);
324        // Après reset, la première mesure doit démarrer depuis 0
325        let out = k.update(0);
326        assert!(
327            out.abs() < 100,
328            "Après reset à 0, la sortie doit être proche de 0, reçu {}",
329            out
330        );
331        assert!(!k.is_warm(), "Après reset, is_warm doit être false");
332    }
333
334    #[test]
335    fn test_signal_negatif() {
336        // Le filtre doit fonctionner avec des valeurs négatives (ex. capteur bipolaire)
337        let mut k = AgnosticKalman::new(-500, 5);
338        let mut out = 0i32;
339        for _ in 0..30 {
340            out = k.update(-500);
341        }
342        assert!(
343            (out - (-500)).abs() < 20,
344            "Signal constant négatif : attendu ≈-500, reçu {}",
345            out
346        );
347    }
348
349    #[test]
350    fn test_pas_de_saturation_sur_valeur_extreme() {
351        // Vérifie qu'une mesure extrême ne provoque pas de panique (overflow)
352        let mut k = AgnosticKalman::new(0, 1);
353        let _ = k.update(i32::MAX / 2);
354        let _ = k.update(i32::MIN / 2);
355        // Pas de panique = succès
356    }
357
358    #[test]
359    fn test_q_eleve_suit_signal() {
360        // Avec un Q élevé, le filtre doit suivre rapidement un changement de signal
361        let mut k = AgnosticKalman::new(0, 1000);
362        for _ in 0..15 {
363            k.update(5000);
364        }
365        let out = k.update(5000);
366        assert!(
367            (out - 5000).abs() < 200,
368            "Filtre réactif (Q élevé) : attendu ≈5000, reçu {}",
369            out
370        );
371    }
372}