Skip to main content

cliffy_gpu/
simd.rs

1// Copyright (C) 2026 Industrial Algebra
2// SPDX-License-Identifier: Apache-2.0
3
4//! SIMD-optimized CPU operations for geometric algebra.
5//!
6//! This module provides CPU-based SIMD acceleration for geometric algebra
7//! operations when GPU is unavailable or for small batch sizes where GPU
8//! overhead would be too high.
9//!
10//! Uses the `wide` crate for portable SIMD that works on x86, ARM, and WASM.
11
12use wide::f32x8;
13
14use crate::GpuMultivector;
15use cliffy_core::GA3;
16
17/// SIMD-optimized geometric product for Cl(3,0).
18///
19/// Computes the geometric product of two multivectors using SIMD
20/// operations where possible. The Cl(3,0) geometric product follows
21/// the multiplication table for basis elements {1, e1, e2, e12, e3, e13, e23, e123}.
22#[must_use]
23pub fn geometric_product_simd(a: &GpuMultivector, b: &GpuMultivector) -> GpuMultivector {
24    // For Cl(3,0), the geometric product expands to:
25    // result[i] = sum over j,k where basis[j] * basis[k] = ±basis[i]
26    //
27    // The sign table for Cl(3,0) is:
28    // e1*e1 = 1, e2*e2 = 1, e3*e3 = 1
29    // e1*e2 = e12, e2*e1 = -e12
30    // e1*e3 = e13, e3*e1 = -e13
31    // e2*e3 = e23, e3*e2 = -e23
32    // etc.
33
34    // Scalar component: 1*1 + e1*e1 + e2*e2 + e12*e12 + e3*e3 + e13*e13 + e23*e23 + e123*e123
35    // Note: e12*e12 = e1*e2*e1*e2 = -e1*e1*e2*e2 = -1
36    //       e123*e123 = e1*e2*e3*e1*e2*e3 = -1
37    let c0 = a.coeffs[0] * b.coeffs[0]   // 1*1
38           + a.coeffs[1] * b.coeffs[1]   // e1*e1 = 1
39           + a.coeffs[2] * b.coeffs[2]   // e2*e2 = 1
40           - a.coeffs[3] * b.coeffs[3]   // e12*e12 = -1
41           + a.coeffs[4] * b.coeffs[4]   // e3*e3 = 1
42           - a.coeffs[5] * b.coeffs[5]   // e13*e13 = -1
43           - a.coeffs[6] * b.coeffs[6]   // e23*e23 = -1
44           - a.coeffs[7] * b.coeffs[7]; // e123*e123 = -1
45
46    // e1 component
47    let c1 = a.coeffs[0] * b.coeffs[1]   // 1*e1
48           + a.coeffs[1] * b.coeffs[0]   // e1*1
49           - a.coeffs[2] * b.coeffs[3]   // e2*e12 = -e1
50           + a.coeffs[3] * b.coeffs[2]   // e12*e2 = e1
51           - a.coeffs[4] * b.coeffs[5]   // e3*e13 = -e1
52           + a.coeffs[5] * b.coeffs[4]   // e13*e3 = e1
53           + a.coeffs[6] * b.coeffs[7]   // e23*e123 = e1
54           - a.coeffs[7] * b.coeffs[6]; // e123*e23 = -e1
55
56    // e2 component
57    let c2 = a.coeffs[0] * b.coeffs[2]   // 1*e2
58           + a.coeffs[1] * b.coeffs[3]   // e1*e12 = e2
59           + a.coeffs[2] * b.coeffs[0]   // e2*1
60           - a.coeffs[3] * b.coeffs[1]   // e12*e1 = -e2
61           - a.coeffs[4] * b.coeffs[6]   // e3*e23 = -e2
62           - a.coeffs[5] * b.coeffs[7]   // e13*e123 = -e2
63           + a.coeffs[6] * b.coeffs[4]   // e23*e3 = e2
64           + a.coeffs[7] * b.coeffs[5]; // e123*e13 = e2
65
66    // e12 component
67    let c3 = a.coeffs[0] * b.coeffs[3]   // 1*e12
68           + a.coeffs[1] * b.coeffs[2]   // e1*e2 = e12
69           - a.coeffs[2] * b.coeffs[1]   // e2*e1 = -e12
70           + a.coeffs[3] * b.coeffs[0]   // e12*1
71           + a.coeffs[4] * b.coeffs[7]   // e3*e123 = e12
72           + a.coeffs[5] * b.coeffs[6]   // e13*e23 = e12
73           - a.coeffs[6] * b.coeffs[5]   // e23*e13 = -e12
74           + a.coeffs[7] * b.coeffs[4]; // e123*e3 = e12
75
76    // e3 component
77    let c4 = a.coeffs[0] * b.coeffs[4]   // 1*e3
78           + a.coeffs[1] * b.coeffs[5]   // e1*e13 = e3
79           + a.coeffs[2] * b.coeffs[6]   // e2*e23 = e3
80           + a.coeffs[3] * b.coeffs[7]   // e12*e123 = e3
81           + a.coeffs[4] * b.coeffs[0]   // e3*1
82           - a.coeffs[5] * b.coeffs[1]   // e13*e1 = -e3
83           - a.coeffs[6] * b.coeffs[2]   // e23*e2 = -e3
84           - a.coeffs[7] * b.coeffs[3]; // e123*e12 = -e3
85
86    // e13 component
87    let c5 = a.coeffs[0] * b.coeffs[5]   // 1*e13
88           + a.coeffs[1] * b.coeffs[4]   // e1*e3 = e13
89           - a.coeffs[2] * b.coeffs[7]   // e2*e123 = -e13
90           - a.coeffs[3] * b.coeffs[6]   // e12*e23 = -e13
91           - a.coeffs[4] * b.coeffs[1]   // e3*e1 = -e13
92           + a.coeffs[5] * b.coeffs[0]   // e13*1
93           + a.coeffs[6] * b.coeffs[3]   // e23*e12 = e13
94           + a.coeffs[7] * b.coeffs[2]; // e123*e2 = e13
95
96    // e23 component
97    let c6 = a.coeffs[0] * b.coeffs[6]   // 1*e23
98           + a.coeffs[1] * b.coeffs[7]   // e1*e123 = e23
99           + a.coeffs[2] * b.coeffs[4]   // e2*e3 = e23
100           + a.coeffs[3] * b.coeffs[5]   // e12*e13 = e23
101           - a.coeffs[4] * b.coeffs[2]   // e3*e2 = -e23
102           - a.coeffs[5] * b.coeffs[3]   // e13*e12 = -e23
103           + a.coeffs[6] * b.coeffs[0]   // e23*1
104           - a.coeffs[7] * b.coeffs[1]; // e123*e1 = -e23
105
106    // e123 component
107    let c7 = a.coeffs[0] * b.coeffs[7]   // 1*e123
108           + a.coeffs[1] * b.coeffs[6]   // e1*e23 = e123
109           - a.coeffs[2] * b.coeffs[5]   // e2*e13 = -e123
110           + a.coeffs[3] * b.coeffs[4]   // e12*e3 = e123
111           + a.coeffs[4] * b.coeffs[3]   // e3*e12 = e123
112           - a.coeffs[5] * b.coeffs[2]   // e13*e2 = -e123
113           + a.coeffs[6] * b.coeffs[1]   // e23*e1 = e123
114           + a.coeffs[7] * b.coeffs[0]; // e123*1
115
116    GpuMultivector {
117        coeffs: [c0, c1, c2, c3, c4, c5, c6, c7],
118    }
119}
120
121/// SIMD-optimized addition of two multivectors.
122#[inline]
123#[must_use]
124pub fn addition_simd(a: &GpuMultivector, b: &GpuMultivector) -> GpuMultivector {
125    let a_vec = f32x8::from(a.coeffs);
126    let b_vec = f32x8::from(b.coeffs);
127    let result = a_vec + b_vec;
128    GpuMultivector {
129        coeffs: result.to_array(),
130    }
131}
132
133/// SIMD-optimized subtraction of two multivectors.
134#[inline]
135#[must_use]
136pub fn subtraction_simd(a: &GpuMultivector, b: &GpuMultivector) -> GpuMultivector {
137    let a_vec = f32x8::from(a.coeffs);
138    let b_vec = f32x8::from(b.coeffs);
139    let result = a_vec - b_vec;
140    GpuMultivector {
141        coeffs: result.to_array(),
142    }
143}
144
145/// SIMD-optimized scalar multiplication.
146#[inline]
147#[must_use]
148pub fn scalar_mul_simd(a: &GpuMultivector, scalar: f32) -> GpuMultivector {
149    let a_vec = f32x8::from(a.coeffs);
150    let s_vec = f32x8::splat(scalar);
151    let result = a_vec * s_vec;
152    GpuMultivector {
153        coeffs: result.to_array(),
154    }
155}
156
157/// SIMD-optimized reverse (reversion) of a multivector.
158///
159/// For Cl(3,0), the reverse negates bivector and pseudoscalar components:
160/// ~(a + b*e12 + c*e13 + d*e23 + e*e123) = a - b*e12 - c*e13 - d*e23 - e*e123
161#[inline]
162#[must_use]
163pub fn reverse_simd(a: &GpuMultivector) -> GpuMultivector {
164    // Reverse: negate grades 2 and 3
165    // Grade 0 (scalar): coeffs[0] - keep
166    // Grade 1 (vectors): coeffs[1,2,4] - keep
167    // Grade 2 (bivectors): coeffs[3,5,6] - negate
168    // Grade 3 (pseudoscalar): coeffs[7] - negate
169    let signs = f32x8::from([1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0, -1.0]);
170    let a_vec = f32x8::from(a.coeffs);
171    let result = a_vec * signs;
172    GpuMultivector {
173        coeffs: result.to_array(),
174    }
175}
176
177/// SIMD-optimized grade involution (main involution).
178///
179/// Negates odd-grade components.
180#[inline]
181#[must_use]
182pub fn grade_involution_simd(a: &GpuMultivector) -> GpuMultivector {
183    // Grade involution: negate odd grades
184    // Grade 0: keep, Grade 1: negate, Grade 2: keep, Grade 3: negate
185    let signs = f32x8::from([1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0]);
186    let a_vec = f32x8::from(a.coeffs);
187    let result = a_vec * signs;
188    GpuMultivector {
189        coeffs: result.to_array(),
190    }
191}
192
193/// SIMD-optimized conjugate (Clifford conjugate).
194///
195/// Combines reversion and grade involution.
196#[inline]
197#[must_use]
198pub fn conjugate_simd(a: &GpuMultivector) -> GpuMultivector {
199    // Conjugate = reverse ∘ grade_involution
200    // Grade 0: keep, Grade 1: negate, Grade 2: negate, Grade 3: keep
201    let signs = f32x8::from([1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0]);
202    let a_vec = f32x8::from(a.coeffs);
203    let result = a_vec * signs;
204    GpuMultivector {
205        coeffs: result.to_array(),
206    }
207}
208
209/// SIMD-optimized squared magnitude (norm squared).
210#[inline]
211#[must_use]
212pub fn norm_squared_simd(a: &GpuMultivector) -> f32 {
213    // ||a||² = a * ~a (scalar part only)
214    let rev = reverse_simd(a);
215    let prod = geometric_product_simd(a, &rev);
216    prod.coeffs[0]
217}
218
219/// SIMD-optimized norm (magnitude).
220#[inline]
221#[must_use]
222pub fn norm_simd(a: &GpuMultivector) -> f32 {
223    norm_squared_simd(a).sqrt()
224}
225
226/// SIMD-optimized normalization.
227#[inline]
228#[must_use]
229pub fn normalize_simd(a: &GpuMultivector) -> GpuMultivector {
230    let n = norm_simd(a);
231    if n > 1e-10 {
232        scalar_mul_simd(a, 1.0 / n)
233    } else {
234        GpuMultivector::zero()
235    }
236}
237
238/// SIMD-optimized sandwich product: b * a * ~b
239///
240/// Used for rotations and reflections.
241#[inline]
242#[must_use]
243pub fn sandwich_simd(a: &GpuMultivector, b: &GpuMultivector) -> GpuMultivector {
244    let b_rev = reverse_simd(b);
245    let temp = geometric_product_simd(b, a);
246    geometric_product_simd(&temp, &b_rev)
247}
248
249/// SIMD-optimized dot product (inner product).
250#[inline]
251#[must_use]
252pub fn dot_simd(a: &GpuMultivector, b: &GpuMultivector) -> f32 {
253    let a_vec = f32x8::from(a.coeffs);
254    let b_vec = f32x8::from(b.coeffs);
255    let prod = a_vec * b_vec;
256    // Sum all elements
257    prod.reduce_add()
258}
259
260/// SIMD-optimized exponential for bivectors (creates rotors).
261///
262/// For a bivector B, exp(B) = cos(|B|) + sin(|B|) * B/|B|
263#[must_use]
264pub fn exp_bivector_simd(b: &GpuMultivector) -> GpuMultivector {
265    // Extract bivector components
266    let bx = b.coeffs[3]; // e12
267    let by = b.coeffs[5]; // e13
268    let bz = b.coeffs[6]; // e23
269
270    // Compute magnitude of bivector
271    let mag_sq = bx * bx + by * by + bz * bz;
272    let mag = mag_sq.sqrt();
273
274    if mag < 1e-10 {
275        // For small bivector, exp(B) ≈ 1 + B
276        let mut result = GpuMultivector::scalar(1.0);
277        result.coeffs[3] = bx;
278        result.coeffs[5] = by;
279        result.coeffs[6] = bz;
280        return result;
281    }
282
283    let cos_mag = mag.cos();
284    let sin_mag_over_mag = mag.sin() / mag;
285
286    GpuMultivector {
287        coeffs: [
288            cos_mag,
289            0.0,
290            0.0,
291            bx * sin_mag_over_mag,
292            0.0,
293            by * sin_mag_over_mag,
294            bz * sin_mag_over_mag,
295            0.0,
296        ],
297    }
298}
299
300/// SIMD-optimized linear interpolation.
301#[inline]
302#[must_use]
303pub fn lerp_simd(a: &GpuMultivector, b: &GpuMultivector, t: f32) -> GpuMultivector {
304    let a_vec = f32x8::from(a.coeffs);
305    let b_vec = f32x8::from(b.coeffs);
306    let t_vec = f32x8::splat(t);
307    let one_minus_t = f32x8::splat(1.0 - t);
308    let result = a_vec * one_minus_t + b_vec * t_vec;
309    GpuMultivector {
310        coeffs: result.to_array(),
311    }
312}
313
314/// SIMD-optimized rotor spherical linear interpolation (SLERP).
315///
316/// Interpolates smoothly between two rotors.
317#[must_use]
318pub fn rotor_slerp_simd(a: &GpuMultivector, b: &GpuMultivector, t: f32) -> GpuMultivector {
319    // Compute dot product to find angle between rotors
320    let dot = dot_simd(a, b);
321
322    // Clamp dot product to valid range
323    let dot = dot.clamp(-1.0, 1.0);
324
325    // If rotors are very close, use linear interpolation
326    if dot.abs() > 0.9995 {
327        let result = lerp_simd(a, b, t);
328        return normalize_simd(&result);
329    }
330
331    // Compute interpolation using geometric algebra
332    // slerp(a, b, t) = a * exp(t * log(~a * b))
333    let a_rev = reverse_simd(a);
334    let ratio = geometric_product_simd(&a_rev, b);
335
336    // Extract bivector part for log
337    // For a rotor r = cos(θ) + sin(θ)B, log(r) = θB
338    let scalar = ratio.coeffs[0];
339    let theta = scalar.clamp(-1.0, 1.0).acos();
340
341    if theta.abs() < 1e-10 {
342        return *a;
343    }
344
345    // Scale the bivector by t*theta/sin(theta)
346    let scale = t * theta / theta.sin();
347    let mut log_ratio = GpuMultivector::zero();
348    log_ratio.coeffs[3] = ratio.coeffs[3] * scale;
349    log_ratio.coeffs[5] = ratio.coeffs[5] * scale;
350    log_ratio.coeffs[6] = ratio.coeffs[6] * scale;
351
352    let exp_log = exp_bivector_simd(&log_ratio);
353    geometric_product_simd(a, &exp_log)
354}
355
356/// Batch processing context for SIMD operations.
357///
358/// Provides efficient batch processing of geometric algebra operations
359/// using SIMD acceleration.
360pub struct SimdBatch;
361
362impl SimdBatch {
363    /// Batch geometric product using SIMD.
364    #[must_use]
365    pub fn geometric_product(a: &[GpuMultivector], b: &[GpuMultivector]) -> Vec<GpuMultivector> {
366        a.iter()
367            .zip(b.iter())
368            .map(|(a, b)| geometric_product_simd(a, b))
369            .collect()
370    }
371
372    /// Batch addition using SIMD.
373    #[must_use]
374    pub fn addition(a: &[GpuMultivector], b: &[GpuMultivector]) -> Vec<GpuMultivector> {
375        a.iter()
376            .zip(b.iter())
377            .map(|(a, b)| addition_simd(a, b))
378            .collect()
379    }
380
381    /// Batch sandwich product using SIMD.
382    #[must_use]
383    pub fn sandwich(rotors: &[GpuMultivector], vectors: &[GpuMultivector]) -> Vec<GpuMultivector> {
384        rotors
385            .iter()
386            .zip(vectors.iter())
387            .map(|(r, v)| sandwich_simd(v, r))
388            .collect()
389    }
390
391    /// Batch exponential using SIMD.
392    pub fn exp(bivectors: &[GpuMultivector]) -> Vec<GpuMultivector> {
393        bivectors.iter().map(exp_bivector_simd).collect()
394    }
395
396    /// Batch rotor SLERP using SIMD.
397    #[must_use]
398    pub fn rotor_slerp(a: &[GpuMultivector], b: &[GpuMultivector], t: f32) -> Vec<GpuMultivector> {
399        a.iter()
400            .zip(b.iter())
401            .map(|(a, b)| rotor_slerp_simd(a, b, t))
402            .collect()
403    }
404
405    /// Batch normalize using SIMD.
406    pub fn normalize(mvs: &[GpuMultivector]) -> Vec<GpuMultivector> {
407        mvs.iter().map(normalize_simd).collect()
408    }
409
410    /// Convert from GA3 to GPU format.
411    #[must_use]
412    pub fn from_ga3(mvs: &[GA3]) -> Vec<GpuMultivector> {
413        mvs.iter().map(std::convert::Into::into).collect()
414    }
415
416    /// Convert from GPU format to GA3.
417    #[must_use]
418    pub fn to_ga3(mvs: &[GpuMultivector]) -> Vec<GA3> {
419        mvs.iter().map(|mv| (*mv).into()).collect()
420    }
421}
422
423#[cfg(test)]
424mod tests {
425    #![allow(clippy::float_cmp)]
426    // Exactly-representable constants: bit equality is the point.
427    // (Stable clippy lacks the test-context exemption newer nightly has.)
428    use super::*;
429
430    #[test]
431    fn test_addition_simd() {
432        let a = GpuMultivector::vector(1.0, 2.0, 3.0);
433        let b = GpuMultivector::vector(4.0, 5.0, 6.0);
434        let result = addition_simd(&a, &b);
435        assert_eq!(result.get_vector(), (5.0, 7.0, 9.0));
436    }
437
438    #[test]
439    fn test_subtraction_simd() {
440        let a = GpuMultivector::vector(5.0, 7.0, 9.0);
441        let b = GpuMultivector::vector(1.0, 2.0, 3.0);
442        let result = subtraction_simd(&a, &b);
443        assert_eq!(result.get_vector(), (4.0, 5.0, 6.0));
444    }
445
446    #[test]
447    fn test_scalar_mul_simd() {
448        let a = GpuMultivector::vector(1.0, 2.0, 3.0);
449        let result = scalar_mul_simd(&a, 2.0);
450        assert_eq!(result.get_vector(), (2.0, 4.0, 6.0));
451    }
452
453    #[test]
454    fn test_geometric_product_scalars() {
455        let a = GpuMultivector::scalar(3.0);
456        let b = GpuMultivector::scalar(4.0);
457        let result = geometric_product_simd(&a, &b);
458        assert!((result.get_scalar() - 12.0).abs() < 1e-6);
459    }
460
461    #[test]
462    fn test_geometric_product_vectors() {
463        // e1 * e1 = 1
464        let e1 = GpuMultivector {
465            coeffs: [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
466        };
467        let result = geometric_product_simd(&e1, &e1);
468        assert!((result.get_scalar() - 1.0).abs() < 1e-6);
469    }
470
471    #[test]
472    fn test_geometric_product_bivector() {
473        // e1 * e2 = e12
474        let e1 = GpuMultivector {
475            coeffs: [0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
476        };
477        let e2 = GpuMultivector {
478            coeffs: [0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0],
479        };
480        let result = geometric_product_simd(&e1, &e2);
481        assert!((result.coeffs[3] - 1.0).abs() < 1e-6); // e12 component
482    }
483
484    #[test]
485    fn test_reverse_simd() {
486        let mut a = GpuMultivector::zero();
487        a.coeffs[0] = 1.0; // scalar
488        a.coeffs[1] = 2.0; // e1
489        a.coeffs[3] = 3.0; // e12 (bivector)
490        a.coeffs[7] = 4.0; // e123 (pseudoscalar)
491
492        let rev = reverse_simd(&a);
493        assert_eq!(rev.coeffs[0], 1.0); // scalar unchanged
494        assert_eq!(rev.coeffs[1], 2.0); // vector unchanged
495        assert_eq!(rev.coeffs[3], -3.0); // bivector negated
496        assert_eq!(rev.coeffs[7], -4.0); // pseudoscalar negated
497    }
498
499    #[test]
500    fn test_norm_simd() {
501        let v = GpuMultivector::vector(3.0, 4.0, 0.0);
502        let n = norm_simd(&v);
503        assert!((n - 5.0).abs() < 1e-6);
504    }
505
506    #[test]
507    fn test_normalize_simd() {
508        let v = GpuMultivector::vector(3.0, 4.0, 0.0);
509        let normalized = normalize_simd(&v);
510        let (x, y, z) = normalized.get_vector();
511        assert!((x - 0.6).abs() < 1e-6);
512        assert!((y - 0.8).abs() < 1e-6);
513        assert!(z.abs() < 1e-6);
514    }
515
516    #[test]
517    fn test_lerp_simd() {
518        let a = GpuMultivector::scalar(0.0);
519        let b = GpuMultivector::scalar(10.0);
520        let mid = lerp_simd(&a, &b, 0.5);
521        assert!((mid.get_scalar() - 5.0).abs() < 1e-6);
522    }
523
524    #[test]
525    fn test_exp_bivector_small() {
526        // Small bivector: exp(B) ≈ 1 + B
527        let mut b = GpuMultivector::zero();
528        b.coeffs[3] = 0.001; // small e12 component
529        let result = exp_bivector_simd(&b);
530        assert!((result.coeffs[0] - 1.0).abs() < 0.01);
531    }
532
533    #[test]
534    fn test_sandwich_identity() {
535        // Sandwich with identity rotor should preserve the vector
536        let rotor = GpuMultivector::scalar(1.0);
537        let v = GpuMultivector::vector(1.0, 2.0, 3.0);
538        let result = sandwich_simd(&v, &rotor);
539        let (x, y, z) = result.get_vector();
540        assert!((x - 1.0).abs() < 1e-6);
541        assert!((y - 2.0).abs() < 1e-6);
542        assert!((z - 3.0).abs() < 1e-6);
543    }
544
545    #[test]
546    fn test_batch_operations() {
547        let a = vec![GpuMultivector::scalar(1.0), GpuMultivector::scalar(2.0)];
548        let b = vec![GpuMultivector::scalar(3.0), GpuMultivector::scalar(4.0)];
549
550        let results = SimdBatch::addition(&a, &b);
551        assert!((results[0].get_scalar() - 4.0).abs() < 1e-6);
552        assert!((results[1].get_scalar() - 6.0).abs() < 1e-6);
553    }
554}