Skip to main content

embedded_exp/
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-exp
9//!
10//! Exponentielle en virgule fixe Q15 pour systèmes embarqués.
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+) et RP2350 (Cortex-M33)
17//! - Algorithme : Réduction d'intervalle + approximation polynomiale de Taylor (degré 4)
18//! - Temps d'exécution **constant** (déterministe) idéal pour les noyaux temps réel
19//! - Précision < 10 ULP sur toute la plage `[-1.0, 0[`
20//!
21//! ## Format Q15
22//!
23//! En Q15, un `i16` représente un nombre réel dans `[-1.0, 1.0[` :
24//! ```text
25//! valeur_réelle = valeur_i16 / 32768.0
26//! ```
27//! Exemples :
28//! - `0`      → 0.0
29//! - `16384`  → 0.5
30//! - `32767`  → ≈ 1.0
31//! - `-32768` → -1.0
32//!
33//! ## Algorithme
34//!
35//! La méthode utilise la **réduction d'intervalle** combinée à une
36//! **approximation polynomiale de Taylor de degré 4** :
37//!
38//! 1. Décomposition : `x = n·ln(2) + r`, avec `n` entier et `r ∈ [0, ln(2)[`
39//! 2. Propriété : `e^x = 2^n · e^r` → `2^n` est un simple décalage de bits
40//! 3. Approximation : `e^r ≈ 1 + r + r²/2 + r³/6 + r⁴/24`,"L'implémentation utilise des multiplications par l'inverse pour éviter les divisions coûteuses
41
42//!
43//! Le reste `r` est toujours dans `[0, ln(2)[ ≈ [0, 0.693[`, ce qui garantit
44//! la convergence rapide du polynôme. Toutes les multiplications restent
45//! dans les bornes `i32` sans risque de débordement.
46//!
47//! ## Exemple
48//!
49//! ```rust
50//! use embedded_exp::exp_q15;
51//!
52//! // exp(0.0) = 1.0 → non représentable en Q15 signé → sature à i16::MAX
53//! assert_eq!(exp_q15(0), i16::MAX);
54//!
55//! // exp(-0.5) ≈ 0.6065 → ≈ 19874 en Q15
56//! let res = exp_q15(-16384);
57//! assert!((res as i32 - 19874).abs() < 10);
58//!
59//! // exp(-1.0) doit être positif
60//! assert!(exp_q15(-32768) > 0);
61//! ```
62
63#![no_std]
64#![forbid(unsafe_code)]
65
66/// ln(2) en Q15 : 0.693147… × 32768 = 22713
67const LN2_Q15: i32 = 22713;
68
69/// 1/ln(2) en Q15 : (1/0.693147…) × 32768 = 47274
70const INV_LN2_Q15: i32 = 47274;
71
72// Multiplicateurs magiques pour éviter les divisions (1/X * 2^15)
73const INV_6_Q15: i32 = 5461;  // round(32768 / 6)
74const INV_24_Q15: i32 = 1365; // round(32768 / 24)
75
76/// Calcule l'exponentielle d'un nombre en virgule fixe Q15.
77/// Temps d'exécution constant et déterministe.
78#[inline]
79pub fn exp_q15(x: i16) -> i16 {
80    // e^x ≥ 1.0 pour tout x ≥ 0 → saturation à i16::MAX
81    if x >= 0 {
82        return i16::MAX;
83    }
84
85    // Protection contre les valeurs trop petites (e^-10 est négligeable en Q15)
86    // -10.0 en Q15 serait -327680, mais x est i16. On couvre toute la plage.
87    let x_i32 = x as i32;
88
89    // Étape 1 : Réduction d'intervalle 
90    // n = floor(x / ln2)
91    let n: i32 = (x_i32 * INV_LN2_Q15) >> 30;
92    
93    // r = x − n·ln(2)
94    let r: i32 = x_i32 - n * LN2_Q15;
95
96    // Étape 2 : Approximation polynomiale e^r sur [0, ln(2)[ 
97    // Taylor degré 4 : e^r ≈ 1 + r + r²/2 + r³/6 + r⁴/24
98    let r2: i32 = (r * r) >> 15;
99    let r3: i32 = (r2 * r) >> 15;
100    let r4: i32 = (r3 * r) >> 15;
101
102    let er: i32 = 32768                      // 1.0 en Q15
103                + r                          // r
104                + (r2 >> 1)                  // r²/2
105                + ((r3 * INV_6_Q15) >> 15)   // r³/6
106                + ((r4 * INV_24_Q15) >> 15); // r⁴/24
107
108    //  Étape 3 : Reconstruction e^x = 2^n · e^r 
109    // Comme x < 0, n est toujours négatif ou nul. 
110    // n est l'exposant de 2, on décale à droite de |n|.
111    let result: i32 = er >> (-n);
112
113    //  Saturation et cast final 
114    if result >= 32767 {
115        32767
116    } else if result <= 0 {
117        0
118    } else {
119        result as i16
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_exp_zero_saturates() {
129        // exp(0.0) = 1.0 → non représentable en Q15 signé → saturation
130        assert_eq!(exp_q15(0), i16::MAX);
131    }
132
133    #[test]
134    fn test_exp_saturation_positive() {
135        // Tout x ≥ 0 → e^x ≥ 1.0 → saturation
136        assert_eq!(exp_q15(1), i16::MAX);
137        assert_eq!(exp_q15(i16::MAX), i16::MAX);
138    }
139
140    #[test]
141    fn test_exp_negative_eighth() {
142        // exp(-0.125) ≈ 0.8825 → 0.8825 × 32768 ≈ 28917
143        // x = -0.125 en Q15 → -4096
144        let res = exp_q15(-4096);
145        assert!((res as i32 - 28917).abs() < 10, "exp(-0.125): attendu ≈28917, reçu {}", res);
146    }
147
148    #[test]
149    fn test_exp_negative_quarter() {
150        // exp(-0.25) ≈ 0.7788 → 0.7788 × 32768 ≈ 25519
151        // x = -0.25 en Q15 → -8192
152        let res = exp_q15(-8192);
153        assert!((res as i32 - 25519).abs() < 10, "exp(-0.25): attendu ≈25519, reçu {}", res);
154    }
155
156    #[test]
157    fn test_exp_negative_half() {
158        // exp(-0.5) ≈ 0.6065 → 0.6065 × 32768 ≈ 19874
159        // x = -0.5 en Q15 → -16384
160        let res = exp_q15(-16384);
161        assert!((res as i32 - 19874).abs() < 10, "exp(-0.5): attendu ≈19874, reçu {}", res);
162    }
163
164    #[test]
165    fn test_exp_negative_one() {
166        // exp(-1.0) ≈ 0.3679 → 0.3679 × 32768 ≈ 12054
167        // x = -1.0 en Q15 → -32768
168        let res = exp_q15(-32768);
169        assert!((res as i32 - 12054).abs() < 10, "exp(-1.0): attendu ≈12054, reçu {}", res);
170    }
171
172    #[test]
173    fn test_exp_strictly_positive() {
174        // exp(x) > 0 pour tout x réel
175        assert!(exp_q15(-32768) > 0);
176        assert!(exp_q15(-16384) > 0);
177        assert!(exp_q15(-1) > 0);
178    }
179
180    #[test]
181    fn test_exp_monotone() {
182        // e^x est strictement croissante
183        let a = exp_q15(-32768); // exp(-1.000)
184        let b = exp_q15(-28672); // exp(-0.875)
185        let c = exp_q15(-24576); // exp(-0.750)
186        let d = exp_q15(-16384); // exp(-0.500)
187        let e = exp_q15(-8192);  // exp(-0.250)
188        assert!(a < b, "exp(-1.0)={} doit être < exp(-0.875)={}", a, b);
189        assert!(b < c, "exp(-0.875)={} doit être < exp(-0.75)={}", b, c);
190        assert!(c < d, "exp(-0.75)={} doit être < exp(-0.5)={}", c, d);
191        assert!(d < e, "exp(-0.5)={} doit être < exp(-0.25)={}", d, e);
192    }
193}