Skip to main content

hoomd_vector/
quaternion.rs

1// Copyright (c) 2024-2026 The Regents of the University of Michigan.
2// Part of hoomd-rs, released under the BSD 3-Clause License.
3
4//! Implement [`Quaternion`] and related types.
5use serde::{Deserialize, Serialize};
6use std::{
7    fmt,
8    ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign},
9};
10
11use approxim::approx_derive::RelativeEq;
12use rand::{
13    Rng, RngExt,
14    distr::{Distribution, StandardUniform},
15};
16use rand_distr::StandardNormal;
17
18use crate::{Cartesian, Cross, Error, InnerProduct, Rotate, Rotation, RotationMatrix, Unit};
19
20/// Extended complex number.
21///
22/// A quaternion has a real value and three complex values, represented by scalar and 3-vector
23/// respectively:
24/// ```math
25/// \mathbf{q} = (s, \vec{v})
26/// ```
27///
28/// Looking for the quaternion representation of 3D rotations? See [`Versor`].
29///
30/// ## Constructing quaternions
31///
32/// Create a quaternion with an array of coordinates (`[scalar, vector_0, vector_1, vector_2]`).
33/// ```
34/// use hoomd_vector::Quaternion;
35///
36/// let q = Quaternion::from([1.0, 2.0, 3.0, 4.0]);
37/// assert_eq!(q.scalar, 1.0);
38/// assert_eq!(q.vector, [2.0, 3.0, 4.0].into());
39/// ```
40///
41/// ## Quaternion properties
42///
43/// Compute a quaternion's norm:
44/// ```
45/// use hoomd_vector::Quaternion;
46///
47/// let q = Quaternion::from([3.0, 0.0, 4.0, 0.0]);
48/// let norm = q.norm();
49/// assert_eq!(norm, 5.0);
50/// ```
51///
52/// Form the conjugate:
53/// ```
54/// use hoomd_vector::Quaternion;
55///
56/// let q = Quaternion::from([1.0, 2.0, 3.0, 4.0]);
57/// let q_star = q.conjugate();
58/// assert_eq!(q_star, [1.0, -2.0, -3.0, -4.0].into());
59/// ```
60///
61/// ## Operating on quaternions
62///
63/// All operation examples use the following two quaternions:
64/// ```
65/// use hoomd_vector::Quaternion;
66///
67/// let a = Quaternion::from([1.0, -2.0, 6.0, -4.0]);
68/// let b = Quaternion::from([-2.0, 6.0, 4.0, 1.0]);
69/// ```
70///
71/// Addition:
72///
73/// ```
74/// # use hoomd_vector::Quaternion;
75/// # let a = Quaternion::from([1.0, -2.0, 6.0, -4.0]);
76/// # let b = Quaternion::from([-2.0, 6.0, 4.0, 1.0]);
77/// let c = a + b;
78/// assert_eq!(c, [-1.0, 4.0, 10.0, -3.0].into());
79/// ```
80///
81/// ```
82/// # use hoomd_vector::Quaternion;
83/// # let mut a = Quaternion::from([1.0, -2.0, 6.0, -4.0]);
84/// # let b = Quaternion::from([-2.0, 6.0, 4.0, 1.0]);
85/// a += b;
86/// assert_eq!(a, [-1.0, 4.0, 10.0, -3.0].into());
87/// ```
88///
89/// Subtraction:
90///
91/// ```
92/// # use hoomd_vector::Quaternion;
93/// # let a = Quaternion::from([1.0, -2.0, 6.0, -4.0]);
94/// # let b = Quaternion::from([-2.0, 6.0, 4.0, 1.0]);
95/// let c = a - b;
96/// assert_eq!(c, [3.0, -8.0, 2.0, -5.0].into());
97/// ```
98///
99/// ```
100/// # use hoomd_vector::Quaternion;
101/// # let mut a = Quaternion::from([1.0, -2.0, 6.0, -4.0]);
102/// # let b = Quaternion::from([-2.0, 6.0, 4.0, 1.0]);
103/// a -= b;
104/// assert_eq!(a, [3.0, -8.0, 2.0, -5.0].into());
105/// ```
106///
107/// Multiplication by a scalar:
108///
109/// ```
110/// # use hoomd_vector::Quaternion;
111/// # let a = Quaternion::from([1.0, -2.0, 6.0, -4.0]);
112/// let c = a * 2.0;
113/// assert_eq!(c, [2.0, -4.0, 12.0, -8.0].into());
114/// ```
115///
116/// ```
117/// # use hoomd_vector::Quaternion;
118/// # let mut a = Quaternion::from([1.0, -2.0, 6.0, -4.0]);
119/// # let b = Quaternion::from([-2.0, 6.0, 4.0, 1.0]);
120/// a *= 2.0;
121/// assert_eq!(a, [2.0, -4.0, 12.0, -8.0].into());
122/// ```
123///
124/// Division by a scalar:
125///
126/// ```
127/// # use hoomd_vector::Quaternion;
128/// # let a = Quaternion::from([1.0, -2.0, 6.0, -4.0]);
129/// let c = a / 2.0;
130/// assert_eq!(c, [0.5, -1.0, 3.0, -2.0].into());
131/// ```
132///
133/// ```
134/// # use hoomd_vector::Quaternion;
135/// # let mut a = Quaternion::from([1.0, -2.0, 6.0, -4.0]);
136/// # let b = Quaternion::from([-2.0, 6.0, 4.0, 1.0]);
137/// a /= 2.0;
138/// assert_eq!(a, [0.5, -1.0, 3.0, -2.0].into());
139/// ```
140///
141/// Quaternion multiplication:
142///
143/// ```
144/// # use hoomd_vector::Quaternion;
145/// # let a = Quaternion::from([1.0, -2.0, 6.0, -4.0]);
146/// # let b = Quaternion::from([-2.0, 6.0, 4.0, 1.0]);
147/// let c = a * b;
148/// assert_eq!(c, [-10.0, 32.0, -30.0, -35.0].into());
149/// ```
150///
151/// ```
152/// # use hoomd_vector::Quaternion;
153/// # let mut a = Quaternion::from([1.0, -2.0, 6.0, -4.0]);
154/// # let b = Quaternion::from([-2.0, 6.0, 4.0, 1.0]);
155/// a *= b;
156/// assert_eq!(a, [-10.0, 32.0, -30.0, -35.0].into());
157/// ```
158#[derive(Clone, Copy, Debug, PartialEq, RelativeEq, Serialize, Deserialize)]
159pub struct Quaternion {
160    /// Scalar component
161    pub scalar: f64,
162
163    /// Vector component
164    pub vector: Cartesian<3>,
165}
166
167impl Quaternion {
168    /// The norm of the quaternion, squared.
169    /// ```math
170    /// |\mathbf{q}|^2
171    /// ```
172    ///
173    /// # Example
174    /// ```
175    /// use hoomd_vector::Quaternion;
176    ///
177    /// let q = Quaternion::from([3.0, 0.0, 4.0, 0.0]);
178    /// let norm_squared = q.norm_squared();
179    /// assert_eq!(norm_squared, 25.0);
180    /// ```
181    #[inline]
182    #[must_use]
183    pub fn norm_squared(&self) -> f64 {
184        self.scalar * self.scalar + self.vector.dot(&self.vector)
185    }
186
187    /// The norm of the quaternion.
188    /// ```math
189    /// |\mathbf{q}|
190    /// ```
191    ///
192    /// # Example
193    /// ```
194    /// use hoomd_vector::Quaternion;
195    ///
196    /// let q = Quaternion::from([3.0, 0.0, 4.0, 0.0]);
197    /// let norm = q.norm();
198    /// assert_eq!(norm, 5.0);
199    /// ```
200    #[inline]
201    #[must_use]
202    pub fn norm(&self) -> f64 {
203        self.norm_squared().sqrt()
204    }
205
206    /// Construct the conjugate of this quaternion.
207    /// ```math
208    /// \mathbf{q}^* = (s, -\vec{v})
209    /// ```
210    ///
211    /// # Example
212    /// ```
213    /// use hoomd_vector::Quaternion;
214    ///
215    /// let q = Quaternion::from([1.0, 2.0, 3.0, 4.0]);
216    /// let q_star = q.conjugate();
217    /// assert_eq!(q_star, [1.0, -2.0, -3.0, -4.0].into());
218    /// ```
219    #[inline]
220    #[must_use]
221    pub fn conjugate(self) -> Self {
222        Self {
223            scalar: self.scalar,
224            vector: -self.vector,
225        }
226    }
227
228    /// Create a [`Versor`] by normalizing the given quaternion.
229    ///
230    /// ```math
231    /// \mathbf{v} = \frac{\mathbf{q}}{|\mathbf{q}|}
232    /// ```
233    ///
234    /// # Example
235    ///
236    /// ```
237    /// use hoomd_vector::{Quaternion, Versor};
238    ///
239    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
240    /// let q = Quaternion::from([3.0, 0.0, 0.0, 4.0]);
241    /// let v = q.to_versor()?;
242    /// assert_eq!(*v.get(), [3.0 / 5.0, 0.0, 0.0, 4.0 / 5.0].into());
243    /// # Ok(())
244    /// # }
245    /// ```
246    ///
247    /// # Errors
248    ///
249    /// [`Error::InvalidQuaternionMagnitude`] when `self` is the 0 quaternion.
250    #[inline]
251    pub fn to_versor(self) -> Result<Versor, Error> {
252        let mag = self.norm();
253        if mag == 0.0 {
254            Err(Error::InvalidQuaternionMagnitude)
255        } else {
256            Ok(Versor(self / mag))
257        }
258    }
259
260    /// Create a [`Versor`] by normalizing the given quaternion.
261    ///
262    /// ```math
263    /// \mathbf{v} = \frac{\mathbf{q}}{|\mathbf{q}|}
264    /// ```
265    ///
266    /// # Example
267    ///
268    /// ```
269    /// use hoomd_vector::{Quaternion, Versor};
270    ///
271    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
272    /// let q = Quaternion::from([3.0, 0.0, 0.0, 4.0]);
273    /// let v = q.to_versor_unchecked();
274    /// assert_eq!(*v.get(), [3.0 / 5.0, 0.0, 0.0, 4.0 / 5.0].into());
275    /// # Ok(())
276    /// # }
277    /// ```
278    ///
279    /// # Panics
280    ///
281    /// Divide by 0 when `self` is the 0 quaternion.
282    #[inline]
283    #[must_use]
284    pub fn to_versor_unchecked(self) -> Versor {
285        Versor(self / self.norm())
286    }
287}
288
289impl From<[f64; 4]> for Quaternion {
290    /// Construct a [`Quaternion`] from 4 values.
291    ///
292    /// The first value is the real part. The 2nd through 4th are the complex vector part:
293    /// `[scalar, vector_0, vector_1, vector_2]`.
294    ///
295    /// # Example
296    /// ```
297    /// use hoomd_vector::Quaternion;
298    ///
299    /// let q = Quaternion::from([1.0, 2.0, 3.0, 4.0]);
300    /// assert_eq!(q.scalar, 1.0);
301    /// assert_eq!(q.vector, [2.0, 3.0, 4.0].into());
302    /// ```
303    #[inline]
304    fn from(value: [f64; 4]) -> Self {
305        Self {
306            scalar: value[0],
307            vector: [value[1], value[2], value[3]].into(),
308        }
309    }
310}
311
312impl fmt::Display for Quaternion {
313    /// Format a [`Quaternion`] as `[{s}, [{v[0]}, {v[1]}, {v[2]}]]`.
314    #[inline]
315    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
316        write!(f, "[{}, {}]", self.scalar, self.vector)
317    }
318}
319
320impl Add for Quaternion {
321    type Output = Self;
322
323    #[inline]
324    fn add(self, rhs: Self) -> Self {
325        Self {
326            scalar: self.scalar + rhs.scalar,
327            vector: self.vector + rhs.vector,
328        }
329    }
330}
331
332impl AddAssign for Quaternion {
333    #[inline]
334    fn add_assign(&mut self, rhs: Self) {
335        self.scalar += rhs.scalar;
336        self.vector += rhs.vector;
337    }
338}
339
340impl Div<f64> for Quaternion {
341    type Output = Self;
342
343    #[inline]
344    fn div(self, rhs: f64) -> Self {
345        Self {
346            scalar: self.scalar / rhs,
347            vector: self.vector / rhs,
348        }
349    }
350}
351
352impl DivAssign<f64> for Quaternion {
353    #[inline]
354    fn div_assign(&mut self, rhs: f64) {
355        self.scalar /= rhs;
356        self.vector /= rhs;
357    }
358}
359
360impl Mul<f64> for Quaternion {
361    type Output = Self;
362
363    #[inline]
364    fn mul(self, rhs: f64) -> Self {
365        Self {
366            scalar: self.scalar * rhs,
367            vector: self.vector * rhs,
368        }
369    }
370}
371
372impl MulAssign<f64> for Quaternion {
373    #[inline]
374    fn mul_assign(&mut self, rhs: f64) {
375        self.scalar *= rhs;
376        self.vector *= rhs;
377    }
378}
379
380impl Mul<Quaternion> for Quaternion {
381    type Output = Self;
382
383    #[inline]
384    fn mul(self, rhs: Quaternion) -> Self {
385        Self {
386            scalar: (self.scalar * rhs.scalar - self.vector.dot(&rhs.vector)),
387            vector: (rhs.vector * self.scalar
388                + self.vector * rhs.scalar
389                + self.vector.cross(&rhs.vector)),
390        }
391    }
392}
393
394impl MulAssign<Quaternion> for Quaternion {
395    #[inline]
396    fn mul_assign(&mut self, rhs: Quaternion) {
397        let result = *self * rhs;
398        self.scalar = result.scalar;
399        self.vector = result.vector;
400    }
401}
402
403impl Sub for Quaternion {
404    type Output = Self;
405
406    #[inline]
407    fn sub(self, rhs: Self) -> Self {
408        Self {
409            scalar: self.scalar - rhs.scalar,
410            vector: self.vector - rhs.vector,
411        }
412    }
413}
414
415impl SubAssign for Quaternion {
416    #[inline]
417    fn sub_assign(&mut self, rhs: Self) {
418        self.scalar -= rhs.scalar;
419        self.vector -= rhs.vector;
420    }
421}
422
423/// A unit [`Quaternion`] that represents a 3D rotation.
424///
425/// [`Versor`] represents a 3D rotation with a **unit quaternion**. Rotation follows the Hamilton
426/// convention.
427///
428/// ## Constructing a [`Versor`]:
429///
430/// The default [`Versor`] is the identity:
431///
432/// ```
433/// use hoomd_vector::Versor;
434///
435/// let v = Versor::default();
436/// assert_eq!(*v.get(), [1.0, 0.0, 0.0, 0.0].into());
437/// ```
438///
439/// Create a [`Versor`] that rotates by an angle about an axis:
440/// ```
441/// use hoomd_vector::Versor;
442/// use std::f64::consts::PI;
443///
444/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
445/// let v = Versor::from_axis_angle([0.0, 1.0, 0.0].try_into()?, PI / 2.0);
446/// assert_eq!(
447///     *v.get(),
448///     [(PI / 4.0).cos(), 0.0, (PI / 4.0).sin(), 0.0].into()
449/// );
450/// # Ok(())
451/// # }
452/// ```
453///
454/// Create a [`Versor`] by normalizing a [`Quaternion`]:
455/// ```
456/// use hoomd_vector::{Quaternion, Versor};
457///
458/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
459/// let q = Quaternion::from([3.0, 0.0, 0.0, 4.0]);
460/// let v = q.to_versor()?;
461/// assert_eq!(*v.get(), [3.0 / 5.0, 0.0, 0.0, 4.0 / 5.0].into());
462/// # Ok(())
463/// # }
464/// ```
465///
466/// Create a random [`Versor`]:
467/// ```
468/// use hoomd_vector::Versor;
469/// use rand::{RngExt, SeedableRng, rngs::StdRng};
470///
471/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
472/// let mut rng = StdRng::seed_from_u64(1);
473/// let v: Versor = rng.random();
474/// # Ok(())
475/// # }
476/// ```
477///
478/// ## Operations using [`Versor`]
479///
480/// Rotate a [`Cartesian<3>`] by a [`Versor`]:
481/// ```
482/// use approxim::assert_relative_eq;
483/// use hoomd_vector::{Cartesian, Rotate, Rotation, Versor};
484/// use std::f64::consts::PI;
485///
486/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
487/// let a = Cartesian::from([-1.0, 0.0, 0.0]);
488/// let v = Versor::from_axis_angle([0.0, 0.0, 1.0].try_into()?, PI / 2.0);
489/// let b = v.rotate(&a);
490/// assert_relative_eq!(b, [0.0, -1.0, 0.0].into());
491/// # Ok(())
492/// # }
493/// ```
494///
495/// Combine two rotations together:
496/// ```
497/// use hoomd_vector::{Rotation, Versor};
498/// use std::f64::consts::PI;
499///
500/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
501/// let a = Versor::from_axis_angle([1.0, 0.0, 1.0].try_into()?, PI / 2.0);
502/// let b = Versor::from_axis_angle([0.0, 0.0, 1.0].try_into()?, PI / 4.0);
503/// let c = a.combine(&b);
504/// # Ok(())
505/// # }
506/// ```
507#[derive(Clone, Copy, Debug, PartialEq, RelativeEq, Serialize, Deserialize)]
508pub struct Versor(Quaternion);
509
510impl Versor {
511    /// Take the dot product of the Versor as an element of $`\mathbb{R}^4`$.
512    #[inline]
513    fn dot_as_cartesian(&self, other: &Self) -> f64 {
514        self.get().scalar * other.get().scalar + self.get().vector.dot(&other.get().vector)
515    }
516    /// Create a [`Versor`] that rotates by an angle (in radians)
517    /// counterclockwise about an axis.
518    ///
519    /// # Example
520    ///
521    /// ```
522    /// use hoomd_vector::Versor;
523    /// use std::f64::consts::PI;
524    ///
525    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
526    /// let v = Versor::from_axis_angle([0.0, 1.0, 0.0].try_into()?, PI / 2.0);
527    /// assert_eq!(
528    ///     *v.get(),
529    ///     [(PI / 4.0).cos(), 0.0, (PI / 4.0).sin(), 0.0].into()
530    /// );
531    /// # Ok(())
532    /// # }
533    /// ```
534    #[inline]
535    #[must_use]
536    pub fn from_axis_angle(axis: Unit<Cartesian<3>>, angle: f64) -> Self {
537        let Unit(axis_vector) = axis;
538
539        Versor(Quaternion {
540            scalar: (angle / 2.0).cos(),
541            vector: axis_vector * (angle / 2.0).sin(),
542        })
543    }
544
545    /// Normalize the versor.
546    ///
547    /// Nominally, all [`Versor`] instances retain a unit norm. Due to limited
548    /// floating point precision, this assumption may not hold after repeated
549    /// operations. Normalize versors when needed to correct this issue.
550    ///
551    /// # Example
552    ///
553    /// ```
554    /// use hoomd_vector::Versor;
555    /// use std::f64::consts::PI;
556    ///
557    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
558    /// let a = Versor::from_axis_angle([0.0, 1.0, 0.0].try_into()?, PI / 2.0);
559    /// let b = a.normalized();
560    /// # Ok(())
561    /// # }
562    /// ```
563    #[inline]
564    #[must_use]
565    pub fn normalized(self) -> Self {
566        let Versor(q) = self;
567        let f = 1.0 / q.norm();
568        Self(Quaternion {
569            scalar: q.scalar * f,
570            vector: q.vector * f,
571        })
572    }
573
574    /// Get the unit quaternion.
575    #[inline]
576    #[must_use]
577    pub fn get(&self) -> &Quaternion {
578        &self.0
579    }
580
581    /// A metric quantifying the angle (in radians) of the spherical arc separating two Versors.
582    ///
583    /// $`d : \mathbb{H} \times \mathbb{H} \to \mathbb{R}^+, \quad d(q_0, q_1) = \arccos(|q_0 \cdot q_1|)`$
584    ///
585    /// This value always lies in the range $`[0, \pi]`$, and is symmetric: while there
586    /// are multiple arcs separating a pair of quaternions, this metric always chooses
587    /// the shortest.
588    #[inline]
589    #[must_use]
590    pub fn arc_distance(&self, other: &Self) -> f64 {
591        self.dot_as_cartesian(other).acos()
592    }
593    /// A fast metric on Versors representing elements of SO(3).
594    ///
595    /// $`d : \mathbb{H} \times \mathbb{H} \to \mathbb{R}^+, \quad d(q_0, q_1) = 1 - |q_0 \cdot q_1 |`$
596    ///
597    /// This has less geometric meaning than the [`arc_distance`](Versor::arc_distance) metric. However, it
598    /// is much faster while still obeying the triangle inequality and the axiom
599    /// $`d(q_0, q_1) = d(q_1, q_0)`$. This metric always lies in the range
600    /// $`[0, 1]`$, and is symmetric such that $`d(q, q)`$ = $`d(q, -q)`$.
601    #[inline]
602    #[must_use]
603    pub fn half_euclidean_norm_squared(&self, other: &Self) -> f64 {
604        1.0 - self.dot_as_cartesian(other)
605    }
606}
607
608impl From<Versor> for RotationMatrix<3> {
609    /// Construct a rotation matrix equivalent to this versor's rotation.
610    ///
611    /// When rotating many vectors by the same [`Versor`], improve performance
612    /// by converting to a matrix first and applying that matrix to the vectors.
613    ///
614    /// # Example
615    /// ```
616    /// use approxim::assert_relative_eq;
617    /// use hoomd_vector::{Cartesian, Rotate, RotationMatrix, Versor};
618    /// use std::f64::consts::PI;
619    ///
620    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
621    /// let a = Cartesian::from([-1.0, 0.0, 0.0]);
622    /// let v = Versor::from_axis_angle([0.0, 0.0, 1.0].try_into()?, PI / 2.0);
623    ///
624    /// let matrix = RotationMatrix::from(v);
625    /// let b = matrix.rotate(&a);
626    /// assert_relative_eq!(b, [0.0, -1.0, 0.0].into());
627    /// # Ok(())
628    /// # }
629    /// ```
630    #[inline]
631    fn from(versor: Versor) -> RotationMatrix<3> {
632        let Versor(quaternion) = versor;
633        let a = quaternion.scalar;
634        let b = quaternion.vector[0];
635        let c = quaternion.vector[1];
636        let d = quaternion.vector[2];
637
638        RotationMatrix {
639            rows: [
640                [
641                    a * a + b * b - c * c - d * d,
642                    2.0 * b * c - 2.0 * a * d,
643                    2.0 * b * d + 2.0 * a * c,
644                ]
645                .into(),
646                [
647                    2.0 * b * c + 2.0 * a * d,
648                    a * a - b * b + c * c - d * d,
649                    2.0 * c * d - 2.0 * a * b,
650                ]
651                .into(),
652                [
653                    2.0 * b * d - 2.0 * a * c,
654                    2.0 * c * d + 2.0 * a * b,
655                    a * a - b * b - c * c + d * d,
656                ]
657                .into(),
658            ],
659        }
660    }
661}
662
663impl Default for Versor {
664    /// Create an identity rotation.
665    ///
666    /// # Example
667    /// ```
668    /// use hoomd_vector::Versor;
669    ///
670    /// let v = Versor::default();
671    /// ```
672    #[inline]
673    fn default() -> Self {
674        Self(Quaternion {
675            scalar: 1.0,
676            vector: [0.0, 0.0, 0.0].into(),
677        })
678    }
679}
680
681impl Rotate<Cartesian<3>> for Versor {
682    type Matrix = RotationMatrix<3>;
683
684    /// Rotate a [`Cartesian<3>`] by a [`Versor`]
685    ///
686    /// ```math
687    /// \mathbf{q} \vec{a} \mathbf{q}^*
688    /// ```
689    ///
690    /// # Example
691    ///
692    /// ```
693    /// use approxim::assert_relative_eq;
694    /// use hoomd_vector::{Cartesian, Rotate, Rotation, Versor};
695    /// use std::f64::consts::PI;
696    ///
697    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
698    /// let a = Cartesian::from([-1.0, 0.0, 0.0]);
699    /// let v = Versor::from_axis_angle([0.0, 0.0, 1.0].try_into()?, PI / 2.0);
700    /// let b = v.rotate(&a);
701    /// assert_relative_eq!(b, [0.0, -1.0, 0.0].into());
702    /// # Ok(())
703    /// # }
704    /// ```
705    #[inline]
706    fn rotate(&self, vector: &Cartesian<3>) -> Cartesian<3> {
707        let Versor(quaternion) = self;
708
709        *vector
710            * (quaternion.scalar * quaternion.scalar - quaternion.vector.dot(&quaternion.vector))
711            + quaternion.vector.cross(vector) * (2.0 * quaternion.scalar)
712            + quaternion.vector * (2.0 * quaternion.vector.dot(vector))
713    }
714}
715
716impl Rotation for Versor {
717    /// Combine two rotations.
718    ///
719    /// The resulting versor is obtained by quaternion multiplication.
720    /// ```math
721    /// \mathbf{q}_{ab} = \mathbf{q}_a \mathbf{q}_b
722    /// ```
723    ///
724    /// # Example
725    ///
726    /// ```
727    /// use hoomd_vector::{Rotation, Versor};
728    ///
729    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
730    /// let q_a = Versor::from_axis_angle([0.0, 1.0, 0.0].try_into()?, 1.5);
731    /// let q_b = Versor::from_axis_angle([1.0, 0.0, 0.0].try_into()?, 0.125);
732    /// let q_ab = q_a.combine(&q_b);
733    /// # Ok(())
734    /// # }
735    /// ```
736    #[inline]
737    fn combine(&self, other: &Self) -> Self {
738        let Versor(a) = self;
739        let Versor(b) = other;
740
741        Versor(a.mul(*b))
742    }
743
744    /// Create the identity [`Versor`]: [1, [0, 0, 0]]
745    ///
746    /// # Example
747    ///
748    /// ```
749    /// use hoomd_vector::{Rotation, Versor};
750    ///
751    /// let identity = Versor::identity();
752    /// ```
753    #[inline]
754    fn identity() -> Self {
755        Self::default()
756    }
757
758    /// Create a [`Versor`] that performs the inverse rotation of the given versor.
759    ///
760    /// ```math
761    /// \mathbf{q}^*
762    /// ```
763    ///
764    /// # Example
765    ///
766    /// ```
767    /// use hoomd_vector::{Rotation, Versor};
768    ///
769    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
770    /// let v = Versor::from_axis_angle([0.0, 1.0, 0.0].try_into()?, 1.5);
771    /// let v_star = v.inverted();
772    /// # Ok(())
773    /// # }
774    /// ```
775    #[inline]
776    fn inverted(self) -> Self {
777        let Versor(quaternion) = self;
778
779        Versor(quaternion.conjugate())
780    }
781}
782
783impl fmt::Display for Versor {
784    /// Format a [`Versor`] as `[{s}, [{v[0]}, {v[1]}, {v[2]}]]`.
785    #[inline]
786    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
787        write!(f, "{}", self.0)
788    }
789}
790
791impl Distribution<Versor> for StandardUniform {
792    /// Sample a random [`Versor`] from the uniform distribution over all rotations.
793    ///
794    /// # Example
795    ///
796    /// ```
797    /// use hoomd_vector::Versor;
798    /// use rand::{RngExt, SeedableRng, rngs::StdRng};
799    ///
800    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
801    /// let mut rng = StdRng::seed_from_u64(1);
802    /// let v: Versor = rng.random();
803    /// # Ok(())
804    /// # }
805    /// ```
806    #[inline]
807    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Versor {
808        // See method 19 from: https://extremelearning.com.au/how-to-generate-uniformly-random-points-on-n-spheres-and-n-balls/
809        let scalar = rng.sample::<f64, _>(StandardNormal);
810        let vector = Cartesian::<3>::from(std::array::from_fn(|_| rng.sample(StandardNormal)));
811
812        let norm = (vector.norm_squared() + (scalar * scalar)).sqrt();
813
814        Versor(Quaternion {
815            scalar: scalar / norm,
816            vector: vector / norm,
817        })
818    }
819}
820
821#[cfg(test)]
822mod tests {
823    use super::*;
824    use approxim::{assert_abs_diff_eq, assert_relative_eq};
825    use rand::{SeedableRng, rngs::StdRng};
826    use rstest::*;
827    use std::f64::consts::PI;
828
829    mod quaternion {
830        use super::*;
831
832        #[test]
833        fn from_array() {
834            let q = Quaternion::from([2.0, -3.0, 4.0, 7.0]);
835            assert!(q.scalar == 2.0);
836            assert!(q.vector == [-3.0, 4.0, 7.0].into());
837        }
838
839        #[test]
840        fn norm() {
841            let q = Quaternion::from([1.0, 4.0, -3.0, -2.0]);
842            assert_eq!(q.norm_squared(), 30.0);
843            assert_eq!(q.norm(), 30.0_f64.sqrt());
844        }
845
846        #[test]
847        fn conjugate() {
848            let q1 = Quaternion::from([1.0, -2.0, 4.0, -0.5]);
849            let q2 = q1.conjugate();
850            assert_eq!(q2, [1.0, 2.0, -4.0, 0.5].into());
851            assert_relative_eq!(q2 * q1, [q2.norm() * q1.norm(), 0.0, 0.0, 0.0].into());
852        }
853
854        #[test]
855        fn to_versor() {
856            let q = Quaternion::from([5.0, 3.0, -1.0, 1.0]);
857
858            assert_relative_eq!(
859                q.to_versor()
860                    .expect("hard-coded quatnernion should be non zero"),
861                Versor(Quaternion {
862                    scalar: 5.0 / 6.0,
863                    vector: [3.0 / 6.0, -1.0 / 6.0, 1.0 / 6.0].into()
864                })
865            );
866
867            assert_relative_eq!(
868                q.to_versor_unchecked(),
869                Versor(Quaternion {
870                    scalar: 5.0 / 6.0,
871                    vector: [3.0 / 6.0, -1.0 / 6.0, 1.0 / 6.0].into()
872                })
873            );
874
875            let zero = Quaternion::from([0.0, 0.0, 0.0, 0.0]);
876            assert!(matches!(
877                zero.to_versor(),
878                Err(Error::InvalidQuaternionMagnitude)
879            ));
880        }
881
882        #[test]
883        fn ops() {
884            let a = Quaternion::from([1.0, -2.0, 6.0, -4.0]);
885            let b = Quaternion::from([-2.0, 6.0, 4.0, 1.0]);
886
887            // +, +=
888            assert_eq!(a + b, [-1.0, 4.0, 10.0, -3.0].into());
889            let mut c = a;
890            c += b;
891            assert_eq!(c, [-1.0, 4.0, 10.0, -3.0].into());
892
893            // -, -=
894            assert_eq!(a - b, [3.0, -8.0, 2.0, -5.0].into());
895            let mut c = a;
896            c -= b;
897            assert_eq!(c, [3.0, -8.0, 2.0, -5.0].into());
898
899            // Scalar * and /
900            assert_eq!(a * 2.0, [2.0, -4.0, 12.0, -8.0].into());
901            let mut c = a;
902            c *= 2.0;
903            assert_eq!(c, [2.0, -4.0, 12.0, -8.0].into());
904
905            assert_eq!(a / 2.0, [0.5, -1.0, 3.0, -2.0].into());
906            let mut c = a;
907            c /= 2.0;
908            assert_eq!(c, [0.5, -1.0, 3.0, -2.0].into());
909
910            // Quaternion multiplication
911            assert_eq!(a * b, [-10.0, 32.0, -30.0, -35.0].into());
912            let mut c = a;
913            c *= b;
914            assert_eq!(c, [-10.0, 32.0, -30.0, -35.0].into());
915        }
916
917        #[test]
918        fn display() {
919            let q = Quaternion {
920                scalar: 0.5,
921                vector: [0.125, -0.875, 2.125].into(),
922            };
923            let s = format!("{q}");
924            assert_eq!(s, "[0.5, [0.125, -0.875, 2.125]]");
925        }
926    }
927
928    mod versor {
929        use super::*;
930        #[test]
931        fn default() {
932            let a = Versor::default();
933            assert!(a.get() == &[1.0, 0.0, 0.0, 0.0].into());
934        }
935
936        #[test]
937        fn identity() {
938            let a = Versor::identity();
939            assert!(a.get() == &[1.0, 0.0, 0.0, 0.0].into());
940        }
941
942        #[rstest(
943        theta => [0.0, PI / 2.0, 1e-12 * PI, -3.0, 12345.6],
944        axis => [[1.0, 0.0, 0.0].try_into().expect("hard-coded vector should have non-zero length"), [1.0, -1.0, 1.0].try_into().expect("hard-coded vector should have non-zero length")],
945    )]
946        fn from_axis_angle(theta: f64, axis: Unit<Cartesian<3>>) {
947            let Unit(axis_vector) = axis;
948
949            let Versor(q) = Versor::from_axis_angle(axis, theta);
950            assert_relative_eq!(q.scalar, (theta / 2.0).cos());
951            assert_relative_eq!(q.vector, axis_vector * (theta / 2.0).sin());
952        }
953
954        #[rstest(
955        theta_1 => [0.0, PI / 2.0, -3.0],
956        theta_2 => [-0.0, -PI / 3.0, PI, 2.0 * PI]
957    )]
958        fn combine_same_axis(theta_1: f64, theta_2: f64) {
959            let axis = [1.0, 0.0, 0.0]
960                .try_into()
961                .expect("hard-coded vector should have non-zero length");
962            let Unit(axis_vector) = axis;
963
964            let a = Versor::from_axis_angle(axis, theta_1);
965            let b = Versor::from_axis_angle(axis, theta_2);
966            let c = a.combine(&b);
967
968            let theta = theta_1 + theta_2;
969            let Versor(q) = c;
970            assert_relative_eq!(q.scalar, (theta / 2.0).cos());
971            assert_relative_eq!(q.vector, axis_vector * (theta / 2.0).sin());
972        }
973
974        fn validate_rotations<R: Rotate<Cartesian<3>>>(z_pi_2: &R, y_pi_4: &R) {
975            assert_relative_eq!(
976                z_pi_2.rotate(&[0.0, 0.0, 1.0].into()),
977                [0.0, 0.0, 1.0].into()
978            );
979            assert_relative_eq!(
980                z_pi_2.rotate(&[1.0, 0.0, 4.25].into()),
981                [0.0, 1.0, 4.25].into()
982            );
983            assert_relative_eq!(
984                z_pi_2.rotate(&[0.0, 1.0, -8.75].into()),
985                [-1.0, 0.0, -8.75].into()
986            );
987
988            let sqrt_2_2 = 2.0_f64.sqrt() / 2.0;
989            assert_relative_eq!(
990                y_pi_4.rotate(&[0.0, -10.0, 0.0].into()),
991                [0.0, -10.0, 0.0].into()
992            );
993            assert_relative_eq!(
994                y_pi_4.rotate(&[1.0, -15.0, 0.0].into()),
995                [sqrt_2_2, -15.0, -sqrt_2_2].into()
996            );
997            assert_relative_eq!(
998                y_pi_4.rotate(&[sqrt_2_2, -15.0, -sqrt_2_2].into()),
999                [0.0, -15.0, -1.0].into()
1000            );
1001        }
1002
1003        #[test]
1004        fn rotate() {
1005            let z_pi_2 = Versor::from_axis_angle(
1006                [0.0, 0.0, 1.0]
1007                    .try_into()
1008                    .expect("hard-coded vector should have non-zero length"),
1009                PI / 2.0,
1010            );
1011            let y_pi_4 = Versor::from_axis_angle(
1012                [0.0, 1.0, 0.0]
1013                    .try_into()
1014                    .expect("hard-coded vector should have non-zero length"),
1015                PI / 4.0,
1016            );
1017
1018            validate_rotations(&z_pi_2, &y_pi_4);
1019        }
1020
1021        #[test]
1022        fn precompute() {
1023            let z_pi_2 = RotationMatrix::from(Versor::from_axis_angle(
1024                [0.0, 0.0, 1.0]
1025                    .try_into()
1026                    .expect("hard-coded vector should have non-zero length"),
1027                PI / 2.0,
1028            ));
1029            let y_pi_4 = RotationMatrix::from(Versor::from_axis_angle(
1030                [0.0, 1.0, 0.0]
1031                    .try_into()
1032                    .expect("hard-coded vector should have non-zero length"),
1033                PI / 4.0,
1034            ));
1035
1036            validate_rotations(&z_pi_2, &y_pi_4);
1037        }
1038
1039        #[test]
1040        fn combine_different_axis() {
1041            let a = Versor::from_axis_angle(
1042                [1.0, 0.0, 0.0]
1043                    .try_into()
1044                    .expect("hard-coded vector should have non-zero length"),
1045                PI / 4.0,
1046            );
1047            let b = Versor::from_axis_angle(
1048                [0.0, 0.0, 1.0]
1049                    .try_into()
1050                    .expect("hard-coded vector should have non-zero length"),
1051                PI / 2.0,
1052            );
1053
1054            let q = a.combine(&b);
1055            let v = q.rotate(&[1.0, 0.0, 0.0].into());
1056            assert_relative_eq!(v, [0.0, 2.0_f64.sqrt() / 2.0, 2.0_f64.sqrt() / 2.0].into());
1057        }
1058
1059        #[rstest(theta => [0.0, 1.0, 2.125])]
1060        fn inverted(theta: f64) {
1061            let q1 = Versor::from_axis_angle(
1062                [1.0, 0.5, -2.0]
1063                    .try_into()
1064                    .expect("hard-coded vector should have non-zero length"),
1065                theta,
1066            );
1067            let q2 = q1.inverted();
1068            assert_relative_eq!(q1.combine(&q2), Versor::identity());
1069        }
1070
1071        #[test]
1072        fn display() {
1073            let v = Versor(Quaternion {
1074                scalar: 0.5,
1075                vector: [0.125, -0.875, 2.125].into(),
1076            });
1077            let s = format!("{v}");
1078            assert_eq!(s, "[0.5, [0.125, -0.875, 2.125]]");
1079        }
1080
1081        #[test]
1082        fn normalized() {
1083            let v = Versor(Quaternion {
1084                scalar: 5.0,
1085                vector: [3.0, -1.0, 1.0].into(),
1086            });
1087            assert_relative_eq!(
1088                v.normalized(),
1089                Versor(Quaternion {
1090                    scalar: 5.0 / 6.0,
1091                    vector: [3.0 / 6.0, -1.0 / 6.0, 1.0 / 6.0].into()
1092                })
1093            );
1094        }
1095
1096        #[test]
1097        fn random() {
1098            const CHECK_VECTORS: [Cartesian<3>; 3] = [
1099                Cartesian {
1100                    coordinates: [1.0, 0.0, 0.0],
1101                },
1102                Cartesian {
1103                    coordinates: [0.0, 1.0, 0.0],
1104                },
1105                Cartesian {
1106                    coordinates: [1.0, 0.0, 1.0],
1107                },
1108            ];
1109
1110            // Perform basic checks on random versors.
1111            // 1) Ensure that each randomly generated versor is unit.
1112            // 2) Check that the result of rotating a reference vector by random versors does not
1113            // point in any special direction. The average dot product should be close to 0.
1114            let samples: u32 = 20_000;
1115
1116            let reference = Cartesian::from([1.0, 0.0, 0.0]);
1117            let mut dot_sums = [0.0; CHECK_VECTORS.len()];
1118
1119            let mut rng = StdRng::seed_from_u64(1);
1120
1121            for _ in 0..samples {
1122                let q: Versor = rng.random();
1123                assert_relative_eq!(q.get().norm_squared(), 1.0, max_relative = 1e-15);
1124
1125                let v = q.rotate(&reference);
1126                for i in 0..CHECK_VECTORS.len() {
1127                    dot_sums[i] += v.dot(&CHECK_VECTORS[i]);
1128                }
1129            }
1130
1131            for dot_sum in dot_sums {
1132                assert_abs_diff_eq!(dot_sum / f64::from(samples), 0.0, epsilon = 0.01);
1133            }
1134        }
1135    }
1136}