Skip to main content

arris_math/
lib.rs

1//! Numeric foundation of the Arris kernel: points, vectors and unit vectors
2//! over `nalgebra`, frames, axes and rigid motions, intervals, exact orientation
3//! predicates over `robust`, polynomial and interval-guarded root finding,
4//! and `Precision`, the model-wide tolerance configuration.
5//!
6//! Guarantees: `f64` throughout, no allocation in evaluation, no panic on
7//! any finite input, and no numeric literal standing in for a tolerance —
8//! every tolerance is a `Precision` field, a [`Tolerance`] derived from it,
9//! or a named constant with a comment (`.agents/rules/kernel.md`). Depends
10//! on nothing in the workspace.
11//!
12//! The point and vector types are `nalgebra`'s by alias and `nalgebra` is
13//! re-exported (`docs/adr/0001-nalgebra-types-by-alias.md`), so a caller
14//! reaches every operator and solver `nalgebra` has and a `nalgebra` major
15//! bump is an Arris API change.
16#![forbid(unsafe_code)]
17#![warn(missing_docs)]
18
19mod aabb;
20mod axis;
21mod frame;
22mod interval;
23mod isometry;
24mod precision;
25pub mod predicates;
26pub mod roots;
27mod tolerance;
28
29pub use nalgebra;
30
31pub use aabb::Aabb;
32pub use axis::Axis;
33pub use frame::{Frame, Frame2, FrameError, Handedness};
34pub use interval::{Interval, IntervalError};
35pub use isometry::Isometry;
36pub use precision::Precision;
37pub use tolerance::Tolerance;
38
39/// Relative rounding slack: a magnitude at or below this fraction of its
40/// natural scale is rounding noise, not a value. Eight ulps — what a
41/// handful of multiplications and one trigonometric evaluation leave
42/// behind, and orders of magnitude below any model tolerance. It is not a
43/// geometric tolerance and never decides whether two things are *the
44/// same*; it decides whether a computed quantity is zero *in floating
45/// point*: the radius of a sphere's parallel at the pole, `cos(π/2)`
46/// evaluated in `f64`, is `6e-17`, not `0`.
47pub const RELATIVE_ROUNDING: f64 = 8.0 * f64::EPSILON;
48
49/// `|x| ≤ RELATIVE_ROUNDING · |scale|`: `x` is zero to rounding at
50/// `scale`. A zero `scale` makes only an exact zero negligible.
51///
52/// ```
53/// use arris_math::is_negligible;
54/// use core::f64::consts::FRAC_PI_2;
55///
56/// assert!(is_negligible(3.0 * FRAC_PI_2.cos(), 3.0));
57/// assert!(!is_negligible(3.0 * (FRAC_PI_2 - 1e-9).cos(), 3.0));
58/// ```
59pub fn is_negligible(x: f64, scale: f64) -> bool {
60    x.abs() <= RELATIVE_ROUNDING * scale.abs()
61}
62
63/// An angle moved into `[0, 2π)`: what a periodic curve's or surface's
64/// parameter is reported in (`docs/DATA-MODEL.md` §Conventions). A
65/// negative angle whose sum with `2π` rounds up to `2π` becomes `0` —
66/// the same point on the circle, and inside the domain. A non-finite
67/// angle comes back unchanged.
68///
69/// ```
70/// use arris_math::wrap_angle;
71/// use core::f64::consts::TAU;
72///
73/// assert_eq!(wrap_angle(0.0), 0.0);
74/// assert_eq!(wrap_angle(-1.0), TAU - 1.0);
75/// assert_eq!(wrap_angle(-1e-300), 0.0);
76/// assert_eq!(wrap_angle(TAU + 1.0), 1.0);
77/// ```
78pub fn wrap_angle(t: f64) -> f64 {
79    if !t.is_finite() {
80        return t;
81    }
82    let t = if (0.0..core::f64::consts::TAU).contains(&t) {
83        t
84    } else {
85        t.rem_euclid(core::f64::consts::TAU)
86    };
87    if t >= core::f64::consts::TAU { 0.0 } else { t }
88}
89
90/// The end of one whole period from `lo`: `lo + period`, stepped down to
91/// the representable value below when that sum rounds up, so that
92/// `end - lo <= period` holds exactly. A closed edge spans one period and
93/// no more (`docs/DATA-MODEL.md` §Invariants, E1), and for a
94/// `lo` that is not a small multiple of the period the sum can round to
95/// one unit in the last place too far; this is the range's construction,
96/// not a tolerance. A non-finite argument, or a `period` that is not
97/// positive, comes back as `lo + period`.
98///
99/// ```
100/// use arris_math::period_end;
101/// use core::f64::consts::TAU;
102///
103/// assert_eq!(period_end(0.0, TAU), TAU);
104/// // A pave one unit in the last place below a full turn: the sum
105/// // rounds up, and the end is stepped back to keep the turn one turn.
106/// let lo = f64::from_bits(TAU.to_bits() - 1);
107/// assert!(lo + TAU - lo > TAU);
108/// assert!(period_end(lo, TAU) - lo <= TAU);
109/// ```
110pub fn period_end(lo: f64, period: f64) -> f64 {
111    let mut end = lo + period;
112    if !(end.is_finite() && period > 0.0) {
113        return end;
114    }
115    while end - lo > period {
116        end = f64::from_bits(end.to_bits() - 1);
117    }
118    end
119}
120
121/// A position in 3D. `nalgebra::Point3<f64>` (ADR-0001).
122pub type Point3 = nalgebra::Point3<f64>;
123/// A displacement or direction in 3D, of any length.
124/// `nalgebra::Vector3<f64>` (ADR-0001).
125pub type Vec3 = nalgebra::Vector3<f64>;
126/// A direction in 3D: a [`Vec3`] of unit length, guaranteed by construction.
127/// `nalgebra::Unit<Vector3<f64>>` (ADR-0001).
128pub type UnitVec3 = nalgebra::Unit<Vec3>;
129/// A position in a surface's (u, v) plane. `nalgebra::Point2<f64>`
130/// (ADR-0001).
131pub type Point2 = nalgebra::Point2<f64>;
132/// A displacement in the (u, v) plane. `nalgebra::Vector2<f64>` (ADR-0001).
133pub type Vec2 = nalgebra::Vector2<f64>;
134/// A direction in the (u, v) plane: a [`Vec2`] of unit length.
135/// `nalgebra::Unit<Vector2<f64>>` (ADR-0001).
136pub type UnitVec2 = nalgebra::Unit<Vec2>;
137/// A 3x3 matrix, column-major: a rotation, or a tensor such as the
138/// inertia of a body. `nalgebra::Matrix3<f64>` (ADR-0001).
139pub type Matrix3 = nalgebra::Matrix3<f64>;
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use core::f64::consts::{PI, TAU};
145
146    #[test]
147    fn wrap_angle_lands_in_the_half_open_turn() {
148        assert_eq!(wrap_angle(0.0), 0.0);
149        assert_eq!(wrap_angle(-1e-300), 0.0);
150        assert_eq!(wrap_angle(-1.0), TAU - 1.0);
151        assert_eq!(wrap_angle(PI), PI);
152        assert!(wrap_angle(-f64::EPSILON) < TAU);
153        assert_eq!(wrap_angle(3.0 * TAU + 1.0), 1.0);
154        assert_eq!(wrap_angle(-3.0 * TAU - 1.0), TAU - 1.0);
155        assert!(wrap_angle(f64::NAN).is_nan());
156        assert_eq!(wrap_angle(f64::INFINITY), f64::INFINITY);
157    }
158}