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}