embedded_so3_f32/lib.rs
1// Copyright (C) 2026 Jorge Andre Castro
2// Ce programme est un logiciel libre : vous pouvez le redistribuer et/ou le modifier
3// selon les termes de la Licence Publique Générale GNU telle que publiée par la
4// Free Software Foundation, soit la version 2 de la licence, soit (à votre convention)
5// n'importe quelle version ultérieure.
6
7//! # embedded-so3-f32
8//!
9//! [`Rotation`] représentée par des quaternions unitaires pour le groupe SO(3).
10//!
11//! Algèbre géométrique `f32` pour systèmes embarqués `no_std`.
12//! Sans dépendance standard, sans `unsafe`, sans allocation.
13//!
14//! ## Exemple
15//!
16//! ```rust
17//! use embedded_so3_f32::Rotation;
18//!
19//! // Rotation 90° autour de l'axe X (cos 45° = sin 45° ≈ 0.7071)
20//! let s = core::f32::consts::FRAC_1_SQRT_2;
21//! let rot = Rotation::new(s, s, 0.0, 0.0).unwrap();
22//!
23//! let v = [0.0_f32, 1.0, 0.0];
24//! let rotated = rot.rotate_vector(v);
25//! // ≈ [0, 0, 1]
26//!
27//! // Composition avec l'opérateur *
28//! let double = (rot * rot).unwrap();
29//! // double ≈ rotation 180° autour de X
30//! ```
31
32#![no_std]
33#![forbid(unsafe_code)]
34#![warn(missing_docs)]
35
36use core::ops::Mul;
37use embedded_f32_sqrt::sqrt;
38
39/// Erreurs de validation pour les opérations SO(3).
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum So3Error {
42 /// La norme des composants est nulle ou trop proche de zéro.
43 InvalidNorm,
44 /// Une valeur `NaN` ou infinie a été détectée.
45 NonFiniteValue,
46}
47
48/// Représente une rotation dans l'espace 3D (Groupe SO(3)).
49///
50/// Stocke un quaternion unitaire q = \[w, x, y, z\] où :
51/// - `w = cos(θ/2)`
52/// - `(x, y, z) = sin(θ/2) · û` avec `û` l'axe de rotation unitaire
53///
54/// L'invariant `w² + x² + y² + z² = 1` est garanti à la construction
55/// et maintenu après chaque composition via re-normalisation automatique.
56#[derive(Debug, Clone, Copy, PartialEq)]
57pub struct Rotation {
58 w: f32,
59 x: f32,
60 y: f32,
61 z: f32,
62}
63
64impl Rotation {
65 /// Identité : aucune rotation (quaternion `[1, 0, 0, 0]`).
66 pub const IDENTITY: Self = Self { w: 1.0, x: 0.0, y: 0.0, z: 0.0 };
67
68 /// Crée une rotation à partir d'un quaternion brut `[w, x, y, z]`.
69 ///
70 /// Les composants sont normalisés automatiquement.
71 ///
72 /// # Erreurs
73 /// - [`So3Error::NonFiniteValue`] si l'un des composants est `NaN` ou infini.
74 /// - [`So3Error::InvalidNorm`] si la norme est nulle (vecteur nul).
75 ///
76 /// # Exemple
77 /// ```rust
78 /// use embedded_so3_f32::Rotation;
79 /// // 90° autour de Z (w = cos 45°, z = sin 45°)
80 /// let s = core::f32::consts::FRAC_1_SQRT_2;
81 /// let rot = Rotation::new(s, 0.0, 0.0, s).unwrap();
82 /// ```
83 pub fn new(w: f32, x: f32, y: f32, z: f32) -> Result<Self, So3Error> {
84 Self::normalize_raw(w, x, y, z)
85 }
86
87 /// Utilitaire interne : normalise les composants sur la sphère unité.
88 fn normalize_raw(w: f32, x: f32, y: f32, z: f32) -> Result<Self, So3Error> {
89 let norm_sq = w * w + x * x + y * y + z * z;
90
91 if !norm_sq.is_finite() {
92 return Err(So3Error::NonFiniteValue);
93 }
94
95 let n = sqrt(norm_sq).map_err(|_| So3Error::InvalidNorm)?;
96 if n < 1e-10 {
97 return Err(So3Error::InvalidNorm);
98 }
99
100 Ok(Self {
101 w: w / n,
102 x: x / n,
103 y: y / n,
104 z: z / n,
105 })
106 }
107
108 /// Compose deux rotations : `R_new = self ∘ other`.
109 ///
110 /// Équivalent à `self * other` via l'implémentation de [`Mul`].
111 ///
112 /// Applique une re-normalisation automatique pour contenir la dérive
113 /// numérique inhérente à l'arithmétique `f32` sur SO(3).
114 ///
115 /// # Erreurs
116 /// Retourne [`So3Error::InvalidNorm`] ou [`So3Error::NonFiniteValue`]
117 /// si le résultat est dégénéré (cas extrêmement rare avec des entrées valides).
118 pub fn compose(&self, other: &Rotation) -> Result<Self, So3Error> {
119 let w = self.w * other.w - self.x * other.x - self.y * other.y - self.z * other.z;
120 let x = self.w * other.x + self.x * other.w + self.y * other.z - self.z * other.y;
121 let y = self.w * other.y - self.x * other.z + self.y * other.w + self.z * other.x;
122 let z = self.w * other.z + self.x * other.y - self.y * other.x + self.z * other.w;
123
124 Self::normalize_raw(w, x, y, z)
125 }
126
127 /// Applique la rotation à un vecteur `[x, y, z]`.
128 ///
129 /// Utilise la formule optimisée de Rodrigues pour quaternions :
130 /// `v' = v + 2w·(q⃗ × v) + 2·(q⃗ × (q⃗ × v))`
131 ///
132 /// Cette forme évite le produit `p·q·p⁻¹` naïf (28 multiplications)
133 /// et ne nécessite que 15 multiplications.
134 pub fn rotate_vector(&self, v: [f32; 3]) -> [f32; 3] {
135 let [vx, vy, vz] = v;
136
137 // t = 2 * q_vec × v
138 let tx = 2.0 * (self.y * vz - self.z * vy);
139 let ty = 2.0 * (self.z * vx - self.x * vz);
140 let tz = 2.0 * (self.x * vy - self.y * vx);
141
142 // v' = v + w·t + q_vec × t
143 [
144 vx + self.w * tx + (self.y * tz - self.z * ty),
145 vy + self.w * ty + (self.z * tx - self.x * tz),
146 vz + self.w * tz + (self.x * ty - self.y * tx),
147 ]
148 }
149
150 /// Retourne la rotation inverse.
151 ///
152 /// Pour un quaternion unitaire, l'inverse est le conjugué :
153 /// `q⁻¹ = [w, -x, -y, -z]`.
154 ///
155 /// Aucune normalisation n'est nécessaire car le conjugué d'un
156 /// quaternion unitaire est déjà unitaire.
157 pub fn inverse(&self) -> Self {
158 Self {
159 w: self.w,
160 x: -self.x,
161 y: -self.y,
162 z: -self.z,
163 }
164 }
165
166 /// Retourne les composants bruts sous la forme `[w, x, y, z]`.
167 pub fn as_array(&self) -> [f32; 4] {
168 [self.w, self.x, self.y, self.z]
169 }
170}
171
172/// Opérateur `*` pour composer deux rotations.
173///
174/// Équivalent à [`Rotation::compose`].
175///
176/// # Exemple
177/// ```rust
178/// use embedded_so3_f32::Rotation;
179/// let s = core::f32::consts::FRAC_1_SQRT_2;
180/// let r = Rotation::new(s, s, 0.0, 0.0).unwrap(); // 90° autour de X
181/// let r180 = (r * r).unwrap(); // 180° autour de X
182/// ```
183impl Mul for Rotation {
184 type Output = Result<Rotation, So3Error>;
185
186 fn mul(self, rhs: Self) -> Self::Output {
187 self.compose(&rhs)
188 }
189}
190
191/// Opérateur `*` par référence pour éviter la copie inutile.
192impl Mul for &Rotation {
193 type Output = Result<Rotation, So3Error>;
194
195 fn mul(self, rhs: Self) -> Self::Output {
196 self.compose(rhs)
197 }
198}
199
200// TESTS UNITAIRES
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn test_identity_stable() {
208 let v = [1.0, 2.0, 3.0];
209 let res = Rotation::IDENTITY.rotate_vector(v);
210 assert!((res[0] - 1.0).abs() < 1e-7);
211 assert!((res[1] - 2.0).abs() < 1e-7);
212 assert!((res[2] - 3.0).abs() < 1e-7);
213 }
214
215 #[test]
216 fn test_rotation_90_x() {
217 // Convention : q = [cos(θ/2), sin(θ/2)·x̂]
218 // 90° autour de X → θ/2 = 45° → w = x = cos(45°) = √2/2
219 let s = core::f32::consts::FRAC_1_SQRT_2;
220 let rot = Rotation::new(s, s, 0.0, 0.0).unwrap();
221 let res = rot.rotate_vector([0.0, 1.0, 0.0]);
222
223 // Y → Z sous une rotation 90° autour de X
224 assert!(res[0].abs() < 1e-6, "x devrait être 0, obtenu {}", res[0]);
225 assert!(res[1].abs() < 1e-6, "y devrait être 0, obtenu {}", res[1]);
226 assert!((res[2] - 1.0).abs() < 1e-6, "z devrait être 1, obtenu {}", res[2]);
227 }
228
229 #[test]
230 fn test_mul_operator() {
231 let s = core::f32::consts::FRAC_1_SQRT_2;
232 let r90 = Rotation::new(s, s, 0.0, 0.0).unwrap(); // 90° autour de X
233
234 // 90° * 90° = 180° autour de X → Y → -Y
235 let r180 = (r90 * r90).unwrap();
236 let res = r180.rotate_vector([0.0, 1.0, 0.0]);
237
238 assert!(res[0].abs() < 1e-6);
239 assert!((res[1] + 1.0).abs() < 1e-6, "y devrait être -1, obtenu {}", res[1]);
240 assert!(res[2].abs() < 1e-6);
241 }
242
243 #[test]
244 fn test_mul_ref_operator() {
245 let s = core::f32::consts::FRAC_1_SQRT_2;
246 let r = Rotation::new(s, 0.0, s, 0.0).unwrap();
247 // Les deux formes doivent donner le même résultat
248 let via_compose = r.compose(&r).unwrap();
249 let via_mul = (&r * &r).unwrap();
250 assert_eq!(via_compose.as_array(), via_mul.as_array());
251 }
252
253 #[test]
254 fn test_composition_stability() {
255 let s = core::f32::consts::FRAC_1_SQRT_2;
256 let r1 = Rotation::new(s, 0.0, s, 0.0).unwrap(); // 90° Y
257 let mut current = Rotation::IDENTITY;
258
259 // 100 compositions successives : la norme ne doit pas dériver
260 for _ in 0..100 {
261 current = current.compose(&r1).unwrap();
262 }
263
264 let [w, x, y, z] = current.as_array();
265 let norm_sq = w * w + x * x + y * y + z * z;
266 assert!(
267 (norm_sq - 1.0).abs() < 1e-6,
268 "La norme a dérivé : {}",
269 norm_sq
270 );
271 }
272
273 #[test]
274 fn test_inverse_property() {
275 let rot = Rotation::new(1.0, 2.0, 3.0, 4.0).unwrap();
276 let res = rot.compose(&rot.inverse()).unwrap();
277
278 assert!((res.w - 1.0).abs() < 1e-6);
279 assert!(res.x.abs() < 1e-6);
280 assert!(res.y.abs() < 1e-6);
281 assert!(res.z.abs() < 1e-6);
282 }
283
284 #[test]
285 fn test_inverse_via_mul() {
286 let rot = Rotation::new(1.0, 2.0, 3.0, 4.0).unwrap();
287 let res = (rot * rot.inverse()).unwrap();
288
289 assert!((res.w - 1.0).abs() < 1e-6);
290 assert!(res.x.abs() < 1e-6);
291 assert!(res.y.abs() < 1e-6);
292 assert!(res.z.abs() < 1e-6);
293 }
294
295 #[test]
296 fn test_invalid_inputs() {
297 assert_eq!(
298 Rotation::new(0.0, 0.0, 0.0, 0.0),
299 Err(So3Error::InvalidNorm)
300 );
301 assert_eq!(
302 Rotation::new(f32::NAN, 0.0, 0.0, 0.0),
303 Err(So3Error::NonFiniteValue)
304 );
305 assert_eq!(
306 Rotation::new(f32::INFINITY, 0.0, 0.0, 0.0),
307 Err(So3Error::NonFiniteValue)
308 );
309 }
310}