lie_groups/quaternion.rs
1//! Quaternionic Formulation of SU(2)
2//!
3//! **Addressing Penrose's Recommendation:**
4//! > "SU(2) ≅ unit quaternions. Why not use this beautiful structure?"
5//!
6//! This module implements SU(2) using quaternions instead of 2×2 matrices.
7//!
8//! # Mathematical Background
9//!
10//! **Quaternions:** ℍ = {a + bi + cj + dk | a,b,c,d ∈ ℝ}
11//! with multiplication rules: i² = j² = k² = ijk = -1
12//!
13//! **Unit Quaternions:** S³ = {q ∈ ℍ | |q| = 1} ⊂ ℍ
14//!
15//! **Fundamental Isomorphism:** SU(2) ≅ S³
16//!
17//! The map is:
18//! ```text
19//! SU(2) matrix [[α, -β*], [β, α*]] ↔ Quaternion q = α + βj
20//! where α = a + bi, β = c + di
21//! ⟺ q = a + bi + cj + dk
22//! ```
23//!
24//! # Advantages Over Matrix Representation
25//!
26//! 1. **Compact**: 4 reals vs 8 reals (complex 2×2 matrix)
27//! 2. **Efficient**: Quaternion multiplication is faster than matrix multiplication
28//! 3. **Geometric**: Direct axis-angle interpretation for rotations
29//! 4. **Numerical**: Better numerical stability (no matrix decomposition needed)
30//! 5. **Elegant**: Exponential map is cleaner
31//!
32//! # The SU(2) Double Cover of SO(3) ⭐
33//!
34//! **Critical Physics Insight:**
35//!
36//! SU(2) is a **double cover** of SO(3) (3D rotations):
37//! ```text
38//! SU(2) → SO(3) is a 2-to-1 surjective homomorphism
39//! ```
40//!
41//! **Meaning:** Quaternions **q** and **-q** represent the **same rotation** in 3D space,
42//! but **different quantum states** in SU(2).
43//!
44//! ## Mathematical Statement
45//!
46//! For any unit quaternion q:
47//! - **q** rotates a vector v by angle θ around axis n̂
48//! - **-q** also rotates v by angle θ around axis n̂ (same rotation!)
49//! - But q ≠ -q as group elements in SU(2)
50//!
51//! Path from q → -q → q (going 4π around) is **topologically non-trivial**.
52//!
53//! ## Physical Consequences
54//!
55//! 1. **Fermions (spin-½ particles)**:
56//! - Under 2π rotation: ψ → -ψ (sign flip!)
57//! - Under 4π rotation: ψ → ψ (back to original)
58//! - This is why fermions have **half-integer spin**
59//!
60//! 2. **Bosons (integer spin)**:
61//! - Under 2π rotation: ψ → ψ (no sign change)
62//! - Described by SO(3), not SU(2)
63//!
64//! 3. **Berry Phase**:
65//! - Geometric phase accumulated when traversing closed path in SU(2)
66//! - q → -q → q acquires phase π (Pancharatnam-Berry phase)
67//!
68//! ## Example: Rotation by 2π
69//!
70//! ```text
71//! Rotation by angle θ = 2π around z-axis:
72//! q = cos(π) + sin(π)·k = -1 + 0·k = -1 (minus identity!)
73//!
74//! But -1 acts on vector v as:
75//! v → -1·v·(-1)⁻¹ = -1·v·(-1) = v (identity rotation in SO(3))
76//! ```
77//!
78//! ## Gauge Theory Interpretation
79//!
80//! In gauge theory on networks:
81//! - **Connections** A_{ij} ∈ SU(2) carry full SU(2) structure (not just rotations)
82//! - **Wilson loops** W(C) can wind around non-trivially in SU(2)
83//! - Path C → 2π rotation can give W(C) = -I ≠ I (topological effect!)
84//! - This is invisible in classical physics but crucial for quantum systems
85//!
86//! ## References
87//!
88//! - Dirac, P.A.M.: "The Principles of Quantum Mechanics" (1930) - Original treatment
89//! - Feynman & Hibbs: "Quantum Mechanics and Path Integrals" - Spin-statistics connection
90//! - Penrose: "The Road to Reality" § 11.3 - Geometric interpretation
91//! - Aharonov & Susskind: "Observability of the sign change of spinors" (1967)
92
93use std::ops::{Mul, MulAssign};
94
95/// Unit quaternion representing an element of SU(2)
96///
97/// Quaternion q = w + xi + yj + zk where w² + x² + y² + z² = 1
98///
99/// # Geometric Interpretation
100///
101/// Every unit quaternion represents a rotation in 3D space:
102/// - Rotation by angle θ around axis n̂ = (nx, ny, nz):
103/// q = cos(θ/2) + sin(θ/2)(nx·i + ny·j + nz·k)
104///
105/// # ⚠️ Important: Double Cover Property
106///
107/// **q and -q represent the SAME rotation but DIFFERENT quantum states:**
108/// - Both rotate 3D vectors identically (v → q·v·q⁻¹ = (-q)·v·(-q)⁻¹)
109/// - But q ≠ -q as SU(2) group elements
110/// - Consequence: Fermions change sign under 2π rotation (ψ → -ψ)
111///
112/// This is NOT a numerical issue or degeneracy - it's fundamental topology!
113///
114/// # Relation to SU(2) Matrix
115///
116/// q = w + xi + yj + zk corresponds to SU(2) matrix:
117/// ```text
118/// U = [[ w + ix, -y + iz],
119/// [ y + iz, w - ix]]
120/// ```
121///
122/// Note: -q gives matrix -U, which acts identically on vectors but represents
123/// a different element of SU(2) (relevant for spinors/fermions).
124#[derive(Debug, Clone, Copy, PartialEq)]
125pub struct UnitQuaternion {
126 // Private fields enforce unit norm invariant through constructors
127 w: f64,
128 x: f64,
129 y: f64,
130 z: f64,
131}
132
133impl UnitQuaternion {
134 /// Get the real (scalar) component.
135 #[inline]
136 #[must_use]
137 pub fn w(&self) -> f64 {
138 self.w
139 }
140
141 /// Get the imaginary i component.
142 #[inline]
143 #[must_use]
144 pub fn x(&self) -> f64 {
145 self.x
146 }
147
148 /// Get the imaginary j component.
149 #[inline]
150 #[must_use]
151 pub fn y(&self) -> f64 {
152 self.y
153 }
154
155 /// Get the imaginary k component.
156 #[inline]
157 #[must_use]
158 pub fn z(&self) -> f64 {
159 self.z
160 }
161
162 /// Get all components as a tuple (w, x, y, z).
163 #[inline]
164 #[must_use]
165 pub fn components(&self) -> (f64, f64, f64, f64) {
166 (self.w, self.x, self.y, self.z)
167 }
168
169 /// Get all components as an array [w, x, y, z].
170 #[inline]
171 #[must_use]
172 pub fn to_array(&self) -> [f64; 4] {
173 [self.w, self.x, self.y, self.z]
174 }
175}
176
177// ============================================================================
178// Numerical Tolerance Constants
179// ============================================================================
180//
181// These thresholds are chosen based on IEEE 754 f64 precision (ε ≈ 2.2e-16)
182// and practical considerations for accumulated floating-point error.
183//
184// Rationale: 1e-10 = ~10^6 × machine_epsilon
185// This allows for approximately 6 orders of magnitude of accumulated error
186// from typical operations (multiplication, addition, normalization) before
187// triggering degeneracy handling.
188//
189// For quaternion operations on unit quaternions, typical error accumulation:
190// - Single operation: O(ε) ≈ 2e-16
191// - After ~1000 operations: O(1000ε) ≈ 2e-13
192// - Threshold 1e-10 provides ~1000× safety margin
193//
194// Reference: Higham, "Accuracy and Stability of Numerical Algorithms" (2002)
195
196/// Threshold below which a norm is considered zero (degenerate case).
197/// Used for normalization guards and axis-angle singularity detection.
198const NORM_EPSILON: f64 = 1e-10;
199
200/// Threshold for detecting near-identity quaternions.
201/// Used in logarithm to avoid division by near-zero sin(θ/2).
202const IDENTITY_EPSILON: f64 = 1e-10;
203
204/// Threshold for detecting rotation angle near π (axis ambiguity).
205/// At θ = π, the rotation axis becomes undefined (any axis works).
206/// Reserved for future implementation of θ≈π handling.
207#[allow(dead_code)]
208const SINGULARITY_EPSILON: f64 = 1e-6;
209
210impl UnitQuaternion {
211 /// Identity element: q = 1
212 #[must_use]
213 pub fn identity() -> Self {
214 Self {
215 w: 1.0,
216 x: 0.0,
217 y: 0.0,
218 z: 0.0,
219 }
220 }
221
222 /// Create from components (automatically normalizes to unit quaternion)
223 ///
224 /// Returns identity if input norm < `NORM_EPSILON` (degenerate case).
225 #[must_use]
226 pub fn new(w: f64, x: f64, y: f64, z: f64) -> Self {
227 let norm = (w * w + x * x + y * y + z * z).sqrt();
228 if norm < NORM_EPSILON {
229 return Self::identity();
230 }
231 Self {
232 w: w / norm,
233 x: x / norm,
234 y: y / norm,
235 z: z / norm,
236 }
237 }
238
239 /// Create from axis-angle representation
240 ///
241 /// # Arguments
242 /// * `axis` - Rotation axis (nx, ny, nz), will be normalized
243 /// * `angle` - Rotation angle in radians
244 ///
245 /// # Returns
246 /// Unit quaternion q = cos(θ/2) + sin(θ/2)(nx·i + ny·j + nz·k)
247 #[must_use]
248 pub fn from_axis_angle(axis: [f64; 3], angle: f64) -> Self {
249 let [nx, ny, nz] = axis;
250 let axis_norm = (nx * nx + ny * ny + nz * nz).sqrt();
251
252 // Degenerate axis: return identity (no rotation)
253 if axis_norm < NORM_EPSILON {
254 return Self::identity();
255 }
256
257 let half_angle = angle / 2.0;
258 let sin_half = half_angle.sin();
259 let cos_half = half_angle.cos();
260
261 Self {
262 w: cos_half,
263 x: sin_half * nx / axis_norm,
264 y: sin_half * ny / axis_norm,
265 z: sin_half * nz / axis_norm,
266 }
267 }
268
269 /// Extract axis and angle from quaternion
270 ///
271 /// # Returns
272 /// (axis, angle) where axis is normalized and angle ∈ [0, 2π]
273 #[must_use]
274 pub fn to_axis_angle(&self) -> ([f64; 3], f64) {
275 // q = cos(θ/2) + sin(θ/2)·n̂
276 let angle = 2.0 * self.w.acos();
277 let sin_half = (1.0 - self.w * self.w).sqrt();
278
279 if sin_half < IDENTITY_EPSILON {
280 // Near identity, axis is arbitrary
281 return ([1.0, 0.0, 0.0], 0.0);
282 }
283
284 let axis = [self.x / sin_half, self.y / sin_half, self.z / sin_half];
285
286 (axis, angle)
287 }
288
289 /// Rotation around X-axis by angle θ
290 #[must_use]
291 pub fn rotation_x(theta: f64) -> Self {
292 Self::from_axis_angle([1.0, 0.0, 0.0], theta)
293 }
294
295 /// Rotation around Y-axis by angle θ
296 #[must_use]
297 pub fn rotation_y(theta: f64) -> Self {
298 Self::from_axis_angle([0.0, 1.0, 0.0], theta)
299 }
300
301 /// Rotation around Z-axis by angle θ
302 #[must_use]
303 pub fn rotation_z(theta: f64) -> Self {
304 Self::from_axis_angle([0.0, 0.0, 1.0], theta)
305 }
306
307 /// Quaternion conjugate: q* = w - xi - yj - zk
308 ///
309 /// For unit quaternions: q* = q⁻¹ (inverse)
310 #[must_use]
311 pub fn conjugate(&self) -> Self {
312 Self {
313 w: self.w,
314 x: -self.x,
315 y: -self.y,
316 z: -self.z,
317 }
318 }
319
320 /// Quaternion inverse: q⁻¹
321 ///
322 /// For unit quaternions: q⁻¹ = q*
323 #[must_use]
324 pub fn inverse(&self) -> Self {
325 self.conjugate()
326 }
327
328 /// Norm squared: |q|² = w² + x² + y² + z²
329 ///
330 /// Should always be 1 for unit quaternions
331 #[must_use]
332 pub fn norm_squared(&self) -> f64 {
333 self.w * self.w + self.x * self.x + self.y * self.y + self.z * self.z
334 }
335
336 /// Norm: |q| = sqrt(w² + x² + y² + z²)
337 ///
338 /// Should always be 1 for unit quaternions
339 #[must_use]
340 pub fn norm(&self) -> f64 {
341 self.norm_squared().sqrt()
342 }
343
344 /// Renormalize to unit quaternion (fix numerical drift)
345 #[must_use]
346 pub fn normalize(&self) -> Self {
347 Self::new(self.w, self.x, self.y, self.z)
348 }
349
350 /// Distance to identity (geodesic distance on S³)
351 ///
352 /// d(q, 1) = 2·arccos(|w|)
353 ///
354 /// This is the rotation angle in [0, π]
355 #[must_use]
356 pub fn distance_to_identity(&self) -> f64 {
357 2.0 * self.w.abs().clamp(-1.0, 1.0).acos()
358 }
359
360 /// Geodesic distance to another quaternion
361 ///
362 /// d(q₁, q₂) = arccos(|q₁·q₂|) where · is quaternion dot product
363 #[must_use]
364 pub fn distance_to(&self, other: &Self) -> f64 {
365 let dot = self.w * other.w + self.x * other.x + self.y * other.y + self.z * other.z;
366 dot.abs().clamp(-1.0, 1.0).acos()
367 }
368
369 /// Spherical linear interpolation (SLERP)
370 ///
371 /// Interpolate between self and other with parameter t ∈ `[0,1]`
372 /// Returns shortest path on S³
373 #[must_use]
374 pub fn slerp(&self, other: &Self, t: f64) -> Self {
375 let dot = self.w * other.w + self.x * other.x + self.y * other.y + self.z * other.z;
376
377 // If quaternions are close, use linear interpolation
378 if dot.abs() > 0.9995 {
379 return Self::new(
380 self.w + t * (other.w - self.w),
381 self.x + t * (other.x - self.x),
382 self.y + t * (other.y - self.y),
383 self.z + t * (other.z - self.z),
384 );
385 }
386
387 // Ensure shortest path
388 let (other_w, other_x, other_y, other_z) = if dot < 0.0 {
389 (-other.w, -other.x, -other.y, -other.z)
390 } else {
391 (other.w, other.x, other.y, other.z)
392 };
393
394 let theta = dot.abs().acos();
395 let sin_theta = theta.sin();
396
397 let a = ((1.0 - t) * theta).sin() / sin_theta;
398 let b = (t * theta).sin() / sin_theta;
399
400 Self::new(
401 a * self.w + b * other_w,
402 a * self.x + b * other_x,
403 a * self.y + b * other_y,
404 a * self.z + b * other_z,
405 )
406 }
407
408 /// Exponential map from Lie algebra 𝔰𝔲(2) to group SU(2)
409 ///
410 /// Given a vector v = (v₁, v₂, v₃) ∈ ℝ³ (Lie algebra element),
411 /// compute exp(v) as a unit quaternion.
412 ///
413 /// Formula: exp(v) = cos(|v|/2) + sin(|v|/2)·(v/|v|)
414 #[must_use]
415 pub fn exp(v: [f64; 3]) -> Self {
416 let norm = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
417
418 if norm < NORM_EPSILON {
419 return Self::identity();
420 }
421
422 let half_norm = norm / 2.0;
423 let cos_half = half_norm.cos();
424 let sin_half = half_norm.sin();
425
426 Self {
427 w: cos_half,
428 x: sin_half * v[0] / norm,
429 y: sin_half * v[1] / norm,
430 z: sin_half * v[2] / norm,
431 }
432 }
433
434 /// Logarithm map from group SU(2) to Lie algebra 𝔰𝔲(2)
435 ///
436 /// Returns vector v ∈ ℝ³ such that exp(v) = q
437 ///
438 /// Formula: log(q) = (θ/sin(θ/2))·(x, y, z) where θ = 2·arccos(w)
439 #[must_use]
440 pub fn log(&self) -> [f64; 3] {
441 let theta = 2.0 * self.w.acos();
442 let sin_half = (1.0 - self.w * self.w).sqrt();
443
444 if sin_half < IDENTITY_EPSILON {
445 // Near identity
446 return [0.0, 0.0, 0.0];
447 }
448
449 let scale = theta / sin_half;
450 [scale * self.x, scale * self.y, scale * self.z]
451 }
452
453 /// Convert to SU(2) matrix representation (for compatibility)
454 ///
455 /// Returns 2×2 complex matrix:
456 /// U = [[ w + ix, -y + iz],
457 /// [ y + iz, w - ix]]
458 #[must_use]
459 pub fn to_matrix(&self) -> [[num_complex::Complex64; 2]; 2] {
460 use num_complex::Complex64;
461
462 [
463 [
464 Complex64::new(self.w, self.x),
465 Complex64::new(-self.y, self.z),
466 ],
467 [
468 Complex64::new(self.y, self.z),
469 Complex64::new(self.w, -self.x),
470 ],
471 ]
472 }
473
474 /// Create from SU(2) matrix
475 ///
476 /// Expects matrix [[α, -β*], [β, α*]] with |α|² + |β|² = 1
477 #[must_use]
478 pub fn from_matrix(matrix: [[num_complex::Complex64; 2]; 2]) -> Self {
479 // Extract α = a + ib from matrix[0][0]
480 let a = matrix[0][0].re;
481 let b = matrix[0][0].im;
482
483 // Extract β = c + id from matrix[1][0]
484 let c = matrix[1][0].re;
485 let d = matrix[1][0].im;
486
487 // q = a + bi + cj + dk
488 Self::new(a, b, c, d)
489 }
490
491 /// Act on a 3D vector by conjugation: v' = qvq*
492 ///
493 /// This is the rotation action of SU(2) on ℝ³
494 /// Identifies v = (x,y,z) with quaternion xi + yj + zk
495 ///
496 /// Uses the direct formula (Rodrigues) for efficiency:
497 /// v' = v + 2w(n×v) + 2(n×(n×v)) where n = (x,y,z)
498 #[must_use]
499 pub fn rotate_vector(&self, v: [f64; 3]) -> [f64; 3] {
500 let (w, qx, qy, qz) = (self.w, self.x, self.y, self.z);
501 let (vx, vy, vz) = (v[0], v[1], v[2]);
502
503 // Cross product: n × v
504 let cx = qy * vz - qz * vy;
505 let cy = qz * vx - qx * vz;
506 let cz = qx * vy - qy * vx;
507
508 // Cross product: n × (n × v)
509 let ccx = qy * cz - qz * cy;
510 let ccy = qz * cx - qx * cz;
511 let ccz = qx * cy - qy * cx;
512
513 // v' = v + 2w(n×v) + 2(n×(n×v))
514 [
515 vx + 2.0 * (w * cx + ccx),
516 vy + 2.0 * (w * cy + ccy),
517 vz + 2.0 * (w * cz + ccz),
518 ]
519 }
520
521 /// Verify this is approximately a unit quaternion
522 #[must_use]
523 pub fn verify_unit(&self, tolerance: f64) -> bool {
524 (self.norm_squared() - 1.0).abs() < tolerance
525 }
526
527 /// Construct the 3×3 rotation matrix corresponding to this unit quaternion.
528 ///
529 /// Returns the SO(3) matrix R such that R·v = `rotate_vector(v)` for all v ∈ ℝ³.
530 ///
531 /// # Lean Correspondence
532 ///
533 /// This is the Rust counterpart of `toRotationMatrix` in
534 /// `OrthogonalGroups.lean`. The Lean proofs establish:
535 /// - `toRotationMatrix_orthogonal_axiom`: R(q)ᵀ·R(q) = I
536 /// - `toRotationMatrix_det_axiom`: det(R(q)) = 1
537 /// - `toRotationMatrix_mul_axiom`: R(q₁·q₂) = R(q₁)·R(q₂)
538 /// - `toRotationMatrix_neg`: R(-q) = R(q)
539 #[must_use]
540 pub fn to_rotation_matrix(&self) -> [[f64; 3]; 3] {
541 let (w, x, y, z) = (self.w, self.x, self.y, self.z);
542 [
543 [
544 1.0 - 2.0 * (y * y + z * z),
545 2.0 * (x * y - w * z),
546 2.0 * (x * z + w * y),
547 ],
548 [
549 2.0 * (x * y + w * z),
550 1.0 - 2.0 * (x * x + z * z),
551 2.0 * (y * z - w * x),
552 ],
553 [
554 2.0 * (x * z - w * y),
555 2.0 * (y * z + w * x),
556 1.0 - 2.0 * (x * x + y * y),
557 ],
558 ]
559 }
560}
561
562// Quaternion multiplication: Hamilton product
563impl Mul for UnitQuaternion {
564 type Output = Self;
565
566 fn mul(self, rhs: Self) -> Self {
567 // (w₁ + x₁i + y₁j + z₁k)(w₂ + x₂i + y₂j + z₂k)
568 Self::new(
569 self.w * rhs.w - self.x * rhs.x - self.y * rhs.y - self.z * rhs.z,
570 self.w * rhs.x + self.x * rhs.w + self.y * rhs.z - self.z * rhs.y,
571 self.w * rhs.y - self.x * rhs.z + self.y * rhs.w + self.z * rhs.x,
572 self.w * rhs.z + self.x * rhs.y - self.y * rhs.x + self.z * rhs.w,
573 )
574 }
575}
576
577impl MulAssign for UnitQuaternion {
578 fn mul_assign(&mut self, rhs: Self) {
579 *self = *self * rhs;
580 }
581}
582
583#[cfg(test)]
584mod tests {
585 use super::*;
586 use std::f64::consts::PI;
587
588 #[test]
589 fn test_identity() {
590 let q = UnitQuaternion::identity();
591 assert_eq!(q.w, 1.0);
592 assert_eq!(q.x, 0.0);
593 assert_eq!(q.y, 0.0);
594 assert_eq!(q.z, 0.0);
595 assert!(q.verify_unit(1e-10));
596 }
597
598 #[test]
599 fn test_normalization() {
600 let q = UnitQuaternion::new(1.0, 2.0, 3.0, 4.0);
601 assert!((q.norm() - 1.0).abs() < 1e-10);
602 assert!(q.verify_unit(1e-10));
603 }
604
605 #[test]
606 fn test_axis_angle_roundtrip() {
607 let axis = [1.0, 2.0, 3.0];
608 let angle = PI / 3.0;
609
610 let q = UnitQuaternion::from_axis_angle(axis, angle);
611 let (axis_out, angle_out) = q.to_axis_angle();
612
613 // Normalize input axis for comparison
614 let axis_norm = (axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]).sqrt();
615 let axis_normalized = [
616 axis[0] / axis_norm,
617 axis[1] / axis_norm,
618 axis[2] / axis_norm,
619 ];
620
621 assert!((angle - angle_out).abs() < 1e-10);
622 for i in 0..3 {
623 assert!((axis_normalized[i] - axis_out[i]).abs() < 1e-10);
624 }
625
626 println!(
627 "✓ Axis-angle roundtrip: axis=[{:.3},{:.3},{:.3}], angle={:.3}",
628 axis_out[0], axis_out[1], axis_out[2], angle_out
629 );
630 }
631
632 #[test]
633 fn test_rotation_x() {
634 let q = UnitQuaternion::rotation_x(PI / 2.0);
635
636 // Should rotate (0,1,0) to (0,0,1)
637 let v = [0.0, 1.0, 0.0];
638 let rotated = q.rotate_vector(v);
639
640 assert!(rotated[0].abs() < 1e-10);
641 assert!(rotated[1].abs() < 1e-10);
642 assert!((rotated[2] - 1.0).abs() < 1e-10);
643
644 println!(
645 "✓ Rotation X by π/2: (0,1,0) → ({:.3},{:.3},{:.3})",
646 rotated[0], rotated[1], rotated[2]
647 );
648 }
649
650 #[test]
651 fn test_quaternion_multiplication() {
652 let q1 = UnitQuaternion::rotation_x(PI / 4.0);
653 let q2 = UnitQuaternion::rotation_y(PI / 4.0);
654
655 let q3 = q1 * q2;
656
657 // Should still be unit quaternion
658 assert!(q3.verify_unit(1e-10));
659
660 // Composition of rotations
661 let v = [1.0, 0.0, 0.0];
662 let rotated_composed = q3.rotate_vector(v);
663 let rotated_separate = q1.rotate_vector(q2.rotate_vector(v));
664
665 for i in 0..3 {
666 assert!((rotated_composed[i] - rotated_separate[i]).abs() < 1e-10);
667 }
668
669 println!("✓ Quaternion multiplication preserves unitarity and composition");
670 }
671
672 #[test]
673 fn test_inverse() {
674 let q = UnitQuaternion::rotation_z(PI / 3.0);
675 let q_inv = q.inverse();
676
677 let product = q * q_inv;
678
679 assert!((product.w - 1.0).abs() < 1e-10);
680 assert!(product.x.abs() < 1e-10);
681 assert!(product.y.abs() < 1e-10);
682 assert!(product.z.abs() < 1e-10);
683
684 println!("✓ Inverse: q · q⁻¹ = identity");
685 }
686
687 #[test]
688 fn test_distance_to_identity() {
689 let q_identity = UnitQuaternion::identity();
690 assert!(q_identity.distance_to_identity() < 1e-10);
691
692 let q_pi = UnitQuaternion::rotation_x(PI);
693 assert!((q_pi.distance_to_identity() - PI).abs() < 1e-10);
694
695 let q_half_pi = UnitQuaternion::rotation_y(PI / 2.0);
696 assert!((q_half_pi.distance_to_identity() - PI / 2.0).abs() < 1e-10);
697
698 println!(
699 "✓ Distance to identity: d(I)=0, d(π)={:.3}, d(π/2)={:.3}",
700 q_pi.distance_to_identity(),
701 q_half_pi.distance_to_identity()
702 );
703 }
704
705 #[test]
706 fn test_exp_log_roundtrip() {
707 let v = [0.5, 0.3, 0.2];
708
709 let q = UnitQuaternion::exp(v);
710 let v_out = q.log();
711
712 for i in 0..3 {
713 assert!((v[i] - v_out[i]).abs() < 1e-10);
714 }
715
716 println!(
717 "✓ Exp-log roundtrip: v=[{:.3},{:.3},{:.3}]",
718 v[0], v[1], v[2]
719 );
720 }
721
722 #[test]
723 fn test_slerp() {
724 let q1 = UnitQuaternion::rotation_x(0.0);
725 let q2 = UnitQuaternion::rotation_x(PI / 2.0);
726
727 let q_mid = q1.slerp(&q2, 0.5);
728
729 // Should be rotation by π/4
730 assert!((q_mid.distance_to_identity() - PI / 4.0).abs() < 1e-10);
731
732 // At t=0, should be q1
733 let q_0 = q1.slerp(&q2, 0.0);
734 assert!(q_0.distance_to(&q1) < 1e-10);
735
736 // At t=1, should be q2
737 let q_1 = q1.slerp(&q2, 1.0);
738 assert!(q_1.distance_to(&q2) < 1e-10);
739
740 println!("✓ SLERP: smooth interpolation on S³");
741 }
742
743 #[test]
744 fn test_matrix_conversion() {
745 let q = UnitQuaternion::rotation_z(PI / 3.0);
746
747 let matrix = q.to_matrix();
748 let q_back = UnitQuaternion::from_matrix(matrix);
749
750 assert!((q.w - q_back.w).abs() < 1e-10);
751 assert!((q.x - q_back.x).abs() < 1e-10);
752 assert!((q.y - q_back.y).abs() < 1e-10);
753 assert!((q.z - q_back.z).abs() < 1e-10);
754
755 println!("✓ Matrix conversion roundtrip preserves quaternion");
756 }
757}