relu_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//! # relu-q15
9//!
10//! Fonction d'activation ReLU 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+) sans fpu, (sans unité de calcul en virgule flottante)
17//! - Temps d'exécution **constant** (déterministe) idéal pour les noyaux temps réel
18//! - Zéro allocation dynamique, traitement in-place possible
19//!
20//! ## Format Q15
21//!
22//! En Q15, un `i16` représente un nombre réel dans `[-1.0, 1.0[` :
23//! ```text
24//! valeur_réelle = valeur_i16 / 32768.0
25//! ```
26//! Exemples :
27//! - `0` → 0.0
28//! - `16384` → 0.5
29//! - `32767` → ≈ 1.0 (i16::MAX)
30//! - `-32768` → -1.0 (i16::MIN)
31//!
32//! ## Algorithme
33//!
34//! ReLU (Rectified Linear Unit) : `f(x) = max(0, x)`
35//!
36//! - Si `x < 0` → retourne `0`
37//! - Si `x >= 0` → retourne `x` inchangé (identité)
38//!
39//! Cas limites traités explicitement :
40//! - `i16::MIN` (-32768) → `0` (valeur la plus négative possible en Q15)
41//! - `i16::MAX` (32767) → `32767` (passé tel quel)
42//! - `0` → `0` (zéro reste zéro)
43//!
44//! ## Exemple
45//!
46//! ```rust
47//! use relu_q15::{relu_q15, relu_slice_q15};
48//!
49//! // Valeurs négatives → 0
50//! assert_eq!(relu_q15(-32768), 0); // i16::MIN
51//! assert_eq!(relu_q15(-16384), 0); // -0.5
52//! assert_eq!(relu_q15(-1), 0);
53//!
54//! // Zéro → 0
55//! assert_eq!(relu_q15(0), 0);
56//!
57//! // Valeurs positives → identité
58//! assert_eq!(relu_q15(1), 1);
59//! assert_eq!(relu_q15(16384), 16384); // 0.5
60//! assert_eq!(relu_q15(32767), 32767); // i16::MAX
61//!
62//! // Traitement in-place d'un vecteur
63//! let mut buf = [-32768i16, -100, 0, 100, 32767];
64//! relu_slice_q15(&mut buf);
65//! assert_eq!(buf, [0, 0, 0, 100, 32767]);
66//! ```
67
68#![no_std]
69#![forbid(unsafe_code)]
70
71/// Calcule la fonction d'activation ReLU pour un seul nombre en virgule fixe Q15.
72///
73/// `f(x) = max(0, x)`
74///
75/// # Cas limites
76///
77/// | Entrée | Valeur Q15 | Sortie |
78/// |----------------|-----------|--------|
79/// | `i16::MIN` | -32768 | `0` |
80/// | `-1` | -1 | `0` |
81/// | `0` | 0 | `0` |
82/// | `1` | 1 | `1` |
83/// | `i16::MAX` | 32767 | `32767`|
84///
85/// # Exemple
86///
87/// ```rust
88/// use relu_q15::relu_q15;
89/// assert_eq!(relu_q15(-16384), 0); // exp(-0.5) → tronqué
90/// assert_eq!(relu_q15(0), 0);
91/// assert_eq!(relu_q15(16384), 16384); // 0.5 → identité
92/// ```
93#[inline]
94pub fn relu_q15(x: i16) -> i16 {
95 // Branchement sur le bit de signe : aucune opération arithmétique nécessaire.
96 // Équivalent à `max(0, x)` sans risque de débordement car on ne fait
97 // jamais de multiplication ni d'addition.
98 if x < 0 { 0 } else { x }
99}
100
101/// Applique ReLU sur un slice de données en virgule fixe Q15, **in-place**.
102///
103/// Chaque élément `v` est remplacé par `max(0, v)`.
104/// L'opération in-place évite toute allocation dynamique, ce qui est
105/// critique sur des MCU avec quelques Ko de RAM (ex. RP2040 : 264 Ko).
106///
107/// # Slice vide
108///
109/// Un slice vide (`&mut []`) est accepté sans erreur : la fonction retourne
110/// immédiatement sans rien faire.
111///
112/// # Exemple
113///
114/// ```rust
115/// use relu_q15::relu_slice_q15;
116///
117/// let mut data = [-32768i16, -1, 0, 1, 32767];
118/// relu_slice_q15(&mut data);
119/// assert_eq!(data, [0, 0, 0, 1, 32767]);
120///
121/// // Slice vide : aucun effet, aucune panique
122/// let mut empty: [i16; 0] = [];
123/// relu_slice_q15(&mut empty);
124/// ```
125#[inline]
126pub fn relu_slice_q15(data: &mut [i16]) {
127 for val in data.iter_mut() {
128 *val = relu_q15(*val);
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 // relu_q15 scalaire
137
138 #[test]
139 fn test_relu_min_is_zero() {
140 // i16::MIN (-32768) est la valeur la plus négative : doit retourner 0
141 assert_eq!(relu_q15(i16::MIN), 0);
142 }
143
144 #[test]
145 fn test_relu_minus_one_is_zero() {
146 assert_eq!(relu_q15(-1), 0);
147 }
148
149 #[test]
150 fn test_relu_zero_is_zero() {
151 // Zéro n'est pas négatif : relu(0) = 0
152 assert_eq!(relu_q15(0), 0);
153 }
154
155 #[test]
156 fn test_relu_one_is_one() {
157 assert_eq!(relu_q15(1), 1);
158 }
159
160 #[test]
161 fn test_relu_max_is_identity() {
162 // i16::MAX (32767) doit être retourné tel quel
163 assert_eq!(relu_q15(i16::MAX), i16::MAX);
164 }
165
166 #[test]
167 fn test_relu_negative_half() {
168 // -0.5 en Q15 → -16384 → 0
169 assert_eq!(relu_q15(-16384), 0);
170 }
171
172 #[test]
173 fn test_relu_positive_half() {
174 // 0.5 en Q15 → 16384 → identité
175 assert_eq!(relu_q15(16384), 16384);
176 }
177
178 #[test]
179 fn test_relu_negative_quarter() {
180 // -0.25 en Q15 → -8192 → 0
181 assert_eq!(relu_q15(-8192), 0);
182 }
183
184 #[test]
185 fn test_relu_positive_quarter() {
186 // 0.25 en Q15 → 8192 → identité
187 assert_eq!(relu_q15(8192), 8192);
188 }
189
190 #[test]
191 fn test_relu_idempotent_positive() {
192 // relu(relu(x)) == relu(x) pour tout x positif
193 for x in [1i16, 100, 8192, 16384, 32767] {
194 assert_eq!(relu_q15(relu_q15(x)), relu_q15(x));
195 }
196 }
197
198 #[test]
199 fn test_relu_idempotent_zero() {
200 assert_eq!(relu_q15(relu_q15(0)), relu_q15(0));
201 }
202
203 #[test]
204 fn test_relu_output_never_negative() {
205 // relu(x) >= 0 pour tout x possible en i16
206 // On teste un sous-ensemble représentatif
207 for x in [i16::MIN, -16384, -8192, -1, 0, 1, 8192, 16384, i16::MAX] {
208 assert!(relu_q15(x) >= 0, "relu_q15({}) doit être >= 0", x);
209 }
210 }
211
212 #[test]
213 fn test_relu_output_never_exceeds_input_positive() {
214 // Pour x >= 0, relu(x) == x (identité stricte)
215 for x in [0i16, 1, 8192, 16384, 32767] {
216 assert_eq!(relu_q15(x), x, "relu_q15({}) doit être == {}", x, x);
217 }
218 }
219
220 // relu_slice_q15
221
222 #[test]
223 fn test_slice_empty() {
224 // Slice vide : aucune panique, aucun effet
225 let mut empty: [i16; 0] = [];
226 relu_slice_q15(&mut empty); // ne doit pas paniquer
227 }
228
229 #[test]
230 fn test_slice_single_negative() {
231 let mut data = [-1i16];
232 relu_slice_q15(&mut data);
233 assert_eq!(data, [0]);
234 }
235
236 #[test]
237 fn test_slice_single_zero() {
238 let mut data = [0i16];
239 relu_slice_q15(&mut data);
240 assert_eq!(data, [0]);
241 }
242
243 #[test]
244 fn test_slice_single_positive() {
245 let mut data = [16384i16];
246 relu_slice_q15(&mut data);
247 assert_eq!(data, [16384]);
248 }
249
250 #[test]
251 fn test_slice_mixed() {
252 let mut data = [-32768i16, -1, 0, 1, 32767];
253 relu_slice_q15(&mut data);
254 assert_eq!(data, [0, 0, 0, 1, 32767]);
255 }
256
257 #[test]
258 fn test_slice_all_negative() {
259 let mut data = [-32768i16, -16384, -8192, -1];
260 relu_slice_q15(&mut data);
261 assert_eq!(data, [0, 0, 0, 0]);
262 }
263
264 #[test]
265 fn test_slice_all_positive() {
266 let mut data = [1i16, 8192, 16384, 32767];
267 relu_slice_q15(&mut data);
268 assert_eq!(data, [1, 8192, 16384, 32767]);
269 }
270
271 #[test]
272 fn test_slice_all_zero() {
273 let mut data = [0i16; 8];
274 relu_slice_q15(&mut data);
275 assert_eq!(data, [0i16; 8]);
276 }
277
278 #[test]
279 fn test_slice_idempotent() {
280 // Appliquer relu deux fois doit donner le même résultat
281 let mut data1 = [-32768i16, -100, 0, 100, 32767];
282 let mut data2 = data1;
283 relu_slice_q15(&mut data1);
284 relu_slice_q15(&mut data1); // deuxième passage
285 relu_slice_q15(&mut data2); // un seul passage
286 assert_eq!(data1, data2);
287 }
288
289 #[test]
290 fn test_slice_boundary_values() {
291 // i16::MIN et i16::MAX dans le même buffer
292 let mut data = [i16::MIN, i16::MAX];
293 relu_slice_q15(&mut data);
294 assert_eq!(data, [0, i16::MAX]);
295 }
296}