Skip to main content

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}