Skip to main content

symtropy_math/
bivector.rs

1// Copyright (C) 2024-2026 Tristan Stoltz / Luminous Dynamics
2// SPDX-License-Identifier: AGPL-3.0-or-later
3// Commercial licensing: see COMMERCIAL_LICENSE.md at repository root
4use nalgebra::{SMatrix, SVector};
5
6/// Maximum supported dimension for bivectors.
7/// D=4 → 6, D=5 → 10, D=6 → 15, D=7 → 21, D=8 → 28, D=9 → 36.
8/// Supports up to 9D physics for dimensional sweep experiments.
9const MAX_BIVECTOR_COMPONENTS: usize = 36;
10
11/// A bivector in D-dimensional space (grade-2 element of the geometric algebra).
12///
13/// Bivectors represent oriented planes. In 3D they are dual to pseudovectors.
14/// In ND they properly represent rotations: a rotation happens IN a plane.
15///
16/// **Stack-allocated**: uses `[f64; 6]` fixed array (max for D=4).
17/// Zero heap allocation — safe for physics hot loops at 60+ Hz.
18///
19/// Component ordering: lexicographic (i,j) with i < j.
20/// For D=4: [e01, e02, e03, e12, e13, e23].
21#[derive(Clone, Copy, Debug)]
22pub struct Bivector<const D: usize> {
23    components: [f64; MAX_BIVECTOR_COMPONENTS],
24}
25
26// Compile-time assertion: D must be ≤ 9 (36 bivector components max)
27const fn _assert_d_le_9<const D: usize>() {
28    assert!(
29        D * (D - 1) / 2 <= MAX_BIVECTOR_COMPONENTS,
30        "Bivector supports D <= 9 (max 36 components)"
31    );
32}
33
34impl<const D: usize> Bivector<D> {
35    /// Number of independent components: D*(D-1)/2.
36    pub const fn num_components() -> usize {
37        D * (D - 1) / 2
38    }
39
40    /// Zero bivector.
41    #[inline]
42    pub fn zero() -> Self {
43        _assert_d_le_9::<D>();
44        Self {
45            components: [0.0; MAX_BIVECTOR_COMPONENTS],
46        }
47    }
48
49    /// Unit bivector in the plane spanned by axes i and j (i < j).
50    pub fn unit_plane(i: usize, j: usize) -> Self {
51        assert!(i < j && j < D, "requires i < j < D");
52        let mut bv = Self::zero();
53        bv.set(i, j, 1.0);
54        bv
55    }
56
57    /// Flat index for the (i,j) component with i < j.
58    #[inline]
59    fn index(i: usize, j: usize) -> usize {
60        debug_assert!(i < j && j < D);
61        i * D - i * (i + 1) / 2 + j - i - 1
62    }
63
64    /// Get the component for the (i, j) plane (i < j).
65    #[inline]
66    pub fn get(&self, i: usize, j: usize) -> f64 {
67        self.components[Self::index(i, j)]
68    }
69
70    /// Set the component for the (i, j) plane (i < j).
71    #[inline]
72    pub fn set(&mut self, i: usize, j: usize, value: f64) {
73        self.components[Self::index(i, j)] = value;
74    }
75
76    /// Convert to antisymmetric matrix representation.
77    pub fn to_matrix(&self) -> SMatrix<f64, D, D> {
78        let mut mat = SMatrix::zeros();
79        for i in 0..D {
80            for j in (i + 1)..D {
81                let val = self.get(i, j);
82                mat[(i, j)] = val;
83                mat[(j, i)] = -val;
84            }
85        }
86        mat
87    }
88
89    /// Construct from an antisymmetric matrix.
90    pub fn from_matrix(mat: &SMatrix<f64, D, D>) -> Self {
91        let mut bv = Self::zero();
92        for i in 0..D {
93            for j in (i + 1)..D {
94                bv.set(i, j, (mat[(i, j)] - mat[(j, i)]) / 2.0);
95            }
96        }
97        bv
98    }
99
100    /// Returns true if all components are finite (not NaN or infinite).
101    #[inline]
102    pub fn is_finite(&self) -> bool {
103        let n = Self::num_components();
104        for i in 0..n {
105            if !self.components[i].is_finite() {
106                return false;
107            }
108        }
109        true
110    }
111
112    /// Squared norm.
113    #[inline]
114    pub fn norm_squared(&self) -> f64 {
115        let n = Self::num_components();
116        let mut sum = 0.0;
117        for i in 0..n {
118            sum += self.components[i] * self.components[i];
119        }
120        sum
121    }
122
123    /// Norm.
124    #[inline]
125    pub fn norm(&self) -> f64 {
126        self.norm_squared().sqrt()
127    }
128
129    /// Normalize to unit bivector. Returns None if zero.
130    pub fn normalized(&self) -> Option<Self> {
131        let n = self.norm();
132        if n < 1e-15 {
133            return None;
134        }
135        Some(self.scale(1.0 / n))
136    }
137
138    /// Scale all components by a scalar.
139    #[inline]
140    pub fn scale(&self, s: f64) -> Self {
141        let mut result = *self;
142        let n = Self::num_components();
143        for i in 0..n {
144            result.components[i] *= s;
145        }
146        result
147    }
148
149    /// Add two bivectors.
150    #[inline]
151    pub fn add(&self, other: &Self) -> Self {
152        let mut result = *self;
153        let n = Self::num_components();
154        for i in 0..n {
155            result.components[i] += other.components[i];
156        }
157        result
158    }
159
160    /// Apply this bivector (as angular velocity) to a vector.
161    /// Returns B · v (antisymmetric matrix × vector).
162    #[inline]
163    pub fn apply_to_vector(&self, v: &SVector<f64, D>) -> SVector<f64, D> {
164        self.to_matrix() * v
165    }
166}
167
168impl<const D: usize> Default for Bivector<D> {
169    fn default() -> Self {
170        Self::zero()
171    }
172}
173
174impl<const D: usize> PartialEq for Bivector<D> {
175    fn eq(&self, other: &Self) -> bool {
176        let n = Self::num_components();
177        for i in 0..n {
178            if (self.components[i] - other.components[i]).abs() > 1e-14 {
179                return false;
180            }
181        }
182        true
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn num_components() {
192        assert_eq!(Bivector::<2>::num_components(), 1);
193        assert_eq!(Bivector::<3>::num_components(), 3);
194        assert_eq!(Bivector::<4>::num_components(), 6);
195    }
196
197    #[test]
198    fn unit_plane_3d() {
199        let bv = Bivector::<3>::unit_plane(0, 1);
200        assert!((bv.get(0, 1) - 1.0).abs() < 1e-15);
201        assert!(bv.get(0, 2).abs() < 1e-15);
202        assert!(bv.get(1, 2).abs() < 1e-15);
203    }
204
205    #[test]
206    fn matrix_roundtrip() {
207        let mut bv = Bivector::<4>::zero();
208        bv.set(0, 1, 1.0);
209        bv.set(0, 3, -0.5);
210        bv.set(2, 3, 0.7);
211        let mat = bv.to_matrix();
212        let recovered = Bivector::<4>::from_matrix(&mat);
213        assert_eq!(bv, recovered);
214    }
215
216    #[test]
217    fn antisymmetric_matrix() {
218        let bv = Bivector::<3>::unit_plane(0, 2);
219        let mat = bv.to_matrix();
220        assert!((mat[(0, 2)] - 1.0).abs() < 1e-15);
221        assert!((mat[(2, 0)] + 1.0).abs() < 1e-15);
222    }
223
224    #[test]
225    fn angular_velocity_3d() {
226        let bv = Bivector::<3>::unit_plane(0, 1);
227        let v = SVector::from([1.0, 0.0, 0.0]);
228        let result = bv.apply_to_vector(&v);
229        assert!(result[0].abs() < 1e-15);
230        assert!((result[1] + 1.0).abs() < 1e-15);
231        assert!(result[2].abs() < 1e-15);
232    }
233
234    #[test]
235    fn norm_and_normalize() {
236        let mut bv = Bivector::<3>::zero();
237        bv.set(0, 1, 3.0);
238        bv.set(0, 2, 4.0);
239        assert!((bv.norm() - 5.0).abs() < 1e-14);
240        let unit = bv.normalized().unwrap();
241        assert!((unit.norm() - 1.0).abs() < 1e-14);
242    }
243
244    #[test]
245    fn four_d_double_rotation() {
246        let mut bv = Bivector::<4>::zero();
247        bv.set(0, 1, 1.0);
248        bv.set(2, 3, 1.0);
249        assert!((bv.norm_squared() - 2.0).abs() < 1e-14);
250    }
251
252    #[test]
253    fn is_copy() {
254        let a = Bivector::<3>::unit_plane(0, 1);
255        let b = a; // Copy, not move
256        assert_eq!(a, b);
257    }
258
259    #[test]
260    fn scale_no_alloc() {
261        let bv = Bivector::<4>::unit_plane(0, 1);
262        let scaled = bv.scale(3.0);
263        assert!((scaled.get(0, 1) - 3.0).abs() < 1e-14);
264        // Original unchanged (Copy semantics)
265        assert!((bv.get(0, 1) - 1.0).abs() < 1e-14);
266    }
267
268    #[test]
269    fn add_no_alloc() {
270        let a = Bivector::<3>::unit_plane(0, 1);
271        let b = Bivector::<3>::unit_plane(0, 2);
272        let c = a.add(&b);
273        assert!((c.get(0, 1) - 1.0).abs() < 1e-14);
274        assert!((c.get(0, 2) - 1.0).abs() < 1e-14);
275    }
276}