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
65/// ln(2) en Q15 : 0.693147… × 32768 = 22713
66const LN2_Q15: i32 = 22713;
67
68/// 1/ln(2) en Q15 : (1/0.693147…) × 32768 = 47274
69const INV_LN2_Q15: i32 = 47274;
70
71// Multiplicateurs magiques pour éviter les divisions (1/X * 2^15)
72const INV_6_Q15: i32 = 5461;  // round(32768 / 6)
73const INV_24_Q15: i32 = 1365; // round(32768 / 24)
74
75/// Calcule l'exponentielle d'un nombre en virgule fixe Q15.
76/// Temps d'exécution constant et déterministe.
77#[inline]
78pub fn exp_q15(x: i16) -> i16 {
79    // e^x ≥ 1.0 pour tout x ≥ 0 → saturation à i16::MAX
80    if x >= 0 {
81        return i16::MAX;
82    }
83
84    // Protection contre les valeurs trop petites (e^-10 est négligeable en Q15)
85    // -10.0 en Q15 serait -327680, mais x est i16. On couvre toute la plage.
86    let x_i32 = x as i32;
87
88    // --- Étape 1 : Réduction d'intervalle ---
89    // n = floor(x / ln2)
90    let n: i32 = (x_i32 * INV_LN2_Q15) >> 30;
91    
92    // r = x − n·ln(2)
93    let r: i32 = x_i32 - n * LN2_Q15;
94
95    // --- Étape 2 : Approximation polynomiale e^r sur [0, ln(2)[ ---
96    // Taylor degré 4 : e^r ≈ 1 + r + r²/2 + r³/6 + r⁴/24
97    let r2: i32 = (r * r) >> 15;
98    let r3: i32 = (r2 * r) >> 15;
99    let r4: i32 = (r3 * r) >> 15;
100
101    let er: i32 = 32768                      // 1.0 en Q15
102                + r                          // r
103                + (r2 >> 1)                  // r²/2
104                + ((r3 * INV_6_Q15) >> 15)   // r³/6
105                + ((r4 * INV_24_Q15) >> 15); // r⁴/24
106
107    // --- Étape 3 : Reconstruction e^x = 2^n · e^r ---
108    // Comme x < 0, n est toujours négatif ou nul. 
109    // n est l'exposant de 2, on décale à droite de |n|.
110    let result: i32 = er >> (-n);
111
112    // --- Saturation et cast final ---
113    if result >= 32767 {
114        32767
115    } else if result <= 0 {
116        0
117    } else {
118        result as i16
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn test_exp_zero_saturates() {
128        // exp(0.0) = 1.0 → non représentable en Q15 signé → saturation
129        assert_eq!(exp_q15(0), i16::MAX);
130    }
131
132    #[test]
133    fn test_exp_saturation_positive() {
134        // Tout x ≥ 0 → e^x ≥ 1.0 → saturation
135        assert_eq!(exp_q15(1), i16::MAX);
136        assert_eq!(exp_q15(i16::MAX), i16::MAX);
137    }
138
139    #[test]
140    fn test_exp_negative_eighth() {
141        // exp(-0.125) ≈ 0.8825 → 0.8825 × 32768 ≈ 28917
142        // x = -0.125 en Q15 → -4096
143        let res = exp_q15(-4096);
144        assert!((res as i32 - 28917).abs() < 10, "exp(-0.125): attendu ≈28917, reçu {}", res);
145    }
146
147    #[test]
148    fn test_exp_negative_quarter() {
149        // exp(-0.25) ≈ 0.7788 → 0.7788 × 32768 ≈ 25519
150        // x = -0.25 en Q15 → -8192
151        let res = exp_q15(-8192);
152        assert!((res as i32 - 25519).abs() < 10, "exp(-0.25): attendu ≈25519, reçu {}", res);
153    }
154
155    #[test]
156    fn test_exp_negative_half() {
157        // exp(-0.5) ≈ 0.6065 → 0.6065 × 32768 ≈ 19874
158        // x = -0.5 en Q15 → -16384
159        let res = exp_q15(-16384);
160        assert!((res as i32 - 19874).abs() < 10, "exp(-0.5): attendu ≈19874, reçu {}", res);
161    }
162
163    #[test]
164    fn test_exp_negative_one() {
165        // exp(-1.0) ≈ 0.3679 → 0.3679 × 32768 ≈ 12054
166        // x = -1.0 en Q15 → -32768
167        let res = exp_q15(-32768);
168        assert!((res as i32 - 12054).abs() < 10, "exp(-1.0): attendu ≈12054, reçu {}", res);
169    }
170
171    #[test]
172    fn test_exp_strictly_positive() {
173        // exp(x) > 0 pour tout x réel
174        assert!(exp_q15(-32768) > 0);
175        assert!(exp_q15(-16384) > 0);
176        assert!(exp_q15(-1) > 0);
177    }
178
179    #[test]
180    fn test_exp_monotone() {
181        // e^x est strictement croissante
182        let a = exp_q15(-32768); // exp(-1.000)
183        let b = exp_q15(-28672); // exp(-0.875)
184        let c = exp_q15(-24576); // exp(-0.750)
185        let d = exp_q15(-16384); // exp(-0.500)
186        let e = exp_q15(-8192);  // exp(-0.250)
187        assert!(a < b, "exp(-1.0)={} doit être < exp(-0.875)={}", a, b);
188        assert!(b < c, "exp(-0.875)={} doit être < exp(-0.75)={}", b, c);
189        assert!(c < d, "exp(-0.75)={} doit être < exp(-0.5)={}", c, d);
190        assert!(d < e, "exp(-0.5)={} doit être < exp(-0.25)={}", d, e);
191    }
192}