Skip to main content

sigmoid_q15/
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//! # sigmoid-q15
9//!
10//! Fonction d'activation Sigmoid 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//! - Utilise [`embedded-exp`](https://crates.io/crates/embedded-exp) pour le calcul de `e^x`.
18//! - Temps d'exécution **constant** (déterministe) : idéal pour les noyaux temps réel.
19//! - Zéro allocation dynamique : traitement in-place possible.
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//!
28//! ## Algorithme
29//!
30//! `sigmoid(x) = 1 / (1 + e^(-x))`
31//!
32//! Le problème : `embedded-exp` ne fonctionne que sur le domaine **négatif**.
33//! On exploite la symétrie de sigmoid pour toujours rester sur ce domaine :
34//!
35//! ```text
36//! sigmoid(x)  = 1 / (1 + e^(-x))      si x <= 0  → -x >= 0... non
37//! ```
38//!
39//! Plus précisément :
40//!
41//! - Si `x <= 0` : `-x >= 0`, mais `e^(-x)` avec `-x >= 0` saturerait.
42//!   On utilise donc `sigmoid(x) = 1 - sigmoid(-x)` pour ramener
43//!   le calcul sur le domaine négatif.
44//! - Si `x > 0` : `-x < 0` → `exp_q15(-x)` calcule normalement.
45//! - Si `x = 0` : `sigmoid(0) = 0.5` → `16384` en Q15.
46//!
47//! **Propriété exploitée :**
48//! ```text
49//! sigmoid(-x) = 1 - sigmoid(x)
50//! ```
51//! On calcule toujours `exp_q15` sur une valeur **négative ou nulle**,
52//! ce qui est exactement le domaine valide de `embedded-exp`.
53//!
54//! ## Exemple
55//!
56//! ```rust
57//! use sigmoid_q15::{sigmoid_q15, sigmoid_slice_q15};
58//!
59//! // sigmoid(0) = 0.5 → 16384 en Q15
60//! assert_eq!(sigmoid_q15(0), 16384);
61//!
62//! // sigmoid(-1.0) ≈ 0.2689 → ≈ 8808 en Q15
63//! let res = sigmoid_q15(-32768);
64//! assert!((res as i32 - 8808).abs() < 20);
65//!
66//! // sigmoid(x) est toujours dans ]0.0, 1.0[ → toujours positif
67//! assert!(sigmoid_q15(i16::MIN) > 0);
68//! assert!(sigmoid_q15(i16::MAX) > 0);
69//!
70//! // Traitement in-place
71//! let mut buf = [-32768i16, 0, 32767];
72//! sigmoid_slice_q15(&mut buf);
73//! assert!(buf[0] > 0);
74//! assert_eq!(buf[1], 16384);
75//! assert!(buf[2] > 16384);
76//! ```
77
78#![no_std]
79#![forbid(unsafe_code)]
80
81use embedded_exp::exp_q15;
82
83/// Un en Q15 : représente 1.0, mais i16::MAX = 32767 ≈ 1.0
84/// On utilise 32768 comme "1.0 exact" en i32 pour les calculs internes.
85const ONE_Q15: i32 = 32768;
86
87/// Calcule la fonction d'activation Sigmoid pour un nombre en virgule fixe Q15.
88///
89/// `sigmoid(x) = 1 / (1 + e^(-x))`
90///
91/// La sortie est toujours dans `]0.0, 1.0[`, soit `[1, 32767]` en Q15.
92///
93/// ## Stratégie
94///
95/// On exploite la symétrie `sigmoid(-x) = 1 - sigmoid(x)` pour toujours
96/// appeler `exp_q15` avec une valeur **négative**, son domaine valide.
97///
98/// | Entrée `x` | Calcul effectué                        |
99/// |-----------|----------------------------------------|
100/// | `x > 0`   | `1 / (1 + exp(-x))` directement        |
101/// | `x == 0`  | retourne `16384` (0.5 exact)           |
102/// | `x < 0`   | `1 - sigmoid(-x)` par symétrie         |
103///
104/// ## Cas limites
105///
106/// | Entrée     | Valeur Q15 | Sortie attendue     |
107/// |------------|-----------|---------------------|
108/// | `i16::MIN` | -32768    | ≈ 8808 (≈ 0.2689)   |
109/// | `0`        | 0         | 16384 (= 0.5 exact) |
110/// | `i16::MAX` | 32767     | ≈ 23960 (≈ 0.7311)  |
111///
112/// # Exemple
113///
114/// ```rust
115/// use sigmoid_q15::sigmoid_q15;
116///
117/// assert_eq!(sigmoid_q15(0), 16384);
118/// assert!(sigmoid_q15(-32768) > 0);
119/// assert!(sigmoid_q15(32767) > 16384);
120/// ```
121#[inline]
122pub fn sigmoid_q15(x: i16) -> i16 {
123    // Cas x = 0 : sigmoid(0) = 0.5 = 16384 en Q15
124    if x == 0 {
125        return 16384;
126    }
127
128    if x > 0 {
129        // x > 0 → -x < 0 → exp_q15(-x) est dans le domaine valide
130        // sigmoid(x) = 1 / (1 + e^(-x))
131        sigmoid_positive(x)
132    } else {
133        // x < 0 → on utilise la symétrie : sigmoid(x) = 1 - sigmoid(-x)
134        // -x > 0 → sigmoid(-x) se calcule via sigmoid_positive(-x)
135        // Comme x >= i16::MIN, -x peut déborder si x == i16::MIN.
136        // On gère ce cas : si x == i16::MIN, on sature -x à i16::MAX.
137        let neg_x = if x == i16::MIN {
138            i16::MAX
139        } else {
140            -x
141        };
142        let s_neg_x = sigmoid_positive(neg_x) as i32;
143        // 1 - sigmoid(-x) en Q15
144        // ONE_Q15 = 32768, mais sigmoid retourne au max 32767
145        // On clamp à 1 minimum pour garantir > 0
146        let result = ONE_Q15 - s_neg_x;
147        result.max(1).min(32767) as i16
148    }
149}
150
151/// Calcul interne de sigmoid pour x > 0.
152/// Précondition : x > 0, donc -x < 0, donc exp_q15(-x) est valide.
153#[inline]
154fn sigmoid_positive(x: i16) -> i16 {
155    debug_assert!(x > 0);
156
157    // -x est négatif : domaine valide pour exp_q15
158    let neg_x = -x; // x > 0 et x != i16::MIN donc pas de débordement
159
160    // e^(-x) en Q15
161    let exp_neg_x = exp_q15(neg_x) as i32;
162
163    // 1 + e^(-x) en Q15
164    // ONE_Q15 = 32768 représente 1.0 ; exp_neg_x ∈ [1, 32767]
165    // Somme max : 32768 + 32767 = 65535 → tient dans i32
166    let denom = ONE_Q15 + exp_neg_x;
167
168    // sigmoid = 1 / (1 + e^(-x)) = ONE_Q15 / denom
169    // On multiplie numérateur par ONE_Q15 pour rester en Q15 :
170    // résultat = (ONE_Q15 * ONE_Q15) / denom
171    // ONE_Q15 * ONE_Q15 = 32768 * 32768 = 1_073_741_824 → tient dans i32 (max ~2.1e9)
172    let result = (ONE_Q15 * ONE_Q15) / denom;
173
174    // Saturation : résultat dans [1, 32767]
175    result.max(1).min(32767) as i16
176}
177
178/// Applique Sigmoid sur un slice de données en virgule fixe Q15, **in-place**.
179///
180/// Chaque élément `v` est remplacé par `sigmoid(v)`.
181/// L'opération in-place évite toute allocation dynamique.
182///
183/// ## Slice vide
184///
185/// Un slice vide (`&mut []`) est accepté sans erreur.
186///
187/// # Exemple
188///
189/// ```rust
190/// use sigmoid_q15::sigmoid_slice_q15;
191///
192/// let mut data = [-32768i16, 0, 32767];
193/// sigmoid_slice_q15(&mut data);
194/// // Tous les résultats sont dans ]0, 32767]
195/// assert!(data[0] > 0 && data[0] < 16384); // < 0.5
196/// assert_eq!(data[1], 16384);               // = 0.5
197/// assert!(data[2] > 16384);                 // > 0.5
198///
199/// // Slice vide : aucun effet, aucune panique
200/// let mut empty: [i16; 0] = [];
201/// sigmoid_slice_q15(&mut empty);
202/// ```
203#[inline]
204pub fn sigmoid_slice_q15(data: &mut [i16]) {
205    for val in data.iter_mut() {
206        *val = sigmoid_q15(*val);
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    //  Valeurs de référence 
215    // sigmoid(0.0)  = 0.5     → 16384 en Q15
216    // sigmoid(-1.0) ≈ 0.2689  → ≈ 8808 en Q15
217    // sigmoid(1.0)  ≈ 0.7311  → ≈ 23960 en Q15
218    // sigmoid(-0.5) ≈ 0.3775  → ≈ 12372 en Q15
219    // sigmoid(0.5)  ≈ 0.6225  → ≈ 20396 en Q15
220
221    const TOLERANCE: i32 = 30; // tolérance en ULP Q15
222
223    #[test]
224    fn test_sigmoid_zero_is_half() {
225        // sigmoid(0) = 0.5 exact → 16384 en Q15
226        assert_eq!(sigmoid_q15(0), 16384);
227    }
228
229    #[test]
230    fn test_sigmoid_negative_one() {
231        // sigmoid(-1.0) ≈ 0.2689 → ≈ 8808
232        let res = sigmoid_q15(-32768);
233        assert!(
234            (res as i32 - 8808).abs() < TOLERANCE,
235            "sigmoid(-1.0): attendu ≈8808, reçu {}",
236            res
237        );
238    }
239
240    #[test]
241    fn test_sigmoid_positive_one() {
242        // sigmoid(1.0) ≈ 0.7311 → ≈ 23960
243        // x = i16::MAX ≈ 1.0 en Q15
244        let res = sigmoid_q15(i16::MAX);
245        assert!(
246            (res as i32 - 23960).abs() < TOLERANCE,
247            "sigmoid(≈1.0): attendu ≈23960, reçu {}",
248            res
249        );
250    }
251
252    #[test]
253    fn test_sigmoid_negative_half() {
254        // sigmoid(-0.5) ≈ 0.3775 → ≈ 12372
255        let res = sigmoid_q15(-16384);
256        assert!(
257            (res as i32 - 12372).abs() < TOLERANCE,
258            "sigmoid(-0.5): attendu ≈12372, reçu {}",
259            res
260        );
261    }
262
263    #[test]
264    fn test_sigmoid_positive_half() {
265        // sigmoid(0.5) ≈ 0.6225 → ≈ 20396
266        let res = sigmoid_q15(16384);
267        assert!(
268            (res as i32 - 20396).abs() < TOLERANCE,
269            "sigmoid(0.5): attendu ≈20396, reçu {}",
270            res
271        );
272    }
273
274    #[test]
275    fn test_sigmoid_symmetry() {
276        // sigmoid(x) + sigmoid(-x) ≈ 1.0 (= 32768 en Q15)
277        // On exclut i16::MIN car -i16::MIN déborde en i16
278        for x in [-16384i16, -8192, -4096, -1] {
279            let s_pos = sigmoid_q15(-x) as i32;
280            let s_neg = sigmoid_q15(x) as i32;
281            let sum = s_pos + s_neg;
282            assert!(
283                (sum - 32768).abs() < TOLERANCE * 2,
284                "symétrie brisée pour x={}: sigmoid({})={} + sigmoid({})={} = {}",
285                x, -x, s_pos, x, s_neg, sum
286            );
287        }
288    }
289
290    #[test]
291    fn test_sigmoid_always_positive() {
292        // sigmoid(x) > 0 pour tout x
293        for x in [i16::MIN, -16384, -1, 0, 1, 16384, i16::MAX] {
294            assert!(sigmoid_q15(x) > 0, "sigmoid({}) doit être > 0", x);
295        }
296    }
297
298    #[test]
299    fn test_sigmoid_always_below_one() {
300        // sigmoid(x) < 1.0 (< 32768) → au max i16::MAX = 32767
301        // On cast en i32 pour que la comparaison ait du sens pour le compilateur
302        for x in [i16::MIN, -16384, -1, 0, 1, 16384, i16::MAX] {
303            assert!(
304                (sigmoid_q15(x) as i32) < 32768,
305                "sigmoid({}) doit être < 32768",
306                x
307            );
308        }
309    }
310
311    #[test]
312    fn test_sigmoid_monotone() {
313        // sigmoid est strictement croissante
314        let a = sigmoid_q15(-32768); // sigmoid(-1.0)
315        let b = sigmoid_q15(-16384); // sigmoid(-0.5)
316        let c = sigmoid_q15(0);      // sigmoid(0.0)
317        let d = sigmoid_q15(16384);  // sigmoid(0.5)
318        let e = sigmoid_q15(32767);  // sigmoid(≈1.0)
319        assert!(a < b, "sigmoid(-1.0)={} < sigmoid(-0.5)={}", a, b);
320        assert!(b < c, "sigmoid(-0.5)={} < sigmoid(0.0)={}", b, c);
321        assert!(c < d, "sigmoid(0.0)={} < sigmoid(0.5)={}", c, d);
322        assert!(d < e, "sigmoid(0.5)={} < sigmoid(1.0)={}", d, e);
323    }
324
325    #[test]
326    fn test_sigmoid_min_no_panic() {
327        // i16::MIN est un cas limite : -i16::MIN déborde en i16
328        // On vérifie que ça ne panique pas et retourne quelque chose de valide
329        let res = sigmoid_q15(i16::MIN);
330        assert!(res > 0, "sigmoid(i16::MIN)={} doit être > 0", res);
331    }
332
333    #[test]
334    fn test_sigmoid_output_in_range() {
335        // Toute sortie doit être dans [1, 32767]
336        for x in [i16::MIN, -32000, -16384, -1, 0, 1, 16384, 32000, i16::MAX] {
337            let res = sigmoid_q15(x);
338            assert!(
339                res >= 1,
340                "sigmoid({})={} doit être >= 1",
341                x, res
342            );
343        }
344    }
345
346    //sigmoid_slice_q15 
347
348    #[test]
349    fn test_slice_empty() {
350        let mut empty: [i16; 0] = [];
351        sigmoid_slice_q15(&mut empty);
352    }
353
354    #[test]
355    fn test_slice_zero_gives_half() {
356        let mut data = [0i16];
357        sigmoid_slice_q15(&mut data);
358        assert_eq!(data[0], 16384);
359    }
360
361    #[test]
362    fn test_slice_mixed() {
363        let mut data = [-32768i16, 0, 32767];
364        sigmoid_slice_q15(&mut data);
365        assert!(data[0] > 0 && data[0] < 16384); // < 0.5
366        assert_eq!(data[1], 16384);               // = 0.5
367        assert!(data[2] > 16384);                 // > 0.5
368    }
369
370    #[test]
371    fn test_slice_all_positive_above_half() {
372        let mut data = [1i16, 8192, 16384, 32767];
373        sigmoid_slice_q15(&mut data);
374        for val in data {
375            assert!(val > 16384, "sigmoid positif doit être > 16384, reçu {}", val);
376        }
377    }
378
379    #[test]
380    fn test_slice_all_negative_below_half() {
381        let mut data = [-1i16, -8192, -16384, -32768];
382        sigmoid_slice_q15(&mut data);
383        for val in data {
384            assert!(val < 16384, "sigmoid négatif doit être < 16384, reçu {}", val);
385        }
386    }
387
388    #[test]
389    fn test_slice_idempotent_after_double_pass() {
390        // sigmoid n'est PAS idempotente (sigmoid(sigmoid(x)) != sigmoid(x))
391        // mais on vérifie que deux passes consécutives ne paniquent pas
392        // et restent dans la plage valide
393        let mut data = [-32768i16, -16384, 0, 16384, 32767];
394        sigmoid_slice_q15(&mut data);
395        sigmoid_slice_q15(&mut data);
396        for val in data {
397            assert!(val >= 1, "hors plage après double passe: {}", val);
398        }
399    }
400
401    #[test]
402    fn test_slice_boundary_values() {
403        let mut data = [i16::MIN, i16::MAX];
404        sigmoid_slice_q15(&mut data);
405        assert!(data[0] > 0 && data[0] < 16384);
406        assert!(data[1] > 16384);
407    }
408}