manifold_rust/robust/exact/rational.rs
1// robust/exact/rational.rs — Exact rational points and correctly rounded
2// rational→f64 conversion for the robust boolean engine.
3//
4// Input mesh vertices are finite f64 and convert to Rational exactly
5// (`rat`, `R3::from_vec3`). Constructed points — plane/segment intersections
6// built in robust/exact/predicates.rs — stay rational through the whole
7// pipeline; only output assembly rounds them back, via `rat_to_f64`, which
8// rounds to the *nearest* f64 (ties to even, subnormals and overflow
9// handled). That single-rounding guarantee is what lets the robust engine's
10// output vertices agree with the exact engine's to the last ulp on
11// intersection points, and bit-for-bit on pass-through input vertices.
12
13use super::backend::{
14 self, denom, int_mag, numer_mag, rat_from_f64, rat_is_negative, rat_is_zero, Int, Rational,
15 Signed, ToPrimitive, Uint,
16};
17
18use crate::linalg::{Vec2, Vec3};
19
20/// Exact conversion of a finite f64. Every finite f64 is a dyadic rational,
21/// so this never loses information.
22///
23/// Panics on NaN/infinity — mesh import rejects non-finite vertices
24/// (`Error::NonFiniteVertex`) long before the robust engine runs, so a
25/// non-finite value here is an internal logic error, not bad user input.
26#[inline]
27pub fn rat(v: f64) -> Rational {
28 rat_from_f64(v).expect("robust engine: coordinate must be finite")
29}
30
31/// 2^e as f64, exact for the full representable range -1074..=1023
32/// (subnormal powers of two included). Built by bit manipulation so no
33/// intermediate rounding can occur.
34#[inline]
35fn pow2(e: i64) -> f64 {
36 debug_assert!((-1074..=1023).contains(&e), "pow2 exponent out of range: {e}");
37 if e >= -1022 {
38 f64::from_bits(((e + 1023) as u64) << 52)
39 } else {
40 f64::from_bits(1u64 << (e + 1074))
41 }
42}
43
44/// Round a rational to the nearest f64, ties to even — the correctly rounded
45/// result, identical to rounding the exact real value once. Values beyond
46/// f64 range become ±infinity; values below half the smallest subnormal
47/// become (signed) zero.
48pub fn rat_to_f64(r: &Rational) -> f64 {
49 if rat_is_zero(r) {
50 return 0.0;
51 }
52 let n_mag = numer_mag(r);
53 mag_ratio_to_f64(n_mag.as_ref(), denom(r), rat_is_negative(r)).0
54}
55
56/// Correctly rounded f64 of the exact value `numer / denom`, built straight
57/// from big integers — no `Rational`, hence no gcd reduction. Also reports
58/// whether the conversion was EXACT (the returned f64 equals `numer/denom`
59/// with no rounding at all).
60///
61/// The rounding core is shared with [`rat_to_f64`] and is value-based, not
62/// representation-based: it derives the binary exponent from bit lengths and
63/// rounds with one exact integer division, so an unreduced fraction and its
64/// reduced form produce the identical f64. That is what makes this a drop-in
65/// replacement for "subtract exactly in `Rational`, then round" — the two
66/// paths round the same exact value and are therefore bit-identical.
67///
68/// `denom` must be nonzero.
69pub fn int_ratio_to_f64(numer: &Int, denom: &Int) -> (f64, bool) {
70 debug_assert!(!denom.is_zero(), "int_ratio_to_f64: zero denominator");
71 if numer.is_zero() {
72 // Exact: the value is zero, and +0.0 is what `rat_to_f64` returns for
73 // a zero rational whatever the denominator's sign.
74 return (0.0, true);
75 }
76 let neg = numer.is_negative() != denom.is_negative();
77 mag_ratio_to_f64(&int_mag(numer), &int_mag(denom), neg)
78}
79
80/// `±n/d` from unsigned magnitudes (`n`, `d` both nonzero), correctly rounded
81/// to nearest with ties to even, plus an exactness flag. Values beyond f64
82/// range become ±infinity; values below half the smallest subnormal become
83/// (signed) zero. Both of those, and every rounding, report `false`.
84fn mag_ratio_to_f64(n: &Uint, d: &Uint, neg: bool) -> (f64, bool) {
85 // Exact floor exponent e: 2^e <= n/d < 2^(e+1).
86 let mut e = backend::uint_bits(n) as i64 - backend::uint_bits(d) as i64;
87 let ge = if e >= 0 {
88 *n >= (d << e as usize)
89 } else {
90 (n << (-e) as usize) >= *d
91 };
92 if !ge {
93 e -= 1;
94 }
95 if e > 1023 {
96 return (if neg { f64::NEG_INFINITY } else { f64::INFINITY }, false);
97 }
98
99 // Position of the result's least significant bit. Normal numbers carry
100 // 53 bits ending at e-52; subnormals are cut off at 2^-1074.
101 let lsb = (e - 52).max(-1074);
102
103 // m = round_nearest_even((n/d) / 2^lsb), computed with one exact
104 // integer division plus a remainder comparison.
105 let (num, den) = if lsb >= 0 {
106 (n.clone(), d << lsb as usize)
107 } else {
108 (n << (-lsb) as usize, d.clone())
109 };
110 let q = &num / &den;
111 let rem = &num - &q * &den;
112 // `q` is the truncation of the value to a multiple of 2^lsb, so a zero
113 // remainder means the value IS such a multiple: no rounding happens below
114 // (both increments require a nonzero remainder) and the result is exact.
115 let exact = rem.is_zero();
116 let mut m = q;
117 let twice_rem = &rem << 1usize;
118 match twice_rem.cmp(&den) {
119 std::cmp::Ordering::Greater => m += 1u32,
120 std::cmp::Ordering::Equal => {
121 if backend::uint_bit(&m, 0) {
122 m += 1u32;
123 }
124 }
125 std::cmp::Ordering::Less => {}
126 }
127
128 if m.is_zero() {
129 // `n`/`d` are nonzero, so a zero mantissa means the value underflowed
130 // to (signed) zero — never exact.
131 return (if neg { -0.0 } else { 0.0 }, false);
132 }
133 // Rounding up may have crossed into the next binade (m = 2^53) or past
134 // the largest finite value (2^1024 -> infinity).
135 if backend::uint_bits(&m) as i64 - 1 + lsb > 1023 {
136 return (if neg { f64::NEG_INFINITY } else { f64::INFINITY }, false);
137 }
138 // m <= 2^53, so both the u64 and the f64 conversion are exact, and
139 // m * 2^lsb is representable by construction — the multiply is exact.
140 let val = m.to_u64().expect("mantissa fits in u64") as f64 * pow2(lsb);
141 (if neg { -val } else { val }, exact)
142}
143
144// ─── R2 — exact 2D point/vector ─────────────────────────────────────────────
145
146/// Exact 2D point (or vector). Derived `Ord` is lexicographic, which the
147/// arrangement code uses for exact point dedup in BTree maps.
148#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
149pub struct R2 {
150 pub x: Rational,
151 pub y: Rational,
152}
153
154impl R2 {
155 #[inline]
156 pub fn new(x: Rational, y: Rational) -> Self {
157 Self { x, y }
158 }
159
160 #[inline]
161 pub fn from_vec2(v: Vec2) -> Self {
162 Self::new(rat(v.x), rat(v.y))
163 }
164
165 pub fn to_vec2_rounded(&self) -> Vec2 {
166 Vec2::new(rat_to_f64(&self.x), rat_to_f64(&self.y))
167 }
168
169 pub fn sub(&self, o: &R2) -> R2 {
170 R2::new(&self.x - &o.x, &self.y - &o.y)
171 }
172
173 pub fn add(&self, o: &R2) -> R2 {
174 R2::new(&self.x + &o.x, &self.y + &o.y)
175 }
176
177 pub fn scale(&self, s: &Rational) -> R2 {
178 R2::new(&self.x * s, &self.y * s)
179 }
180
181 pub fn dot(&self, o: &R2) -> Rational {
182 &self.x * &o.x + &self.y * &o.y
183 }
184
185 /// 2D cross product (z of the 3D cross of the embedded vectors).
186 pub fn cross(&self, o: &R2) -> Rational {
187 &self.x * &o.y - &self.y * &o.x
188 }
189
190 pub fn is_zero(&self) -> bool {
191 rat_is_zero(&self.x) && rat_is_zero(&self.y)
192 }
193}
194
195// ─── R3 — exact 3D point/vector ─────────────────────────────────────────────
196
197/// Exact 3D point (or vector). Derived `Ord` is lexicographic (x, y, z) for
198/// exact vertex welding in output assembly.
199#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
200pub struct R3 {
201 pub x: Rational,
202 pub y: Rational,
203 pub z: Rational,
204}
205
206impl R3 {
207 #[inline]
208 pub fn new(x: Rational, y: Rational, z: Rational) -> Self {
209 Self { x, y, z }
210 }
211
212 #[inline]
213 pub fn from_vec3(v: Vec3) -> Self {
214 Self::new(rat(v.x), rat(v.y), rat(v.z))
215 }
216
217 pub fn to_vec3_rounded(&self) -> Vec3 {
218 Vec3::new(rat_to_f64(&self.x), rat_to_f64(&self.y), rat_to_f64(&self.z))
219 }
220
221 pub fn sub(&self, o: &R3) -> R3 {
222 R3::new(&self.x - &o.x, &self.y - &o.y, &self.z - &o.z)
223 }
224
225 pub fn add(&self, o: &R3) -> R3 {
226 R3::new(&self.x + &o.x, &self.y + &o.y, &self.z + &o.z)
227 }
228
229 pub fn scale(&self, s: &Rational) -> R3 {
230 R3::new(&self.x * s, &self.y * s, &self.z * s)
231 }
232
233 pub fn dot(&self, o: &R3) -> Rational {
234 &self.x * &o.x + &self.y * &o.y + &self.z * &o.z
235 }
236
237 pub fn cross(&self, o: &R3) -> R3 {
238 R3::new(
239 &self.y * &o.z - &self.z * &o.y,
240 &self.z * &o.x - &self.x * &o.z,
241 &self.x * &o.y - &self.y * &o.x,
242 )
243 }
244
245 pub fn is_zero(&self) -> bool {
246 rat_is_zero(&self.x) && rat_is_zero(&self.y) && rat_is_zero(&self.z)
247 }
248
249 /// Drop the coordinate at `axis` (0=x, 1=y, 2=z), keeping the other two
250 /// in cyclic order — the paper's bijective dominant-axis projection used
251 /// to embed per-triangle 2D arrangements.
252 pub fn project_drop(&self, axis: usize) -> R2 {
253 match axis {
254 0 => R2::new(self.y.clone(), self.z.clone()),
255 1 => R2::new(self.z.clone(), self.x.clone()),
256 2 => R2::new(self.x.clone(), self.y.clone()),
257 _ => unreachable!("axis must be 0, 1, or 2"),
258 }
259 }
260}
261
262// ─── Cheap exact hash keys ───────────────────────────────────────────────────
263//
264// A general-purpose rational Hash/Eq must stay consistent for UNREDUCED
265// ratios, which costs at least a cross-multiplication (and, in some
266// libraries, a Euclidean recursion). Every rational this pipeline stores is canonical —
267// built by rat_new, the arithmetic operators, or rat_from_f64, all of which
268// reduce — so field identity IS value identity, and hashing the raw sign/limb
269// data is both exact and division-free. The wrappers below are drop-in
270// hash-map keys that are 1–2 orders of magnitude cheaper than hashing R2/R3
271// directly. See backend.rs items (1) and (2).
272
273use backend::{hash_rational as hash_rat, rat_eq as rat_fields_eq};
274
275/// Hash-map key wrapper around a canonical R2.
276#[derive(Clone, Debug)]
277pub struct R2Key(pub R2);
278
279impl PartialEq for R2Key {
280 #[inline]
281 fn eq(&self, other: &Self) -> bool {
282 rat_fields_eq(&self.0.x, &other.0.x) && rat_fields_eq(&self.0.y, &other.0.y)
283 }
284}
285impl Eq for R2Key {}
286impl std::hash::Hash for R2Key {
287 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
288 hash_rat(&self.0.x, state);
289 hash_rat(&self.0.y, state);
290 }
291}
292
293/// Hash-map key wrapper around a canonical R3.
294#[derive(Clone, Debug)]
295pub struct R3Key(pub R3);
296
297impl PartialEq for R3Key {
298 #[inline]
299 fn eq(&self, other: &Self) -> bool {
300 rat_fields_eq(&self.0.x, &other.0.x)
301 && rat_fields_eq(&self.0.y, &other.0.y)
302 && rat_fields_eq(&self.0.z, &other.0.z)
303 }
304}
305impl Eq for R3Key {}
306impl std::hash::Hash for R3Key {
307 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
308 hash_rat(&self.0.x, state);
309 hash_rat(&self.0.y, state);
310 hash_rat(&self.0.z, state);
311 }
312}
313
314/// Field-wise equality of canonical R2 values — see [`r3_eq`].
315#[inline]
316pub fn r2_eq(a: &R2, b: &R2) -> bool {
317 rat_fields_eq(&a.x, &b.x) && rat_fields_eq(&a.y, &b.y)
318}
319
320/// Field-wise equality of canonical R3 values — value equality without
321/// the backend's general comparison (which is most expensive exactly when
322/// the values ARE equal).
323#[inline]
324pub fn r3_eq(a: &R3, b: &R3) -> bool {
325 rat_fields_eq(&a.x, &b.x) && rat_fields_eq(&a.y, &b.y) && rat_fields_eq(&a.z, &b.z)
326}