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 control;
22mod disjoint_sets;
23mod frame;
24mod interval;
25mod isometry;
26mod periodic;
27mod precision;
28pub mod predicates;
29pub mod real;
30mod reflection;
31pub mod roots;
32pub mod sweep_steps;
33mod tolerance;
34
35pub use nalgebra;
36
37pub use aabb::Aabb;
38pub use axis::Axis;
39pub use control::{Control, Interrupted, Meter, Poll, Stop};
40pub use disjoint_sets::DisjointSets;
41pub use frame::{Frame, Frame2, FrameError, Handedness};
42pub use interval::{Interval, IntervalError};
43pub use isometry::Isometry;
44pub use periodic::{
45 period_end, shift_into_range, shift_nearest, shift_nearest_uv, wrap_angle, wrap_into,
46 wrap_offset, wrap_signed,
47};
48pub use precision::Precision;
49pub use reflection::{Reflection, ReflectionError};
50pub use tolerance::Tolerance;
51
52/// Relative rounding slack: a magnitude at or below this fraction of its
53/// natural scale is rounding noise, not a value. Eight ulps — what a
54/// handful of multiplications and one trigonometric evaluation leave
55/// behind, and orders of magnitude below any model tolerance. It is not a
56/// geometric tolerance and never decides whether two things are *the
57/// same*; it decides whether a computed quantity is zero *in floating
58/// point*: the radius of a sphere's parallel at the pole, `cos(π/2)`
59/// evaluated in `f64`, is `6e-17`, not `0`.
60pub const RELATIVE_ROUNDING: f64 = 8.0 * f64::EPSILON;
61
62/// The widest gap a file's entities may leave that a read closes by a
63/// tolerance, as a fraction of the part's size — the diameter of the ball
64/// holding its vertices and edges (ADR-0025 §4). A vertex or an edge whose
65/// measured gap is above it, or above `Precision::max_tolerance`, is
66/// refused as a gap: closing it would be sewing, which is healing, and a
67/// solid grown to cover it would be a wrong one.
68///
69/// Evidence: an exact writer leaves gaps at the rounding of its decimal
70/// digits, around `1e-15` of the part (Arris's own files); a kernel's
71/// raised tolerances reach a few `1e-7` on a part of unit size (the
72/// corpus's `boolean/seam-a-tolerance-from-crossing-fuse`, `1.2e-7` on a
73/// part of size 7). Open CASCADE's own STEP of every corpus fixture
74/// (the corpus's read-back stage) measures at most `2.5e-6` of the part's
75/// diagonal on 201 of 202 files, and `8.5e-5` on the last — `7.4e-4` on a
76/// part `8.7` across, `boolean/seam-beside-crossing-fuse`, whose boolean
77/// Open CASCADE itself gets wrong by that much. Converted to B-splines
78/// (step 15), its `closed-form-band` variant measures `1.05e-3` on a part
79/// whose ball is `9.3` across, `1.13e-4` of it — exactly the tolerance
80/// Open CASCADE's own shape carries there, which its checker passes.
81/// Healing tools commonly default their sewing tolerance to `1e-6`–`1e-3`
82/// of the model's size, so `2e-4` admits every file an exporting kernel
83/// called valid by its own tolerances, the largest seen with `1.8×` to
84/// spare, and refuses the gaps a healer would have to sew. A tolerance
85/// policy, not a rounding slack. Over the real-part corpus it refuses
86/// gaps in 7 of NIST's 38 parts, which count for the healing cycle
87/// (ADR-0026 §5, `docs/ROADMAP.md` §C4).
88pub const READ_GAP_FRACTION: f64 = 2e-4;
89
90/// The healing ceiling: the largest gap, as a fraction of the part's size
91/// (the same ball [`READ_GAP_FRACTION`] is of), that a STEP read heals by
92/// growing the tolerance of the entity it lies at (ADR-0058). A gap past
93/// it is still a refusal.
94///
95/// Measured on the real-part corpus by the gap census (`real_parts
96/// --gaps`): the seven parts the read refuses a gap of hold gaps of
97/// `8.6e-6` to `1.5e-4` of their size, the largest FTC-11's `0.0141` on a
98/// part `96.5` across, and Open CASCADE's reading of the same parts
99/// carries tolerances of up to `2.6e-4` of the part (FTC-07's `0.104` on
100/// `407`; CTC-02's `0.108` on `1167` is `9e-5`). `5e-4` is `3.4×` the
101/// largest gap Arris measured and `1.9×` the largest Open CASCADE carries: room for a part
102/// a little worse than any seen, and under every healing tool's usual
103/// sewing tolerance (`1e-3` of the size). A tolerance policy, not a
104/// rounding slack.
105pub const HEAL_GAP_FRACTION: f64 = 5e-4;
106
107/// The most a read edge's gap is raised for the angle its two faces cross
108/// at (ADR-0059): an edge lying `d` off one face and on the other is
109/// `d / sin θ` from where faces crossing at `θ` meet, and its tolerance
110/// is raised to that where `1 / sin θ` is at most this — faces crossing at
111/// `30°` or more. Under it the faces are near tangent, where they meet is
112/// ill-conditioned (a G1 blend's faces meet anywhere along a band), and
113/// the edge keeps its gap from each surface.
114///
115/// Evidence: CTC-02's drafted bosses, a cylinder `2°` off the normal of
116/// the plane it stands on, `1 / sin θ = 1.0006`
117/// (`real/edge-off-a-drafted-cylinder`). A geometric bound, not a
118/// rounding slack.
119pub const SECTION_GROWTH_LIMIT: f64 = 2.0;
120
121/// The parameter resolution below which two knots are coincident and
122/// form no distinct span. Open CASCADE's `Precision::PConfusion` is
123/// `1e-9`; the model's [`Precision::parametric_tolerance`] is `1e-7` by
124/// default. Knot spans narrower than this are merged before isolation.
125pub const KNOT_RESOLUTION: f64 = 1e-7;
126
127/// `|x| ≤ RELATIVE_ROUNDING · |scale|`: `x` is zero to rounding at
128/// `scale`. A zero `scale` makes only an exact zero negligible.
129///
130/// ```
131/// use arris_math::is_negligible;
132/// use core::f64::consts::FRAC_PI_2;
133///
134/// assert!(is_negligible(3.0 * FRAC_PI_2.cos(), 3.0));
135/// assert!(!is_negligible(3.0 * (FRAC_PI_2 - 1e-9).cos(), 3.0));
136/// ```
137pub fn is_negligible(x: f64, scale: f64) -> bool {
138 x.abs() <= RELATIVE_ROUNDING * scale.abs()
139}
140
141/// A position in 3D. `nalgebra::Point3<f64>` (ADR-0001).
142pub type Point3 = nalgebra::Point3<f64>;
143/// A displacement or direction in 3D, of any length.
144/// `nalgebra::Vector3<f64>` (ADR-0001).
145pub type Vec3 = nalgebra::Vector3<f64>;
146/// A direction in 3D: a [`Vec3`] of unit length, guaranteed by construction.
147/// `nalgebra::Unit<Vector3<f64>>` (ADR-0001).
148pub type UnitVec3 = nalgebra::Unit<Vec3>;
149/// A position in a surface's (u, v) plane. `nalgebra::Point2<f64>`
150/// (ADR-0001).
151pub type Point2 = nalgebra::Point2<f64>;
152/// A displacement in the (u, v) plane. `nalgebra::Vector2<f64>` (ADR-0001).
153pub type Vec2 = nalgebra::Vector2<f64>;
154/// A direction in the (u, v) plane: a [`Vec2`] of unit length.
155/// `nalgebra::Unit<Vector2<f64>>` (ADR-0001).
156pub type UnitVec2 = nalgebra::Unit<Vec2>;
157/// A 3x3 matrix, column-major: a rotation, or a tensor such as the
158/// inertia of a body. `nalgebra::Matrix3<f64>` (ADR-0001).
159pub type Matrix3 = nalgebra::Matrix3<f64>;
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use core::f64::consts::{PI, TAU};
165
166 #[test]
167 fn wrap_angle_lands_in_the_half_open_turn() {
168 assert_eq!(wrap_angle(0.0), 0.0);
169 assert_eq!(wrap_angle(-1e-300), 0.0);
170 assert_eq!(wrap_angle(-1.0), TAU - 1.0);
171 assert_eq!(wrap_angle(PI), PI);
172 assert!(wrap_angle(-f64::EPSILON) < TAU);
173 assert_eq!(wrap_angle(3.0 * TAU + 1.0), 1.0);
174 assert_eq!(wrap_angle(-3.0 * TAU - 1.0), TAU - 1.0);
175 assert!(wrap_angle(f64::NAN).is_nan());
176 assert_eq!(wrap_angle(f64::INFINITY), f64::INFINITY);
177 }
178}