Skip to main content

phyz_math/
lib.rs

1//! Spatial algebra and math primitives for phyz physics engine.
2//!
3//! Implements 6D spatial vectors, Plucker transforms, and spatial inertia
4//! following Featherstone's conventions.
5
6#![warn(missing_docs)]
7
8// Compile the crate README's Rust blocks as doc-tests so the documented API
9// cannot drift from the real one. `cfg(doctest)` keeps it out of rendered docs.
10#[cfg(doctest)]
11#[doc = include_str!("../README.md")]
12pub struct ReadmeDocTests;
13
14pub mod fp;
15pub mod spatial;
16
17pub use spatial::{SpatialInertia, SpatialMat, SpatialTransform, SpatialTransformExt, SpatialVec};
18
19/// Unit quaternion for 3D rotations (scalar `w`, vector `v`).
20pub type Quat = tang::Quat<f64>;
21
22/// 3D vector alias.
23pub type Vec3 = tang::Vec3<f64>;
24/// 3x3 matrix alias.
25pub type Mat3 = tang::Mat3<f64>;
26/// 4x4 matrix alias.
27pub type Mat4 = tang::Mat4<f64>;
28/// Dynamic vector.
29pub type DVec = tang_la::DVec<f64>;
30/// Dynamic matrix.
31pub type DMat = tang_la::DMat<f64>;
32
33pub use tang::{GRAVITY, skew};
34
35/// Exponential map `so(3) → SO(3)`, computed through [`fp`].
36///
37/// Numerically identical to [`tang::Quat::exp`] except that the `sin`/`cos`
38/// come from [`fp::sin_cos`] rather than the platform's libm. Every rotational
39/// configuration update in the engine goes through this function — a free
40/// joint touches it twice per step — so it is the single highest-traffic
41/// transcendental site in a rollout, and the one that most needs to return the
42/// same bits on every machine. See `docs/determinism.md`.
43pub fn quat_exp(omega: &Vec3) -> Quat {
44    let angle = omega.norm();
45    // Same small-angle branch as tang: below one epsilon the series is the
46    // identity plus half the rotation vector, and dividing by `angle` would be
47    // the thing that loses precision rather than the thing that gains it.
48    if angle < f64::EPSILON {
49        return Quat {
50            w: 1.0,
51            v: *omega * 0.5,
52        };
53    }
54    let (s, c) = fp::sin_cos(angle * 0.5);
55    Quat {
56        w: c,
57        v: *omega * (s / angle),
58    }
59}
60
61/// Logarithmic map `SO(3) → so(3)`, computed through [`fp`].
62///
63/// The inverse of [`quat_exp`], and likewise identical to [`tang::Quat::log`]
64/// apart from routing its `atan2` through [`fp::atan2`].
65pub fn quat_log(q: &Quat) -> Vec3 {
66    let norm_v = q.v.norm();
67    if norm_v < f64::EPSILON {
68        return q.v * 2.0;
69    }
70    let angle = 2.0 * fp::atan2(norm_v, q.w);
71    q.v * (angle / norm_v)
72}
73
74#[cfg(test)]
75mod quat_map_tests {
76    use super::*;
77
78    #[test]
79    fn matches_tang_to_a_few_ulp_and_round_trips() {
80        for w in [
81            Vec3::new(0.0, 0.0, 0.0),
82            Vec3::new(1e-18, 0.0, 0.0),
83            Vec3::new(0.3, -0.7, 1.1),
84            Vec3::new(0.0, 0.0, 3.0),
85            Vec3::new(-2.0, 0.5, 0.25),
86        ] {
87            let ours = quat_exp(&w);
88            let theirs = Quat::exp(&w);
89            let tol = 8.0 * f64::EPSILON;
90            assert!((ours.w - theirs.w).abs() <= tol, "exp w for {w:?}");
91            assert!((ours.v - theirs.v).norm() <= tol, "exp v for {w:?}");
92
93            let back = quat_log(&ours);
94            assert!((back - w).norm() <= 1e-12, "round trip for {w:?}");
95            assert!(
96                (quat_log(&theirs) - theirs.log()).norm() <= tol,
97                "log {w:?}"
98            );
99        }
100    }
101}