Skip to main content

geop_core_math/primitives/
pose.rs

1//! [`Pose`]: where a rigid body sits, as a rotation — a unit
2//! [`Quaternion`] — and a position, in any [`Number`].
3//!
4//! Unlike angles, a unit quaternion has no singular configurations, and
5//! moving a point by it is a polynomial of its components — which is what
6//! makes gradients through it well behaved, and its enclosures tight.
7
8use serde::{Deserialize, Serialize};
9
10use super::CoordinateSystem;
11use crate::{
12    dual::Dual,
13    geop_error::{GeopError, GeopResult},
14    scalars::{Number, Scalar},
15    solvers::{least_squares::Rows, system::Variable},
16    vector::{Vector, Vector3},
17};
18
19/// How many terms of each series [`Pose::exp`] sums near zero before it
20/// bounds the rest: how tight the map is there, never whether it encloses.
21const SERIES_TERMS: usize = 12;
22
23/// A quaternion `w + x i + y j + z k`.
24#[derive(Clone, Copy, Debug, PartialEq)]
25pub struct Quaternion<T = Scalar> {
26    pub w: T,
27    pub x: T,
28    pub y: T,
29    pub z: T,
30}
31
32impl<T: Number> Quaternion<T> {
33    pub fn new(w: T, x: T, y: T, z: T) -> Self {
34        Self { w, x, y, z }
35    }
36
37    pub fn identity() -> Self {
38        Self::new(T::ONE, T::ZERO, T::ZERO, T::ZERO)
39    }
40
41    /// Whether it is exactly the identity: every component sharp, `1` and
42    /// three `0`s — no rotation at all, rather than one too small to tell.
43    pub fn is_identity(&self) -> bool {
44        let exactly = |c: T, value: f64| c.is_sharp() && c.to_f64() == value;
45        exactly(self.w, 1.0) && exactly(self.x, 0.0) && exactly(self.y, 0.0) && exactly(self.z, 0.0)
46    }
47
48    /// `0 + v`: a vector as a pure quaternion.
49    pub fn pure(v: &Vector3<T>) -> Self {
50        Self::new(T::ZERO, v[0], v[1], v[2])
51    }
52
53    /// Its vector part.
54    pub fn vector(&self) -> Vector3<T> {
55        Vector3::from_array([self.x, self.y, self.z])
56    }
57
58    /// Its components `[w, x, y, z]`.
59    pub fn components(&self) -> [T; 4] {
60        [self.w, self.x, self.y, self.z]
61    }
62
63    /// Every component mapped by `f`.
64    pub fn map<U: Number>(&self, f: impl Fn(T) -> U) -> Quaternion<U> {
65        Quaternion::new(f(self.w), f(self.x), f(self.y), f(self.z))
66    }
67
68    pub fn mul(&self, o: &Self) -> Self {
69        Self {
70            w: self
71                .w
72                .mul(o.w)
73                .sub(self.x.mul(o.x))
74                .sub(self.y.mul(o.y))
75                .sub(self.z.mul(o.z)),
76            x: self
77                .w
78                .mul(o.x)
79                .add(self.x.mul(o.w))
80                .add(self.y.mul(o.z))
81                .sub(self.z.mul(o.y)),
82            y: self
83                .w
84                .mul(o.y)
85                .sub(self.x.mul(o.z))
86                .add(self.y.mul(o.w))
87                .add(self.z.mul(o.x)),
88            z: self
89                .w
90                .mul(o.z)
91                .add(self.x.mul(o.y))
92                .sub(self.y.mul(o.x))
93                .add(self.z.mul(o.w)),
94        }
95    }
96
97    pub fn conj(&self) -> Self {
98        Self::new(self.w, self.x.neg(), self.y.neg(), self.z.neg())
99    }
100
101    pub fn scale(&self, s: T) -> Self {
102        self.map(|c| c.mul(s))
103    }
104
105    pub fn add(&self, o: &Self) -> Self {
106        Self::new(
107            self.w.add(o.w),
108            self.x.add(o.x),
109            self.y.add(o.y),
110            self.z.add(o.z),
111        )
112    }
113
114    pub fn dot(&self, o: &Self) -> T {
115        self.w
116            .mul(o.w)
117            .add(self.x.mul(o.x))
118            .add(self.y.mul(o.y))
119            .add(self.z.mul(o.z))
120    }
121
122    pub fn norm_sq(&self) -> T {
123        self.dot(self)
124    }
125
126    /// The unit quaternion of the same rotation; fails for one that could be
127    /// zero, which stands for no rotation at all.
128    pub fn normalized(&self) -> GeopResult<Self> {
129        let n = self.norm_sq();
130        if !n.definitely_greater(T::ZERO) {
131            return Err(GeopError::new("a rotation cannot be the zero quaternion"));
132        }
133        Ok(self.scale(T::ONE.div(n.sqrt()?)?))
134    }
135
136    /// Turning by `degrees` about `x`, then `y`, then `z` — the world's axes.
137    pub fn from_euler(degrees: [T; 3]) -> GeopResult<Self> {
138        let half_radians = T::PI.div(T::from_i64(360))?;
139        let [a, b, c] = degrees.map(|d| d.mul(half_radians));
140        let qx = Self::new(a.cos(), a.sin(), T::ZERO, T::ZERO);
141        let qy = Self::new(b.cos(), T::ZERO, b.sin(), T::ZERO);
142        let qz = Self::new(c.cos(), T::ZERO, T::ZERO, c.sin());
143        Ok(qz.mul(&qy.mul(&qx)))
144    }
145
146    /// The rotation whose matrix has the columns `columns` — where `x`, `y`
147    /// and `z` turn to, orthonormal and right-handed: the inverse of
148    /// [`Quaternion::rotation_columns`], up to sign. Of the four ways to
149    /// read it off the matrix (Shepperd's), the one dividing by the largest
150    /// of the trace and the diagonal: a free choice, and the
151    /// well-conditioned one.
152    pub fn from_rotation_columns(columns: [Vector3<T>; 3]) -> GeopResult<Self> {
153        let m = |r: usize, c: usize| columns[c][r];
154        let one = T::ONE;
155        let trace = m(0, 0).add(m(1, 1)).add(m(2, 2));
156        let size = |v: T| v.to_f64();
157        let candidates = [trace, m(0, 0), m(1, 1), m(2, 2)];
158        let largest = (1..4).fold(0, |best, k| {
159            if size(candidates[k]) > size(candidates[best]) {
160                k
161            } else {
162                best
163            }
164        });
165        let quarter = T::ONE.div(T::from_i64(4))?;
166        let q = match largest {
167            0 => {
168                let s = one.add(trace).sqrt()?.mul(T::TWO);
169                let inv = one.div(s)?;
170                Self::new(
171                    s.mul(quarter),
172                    m(2, 1).sub(m(1, 2)).mul(inv),
173                    m(0, 2).sub(m(2, 0)).mul(inv),
174                    m(1, 0).sub(m(0, 1)).mul(inv),
175                )
176            }
177            1 => {
178                let s = one
179                    .add(m(0, 0))
180                    .sub(m(1, 1))
181                    .sub(m(2, 2))
182                    .sqrt()?
183                    .mul(T::TWO);
184                let inv = one.div(s)?;
185                Self::new(
186                    m(2, 1).sub(m(1, 2)).mul(inv),
187                    s.mul(quarter),
188                    m(0, 1).add(m(1, 0)).mul(inv),
189                    m(0, 2).add(m(2, 0)).mul(inv),
190                )
191            }
192            2 => {
193                let s = one
194                    .add(m(1, 1))
195                    .sub(m(0, 0))
196                    .sub(m(2, 2))
197                    .sqrt()?
198                    .mul(T::TWO);
199                let inv = one.div(s)?;
200                Self::new(
201                    m(0, 2).sub(m(2, 0)).mul(inv),
202                    m(0, 1).add(m(1, 0)).mul(inv),
203                    s.mul(quarter),
204                    m(1, 2).add(m(2, 1)).mul(inv),
205                )
206            }
207            _ => {
208                let s = one
209                    .add(m(2, 2))
210                    .sub(m(0, 0))
211                    .sub(m(1, 1))
212                    .sqrt()?
213                    .mul(T::TWO);
214                let inv = one.div(s)?;
215                Self::new(
216                    m(1, 0).sub(m(0, 1)).mul(inv),
217                    m(0, 2).add(m(2, 0)).mul(inv),
218                    m(1, 2).add(m(2, 1)).mul(inv),
219                    s.mul(quarter),
220                )
221            }
222        };
223        q.normalized()
224            .map_err(|e| e.with_context(format!("from_rotation_columns({columns:?})")))
225    }
226
227    /// The rotation it stands for, as a matrix's columns — where `x`, `y`
228    /// and `z` turn to. It need not be unit: dividing by its norm squared
229    /// makes the rotation of any non-zero quaternion, smoothly.
230    pub fn rotation_columns(&self) -> GeopResult<[Vector3<T>; 3]> {
231        let n = T::ONE.div(self.norm_sq())?;
232        let two = T::TWO.mul(n);
233        let Self { w, x, y, z } = *self;
234        let (xx, yy, zz) = (x.mul(x), y.mul(y), z.mul(z));
235        let (xy, xz, yz) = (x.mul(y), x.mul(z), y.mul(z));
236        let (wx, wy, wz) = (w.mul(x), w.mul(y), w.mul(z));
237        let one = T::ONE;
238        let v = |a: T, b: T, c: T| Vector3::from_array([a, b, c]);
239        Ok([
240            v(
241                one.sub(two.mul(yy.add(zz))),
242                two.mul(xy.add(wz)),
243                two.mul(xz.sub(wy)),
244            ),
245            v(
246                two.mul(xy.sub(wz)),
247                one.sub(two.mul(xx.add(zz))),
248                two.mul(yz.add(wx)),
249            ),
250            v(
251                two.mul(xz.add(wy)),
252                two.mul(yz.sub(wx)),
253                one.sub(two.mul(xx.add(yy))),
254            ),
255        ])
256    }
257}
258
259/// A rigid motion — where a body sits: a point `p` of it is at
260/// `R p + position`, with `R` the rotation of a quaternion, which need not
261/// be unit (see [`Pose::apply`]) — in the
262/// number type `T`. Held as the two themselves, so that what is stored is
263/// exactly what was set, and a pose read and written again is written
264/// exactly as it was read.
265///
266/// Serialized as `{"position": [x, y, z], "rotation": [w, x, y, z]}`, each
267/// number its value's midpoint; a rotation read is kept as read, sharp —
268/// a hair off unit as a midpoint is, as a solver's step leaves one too —
269/// so that reading what was written gives back what was read before
270/// (see [`Pose::saved`]). Angles are
271/// for showing a pose to people only ([`Pose::euler_degrees`],
272/// [`Pose::from_euler`]).
273#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
274#[serde(into = "PoseFile", try_from = "PoseFile", bound = "T: Number")]
275pub struct Pose<T: Number = Scalar> {
276    rotation: Quaternion<T>,
277    position: Vector3<T>,
278}
279
280/// A [`Pose`] as a file has it.
281#[derive(Serialize, Deserialize)]
282struct PoseFile {
283    position: [f64; 3],
284    rotation: [f64; 4],
285}
286
287impl<T: Number> From<Pose<T>> for PoseFile {
288    fn from(pose: Pose<T>) -> Self {
289        let p = pose.position();
290        PoseFile {
291            position: p.to_f64_array(),
292            rotation: pose.rotation().components().map(|c| c.to_f64()),
293        }
294    }
295}
296
297impl<T: Number> TryFrom<PoseFile> for Pose<T> {
298    type Error = GeopError;
299
300    /// The pose as read: normalizing the rotation would make it an
301    /// interval no file holds, written as another midpoint, read as another
302    /// interval — a pose that changes every time it is saved. The zero
303    /// quaternion, which stands for no rotation, is refused.
304    fn try_from(file: PoseFile) -> GeopResult<Self> {
305        let [w, x, y, z] = file.rotation.map(T::from_f64);
306        let rotation = Quaternion::new(w, x, y, z);
307        if !rotation.norm_sq().definitely_greater(T::ZERO) {
308            return Err(GeopError::new("a rotation cannot be the zero quaternion"));
309        }
310        Ok(Pose {
311            rotation,
312            position: Vector3::from_array(file.position.map(T::from_f64)),
313        })
314    }
315}
316
317/// Its position and its rotation's quaternion, each component exactly —
318/// the quaternion as it is, not normalized: what a value kept to be read
319/// again holds ([`crate::exact`]).
320impl crate::exact::Exact for Pose {
321    fn write<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
322        let [w, x, y, z] = self.rotation.components();
323        let rotation = Vector::from_array([w, x, y, z]);
324        let written = (
325            crate::exact::Written(&self.position),
326            crate::exact::Written(&rotation),
327        );
328        written.serialize(serializer)
329    }
330
331    fn read<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
332        let (crate::exact::Read(position), crate::exact::Read(rotation)): (
333            crate::exact::Read<Vector3>,
334            crate::exact::Read<Vector<4>>,
335        ) = Deserialize::deserialize(deserializer)?;
336        Ok(Pose {
337            rotation: Quaternion::new(rotation[0], rotation[1], rotation[2], rotation[3]),
338            position,
339        })
340    }
341}
342
343impl<T: Number> Default for Pose<T> {
344    fn default() -> Self {
345        Pose::identity()
346    }
347}
348
349impl<T: Number> Pose<T> {
350    pub fn identity() -> Self {
351        Pose {
352            rotation: Quaternion::identity(),
353            position: Vector3::zero(),
354        }
355    }
356
357    /// Turning by `rotation` — any quaternion definitely not zero,
358    /// normalized here — then moving to `position`.
359    pub fn new(position: Vector3<T>, rotation: Quaternion<T>) -> GeopResult<Self> {
360        Ok(Pose {
361            rotation: rotation.normalized()?,
362            position,
363        })
364    }
365
366    /// Moving to `position`, turned by `degrees` about `x`, then `y`, then
367    /// `z` — the world's axes — as a dialog shows a pose.
368    pub fn from_euler(position: Vector3<T>, degrees: [T; 3]) -> GeopResult<Self> {
369        Pose::new(position, Quaternion::from_euler(degrees)?)
370    }
371
372    /// The pose a file holds of this one: written, and read back (see
373    /// [`Pose`]'s serialization). Reading it and writing it again gives it
374    /// back exactly.
375    pub fn saved(&self) -> GeopResult<Self> {
376        Pose::try_from(PoseFile::from(*self))
377    }
378
379    /// Where its body's origin is.
380    pub fn position(&self) -> Vector3<T> {
381        self.position
382    }
383
384    /// Its rotation's quaternion: unit as [`Pose::new`] makes it, as read
385    /// from a file otherwise — a quaternion of any norm but zero stands for
386    /// the rotation it divides out (see [`Pose::apply`]).
387    pub fn rotation(&self) -> Quaternion<T> {
388        self.rotation
389    }
390
391    /// Every component mapped by `f` — into dual numbers, say.
392    pub fn map<U: Number>(&self, f: impl Fn(T) -> U) -> Pose<U> {
393        Pose {
394            rotation: self.rotation.map(&f),
395            position: self.position.map(&f),
396        }
397    }
398
399    /// Its rotation as angles in degrees about `x`, then `y`, then `z` (see
400    /// [`Pose::from_euler`]) — for showing it to people, and so read off
401    /// its midpoint. At gimbal lock (the second at ±90°) only the sum or
402    /// difference of the other two is determined; the split is arbitrary
403    /// but exact.
404    pub fn euler_degrees(&self) -> [f64; 3] {
405        // The unit quaternion of its rotation, which it need not be itself.
406        let q = self.rotation().components().map(|c| c.to_f64());
407        let norm = q.iter().map(|c| c * c).sum::<f64>().sqrt();
408        let [w, x, y, z] = q.map(|c| c / norm);
409        // The rotation matrix's first column and last row.
410        let (r00, r10, r20) = (
411            1.0 - 2.0 * (y * y + z * z),
412            2.0 * (x * y + w * z),
413            2.0 * (x * z - w * y),
414        );
415        let r21 = 2.0 * (y * z + w * x);
416        let (r01, r11) = (2.0 * (x * y - w * z), 1.0 - 2.0 * (x * x + z * z));
417        // `c` from the first column; whatever it comes out as — even from
418        // rounding noise at gimbal lock — `b` and `a` are solved for the
419        // rest of the matrix with it, so the triple reproduces the rotation.
420        let c = r10.atan2(r00);
421        let (sc, cc) = c.sin_cos();
422        let b = (-r20).atan2(r00 * cc + r10 * sc);
423        // Rx(a) = Ry(b)^T Rz(c)^T R: its (2,1) and (1,1) entries.
424        let (sb, cb) = b.sin_cos();
425        let m11 = -sc * r01 + cc * r11;
426        let m21 = sb * (cc * r01 + sc * r11) + cb * r21;
427        let a = m21.atan2(m11);
428        [a, b, c].map(f64::to_degrees)
429    }
430
431    /// The same rotation, moved to `position`.
432    pub fn with_position(&self, position: Vector3<T>) -> Pose<T> {
433        Pose {
434            rotation: self.rotation,
435            position,
436        }
437    }
438
439    /// This pose after `inner`: a point is moved by `inner` first.
440    pub fn compose(&self, inner: &Pose<T>) -> Pose<T> {
441        Pose {
442            rotation: self.rotation.mul(&inner.rotation),
443            position: self.apply(&inner.position),
444        }
445    }
446
447    /// The pose that undoes this one. Its rotation is the inverse
448    /// quaternion, `q̄ / |q|²`, so that a pose composed with its inverse is
449    /// the identity whatever `q`'s norm — and norms do not grow along a
450    /// chain of poses undone and done again.
451    pub fn inverse(&self) -> Pose<T> {
452        let conj = self.rotation.conj();
453        let turned = conj
454            .mul(&Quaternion::pure(&self.position))
455            .mul(&self.rotation);
456        Pose {
457            rotation: self.unturned(conj),
458            position: self.unturned(turned).vector().neg(),
459        }
460    }
461
462    /// The rigid motion of the twist `(ω, v)` — turning at `ω` while moving
463    /// at `v`, for unit time: the exponential map of `ξ̂ = ω + ε v` as a
464    /// unit dual quaternion, `exp(ξ̂ / 2)`. Its rotation turns by `|ω|`
465    /// radians about `ω`; it is a screw motion along that axis. Zero is the
466    /// identity, and to first order it is `(1, ω / 2) + ε (0, v / 2)`.
467    ///
468    /// With `φ = |ω| / 2`, it is `(cos φ, s ω) + ε (−s (ω·v) / 2, s v + c
469    /// (ω·v) ω)`, where `s = sin φ / (2φ)` and `c = (cos φ − sin φ / φ) /
470    /// (8φ²)`. Near `φ = 0` both are summed as their series in `φ²`, which
471    /// are polynomials: smooth through zero, where the closed forms are
472    /// `0 / 0` — so the map can be differentiated there with dual numbers,
473    /// as a solver does to linearize a pose. The series are enclosed with
474    /// what they leave out, so the map is an enclosure for a twist that is
475    /// a box around zero too, and so are its derivatives.
476    ///
477    /// Its rotation is the real part; its position, the translation `2 d r*`
478    /// of the dual part `d` and the rotation `r`, multiplied out:
479    /// `2 s cos φ v + (s² + 2 c cos φ) (ω·v) ω + 2 s² ω × v`.
480    pub fn exp(omega: &Vector3<T>, v: &Vector3<T>) -> GeopResult<Self> {
481        let phi2 = omega.prod_dot(omega).div(T::from_i64(4))?;
482        let (cos, s, c) = if phi2.definitely_less(T::ONE) {
483            // Series in φ²: Σ (−φ²)^k / (2k)!, ½ Σ (−φ²)^k / (2k+1)! and
484            // −Σ (−φ²)^k 2(k+1) / (8 (2k+3)!), each term made from the one
485            // before — no factorial is formed, so they hold in any `Number`.
486            let minus = phi2.neg();
487            let (mut cos, mut s, mut c) = (T::ZERO, T::ZERO, T::ZERO);
488            // The k-th terms, as k counts up (`d` the one subtracted from c).
489            let mut cos_term = T::ONE;
490            let mut s_term = T::ONE.div(T::TWO)?;
491            let mut d = T::from_i64(2).div(T::from_i64(48))?;
492            for k in 0..SERIES_TERMS {
493                cos = cos.add(cos_term);
494                s = s.add(s_term);
495                c = c.sub(d);
496                let k = k as i64;
497                cos_term = cos_term
498                    .mul(minus)
499                    .div(T::from_i64((2 * k + 1) * (2 * k + 2)))?;
500                s_term = s_term
501                    .mul(minus)
502                    .div(T::from_i64((2 * k + 2) * (2 * k + 3)))?;
503                d = d
504                    .mul(minus)
505                    .mul(T::from_i64(k + 2))
506                    .div(T::from_i64((k + 1) * (2 * k + 4) * (2 * k + 5)))?;
507            }
508            // Each series alternates, and for φ² < 1 its terms shrink from
509            // the first one left out on — as do those of its derivative by
510            // φ²: each sum is within that term of where it stops, and so,
511            // for a dual number, is each derivative. A term times
512            // `[-1, 1]` is that bound, part by part.
513            let within = T::ONE.neg().union(T::ONE);
514            (
515                cos.add(cos_term.mul(within)),
516                s.add(s_term.mul(within)),
517                c.sub(d.mul(within)),
518            )
519        } else {
520            let phi = phi2.sqrt()?;
521            let (cos, sin) = (phi.cos(), phi.sin());
522            let s = sin.div(T::TWO.mul(phi))?;
523            let c = cos.sub(sin.div(phi)?).div(T::from_i64(8).mul(phi2))?;
524            (cos, s, c)
525        };
526        let rotation = Quaternion::new(cos, s.mul(omega[0]), s.mul(omega[1]), s.mul(omega[2]));
527        let (two_s_cos, s2) = (T::TWO.mul(s).mul(cos), s.mul(s));
528        let along = s2.add(T::TWO.mul(c).mul(cos)).mul(omega.prod_dot(v));
529        let position = (v.prod_scalar(two_s_cos))
530            .add(&omega.prod_scalar(along))
531            .add(&omega.prod_cross(v).prod_scalar(T::TWO.mul(s2)));
532        Ok(Pose { rotation, position })
533    }
534
535    /// The point `p` moved by the pose: turned by the rotation its
536    /// quaternion stands for, `q p q̄ / |q|²`, then moved.
537    pub fn apply(&self, p: &Vector3<T>) -> Vector3<T> {
538        let r = &self.rotation;
539        let turned = self.unturned(r.mul(&Quaternion::pure(p)).mul(&r.conj()));
540        turned.vector().add(&self.position)
541    }
542
543    /// `q` divided by the norm squared of the pose's quaternion: what a
544    /// product with it and its conjugate turns by. A pose's quaternion
545    /// stands for its rotation whatever its norm — a file keeps it as read,
546    /// a solver's step leaves it a hair off unit — but a product with it
547    /// scales by its norm squared. Exactly unit, nothing is divided, and
548    /// nothing widened.
549    fn unturned(&self, q: Quaternion<T>) -> Quaternion<T> {
550        let n = self.rotation.norm_sq();
551        if n.is_exactly(T::ONE) {
552            return q;
553        }
554        // A pose's quaternion is not zero (see `Pose::new` and its reader).
555        q.scale(
556            T::ONE
557                .div(n)
558                .expect("a pose's rotation is not the zero quaternion"),
559        )
560    }
561
562    /// Turning by `radians` about the line through `point` along the unit
563    /// vector `direction` — counter-clockwise, looking against `direction`
564    /// — as a pose: the rotation `(cos ½θ, sin ½θ d)`, and the position
565    /// that keeps `point` where it is. That quaternion is unit already,
566    /// within its enclosure, and kept as it is: normalized again, honestly,
567    /// it came out four times as wide, and so did everything the pose turned.
568    pub fn rotation_about(
569        point: &Vector3<T>,
570        direction: &Vector3<T>,
571        radians: T,
572    ) -> GeopResult<Self> {
573        let half = radians.div(T::TWO)?;
574        let (c, s) = (half.cos(), half.sin());
575        let turn = Pose {
576            rotation: Quaternion::new(
577                c,
578                s.mul(direction[0]),
579                s.mul(direction[1]),
580                s.mul(direction[2]),
581            ),
582            position: Vector3::zero(),
583        };
584        if !turn.rotation.norm_sq().definitely_greater(T::ZERO) {
585            return Err(GeopError::new(format!(
586                "Pose::rotation_about: the direction {direction:?} is no unit vector"
587            )));
588        }
589        Ok(turn.with_position(point.sub(&turn.apply(point))))
590    }
591
592    /// What the pose does to geometry: its rotation's columns and its
593    /// position, enclosed once, so moving many points costs a matrix
594    /// product each. A pose that does not turn at all — its rotation
595    /// exactly the identity — moves geometry by additions alone.
596    pub fn motion(&self) -> Motion<T> {
597        let turns = !self.rotation.is_identity();
598        Motion {
599            axes: turns.then(|| {
600                self.rotation
601                    .rotation_columns()
602                    .expect("a pose's rotation is no zero quaternion")
603            }),
604            position: self.position,
605            mirrors: false,
606        }
607    }
608}
609
610impl Pose {
611    /// The differential of [`Pose::exp`] in the frame it leads to — the
612    /// right Jacobian of the exponential map: the matrix `J` with
613    /// `exp(ξ + dξ) = exp(ξ) ∘ exp(J dξ)` to first order, for the twist
614    /// `ξ = (ω, v)`, rows and columns a turn's three numbers, then a move's.
615    /// An enclosure for every twist in the boxes `omega` and `v`, made by
616    /// differentiating the map — enclosed with what its series leave out —
617    /// with dual numbers: column `k` is the twist `exp(ξ)⁻¹ ∂ₖ exp(ξ)`, its
618    /// turn `2 r̄ ∂ₖr` and its move `Rᵀ ∂ₖt` for the rotation `r` and the
619    /// position `t`.
620    #[allow(clippy::needless_range_loop, reason = "a column of each of two blocks")]
621    pub fn exp_differential(omega: &Vector3, v: &Vector3) -> GeopResult<[[Scalar; 6]; 6]> {
622        let moved = Pose::exp_with_derivatives(omega, v)?;
623        let r = moved.rotation();
624        let at = r.map(|c| c.v);
625        let [x, y, z] = at.rotation_columns()?;
626        let mut jacobian = [[Scalar::ZERO; 6]; 6];
627        for k in 0..6 {
628            let dr = r.map(|c| c.d[k]);
629            let turn = at.conj().mul(&dr).vector();
630            let dt = Vector3::from_array([0, 1, 2].map(|i| moved.position()[i].d[k]));
631            let along = [x.prod_dot(&dt), y.prod_dot(&dt), z.prod_dot(&dt)];
632            for i in 0..3 {
633                jacobian[i][k] = Scalar::TWO.mul(turn[i]);
634                jacobian[3 + i][k] = along[i];
635            }
636        }
637        Ok(jacobian)
638    }
639}
640
641/// A pose steps by a twist `(ω, v)` in its own frame — turning about its
642/// own origin, and moving along its own axes — by the exponential map:
643/// `pose ∘ exp(ω, v)`.
644impl Pose<Dual<6>> {
645    /// The pose of the twist `(omega, v)` (see [`Pose::exp`]), with its
646    /// derivatives by the twist's six numbers — a turn's three, then a
647    /// move's: at a twist of zero, the step of a pose by its own six
648    /// variables, in its own frame.
649    ///
650    /// At a twist of zero — what every constraint a solve makes is
651    /// differentiated by, many thousand times a drag — it is made once per
652    /// thread: the same value every time.
653    pub fn exp_with_derivatives(omega: &Vector3, v: &Vector3) -> GeopResult<Self> {
654        thread_local! {
655            static AT_ZERO: std::cell::Cell<Option<Pose<Dual<6>>>> =
656                const { std::cell::Cell::new(None) };
657        }
658        let zero = |x: &Vector3| x.to_array().iter().all(|c| c.is_zero() && c.is_sharp());
659        let at_zero = zero(omega) && zero(v);
660        if at_zero && let Some(made) = AT_ZERO.get() {
661            return Ok(made);
662        }
663        let var = |k: usize, x: Scalar| Dual::var(x, k);
664        let made = Pose::exp(
665            &Vector3::from_array([var(0, omega[0]), var(1, omega[1]), var(2, omega[2])]),
666            &Vector3::from_array([var(3, v[0]), var(4, v[1]), var(5, v[2])]),
667        )?;
668        if at_zero {
669            AT_ZERO.set(Some(made));
670        }
671        Ok(made)
672    }
673}
674
675impl Variable for Pose {
676    fn dimension(&self) -> usize {
677        6
678    }
679
680    fn retract(&self, step: &[Scalar]) -> GeopResult<Self> {
681        let (omega, v) = twist(step)?;
682        Ok(self.compose(&Pose::exp(&omega, &v)?).map(Scalar::sharpen))
683    }
684
685    /// Of a rigid motion: its rotation a unit quaternion. One that a step
686    /// sharpened is a hair off unit, and turning a point by it scales it by
687    /// its norm squared — a hair off every rigid motion, so off the true
688    /// solution too. So it is made unit first, honestly: an enclosure of
689    /// the rotation it stands for.
690    fn retract_over(&self, step: &[Scalar]) -> GeopResult<Self> {
691        let unit = Pose {
692            rotation: self.rotation.normalized()?,
693            position: self.position,
694        };
695        if step.iter().all(|d| d.is_zero() && d.is_sharp()) {
696            return Ok(unit);
697        }
698        let (omega, v) = twist(step)?;
699        Ok(unit.compose(&Pose::exp(&omega, &v)?))
700    }
701
702    /// The right Jacobian of the exponential map ([`Pose::exp_differential`]);
703    /// the identity, exactly, for a step of exact zeros.
704    fn differential(&self, step: &[Scalar]) -> GeopResult<Option<Vec<Vec<Scalar>>>> {
705        if step.iter().all(|d| d.is_zero() && d.is_sharp()) {
706            return Ok(None);
707        }
708        let (omega, v) = twist(step)?;
709        let jacobian = Pose::exp_differential(&omega, &v)?;
710        Ok(Some(jacobian.iter().map(|row| row.to_vec()).collect()))
711    }
712
713    /// Its position's difference from `at`'s, and its rotation's as the
714    /// unit quaternions' difference — of the two quaternions of `at`'s
715    /// rotation, the nearer — times `scale`, so that a turn counts as the
716    /// distance it moves a point `scale` from its axis.
717    fn stay(&self, at: &Self, scale: Scalar) -> GeopResult<Rows> {
718        type D = crate::dual::Dual<6>;
719        let step = Pose::exp_with_derivatives(&Vector3::zero(), &Vector3::zero())?;
720        let moved = self.map(D::cst).compose(&step);
721        let offset = moved.position().sub(&at.position().map(D::cst));
722        let r = moved.rotation();
723        let mut target = at.rotation();
724        if r.map(|c| c.v).dot(&target).definitely_less(Scalar::ZERO) {
725            target = target.scale(Scalar::ONE.neg());
726        }
727        let turned = (r.components().into_iter().zip(target.components()))
728            .map(|(c, t)| c.sub(D::cst(t)).mul(D::cst(scale)));
729        let mut rows = Rows::default();
730        for residual in offset.to_array().into_iter().chain(turned) {
731            rows.push(residual.v, residual.d.to_vec());
732        }
733        Ok(rows)
734    }
735
736    fn same(&self, other: &Self) -> bool {
737        self == other
738    }
739
740    /// Moved as `from` moved to `to`: `to ∘ from⁻¹ ∘ self`.
741    fn carried(&self, from: &Self, to: &Self) -> Option<Self> {
742        Some(
743            to.compose(&from.inverse())
744                .compose(self)
745                .map(Scalar::sharpen),
746        )
747    }
748}
749
750/// The twist `(ω, v)` a step of six numbers is.
751fn twist(step: &[Scalar]) -> GeopResult<(Vector3, Vector3)> {
752    match step {
753        [w0, w1, w2, v0, v1, v2] => Ok((
754            Vector3::from_array([*w0, *w1, *w2]),
755            Vector3::from_array([*v0, *v1, *v2]),
756        )),
757        _ => Err(GeopError::new(format!(
758            "a step of a pose is six numbers, not {}",
759            step.len()
760        ))),
761    }
762}
763
764/// An affine map as it moves geometry: a [`Pose`]'s rigid motion (see
765/// [`Pose::motion`]), a plain translation, a mirror in a plane, or a
766/// scaling ([`Motion::scaling`], the one that is no isometry) — what a
767/// body is moved, patterned, mirrored or scaled by.
768///
769/// A point `p` goes to `A p + position`, with `A` the matrix whose columns
770/// are `axes`: a rotation's, a mirror's reflection or a scaling's diagonal.
771/// Without axes `A` is the identity and nothing is multiplied: a
772/// translation moves every point by one addition per coordinate, as
773/// exactly as an interval addition can.
774#[derive(Clone, Copy, Debug, PartialEq)]
775pub struct Motion<T: Number = Scalar> {
776    axes: Option<[Vector3<T>; 3]>,
777    position: Vector3<T>,
778    mirrors: bool,
779}
780
781impl<T: Number> Motion<T> {
782    /// Moving every point by `offset`.
783    pub fn translation(offset: Vector3<T>) -> Self {
784        Motion {
785            axes: None,
786            position: offset,
787            mirrors: false,
788        }
789    }
790
791    /// Mirroring in the plane through `point` with the normal `normal`,
792    /// which need not be unit: with `n` the unit normal, `p` goes to
793    /// `p - 2 ((p - point) . n) n` — `A = I - 2 n nᵀ`, and the position
794    /// `2 (point . n) n`.
795    pub fn mirror(point: &Vector3<T>, normal: &Vector3<T>) -> GeopResult<Self> {
796        let n = normal.normalize()?;
797        let column = |k: usize| {
798            let mut c = n.prod_scalar(T::TWO.mul(n[k]).neg());
799            c[k] = T::ONE.add(c[k]);
800            c
801        };
802        Ok(Motion {
803            axes: Some([column(0), column(1), column(2)]),
804            position: n.prod_scalar(T::TWO.mul(point.prod_dot(&n))),
805            mirrors: true,
806        })
807    }
808
809    /// Whether it mirrors: turns a right-handed frame into a left-handed
810    /// one, and with it the side of a surface its normal points to.
811    pub fn mirrors(&self) -> bool {
812        self.mirrors
813    }
814
815    /// Whether it turns or mirrors directions at all, rather than only
816    /// moving points.
817    pub fn turns(&self) -> bool {
818        self.axes.is_some()
819    }
820
821    /// Where it moves the origin to: what it adds to every point it has
822    /// turned.
823    pub fn position(&self) -> Vector3<T> {
824        self.position
825    }
826
827    /// The direction `d` turned — or mirrored; a translation leaves it as
828    /// it is.
829    pub fn rotate(&self, d: &Vector3<T>) -> Vector3<T> {
830        match &self.axes {
831            None => *d,
832            Some([x, y, z]) => x
833                .prod_scalar(d[0])
834                .add(&y.prod_scalar(d[1]))
835                .add(&z.prod_scalar(d[2])),
836        }
837    }
838
839    /// The point `p` moved.
840    pub fn apply(&self, p: &Vector3<T>) -> Vector3<T> {
841        self.rotate(p).add(&self.position)
842    }
843}
844
845impl Motion {
846    /// The affine map `p -> A p + position`, `A` the matrix whose columns
847    /// are `columns`: what a body is scaled by, or carried by past the end
848    /// of a deformation's zone (`geop_core_geometry::deform`). No isometry
849    /// in general — lengths and angles change, directions it turns are no
850    /// longer unit, and only a body's geometry is moved by it, whose NURBS
851    /// map exactly (an affine map of their homogeneous control points). It
852    /// mirrors where the determinant of `A` is negative, and fails where
853    /// the determinant could be zero, which would flatten everything.
854    pub fn affine(columns: [Vector3; 3], position: Vector3) -> GeopResult<Self> {
855        let det = columns[0].prod_dot(&columns[1].prod_cross(&columns[2]));
856        if det.could_be_equal(Scalar::ZERO) {
857            return Err(GeopError::new(format!(
858                "Motion::affine: the columns {columns:?} flatten everything, their determinant being {det:?}"
859            )));
860        }
861        Ok(Motion {
862            axes: Some(columns),
863            position,
864            mirrors: det.definitely_less(Scalar::ZERO),
865        })
866    }
867
868    /// Scaling about `center` by `factors` along `x`, `y` and `z`: `p` goes
869    /// to `center + diag(factors) (p - center)` (see [`Motion::affine`]).
870    /// An odd number of negative factors mirrors. Fails for a factor that
871    /// could be zero.
872    pub fn scaling(center: &Vector3, factors: [Scalar; 3]) -> GeopResult<Self> {
873        if let Some(zero) = factors.iter().find(|f| f.could_be_equal(Scalar::ZERO)) {
874            return Err(GeopError::new(format!(
875                "Motion::scaling: a factor of {zero:?} flattens everything"
876            )));
877        }
878        let column = |k: usize| {
879            let mut c = Vector3::zero();
880            c[k] = factors[k];
881            c
882        };
883        let position = Vector3::from_array(std::array::from_fn(|k| {
884            center[k].sub(factors[k].mul(center[k]))
885        }));
886        Self::affine([column(0), column(1), column(2)], position)
887    }
888
889    /// The frame `frame` moved. Fails for a mirror, which would leave it
890    /// left-handed.
891    pub fn apply_frame(&self, frame: &CoordinateSystem) -> GeopResult<CoordinateSystem> {
892        if self.mirrors {
893            return Err(GeopError::new(
894                "Motion::apply_frame: a mirrored frame is left-handed",
895            ));
896        }
897        CoordinateSystem::try_new(
898            self.apply(frame.origin()),
899            self.rotate(frame.u()),
900            self.rotate(frame.v()),
901            self.rotate(frame.w()),
902        )
903    }
904}
905
906#[cfg(test)]
907mod tests {
908    use super::*;
909
910    fn v(p: [f64; 3]) -> Vector3 {
911        Vector3::from_array(p.map(Scalar::from_f64))
912    }
913
914    fn pose(position: [f64; 3], degrees: [f64; 3]) -> Pose {
915        Pose::from_euler(v(position), degrees.map(Scalar::from_f64)).unwrap()
916    }
917
918    /// `a` could be `b`, as an enclosure of it.
919    fn encloses(a: &Vector3, b: [f64; 3]) -> bool {
920        (0..3).all(|k| a[k].could_be_equal(Scalar::from_f64(b[k])))
921    }
922
923    fn close(a: &Vector3, b: &Vector3) -> bool {
924        (0..3).all(|k| (a[k].to_f64() - b[k].to_f64()).abs() < 1e-12)
925    }
926
927    /// A rotation read back from its matrix is the same rotation, whichever
928    /// of the trace and the diagonal is largest: no turn, half turns about
929    /// each axis, and turns in between.
930    #[test]
931    fn rotations_are_read_back_from_their_matrices() {
932        for degrees in [
933            [0.0, 0.0, 0.0],
934            [180.0, 0.0, 0.0],
935            [0.0, 180.0, 0.0],
936            [0.0, 0.0, 180.0],
937            [30.0, -70.0, 125.0],
938            [170.0, 10.0, -175.0],
939        ] {
940            let rotation = pose([0.0; 3], degrees).rotation();
941            let columns = rotation.rotation_columns().unwrap();
942            let back = Quaternion::from_rotation_columns(columns).unwrap();
943            let again = back.rotation_columns().unwrap();
944            for k in 0..3 {
945                assert!(close(&columns[k], &again[k]), "{degrees:?}");
946            }
947        }
948    }
949
950    /// The angles turn about `x`, then `y`, then `z`: a quarter turn about
951    /// `z` takes `x` to `y` — as an enclosure of the exact result.
952    #[test]
953    fn angles_turn_about_fixed_axes() {
954        let turned = pose([1.0, 2.0, 3.0], [0.0, 0.0, 90.0]);
955        assert!(encloses(
956            &turned.apply(&v([1.0, 0.0, 0.0])),
957            [1.0, 3.0, 3.0]
958        ));
959        assert!(encloses(
960            &turned.motion().apply(&v([1.0, 0.0, 0.0])),
961            [1.0, 3.0, 3.0]
962        ));
963        // About x first, then z: x stays x, then turns to y.
964        let both = pose([0.0; 3], [90.0, 0.0, 90.0]);
965        assert!(encloses(&both.apply(&v([1.0, 0.0, 0.0])), [0.0, 1.0, 0.0]));
966        assert!(encloses(&both.apply(&v([0.0, 1.0, 0.0])), [0.0, 0.0, 1.0]));
967    }
968
969    /// Angles read back give the same motion, also at gimbal lock.
970    #[test]
971    fn angles_round_trip() {
972        for degrees in [
973            [10.0, 20.0, 30.0],
974            [-170.0, 45.0, 100.0],
975            [30.0, 90.0, 20.0],
976        ] {
977            let original = pose([0.5, -1.0, 2.0], degrees);
978            let back = Pose::from_euler(
979                original.position(),
980                original.euler_degrees().map(Scalar::from_f64),
981            )
982            .unwrap();
983            for p in [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.3, -0.2, 0.9]] {
984                assert!(
985                    close(&back.apply(&v(p)), &original.apply(&v(p))),
986                    "{degrees:?}"
987                );
988            }
989        }
990    }
991
992    #[test]
993    fn inverse_and_compose_undo_each_other() {
994        let a = pose([0.5, -1.0, 2.0], [10.0, -20.0, 70.0]);
995        let p = [0.3, 0.4, 0.5];
996        assert!(encloses(&a.compose(&a.inverse()).apply(&v(p)), p));
997        let b = pose([1.0, 0.0, -1.0], [45.0, 30.0, 0.0]);
998        let q = v([0.3, -0.7, 0.2]);
999        let composed = a.compose(&b).apply(&q);
1000        let each = a.apply(&b.apply(&q));
1001        assert!((0..3).all(|k| composed[k].could_be_equal(each[k])));
1002    }
1003
1004    /// Files hold a position and a quaternion; the zero quaternion is
1005    /// refused.
1006    #[test]
1007    fn poses_read_and_write_position_and_quaternion() {
1008        let written = Pose::new(
1009            v([1.0, 2.0, 3.0]),
1010            Quaternion::new(Scalar::ZERO, Scalar::ZERO, Scalar::ZERO, Scalar::TWO),
1011        )
1012        .unwrap();
1013        let json = serde_json::to_value(written).unwrap();
1014        assert_eq!(
1015            json,
1016            serde_json::json!({"position": [1.0, 2.0, 3.0], "rotation": [0.0, 0.0, 0.0, 1.0]})
1017        );
1018        let read: Pose = serde_json::from_value(json).unwrap();
1019        assert!(encloses(&read.apply(&v([1.0, 0.0, 0.0])), [0.0, 2.0, 3.0]));
1020        let zero =
1021            serde_json::json!({"position": [0.0, 0.0, 0.0], "rotation": [0.0, 0.0, 0.0, 0.0]});
1022        assert!(serde_json::from_value::<Pose>(zero).is_err());
1023    }
1024
1025    /// A pose read from a file is written back as it was, and read again
1026    /// is the same pose: saving a pose twice changes nothing.
1027    #[test]
1028    fn a_pose_read_is_written_back_as_it_was() {
1029        let file = serde_json::json!({
1030            "position": [34.18148346551969, 62.89507987346716, 5.871850811598488e-15],
1031            "rotation": [0.9731912888550477, -5.175907578430137e-17, 4.296936517018427e-17,
1032                         -0.22999720714967709],
1033        });
1034        let read: Pose = serde_json::from_value(file.clone()).unwrap();
1035        let written = serde_json::to_value(read).unwrap();
1036        assert_eq!(written, file);
1037        assert_eq!(serde_json::from_value::<Pose>(written).unwrap(), read);
1038        let turned = pose([1.0, -2.0, 0.5], [10.0, 20.0, 30.0]);
1039        let saved = turned.saved().unwrap();
1040        assert_eq!(saved.saved().unwrap(), saved);
1041    }
1042
1043    /// A quaternion read sharp, a hair off unit, turns a point where the
1044    /// unit quaternion it stands for does: the two enclosures of where it
1045    /// goes overlap.
1046    #[test]
1047    fn a_rotation_read_off_unit_turns_as_its_unit_one() {
1048        let turned = pose([0.0; 3], [10.0, 20.0, 30.0]);
1049        let saved = turned.saved().unwrap();
1050        let unit = Pose::new(saved.position(), saved.rotation()).unwrap();
1051        let p = v([3.0, -1.0, 2.0]);
1052        let (a, b) = (saved.apply(&p), unit.apply(&p));
1053        assert!((0..3).all(|k| a[k].could_be_equal(b[k])), "{a:?} {b:?}");
1054    }
1055
1056    /// A quarter turn about the line through (1, 1, 0) along `z` keeps that
1057    /// line where it is and takes (2, 1, 5) to (1, 2, 5).
1058    #[test]
1059    fn rotation_about_a_line_keeps_it() {
1060        let turn = Pose::rotation_about(
1061            &v([1.0, 1.0, 0.0]),
1062            &v([0.0, 0.0, 1.0]),
1063            Scalar::PI.div(Scalar::TWO).unwrap(),
1064        )
1065        .unwrap();
1066        assert!(encloses(&turn.apply(&v([1.0, 1.0, 7.0])), [1.0, 1.0, 7.0]));
1067        assert!(encloses(&turn.apply(&v([2.0, 1.0, 5.0])), [1.0, 2.0, 5.0]));
1068        let motion = turn.motion();
1069        assert!(motion.turns() && !motion.mirrors());
1070        assert!(encloses(
1071            &motion.apply(&v([2.0, 1.0, 5.0])),
1072            [1.0, 2.0, 5.0]
1073        ));
1074    }
1075
1076    /// A pose that does not turn moves points by additions alone; a
1077    /// mirror in a slanted plane is undone by itself and reverses
1078    /// orientation.
1079    #[test]
1080    fn translations_and_mirrors() {
1081        let shift = Pose::identity().with_position(v([0.1, 0.2, 0.3])).motion();
1082        assert!(!shift.turns());
1083        let p = v([1.0, 2.0, 3.0]);
1084        assert_eq!(
1085            format!("{:?}", shift.apply(&p)),
1086            format!("{:?}", p.add(&v([0.1, 0.2, 0.3])))
1087        );
1088        let mirror = Motion::mirror(&v([1.0, 0.0, 2.0]), &v([1.0, -2.0, 0.5])).unwrap();
1089        assert!(mirror.mirrors());
1090        let q = v([0.3, -0.7, 4.0]);
1091        assert!(mirror.apply(&mirror.apply(&q)).could_be_equal(&q));
1092        assert!(encloses(
1093            &mirror.apply(&v([1.0, 0.0, 2.0])),
1094            [1.0, 0.0, 2.0]
1095        ));
1096        let (x, y, z) = (v([1.0, 0.0, 0.0]), v([0.0, 1.0, 0.0]), v([0.0, 0.0, 1.0]));
1097        let handedness = mirror
1098            .rotate(&x)
1099            .prod_cross(&mirror.rotate(&y))
1100            .prod_dot(&mirror.rotate(&z));
1101        assert!(
1102            handedness.could_be_equal(Scalar::ONE.neg()),
1103            "{handedness:?}"
1104        );
1105        assert!(mirror.apply_frame(&CoordinateSystem::world_at(q)).is_err());
1106    }
1107
1108    /// The exponential of a twist: nothing for zero; a turn alone about the
1109    /// origin; a move alone; and a turn about a line through `q`, whose
1110    /// twist has `v = −ω × q`, the turn `rotation_about` gives.
1111    #[test]
1112    fn exp_is_the_motion_of_a_twist() {
1113        let zero = Pose::exp(&v([0.0; 3]), &v([0.0; 3])).unwrap();
1114        assert!(encloses(&zero.apply(&v([1.0, 2.0, 3.0])), [1.0, 2.0, 3.0]));
1115
1116        let quarter = std::f64::consts::FRAC_PI_2;
1117        let turn = Pose::exp(&v([0.0, 0.0, quarter]), &v([0.0; 3])).unwrap();
1118        let p = turn.apply(&v([1.0, 0.0, 0.0]));
1119        assert!(
1120            (p[0].to_f64()).abs() < 1e-15 && (p[1].to_f64() - 1.0).abs() < 1e-15,
1121            "{p:?}"
1122        );
1123
1124        let shift = Pose::exp(&v([0.0; 3]), &v([1.0, 2.0, 3.0])).unwrap();
1125        assert!(encloses(&shift.position(), [1.0, 2.0, 3.0]));
1126
1127        for angle in [0.3, 1.9, 3.0, 5.5] {
1128            let (axis, q) = (v([0.0, 0.0, 1.0]), v([2.0, 1.0, 0.5]));
1129            let omega = axis.prod_scalar(Scalar::from_f64(angle));
1130            let screw = Pose::exp(&omega, &omega.prod_cross(&q).neg()).unwrap();
1131            let about = Pose::rotation_about(&q, &axis, Scalar::from_f64(angle)).unwrap();
1132            for point in [[0.0; 3], [1.0, -2.0, 4.0]] {
1133                let (a, b) = (screw.apply(&v(point)), about.apply(&v(point)));
1134                for k in 0..3 {
1135                    let gap = (a[k].to_f64() - b[k].to_f64()).abs();
1136                    assert!(gap < 1e-12, "{angle}: {a:?} vs {b:?}");
1137                }
1138            }
1139        }
1140    }
1141
1142    /// The two ways the map is summed agree where they meet, at a turn of
1143    /// two radians: the series just below, the closed form just above.
1144    #[test]
1145    fn exp_is_continuous_where_its_series_ends() {
1146        let at = |angle: f64| {
1147            let omega = v([0.6, 0.0, 0.8]).prod_scalar(Scalar::from_f64(angle));
1148            Pose::exp(&omega, &v([0.3, -1.0, 2.0])).unwrap()
1149        };
1150        let (below, above) = (at(2.0 - 1e-12), at(2.0 + 1e-12));
1151        for point in [[0.0; 3], [1.0, 1.0, 1.0]] {
1152            let (a, b) = (below.apply(&v(point)), above.apply(&v(point)));
1153            for k in 0..3 {
1154                assert!(
1155                    (a[k].to_f64() - b[k].to_f64()).abs() < 1e-10,
1156                    "{a:?} vs {b:?}"
1157                );
1158            }
1159        }
1160    }
1161
1162    /// Differentiated at zero with dual numbers, the map is its first order:
1163    /// a point `p` moves at `ω × p + v`.
1164    #[test]
1165    fn exp_differentiates_at_zero() {
1166        use crate::dual::Dual;
1167        type D = Dual<6>;
1168        let var = |k: usize| D::var(Scalar::ZERO, k);
1169        let omega = Vector3::from_array([var(0), var(1), var(2)]);
1170        let velocity = Vector3::from_array([var(3), var(4), var(5)]);
1171        let moved = Pose::<D>::exp(&omega, &velocity).unwrap();
1172        let p = [1.0, 2.0, 3.0];
1173        let at = moved.apply(&Vector3::from_array(p.map(|c| D::cst(Scalar::from_f64(c)))));
1174        // d/dω (ω × p) = −[p]×, d/dv v = I.
1175        let expected = [
1176            [0.0, p[2], -p[1], 1.0, 0.0, 0.0],
1177            [-p[2], 0.0, p[0], 0.0, 1.0, 0.0],
1178            [p[1], -p[0], 0.0, 0.0, 0.0, 1.0],
1179        ];
1180        for (k, row) in expected.iter().enumerate() {
1181            assert!(at[k].v.could_be_equal(Scalar::from_f64(p[k])));
1182            for (j, &exact) in row.iter().enumerate() {
1183                let d = at[k].d[j];
1184                assert!(
1185                    d.could_be_equal(Scalar::from_f64(exact)),
1186                    "∂{k}/∂{j} = {d:?}"
1187                );
1188            }
1189        }
1190    }
1191
1192    /// A quaternion off unit — as a file keeps it — stands for the rotation
1193    /// of its unit one: a pose with it moves points as that one does, and
1194    /// composed with its inverse is the identity, its norm not compounded.
1195    #[test]
1196    fn a_rotation_off_unit_moves_points_as_its_unit_one() {
1197        let saved: Pose = serde_json::from_value(serde_json::json!({
1198            "position": [1.0, -7.25, 2.0],
1199            "rotation": [1.09, 0.2, -0.3, 0.1],
1200        }))
1201        .unwrap();
1202        let unit = Pose::new(saved.position(), saved.rotation()).unwrap();
1203        let p = [3.0, 4.0, -5.0];
1204        let moved = unit.apply(&v(p)).to_f64_array();
1205        assert!(
1206            encloses(&saved.apply(&v(p)), moved),
1207            "{:?}",
1208            saved.apply(&v(p))
1209        );
1210        let back = saved.inverse().compose(&saved);
1211        assert!(encloses(&back.apply(&v(p)), p), "{back:?}");
1212        assert!(back.rotation().norm_sq().could_be_equal(Scalar::ONE));
1213        assert!(encloses(&saved.inverse().apply(&saved.apply(&v(p))), p));
1214    }
1215}