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}