Skip to main content

amari_core/
generic.rs

1//! Generic (runtime-signature) Clifford algebra operations.
2//!
3//! These functions work directly on coefficient slices with a runtime
4//! signature (p, q, r), unlike the const-generic Multivector<P,Q,R> API.
5//! They serve the WASM fallback path and any consumer that needs
6//! signature selection at runtime.
7
8/// Compute the result basis-blade index and sign for the geometric product
9/// of two basis blades `i` and `j` in Cl(p, q, r).
10///
11/// - `p`: number of basis vectors squaring to +1
12/// - `q`: number of basis vectors squaring to -1
13/// - `r`: number of basis vectors squaring to 0
14/// - `dim`: total dimension = p + q + r
15/// - `i`, `j`: basis-blade indices (0 .. 2^dim)
16///
17/// Returns `(result_blade_index, sign)` where sign is +1.0, -1.0, or
18/// 0.0 (if a null-squaring basis vector causes the product to vanish).
19pub fn blade_product(
20    p: usize,
21    q: usize,
22    _r: usize,
23    dim: usize,
24    i: usize,
25    j: usize,
26) -> (usize, f64) {
27    let k = i ^ j; // XOR for result blade
28
29    // Compute reordering sign: number of swaps to merge sorted bases.
30    // Each basis vector in `j` must be moved past the basis vectors in
31    // `i` that have lower index.
32    let mut sign = 1.0f64;
33    let mut swap_count = 0u32;
34
35    // For each bit position, if it is set in j, count how many set bits
36    // in i appear to its left (at higher positions).  We iterate from
37    // LSB to MSB so "already processed" means lower bits.
38    let mut remaining_i = i;
39    for bit in 0..dim {
40        if (j >> bit) & 1 == 1 {
41            // Count set bits in i at positions > bit (higher index = left)
42            let higher_bits = (remaining_i >> (bit + 1)).count_ones();
43            swap_count += higher_bits;
44        }
45        remaining_i &= !(1 << bit);
46    }
47
48    if swap_count % 2 == 1 {
49        sign = -sign;
50    }
51
52    // Compute metric sign: for each bit set in BOTH i and j,
53    // multiply by the metric of that basis vector.
54    let common = i & j;
55    for bit in 0..dim {
56        if (common >> bit) & 1 == 1 {
57            if bit < p {
58                // Positive signature: squares to +1, no sign change
59            } else if bit < p + q {
60                // Negative signature: squares to -1
61                sign = -sign;
62            } else {
63                // Null signature: squares to 0 → product vanishes
64                return (k, 0.0);
65            }
66        }
67    }
68
69    (k, sign)
70}
71
72/// Compute the full geometric product of two multivectors in Cl(p, q, r).
73///
74/// `a` and `b` are coefficient slices of length 2^(p+q+r).  Returns a new
75/// coefficient vector of the same length.
76pub fn generic_geometric_product(p: usize, q: usize, r: usize, a: &[f64], b: &[f64]) -> Vec<f64> {
77    let dim = p + q + r;
78    let basis_count = 1 << dim;
79    let mut result = vec![0.0; basis_count];
80
81    for (i, &ai) in a.iter().enumerate() {
82        if ai.abs() < f64::MIN_POSITIVE {
83            continue;
84        }
85
86        for (j, &bj) in b.iter().enumerate() {
87            if bj.abs() < f64::MIN_POSITIVE {
88                continue;
89            }
90
91            let (k, sign) = blade_product(p, q, r, dim, i, j);
92            if sign != 0.0 {
93                result[k] += sign * ai * bj;
94            }
95        }
96    }
97
98    result
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use crate::Multivector;
105
106    // ---- blade_product tests ----
107
108    #[test]
109    fn test_blade_product_e1_e1_cl300() {
110        // e1 * e1 = 1 in Cl(3,0,0)
111        let (k, sign) = blade_product(3, 0, 0, 3, 1, 1);
112        assert_eq!(k, 0); // scalar
113        assert!((sign - 1.0).abs() < 1e-10);
114    }
115
116    #[test]
117    fn test_blade_product_e1_e2_cl300() {
118        // e1 * e2 = e12 in Cl(3,0,0)
119        let (k, sign) = blade_product(3, 0, 0, 3, 1, 2);
120        assert_eq!(k, 3); // e12 (binary 011)
121        assert!((sign - 1.0).abs() < 1e-10);
122    }
123
124    #[test]
125    fn test_blade_product_e2_e1_cl300() {
126        // e2 * e1 = -e12 in Cl(3,0,0) — anticommutative
127        let (k, sign) = blade_product(3, 0, 0, 3, 2, 1);
128        assert_eq!(k, 3);
129        assert!((sign + 1.0).abs() < 1e-10);
130    }
131
132    #[test]
133    fn test_blade_product_e3_e3_cl210() {
134        // e3 * e3 = -1 in Cl(2,1,0) — negative signature
135        // e3 is index 4 (binary 100, bit 2)
136        let (k, sign) = blade_product(2, 1, 0, 3, 4, 4);
137        assert_eq!(k, 0);
138        assert!((sign + 1.0).abs() < 1e-10);
139    }
140
141    #[test]
142    fn test_blade_product_null_signature() {
143        // e1 * e1 = 0 when e1 is null (Cl(0,0,1))
144        let (k, sign) = blade_product(0, 0, 1, 1, 1, 1);
145        assert_eq!(k, 0);
146        assert!((sign - 0.0).abs() < 1e-10);
147    }
148
149    #[test]
150    fn test_blade_product_e12_e1_cl300() {
151        // e12 * e1 = -e2 in Cl(3,0,0)
152        // e12 = index 3, e1 = index 1, result e2 = index 2, sign = -1
153        let (k, sign) = blade_product(3, 0, 0, 3, 3, 1);
154        assert_eq!(k, 2); // e2
155        assert!((sign + 1.0).abs() < 1e-10);
156    }
157
158    #[test]
159    fn test_blade_product_scalar_scalar() {
160        // scalar * scalar = scalar
161        let (k, sign) = blade_product(3, 0, 0, 3, 0, 0);
162        assert_eq!(k, 0);
163        assert!((sign - 1.0).abs() < 1e-10);
164    }
165
166    // ---- generic_geometric_product tests ----
167
168    #[test]
169    fn test_generic_gp_matches_const_generic_cl300() {
170        let mv_a = Multivector::<3, 0, 0>::basis_vector(0);
171        let mv_b = Multivector::<3, 0, 0>::basis_vector(1);
172        let expected = mv_a.geometric_product(&mv_b);
173
174        let a_coeffs: Vec<f64> = (0..8).map(|i| mv_a.get(i)).collect();
175        let b_coeffs: Vec<f64> = (0..8).map(|i| mv_b.get(i)).collect();
176        let result = generic_geometric_product(3, 0, 0, &a_coeffs, &b_coeffs);
177
178        for i in 0..8 {
179            assert!(
180                (result[i] - expected.get(i)).abs() < 1e-10,
181                "Coefficient {} differs: {} vs {}",
182                i,
183                result[i],
184                expected.get(i)
185            );
186        }
187    }
188
189    #[test]
190    fn test_generic_gp_matches_const_generic_cl210() {
191        // Cl(2,1,0): e3 squares to -1
192        let mv_a = Multivector::<2, 1, 0>::basis_vector(2);
193        let mv_b = Multivector::<2, 1, 0>::basis_vector(2);
194        let expected = mv_a.geometric_product(&mv_b);
195
196        let a_coeffs: Vec<f64> = (0..8).map(|i| mv_a.get(i)).collect();
197        let b_coeffs: Vec<f64> = (0..8).map(|i| mv_b.get(i)).collect();
198        let result = generic_geometric_product(2, 1, 0, &a_coeffs, &b_coeffs);
199
200        for i in 0..8 {
201            assert!(
202                (result[i] - expected.get(i)).abs() < 1e-10,
203                "Coefficient {} differs: {} vs {}",
204                i,
205                result[i],
206                expected.get(i)
207            );
208        }
209    }
210
211    #[test]
212    fn test_generic_gp_random_cl410() {
213        // Cl(4,1,0) — 32 coefficients, CGA signature
214        // Use deterministic values to avoid rand API version churn
215        let a_coeffs: Vec<f64> = (0..32).map(|i| ((i as f64) * 0.137) % 1.5 - 0.75).collect();
216        let b_coeffs: Vec<f64> = (0..32)
217            .map(|i| ((i as f64 + 17.0) * 0.097) % 1.5 - 0.75)
218            .collect();
219
220        let mv_a = Multivector::<4, 1, 0>::from_coefficients(a_coeffs.clone());
221        let mv_b = Multivector::<4, 1, 0>::from_coefficients(b_coeffs.clone());
222        let expected = mv_a.geometric_product(&mv_b);
223
224        let result = generic_geometric_product(4, 1, 0, &a_coeffs, &b_coeffs);
225
226        for i in 0..32 {
227            assert!(
228                (result[i] - expected.get(i)).abs() < 1e-8,
229                "Coefficient {} differs: {} vs {}",
230                i,
231                result[i],
232                expected.get(i)
233            );
234        }
235    }
236
237    #[test]
238    fn test_generic_gp_scalar_only() {
239        // Scalar algebra Cl(0,0,0) — only the scalar component
240        let a = vec![2.0];
241        let b = vec![3.0];
242        let result = generic_geometric_product(0, 0, 0, &a, &b);
243        assert_eq!(result.len(), 1);
244        assert!((result[0] - 6.0).abs() < 1e-10);
245    }
246}