Skip to main content

sensor_fusion/
sensor_fusion_math.rs

1#![allow(clippy::inline_always)]
2#![allow(clippy::many_single_char_names)]
3
4#[cfg(feature = "simd")]
5use core::simd::{f32x4, simd_swizzle};
6
7use vqm::{Quaternion, Vector3d};
8
9/// Math functions for `SensorFusion`, using **SIMD** accelerations for `f32`.
10pub trait SensorFusionMath: Sized {
11    fn estimate_gravity(q: Quaternion<Self>) -> Vector3d<Self>;
12    fn derivative(q: Quaternion<Self>, gyro: Vector3d<Self>) -> Quaternion<Self>;
13    fn madgwick_step_acc(q: Quaternion<Self>, acc: Vector3d<Self>, max_acc_magnitude_squared: Self)
14    -> Quaternion<Self>;
15    fn madgwick_step_acc_mag(
16        q: Quaternion<Self>,
17        acc: Vector3d<Self>,
18        mag: Vector3d<Self>,
19        max_acc_magnitude_squared: Self,
20    ) -> Quaternion<Self>;
21}
22
23impl SensorFusionMath for f32 {
24    #[inline(always)]
25    fn estimate_gravity(q: Quaternion<Self>) -> Vector3d<Self> {
26        Vector3d {
27            x: 2.0 * (q.x * q.z - q.w * q.y),
28            y: 2.0 * (q.y * q.z + q.w * q.x),
29            z: q.w * q.w - q.x * q.x - q.y * q.y + q.z * q.z,
30        }
31    }
32
33    #[inline(always)]
34    fn derivative(q: Quaternion<Self>, gyro: Vector3d<Self>) -> Quaternion<Self> {
35        #[cfg(feature = "simd")]
36        {
37            /*// Load q: [w, x, y, z]
38            let q_v: f32x4 = unsafe { core::mem::transmute(q) };
39
40            // Load gyro: [x, y, z, padding] -> Swizzle to [0, x, y, z]
41            let g_raw: f32x4 = unsafe { core::mem::transmute(gyro) };
42            //let g_v = simd_swizzle!(g_raw, [f32x4::splat(0.0)], [4, 0, 1, 2]);
43
44            // Efficiently shift: [x, y, z, 0] -> [0, x, y, z]
45            // We rotate right and then zero out the 'w' (index 0)
46            let g_v = simd_swizzle!(g_raw, [3, 0, 1, 2]) * f32x4::from_array([0.0, 1.0, 1.0, 1.0]);
47
48            // Hamilton Product (q * g) for [w, x, y, z] layout:
49            // w_out = -x1*gx - y1*gy - z1*gz
50            // x_out =  w1*gx + y1*gz - z1*gy
51            // y_out =  w1*gy - x1*gz + z1*gx
52            // z_out =  w1*gz + x1*gy - y1*gx
53
54            // Row A: [w, w, w, w] * [0, gx, gy, gz]
55            let res = simd_swizzle!(q_v, [0, 0, 0, 0]) * g_v;
56
57            // Row B: [x, -x, -x, -x] * [gx, 0, gz, gy]
58            // (Using signs and swizzles to match the Hamilton rows)
59            let x_part = simd_swizzle!(q_v, [1, 1, 1, 1])
60                * simd_swizzle!(g_v, [1, 0, 3, 2])
61                * f32x4::from_array([-1.0, 1.0, -1.0, 1.0]);
62
63            // Row C: [y, y, -y, y] * [gy, gz, 0, gx]
64            let y_part = simd_swizzle!(q_v, [2, 2, 2, 2])
65                * simd_swizzle!(g_v, [2, 3, 0, 1])
66                * f32x4::from_array([-1.0, 1.0, 1.0, -1.0]);
67
68            // Row D: [z, z, z, -z] * [gz, gy, gx, 0]
69            let z_part = simd_swizzle!(q_v, [3, 3, 3, 3])
70                * simd_swizzle!(g_v, [3, 2, 1, 0])
71                * f32x4::from_array([-1.0, -1.0, 1.0, 1.0]);
72
73            let q_dot = (res + x_part + y_part + z_part) * f32x4::splat(0.5);
74
75            unsafe { core::mem::transmute(q_dot) }*/
76            let q_v = f32x4::from(q);
77            let g_raw = f32x4::from(gyro);
78
79            // Shift [x, y, z, pad] to [0, x, y, z] and zero the w lane
80            let g_v = simd_swizzle!(g_raw, [3, 0, 1, 2]) * f32x4::from_array([0.0, 1.0, 1.0, 1.0]);
81
82            // Parallel Hamilton Calculation
83            let w1 = simd_swizzle!(q_v, [0, 0, 0, 0]);
84            let x1 = simd_swizzle!(q_v, [1, 1, 1, 1]);
85            let y1 = simd_swizzle!(q_v, [2, 2, 2, 2]);
86            let z1 = simd_swizzle!(q_v, [3, 3, 3, 3]);
87
88            let g_w = g_v; // [0, gx, gy, gz]
89            let g_x = simd_swizzle!(g_v, [1, 0, 3, 2]); // [gx, 0, gz, gy]
90            let g_y = simd_swizzle!(g_v, [2, 3, 0, 1]); // [gy, gz, 0, gx]
91            let g_z = simd_swizzle!(g_v, [3, 2, 1, 0]); // [gz, gy, gx, 0]
92
93            let res = (w1 * g_w)
94                + (x1 * g_x * f32x4::from_array([-1.0, 1.0, -1.0, 1.0]))
95                + (y1 * g_y * f32x4::from_array([-1.0, 1.0, 1.0, -1.0]))
96                + (z1 * g_z * f32x4::from_array([-1.0, -1.0, 1.0, 1.0]));
97
98            let q_dot = res * f32x4::splat(0.5);
99            q_dot.into()
100        }
101        #[cfg(not(feature = "simd"))]
102        {
103            Quaternion {
104                w: (-q.x * gyro.x - q.y * gyro.y - q.z * gyro.z) * 0.5,
105                x: (q.w * gyro.x - q.z * gyro.y + q.y * gyro.z) * 0.5,
106                y: (q.z * gyro.x + q.w * gyro.y - q.x * gyro.z) * 0.5,
107                z: (-q.y * gyro.x + q.x * gyro.y + q.w * gyro.z) * 0.5,
108            }
109        }
110    }
111
112    /// Features of this implementation:
113    ///
114    /// 1. Parallel Throughput: Instead of 12 separate floating-point multiplications and 8 additions,
115    ///    the SIMD unit performs 3 vector multiplications and 2 vector additions.
116    ///
117    /// 2. Instruction Density: The `simd_swizzle!` maps directly to the VREV or VMOV instructions on the M33.
118    ///
119    /// 3. Register Reuse: `q_v` stays in its SIMD register the entire time.
120    ///    The compiler will likely use VFMA (Vector Fused Multiply-Add) to combine the terms,
121    ///    meaning this whole function could resolve in under 15 clock cycles.
122    ///
123    #[inline(always)]
124    fn madgwick_step_acc(
125        q: Quaternion<Self>,
126        acc: Vector3d<Self>,
127        max_acc_magnitude_squared: Self,
128    ) -> Quaternion<Self> {
129        use num_traits::Zero;
130        use vqm::SqrtMethods;
131
132        let acc_magnitude_squared = acc.norm_squared();
133        // Acceleration is an unreliable indicator of orientation when in high-g maneuvers,
134        // so exclude it from the calculation in these cases
135        let mut a = acc;
136        if acc_magnitude_squared > max_acc_magnitude_squared || acc_magnitude_squared == 0.0 {
137            a = Vector3d::zero();
138        } else {
139            a *= acc_magnitude_squared.sqrt_reciprocal();
140        }
141        #[cfg(feature = "simd")]
142        {
143            let q_v = f32x4::from(q);
144
145            // 1. Calculate wz_common
146            let wz_common = 2.0 * (q.x * q.x + q.y * q.y);
147
148            // 2. Calculate xy_common (w*w + z*z - 1.0 + 2.0*wz_common + a.z)
149            let xy_common = 2.0 * (q.w * q.w + q.z * q.z - 1.0 + 2.0 * wz_common + a.z);
150
151            // 3. Calculate common term: [w, x, y, z] * [qq, xy_common, xy_common, qq]
152            let common_scalars = f32x4::from_array([wz_common, xy_common, xy_common, wz_common]);
153            let common = q_v * common_scalars;
154
155            // 4. Calculate ax term: [y, -z, w, -x] * ax
156            // Term 2: [y, -z, w, -x] * ax
157            // Indices needed: y=2, z=3, w=0, x=1
158            let ax_q_swiz = simd_swizzle!(q_v, [2, 3, 0, 1]);
159            let ax_signs = f32x4::from_array([1.0, -1.0, 1.0, -1.0]);
160            let ax = (ax_q_swiz * ax_signs) * f32x4::splat(a.x);
161
162            // 5. Calculate ay term: [-x, -w, -z, -y] * ay
163            // Term 3: [-x, -w, -z, -y] * ay
164            // Indices needed: x=1, w=0, z=3, y=2
165            let ay_q_swiz = simd_swizzle!(q_v, [1, 0, 3, 2]);
166            let ay_signs = f32x4::splat(-1.0);
167            let ay = (ay_q_swiz * ay_signs) * f32x4::splat(a.y);
168
169            // 6. Combine: step = common + ax + ay
170            let ret_v = common + ax + ay;
171
172            ret_v.into()
173        }
174        #[cfg(not(feature = "simd"))]
175        {
176            let wz_common = 2.0 * (q.x * q.x + q.y * q.y);
177            let xy_common = 2.0 * (q.w * q.w + q.z * q.z - 1.0 + 2.0 * wz_common + a.z);
178            Quaternion {
179                w: q.w * wz_common + q.y * a.x - q.x * a.y,
180                x: q.x * xy_common - q.z * a.x - q.w * a.y,
181                y: q.y * xy_common + q.w * a.x - q.z * a.y,
182                z: q.z * wz_common - q.x * a.x - q.y * a.y,
183            }
184        }
185    }
186
187    #[inline(always)]
188    fn madgwick_step_acc_mag(
189        q: Quaternion<Self>,
190        acc: Vector3d<Self>,
191        mag: Vector3d<Self>,
192        max_acc_magnitude_squared: Self,
193    ) -> Quaternion<Self> {
194        use num_traits::Zero;
195        use vqm::SqrtMethods;
196
197        let acc_magnitude_squared = acc.norm_squared();
198        // Acceleration is an unreliable indicator of orientation when in high-g maneuvers,
199        // so exclude it from the calculation in these cases
200        let mut a = acc;
201        if acc_magnitude_squared > max_acc_magnitude_squared || acc_magnitude_squared == 0.0 {
202            a = Vector3d::zero();
203        } else {
204            a *= acc_magnitude_squared.sqrt_reciprocal();
205        }
206
207        let m = mag.normalize();
208
209        // make copies of the components of q to simplify the algebraic expressions
210        let q0 = q.w;
211        let q1 = q.x;
212        let q2 = q.y;
213        let q3 = q.z;
214        // Auxiliary variables to avoid repeated arithmetic
215        let q0q0 = q0 * q0;
216        let q0q1 = q0 * q1;
217        let q0q2 = q0 * q2;
218        let q0q3 = q0 * q3;
219        let q1q1 = q1 * q1;
220        let q1q2 = q1 * q2;
221        let q1q3 = q1 * q3;
222        let q2q2 = q2 * q2;
223        let q2q3 = q2 * q3;
224        let q3q3 = q3 * q3;
225
226        let q1q1_plus_q2q2 = q1q1 + q2q2;
227        let q2q2_plus_q3q3 = q2q2 + q3q3;
228
229        // Reference direction of Earth's magnetic field
230        let h = Vector3d {
231            x: m.x * (q0q0 + q1q1 - q2q2_plus_q3q3) + 2.0 * (m.y * (q1q2 - q0q3) + m.z * (q0q2 + q1q3)),
232            y: (m.x * (q0q3 + q1q2) + m.y * (q0q0 - q1q1 + q2q2 - q3q3) + m.z * (q2q3 - q0q1)) * 2.0,
233            z: 0.0,
234        };
235
236        let bx_bx = h.x * h.x + h.y * h.y;
237        let b = Vector3d {
238            x: bx_bx.sqrt(),
239            y: 0.0,
240            z: 2.0 * (m.x * (q1q3 - q0q2) + m.y * (q0q1 + q2q3)) + m.z * (q0q0 - q1q1_plus_q2q2 + q3q3),
241        };
242
243        let a_dash = Vector3d { x: a.x + m.x * b.z, y: a.y + m.y * b.z, z: 0.0 };
244        let bz_bz = b.z * b.z;
245        let _4bx_bz = 4.0 * b.x * b.z;
246
247        let m_bx = m * b.x;
248        let mz_bz = m.z * b.z;
249
250        let sum_squares_minus_one = q0q0 + q1q1_plus_q2q2 + q3q3 - 1.0;
251        let xy_common = sum_squares_minus_one + q1q1_plus_q2q2;
252        let yz_common = sum_squares_minus_one + q2q2_plus_q3q3;
253        let wz_common = q1q1_plus_q2q2 * (1.0 + bz_bz) + bx_bx;
254
255        // Gradient decent algorithm corrective step
256        #[allow(clippy::used_underscore_binding)]
257        Quaternion {
258            w: q0 * 2.0 * (wz_common * q2q2_plus_q3q3) - q1 * a_dash.y
259                + q2 * (a_dash.x - m_bx.z)
260                + q3 * (m_bx.y - _4bx_bz * q0q1),
261
262            x: -q0 * a_dash.y + q1 * 2.0 * (xy_common * (1.0 + 2.0 * bz_bz) + mz_bz + bx_bx * q2q2_plus_q3q3 + a.z)
263                - q2 * m_bx.y
264                - q3 * (a_dash.x + m_bx.z + _4bx_bz * (0.5 * sum_squares_minus_one + q1q1)),
265
266            y: q0 * (a_dash.x - m_bx.z) - q1 * m_bx.y
267                + q2 * 2.0 * (xy_common * (1.0 + bz_bz) + mz_bz + m_bx.x + bx_bx * yz_common + a.z)
268                - q3 * (a_dash.y + _4bx_bz * q1q2),
269
270            z: q0 * m_bx.y - q1 * (a_dash.x + m_bx.z + _4bx_bz * (0.5 * sum_squares_minus_one + q3q3)) - q2 * a_dash.y
271                + q3 * 2.0 * (wz_common + m_bx.x * yz_common),
272        }
273        .normalize()
274    }
275}
276
277impl SensorFusionMath for f64 {
278    #[inline(always)]
279    fn estimate_gravity(q: Quaternion<Self>) -> Vector3d<Self> {
280        Vector3d {
281            x: 2.0 * (q.x * q.z - q.w * q.y),
282            y: 2.0 * (q.y * q.z + q.w * q.x),
283            z: q.w * q.w - q.x * q.x - q.y * q.y + q.z * q.z,
284        }
285    }
286
287    #[inline(always)]
288    fn derivative(q: Quaternion<Self>, gyro_rps: Vector3d<Self>) -> Quaternion<Self> {
289        Quaternion {
290            w: (-q.x * gyro_rps.x - q.y * gyro_rps.y - q.z * gyro_rps.z) * 0.5,
291            x: (q.w * gyro_rps.x + q.y * gyro_rps.z - q.z * gyro_rps.y) * 0.5,
292            y: (q.w * gyro_rps.y - q.x * gyro_rps.z + q.z * gyro_rps.x) * 0.5,
293            z: (q.w * gyro_rps.z + q.x * gyro_rps.y - q.y * gyro_rps.x) * 0.5,
294        }
295    }
296
297    #[inline(always)]
298    fn madgwick_step_acc(
299        q: Quaternion<Self>,
300        acc: Vector3d<Self>,
301        max_acc_magnitude_squared: Self,
302    ) -> Quaternion<Self> {
303        use num_traits::Zero;
304        use vqm::SqrtMethods;
305
306        let acc_magnitude_squared = acc.norm_squared();
307        // Acceleration is an unreliable indicator of orientation when in high-g maneuvers,
308        // so exclude it from the calculation in these cases
309        let mut a = acc;
310        if acc_magnitude_squared > max_acc_magnitude_squared || acc_magnitude_squared == 0.0 {
311            a = Vector3d::zero();
312        } else {
313            a *= acc_magnitude_squared.sqrt_reciprocal();
314        }
315        let wz_common = 2.0 * (q.x * q.x + q.y * q.y);
316        let xy_common = 2.0 * (q.w * q.w + q.z * q.z - 1.0 + 2.0 * wz_common + a.z);
317        Quaternion {
318            w: q.w * wz_common + q.y * a.x - q.x * a.y,
319            x: q.x * xy_common - q.z * a.x - q.w * a.y,
320            y: q.y * xy_common + q.w * a.x - q.z * a.y,
321            z: q.z * wz_common - q.x * a.x - q.y * a.y,
322        }
323    }
324
325    /// Not implemented for `f64`.
326    #[inline(always)]
327    fn madgwick_step_acc_mag(
328        q: Quaternion<Self>,
329        _acc: Vector3d<Self>,
330        _mag: Vector3d<Self>,
331        _max_acc_magnitude_squared: Self,
332    ) -> Quaternion<Self> {
333        q
334    }
335}