Skip to main content

geop_core_math/scalars/
scal_in_fpa64.rs

1use super::{Field, Ring, Scalar};
2use crate::geop_error::{GeopError, GeopResult};
3
4/// Number of fractional bits.
5const F: u32 = 32;
6/// Scale factor: 2^F as i128 for widened arithmetic.
7const SCALE: i128 = 1_i128 << F;
8
9// ── OVERFLOW sentinel values ──────────────────────────────────────────────────
10const OV_LO: i64 = i64::MIN;
11const OV_HI: i64 = i64::MAX;
12
13// ── Directed-rounding integer helpers ────────────────────────────────────────
14
15/// Floor division for signed integers (rounds toward −∞).
16#[inline]
17fn floor_div(n: i128, d: i128) -> i128 {
18    let q = n / d;
19    let r = n % d;
20    // If remainder is non-zero and signs differ, subtract 1
21    if r != 0 && (n ^ d) < 0 { q - 1 } else { q }
22}
23
24/// Ceiling division for signed integers (rounds toward +∞).
25#[inline]
26fn ceil_div(n: i128, d: i128) -> i128 {
27    -floor_div(-n, d)
28}
29
30/// Saturate an i128 into i64, treating extremes as the OVERFLOW sentinel.
31#[inline]
32fn sat_lo(v: i128) -> i64 {
33    if v <= i64::MIN as i128 {
34        OV_LO
35    } else if v >= i64::MAX as i128 {
36        OV_HI
37    } else {
38        v as i64
39    }
40}
41
42#[inline]
43fn sat_hi(v: i128) -> i64 {
44    sat_lo(v)
45}
46
47// ── next_up / next_down for f64 (mirrored from scal_in_f64) ──────────────────
48
49#[inline]
50fn next_up_f64(x: f64) -> f64 {
51    if x.is_nan() || x == f64::INFINITY {
52        return x;
53    }
54    if x == 0.0 {
55        return f64::MIN_POSITIVE;
56    }
57    let bits = x.to_bits();
58    f64::from_bits(if x > 0.0 { bits + 1 } else { bits - 1 })
59}
60
61#[inline]
62fn next_down_f64(x: f64) -> f64 {
63    if x.is_nan() || x == f64::NEG_INFINITY {
64        return x;
65    }
66    if x == 0.0 {
67        return -f64::MIN_POSITIVE;
68    }
69    let bits = x.to_bits();
70    f64::from_bits(if x < 0.0 { bits + 1 } else { bits - 1 })
71}
72
73// ── Interval trigonometry (widened to f64, like `sqrt` above) ────────────────
74
75/// True iff `[lo, hi]` contains some `target + k * period` for an integer `k`.
76#[inline]
77fn contains_periodic(lo: f64, hi: f64, target: f64, period: f64) -> bool {
78    let k = ((lo - target) / period).ceil();
79    target + k * period <= hi
80}
81
82/// Outward-rounded enclosure of `sin`/`cos([lo, hi])`, as f64 bounds — mirrors
83/// `scal_in_f64::interval_trig`.
84fn interval_trig(lo: f64, hi: f64, f: impl Fn(f64) -> f64, max_at: f64, min_at: f64) -> (f64, f64) {
85    use std::f64::consts::TAU;
86    if !lo.is_finite() || !hi.is_finite() || hi - lo >= TAU {
87        return (-1.0, 1.0);
88    }
89    let (a, b) = (f(lo), f(hi));
90    let mut out_lo = next_down_f64(a.min(b));
91    let mut out_hi = next_up_f64(a.max(b));
92    if contains_periodic(lo, hi, max_at, TAU) {
93        out_hi = 1.0;
94    }
95    if contains_periodic(lo, hi, min_at, TAU) {
96        out_lo = -1.0;
97    }
98    (out_lo.max(-1.0), out_hi.min(1.0))
99}
100
101// ── Type ──────────────────────────────────────────────────────────────────────
102
103/// Interval fixed-point scalar: `[lo, hi]` at 2^-32 resolution (i64 units).
104#[derive(Copy, Clone, PartialEq)]
105pub struct ScalInFPA64 {
106    pub lo: i64,
107    pub hi: i64,
108}
109
110impl ScalInFPA64 {
111    #[inline]
112    pub fn new(lo: i64, hi: i64) -> Self {
113        debug_assert!(lo <= hi, "ScalInFPA64::new: lo ({lo}) > hi ({hi})");
114        ScalInFPA64 { lo, hi }
115    }
116
117    #[inline]
118    pub fn degenerate(v: i64) -> Self {
119        ScalInFPA64 { lo: v, hi: v }
120    }
121
122    #[inline]
123    fn is_overflow(self) -> bool {
124        self.lo == OV_LO || self.hi == OV_HI
125    }
126
127    /// Convert a real-valued f64 to fixed-point, rounding outward.
128    pub fn from_f64_outward(lo_f: f64, hi_f: f64) -> Self {
129        let lo_i = (lo_f * SCALE as f64).floor() as i128;
130        let hi_i = (hi_f * SCALE as f64).ceil() as i128;
131        ScalInFPA64::new(sat_lo(lo_i), sat_hi(hi_i))
132    }
133}
134
135impl core::fmt::Debug for ScalInFPA64 {
136    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
137        let lo_f = self.lo as f64 / SCALE as f64;
138        let hi_f = self.hi as f64 / SCALE as f64;
139        write!(f, "[{lo_f}, {hi_f}]")
140    }
141}
142
143impl core::fmt::Display for ScalInFPA64 {
144    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
145        let lo_f = self.lo as f64 / SCALE as f64;
146        let hi_f = self.hi as f64 / SCALE as f64;
147        write!(f, "{:.3}", (lo_f + hi_f) / 2.0)
148    }
149}
150
151// ── Constants (pre-computed bit patterns) ─────────────────────────────────────
152//
153// π * 2^32 = 13493037704.92…  → floor = 13493037704, ceil = 13493037705
154// e * 2^32 = 11674931555.08…  → floor = 11674931555, ceil = 11674931556
155
156const PI_LO: i64 = 13493037704;
157const PI_HI: i64 = 13493037705;
158const E_LO: i64 = 11674931555;
159const E_HI: i64 = 11674931556;
160
161impl Ring for ScalInFPA64 {
162    fn add(self, other: Self) -> Self {
163        let lo = self.lo.saturating_add(other.lo);
164        let hi = self.hi.saturating_add(other.hi);
165        ScalInFPA64::new(lo, hi)
166    }
167
168    fn sub(self, other: Self) -> Self {
169        let lo = self.lo.saturating_sub(other.hi);
170        let hi = self.hi.saturating_sub(other.lo);
171        ScalInFPA64::new(lo, hi)
172    }
173
174    fn mul(self, other: Self) -> Self {
175        let products = [
176            (self.lo as i128) * (other.lo as i128),
177            (self.lo as i128) * (other.hi as i128),
178            (self.hi as i128) * (other.lo as i128),
179            (self.hi as i128) * (other.hi as i128),
180        ];
181        let raw_lo = *products.iter().min().unwrap();
182        let raw_hi = *products.iter().max().unwrap();
183        let lo = floor_div(raw_lo, SCALE);
184        let hi = ceil_div(raw_hi, SCALE);
185        ScalInFPA64::new(sat_lo(lo), sat_hi(hi))
186    }
187
188    fn neg(self) -> Self {
189        ScalInFPA64::new(self.hi.saturating_neg(), self.lo.saturating_neg())
190    }
191}
192
193impl Field for ScalInFPA64 {
194    fn div(self, other: Self) -> GeopResult<Self> {
195        if other.lo <= 0 && other.hi >= 0 {
196            return Err(GeopError::new(
197                "ScalInFPA64::div: divisor interval contains zero",
198            ));
199        }
200        let a_lo = (self.lo as i128) << F;
201        let a_hi = (self.hi as i128) << F;
202        let b_lo = other.lo as i128;
203        let b_hi = other.hi as i128;
204
205        let candidates = [
206            floor_div(a_lo, b_hi),
207            floor_div(a_lo, b_lo),
208            floor_div(a_hi, b_hi),
209            floor_div(a_hi, b_lo),
210        ];
211        let candidates_hi = [
212            ceil_div(a_lo, b_hi),
213            ceil_div(a_lo, b_lo),
214            ceil_div(a_hi, b_hi),
215            ceil_div(a_hi, b_lo),
216        ];
217        let lo = *candidates.iter().min().unwrap();
218        let hi = *candidates_hi.iter().max().unwrap();
219        Ok(ScalInFPA64::new(sat_lo(lo), sat_hi(hi)))
220    }
221}
222
223// ── Scalar impl ───────────────────────────────────────────────────────────────
224
225impl Scalar for ScalInFPA64 {
226    const ZERO: Self = ScalInFPA64 { lo: 0, hi: 0 };
227    const ONE: Self = ScalInFPA64 {
228        lo: SCALE as i64,
229        hi: SCALE as i64,
230    };
231    const TWO: Self = ScalInFPA64 {
232        lo: 2 * SCALE as i64,
233        hi: 2 * SCALE as i64,
234    };
235    const PI: Self = ScalInFPA64 {
236        lo: PI_LO,
237        hi: PI_HI,
238    };
239    const E: Self = ScalInFPA64 { lo: E_LO, hi: E_HI };
240    const INFINITY: Self = ScalInFPA64 {
241        lo: OV_LO,
242        hi: OV_HI,
243    };
244    const ENTIRE: Self = ScalInFPA64 {
245        lo: i64::MIN,
246        hi: i64::MAX,
247    };
248
249    fn from_f64(v: f64) -> Self {
250        ScalInFPA64::from_f64_outward(v, v)
251    }
252    fn from_i64(v: i64) -> Self {
253        let shifted = (v as i128).checked_mul(SCALE);
254        match shifted {
255            Some(s) if s >= i64::MIN as i128 && s <= i64::MAX as i128 => {
256                ScalInFPA64::degenerate(s as i64)
257            }
258            _ => ScalInFPA64::INFINITY,
259        }
260    }
261
262    fn from_ratio(num: i64, den: i64) -> GeopResult<Self> {
263        if den == 0 {
264            return Err(GeopError::new(
265                "ScalInFPA64::from_ratio: denominator is zero",
266            ));
267        }
268        let n = (num as i128) << F;
269        let d = den as i128;
270        let lo = floor_div(n, d);
271        let hi = ceil_div(n, d);
272        Ok(ScalInFPA64::new(sat_lo(lo), sat_hi(hi)))
273    }
274
275    fn abs(self) -> Self {
276        if self.lo >= 0 {
277            self
278        } else if self.hi <= 0 {
279            ScalInFPA64::new(self.hi.saturating_neg(), self.lo.saturating_neg())
280        } else {
281            let hi = self.lo.saturating_neg().max(self.hi);
282            ScalInFPA64::new(0, hi)
283        }
284    }
285
286    fn sqrt(self) -> GeopResult<Self> {
287        if self.hi < 0 {
288            return Err(GeopError::new(
289                "ScalInFPA64::sqrt: interval is definitely negative",
290            ));
291        }
292        let lo_clamped = if self.lo < 0 { 0i64 } else { self.lo };
293        // Convert to f64, sqrt with outward rounding, convert back
294        let lo_f = (lo_clamped as f64) / SCALE as f64;
295        let hi_f = (self.hi as f64) / SCALE as f64;
296        let sqrt_lo = next_down_f64(lo_f.sqrt());
297        let sqrt_hi = next_up_f64(hi_f.sqrt());
298        // Convert back to fixed-point with outward rounding
299        let lo_fixed = (sqrt_lo * SCALE as f64).floor() as i64;
300        let hi_fixed = (sqrt_hi * SCALE as f64).ceil() as i64;
301        Ok(ScalInFPA64::new(lo_fixed, hi_fixed))
302    }
303
304    fn sin(self) -> Self {
305        // No native fixed-point trig: widen to f64 (as `sqrt` already does
306        // above), reuse the f64 interval routine, then round outward back
307        // into fixed point. An overflowed operand converts to a huge (but
308        // finite) f64 span, which the `hi - lo >= TAU` check below still
309        // correctly collapses to the full `[-1, 1]` range.
310        use std::f64::consts::FRAC_PI_2;
311        let (lo, hi) = interval_trig(
312            self.lo as f64 / SCALE as f64,
313            self.hi as f64 / SCALE as f64,
314            f64::sin,
315            FRAC_PI_2,
316            -FRAC_PI_2,
317        );
318        ScalInFPA64::from_f64_outward(lo, hi)
319    }
320
321    fn cos(self) -> Self {
322        use std::f64::consts::PI;
323        let (lo, hi) = interval_trig(
324            self.lo as f64 / SCALE as f64,
325            self.hi as f64 / SCALE as f64,
326            f64::cos,
327            0.0,
328            PI,
329        );
330        ScalInFPA64::from_f64_outward(lo, hi)
331    }
332
333    fn could_be_equal(self, other: Self) -> bool {
334        self.lo <= other.hi && other.lo <= self.hi
335    }
336
337    fn definitely_not_equal(self, other: Self) -> bool {
338        self.hi < other.lo || self.lo > other.hi
339    }
340
341    fn could_be_greater(self, other: Self) -> bool {
342        self.hi > other.lo
343    }
344
345    fn definitely_greater(self, other: Self) -> bool {
346        self.lo > other.hi
347    }
348
349    fn could_be_less(self, other: Self) -> bool {
350        self.lo < other.hi
351    }
352
353    fn definitely_less(self, other: Self) -> bool {
354        self.hi < other.lo
355    }
356
357    fn is_infinite(self) -> bool {
358        self.is_overflow()
359    }
360
361    fn is_finite(self) -> bool {
362        !self.is_overflow()
363    }
364
365    fn midpoint(self) -> Self {
366        // Avoid overflow: (lo + hi) / 2 using i128
367        let m = ((self.lo as i128 + self.hi as i128) / 2) as i64;
368        ScalInFPA64::degenerate(m)
369    }
370
371    fn is_sharp(self) -> bool {
372        self.lo == self.hi
373    }
374
375    fn lower(self) -> Self {
376        ScalInFPA64::degenerate(self.lo)
377    }
378
379    fn upper(self) -> Self {
380        ScalInFPA64::degenerate(self.hi)
381    }
382
383    fn width(self) -> Self {
384        let w = self.hi.saturating_sub(self.lo).max(0);
385        ScalInFPA64::new(w, w)
386    }
387
388    fn intersect(self, other: Self) -> Self {
389        let lo = self.lo.max(other.lo);
390        let hi = self.hi.min(other.hi);
391        if lo <= hi {
392            ScalInFPA64::new(lo, hi)
393        } else if self.hi.saturating_sub(self.lo) <= other.hi.saturating_sub(other.lo) {
394            self
395        } else {
396            other
397        }
398    }
399
400    fn to_f64(self) -> f64 {
401        let m = (self.lo as i128 + self.hi as i128) / 2;
402        m as f64 / SCALE as f64
403    }
404
405    fn union(self, other: Self) -> Self {
406        ScalInFPA64::new(self.lo.min(other.lo), self.hi.max(other.hi))
407    }
408
409    fn is_subset_of(self, other: Self) -> bool {
410        other.lo <= self.lo && self.hi <= other.hi
411    }
412}
413
414impl core::ops::Add for ScalInFPA64 {
415    type Output = Self;
416    fn add(self, rhs: Self) -> Self {
417        Ring::add(self, rhs)
418    }
419}
420impl core::ops::Sub for ScalInFPA64 {
421    type Output = Self;
422    fn sub(self, rhs: Self) -> Self {
423        Ring::sub(self, rhs)
424    }
425}
426impl core::ops::Mul for ScalInFPA64 {
427    type Output = Self;
428    fn mul(self, rhs: Self) -> Self {
429        Ring::mul(self, rhs)
430    }
431}
432impl core::ops::Neg for ScalInFPA64 {
433    type Output = Self;
434    fn neg(self) -> Self {
435        Ring::neg(self)
436    }
437}
438
439impl From<i64> for ScalInFPA64 {
440    fn from(v: i64) -> Self {
441        ScalInFPA64::from_i64(v)
442    }
443}
444
445impl From<f64> for ScalInFPA64 {
446    fn from(v: f64) -> Self {
447        ScalInFPA64::from_f64(v)
448    }
449}
450
451impl Default for ScalInFPA64 {
452    fn default() -> Self {
453        ScalInFPA64::ZERO
454    }
455}
456
457// ── Tests ─────────────────────────────────────────────────────────────────────
458
459#[cfg(test)]
460mod tests {
461    use super::*;
462
463    fn pt(v: i64) -> ScalInFPA64 {
464        ScalInFPA64::from_i64(v)
465    }
466
467    fn iv(lo: i64, hi: i64) -> ScalInFPA64 {
468        ScalInFPA64::new(lo << F, hi << F)
469    }
470
471    #[test]
472    fn arithmetic_add() {
473        let r = pt(1).add(pt(2));
474        assert!(r.could_be_equal(pt(3)));
475    }
476
477    #[test]
478    fn arithmetic_sub() {
479        let r = pt(5).sub(pt(3));
480        assert!(r.could_be_equal(pt(2)));
481    }
482
483    #[test]
484    fn arithmetic_mul() {
485        let r = pt(3).mul(pt(4));
486        assert!(r.could_be_equal(pt(12)));
487    }
488
489    #[test]
490    fn arithmetic_div() {
491        let r = pt(10).div(pt(2)).unwrap();
492        assert!(r.could_be_equal(pt(5)));
493    }
494
495    #[test]
496    fn arithmetic_sqrt_4() {
497        let r = pt(4).sqrt().unwrap();
498        assert!(r.could_be_equal(pt(2)));
499    }
500
501    #[test]
502    fn arithmetic_sqrt_9() {
503        let r = pt(9).sqrt().unwrap();
504        assert!(r.could_be_equal(pt(3)));
505    }
506
507    #[test]
508    fn arithmetic_abs() {
509        assert!(pt(-3).abs().could_be_equal(pt(3)));
510    }
511
512    #[test]
513    fn arithmetic_neg() {
514        assert!(pt(-5).neg().could_be_equal(pt(5)));
515    }
516
517    #[test]
518    fn sin_at_a_point() {
519        assert!(pt(0).sin().could_be_equal(pt(0)));
520    }
521
522    #[test]
523    fn cos_at_a_point() {
524        assert!(pt(0).cos().could_be_equal(pt(1)));
525    }
526
527    #[test]
528    fn sin_over_a_peak_reaches_exactly_one() {
529        let lo = ScalInFPA64::from_f64(0.0);
530        let hi = ScalInFPA64::from_f64(std::f64::consts::PI);
531        let r = ScalInFPA64::new(lo.lo, hi.hi).sin();
532        assert!(r.hi == ScalInFPA64::ONE.hi && r.lo >= -1);
533    }
534
535    #[test]
536    fn err_div_by_zero() {
537        assert!(pt(1).div(ScalInFPA64::ZERO).is_err());
538    }
539
540    #[test]
541    fn err_sqrt_negative() {
542        assert!(pt(-1).sqrt().is_err());
543    }
544
545    #[test]
546    fn sqrt_straddles_zero_ok() {
547        // [-1, 4] straddles zero; sqrt should succeed and contain 2
548        let neg_one = -(1_i64 << F);
549        let four = 4_i64 << F;
550        let r = ScalInFPA64::new(neg_one, four).sqrt().unwrap();
551        assert!(r.could_be_equal(pt(2)));
552    }
553
554    #[test]
555    fn overflow_infinity() {
556        let big = ScalInFPA64::new(i64::MAX / 2, i64::MAX / 2);
557        let result = big.mul(big);
558        assert!(result.is_infinite());
559    }
560
561    #[test]
562    fn cmp_definitely_greater() {
563        assert!(pt(3).definitely_greater(pt(2)));
564    }
565
566    #[test]
567    fn cmp_could_be_less_false() {
568        assert!(!pt(3).definitely_less(pt(2)));
569    }
570
571    #[test]
572    fn cmp_overlapping_intervals_could_be_equal() {
573        assert!(iv(1, 3).could_be_equal(iv(2, 4)));
574    }
575
576    #[test]
577    fn cmp_disjoint_intervals_definitely_not_equal() {
578        assert!(iv(1, 2).definitely_not_equal(iv(3, 4)));
579    }
580
581    #[test]
582    fn entire_could_be_equal_point() {
583        assert!(ScalInFPA64::ENTIRE.could_be_equal(pt(42)));
584        assert!(pt(-1_000_000).could_be_equal(ScalInFPA64::ENTIRE));
585    }
586
587    #[test]
588    fn entire_could_be_equal_interval() {
589        assert!(ScalInFPA64::ENTIRE.could_be_equal(iv(-5, 5)));
590    }
591
592    #[test]
593    fn entire_could_be_greater_and_less() {
594        assert!(ScalInFPA64::ENTIRE.could_be_greater(pt(1_000_000)));
595        assert!(ScalInFPA64::ENTIRE.could_be_less(pt(-1_000_000)));
596    }
597
598    #[test]
599    fn entire_never_definitely() {
600        assert!(!ScalInFPA64::ENTIRE.definitely_not_equal(pt(0)));
601        assert!(!ScalInFPA64::ENTIRE.definitely_greater(pt(1_000_000)));
602        assert!(!ScalInFPA64::ENTIRE.definitely_less(pt(-1_000_000)));
603    }
604}