Skip to main content

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 BigRational 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 num_bigint::BigUint;
14use num_rational::BigRational;
15use num_traits::{Signed, ToPrimitive, Zero};
16
17use crate::linalg::{Vec2, Vec3};
18
19/// Exact conversion of a finite f64. Every finite f64 is a dyadic rational,
20/// so this never loses information.
21///
22/// Panics on NaN/infinity — mesh import rejects non-finite vertices
23/// (`Error::NonFiniteVertex`) long before the robust engine runs, so a
24/// non-finite value here is an internal logic error, not bad user input.
25#[inline]
26pub fn rat(v: f64) -> BigRational {
27    BigRational::from_float(v).expect("robust engine: coordinate must be finite")
28}
29
30/// 2^e as f64, exact for the full representable range -1074..=1023
31/// (subnormal powers of two included). Built by bit manipulation so no
32/// intermediate rounding can occur.
33#[inline]
34fn pow2(e: i64) -> f64 {
35    debug_assert!((-1074..=1023).contains(&e), "pow2 exponent out of range: {e}");
36    if e >= -1022 {
37        f64::from_bits(((e + 1023) as u64) << 52)
38    } else {
39        f64::from_bits(1u64 << (e + 1074))
40    }
41}
42
43/// Round a rational to the nearest f64, ties to even — the correctly rounded
44/// result, identical to rounding the exact real value once. Values beyond
45/// f64 range become ±infinity; values below half the smallest subnormal
46/// become (signed) zero.
47pub fn rat_to_f64(r: &BigRational) -> f64 {
48    if r.is_zero() {
49        return 0.0;
50    }
51    let neg = r.is_negative();
52    let n: &BigUint = r.numer().magnitude();
53    let d: &BigUint = r.denom().magnitude();
54
55    // Exact floor exponent e: 2^e <= n/d < 2^(e+1).
56    let mut e = n.bits() as i64 - d.bits() as i64;
57    let ge = if e >= 0 {
58        *n >= (d << e as usize)
59    } else {
60        (n << (-e) as usize) >= *d
61    };
62    if !ge {
63        e -= 1;
64    }
65    if e > 1023 {
66        return if neg { f64::NEG_INFINITY } else { f64::INFINITY };
67    }
68
69    // Position of the result's least significant bit. Normal numbers carry
70    // 53 bits ending at e-52; subnormals are cut off at 2^-1074.
71    let lsb = (e - 52).max(-1074);
72
73    // m = round_nearest_even((n/d) / 2^lsb), computed with one exact
74    // integer division plus a remainder comparison.
75    let (num, den) = if lsb >= 0 {
76        (n.clone(), d << lsb as usize)
77    } else {
78        (n << (-lsb) as usize, d.clone())
79    };
80    let q = &num / &den;
81    let rem = &num - &q * &den;
82    let mut m = q;
83    let twice_rem = &rem << 1usize;
84    match twice_rem.cmp(&den) {
85        std::cmp::Ordering::Greater => m += 1u32,
86        std::cmp::Ordering::Equal => {
87            if m.bit(0) {
88                m += 1u32;
89            }
90        }
91        std::cmp::Ordering::Less => {}
92    }
93
94    if m.is_zero() {
95        return if neg { -0.0 } else { 0.0 };
96    }
97    // Rounding up may have crossed into the next binade (m = 2^53) or past
98    // the largest finite value (2^1024 -> infinity).
99    if m.bits() as i64 - 1 + lsb > 1023 {
100        return if neg { f64::NEG_INFINITY } else { f64::INFINITY };
101    }
102    // m <= 2^53, so both the u64 and the f64 conversion are exact, and
103    // m * 2^lsb is representable by construction — the multiply is exact.
104    let val = m.to_u64().expect("mantissa fits in u64") as f64 * pow2(lsb);
105    if neg {
106        -val
107    } else {
108        val
109    }
110}
111
112// ─── R2 — exact 2D point/vector ─────────────────────────────────────────────
113
114/// Exact 2D point (or vector). Derived `Ord` is lexicographic, which the
115/// arrangement code uses for exact point dedup in BTree maps.
116#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
117pub struct R2 {
118    pub x: BigRational,
119    pub y: BigRational,
120}
121
122impl R2 {
123    #[inline]
124    pub fn new(x: BigRational, y: BigRational) -> Self {
125        Self { x, y }
126    }
127
128    #[inline]
129    pub fn from_vec2(v: Vec2) -> Self {
130        Self::new(rat(v.x), rat(v.y))
131    }
132
133    pub fn to_vec2_rounded(&self) -> Vec2 {
134        Vec2::new(rat_to_f64(&self.x), rat_to_f64(&self.y))
135    }
136
137    pub fn sub(&self, o: &R2) -> R2 {
138        R2::new(&self.x - &o.x, &self.y - &o.y)
139    }
140
141    pub fn add(&self, o: &R2) -> R2 {
142        R2::new(&self.x + &o.x, &self.y + &o.y)
143    }
144
145    pub fn scale(&self, s: &BigRational) -> R2 {
146        R2::new(&self.x * s, &self.y * s)
147    }
148
149    pub fn dot(&self, o: &R2) -> BigRational {
150        &self.x * &o.x + &self.y * &o.y
151    }
152
153    /// 2D cross product (z of the 3D cross of the embedded vectors).
154    pub fn cross(&self, o: &R2) -> BigRational {
155        &self.x * &o.y - &self.y * &o.x
156    }
157
158    pub fn is_zero(&self) -> bool {
159        self.x.is_zero() && self.y.is_zero()
160    }
161}
162
163// ─── R3 — exact 3D point/vector ─────────────────────────────────────────────
164
165/// Exact 3D point (or vector). Derived `Ord` is lexicographic (x, y, z) for
166/// exact vertex welding in output assembly.
167#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
168pub struct R3 {
169    pub x: BigRational,
170    pub y: BigRational,
171    pub z: BigRational,
172}
173
174impl R3 {
175    #[inline]
176    pub fn new(x: BigRational, y: BigRational, z: BigRational) -> Self {
177        Self { x, y, z }
178    }
179
180    #[inline]
181    pub fn from_vec3(v: Vec3) -> Self {
182        Self::new(rat(v.x), rat(v.y), rat(v.z))
183    }
184
185    pub fn to_vec3_rounded(&self) -> Vec3 {
186        Vec3::new(rat_to_f64(&self.x), rat_to_f64(&self.y), rat_to_f64(&self.z))
187    }
188
189    pub fn sub(&self, o: &R3) -> R3 {
190        R3::new(&self.x - &o.x, &self.y - &o.y, &self.z - &o.z)
191    }
192
193    pub fn add(&self, o: &R3) -> R3 {
194        R3::new(&self.x + &o.x, &self.y + &o.y, &self.z + &o.z)
195    }
196
197    pub fn scale(&self, s: &BigRational) -> R3 {
198        R3::new(&self.x * s, &self.y * s, &self.z * s)
199    }
200
201    pub fn dot(&self, o: &R3) -> BigRational {
202        &self.x * &o.x + &self.y * &o.y + &self.z * &o.z
203    }
204
205    pub fn cross(&self, o: &R3) -> R3 {
206        R3::new(
207            &self.y * &o.z - &self.z * &o.y,
208            &self.z * &o.x - &self.x * &o.z,
209            &self.x * &o.y - &self.y * &o.x,
210        )
211    }
212
213    pub fn is_zero(&self) -> bool {
214        self.x.is_zero() && self.y.is_zero() && self.z.is_zero()
215    }
216
217    /// Drop the coordinate at `axis` (0=x, 1=y, 2=z), keeping the other two
218    /// in cyclic order — the paper's bijective dominant-axis projection used
219    /// to embed per-triangle 2D arrangements.
220    pub fn project_drop(&self, axis: usize) -> R2 {
221        match axis {
222            0 => R2::new(self.y.clone(), self.z.clone()),
223            1 => R2::new(self.z.clone(), self.x.clone()),
224            2 => R2::new(self.x.clone(), self.y.clone()),
225            _ => unreachable!("axis must be 0, 1, or 2"),
226        }
227    }
228}
229
230// ─── Cheap exact hash keys ───────────────────────────────────────────────────
231//
232// num-rational's Hash and Eq run continued-fraction recursions (a BigInt
233// division per level, a full Euclidean algorithm for equal values) so they
234// stay consistent for UNREDUCED ratios. Every rational this pipeline stores
235// is canonical — built by Ratio::new, arithmetic operators, or from_float,
236// all of which reduce — so field identity IS value identity, and hashing the
237// raw sign/limb data is both exact and division-free. The wrappers below are
238// drop-in hash-map keys that are 1–2 orders of magnitude cheaper than
239// hashing R2/R3 directly. (classify.rs's new_raw fractions are sign/compare
240// scratch values and must never be used as keys.)
241
242#[inline]
243fn rat_fields_eq(a: &BigRational, b: &BigRational) -> bool {
244    a.numer() == b.numer() && a.denom() == b.denom()
245}
246
247fn hash_rat<H: std::hash::Hasher>(r: &BigRational, state: &mut H) {
248    use std::hash::Hash;
249    (r.numer().sign() == num_bigint::Sign::Minus).hash(state);
250    for d in r.numer().iter_u64_digits() {
251        d.hash(state);
252    }
253    0xfeed_u64.hash(state); // length separator between numerator and denominator
254    for d in r.denom().iter_u64_digits() {
255        d.hash(state);
256    }
257}
258
259/// Hash-map key wrapper around a canonical R2.
260#[derive(Clone, Debug)]
261pub struct R2Key(pub R2);
262
263impl PartialEq for R2Key {
264    #[inline]
265    fn eq(&self, other: &Self) -> bool {
266        rat_fields_eq(&self.0.x, &other.0.x) && rat_fields_eq(&self.0.y, &other.0.y)
267    }
268}
269impl Eq for R2Key {}
270impl std::hash::Hash for R2Key {
271    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
272        hash_rat(&self.0.x, state);
273        hash_rat(&self.0.y, state);
274    }
275}
276
277/// Hash-map key wrapper around a canonical R3.
278#[derive(Clone, Debug)]
279pub struct R3Key(pub R3);
280
281impl PartialEq for R3Key {
282    #[inline]
283    fn eq(&self, other: &Self) -> bool {
284        rat_fields_eq(&self.0.x, &other.0.x)
285            && rat_fields_eq(&self.0.y, &other.0.y)
286            && rat_fields_eq(&self.0.z, &other.0.z)
287    }
288}
289impl Eq for R3Key {}
290impl std::hash::Hash for R3Key {
291    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
292        hash_rat(&self.0.x, state);
293        hash_rat(&self.0.y, state);
294        hash_rat(&self.0.z, state);
295    }
296}
297
298/// Field-wise equality of canonical R2 values — see [`r3_eq`].
299#[inline]
300pub fn r2_eq(a: &R2, b: &R2) -> bool {
301    rat_fields_eq(&a.x, &b.x) && rat_fields_eq(&a.y, &b.y)
302}
303
304/// Field-wise equality of canonical R3 values — value equality without
305/// num-rational's Euclidean comparison (which is most expensive exactly when
306/// the values ARE equal).
307#[inline]
308pub fn r3_eq(a: &R3, b: &R3) -> bool {
309    rat_fields_eq(&a.x, &b.x) && rat_fields_eq(&a.y, &b.y) && rat_fields_eq(&a.z, &b.z)
310}