Skip to main content

arris_math/
precision.rs

1//! Model-wide tolerance configuration.
2
3/// The tolerance configuration of one model, set when the model is created
4/// (`docs/DATA-MODEL.md` §Tolerances).
5///
6/// Arris carries no unit; `Precision` is what makes a model's numbers
7/// meaningful. Every tolerance an algorithm uses is an entity's own or a
8/// field of this struct — never a literal. The defaults are for a model
9/// whose features are of order 1–1000 units; a consumer in metres sets
10/// `default_tolerance` at the micrometre scale.
11///
12/// The fields are public: a `Precision` is plain configuration data, and
13/// the checker's V1/F2 rows (`docs/DATA-MODEL.md` §Invariants) hold every
14/// entity to the bounds it states.
15///
16/// ```
17/// use arris_math::Precision;
18///
19/// let p = Precision { default_tolerance: 1e-6, ..Precision::DEFAULT };
20/// assert!(p.min_tolerance <= p.default_tolerance);
21/// assert!(p.default_tolerance <= p.max_tolerance);
22/// ```
23#[derive(Debug, Clone, Copy, PartialEq)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25pub struct Precision {
26    /// The tolerance a primitive's entities are created with. Two points
27    /// closer than this are the same point.
28    pub default_tolerance: f64,
29    /// The floor: no entity carries a tolerance below it. It bounds how
30    /// far an operation may *tighten* a tolerance and is the smallest
31    /// distance the model distinguishes.
32    pub min_tolerance: f64,
33    /// The ceiling: an operation whose result would need an entity
34    /// tolerance above it returns an error instead of a sloppy body.
35    pub max_tolerance: f64,
36    /// Angle in radians below which two directions are parallel and two
37    /// surfaces are tangent.
38    pub angular_tolerance: f64,
39    /// How far a pcurve may deviate in (u, v), at unit parametric speed —
40    /// on a surface where one unit of parameter is one unit of length. The
41    /// checker scales it by the surface's parametric derivative, so on a
42    /// cylinder of radius `r` the bound in `u` is this divided by `r`.
43    pub parametric_tolerance: f64,
44    /// How many parameters the checker samples along an edge when it
45    /// compares a pcurve's image against the 3D curve (invariant E4),
46    /// both ends included.
47    pub check_samples: usize,
48}
49
50impl Precision {
51    /// The defaults, as a constant so they can be spread into a struct
52    /// literal. Values: `default_tolerance` 1e-7 (Open CASCADE's
53    /// `Precision::Confusion`, so the oracle and Arris merge the same
54    /// points), `min_tolerance` 1e-12, `max_tolerance` 1e-2,
55    /// `angular_tolerance` 1e-12 (Open CASCADE's `Precision::Angular`),
56    /// `parametric_tolerance` 1e-7, `check_samples` 23 (the sample count of
57    /// Open CASCADE's edge check, so an edge the oracle accepts is sampled
58    /// at least as finely here).
59    pub const DEFAULT: Precision = Precision {
60        default_tolerance: 1e-7,
61        min_tolerance: 1e-12,
62        max_tolerance: 1e-2,
63        angular_tolerance: 1e-12,
64        parametric_tolerance: 1e-7,
65        check_samples: 23,
66    };
67
68    /// `true` when the fields are finite, positive and ordered
69    /// (`min_tolerance ≤ default_tolerance ≤ max_tolerance`) and there is at
70    /// least one sample; a `Precision` that fails this is rejected when a
71    /// model is created.
72    pub fn is_consistent(&self) -> bool {
73        let finite_positive = |x: f64| x.is_finite() && x > 0.0;
74        finite_positive(self.min_tolerance)
75            && finite_positive(self.default_tolerance)
76            && finite_positive(self.max_tolerance)
77            && finite_positive(self.angular_tolerance)
78            && finite_positive(self.parametric_tolerance)
79            && self.min_tolerance <= self.default_tolerance
80            && self.default_tolerance <= self.max_tolerance
81            && self.check_samples >= 1
82    }
83}
84
85impl Default for Precision {
86    fn default() -> Self {
87        Self::DEFAULT
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn defaults_are_consistent() {
97        assert!(Precision::DEFAULT.is_consistent());
98        assert_eq!(Precision::default(), Precision::DEFAULT);
99    }
100
101    #[test]
102    fn disordered_or_non_finite_is_inconsistent() {
103        let p = Precision {
104            min_tolerance: 1.0,
105            ..Precision::DEFAULT
106        };
107        assert!(!p.is_consistent());
108        let p = Precision {
109            max_tolerance: f64::NAN,
110            ..Precision::DEFAULT
111        };
112        assert!(!p.is_consistent());
113        let p = Precision {
114            check_samples: 0,
115            ..Precision::DEFAULT
116        };
117        assert!(!p.is_consistent());
118    }
119
120    #[cfg(feature = "serde")]
121    #[test]
122    fn serde_round_trip() {
123        let p = Precision {
124            default_tolerance: 1e-6,
125            ..Precision::DEFAULT
126        };
127        let text = serde_json::to_string(&p).unwrap();
128        let back: Precision = serde_json::from_str(&text).unwrap();
129        assert_eq!(p, back);
130    }
131}