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.
76struct 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    fn new() -> Self {
85        Self { state: SEEDS }
86    }
87
88    #[inline]
89    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    fn usize(&mut self, value: usize) {
99        self.u64(value as u64);
100    }
101
102    #[inline]
103    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    fn f64(&mut self, value: f64) {
114        self.u64(value.to_bits());
115    }
116
117    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    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
147fn 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
161fn hash_face(lane: &mut TwoLane, face: &FaceRecord) {
162    hash_surface(lane, &face.surface);
163    lane.bool(face.same_sense);
164    lane.usize(face.loops.len());
165    for loop_record in &face.loops {
166        lane.usize(loop_record.coedges.len());
167        for coedge in &loop_record.coedges {
168            lane.bool(coedge.forward);
169            hash_curve(lane, &coedge.pcurve);
170        }
171    }
172}
173
174/// The 128-bit content identity of one coedge's USE: the face's carrier, the
175/// pcurve, the edge's curve and domain and the coedge's sense — every input
176/// of a reading that compares the trim's image with the edge (the refit's
177/// detection miss, the challenger's old-trim tracking). Two uses with equal
178/// identities read the same number, bit for bit; `reading_memo.rs` keys on it.
179pub(crate) fn coedge_use_identity(
180    surface: &NurbsSurface,
181    pcurve: &NurbsCurve,
182    edge: &crate::topology::EdgeRecord,
183    forward: bool,
184) -> [u64; 2] {
185    let mut lane = TwoLane::new();
186    hash_surface(&mut lane, surface);
187    hash_curve(&mut lane, pcurve);
188    hash_curve(&mut lane, &edge.curve);
189    lane.f64(edge.t0);
190    lane.f64(edge.t1);
191    lane.bool(edge.degenerate);
192    lane.bool(forward);
193    lane.finish()
194}
195
196/// The 128-bit content identity of one face's volume contribution about
197/// `reference` under the sign reading's correction policy: everything
198/// `face_volume_contribution_about` reads (the face as `hash_face` walks it,
199/// the reference point's bits). `reading_memo.rs` keys the boolean's
200/// assembly sign check on it.
201pub(crate) fn face_contribution_identity(face: &FaceRecord, reference: crate::Vec3) -> [u64; 2] {
202    let mut lane = TwoLane::new();
203    hash_face(&mut lane, face);
204    lane.f64(reference.x);
205    lane.f64(reference.y);
206    lane.f64(reference.z);
207    lane.finish()
208}
209
210/// Hash exactly what the integrator reads, in the order it reads it.
211pub fn face_set_identity(solid: &BrepSolid) -> FaceSetIdentity {
212    let mut lane = TwoLane::new();
213    let mut faces = 0u64;
214    lane.usize(solid.shells.len());
215    for shell in &solid.shells {
216        lane.usize(shell.faces.len());
217        for face in &shell.faces {
218            faces += 1;
219            hash_face(&mut lane, face);
220        }
221    }
222    FaceSetIdentity {
223        lanes: lane.finish(),
224        faces,
225    }
226}