Skip to main content

ocas_domain/
double_float.rs

1//! Double-precision floating-point arithmetic (Dekker/Knuth "double-float").
2//!
3//! [`DoubleF64`] represents a floating-point number as the unevaluated sum of
4//! two `f64` values (`hi + lo`), where `|lo| ≤ 0.5 · ulp(hi)`. This gives
5//! approximately 31 decimal digits of precision (~84 binary bits) — roughly
6//! double that of a single `f64`.
7//!
8//! The arithmetic is based on Dekker's and Knuth's algorithms for error-free
9//! transformations (TwoSum, TwoProd) and is significantly faster than
10//! arbitrary-precision alternatives like MPFR.
11
12use std::fmt;
13use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};
14
15use num_traits::Zero;
16
17use crate::domain::Domain;
18
19// =========================================================================
20// Type definition
21// =========================================================================
22
23/// A double-precision floating-point number: `hi + lo` with `|lo| ≤ 0.5·ulp(hi)`.
24///
25/// Provides ~31 decimal digits (~84 binary bits) of precision.
26#[derive(Debug, Clone, Copy, PartialEq)]
27pub struct DoubleF64 {
28    /// High-order component (the "main" value).
29    pub hi: f64,
30    /// Low-order component (the error term).
31    pub lo: f64,
32}
33
34impl Eq for DoubleF64 {}
35
36// =========================================================================
37// Construction and conversion
38// =========================================================================
39
40impl DoubleF64 {
41    /// The zero value.
42    pub const ZERO: Self = Self { hi: 0.0, lo: 0.0 };
43    /// The unit value.
44    pub const ONE: Self = Self { hi: 1.0, lo: 0.0 };
45
46    /// Create a new `DoubleF64` from high and low components.
47    /// The caller must ensure `|lo| ≤ 0.5·ulp(hi)` for correct results.
48    #[inline]
49    pub fn new(hi: f64, lo: f64) -> Self {
50        Self { hi, lo }
51    }
52
53    /// Create from a single `f64`.
54    #[inline]
55    pub fn from_f64(x: f64) -> Self {
56        Self { hi: x, lo: 0.0 }
57    }
58
59    /// Extract the high-order component as `f64`.
60    #[inline]
61    pub fn to_f64(self) -> f64 {
62        self.hi
63    }
64
65    /// Create from two `f64` values, normalizing via TwoSum.
66    pub fn quick_two_sum(a: f64, b: f64) -> Self {
67        let s = a + b;
68        let e = b - (s - a);
69        Self { hi: s, lo: e }
70    }
71
72    /// Dekker's TwoSum: error-free sum with rounding error captured.
73    pub fn two_sum(a: f64, b: f64) -> Self {
74        let s = a + b;
75        let a_prime = s - b;
76        let b_prime = s - a_prime;
77        let delta_a = a - a_prime;
78        let delta_b = b - b_prime;
79        let e = delta_a + delta_b;
80        Self { hi: s, lo: e }
81    }
82
83    /// Dekker's split: split a f64 into two 26-bit halves.
84    #[allow(dead_code)]
85    fn split(x: f64) -> (f64, f64) {
86        const SPLITTER: f64 = 134217729.0; // 2^27 + 1
87        let c = SPLITTER * x;
88        let c_hi = c - (c - x);
89        let c_lo = x - c_hi;
90        (c_hi, c_lo)
91    }
92
93    /// Dekker's TwoProd without FMA: error-free product with error term.
94    #[allow(dead_code)]
95    fn two_prod_no_fma(a: f64, b: f64) -> Self {
96        let p = a * b;
97        let (a_hi, a_lo) = Self::split(a);
98        let (b_hi, b_lo) = Self::split(b);
99        let err = ((a_hi * b_hi - p) + a_hi * b_lo + a_lo * b_hi) + a_lo * b_lo;
100        Self { hi: p, lo: err }
101    }
102
103    /// TwoProd using FMA when available (preferred).
104    #[inline]
105    fn two_prod(a: f64, b: f64) -> Self {
106        let p = a * b;
107        let err = f64::mul_add(a, b, -p);
108        Self { hi: p, lo: err }
109    }
110
111    /// Absolute value.
112    #[inline]
113    pub fn abs(self) -> Self {
114        if self.hi < 0.0 {
115            Self {
116                hi: -self.hi,
117                lo: -self.lo,
118            }
119        } else {
120            self
121        }
122    }
123
124    /// Check if the value is NaN.
125    #[inline]
126    pub fn is_nan(self) -> bool {
127        self.hi.is_nan()
128    }
129
130    /// Check if the value is infinite.
131    #[inline]
132    pub fn is_infinite(self) -> bool {
133        self.hi.is_infinite()
134    }
135
136    /// Check if the value is finite.
137    #[inline]
138    pub fn is_finite(self) -> bool {
139        self.hi.is_finite()
140    }
141
142    // =====================================================================
143    // Arithmetic operations
144    // =====================================================================
145
146    /// Add two `DoubleF64` values.
147    #[allow(clippy::should_implement_trait)]
148    pub fn add(self, other: Self) -> Self {
149        let s = Self::two_sum(self.hi, other.hi);
150        let v = self.lo + other.lo;
151        let w = s.lo + v;
152        Self::quick_two_sum(s.hi, w)
153    }
154
155    /// Subtract two `DoubleF64` values.
156    #[allow(clippy::should_implement_trait)]
157    pub fn sub(self, other: Self) -> Self {
158        self.add(-other)
159    }
160
161    /// Multiply two `DoubleF64` values.
162    #[allow(clippy::should_implement_trait)]
163    pub fn mul(self, other: Self) -> Self {
164        let p = Self::two_prod(self.hi, other.hi);
165        let err = self.hi * other.lo + self.lo * other.hi;
166        Self::quick_two_sum(p.hi, p.lo + err)
167    }
168
169    /// Divide two `DoubleF64` values.
170    #[allow(clippy::should_implement_trait)]
171    pub fn div(self, other: Self) -> Self {
172        let q1 = self.hi / other.hi;
173        let p = Self::two_prod(q1, other.hi);
174        let delta = self.hi - p.hi;
175        let err = (delta - p.lo + self.lo) / other.hi;
176        Self::quick_two_sum(q1, err)
177    }
178
179    /// Integer power via binary exponentiation.
180    pub fn powi(self, mut n: i64) -> Self {
181        if n == 0 {
182            return Self::ONE;
183        }
184        let negate = n < 0;
185        if negate {
186            n = -n;
187        }
188        let mut base = self;
189        let mut result = Self::ONE;
190        let mut exp = n as u64;
191        while exp > 0 {
192            if exp & 1 == 1 {
193                result = result.mul(base);
194            }
195            base = base.mul(base);
196            exp >>= 1;
197        }
198        if negate {
199            Self::ONE.div(result)
200        } else {
201            result
202        }
203    }
204
205    // =====================================================================
206    // Transcendental functions
207    // =====================================================================
208
209    /// Square root via Newton iteration.
210    pub fn sqrt(self) -> Self {
211        if self.hi < 0.0 {
212            return Self::from_f64(f64::NAN);
213        }
214        if self.hi == 0.0 {
215            return Self::ZERO;
216        }
217        // Initial estimate from hardware sqrt
218        let x0 = self.hi.sqrt();
219        let mut x = Self::from_f64(x0);
220        // Newton iteration: x = (x + self/x) / 2
221        // Two iterations give full DoubleF64 precision
222        for _ in 0..4 {
223            x = x.add(self.div(x)).mul(Self::from_f64(0.5));
224        }
225        x
226    }
227
228    /// Absolute value.
229    pub fn dabs(self) -> Self {
230        if self.hi < 0.0 { -self } else { self }
231    }
232
233    /// Exponential function via Taylor series with argument reduction.
234    #[allow(clippy::approx_constant)]
235    pub fn exp(self) -> Self {
236        if self.hi == 0.0 && self.lo == 0.0 {
237            return Self::ONE;
238        }
239        // Argument reduction: exp(x) = exp(x - k*ln2) * 2^k
240        const LN2: DoubleF64 = DoubleF64 {
241            hi: std::f64::consts::LN_2,
242            lo: 2.319046813846299e-17,
243        };
244        let k = (self.hi / LN2.hi).round() as i64;
245        let reduced = self.sub(LN2.mul(Self::from_f64(k as f64)));
246
247        // Taylor series for exp(r) where r is small
248        let one = Self::ONE;
249        let mut term = one;
250        let mut sum = one;
251        for i in 1..40 {
252            term = term.mul(reduced).div(Self::from_f64(i as f64));
253            sum = sum.add(term);
254        }
255
256        // Multiply by 2^k
257        if k >= 0 {
258            sum.mul(Self::from_f64((1u64 << k.min(62) as u64) as f64))
259        } else {
260            sum.div(Self::from_f64((1u64 << (-k).min(62) as u64) as f64))
261        }
262    }
263
264    /// Natural logarithm via Newton iteration on exp.
265    pub fn ln(self) -> Self {
266        if self.hi <= 0.0 {
267            return Self::from_f64(f64::NAN);
268        }
269        if self.hi == 1.0 && self.lo == 0.0 {
270            return Self::ZERO;
271        }
272        // Initial estimate
273        let x0 = self.hi.ln();
274        let mut x = Self::from_f64(x0);
275        // Newton iteration for ln: x = x + (self - exp(x)) / exp(x)
276        // Use a few iterations for full precision
277        for _ in 0..6 {
278            let ex = x.exp();
279            x = x.add(self.sub(ex).div(ex));
280        }
281        x
282    }
283
284    /// Sine via Taylor series with argument reduction.
285    #[allow(clippy::approx_constant)]
286    pub fn sin(self) -> Self {
287        const PI: DoubleF64 = DoubleF64 {
288            hi: std::f64::consts::PI,
289            lo: 1.2246467991473532e-16,
290        };
291        const TWO_PI: DoubleF64 = DoubleF64 {
292            hi: std::f64::consts::TAU,
293            lo: 2.4492935982947064e-16,
294        };
295
296        // Reduce to [-π, π]
297        let mut x = self;
298        if x.dabs().hi > PI.hi {
299            let k = (x.hi / TWO_PI.hi).round();
300            x = x.sub(TWO_PI.mul(Self::from_f64(k)));
301        }
302
303        // Taylor series
304        let x2 = x.mul(x);
305        let mut term = x;
306        let mut sum = x;
307        for i in 1..25 {
308            let n = (2 * i + 1) as f64;
309            term = term.mul(x2).div(Self::from_f64(-n * (n - 1.0)));
310            sum = sum.add(term);
311        }
312        sum
313    }
314
315    /// Cosine via sin(π/2 - x).
316    #[allow(clippy::approx_constant)]
317    pub fn cos(self) -> Self {
318        const FRAC_PI_2: DoubleF64 = DoubleF64 {
319            hi: std::f64::consts::FRAC_PI_2,
320            lo: 6.123233995736766e-17,
321        };
322        FRAC_PI_2.sub(self).sin()
323    }
324
325    /// Tangent = sin / cos.
326    pub fn tan(self) -> Self {
327        self.sin().div(self.cos())
328    }
329}
330
331// Note: cos(x) is implemented via the identity cos(x) = sin(π/2 − x).  This
332// is accurate to double-double precision for arguments up to a few multiples
333// of π; for very large |x| the same argument-reduction accuracy limits as
334// `sin` apply.
335
336// =========================================================================
337// std::ops trait implementations
338// =========================================================================
339
340impl Add for DoubleF64 {
341    type Output = Self;
342    #[inline]
343    fn add(self, other: Self) -> Self {
344        DoubleF64::add(self, other)
345    }
346}
347
348impl Sub for DoubleF64 {
349    type Output = Self;
350    #[inline]
351    fn sub(self, other: Self) -> Self {
352        DoubleF64::sub(self, other)
353    }
354}
355
356impl Mul for DoubleF64 {
357    type Output = Self;
358    #[inline]
359    fn mul(self, other: Self) -> Self {
360        DoubleF64::mul(self, other)
361    }
362}
363
364impl Div for DoubleF64 {
365    type Output = Self;
366    #[inline]
367    fn div(self, other: Self) -> Self {
368        DoubleF64::div(self, other)
369    }
370}
371
372impl Neg for DoubleF64 {
373    type Output = Self;
374    #[inline]
375    fn neg(self) -> Self {
376        Self {
377            hi: -self.hi,
378            lo: -self.lo,
379        }
380    }
381}
382
383impl AddAssign for DoubleF64 {
384    fn add_assign(&mut self, other: Self) {
385        *self = *self + other;
386    }
387}
388
389impl SubAssign for DoubleF64 {
390    fn sub_assign(&mut self, other: Self) {
391        *self = *self - other;
392    }
393}
394
395impl MulAssign for DoubleF64 {
396    fn mul_assign(&mut self, other: Self) {
397        *self = *self * other;
398    }
399}
400
401impl DivAssign for DoubleF64 {
402    fn div_assign(&mut self, other: Self) {
403        *self = *self / other;
404    }
405}
406
407impl PartialOrd for DoubleF64 {
408    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
409        self.hi.partial_cmp(&other.hi)
410    }
411}
412
413impl From<f64> for DoubleF64 {
414    fn from(x: f64) -> Self {
415        Self::from_f64(x)
416    }
417}
418
419impl Zero for DoubleF64 {
420    fn zero() -> Self {
421        Self::ZERO
422    }
423    fn is_zero(&self) -> bool {
424        self.hi == 0.0 && self.lo == 0.0
425    }
426}
427
428impl fmt::Display for DoubleF64 {
429    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
430        // Display with full precision
431        if self.lo == 0.0 {
432            write!(f, "{}", self.hi)
433        } else {
434            write!(f, "{:.31e}", self.hi + self.lo)
435        }
436    }
437}
438
439// =========================================================================
440// Domain trait implementation
441// =========================================================================
442
443/// Double-float domain for algebraic computations.
444#[derive(Debug, Clone, Copy, PartialEq, Eq)]
445pub struct DoubleF64Domain;
446
447impl Domain for DoubleF64Domain {
448    type Element = DoubleF64;
449
450    fn zero(&self) -> Self::Element {
451        DoubleF64::ZERO
452    }
453
454    fn one(&self) -> Self::Element {
455        DoubleF64::ONE
456    }
457
458    fn add(&self, a: &Self::Element, b: &Self::Element) -> Self::Element {
459        *a + *b
460    }
461
462    fn sub(&self, a: &Self::Element, b: &Self::Element) -> Self::Element {
463        *a - *b
464    }
465
466    fn neg(&self, a: &Self::Element) -> Self::Element {
467        -*a
468    }
469
470    fn mul(&self, a: &Self::Element, b: &Self::Element) -> Self::Element {
471        *a * *b
472    }
473
474    fn div(&self, a: &Self::Element, b: &Self::Element) -> Option<Self::Element> {
475        if b.is_zero() { None } else { Some(*a / *b) }
476    }
477
478    fn inv(&self, a: &Self::Element) -> Option<Self::Element> {
479        if a.is_zero() {
480            None
481        } else {
482            Some(DoubleF64::ONE / *a)
483        }
484    }
485
486    fn is_zero(&self, a: &Self::Element) -> bool {
487        a.is_zero()
488    }
489
490    fn is_one(&self, a: &Self::Element) -> bool {
491        *a == DoubleF64::ONE
492    }
493}
494
495// =========================================================================
496// Tests
497// =========================================================================
498
499#[cfg(test)]
500mod tests {
501    use super::*;
502
503    #[test]
504    fn basic_arithmetic() {
505        let a = DoubleF64::from_f64(1.0);
506        let b = DoubleF64::from_f64(2.0);
507        assert_eq!((a + b).hi, 3.0);
508        assert_eq!((a - b).hi, -1.0);
509        assert_eq!((a * b).hi, 2.0);
510        assert_eq!((a / b).hi, 0.5);
511    }
512
513    #[test]
514    fn precision_gain() {
515        // DoubleF64 should capture rounding errors that f64 loses
516        let a = DoubleF64::from_f64(1.0);
517        let b = DoubleF64::from_f64(f64::EPSILON);
518        let sum = a + b;
519        // 1 + eps in f64: hi=1.0, lo=eps (captured in lo)
520        let reconstructed = sum.hi + sum.lo;
521        assert_eq!(reconstructed, 1.0 + f64::EPSILON);
522    }
523
524    #[test]
525    fn two_sum_correctness() {
526        let s = DoubleF64::two_sum(1.0, f64::EPSILON);
527        assert_eq!(s.hi, 1.0 + f64::EPSILON);
528        // s.lo should capture the rounding error
529    }
530
531    #[test]
532    fn two_prod_correctness() {
533        let p = DoubleF64::two_prod(3.0, 5.0);
534        assert_eq!(p.hi, 15.0);
535        assert_eq!(p.lo, 0.0);
536    }
537
538    #[test]
539    fn powi_basic() {
540        let x = DoubleF64::from_f64(3.0);
541        assert_eq!(x.powi(0).hi, 1.0);
542        assert_eq!(x.powi(1).hi, 3.0);
543        assert_eq!(x.powi(2).hi, 9.0);
544        assert_eq!(x.powi(3).hi, 27.0);
545    }
546
547    #[test]
548    fn powi_negative() {
549        let x = DoubleF64::from_f64(2.0);
550        assert_eq!(x.powi(-1).hi, 0.5);
551        assert_eq!(x.powi(-2).hi, 0.25);
552    }
553
554    #[test]
555    fn sqrt_basic() {
556        let x = DoubleF64::from_f64(4.0);
557        let s = x.sqrt();
558        assert!((s.hi - 2.0).abs() < 1e-30);
559    }
560
561    #[test]
562    fn sqrt_two() {
563        let x = DoubleF64::from_f64(2.0);
564        let s = x.sqrt();
565        // sqrt(2)^2 should be very close to 2
566        let sq = s * s;
567        assert!((sq.hi - 2.0).abs() < 1e-28);
568    }
569
570    #[test]
571    fn exp_basic() {
572        let zero = DoubleF64::ZERO;
573        assert_eq!(zero.exp().hi, 1.0);
574
575        let one = DoubleF64::ONE;
576        let e = one.exp();
577        // exp(1) ≈ 2.718281828...
578        assert!((e.hi - std::f64::consts::E).abs() < 1e-28);
579    }
580
581    #[test]
582    fn ln_basic() {
583        let one = DoubleF64::ONE;
584        assert_eq!(one.ln().hi, 0.0);
585
586        let e = DoubleF64::from_f64(std::f64::consts::E);
587        let ln_e = e.ln();
588        assert!((ln_e.hi - 1.0).abs() < 1e-28);
589    }
590
591    #[test]
592    fn sin_cos_basic() {
593        let zero = DoubleF64::ZERO;
594        assert_eq!(zero.sin().hi, 0.0);
595        assert_eq!(zero.cos().hi, 1.0);
596    }
597
598    #[test]
599    fn sin_pi() {
600        let pi = DoubleF64::from_f64(std::f64::consts::PI);
601        let s = pi.sin();
602        // sin(π) ≈ 0; residual is rounding error in the PI constant
603        assert!(s.hi.abs() < 1e-14, "sin(π) ≈ 0, got {}", s.hi);
604    }
605
606    #[test]
607    fn domain_trait() {
608        let dom = DoubleF64Domain;
609        let a = DoubleF64::from_f64(3.0);
610        let b = DoubleF64::from_f64(4.0);
611        assert_eq!(dom.add(&a, &b).hi, 7.0);
612        assert_eq!(dom.mul(&a, &b).hi, 12.0);
613        assert_eq!(dom.div(&a, &b).unwrap().hi, 0.75);
614        assert!(dom.div(&a, &DoubleF64::ZERO).is_none());
615    }
616
617    #[test]
618    fn display() {
619        #[allow(clippy::approx_constant)]
620        let x = DoubleF64::from_f64(3.14);
621        assert_eq!(format!("{x}"), "3.14");
622    }
623}