Skip to main content

loam_math/
bivector.rs

1//! `R = exp(B/2)`, so a bivector of magnitude θ in plane `e_i ∧ e_j` rotates
2//! by θ from `e_i` toward `e_j`. Rotor application is the sandwich
3//! `v' = R̃ · v · R`.
4
5use std::ops::{Add, Mul};
6
7use glam::{Vec2, Vec3, Vec4};
8
9pub trait Bivector: Copy + Add<Output = Self> + Mul<f32, Output = Self> {
10    type Rotor: Rotor<Bivector = Self>;
11
12    fn zero() -> Self;
13
14    fn exp(self) -> Self::Rotor;
15}
16
17/// Rotor operations require unit elements of Spin(N); public fields do not enforce this.
18pub trait Rotor: Copy + Mul<Output = Self> {
19    type Bivector: Bivector<Rotor = Self>;
20
21    type Vector: Copy;
22
23    fn identity() -> Self;
24
25    fn inverse(self) -> Self;
26
27    fn apply(&self, v: Self::Vector) -> Self::Vector;
28
29    /// Inverse of [`Bivector::exp`]; [`Rotor4`] takes the shorter rotation, so its result may generate `−self`.
30    fn log(self) -> Self::Bivector;
31}
32
33#[derive(Copy, Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
34pub struct Bivector2(pub f32);
35
36impl Add for Bivector2 {
37    type Output = Self;
38    fn add(self, rhs: Self) -> Self {
39        Self(self.0 + rhs.0)
40    }
41}
42
43impl Mul<f32> for Bivector2 {
44    type Output = Self;
45    fn mul(self, k: f32) -> Self {
46        Self(self.0 * k)
47    }
48}
49
50impl Bivector for Bivector2 {
51    type Rotor = Rotor2;
52
53    fn zero() -> Self {
54        Self(0.0)
55    }
56
57    fn exp(self) -> Rotor2 {
58        let half = self.0 * 0.5;
59        Rotor2 {
60            a: half.cos(),
61            b: half.sin(),
62        }
63    }
64}
65
66/// Unit complex number `a + b·e1e2` with `a = cos(θ/2)`, `b = sin(θ/2)`.
67#[derive(Copy, Clone, Debug, PartialEq)]
68pub struct Rotor2 {
69    pub a: f32,
70    pub b: f32,
71}
72
73impl Rotor2 {
74    pub const IDENTITY: Self = Self { a: 1.0, b: 0.0 };
75}
76
77impl Default for Rotor2 {
78    fn default() -> Self {
79        Self::IDENTITY
80    }
81}
82
83impl Mul for Rotor2 {
84    type Output = Self;
85    fn mul(self, rhs: Self) -> Self {
86        Self {
87            a: self.a * rhs.a - self.b * rhs.b,
88            b: self.a * rhs.b + self.b * rhs.a,
89        }
90    }
91}
92
93impl Rotor for Rotor2 {
94    type Bivector = Bivector2;
95    type Vector = Vec2;
96
97    fn identity() -> Self {
98        Self::IDENTITY
99    }
100
101    fn inverse(self) -> Self {
102        Self {
103            a: self.a,
104            b: -self.b,
105        }
106    }
107
108    fn apply(&self, v: Vec2) -> Vec2 {
109        let c = self.a * self.a - self.b * self.b;
110        let s = 2.0 * self.a * self.b;
111        Vec2::new(c * v.x - s * v.y, s * v.x + c * v.y)
112    }
113
114    fn log(self) -> Bivector2 {
115        Bivector2(2.0 * self.b.atan2(self.a))
116    }
117}
118
119#[derive(Copy, Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
120pub struct Bivector3 {
121    pub xy: f32,
122    pub yz: f32,
123    pub zx: f32,
124}
125
126impl Bivector3 {
127    pub const ZERO: Self = Self {
128        xy: 0.0,
129        yz: 0.0,
130        zx: 0.0,
131    };
132
133    pub fn new(xy: f32, yz: f32, zx: f32) -> Self {
134        Self { xy, yz, zx }
135    }
136
137    pub fn magnitude(self) -> f32 {
138        (self.xy * self.xy + self.yz * self.yz + self.zx * self.zx).sqrt()
139    }
140}
141
142impl Add for Bivector3 {
143    type Output = Self;
144    fn add(self, rhs: Self) -> Self {
145        Self {
146            xy: self.xy + rhs.xy,
147            yz: self.yz + rhs.yz,
148            zx: self.zx + rhs.zx,
149        }
150    }
151}
152
153impl Mul<f32> for Bivector3 {
154    type Output = Self;
155    fn mul(self, k: f32) -> Self {
156        Self {
157            xy: self.xy * k,
158            yz: self.yz * k,
159            zx: self.zx * k,
160        }
161    }
162}
163
164impl Bivector for Bivector3 {
165    type Rotor = Rotor3;
166
167    fn zero() -> Self {
168        Self::ZERO
169    }
170
171    fn exp(self) -> Rotor3 {
172        let mag_sq = self.xy * self.xy + self.yz * self.yz + self.zx * self.zx;
173
174        if mag_sq < 1e-16 {
175            return Rotor3 {
176                s: 1.0,
177                xy: self.xy * 0.5,
178                yz: self.yz * 0.5,
179                zx: self.zx * 0.5,
180            };
181        }
182        let mag = mag_sq.sqrt();
183        let half = mag * 0.5;
184        let c = half.cos();
185        let k = half.sin() / mag;
186        Rotor3 {
187            s: c,
188            xy: self.xy * k,
189            yz: self.yz * k,
190            zx: self.zx * k,
191        }
192    }
193}
194
195#[derive(Copy, Clone, Debug, PartialEq)]
196pub struct Rotor3 {
197    pub s: f32,
198    pub xy: f32,
199    pub yz: f32,
200    pub zx: f32,
201}
202
203impl Rotor3 {
204    pub const IDENTITY: Self = Self {
205        s: 1.0,
206        xy: 0.0,
207        yz: 0.0,
208        zx: 0.0,
209    };
210}
211
212impl Default for Rotor3 {
213    fn default() -> Self {
214        Self::IDENTITY
215    }
216}
217
218impl Mul for Rotor3 {
219    type Output = Self;
220    fn mul(self, rhs: Self) -> Self {
221        let (s1, a1, b1, c1) = (self.s, self.xy, self.yz, self.zx);
222        let (s2, a2, b2, c2) = (rhs.s, rhs.xy, rhs.yz, rhs.zx);
223        Self {
224            s: s1 * s2 - a1 * a2 - b1 * b2 - c1 * c2,
225            xy: s1 * a2 + s2 * a1 - b1 * c2 + c1 * b2,
226            yz: s1 * b2 + s2 * b1 + a1 * c2 - c1 * a2,
227            zx: s1 * c2 + s2 * c1 - a1 * b2 + b1 * a2,
228        }
229    }
230}
231
232impl Rotor for Rotor3 {
233    type Bivector = Bivector3;
234    type Vector = Vec3;
235
236    fn identity() -> Self {
237        Self::IDENTITY
238    }
239
240    fn inverse(self) -> Self {
241        Self {
242            s: self.s,
243            xy: -self.xy,
244            yz: -self.yz,
245            zx: -self.zx,
246        }
247    }
248
249    fn apply(&self, v: Vec3) -> Vec3 {
250        let (s, a, b, c) = (self.s, self.xy, self.yz, self.zx);
251        let (vx, vy, vz) = (v.x, v.y, v.z);
252
253        let p1 = s * vx - a * vy + c * vz;
254        let p2 = s * vy + a * vx - b * vz;
255        let p3 = s * vz + b * vy - c * vx;
256        let pt = -(a * vz + b * vx + c * vy);
257
258        Vec3::new(
259            p1 * s - p2 * a + p3 * c - pt * b,
260            p2 * s + p1 * a - p3 * b - pt * c,
261            p3 * s + p2 * b - p1 * c - pt * a,
262        )
263    }
264
265    fn log(self) -> Bivector3 {
266        let mag_sq = self.xy * self.xy + self.yz * self.yz + self.zx * self.zx;
267        if mag_sq < 1e-16 {
268            return Bivector3 {
269                xy: self.xy * 2.0,
270                yz: self.yz * 2.0,
271                zx: self.zx * 2.0,
272            };
273        }
274        let mag = mag_sq.sqrt();
275        let theta = 2.0 * mag.atan2(self.s);
276        let k = theta / mag;
277        Bivector3 {
278            xy: self.xy * k,
279            yz: self.yz * k,
280            zx: self.zx * k,
281        }
282    }
283}
284
285#[derive(Copy, Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
286pub struct Bivector4 {
287    pub xy: f32,
288    pub xz: f32,
289    pub xw: f32,
290    pub yz: f32,
291    pub yw: f32,
292    pub zw: f32,
293}
294
295impl Bivector4 {
296    pub const ZERO: Self = Self {
297        xy: 0.0,
298        xz: 0.0,
299        xw: 0.0,
300        yz: 0.0,
301        yw: 0.0,
302        zw: 0.0,
303    };
304
305    pub fn new(xy: f32, xz: f32, xw: f32, yz: f32, yw: f32, zw: f32) -> Self {
306        Self {
307            xy,
308            xz,
309            xw,
310            yz,
311            yw,
312            zw,
313        }
314    }
315
316    /// Panics if `i >= 6`; [`Plane4::unit_bivector`] is the typed form.
317    pub fn basis(i: usize) -> Self {
318        let mut c = [0.0_f32; 6];
319        c[i] = 1.0;
320        Self::new(c[0], c[1], c[2], c[3], c[4], c[5])
321    }
322
323    pub fn magnitude_squared(self) -> f32 {
324        self.xy * self.xy
325            + self.xz * self.xz
326            + self.xw * self.xw
327            + self.yz * self.yz
328            + self.yw * self.yw
329            + self.zw * self.zw
330    }
331
332    /// `sqrt(θ₁² + θ₂²)` over the invariant planes; the angle only when simple.
333    pub fn magnitude(self) -> f32 {
334        self.magnitude_squared().sqrt()
335    }
336
337    pub fn component(self, plane: Plane4) -> f32 {
338        match plane {
339            Plane4::Xy => self.xy,
340            Plane4::Xz => self.xz,
341            Plane4::Xw => self.xw,
342            Plane4::Yz => self.yz,
343            Plane4::Yw => self.yw,
344            Plane4::Zw => self.zw,
345        }
346    }
347
348    pub fn set_component(&mut self, plane: Plane4, value: f32) {
349        match plane {
350            Plane4::Xy => self.xy = value,
351            Plane4::Xz => self.xz = value,
352            Plane4::Xw => self.xw = value,
353            Plane4::Yz => self.yz = value,
354            Plane4::Yw => self.yw = value,
355            Plane4::Zw => self.zw = value,
356        }
357    }
358
359    /// Euclidean product of the six coefficients, not the Clifford scalar part.
360    pub fn dot(self, other: Self) -> f32 {
361        self.xy * other.xy
362            + self.xz * other.xz
363            + self.xw * other.xw
364            + self.yz * other.yz
365            + self.yw * other.yw
366            + self.zw * other.zw
367    }
368
369    /// Pseudoscalar coefficient of `B ∧ B`; zero iff `B` is simple.
370    pub fn wedge_self_coeff(self) -> f32 {
371        2.0 * (self.xy * self.zw - self.xz * self.yw + self.xw * self.yz)
372    }
373
374    pub fn wedge(u: Vec4, v: Vec4) -> Self {
375        Self {
376            xy: u.x * v.y - u.y * v.x,
377            xz: u.x * v.z - u.z * v.x,
378            xw: u.x * v.w - u.w * v.x,
379            yz: u.y * v.z - u.z * v.y,
380            yw: u.y * v.w - u.w * v.y,
381            zw: u.z * v.w - u.w * v.z,
382        }
383    }
384
385    /// Left contraction `B ⌋ v`, the grade-1 part of `B·v`: `e_xy ⌋ e_x = −e_y`.
386    pub fn contract_vec(self, v: Vec4) -> Vec4 {
387        Vec4::new(
388            self.xy * v.y + self.xz * v.z + self.xw * v.w,
389            -self.xy * v.x + self.yz * v.z + self.yw * v.w,
390            -self.xz * v.x - self.yz * v.y + self.zw * v.w,
391            -self.xw * v.x - self.yw * v.y - self.zw * v.z,
392        )
393    }
394
395    pub fn dual(self) -> Self {
396        Self {
397            xy: -self.zw,
398            xz: self.yw,
399            xw: -self.yz,
400            yz: -self.xw,
401            yw: self.xz,
402            zw: -self.xy,
403        }
404    }
405}
406
407#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
408#[repr(usize)]
409pub enum Plane4 {
410    Xy = 0,
411    Xz = 1,
412    Xw = 2,
413    Yz = 3,
414    Yw = 4,
415    Zw = 5,
416}
417
418impl Plane4 {
419    pub const ALL: [Self; 6] = [Self::Xy, Self::Xz, Self::Xw, Self::Yz, Self::Yw, Self::Zw];
420
421    /// Stable serialization key.
422    pub fn label(self) -> &'static str {
423        match self {
424            Self::Xy => "xy",
425            Self::Xz => "xz",
426            Self::Xw => "xw",
427            Self::Yz => "yz",
428            Self::Yw => "yw",
429            Self::Zw => "zw",
430        }
431    }
432
433    pub fn unit_bivector(self) -> Bivector4 {
434        Bivector4::basis(self as usize)
435    }
436}
437
438impl Add for Bivector4 {
439    type Output = Self;
440    fn add(self, rhs: Self) -> Self {
441        Self {
442            xy: self.xy + rhs.xy,
443            xz: self.xz + rhs.xz,
444            xw: self.xw + rhs.xw,
445            yz: self.yz + rhs.yz,
446            yw: self.yw + rhs.yw,
447            zw: self.zw + rhs.zw,
448        }
449    }
450}
451
452impl Mul<f32> for Bivector4 {
453    type Output = Self;
454    fn mul(self, k: f32) -> Self {
455        Self {
456            xy: self.xy * k,
457            xz: self.xz * k,
458            xw: self.xw * k,
459            yz: self.yz * k,
460            yw: self.yw * k,
461            zw: self.zw * k,
462        }
463    }
464}
465
466impl Bivector for Bivector4 {
467    type Rotor = Rotor4;
468
469    fn zero() -> Self {
470        Self::ZERO
471    }
472
473    fn exp(self) -> Rotor4 {
474        let s = self.magnitude_squared();
475        if s < 1e-16 {
476            return Rotor4::IDENTITY;
477        }
478        let delta = self.wedge_self_coeff();
479
480        if delta.abs() < 1e-6 * s.max(1.0) {
481            let mag = s.sqrt();
482            let half = mag * 0.5;
483            let c = half.cos();
484            let k = half.sin() / mag;
485            return Rotor4 {
486                s: c,
487                xy: self.xy * k,
488                xz: self.xz * k,
489                xw: self.xw * k,
490                yz: self.yz * k,
491                yw: self.yw * k,
492                zw: self.zw * k,
493                xyzw: 0.0,
494            };
495        }
496
497        let disc_sq = (s * s - delta * delta).max(0.0);
498        let disc = disc_sq.sqrt();
499
500        if disc < 1e-6 * s.max(1.0) {
501            let theta_sq = s * 0.5;
502            let theta = theta_sq.sqrt();
503            let half = theta * 0.5;
504            let ch = half.cos();
505            let sh = half.sin();
506            let sign_i = delta.signum();
507
508            let b_coef = if theta > 1e-8 {
509                (theta.sin()) / (2.0 * theta)
510            } else {
511                0.5
512            };
513            return Rotor4 {
514                s: ch * ch,
515                xy: self.xy * b_coef,
516                xz: self.xz * b_coef,
517                xw: self.xw * b_coef,
518                yz: self.yz * b_coef,
519                yw: self.yw * b_coef,
520                zw: self.zw * b_coef,
521                xyzw: sh * sh * sign_i,
522            };
523        }
524
525        // Press et al., Numerical Recipes, 3rd ed., §5.6.
526        let t1 = ((s + disc) * 0.5).max(0.0).sqrt();
527        let t2 = delta / (2.0 * t1);
528
529        let half1 = t1 * 0.5;
530        let half2 = t2 * 0.5;
531        let c1 = half1.cos();
532        let s1 = half1.sin();
533        let c2 = half2.cos();
534        let s2 = half2.sin();
535
536        let s1_t1 = s1 / t1;
537        let s2_t2 = s2 / t2;
538
539        let b_coef = (s1_t1 * c2 * (s + disc) - c1 * s2_t2 * (s - disc)) / (2.0 * disc);
540        let bstar_coef = delta * (s1_t1 * c2 - c1 * s2_t2) / (2.0 * disc);
541
542        let dual = self.dual();
543        Rotor4 {
544            s: c1 * c2,
545            xy: self.xy * b_coef + dual.xy * bstar_coef,
546            xz: self.xz * b_coef + dual.xz * bstar_coef,
547            xw: self.xw * b_coef + dual.xw * bstar_coef,
548            yz: self.yz * b_coef + dual.yz * bstar_coef,
549            yw: self.yw * b_coef + dual.yw * bstar_coef,
550            zw: self.zw * b_coef + dual.zw * bstar_coef,
551            xyzw: s1 * s2,
552        }
553    }
554}
555
556#[derive(Copy, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
557pub struct Rotor4 {
558    pub s: f32,
559    pub xy: f32,
560    pub xz: f32,
561    pub xw: f32,
562    pub yz: f32,
563    pub yw: f32,
564    pub zw: f32,
565    pub xyzw: f32,
566}
567
568impl Rotor4 {
569    /// Shortest simple rotation between unit vectors; antipodes use the least-aligned coordinate axis.
570    pub fn from_rotation_arc(from: Vec4, to: Vec4) -> Self {
571        let plane = Bivector4::wedge(from, to);
572        let magnitude = plane.magnitude();
573        if magnitude < ARC_PLANE_EPSILON {
574            if from.dot(to) > 0.0 {
575                return Self::IDENTITY;
576            }
577            let axes = [Vec4::X, Vec4::Y, Vec4::Z, Vec4::W];
578            let least = (1..4).fold(0, |best, i| {
579                if from[i].abs() < from[best].abs() {
580                    i
581                } else {
582                    best
583                }
584            });
585            let axis = (axes[least] - from * from[least]).normalize();
586            return (Bivector4::wedge(from, axis) * std::f32::consts::PI)
587                .exp()
588                .normalize();
589        }
590        // Kahan, How Futile are Mindless Assessments of Roundoff?, 2006, Mangled Angles.
591        let angle = 2.0 * (to - from).length().atan2((to + from).length());
592        (plane * (angle / magnitude)).exp().normalize()
593    }
594
595    pub const IDENTITY: Self = Self {
596        s: 1.0,
597        xy: 0.0,
598        xz: 0.0,
599        xw: 0.0,
600        yz: 0.0,
601        yw: 0.0,
602        zw: 0.0,
603        xyzw: 0.0,
604    };
605
606    /// [`Rotor4::IDENTITY`] in the `From<Rotor4> for [f32; 8]` slot order.
607    pub const IDENTITY_SLOT: [f32; 8] = [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
608
609    pub fn norm_squared(self) -> f32 {
610        self.s * self.s
611            + self.xy * self.xy
612            + self.xz * self.xz
613            + self.xw * self.xw
614            + self.yz * self.yz
615            + self.yw * self.yw
616            + self.zw * self.zw
617            + self.xyzw * self.xyzw
618    }
619
620    /// Rescales a nonzero scalar multiple of a rotor; arbitrary even elements need not lie in Spin(4).
621    pub fn normalize(self) -> Self {
622        let n = self.norm_squared().sqrt();
623        if n > 0.0 {
624            let k = 1.0 / n;
625            Self {
626                s: self.s * k,
627                xy: self.xy * k,
628                xz: self.xz * k,
629                xw: self.xw * k,
630                yz: self.yz * k,
631                yw: self.yw * k,
632                zw: self.zw * k,
633                xyzw: self.xyzw * k,
634            }
635        } else {
636            Self::IDENTITY
637        }
638    }
639}
640
641impl Default for Rotor4 {
642    fn default() -> Self {
643        Self::IDENTITY
644    }
645}
646
647// Slot order is the GPU uniform ABI.
648impl From<Rotor4> for [f32; 8] {
649    fn from(r: Rotor4) -> Self {
650        [r.s, r.xy, r.xz, r.xw, r.yz, r.yw, r.zw, r.xyzw]
651    }
652}
653
654impl Rotor4 {
655    // Hestenes, New Foundations for Classical Mechanics, 2nd ed., §2.5.
656    /// Column-major, matching glam's `Mat4` and WGSL's `mat4x4<f32>`.
657    pub fn to_mat4(&self) -> [[f32; 4]; 4] {
658        let c0 = <Self as Rotor>::apply(self, Vec4::new(1.0, 0.0, 0.0, 0.0));
659        let c1 = <Self as Rotor>::apply(self, Vec4::new(0.0, 1.0, 0.0, 0.0));
660        let c2 = <Self as Rotor>::apply(self, Vec4::new(0.0, 0.0, 1.0, 0.0));
661        let c3 = <Self as Rotor>::apply(self, Vec4::new(0.0, 0.0, 0.0, 1.0));
662        [c0.to_array(), c1.to_array(), c2.to_array(), c3.to_array()]
663    }
664
665    /// The rotor whose frame is `columns`, or `None` for a frame no rotor rebuilds within 1e-3, such as a reflection or a skew.
666    pub fn from_mat4(columns: &[[f32; 4]; 4]) -> Option<Self> {
667        let target = columns.map(Vec4::from_array);
668        let axes = [Vec4::X, Vec4::Y, Vec4::Z];
669        let mut rotor = Self::IDENTITY;
670        for (axis, basis) in axes.into_iter().enumerate() {
671            let from = <Self as Rotor>::apply(&rotor, basis);
672            rotor = rotor * arc_holding(from, target[axis], &target[..axis]);
673        }
674        rotor
675            .to_mat4()
676            .iter()
677            .flatten()
678            .zip(columns.iter().flatten())
679            .all(|(built, given)| (built - given).abs() <= ROTATION_TOLERANCE)
680            .then_some(rotor)
681    }
682}
683
684const ARC_PLANE_EPSILON: f32 = 1e-6;
685const ROTATION_TOLERANCE: f32 = 1e-3;
686
687fn arc_holding(from: Vec4, to: Vec4, held: &[Vec4]) -> Rotor4 {
688    if Bivector4::wedge(from, to).magnitude() >= ARC_PLANE_EPSILON || from.dot(to) > 0.0 {
689        return Rotor4::from_rotation_arc(from, to);
690    }
691    let mut free = Vec4::ZERO;
692    for axis in [Vec4::X, Vec4::Y, Vec4::Z, Vec4::W] {
693        let mut residual = axis - from * axis.dot(from);
694        for fixed in held {
695            residual -= *fixed * residual.dot(*fixed);
696        }
697        if residual.length_squared() > free.length_squared() {
698            free = residual;
699        }
700    }
701    (Bivector4::wedge(from, free.normalize()) * std::f32::consts::PI).exp()
702}
703
704impl Mul for Rotor4 {
705    type Output = Self;
706    fn mul(self, rhs: Self) -> Self {
707        let (a0, a12, a13, a14, a23, a24, a34, a_i) = (
708            self.s, self.xy, self.xz, self.xw, self.yz, self.yw, self.zw, self.xyzw,
709        );
710        let (b0, b12, b13, b14, b23, b24, b34, b_i) = (
711            rhs.s, rhs.xy, rhs.xz, rhs.xw, rhs.yz, rhs.yw, rhs.zw, rhs.xyzw,
712        );
713
714        let s = a0 * b0 - a12 * b12 - a13 * b13 - a14 * b14 - a23 * b23 - a24 * b24 - a34 * b34
715            + a_i * b_i;
716
717        let xy = a0 * b12 + a12 * b0 - a13 * b23 + a23 * b13 - a14 * b24 + a24 * b14
718            - a34 * b_i
719            - a_i * b34;
720        let xz = a0 * b13 + a13 * b0 + a12 * b23 - a14 * b34 - a23 * b12
721            + a34 * b14
722            + a24 * b_i
723            + a_i * b24;
724        let xw = a0 * b14 + a14 * b0 + a12 * b24 + a13 * b34
725            - a24 * b12
726            - a34 * b13
727            - a23 * b_i
728            - a_i * b23;
729        let yz = a0 * b23 + a23 * b0 - a12 * b13 + a13 * b12 - a24 * b34 + a34 * b24
730            - a14 * b_i
731            - a_i * b14;
732        let yw = a0 * b24 + a24 * b0 - a12 * b14 + a14 * b12 + a23 * b34 - a34 * b23
733            + a13 * b_i
734            + a_i * b13;
735        let zw = a0 * b34 + a34 * b0 - a13 * b14 + a14 * b13 - a23 * b24 + a24 * b23
736            - a12 * b_i
737            - a_i * b12;
738
739        let xyzw = a0 * b_i + a_i * b0 + a12 * b34 + a34 * b12 - a13 * b24 - a24 * b13
740            + a14 * b23
741            + a23 * b14;
742
743        Self {
744            s,
745            xy,
746            xz,
747            xw,
748            yz,
749            yw,
750            zw,
751            xyzw,
752        }
753    }
754}
755
756impl Rotor for Rotor4 {
757    type Bivector = Bivector4;
758    type Vector = Vec4;
759
760    fn identity() -> Self {
761        Self::IDENTITY
762    }
763
764    fn inverse(self) -> Self {
765        Self {
766            s: self.s,
767            xy: -self.xy,
768            xz: -self.xz,
769            xw: -self.xw,
770            yz: -self.yz,
771            yw: -self.yw,
772            zw: -self.zw,
773            xyzw: self.xyzw,
774        }
775    }
776
777    fn apply(&self, v: Vec4) -> Vec4 {
778        let (vx, vy, vz, vw) = (v.x, v.y, v.z, v.w);
779        let rs = self.s;
780        let rxy = self.xy;
781        let rxz = self.xz;
782        let rxw = self.xw;
783        let ryz = self.yz;
784        let ryw = self.yw;
785        let rzw = self.zw;
786        let r_i = self.xyzw;
787
788        let p1 = rs * vx - rxy * vy - rxz * vz - rxw * vw;
789        let p2 = rs * vy + rxy * vx - ryz * vz - ryw * vw;
790        let p3 = rs * vz + rxz * vx + ryz * vy - rzw * vw;
791        let p4 = rs * vw + rxw * vx + ryw * vy + rzw * vz;
792
793        let t123 = -rxy * vz + rxz * vy - ryz * vx + r_i * vw;
794        let t124 = -rxy * vw + rxw * vy - ryw * vx - r_i * vz;
795        let t134 = -rxz * vw + rxw * vz - rzw * vx + r_i * vy;
796        let t234 = -ryz * vw + ryw * vz - rzw * vy - r_i * vx;
797
798        let q1 = rs * p1 - rxy * p2 - rxz * p3 - rxw * p4 - ryz * t123 - ryw * t124 - rzw * t134
799            + r_i * t234;
800        let q2 = rs * p2 + rxy * p1 - ryz * p3 - ryw * p4 + rxz * t123 + rxw * t124
801            - rzw * t234
802            - r_i * t134;
803        let q3 = rs * p3 + rxz * p1 + ryz * p2 - rzw * p4 - rxy * t123
804            + rxw * t134
805            + ryw * t234
806            + r_i * t124;
807        let q4 = rs * p4 + rxw * p1 + ryw * p2 + rzw * p3
808            - rxy * t124
809            - rxz * t134
810            - ryz * t234
811            - r_i * t123;
812
813        Vec4::new(q1, q2, q3, q4)
814    }
815
816    fn log(self) -> Bivector4 {
817        // Kahan, How Futile are Mindless Assessments of Roundoff?, 2006, Mangled Angles.
818        let branch = if self.s < 0.0 { -1.0 } else { 1.0 };
819        let c = branch * self.s;
820        let p = branch * self.xyzw;
821        let sum_part = Vec3::new(self.xy + self.zw, self.xz - self.yw, self.xw + self.yz) * branch;
822        let diff_part = Vec3::new(self.xy - self.zw, self.xz + self.yw, self.xw - self.yz) * branch;
823        let sin_sum = sum_part.length();
824        let sin_diff = diff_part.length();
825
826        if sum_part == Vec3::ZERO && diff_part == Vec3::ZERO && p.abs() > c {
827            let half_turn = std::f32::consts::PI;
828            return Bivector4::new(half_turn, 0.0, 0.0, 0.0, 0.0, half_turn * p.signum());
829        }
830
831        let k_sum = if sin_sum > 0.0 {
832            sin_sum.atan2(c - p) / sin_sum
833        } else {
834            1.0
835        };
836        let k_diff = if sin_diff > 0.0 {
837            sin_diff.atan2(c + p) / sin_diff
838        } else {
839            1.0
840        };
841
842        Bivector4 {
843            xy: k_sum * sum_part.x + k_diff * diff_part.x,
844            xz: k_sum * sum_part.y + k_diff * diff_part.y,
845            xw: k_sum * sum_part.z + k_diff * diff_part.z,
846            yz: k_sum * sum_part.z - k_diff * diff_part.z,
847            yw: k_diff * diff_part.y - k_sum * sum_part.y,
848            zw: k_sum * sum_part.x - k_diff * diff_part.x,
849        }
850    }
851}
852
853#[cfg(test)]
854mod tests {
855    use super::*;
856    use std::f32::consts::{FRAC_PI_2, PI, SQRT_2, TAU};
857
858    fn assert_close(a: f32, b: f32) {
859        assert!(
860            (a - b).abs() <= 1e-5,
861            "expected {a} close to {b} (diff {})",
862            (a - b).abs()
863        );
864    }
865
866    fn assert_vec2_close(a: Vec2, b: Vec2) {
867        assert!(
868            (a - b).length() <= 1e-5,
869            "expected {a:?} close to {b:?} (diff {})",
870            (a - b).length()
871        );
872    }
873
874    #[test]
875    fn zero_bivector_exp_is_identity() {
876        let r = Bivector2::zero().exp();
877        assert_eq!(r, Rotor2::identity());
878    }
879
880    #[test]
881    fn quarter_turn_rotates_x_to_y() {
882        let r = Bivector2(FRAC_PI_2).exp();
883        assert_vec2_close(r.apply(Vec2::X), Vec2::Y);
884        assert_vec2_close(r.apply(Vec2::Y), -Vec2::X);
885    }
886
887    #[test]
888    fn half_turn_negates() {
889        let r = Bivector2(PI).exp();
890        assert_vec2_close(r.apply(Vec2::new(1.0, 2.0)), Vec2::new(-1.0, -2.0));
891    }
892
893    #[test]
894    fn full_turn_is_identity_up_to_sign() {
895        let r = Bivector2(TAU).exp();
896        assert_vec2_close(r.apply(Vec2::X), Vec2::X);
897    }
898
899    #[test]
900    fn composition_adds_angles() {
901        let a = Bivector2(0.3).exp();
902        let b = Bivector2(0.5).exp();
903        let composed = a * b;
904        let direct = Bivector2(0.8).exp();
905        assert_close(composed.a, direct.a);
906        assert_close(composed.b, direct.b);
907    }
908
909    #[test]
910    fn inverse_cancels() {
911        let r = Bivector2(1.234).exp();
912        let id = r * r.inverse();
913        assert_close(id.a, 1.0);
914        assert_close(id.b, 0.0);
915    }
916
917    #[test]
918    fn rotation_arc_handles_parallel_and_antipodal_vectors() {
919        let cases = [
920            (Vec4::X, Vec4::Y),
921            (Vec4::W, -Vec4::Y),
922            (Vec4::Y, Vec4::Y),
923            (Vec4::Y, -Vec4::Y),
924            (Vec4::new(0.5, 0.5, 0.5, 0.5), -Vec4::Y),
925            (
926                Vec4::new(0.5, -0.5, 0.5, -0.5),
927                Vec4::new(1.0, 0.0, 0.0, 0.0),
928            ),
929        ];
930        for (from, to) in cases {
931            let turned = Rotor4::from_rotation_arc(from, to).apply(from);
932            assert!(
933                (turned - to).length() < 1e-5,
934                "{from} turned to {turned}, not {to}"
935            );
936        }
937    }
938
939    #[test]
940    fn rotor4_to_mat4_agrees_with_apply() {
941        let b = Bivector4 {
942            xy: 0.7,
943            xz: 0.0,
944            xw: 0.0,
945            yz: 0.0,
946            yw: 0.0,
947            zw: 0.3,
948        };
949        let r: Rotor4 = b.exp();
950        let m = r.to_mat4();
951
952        let e1 = Vec4::new(1.0, 0.0, 0.0, 0.0);
953        let img1 = <Rotor4 as Rotor>::apply(&r, e1);
954        assert_close(m[0][0], img1.x);
955        assert_close(m[0][1], img1.y);
956        assert_close(m[0][2], img1.z);
957        assert_close(m[0][3], img1.w);
958
959        let v = Vec4::new(0.6, -0.4, 0.2, 0.9);
960        let row = |k: usize| m[0][k] * v.x + m[1][k] * v.y + m[2][k] * v.z + m[3][k] * v.w;
961        let by_matrix = Vec4::new(row(0), row(1), row(2), row(3));
962        let by_rotor = <Rotor4 as Rotor>::apply(&r, v);
963        assert!(
964            (by_matrix - by_rotor).length() < 1e-5,
965            "matrix application {by_matrix:?} should match rotor application {by_rotor:?}"
966        );
967    }
968
969    #[test]
970    fn rotor4_from_mat4_rebuilds_the_frame_including_half_turns() {
971        let quarter = std::f32::consts::FRAC_PI_2;
972        let half = std::f32::consts::PI;
973        for rotor in [
974            Rotor4::IDENTITY,
975            Bivector4::new(0.7, 0.0, 0.0, 0.0, 0.0, 0.3).exp(),
976            Bivector4::new(quarter, 0.0, 0.0, 0.0, 0.0, quarter).exp(),
977            Bivector4::new(0.0, 0.0, 0.0, half, 0.0, 0.0).exp(),
978            Bivector4::new(0.0, 0.0, 0.0, 0.0, 0.0, half).exp(),
979        ] {
980            let frame = rotor.to_mat4();
981            let rebuilt = Rotor4::from_mat4(&frame)
982                .unwrap_or_else(|| panic!("{rotor:?} gave a frame no rotor rebuilds"))
983                .to_mat4();
984            for (slot, (built, given)) in rebuilt
985                .iter()
986                .flatten()
987                .zip(frame.iter().flatten())
988                .enumerate()
989            {
990                assert!(
991                    (built - given).abs() < 1e-5,
992                    "{rotor:?} slot {slot}: rebuilt {built} against {given}"
993                );
994            }
995        }
996    }
997
998    #[test]
999    fn log_is_inverse_of_exp() {
1000        for &theta in &[-1.0_f32, -0.1, 0.0, 0.1, 1.0, 3.0] {
1001            let back = Bivector2(theta).exp().log();
1002            assert_close(back.0, theta);
1003        }
1004    }
1005
1006    fn assert_vec3_close(a: Vec3, b: Vec3) {
1007        assert!((a - b).length() <= 1e-5, "expected {a:?} close to {b:?}");
1008    }
1009
1010    #[test]
1011    fn bivector3_zero_exp_is_identity() {
1012        let r = Bivector3::ZERO.exp();
1013        assert_eq!(r, Rotor3::IDENTITY);
1014    }
1015
1016    #[test]
1017    fn rotor3_identity_leaves_vector_unchanged() {
1018        let v = Vec3::new(1.2, -3.4, 0.5);
1019        assert_vec3_close(Rotor3::IDENTITY.apply(v), v);
1020    }
1021
1022    #[test]
1023    fn bivector3_xy_quarter_turn_sends_x_to_y() {
1024        let r = Bivector3::new(FRAC_PI_2, 0.0, 0.0).exp();
1025        assert_vec3_close(r.apply(Vec3::X), Vec3::Y);
1026        assert_vec3_close(r.apply(Vec3::Y), -Vec3::X);
1027        assert_vec3_close(r.apply(Vec3::Z), Vec3::Z);
1028    }
1029
1030    #[test]
1031    fn bivector3_yz_rotation() {
1032        let r = Bivector3::new(0.0, FRAC_PI_2, 0.0).exp();
1033        assert_vec3_close(r.apply(Vec3::Y), Vec3::Z);
1034        assert_vec3_close(r.apply(Vec3::Z), -Vec3::Y);
1035        assert_vec3_close(r.apply(Vec3::X), Vec3::X);
1036    }
1037
1038    #[test]
1039    fn bivector3_zx_rotation() {
1040        let r = Bivector3::new(0.0, 0.0, FRAC_PI_2).exp();
1041        assert_vec3_close(r.apply(Vec3::Z), Vec3::X);
1042        assert_vec3_close(r.apply(Vec3::X), -Vec3::Z);
1043        assert_vec3_close(r.apply(Vec3::Y), Vec3::Y);
1044    }
1045
1046    #[test]
1047    fn rotor3_full_turn_is_identity_on_vectors() {
1048        let r = Bivector3::new(TAU, 0.0, 0.0).exp();
1049        assert_vec3_close(r.apply(Vec3::new(1.0, 2.0, 3.0)), Vec3::new(1.0, 2.0, 3.0));
1050    }
1051
1052    #[test]
1053    fn rotor3_inverse_cancels() {
1054        let r = Bivector3::new(0.7, -0.3, 0.5).exp();
1055        let id = r * r.inverse();
1056        assert_close(id.s, 1.0);
1057        assert_close(id.xy, 0.0);
1058        assert_close(id.yz, 0.0);
1059        assert_close(id.zx, 0.0);
1060    }
1061
1062    #[test]
1063    fn rotor3_composition_matches_sequential_apply() {
1064        let ra = Bivector3::new(0.4, 0.0, 0.0).exp();
1065        let rb = Bivector3::new(0.0, 0.5, 0.0).exp();
1066        let composed = ra * rb;
1067        let v = Vec3::new(0.7, -0.3, 1.1);
1068        assert_vec3_close(composed.apply(v), rb.apply(ra.apply(v)));
1069    }
1070
1071    #[test]
1072    fn rotor3_preserves_length() {
1073        let r = Bivector3::new(0.3, 0.4, 0.5).exp();
1074        for &v in &[Vec3::X, Vec3::Y, Vec3::Z, Vec3::new(1.0, 2.0, -3.0)] {
1075            assert!(
1076                (r.apply(v).length() - v.length()).abs() < 1e-5,
1077                "length not preserved: input {v}, output length {}",
1078                r.apply(v).length()
1079            );
1080        }
1081    }
1082
1083    #[test]
1084    fn rotor3_log_is_inverse_of_exp() {
1085        for bv in [
1086            Bivector3::new(0.0, 0.0, 0.0),
1087            Bivector3::new(0.1, 0.0, 0.0),
1088            Bivector3::new(0.0, 0.6, 0.0),
1089            Bivector3::new(-0.3, 0.4, 0.5),
1090            Bivector3::new(1.0, -0.5, 0.2),
1091        ] {
1092            let back = bv.exp().log();
1093            assert!(
1094                (back.xy - bv.xy).abs() < 1e-5
1095                    && (back.yz - bv.yz).abs() < 1e-5
1096                    && (back.zx - bv.zx).abs() < 1e-5,
1097                "log∘exp mismatch: {bv:?} -> {back:?}"
1098            );
1099        }
1100    }
1101
1102    #[test]
1103    fn rotor3_matches_glam_quat_for_axis_rotation() {
1104        use glam::Quat;
1105
1106        let theta = 0.7;
1107        let v = Vec3::new(1.0, 2.0, 3.0).normalize();
1108
1109        let rotor = Bivector3::new(theta, 0.0, 0.0).exp();
1110        let quat = Quat::from_axis_angle(Vec3::Z, theta);
1111        assert_vec3_close(rotor.apply(v), quat * v);
1112
1113        let rotor = Bivector3::new(0.0, theta, 0.0).exp();
1114        let quat = Quat::from_axis_angle(Vec3::X, theta);
1115        assert_vec3_close(rotor.apply(v), quat * v);
1116
1117        let rotor = Bivector3::new(0.0, 0.0, theta).exp();
1118        let quat = Quat::from_axis_angle(Vec3::Y, theta);
1119        assert_vec3_close(rotor.apply(v), quat * v);
1120    }
1121
1122    fn assert_vec4_close(a: Vec4, b: Vec4) {
1123        assert!(
1124            (a - b).length() <= 1e-4,
1125            "expected {a:?} close to {b:?} (diff {})",
1126            (a - b).length()
1127        );
1128    }
1129
1130    fn assert_vec4_close_tol(a: Vec4, b: Vec4, tol: f32) {
1131        assert!(
1132            (a - b).length() <= tol,
1133            "expected {a:?} close to {b:?} (diff {})",
1134            (a - b).length()
1135        );
1136    }
1137
1138    #[test]
1139    fn bivector4_zero_exp_is_identity() {
1140        let r = Bivector4::ZERO.exp();
1141        assert_eq!(r, Rotor4::IDENTITY);
1142    }
1143
1144    #[test]
1145    fn bivector4_dot_inner_product_invariants() {
1146        let xy = Plane4::Xy.unit_bivector();
1147        let zw = Plane4::Zw.unit_bivector();
1148        let xw = Plane4::Xw.unit_bivector();
1149        assert_eq!(xy.dot(xy), 1.0);
1150        assert_eq!(xy.dot(zw), 0.0);
1151        assert_eq!(xy.dot(xw), 0.0);
1152        let a = Bivector4::new(1.0, 0.5, -0.25, 0.75, -1.0, 2.0);
1153        let b = xy * 3.0;
1154        let c = xw * 4.0;
1155        assert!((a.dot(b + c) - (a.dot(b) + a.dot(c))).abs() < 1e-6);
1156    }
1157
1158    #[test]
1159    fn rotor4_identity_leaves_vector_unchanged() {
1160        let v = Vec4::new(1.2, -3.4, 0.5, 0.7);
1161        assert_vec4_close(Rotor4::IDENTITY.apply(v), v);
1162    }
1163
1164    #[test]
1165    fn bivector4_single_plane_rotations_are_planar() {
1166        let r = Bivector4::new(FRAC_PI_2, 0.0, 0.0, 0.0, 0.0, 0.0).exp();
1167        assert_vec4_close(r.apply(Vec4::X), Vec4::Y);
1168        assert_vec4_close(r.apply(Vec4::Y), -Vec4::X);
1169        assert_vec4_close(r.apply(Vec4::Z), Vec4::Z);
1170        assert_vec4_close(r.apply(Vec4::W), Vec4::W);
1171
1172        let r = Bivector4::new(0.0, FRAC_PI_2, 0.0, 0.0, 0.0, 0.0).exp();
1173        assert_vec4_close(r.apply(Vec4::X), Vec4::Z);
1174        assert_vec4_close(r.apply(Vec4::Z), -Vec4::X);
1175        assert_vec4_close(r.apply(Vec4::Y), Vec4::Y);
1176        assert_vec4_close(r.apply(Vec4::W), Vec4::W);
1177
1178        let r = Bivector4::new(0.0, 0.0, FRAC_PI_2, 0.0, 0.0, 0.0).exp();
1179        assert_vec4_close(r.apply(Vec4::X), Vec4::W);
1180        assert_vec4_close(r.apply(Vec4::W), -Vec4::X);
1181
1182        let r = Bivector4::new(0.0, 0.0, 0.0, FRAC_PI_2, 0.0, 0.0).exp();
1183        assert_vec4_close(r.apply(Vec4::Y), Vec4::Z);
1184        assert_vec4_close(r.apply(Vec4::Z), -Vec4::Y);
1185
1186        let r = Bivector4::new(0.0, 0.0, 0.0, 0.0, FRAC_PI_2, 0.0).exp();
1187        assert_vec4_close(r.apply(Vec4::Y), Vec4::W);
1188        assert_vec4_close(r.apply(Vec4::W), -Vec4::Y);
1189
1190        let r = Bivector4::new(0.0, 0.0, 0.0, 0.0, 0.0, FRAC_PI_2).exp();
1191        assert_vec4_close(r.apply(Vec4::Z), Vec4::W);
1192        assert_vec4_close(r.apply(Vec4::W), -Vec4::Z);
1193    }
1194
1195    #[test]
1196    fn bivector4_double_rotation_xy_plus_zw() {
1197        let theta = FRAC_PI_2;
1198        let r = Bivector4::new(theta, 0.0, 0.0, 0.0, 0.0, theta).exp();
1199        assert_vec4_close(r.apply(Vec4::X), Vec4::Y);
1200        assert_vec4_close(r.apply(Vec4::Y), -Vec4::X);
1201        assert_vec4_close(r.apply(Vec4::Z), Vec4::W);
1202        assert_vec4_close(r.apply(Vec4::W), -Vec4::Z);
1203
1204        assert_close(r.xyzw, 0.5);
1205    }
1206
1207    #[test]
1208    fn rotor4_scalar_and_pseudoscalar_carry_the_two_invariant_half_angles() {
1209        for (t1, t2) in [(0.7, 0.0), (1.1, 0.4), (0.9, 0.9)] {
1210            let r = Bivector4::new(t1, 0.0, 0.0, 0.0, 0.0, t2).exp();
1211            assert_close(r.s, (t1 * 0.5).cos() * (t2 * 0.5).cos());
1212            assert_close(r.xyzw, (t1 * 0.5).sin() * (t2 * 0.5).sin());
1213        }
1214
1215        for b in [
1216            Bivector4::new(0.0, 0.0, 0.0, 0.0, 0.0, 1.3),
1217            Bivector4::new(0.0, 0.6, 0.0, 0.0, 0.0, 0.0),
1218        ] {
1219            assert_close(b.exp().xyzw, 0.0);
1220        }
1221    }
1222
1223    #[test]
1224    fn rotor4_full_turn_is_identity_on_vectors() {
1225        let r = Bivector4::new(TAU, 0.0, 0.0, 0.0, 0.0, 0.0).exp();
1226        assert_vec4_close_tol(
1227            r.apply(Vec4::new(1.0, 2.0, 3.0, 4.0)),
1228            Vec4::new(1.0, 2.0, 3.0, 4.0),
1229            3e-4,
1230        );
1231    }
1232
1233    #[test]
1234    fn rotor4_inverse_cancels() {
1235        let r = Bivector4::new(0.3, -0.2, 0.4, 0.1, -0.5, 0.25).exp();
1236        let id = r * r.inverse();
1237        assert_close(id.s, 1.0);
1238        assert_close(id.xy, 0.0);
1239        assert_close(id.xz, 0.0);
1240        assert_close(id.xw, 0.0);
1241        assert_close(id.yz, 0.0);
1242        assert_close(id.yw, 0.0);
1243        assert_close(id.zw, 0.0);
1244        assert_close(id.xyzw, 0.0);
1245    }
1246
1247    #[test]
1248    fn rotor4_is_unit_norm() {
1249        for b in [
1250            Bivector4::new(0.1, 0.0, 0.0, 0.0, 0.0, 0.0),
1251            Bivector4::new(0.5, 0.3, -0.4, 0.0, 0.0, 0.0),
1252            Bivector4::new(0.7, 0.0, 0.0, 0.0, 0.0, 0.5),
1253            Bivector4::new(0.3, -0.2, 0.4, 0.1, -0.5, 0.25),
1254        ] {
1255            let r = b.exp();
1256            let n = r.norm_squared();
1257            assert!(
1258                (n - 1.0).abs() < 1e-4,
1259                "rotor not unit-norm: |R|² = {n} for B = {b:?}"
1260            );
1261        }
1262    }
1263
1264    #[test]
1265    fn rotor4_preserves_length() {
1266        let r = Bivector4::new(0.3, -0.2, 0.4, 0.1, -0.5, 0.25).exp();
1267        for v in [
1268            Vec4::X,
1269            Vec4::Y,
1270            Vec4::Z,
1271            Vec4::W,
1272            Vec4::new(1.0, 2.0, 3.0, 4.0),
1273            Vec4::new(-0.5, 0.5, -0.5, 0.5),
1274        ] {
1275            let rotated = r.apply(v);
1276            assert!(
1277                (rotated.length() - v.length()).abs() < 1e-3,
1278                "length drift: {v:?} -> {rotated:?}"
1279            );
1280        }
1281    }
1282
1283    #[test]
1284    fn rotor4_composition_matches_sequential_apply() {
1285        let ra = Bivector4::new(0.4, 0.0, 0.0, 0.0, 0.0, 0.0).exp();
1286        let rb = Bivector4::new(0.0, 0.0, 0.0, 0.5, 0.0, 0.0).exp();
1287        let composed = ra * rb;
1288        let v = Vec4::new(0.7, -0.3, 1.1, 0.2);
1289        assert_vec4_close(composed.apply(v), rb.apply(ra.apply(v)));
1290    }
1291
1292    fn invariant_plane_pairs() -> [(Bivector4, Bivector4); 4] {
1293        let root_half = 0.5_f32.sqrt();
1294        let u1 = Vec4::new(root_half, root_half, 0.0, 0.0);
1295        let v1 = Vec4::new(0.0, 0.0, root_half, root_half);
1296        let u2 = Vec4::new(root_half, -root_half, 0.0, 0.0);
1297        let v2 = Vec4::new(0.0, 0.0, root_half, -root_half);
1298        let [oblique, orthogonal] = nondegenerate_plane_pairs();
1299        [
1300            (Bivector4::basis(0), Bivector4::basis(5)),
1301            (Bivector4::wedge(u1, v1), Bivector4::wedge(v2, u2)),
1302            oblique,
1303            orthogonal,
1304        ]
1305    }
1306
1307    fn plane_pair_from_eigenparts(sum: Vec3, diff: Vec3) -> (Bivector4, Bivector4) {
1308        let plane = |d: Vec3| Bivector4 {
1309            xy: 0.5 * (sum.x + d.x),
1310            xz: 0.5 * (sum.y + d.y),
1311            xw: 0.5 * (sum.z + d.z),
1312            yz: 0.5 * (sum.z - d.z),
1313            yw: 0.5 * (d.y - sum.y),
1314            zw: 0.5 * (sum.x - d.x),
1315        };
1316        (plane(diff), plane(-diff))
1317    }
1318
1319    fn nondegenerate_plane_pairs() -> [(Bivector4, Bivector4); 2] {
1320        let sum = Vec3::new(2.0, 3.0, 6.0) / 7.0;
1321        let oblique_diff = Vec3::new(9.0, 2.0, 6.0) / 11.0;
1322        let orthogonal_diff = Vec3::new(3.0, -6.0, 2.0) / 7.0;
1323        [
1324            plane_pair_from_eigenparts(sum, oblique_diff),
1325            plane_pair_from_eigenparts(sum, orthogonal_diff),
1326        ]
1327    }
1328
1329    const HALF_ANGLE_STRESS_PAIRS: [(f32, f32); 10] = [
1330        (1.0e-3, 1.0e-3),
1331        (1.0e-3, 5.0e-4),
1332        (1.0e-3, -1.0e-3),
1333        (1.0e-3, 0.0),
1334        (0.7, 0.7),
1335        (0.7, 0.6999),
1336        (1.0, 0.999),
1337        (2.0, 1.999),
1338        (1.2, 0.3),
1339        (0.5, -0.4),
1340    ];
1341
1342    #[test]
1343    fn rotor4_log_recovers_both_invariant_angles() {
1344        for (p1, p2) in invariant_plane_pairs() {
1345            for (t1, t2) in HALF_ANGLE_STRESS_PAIRS {
1346                let b = p1 * t1 + p2 * t2;
1347                let back = ((p1 * t1).exp() * (p2 * t2).exp()).log();
1348                let diff = (back + b * (-1.0)).magnitude();
1349                assert!(
1350                    diff <= 1e-6,
1351                    "log mismatch at ({t1}, {t2}): {b:?} -> {back:?}"
1352                );
1353            }
1354        }
1355    }
1356
1357    #[test]
1358    fn rotor4_log_of_exp_round_trips_at_small_and_near_equal_angles() {
1359        for (p1, p2) in invariant_plane_pairs() {
1360            for (t1, t2) in HALF_ANGLE_STRESS_PAIRS {
1361                let b = p1 * t1 + p2 * t2;
1362                let back = b.exp().log();
1363                let diff = (back + b * (-1.0)).magnitude();
1364                assert!(
1365                    diff <= 2e-5 * b.magnitude(),
1366                    "log∘exp mismatch at ({t1}, {t2}): {b:?} -> {back:?}"
1367                );
1368            }
1369        }
1370    }
1371
1372    #[test]
1373    fn rotor4_log_of_a_simple_turn_takes_the_short_way_round() {
1374        let long_way = 1.9 * PI;
1375        let logged = Bivector4::new(long_way, 0.0, 0.0, 0.0, 0.0, 0.0)
1376            .exp()
1377            .log();
1378        assert_close(logged.xy, long_way - TAU);
1379        assert_close(logged.magnitude(), TAU - long_way);
1380
1381        const STEPS: i32 = 47;
1382        for step in -STEPS..=STEPS {
1383            let theta = step as f32 * (TAU / STEPS as f32);
1384            let short = theta - TAU * (theta / TAU).round();
1385            let back = Bivector4::new(0.0, 0.0, 0.0, theta, 0.0, 0.0).exp().log();
1386            assert!(
1387                (back.yz - short).abs() <= 1e-5 && back.magnitude() <= PI,
1388                "a turn of {theta} logged as {back:?}, not {short}"
1389            );
1390        }
1391    }
1392
1393    struct Xorshift(u32);
1394
1395    impl Xorshift {
1396        fn next_u32(&mut self) -> u32 {
1397            self.0 ^= self.0 << 13;
1398            self.0 ^= self.0 >> 17;
1399            self.0 ^= self.0 << 5;
1400            self.0
1401        }
1402
1403        fn signed(&mut self, span: f32) -> f32 {
1404            let unit = self.next_u32() as f32 / u32::MAX as f32;
1405            (unit * 2.0 - 1.0) * span
1406        }
1407
1408        fn bivector4(&mut self, span: f32) -> Bivector4 {
1409            Bivector4::new(
1410                self.signed(span),
1411                self.signed(span),
1412                self.signed(span),
1413                self.signed(span),
1414                self.signed(span),
1415                self.signed(span),
1416            )
1417        }
1418    }
1419
1420    #[test]
1421    fn rotor4_log_is_the_shorter_of_the_two_representatives() {
1422        const SAMPLES: usize = 100_000;
1423        let probes = [Vec4::X, Vec4::W, Vec4::new(1.0, -0.5, 0.3, 0.7)];
1424        let mut rng = Xorshift(0x5eed_4104);
1425        for _ in 0..SAMPLES {
1426            let rotor = rng.bivector4(TAU).exp();
1427            let back = rotor.log();
1428            let sum = Vec3::new(back.xy + back.zw, back.xz - back.yw, back.xw + back.yz);
1429            let diff = Vec3::new(back.xy - back.zw, back.xz + back.yw, back.xw - back.yz);
1430
1431            assert!(
1432                sum.length() + diff.length() <= 2.0 * PI + 1e-5,
1433                "long way round: {rotor:?} -> {back:?}"
1434            );
1435            assert!(
1436                back.magnitude() <= PI * SQRT_2 + 1e-5,
1437                "past the SO(4) diameter: {rotor:?} -> {back:?}"
1438            );
1439            let regenerated = back.exp();
1440            for v in probes {
1441                assert_vec4_close_tol(regenerated.apply(v), rotor.apply(v), 1e-3);
1442            }
1443        }
1444    }
1445
1446    #[test]
1447    fn rotor4_exp_survives_a_double_rotation_with_one_tiny_angle() {
1448        for (t1, t2) in [(3.0_f32, 1.0e-4_f32), (1.0, 1.0e-5), (0.5, -1.0e-4)] {
1449            let b = Bivector4::new(t1, 0.0, 0.0, 0.0, 0.0, t2);
1450            let rotor = b.exp();
1451            assert_close(rotor.norm_squared(), 1.0);
1452            let back = rotor.log();
1453            assert!(
1454                (back + b * (-1.0)).magnitude() <= 1e-6,
1455                "({t1}, {t2}) round-tripped as {back:?}"
1456            );
1457        }
1458    }
1459
1460    #[test]
1461    fn isoclinic_half_turn_log_preserves_action() {
1462        for sign in [-1.0, 1.0] {
1463            let pseudoscalar = Rotor4 {
1464                s: 0.0,
1465                xy: 0.0,
1466                xz: 0.0,
1467                xw: 0.0,
1468                yz: 0.0,
1469                yw: 0.0,
1470                zw: 0.0,
1471                xyzw: sign,
1472            };
1473            let recovered = pseudoscalar.log().exp();
1474            for v in [
1475                Vec4::X,
1476                Vec4::Y,
1477                Vec4::Z,
1478                Vec4::W,
1479                Vec4::new(1.0, -0.5, 0.3, 0.7),
1480            ] {
1481                assert_vec4_close_tol(pseudoscalar.apply(v), -v, 1e-6);
1482                assert_vec4_close_tol(recovered.apply(v), -v, 1e-6);
1483            }
1484        }
1485
1486        let near = Bivector4::new(0.99 * PI, 0.0, 0.0, 0.0, 0.0, 0.99 * PI);
1487        let back = near.exp().log();
1488        assert!(back.magnitude() <= PI * SQRT_2);
1489        assert!((back + near * (-1.0)).magnitude() <= 1e-3, "{back:?}");
1490    }
1491
1492    #[test]
1493    fn rotor4_xy_matches_mat4_rotation_z_on_xy_subspace() {
1494        use glam::Mat4;
1495        let theta = 0.7;
1496        let rotor = Bivector4::new(theta, 0.0, 0.0, 0.0, 0.0, 0.0).exp();
1497        let mat = Mat4::from_rotation_z(theta);
1498
1499        for v in [
1500            Vec4::new(1.0, 0.0, 0.0, 0.0),
1501            Vec4::new(0.0, 1.0, 0.0, 0.0),
1502            Vec4::new(0.6, -0.8, 0.0, 0.0),
1503        ] {
1504            let via_rotor = rotor.apply(v);
1505            let via_mat = mat * v;
1506            assert_vec4_close_tol(via_rotor, via_mat, 1e-4);
1507        }
1508    }
1509
1510    #[test]
1511    fn rotor4_normalize_produces_unit() {
1512        let r = Bivector4::new(0.3, 0.1, -0.2, 0.4, 0.1, 0.0).exp();
1513        let perturbed = Rotor4 {
1514            s: r.s * 1.01,
1515            xy: r.xy * 1.01,
1516            xz: r.xz * 1.01,
1517            xw: r.xw * 1.01,
1518            yz: r.yz * 1.01,
1519            yw: r.yw * 1.01,
1520            zw: r.zw * 1.01,
1521            xyzw: r.xyzw * 1.01,
1522        };
1523        let back = perturbed.normalize();
1524        assert_close(back.norm_squared(), 1.0);
1525    }
1526
1527    #[test]
1528    fn rotor4_compound_xy_xz_xw_yz_integrated_matches_closed_form() {
1529        let omega = Bivector4::new(1.0, 1.0, 1.0, 1.0, 0.0, 0.0);
1530        let dt = 1.0 / 60.0;
1531        let n_steps = 60_u32;
1532        let total_angle = dt * n_steps as f32;
1533        let delta = (omega * dt).exp();
1534
1535        let mut integrated = Rotor4::IDENTITY;
1536        for _ in 0..n_steps {
1537            integrated = delta * integrated;
1538        }
1539        let closed_form = (omega * total_angle).exp();
1540
1541        for v in [
1542            Vec4::X,
1543            Vec4::Y,
1544            Vec4::Z,
1545            Vec4::W,
1546            Vec4::new(1.0, 2.0, 3.0, 4.0),
1547            Vec4::new(-0.5, 0.5, -0.5, 0.5),
1548        ] {
1549            let via_integrated = integrated.apply(v);
1550            let via_closed = closed_form.apply(v);
1551            let diff = (via_integrated - via_closed).length();
1552            assert!(
1553                diff < 1e-5,
1554                "compound xy+xz+xw+yz integration drift: v={v:?} \
1555                 integrated={via_integrated:?} closed={via_closed:?} \
1556                 diff={diff}",
1557            );
1558        }
1559    }
1560
1561    #[test]
1562    fn rotor4_compound_integration_preserves_unit_norm_over_900_steps() {
1563        let omega = Bivector4::new(1.0, 1.0, 1.0, 1.0, 0.0, 0.0);
1564        let dt = 1.0 / 60.0;
1565        let delta = (omega * dt).exp();
1566        let mut r = Rotor4::IDENTITY;
1567        for _ in 0..900 {
1568            r = delta * r;
1569        }
1570        let n2 = r.norm_squared();
1571
1572        assert!(
1573            (n2 - 1.0).abs() < 1e-5,
1574            "rotor norm drifted after 900 compositions: |R|² = {n2}",
1575        );
1576    }
1577
1578    #[test]
1579    fn bivector4_contract_vec_is_clifford_left_contraction() {
1580        let b = Bivector4::new(1.0, 0.0, 0.0, 0.0, 0.0, 0.0);
1581        assert_vec4_close_tol(b.contract_vec(Vec4::X), -Vec4::Y, 1e-6);
1582        assert_vec4_close_tol(b.contract_vec(Vec4::Y), Vec4::X, 1e-6);
1583        let b = Bivector4::new(0.0, 0.0, 0.0, 0.0, 0.0, 1.0);
1584        assert_vec4_close_tol(b.contract_vec(Vec4::Z), -Vec4::W, 1e-6);
1585        assert_vec4_close_tol(b.contract_vec(Vec4::W), Vec4::Z, 1e-6);
1586        let b = Bivector4::new(1.0, 0.0, 0.0, 0.0, 0.0, 0.0);
1587        assert_vec4_close_tol(b.contract_vec(Vec4::Z), Vec4::ZERO, 1e-6);
1588        assert_vec4_close_tol(b.contract_vec(Vec4::W), Vec4::ZERO, 1e-6);
1589    }
1590}