Skip to main content

brep_kernel/io/step_import/
precision.rs

1//! Stated precision as a consistency check at import.
2//!
3//! A STEP file states its own precision in `UNCERTAINTY_MEASURE_WITH_UNIT`.
4//! Seeding a per-entity tolerance band from that figure was measured across
5//! the 54-fixture corpus and DROPPED: the figure is not a property of the
6//! geometry — half the corpus states 1e-17 (exporter boilerplate seven
7//! orders under the policy floor), three files state several values at once,
8//! and where a seed would act at all it could only widen a band on the
9//! exporter's word. What survives is the other direction of the same
10//! comparison: a file whose stated figure is far UNDER the deviation its own
11//! edges show is misdescribing itself, and the import should say so.
12//!
13//! This module is that diagnostic. It compares the loosest figure the file
14//! states against the largest edge-curve-vs-carrier disagreement and vertex
15//! gap the imported bodies measure (`EntityTolerances`, the same reading the
16//! census took, unclamped), records both as measurements, and emits one
17//! warning when the measured deviation exceeds the stated figure by more
18//! than [`STATED_PRECISION_INCONSISTENCY_RATIO`]. It never changes
19//! acceptance: nothing here is a band, a gate, or an input to one.
20//!
21//! Units are load-bearing. The stated figure arrives already scaled to
22//! millimetres through the unit its own entity names
23//! (`parse::stated_precisions_mm`); 31 of the 48 corpus fixtures that state a
24//! precision declare metres, and comparing the raw figure inverts the answer.
25
26use super::BrepSolid;
27use crate::{DiagnosticSeverity, EntityTolerances, KernelDiagnostics, KernelStage};
28
29/// How far the measured deviation may exceed the stated precision before the
30/// file is called inconsistent with itself.
31///
32/// A decade, set from the corpus (54 files). Of the 48 that state a
33/// precision, 25 state exporter boilerplate (`1.E-17` m = 1e-14 mm, under
34/// double resolution at any part size) and measure 3e4× to 5e14× over it; 14
35/// state a real export tolerance their geometry meets, at 0.07× to 2.3× (two
36/// outliers at 4.8× and 9.0×); and 9 state a real figure their geometry
37/// misses by 12× to 5e4× (`EngineBlock` 1e-7 mm against 5.1e-3 measured;
38/// `raspberry_pi_3_molex` 1e-5 against 9.8e-4). A decade sits in the gap
39/// between the second and third groups, says "an order of magnitude" in the
40/// same breath it names the numbers, and leaves headroom for what the
41/// measurement itself cannot see below: the edge reading is the vendor's 3D
42/// curve against the kernel's OWN projected pcurve, whose fit residual is of
43/// order 1e-6..1e-5 mm, so a stated 5e-6 mm cannot be judged finer than that.
44pub const STATED_PRECISION_INCONSISTENCY_RATIO: f64 = 10.0;
45
46/// Measurement keys the report always carries when the file states a
47/// precision, so a caller can read the comparison whether or not it fired.
48pub const STATED_PRECISION_MM: &str = "import.stated_precision_mm";
49pub const STATED_PRECISION_TIGHTEST_MM: &str = "import.stated_precision_tightest_mm";
50pub const MEASURED_EDGE_DEVIATION_MM: &str = "import.measured_edge_deviation_mm";
51pub const MEASURED_VERTEX_GAP_MM: &str = "import.measured_vertex_gap_mm";
52/// The event code, and the counter of how many bodies were measured.
53pub const STATED_PRECISION_INCONSISTENT: &str = "import.stated_precision_inconsistent";
54pub const MEASURED_BODIES: &str = "import.stated_precision.measured_bodies";
55
56/// The stated-precision consistency check over `solids`, which imported from
57/// a file stating `stated_mm` (ascending, in mm; empty when it states none).
58///
59/// A file that states nothing is not measured — there is nothing to compare
60/// against, and the measurement is the cost of walking every edge. A file
61/// that states several figures is judged against the LOOSEST: the check is
62/// meant to be believed when it fires, and a file with one claim that covers
63/// its deviation is not unambiguously misdescribing itself. The tightest is
64/// recorded beside it so the spread is visible.
65///
66/// Every measurement is taken through `EntityTolerances::measured_*`, the
67/// unclamped readings, so a defective entity reports its real deviation and
68/// not the cap. A measurement that cannot be taken is skipped; it can only
69/// make the check quieter, never louder, and never fails the import.
70pub(super) fn stated_precision_diagnostics(
71    stated_mm: &[f64],
72    solids: &[BrepSolid],
73) -> KernelDiagnostics {
74    let mut diagnostics = KernelDiagnostics::default();
75    let (Some(&tightest), Some(&loosest)) = (stated_mm.first(), stated_mm.last()) else {
76        return diagnostics;
77    };
78    diagnostics.measure_max(STATED_PRECISION_MM, loosest);
79    diagnostics.measure_max(STATED_PRECISION_TIGHTEST_MM, tightest);
80
81    // (deviation, solid index, entity id) — the worst edge and vertex.
82    let mut worst_edge: Option<(f64, usize, u64)> = None;
83    let mut worst_vertex: Option<(f64, usize, u64)> = None;
84    let mut scale = 0.0f64;
85    for (index, solid) in solids.iter().enumerate() {
86        diagnostics.count(MEASURED_BODIES);
87        scale = scale.max(crate::solid_scale(solid));
88        let mut tolerances = EntityTolerances::with_floor(solid, 0.0);
89        for edge in &solid.edges {
90            if edge.degenerate {
91                continue;
92            }
93            if let Some(deviation) = tolerances.measured_edge_deviation(edge.id) {
94                if worst_edge.is_none_or(|(w, _, _)| deviation > w) {
95                    worst_edge = Some((deviation, index, edge.id));
96                }
97            }
98        }
99        for vertex in &solid.vertices {
100            if let Some(gap) = tolerances.measured_vertex_gap(vertex.id) {
101                if worst_vertex.is_none_or(|(w, _, _)| gap > w) {
102                    worst_vertex = Some((gap, index, vertex.id));
103                }
104            }
105        }
106    }
107    if let Some((deviation, _, _)) = worst_edge {
108        diagnostics.measure_max(MEASURED_EDGE_DEVIATION_MM, deviation);
109    }
110    if let Some((gap, _, _)) = worst_vertex {
111        diagnostics.measure_max(MEASURED_VERTEX_GAP_MM, gap);
112    }
113
114    // The worse of the two readings decides; the message names whichever it is.
115    let worst = match (worst_edge, worst_vertex) {
116        (Some(edge), Some(vertex)) => {
117            if edge.0 >= vertex.0 {
118                Some(("edge", edge))
119            } else {
120                Some(("vertex", vertex))
121            }
122        }
123        (Some(edge), None) => Some(("edge", edge)),
124        (None, Some(vertex)) => Some(("vertex", vertex)),
125        (None, None) => None,
126    };
127    let Some((kind, (measured, index, id))) = worst else {
128        return diagnostics;
129    };
130    if !(measured > loosest * STATED_PRECISION_INCONSISTENCY_RATIO) {
131        return diagnostics;
132    }
133    let ratio = measured / loosest;
134    let declarations = if stated_mm.len() > 1 {
135        format!(
136            " (the loosest of {} distinct figures, tightest {:.3e} mm)",
137            stated_mm.len(),
138            tightest
139        )
140    } else {
141        String::new()
142    };
143    // A figure under the resolution of a double at the part's own size is not
144    // a claim any geometry could meet, whatever the exporter wrote.
145    let resolution = f64::EPSILON * scale;
146    let boilerplate = if loosest < resolution {
147        format!(
148            " The stated figure is below double resolution at this part's size ({:.1e} mm), so it cannot describe any geometry.",
149            resolution
150        )
151    } else {
152        String::new()
153    };
154    diagnostics.event(
155        DiagnosticSeverity::Warning,
156        KernelStage::Collect,
157        STATED_PRECISION_INCONSISTENT,
158        format!(
159            "the file states a precision of {loosest:.3e} mm{declarations}, but its own geometry disagrees with itself by up to {measured:.3e} mm ({kind} {id} of body {index}), {ratio:.3e}× the stated figure: the stated precision does not describe this geometry.{boilerplate} Acceptance is unchanged."
160        ),
161    );
162    diagnostics
163}