Skip to main content

viam_rust_utils/spatialmath/
utils.rs

1use float_cmp::{ApproxEq, F64Margin};
2use nalgebra::{Quaternion, UnitQuaternion, UnitVector3, Vector3};
3
4const ANGLE_ACCEPTANCE: f64 = 0.0001;
5
6#[repr(C)]
7#[derive(Clone, Copy, Debug)]
8pub struct EulerAngles {
9    pub roll: f64,
10    pub pitch: f64,
11    pub yaw: f64,
12}
13
14impl EulerAngles {
15    pub fn new(roll: f64, pitch: f64, yaw: f64) -> Self {
16        EulerAngles { roll, pitch, yaw }
17    }
18
19    /// Converts a quaternion into euler angles (in radians). The euler angles are
20    /// represented according to the Tait-Bryan formalism and applied
21    /// in the Z-Y'-X" order (where Z -> yaw, Y -> pitch, X -> roll).
22    pub fn from_quaternion(quat: &Quaternion<f64>) -> Self {
23        // get a normalized version of the quaternion
24        let norm_quat = quat.normalize();
25
26        // calculate yaw
27        let yaw_sin_pitch_cos: f64 =
28            2.0 * ((norm_quat.w * norm_quat.k) + (norm_quat.i * norm_quat.j));
29        let yaw_cos_pitch_cos: f64 =
30            1.0 - 2.0 * ((norm_quat.j * norm_quat.j) + (norm_quat.k * norm_quat.k));
31        let yaw = yaw_sin_pitch_cos.atan2(yaw_cos_pitch_cos);
32
33        // calculate pitch and roll
34        let pitch_sin: f64 = 2.0 * ((norm_quat.w * norm_quat.j) - (norm_quat.k * norm_quat.i));
35        let pitch: f64;
36        let roll: f64;
37        // for a pitch that is π / 2, we experience gimbal lock
38        // and must calculate roll based on the real rotation and yaw
39        if pitch_sin.abs() >= 1.0 {
40            pitch = (std::f64::consts::PI / 2.0).copysign(pitch_sin);
41            roll = (2.0 * norm_quat.i.atan2(norm_quat.w)) + yaw.copysign(pitch_sin);
42        } else {
43            pitch = pitch_sin.asin();
44            let roll_sin_pitch_cos =
45                2.0 * ((norm_quat.w * norm_quat.i) + (norm_quat.j * norm_quat.k));
46            let roll_cos_pitch_cos =
47                1.0 - 2.0 * ((norm_quat.i * norm_quat.i) + (norm_quat.j * norm_quat.j));
48            roll = roll_sin_pitch_cos.atan2(roll_cos_pitch_cos);
49        }
50
51        EulerAngles { roll, pitch, yaw }
52    }
53}
54
55impl From<Quaternion<f64>> for EulerAngles {
56    fn from(quat: Quaternion<f64>) -> Self {
57        // get a normalized version of the quaternion
58        let norm_quat = quat.normalize();
59
60        // calculate yaw
61        let yaw_sin_pitch_cos: f64 =
62            2.0 * ((norm_quat.w * norm_quat.k) + (norm_quat.i * norm_quat.j));
63        let yaw_cos_pitch_cos: f64 =
64            1.0 - 2.0 * ((norm_quat.j * norm_quat.j) + (norm_quat.k * norm_quat.k));
65        let yaw = yaw_sin_pitch_cos.atan2(yaw_cos_pitch_cos);
66
67        // calculate pitch and roll
68        let pitch_sin: f64 = 2.0 * ((norm_quat.w * norm_quat.j) - (norm_quat.k * norm_quat.i));
69        let pitch: f64;
70        let roll: f64;
71        // for a pitch that is π / 2, we experience gimbal lock
72        // and must calculate roll based on the real rotation and yaw
73        if pitch_sin.abs() >= 1.0 {
74            pitch = (std::f64::consts::PI / 2.0).copysign(pitch_sin);
75            roll = (2.0 * norm_quat.i.atan2(norm_quat.w)) + yaw.copysign(pitch_sin);
76        } else {
77            pitch = pitch_sin.asin();
78            let roll_sin_pitch_cos =
79                2.0 * ((norm_quat.w * norm_quat.i) + (norm_quat.j * norm_quat.k));
80            let roll_cos_pitch_cos =
81                1.0 - 2.0 * ((norm_quat.i * norm_quat.i) + (norm_quat.j * norm_quat.j));
82            roll = roll_sin_pitch_cos.atan2(roll_cos_pitch_cos);
83        }
84
85        Self { roll, pitch, yaw }
86    }
87}
88
89#[repr(C)]
90#[derive(Clone, Copy, Debug)]
91pub struct AxisAngle {
92    pub axis: Vector3<f64>,
93    pub theta: f64,
94}
95
96impl AxisAngle {
97    pub fn new(x: f64, y: f64, z: f64, theta: f64) -> Self {
98        AxisAngle {
99            axis: Vector3::new(x, y, z),
100            theta,
101        }
102    }
103}
104
105impl TryFrom<Quaternion<f64>> for AxisAngle {
106    type Error = ();
107
108    fn try_from(quat: Quaternion<f64>) -> Result<Self, Self::Error> {
109        let unit_quat = UnitQuaternion::from_quaternion(quat);
110        let axis_opt = unit_quat.axis();
111        let angle = unit_quat.angle();
112        match axis_opt {
113            Some(value) => Ok(Self::new(value[0], value[1], value[2], angle)),
114            None => Err(()),
115        }
116    }
117}
118
119#[repr(C)]
120#[derive(Clone, Copy, Debug)]
121pub struct OrientationVector {
122    pub o_vector: UnitVector3<f64>,
123    pub theta: f64,
124}
125
126impl OrientationVector {
127    pub fn new(o_x: f64, o_y: f64, o_z: f64, theta: f64) -> Self {
128        let o_vector = UnitVector3::new_normalize(Vector3::new(o_x, o_y, o_z));
129        OrientationVector { o_vector, theta }
130    }
131
132    pub fn to_quaternion(&self) -> Quaternion<f64> {
133        let lat = self.o_vector.z.acos();
134        // Longitude is undefined inside the pole band; rdk's OrientationVector.Quaternion
135        // pins it at both poles via 1 - abs(OZ).
136        let lon = match self.o_vector.z {
137            val if 1.0 - val.abs() > ANGLE_ACCEPTANCE => self.o_vector.y.atan2(self.o_vector.x),
138            _ => 0.0,
139        };
140
141        // convert angles as euler angles (lon, lat, theta) to quaternion
142        // using the zyz rotational order
143        let s: [f64; 3] = [
144            (lon / 2.0).sin(),
145            (lat / 2.0).sin(),
146            (self.theta / 2.0).sin(),
147        ];
148
149        let c: [f64; 3] = [
150            (lon / 2.0).cos(),
151            (lat / 2.0).cos(),
152            (self.theta / 2.0).cos(),
153        ];
154
155        let real = c[0] * c[1] * c[2] - s[0] * c[1] * s[2];
156        let i = c[0] * s[1] * s[2] - s[0] * s[1] * c[2];
157        let j = c[0] * s[1] * c[2] + s[0] * s[1] * s[2];
158        let k = s[0] * c[1] * c[2] + c[0] * c[1] * s[2];
159
160        Quaternion::new(real, i, j, k)
161    }
162}
163
164impl ApproxEq for OrientationVector {
165    type Margin = F64Margin;
166
167    fn approx_eq<M: Into<Self::Margin>>(self, other: Self, margin: M) -> bool {
168        let margin = margin.into();
169        let vec_diff = self.o_vector.into_inner() - other.o_vector.into_inner();
170        vec_diff.norm_squared().approx_eq(0.0, margin) && self.theta.approx_eq(other.theta, margin)
171    }
172}
173
174impl From<Quaternion<f64>> for OrientationVector {
175    fn from(quat: Quaternion<f64>) -> Self {
176        let x_quat = Quaternion::new(0.0, -1.0, 0.0, 0.0);
177        let z_quat = Quaternion::new(0.0, 0.0, 0.0, 1.0);
178
179        let conj = quat.conjugate();
180        let new_x = (quat * x_quat) * conj;
181        let new_z = (quat * z_quat) * conj;
182
183        let o_vector = UnitVector3::new_normalize(new_z.imag());
184        // let theta = orientation_vector_theta_from_rotated_axes(new_x, new_z);
185        let theta = match 1.0 - new_z.k.abs() > ANGLE_ACCEPTANCE {
186            true => {
187                let new_z_imag = new_z.imag();
188                let new_x_imag = new_x.imag();
189                let z_imag_axis = Vector3::z_axis();
190
191                let normal_1 = new_z_imag.cross(&new_x_imag);
192                let normal_2 = new_z_imag.cross(&z_imag_axis);
193                let cos_theta_cand = normal_1.dot(&normal_2) / (normal_1.norm() * normal_2.norm());
194                let cos_theta = match cos_theta_cand {
195                    val if val < -1.0 => -1.0,
196                    val if val > 1.0 => 1.0,
197                    _ => cos_theta_cand,
198                };
199
200                match cos_theta.acos() {
201                    val if val > ANGLE_ACCEPTANCE => {
202                        let new_z_imag_unit = UnitVector3::new_normalize(new_z_imag);
203                        let rot_quat_unit =
204                            UnitQuaternion::from_axis_angle(&new_z_imag_unit, -1.0 * val);
205                        let rot_quat = rot_quat_unit.quaternion();
206                        let z_axis_quat = Quaternion::new(0.0, 0.0, 0.0, 1.0);
207                        let test_z = (rot_quat * z_axis_quat) * rot_quat.conjugate();
208                        let test_z_imag = test_z.imag();
209
210                        let normal_3 = new_z_imag.cross(&test_z_imag);
211                        let cos_test =
212                            normal_1.dot(&normal_3) / (normal_3.norm() * normal_1.norm());
213                        match cos_test {
214                            val2 if (1.0 - val2) < (ANGLE_ACCEPTANCE * ANGLE_ACCEPTANCE) => {
215                                -1.0 * val
216                            }
217                            _ => val,
218                        }
219                    }
220                    _ => 0.0,
221                }
222            }
223            _ => match new_z.k {
224                val if val < 0.0 => -1.0 * new_x.j.atan2(new_x.i),
225                _ => -1.0 * new_x.j.atan2(new_x.i * -1.0),
226            },
227        };
228        Self { o_vector, theta }
229    }
230}
231
232pub fn rotate_vector_by_quaternion(quat: &Quaternion<f64>, vector: &Vector3<f64>) -> Vector3<f64> {
233    let quat_vec = Vector3::new(quat.i, quat.j, quat.k);
234    let quat_real = quat.w;
235    (2.0 * quat_vec.dot(vector) * quat_vec)
236        + ((quat_real * quat_real - quat_vec.norm_squared()) * vector)
237        + (2.0 * quat_real) * quat_vec.cross(vector)
238}
239
240#[cfg(test)]
241mod tests {
242    use float_cmp::assert_approx_eq;
243    use nalgebra::{Quaternion, Vector3};
244
245    use super::{rotate_vector_by_quaternion, EulerAngles, OrientationVector};
246
247    fn get_quaternion_diff_norm(quat1: &Quaternion<f64>, quat2: &Quaternion<f64>) -> f64 {
248        let quat_diff = quat1.coords - quat2.coords;
249        quat_diff.norm_squared()
250    }
251
252    fn get_vector_diff_norm(vec1: &Vector3<f64>, vec2: &Vector3<f64>) -> f64 {
253        let vec_diff = vec2 - vec1;
254        vec_diff.norm_squared()
255    }
256
257    #[test]
258    fn quaternion_to_orientation_vector_works() {
259        let quat = Quaternion::new(0.7071067811865476, 0.7071067811865476, 0.0, 0.0);
260        let expected_ov = OrientationVector::new(0.0, -1.0, 0.0, 1.5707963267948966);
261        let calc_ov: OrientationVector = quat.into();
262        assert_approx_eq!(OrientationVector, calc_ov, expected_ov);
263
264        let quat2 = Quaternion::new(0.7071067811865476, -0.7071067811865476, 0.0, 0.0);
265        let expected_ov2 = OrientationVector::new(0.0, 1.0, 0.0, -1.5707963267948966);
266        let calc_ov2: OrientationVector = quat2.into();
267        assert_approx_eq!(OrientationVector, calc_ov2, expected_ov2);
268
269        let quat3 = Quaternion::new(0.96, 0.0, -0.28, 0.0);
270        let expected_ov3 =
271            OrientationVector::new(-0.5376, 0.0, 0.8432, -1.0 * std::f64::consts::PI);
272        let calc_ov3: OrientationVector = quat3.into();
273        assert_approx_eq!(OrientationVector, calc_ov3, expected_ov3);
274
275        let quat4 = Quaternion::new(0.96, 0.0, 0.0, -0.28);
276        let expected_ov4 = OrientationVector::new(0.0, 0.0, 1.0, -0.5675882184166557);
277        let calc_ov4: OrientationVector = quat4.into();
278        assert_approx_eq!(OrientationVector, calc_ov4, expected_ov4);
279
280        let quat5 = Quaternion::new(0.96, -0.28, 0.0, 0.0);
281        let expected_ov5 = OrientationVector::new(0.0, 0.5376, 0.8432, -1.5707963267948966);
282        let calc_ov5: OrientationVector = quat5.into();
283        assert_approx_eq!(OrientationVector, calc_ov5, expected_ov5);
284
285        let quat6 = Quaternion::new(0.96, 0.28, 0.0, 0.0);
286        let expected_ov6 = OrientationVector::new(0.0, -0.5376, 0.8432, 1.5707963267948966);
287        let calc_ov6: OrientationVector = quat6.into();
288        assert_approx_eq!(OrientationVector, calc_ov6, expected_ov6);
289
290        let quat7 = Quaternion::new(0.5, -0.5, -0.5, -0.5);
291        let expected_ov7 = OrientationVector::new(0.0, 1.0, 0.0, -1.0 * std::f64::consts::PI);
292        let calc_ov7: OrientationVector = quat7.into();
293        assert_approx_eq!(OrientationVector, calc_ov7, expected_ov7);
294
295        let quat8 = Quaternion::new(
296            0.816632212270443,
297            -0.17555966025413142,
298            0.39198397193979817,
299            0.3855375485164001,
300        );
301        let expected_ov8 = OrientationVector::new(
302            0.5048437942940054,
303            0.5889844266763397,
304            0.631054742867507,
305            0.02,
306        );
307        let calc_ov8: OrientationVector = quat8.into();
308        assert_approx_eq!(OrientationVector, calc_ov8, expected_ov8, epsilon = 0.0001);
309    }
310
311    #[test]
312    fn orientation_vector_to_quaternion_works() {
313        let ov = OrientationVector::new(0.0, -1.0, 0.0, 1.5707963267948966);
314        let expected_quat = Quaternion::new(0.7071067811865476, 0.7071067811865476, 0.0, 0.0);
315        let calc_quat = ov.to_quaternion();
316        let mut diff = get_quaternion_diff_norm(&expected_quat, &calc_quat);
317        assert_approx_eq!(f64, diff, 0.0);
318
319        let ov2 = OrientationVector::new(0.0, 1.0, 0.0, -1.5707963267948966);
320        let expected_quat2 = Quaternion::new(0.7071067811865476, -0.7071067811865476, 0.0, 0.0);
321        let calc_quat2 = ov2.to_quaternion();
322        diff = get_quaternion_diff_norm(&expected_quat2, &calc_quat2);
323        assert_approx_eq!(f64, diff, 0.0);
324
325        let ov3 = OrientationVector::new(-0.5376, 0.0, 0.8432, -1.0 * std::f64::consts::PI);
326        let expected_quat3 = Quaternion::new(0.96, 0.0, -0.28, 0.0);
327        let calc_quat3 = ov3.to_quaternion();
328        diff = get_quaternion_diff_norm(&expected_quat3, &calc_quat3);
329        assert_approx_eq!(f64, diff, 0.0);
330
331        let ov4 = OrientationVector::new(0.0, 0.0, 1.0, -0.5675882184166557);
332        let expected_quat4 = Quaternion::new(0.96, 0.0, 0.0, -0.28);
333        let calc_quat4 = ov4.to_quaternion();
334        diff = get_quaternion_diff_norm(&expected_quat4, &calc_quat4);
335        assert_approx_eq!(f64, diff, 0.0);
336
337        let ov5 = OrientationVector::new(0.0, 0.5376, 0.8432, -1.5707963267948966);
338        let expected_quat5 = Quaternion::new(0.96, -0.28, 0.0, 0.0);
339        let calc_quat5 = ov5.to_quaternion();
340        diff = get_quaternion_diff_norm(&expected_quat5, &calc_quat5);
341        assert_approx_eq!(f64, diff, 0.0);
342
343        let ov6 = OrientationVector::new(0.0, -0.5376, 0.8432, 1.5707963267948966);
344        let expected_quat6 = Quaternion::new(0.96, 0.28, 0.0, 0.0);
345        let calc_quat6 = ov6.to_quaternion();
346        diff = get_quaternion_diff_norm(&expected_quat6, &calc_quat6);
347        assert_approx_eq!(f64, diff, 0.0);
348
349        let ov7 = OrientationVector::new(0.0, 1.0, 0.0, -1.0 * std::f64::consts::PI);
350        let expected_quat7 = Quaternion::new(0.5, -0.5, -0.5, -0.5);
351        let calc_quat7 = ov7.to_quaternion();
352        diff = get_quaternion_diff_norm(&expected_quat7, &calc_quat7);
353        assert_approx_eq!(f64, diff, 0.0);
354
355        let ov8 = OrientationVector::new(
356            0.5048437942940054,
357            0.5889844266763397,
358            0.631054742867507,
359            0.02,
360        );
361        let expected_quat8 = Quaternion::new(
362            0.816632212270443,
363            -0.17555966025413142,
364            0.39198397193979817,
365            0.3855375485164001,
366        );
367        let calc_quat8 = ov8.to_quaternion();
368        diff = get_quaternion_diff_norm(&expected_quat8, &calc_quat8);
369        assert_approx_eq!(f64, diff, 0.0);
370    }
371
372    // Expected quaternions are computed independently of this module, as the quaternion
373    // product Rz(lon)·Ry(lat)·Rz(theta) with lon pinned to 0.
374    #[test]
375    fn orientation_vector_to_quaternion_pins_longitude_at_south_pole() {
376        let o_z = -0.99999; // 1 - |o_z| = 1e-5, inside the 1e-4 band
377        let r = 0.004472124774634615; // sin(lat), so the vectors below are already unit length
378        let theta = 0.5;
379
380        let expected_quat = Quaternion::new(
381            0.002166554039183957,
382            0.24740334074385167,
383            0.9689099994265626,
384            0.0005532120707944279,
385        );
386
387        let ov_45 =
388            OrientationVector::new(0.0031622697544564974, 0.0031622697544564965, o_z, theta);
389        let diff_45 = get_quaternion_diff_norm(&expected_quat, &ov_45.to_quaternion());
390        assert_approx_eq!(f64, diff_45, 0.0);
391
392        let ov_90 = OrientationVector::new(0.0, r, o_z, theta);
393        let diff_90 = get_quaternion_diff_norm(&expected_quat, &ov_90.to_quaternion());
394        assert_approx_eq!(f64, diff_90, 0.0);
395    }
396
397    // Pinning must hold at o_z = +1 too: a sign-sensitive guard satisfies one pole only.
398    #[test]
399    fn orientation_vector_to_quaternion_pins_longitude_at_north_pole() {
400        let o_z = 0.99999;
401        let r = 0.004472124774634615;
402        let theta = 0.5;
403
404        let expected_quat = Quaternion::new(
405            0.9689099994265626,
406            0.0005532120707944427,
407            0.0021665540391840148,
408            0.24740334074385167,
409        );
410
411        let ov_45 =
412            OrientationVector::new(0.0031622697544564974, 0.0031622697544564965, o_z, theta);
413        let diff_45 = get_quaternion_diff_norm(&expected_quat, &ov_45.to_quaternion());
414        assert_approx_eq!(f64, diff_45, 0.0);
415
416        let ov_90 = OrientationVector::new(0.0, r, o_z, theta);
417        let diff_90 = get_quaternion_diff_norm(&expected_quat, &ov_90.to_quaternion());
418        assert_approx_eq!(f64, diff_90, 0.0);
419    }
420
421    // Tool z near the south pole at 45 degrees azimuth. The residual is the intrinsic
422    // pinning bound and shrinks with 1 - |o_z|, hence the per-case epsilons.
423    #[test]
424    fn quaternion_orientation_vector_round_trip_near_south_pole() {
425        let quat = Quaternion::new(
426            0.0017899298388043034,
427            -0.1422149214452539,
428            0.9898332769301709,
429            0.0013402056454645229,
430        );
431        let ov: OrientationVector = quat.into();
432        let diff = get_quaternion_diff_norm(&quat, &ov.to_quaternion());
433        assert_approx_eq!(f64, diff, 0.0, epsilon = 1e-5);
434
435        let closer_quat = Quaternion::new(
436            1.7899298135059344e-05,
437            -0.14221527694833697,
438            0.9898357512751839,
439            1.34020562652243e-05,
440        );
441        let closer_ov: OrientationVector = closer_quat.into();
442        let closer_diff = get_quaternion_diff_norm(&closer_quat, &closer_ov.to_quaternion());
443        assert_approx_eq!(f64, closer_diff, 0.0, epsilon = 1e-9);
444    }
445
446    #[test]
447    fn euler_angles_from_quaternion_works() {
448        let quat = Quaternion::new(
449            0.2705980500730985,
450            -0.6532814824381882,
451            0.27059805007309856,
452            0.6532814824381883,
453        );
454        let euler_angles: EulerAngles = quat.into();
455        assert_approx_eq!(f64, euler_angles.pitch, std::f64::consts::PI / 2.0);
456        assert_approx_eq!(f64, euler_angles.yaw, std::f64::consts::PI);
457        assert_approx_eq!(f64, euler_angles.roll, std::f64::consts::PI / 4.0);
458
459        let quat2 = Quaternion::new(
460            0.4619397662556435,
461            -0.19134171618254486,
462            0.4619397662556434,
463            0.7325378163287418,
464        );
465        let euler_angles2: EulerAngles = quat2.into();
466        assert_approx_eq!(f64, euler_angles2.pitch, std::f64::consts::PI / 4.0);
467        assert_approx_eq!(f64, euler_angles2.yaw, 3.0 * std::f64::consts::PI / 4.0);
468        assert_approx_eq!(f64, euler_angles2.roll, std::f64::consts::PI / 4.0);
469    }
470
471    #[test]
472    fn rotation_by_quaternion_works() {
473        // rotation of (0,0,1) by 90 degrees about (0,1,0)
474        let quat = Quaternion::new(0.7071068, 0.0, 0.7071068, 0.0);
475        let vector = Vector3::new(0.0, 0.0, 1.0);
476        let expected_vector = Vector3::new(1.0, 0.0, 0.0);
477        let rotated_vector = rotate_vector_by_quaternion(&quat, &vector);
478        let diff = get_vector_diff_norm(&expected_vector, &rotated_vector);
479        assert_approx_eq!(f64, diff, 0.0, epsilon = 0.0001);
480
481        // rotation of (4.5, 1.3, 2.0) by 175 degrees about (2,3,4)
482        let quat2 = Quaternion::new(0.0436194, 0.3710372, 0.5565558, 0.7420744);
483        let vector2 = Vector3::new(4.5, 1.3, 2.0);
484        let expected_vector2 = Vector3::new(-1.593, 3.247, 3.586);
485        let rotated_vector2 = rotate_vector_by_quaternion(&quat2, &vector2);
486        let diff = get_vector_diff_norm(&expected_vector2, &rotated_vector2);
487        assert_approx_eq!(f64, diff, 0.0, epsilon = 0.0001);
488    }
489}