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
138fn 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}