Skip to main content

brep_kernel/props/mass_properties/
identity.rs

1//! The content identity of the face set the integrator actually reads.
2//!
3//! Two `solid_mass_properties` / `solid_signed_volume` calls return the same
4//! bits exactly when they see the same geometry in the same order. This hash
5//! is that statement, made checkable: it walks precisely the fields the
6//! integrator reads and nothing else, in the order it reads them.
7//!
8//! **What goes in.** Per shell in order, per face in order: the carrier's
9//! degrees, knot vectors and control points, `same_sense`, then per loop in
10//! order and per coedge in order the pcurve's degree, knots and control
11//! points and the coedge's `forward` flag.
12//!
13//! **What stays out.** `id` and `name` on every record, and the solid's
14//! `vertices` and `edges` — `solid_props.rs` never reads them, so a solid
15//! whose edge table was renumbered integrates to the same bits and must hash
16//! the same. `genus` is topology bookkeeping the integrator never consults.
17//!
18//! **Order, not set.** Both entry points run a compensated sum over the faces
19//! in shell order; permuting the faces permutes the addends and can move the
20//! last bits. So the hash is over the SEQUENCE. "The same face set" is a
21//! station-signature statement; this is the stronger one that licenses
22//! serving a stored value.
23//!
24//! **Why the bits and not the value.** `f64::to_bits` distinguishes `-0.0`
25//! from `0.0` and makes every NaN payload its own key. Both are what the
26//! integrator sees: it can consume a negative zero (the sign survives a
27//! multiply) and it never normalizes a NaN. Hashing the value would let two
28//! inputs the integrator distinguishes share a key.
29
30use crate::{BrepSolid, FaceRecord, NurbsCurve, NurbsSurface, Vec4};
31
32/// The 128-bit content identity of a solid's face set.
33///
34/// Two hashes are equal when the two solids present the integrator with the
35/// same geometry in the same order. It is a hash, so equality is evidence and
36/// not proof — but see the width note on `absorb`: a consumer that serves a
37/// stored value on a hit is betting on 2^-128, and this file owes that bet a
38/// mixer worth the number.
39#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
40pub struct FaceSetIdentity {
41    lanes: [u64; 2],
42    /// Faces walked, carried beside the digest as a cheap first discriminator
43    /// and as the thing a profile line can print.
44    pub faces: u64,
45}
46
47impl FaceSetIdentity {
48    pub fn hex(&self) -> String {
49        format!("{:016x}{:016x}", self.lanes[0], self.lanes[1])
50    }
51}
52
53/// The splitmix64 finalizer (Steele/Lea/Flood, "Fast Splittable Pseudorandom
54/// Number Generators"): two multiply-xorshift rounds that avalanche every
55/// input bit across all 64 output bits.
56#[inline]
57fn mix64(mut z: u64) -> u64 {
58    z ^= z >> 30;
59    z = z.wrapping_mul(0xbf58_476d_1ce4_e5b9);
60    z ^= z >> 27;
61    z = z.wrapping_mul(0x94d0_49bb_1331_11eb);
62    z ^ (z >> 31)
63}
64
65/// Two independent 64-bit streams over the same word sequence, distinguished
66/// by their seed and their per-word increment.
67///
68/// **Why not `FxHasher`.** The crate's fast hasher is a rotate-xor-multiply
69/// per word with no finalizer; it is the right tool for a `HashMap` bucket
70/// index and the wrong one here. Control-point words are highly structured —
71/// exact zeros, exact ones, repeated knots, shared coordinates — and a weak
72/// mixer's collisions cluster on exactly that kind of input. A collision here
73/// does not cost a slow lookup, it serves a WRONG VOLUME silently. splitmix64
74/// per word, twice, is a few ns per station's worth of data against an
75/// integrator that spends 335 ns per station.
76pub(crate) struct TwoLane {
77    state: [u64; 2],
78}
79
80const SEEDS: [u64; 2] = [0x9e37_79b9_7f4a_7c15, 0xc2b2_ae3d_27d4_eb4f];
81const STEPS: [u64; 2] = [0xa076_1d64_78bd_642f, 0xe703_7ed1_a0b4_28db];
82
83impl TwoLane {
84    pub(crate) fn new() -> Self {
85        Self { state: SEEDS }
86    }
87
88    #[inline]
89    pub(crate) fn u64(&mut self, value: u64) {
90        for lane in 0..2 {
91            // Order-sensitive by construction: the state enters the mix, so
92            // permuting the words permutes the digest.
93            self.state[lane] = mix64(self.state[lane] ^ value).wrapping_add(STEPS[lane]);
94        }
95    }
96
97    #[inline]
98    pub(crate) fn usize(&mut self, value: usize) {
99        self.u64(value as u64);
100    }
101
102    #[inline]
103    pub(crate) fn bool(&mut self, value: bool) {
104        self.u64(value as u64);
105    }
106
107    /// `to_bits`, not the value: it separates `-0.0` from `0.0` and gives every
108    /// NaN payload its own key. Both are what the integrator sees — it can
109    /// consume a negative zero (the sign survives a multiply) and it never
110    /// normalizes a NaN — so hashing the value would let two inputs the
111    /// integrator distinguishes share a key.
112    #[inline]
113    pub(crate) fn f64(&mut self, value: f64) {
114        self.u64(value.to_bits());
115    }
116
117    pub(crate) fn floats(&mut self, values: &[f64]) {
118        // Length first: without it `[a], [b, c]` and `[a, b], [c]` are the
119        // same word stream.
120        self.usize(values.len());
121        for value in values {
122            self.f64(*value);
123        }
124    }
125
126    fn control_point(&mut self, point: &Vec4) {
127        self.f64(point.x);
128        self.f64(point.y);
129        self.f64(point.z);
130        self.f64(point.w);
131    }
132
133    pub(crate) fn finish(self) -> [u64; 2] {
134        [mix64(self.state[0]), mix64(self.state[1])]
135    }
136}
137
138pub(crate) fn hash_curve(lane: &mut TwoLane, curve: &NurbsCurve) {
139    lane.usize(curve.degree);
140    lane.floats(&curve.knots);
141    lane.usize(curve.control_points.len());
142    for point in &curve.control_points {
143        lane.control_point(point);
144    }
145}
146
147pub(crate) fn hash_surface(lane: &mut TwoLane, surface: &NurbsSurface) {
148    lane.usize(surface.degree_u);
149    lane.usize(surface.degree_v);
150    lane.floats(&surface.knots_u);
151    lane.floats(&surface.knots_v);
152    lane.usize(surface.control_points.len());
153    for row in &surface.control_points {
154        lane.usize(row.len());
155        for point in row {
156            lane.control_point(point);
157        }
158    }
159}
160
161/// Everything the per-face tessellator reads off a face record besides its
162/// coedges' shared edge samples: the carrier, `same_sense`, and per loop and
163/// coedge in order the sense and the pcurve. `watertight_tessellation::face_memo`
164/// keys a face's mesh fragment on this plus the samples' bits.
165pub(crate) fn hash_face(lane: &mut TwoLane, face: &FaceRecord) {
166    hash_surface(lane, &face.surface);
167    lane.bool(face.same_sense);
168    lane.usize(face.loops.len());
169    for loop_record in &face.loops {
170        lane.usize(loop_record.coedges.len());
171        for coedge in &loop_record.coedges {
172            lane.bool(coedge.forward);
173            hash_curve(lane, &coedge.pcurve);
174        }
175    }
176}
177
178/// The 128-bit content identity of one coedge's USE: the face's carrier, the
179/// pcurve, the edge's curve and domain and the coedge's sense — every input
180/// of a reading that compares the trim's image with the edge (the refit's
181/// detection miss, the challenger's old-trim tracking). Two uses with equal
182/// identities read the same number, bit for bit; `reading_memo.rs` keys on it.
183pub(crate) fn coedge_use_identity(
184    surface: &NurbsSurface,
185    pcurve: &NurbsCurve,
186    edge: &crate::topology::EdgeRecord,
187    forward: bool,
188) -> [u64; 2] {
189    let mut lane = TwoLane::new();
190    hash_surface(&mut lane, surface);
191    hash_curve(&mut lane, pcurve);
192    hash_curve(&mut lane, &edge.curve);
193    lane.f64(edge.t0);
194    lane.f64(edge.t1);
195    lane.bool(edge.degenerate);
196    lane.bool(forward);
197    lane.finish()
198}
199
200/// Inputs of assembly's sampled edge-to-carrier residual. IDs, trims and
201/// tolerance are absent because the reader does not consult them. Preserve
202/// carrier order, represented interval, and every geometric bit.
203pub(crate) fn carrier_residual_identity(
204    curve: &NurbsCurve, t0: f64, t1: f64, surfaces: &[&NurbsSurface],
205) -> [u64; 2] {
206    let mut lane = TwoLane::new();
207    hash_curve(&mut lane, curve);
208    lane.f64(t0);
209    lane.f64(t1);
210    lane.usize(surfaces.len());
211    for surface in surfaces { hash_surface(&mut lane, surface); }
212    lane.finish()
213}
214
215/// `key` with a tolerance folded in: the identity of a reading that is a pure
216/// function of a use's content AND one policy value (the refit's candidacy
217/// standoff, read against `policy.model`). The same content under another
218/// model is another key.
219pub(crate) fn identity_with_model(key: [u64; 2], model: f64) -> [u64; 2] {
220    let mut lane = TwoLane::new();
221    lane.u64(key[0]);
222    lane.u64(key[1]);
223    lane.f64(model);
224    lane.finish()
225}
226
227/// The 128-bit identity of an EDGE's scope claim (`heal::edge_claim_with`):
228/// the identities of the edge's uses in the order the claim is handed them,
229/// the order in which it visits their distinct carriers (`ranks`, the first
230/// use of each carrier in that visiting order) and the model tolerance.
231pub(crate) fn edge_claim_identity(uses: &[[u64; 2]], ranks: &[usize], model: f64) -> [u64; 2] {
232    let mut lane = TwoLane::new();
233    lane.usize(uses.len());
234    for key in uses {
235        lane.u64(key[0]);
236        lane.u64(key[1]);
237    }
238    lane.usize(ranks.len());
239    for &rank in ranks {
240        lane.usize(rank);
241    }
242    lane.f64(model);
243    lane.finish()
244}
245
246/// The 128-bit content identity of one face's volume contribution about
247/// `reference` under the sign reading's correction policy: everything
248/// `face_volume_contribution_about` reads (the face as `hash_face` walks it,
249/// the reference point's bits). `reading_memo.rs` keys the boolean's
250/// assembly sign check on it.
251pub(crate) fn face_contribution_identity(face: &FaceRecord, reference: crate::Vec3) -> [u64; 2] {
252    let mut lane = TwoLane::new();
253    hash_face(&mut lane, face);
254    lane.f64(reference.x);
255    lane.f64(reference.y);
256    lane.f64(reference.z);
257    lane.finish()
258}
259
260/// Hash exactly what the integrator reads, in the order it reads it.
261pub fn face_set_identity(solid: &BrepSolid) -> FaceSetIdentity {
262    let mut lane = TwoLane::new();
263    let mut faces = 0u64;
264    lane.usize(solid.shells.len());
265    for shell in &solid.shells {
266        lane.usize(shell.faces.len());
267        for face in &shell.faces {
268            faces += 1;
269            hash_face(&mut lane, face);
270        }
271    }
272    FaceSetIdentity {
273        lanes: lane.finish(),
274        faces,
275    }
276}