Skip to main content

scirs2_core/arithmetic/
double_double.rs

1//! Double-double arithmetic (~31 decimal digits of precision).
2//!
3//! A double-double number is represented as `(hi, lo)` where `hi + lo` is more
4//! precise than a single `f64`. The invariant is `|lo| ≤ 0.5 ulp(hi)`,
5//! giving approximately 106 bits (~31.9 significant decimal digits) of precision.
6//!
7//! ## Error-free Transformations
8//!
9//! All arithmetic operations use classic error-free transformations (EFTs):
10//!
11//! - **TwoSum** (Knuth/Møller): computes `(s, e)` such that `s + e == a + b` exactly.
12//! - **TwoProd** (Veltkamp splitting): computes `(p, e)` such that `p + e == a * b` exactly.
13//!
14//! ## References
15//!
16//! * Dekker, T. J. (1971). "A floating-point technique for extending the available precision."
17//!   *Numerische Mathematik* 18, 224–242.
18//! * Shewchuk, J. R. (1997). "Adaptive precision floating-point arithmetic."
19//!   *DCG* 18, 305–363.
20//! * Ogita, T.; Rump, S. M.; Oishi, S. (2005). "Accurate sum and dot product."
21//!   *SIAM J. Sci. Comput.* 26(6), 1955–1988.
22
23use core::cmp::Ordering;
24use core::fmt;
25use core::ops::{Add, Div, Mul, Neg, Sub};
26
27use crate::error::{CoreError, CoreResult, ErrorContext};
28
29// ─── Internal helpers ────────────────────────────────────────────────────────
30
31#[inline(always)]
32fn comp_err(msg: impl Into<String>) -> CoreError {
33    CoreError::ComputationError(ErrorContext::new(msg))
34}
35
36// ─── Error-free transformations ──────────────────────────────────────────────
37
38/// TwoSum (Knuth algorithm): returns `(s, e)` with `s + e == a + b` exactly
39/// in round-to-nearest IEEE 754 arithmetic.
40///
41/// # Example
42/// ```rust
43/// use scirs2_core::arithmetic::DoubleDouble;
44/// let dd = DoubleDouble::new(1.0) + DoubleDouble::new(2.0_f64.powi(-53));
45/// // The result captures full precision.
46/// assert!(dd.is_finite());
47/// ```
48#[inline]
49fn two_sum(a: f64, b: f64) -> (f64, f64) {
50    let s = a + b;
51    let v = s - a;
52    let e = (a - (s - v)) + (b - v);
53    (s, e)
54}
55
56/// TwoProd via Veltkamp splitting: returns `(p, e)` with `p + e == a * b` exactly.
57///
58/// Uses the Veltkamp split constant 2^27 + 1 = 134_217_729.
59#[inline]
60fn two_product(a: f64, b: f64) -> (f64, f64) {
61    let p = a * b;
62    // Veltkamp split of a.
63    let c = 134_217_729.0_f64 * a; // 2^27 + 1
64    let a_hi = c - (c - a);
65    let a_lo = a - a_hi;
66    // Veltkamp split of b.
67    let c2 = 134_217_729.0_f64 * b;
68    let b_hi = c2 - (c2 - b);
69    let b_lo = b - b_hi;
70    let e = ((a_hi * b_hi - p) + a_hi * b_lo + a_lo * b_hi) + a_lo * b_lo;
71    (p, e)
72}
73
74/// Veltkamp split: returns `(hi, lo)` such that `a == hi + lo` and
75/// `hi` has at most 26 significant bits.
76#[inline]
77fn split(a: f64) -> (f64, f64) {
78    let c = 134_217_729.0_f64 * a;
79    let hi = c - (c - a);
80    let lo = a - hi;
81    (hi, lo)
82}
83
84// ─── DoubleDouble struct ──────────────────────────────────────────────────────
85
86/// Double-double precision floating-point number.
87///
88/// Represents the unevaluated sum `hi + lo` where `|lo| ≤ 0.5 ulp(hi)`.
89/// Provides approximately 31 decimal digits of precision using two `f64` values.
90///
91/// # Constants
92///
93/// ```rust
94/// use scirs2_core::arithmetic::DoubleDouble;
95///
96/// let zero = DoubleDouble::ZERO;
97/// let one = DoubleDouble::ONE;
98/// let pi = DoubleDouble::PI;
99/// let e = DoubleDouble::E;
100/// ```
101#[derive(Debug, Clone, Copy, Default)]
102pub struct DoubleDouble {
103    /// High-order word (most significant bits).
104    pub hi: f64,
105    /// Low-order word (error term; `|lo| ≤ 0.5 ulp(hi)`).
106    pub lo: f64,
107}
108
109impl DoubleDouble {
110    // ── Constants ──────────────────────────────────────────────────────────────
111
112    /// Zero.
113    pub const ZERO: Self = Self { hi: 0.0, lo: 0.0 };
114
115    /// One.
116    pub const ONE: Self = Self { hi: 1.0, lo: 0.0 };
117
118    /// π to double-double precision.
119    /// Value: 3.14159265358979323846264338327950288...
120    pub const PI: Self = Self {
121        hi: std::f64::consts::PI,
122        lo: 1.224_646_799_147_353_2e-16,
123    };
124
125    /// e = exp(1) to double-double precision.
126    /// Value: 2.71828182845904523536028747135266249...
127    pub const E: Self = Self {
128        hi: std::f64::consts::E,
129        lo: -2.842_170_943_040_400_8e-17,
130    };
131
132    // ── Constructors ───────────────────────────────────────────────────────────
133
134    /// Construct from a single `f64` (error term is zero).
135    #[inline]
136    #[must_use]
137    pub fn new(x: f64) -> Self {
138        Self { hi: x, lo: 0.0 }
139    }
140
141    /// Construct from explicit `(hi, lo)` parts with renormalization.
142    #[inline]
143    #[must_use]
144    pub fn from_f64(hi: f64, lo: f64) -> Self {
145        Self::renorm(hi, lo)
146    }
147
148    /// Construct from an `i64` integer exactly.
149    #[inline]
150    #[must_use]
151    pub fn from_i64(x: i64) -> Self {
152        let hi = x as f64;
153        // Compute residual without overflow risk: works for i64 values fitting in f64.
154        let lo = (x - hi as i64) as f64;
155        Self::renorm(hi, lo)
156    }
157
158    // ── Conversion ─────────────────────────────────────────────────────────────
159
160    /// Convert to `f64` — returns only the high-order word.
161    #[inline]
162    #[must_use]
163    pub fn to_f64(self) -> f64 {
164        self.hi
165    }
166
167    /// Convert to `f64` with compensated rounding (returns `hi + lo`).
168    /// Loses precision in the conversion but is numerically more accurate than `to_f64`.
169    #[inline]
170    #[must_use]
171    pub fn to_f128_approx(self) -> f64 {
172        self.hi + self.lo
173    }
174
175    // ── Predicates ─────────────────────────────────────────────────────────────
176
177    /// Returns `true` if both `hi` and `lo` are finite.
178    #[inline]
179    #[must_use]
180    pub fn is_finite(self) -> bool {
181        self.hi.is_finite() && self.lo.is_finite()
182    }
183
184    /// Returns `true` if `hi` is NaN.
185    #[inline]
186    #[must_use]
187    pub fn is_nan(self) -> bool {
188        self.hi.is_nan()
189    }
190
191    /// Returns `true` if both components are zero.
192    #[inline]
193    #[must_use]
194    pub fn is_zero(self) -> bool {
195        self.hi == 0.0 && self.lo == 0.0
196    }
197
198    // ── Arithmetic ─────────────────────────────────────────────────────────────
199
200    /// Absolute value.
201    #[inline]
202    #[must_use]
203    pub fn abs(self) -> Self {
204        if self.hi < 0.0 {
205            Self {
206                hi: -self.hi,
207                lo: -self.lo,
208            }
209        } else {
210            self
211        }
212    }
213
214    /// Negate: returns `−self`.
215    #[inline]
216    #[must_use]
217    pub fn negate(self) -> Self {
218        Self {
219            hi: -self.hi,
220            lo: -self.lo,
221        }
222    }
223
224    /// Add `self + rhs` using double-double arithmetic.
225    ///
226    /// Uses the precise algorithm (Shewchuk 1997).
227    #[must_use]
228    #[allow(clippy::should_implement_trait)]
229    pub fn add(self, rhs: Self) -> Self {
230        let (s1, s2) = two_sum(self.hi, rhs.hi);
231        let (t1, t2) = two_sum(self.lo, rhs.lo);
232        let c = s2 + t1;
233        let (v_hi, v_lo) = two_sum(s1, c);
234        let w = t2 + v_lo;
235        Self::renorm(v_hi, w)
236    }
237
238    /// Subtract `self - rhs`.
239    #[must_use]
240    #[allow(clippy::should_implement_trait)]
241    pub fn sub(self, rhs: Self) -> Self {
242        self.add(rhs.negate())
243    }
244
245    /// Multiply `self * rhs`.
246    ///
247    /// Uses Dekker's algorithm with TwoProd.
248    #[must_use]
249    #[allow(clippy::should_implement_trait)]
250    pub fn mul(self, rhs: Self) -> Self {
251        let (p1, p2) = two_product(self.hi, rhs.hi);
252        let p2 = p2 + self.hi * rhs.lo + self.lo * rhs.hi;
253        Self::renorm(p1, p2)
254    }
255
256    /// Divide `self / rhs`.
257    ///
258    /// Returns `Err` if `rhs` is zero.
259    #[allow(clippy::should_implement_trait)]
260    pub fn div(self, rhs: Self) -> CoreResult<Self> {
261        if rhs.is_zero() {
262            return Err(comp_err("DoubleDouble::div — division by zero"));
263        }
264        // First approximation in f64.
265        let q1 = self.hi / rhs.hi;
266        // Residual: self − q1 × rhs.
267        let r = self.sub(Self::new(q1).mul(rhs));
268        // Correction term.
269        let q2 = r.hi / rhs.hi;
270        Ok(Self::renorm(q1, q2))
271    }
272
273    /// Multiply `self × rhs` where `rhs` is a plain `f64`.
274    ///
275    /// Slightly cheaper than the full double-double multiplication.
276    #[must_use]
277    pub fn mul_f64(self, rhs: f64) -> Self {
278        let (p1, p2) = two_product(self.hi, rhs);
279        let p2 = p2 + self.lo * rhs;
280        Self::renorm(p1, p2)
281    }
282
283    /// Square of `self` (cheaper than `mul(self, self)`).
284    #[must_use]
285    pub fn square(self) -> Self {
286        let (p1, p2) = two_product(self.hi, self.hi);
287        let p2 = p2 + 2.0 * self.hi * self.lo;
288        Self::renorm(p1, p2)
289    }
290
291    /// Square root via Newton-Raphson iteration in double-double precision.
292    ///
293    /// Returns `Err` for negative inputs.
294    pub fn sqrt(self) -> CoreResult<Self> {
295        if self.hi < 0.0 {
296            return Err(comp_err("DoubleDouble::sqrt — negative input"));
297        }
298        if self.is_zero() {
299            return Ok(Self::ZERO);
300        }
301        // Initial approximation from f64 sqrt.
302        let x0 = Self::new(self.hi.sqrt());
303        let half = Self::new(0.5);
304        // Newton step: x_{n+1} = (x_n + a/x_n) / 2
305        let x1 = x0.add(self.div(x0)?).mul(half);
306        // Second refinement for full double-double accuracy.
307        let x2 = x1.add(self.div(x1)?).mul(half);
308        Ok(x2)
309    }
310
311    /// Compute `e^self` using range reduction and Taylor series in DD arithmetic.
312    ///
313    /// Range reduction: `x = k*ln(2) + r`, then `exp(x) = 2^k * exp(r)`.
314    pub fn exp(self) -> CoreResult<Self> {
315        if !self.is_finite() {
316            return Err(comp_err("DoubleDouble::exp — non-finite input"));
317        }
318        let ln2 = Self::ln2_const();
319        let k_f = (self.hi / ln2.hi).round();
320        let k = k_f as i64;
321        let k_ln2 = Self::new(k_f).mul(ln2);
322        let r = self.sub(k_ln2);
323        // Taylor series for exp(r): sum_{n=0}^{N} r^n / n!
324        let n_terms = 30usize;
325        let mut sum = Self::ONE;
326        let mut term = Self::ONE;
327        for n in 1..=n_terms {
328            term = term.mul(r).div(Self::from_i64(n as i64))?;
329            let new_sum = sum.add(term);
330            if term.abs().hi.abs() < sum.abs().hi * f64::EPSILON * 0.5 {
331                sum = new_sum;
332                break;
333            }
334            sum = new_sum;
335        }
336        // Multiply by 2^k using ldexp.
337        let exp_k = k + 1023;
338        if exp_k <= 0 || exp_k >= 2047 {
339            // Underflow or overflow: use f64 as fallback.
340            let scale = f64::from(2.0_f32).powi(k as i32);
341            return Ok(Self::renorm(sum.hi * scale, sum.lo * scale));
342        }
343        let scale = f64::from_bits((exp_k as u64) << 52);
344        Ok(Self::renorm(sum.hi * scale, sum.lo * scale))
345    }
346
347    /// Compute `ln(self)` for positive inputs.
348    ///
349    /// Uses a Newton refinement of the f64 result.
350    pub fn ln(self) -> CoreResult<Self> {
351        if self.hi <= 0.0 {
352            return Err(comp_err("DoubleDouble::ln — argument must be positive"));
353        }
354        if !self.is_finite() {
355            return Err(comp_err("DoubleDouble::ln — non-finite input"));
356        }
357        // Starting approximation.
358        let a0 = Self::new(self.hi.ln());
359        // Newton refinement: a1 = a0 + (x - exp(a0)) / exp(a0)
360        let exp_a0 = a0.exp()?;
361        let correction = self.sub(exp_a0).div(exp_a0)?;
362        Ok(a0.add(correction))
363    }
364
365    /// Compute `sin(self)` in double-double precision.
366    ///
367    /// Returns only the sine component. For simultaneous sin/cos, prefer `sincos`.
368    pub fn sin(self) -> CoreResult<Self> {
369        let (s, _c) = self.sincos()?;
370        Ok(s)
371    }
372
373    /// Compute `cos(self)` in double-double precision.
374    ///
375    /// Returns only the cosine component. For simultaneous sin/cos, prefer `sincos`.
376    pub fn cos(self) -> CoreResult<Self> {
377        let (_s, c) = self.sincos()?;
378        Ok(c)
379    }
380
381    /// Compute `(sin(self), cos(self))` simultaneously.
382    ///
383    /// Uses argument reduction to `[-π/4, π/4]` followed by Taylor series.
384    pub fn sincos(self) -> CoreResult<(Self, Self)> {
385        if !self.is_finite() {
386            return Err(comp_err("DoubleDouble::sincos — non-finite input"));
387        }
388        let pi = Self::PI;
389        let two = Self::new(2.0);
390        let two_over_pi = two.div(pi)?;
391        let k_f = self.mul(two_over_pi).hi.round();
392        let k = k_f as i64;
393        let half_pi = pi.mul_f64(0.5);
394        let r = self.sub(Self::from_i64(k).mul(half_pi));
395        let r2 = r.square();
396        let n_terms = 20usize;
397        // sin(r) = r - r³/3! + r⁵/5! - …
398        let mut sin_val = r;
399        let mut term_sin = r;
400        // cos(r) = 1 - r²/2! + r⁴/4! - …
401        let mut cos_val = Self::ONE;
402        let mut term_cos = Self::ONE;
403        for i in 1..=n_terms {
404            term_sin = term_sin
405                .mul(r2.negate())
406                .div(Self::from_i64((2 * i) as i64))?
407                .div(Self::from_i64((2 * i + 1) as i64))?;
408            term_cos = term_cos
409                .mul(r2.negate())
410                .div(Self::from_i64((2 * i - 1) as i64))?
411                .div(Self::from_i64((2 * i) as i64))?;
412            let new_sin = sin_val.add(term_sin);
413            let new_cos = cos_val.add(term_cos);
414            let conv = term_sin.abs().hi.abs() < sin_val.abs().hi * f64::EPSILON * 0.5;
415            sin_val = new_sin;
416            cos_val = new_cos;
417            if conv {
418                break;
419            }
420        }
421        // Unreduce based on quarter index k mod 4.
422        let km4 = ((k % 4) + 4) as usize % 4;
423        let (s, c) = match km4 {
424            0 => (sin_val, cos_val),
425            1 => (cos_val, sin_val.negate()),
426            2 => (sin_val.negate(), cos_val.negate()),
427            _ => (cos_val.negate(), sin_val),
428        };
429        Ok((s, c))
430    }
431
432    /// Integer power via repeated squaring.
433    ///
434    /// Handles negative exponents by inversion.
435    pub fn powi(self, n: i32) -> CoreResult<Self> {
436        if n == 0 {
437            return Ok(Self::ONE);
438        }
439        if n < 0 {
440            let inv = Self::ONE.div(self)?;
441            return inv.powi(-n);
442        }
443        let mut result = Self::ONE;
444        let mut base = self;
445        let mut exp = n as u32;
446        while exp > 0 {
447            if exp & 1 == 1 {
448                result = result.mul(base);
449            }
450            base = base.square();
451            exp >>= 1;
452        }
453        Ok(result)
454    }
455
456    // ── Comparison ──────────────────────────────────────────────────────────────
457
458    /// Compare `self` with `rhs` for ordering.
459    #[must_use]
460    pub fn compare(&self, rhs: &Self) -> Ordering {
461        match self.hi.partial_cmp(&rhs.hi) {
462            Some(Ordering::Equal) => self.lo.partial_cmp(&rhs.lo).unwrap_or(Ordering::Equal),
463            Some(ord) => ord,
464            None => Ordering::Equal, // NaN
465        }
466    }
467
468    // ── Internal helpers ────────────────────────────────────────────────────────
469
470    /// Renormalize `hi + lo` so the invariant `|lo| ≤ 0.5 ulp(hi)` holds.
471    #[inline]
472    #[must_use]
473    pub fn renorm(hi: f64, lo: f64) -> Self {
474        let (s, e) = two_sum(hi, lo);
475        Self { hi: s, lo: e }
476    }
477
478    /// ln(2) to double-double precision (internal constant).
479    #[inline]
480    fn ln2_const() -> Self {
481        Self {
482            hi: std::f64::consts::LN_2,
483            lo: 2.319_046_813_846_299_6e-17,
484        }
485    }
486}
487
488// ─── Trait implementations ────────────────────────────────────────────────────
489
490impl PartialEq for DoubleDouble {
491    fn eq(&self, other: &Self) -> bool {
492        self.hi == other.hi && self.lo == other.lo
493    }
494}
495
496impl PartialOrd for DoubleDouble {
497    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
498        Some(self.compare(other))
499    }
500}
501
502impl Neg for DoubleDouble {
503    type Output = DoubleDouble;
504    fn neg(self) -> DoubleDouble {
505        self.negate()
506    }
507}
508
509impl Add for DoubleDouble {
510    type Output = DoubleDouble;
511    fn add(self, rhs: DoubleDouble) -> DoubleDouble {
512        DoubleDouble::add(self, rhs)
513    }
514}
515
516impl Sub for DoubleDouble {
517    type Output = DoubleDouble;
518    fn sub(self, rhs: DoubleDouble) -> DoubleDouble {
519        DoubleDouble::sub(self, rhs)
520    }
521}
522
523impl Mul for DoubleDouble {
524    type Output = DoubleDouble;
525    fn mul(self, rhs: DoubleDouble) -> DoubleDouble {
526        DoubleDouble::mul(self, rhs)
527    }
528}
529
530/// Division via `div`; returns `DoubleDouble { hi: NaN, lo: NaN }` if `rhs` is zero.
531/// Prefer [`DoubleDouble::div`] for error handling.
532impl Div for DoubleDouble {
533    type Output = DoubleDouble;
534    fn div(self, rhs: DoubleDouble) -> DoubleDouble {
535        DoubleDouble::div(self, rhs).unwrap_or(DoubleDouble {
536            hi: f64::NAN,
537            lo: f64::NAN,
538        })
539    }
540}
541
542impl From<f64> for DoubleDouble {
543    fn from(x: f64) -> DoubleDouble {
544        DoubleDouble::new(x)
545    }
546}
547
548impl From<i64> for DoubleDouble {
549    fn from(x: i64) -> DoubleDouble {
550        DoubleDouble::from_i64(x)
551    }
552}
553
554impl From<i32> for DoubleDouble {
555    fn from(x: i32) -> DoubleDouble {
556        DoubleDouble::new(x as f64)
557    }
558}
559
560impl fmt::Display for DoubleDouble {
561    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
562        // Display using hi + lo for maximum precision representation.
563        write!(f, "{:.30e} + {:.10e}", self.hi, self.lo)
564    }
565}
566
567// ─── Free functions ────────────────────────────────────────────────────────────
568
569/// Compute an accumulated dot product `Σ a[i] × b[i]` in double-double precision.
570///
571/// This is more accurate than a naive f64 dot product for ill-conditioned inputs
572/// (Ogita, Rump, Oishi 2005 — Algorithm `AccDot`).
573///
574/// # Panics
575///
576/// Does not panic. Returns `DoubleDouble::ZERO` if either slice is empty or lengths differ.
577///
578/// # Example
579///
580/// ```rust
581/// use scirs2_core::arithmetic::dot_dd;
582///
583/// let a = [1.0_f64, 2.0, 3.0];
584/// let b = [4.0_f64, 5.0, 6.0];
585/// let result = dot_dd(&a, &b);
586/// assert!((result.to_f64() - 32.0).abs() < 1e-12);
587/// ```
588pub fn dot_dd(a: &[f64], b: &[f64]) -> DoubleDouble {
589    let n = a.len().min(b.len());
590    let mut sum = DoubleDouble::ZERO;
591    for i in 0..n {
592        let (p, e) = two_product(a[i], b[i]);
593        let (s1, s2) = two_sum(sum.hi, p);
594        let err = s2 + e + sum.lo;
595        let (hi, lo_part) = two_sum(s1, err);
596        sum = DoubleDouble { hi, lo: lo_part };
597    }
598    sum
599}
600
601/// Compute the sum of `values` in double-double precision.
602///
603/// Uses the Ogita-Rump-Oishi compensated summation algorithm (`AccSum`),
604/// which correctly handles catastrophic cancellation.
605///
606/// # Example
607///
608/// ```rust
609/// use scirs2_core::arithmetic::sum_dd;
610///
611/// // Without compensation, [1.0, 1e100, 1.0, -1e100] loses the 2.0.
612/// let values = [1.0_f64, 1e100, 1.0, -1e100];
613/// let result = sum_dd(&values);
614/// assert!((result.to_f64() - 2.0).abs() < 1e-10,
615///     "Expected 2.0, got {}", result.to_f64());
616/// ```
617pub fn sum_dd(values: &[f64]) -> DoubleDouble {
618    let mut sum = DoubleDouble::ZERO;
619    for &v in values {
620        let (s, e) = two_sum(sum.hi, v);
621        sum.lo += e;
622        sum.hi = s;
623    }
624    // Final pass to absorb accumulated lo.
625    let (hi, lo_part) = two_sum(sum.hi, sum.lo);
626    DoubleDouble { hi, lo: lo_part }
627}
628
629// ─── Tests ────────────────────────────────────────────────────────────────────
630
631#[cfg(test)]
632mod tests {
633    use super::*;
634
635    #[test]
636    fn test_zero_identity_add() {
637        let x = DoubleDouble::new(std::f64::consts::PI);
638        let result = DoubleDouble::ZERO + x;
639        assert!((result.hi - x.hi).abs() < f64::EPSILON);
640        assert!((result.lo - x.lo).abs() < f64::EPSILON * 2.0);
641    }
642
643    #[test]
644    fn test_one_identity_mul() {
645        let x = DoubleDouble::new(std::f64::consts::E);
646        let result = DoubleDouble::ONE * x;
647        assert!((result.hi - x.hi).abs() < f64::EPSILON * 2.0);
648    }
649
650    #[test]
651    fn test_two_sum_exact() {
652        let a = 1.0_f64;
653        let b = f64::EPSILON / 2.0;
654        let (s, e) = two_sum(a, b);
655        assert_eq!(s + e, a + b, "TwoSum: s + e must equal a + b exactly");
656    }
657
658    #[test]
659    fn test_two_product_exact() {
660        let a = 1.0_f64 + f64::EPSILON;
661        let b = 1.0_f64 + f64::EPSILON;
662        let (p, e) = two_product(a, b);
663        let reconstructed = p + e;
664        assert!(
665            (reconstructed - a * b).abs() <= f64::EPSILON * 4.0,
666            "TwoProd roundtrip: {reconstructed} vs {}",
667            a * b
668        );
669    }
670
671    #[test]
672    fn test_split_exact() {
673        let a = 1.234_567_890_123_456_7_f64;
674        let (hi, lo) = split(a);
675        assert_eq!(hi + lo, a, "split: hi + lo must equal a exactly");
676    }
677
678    #[test]
679    fn test_sqrt_four() {
680        let four = DoubleDouble::new(4.0);
681        let s = four.sqrt().expect("sqrt(4.0) should succeed");
682        assert!(
683            (s.hi - 2.0).abs() < f64::EPSILON * 4.0,
684            "sqrt(4.0) = 2.0, got {}",
685            s.hi
686        );
687    }
688
689    #[test]
690    fn test_exp_one_approx_e() {
691        let one = DoubleDouble::ONE;
692        let e_val = one.exp().expect("exp(1) should succeed");
693        let e_exact = std::f64::consts::E;
694        // DD result should agree with e to at least 30 decimal places.
695        let diff = (e_val.hi + e_val.lo - e_exact).abs();
696        assert!(diff < 1e-30, "exp(1) − e = {diff}");
697    }
698
699    #[test]
700    fn test_sin_pi_over_6() {
701        // sin(π/6) = 0.5 exactly
702        let pi_over_6 = DoubleDouble::PI.mul_f64(1.0 / 6.0);
703        let s = pi_over_6.sin().expect("sin should succeed");
704        let diff = (s.hi + s.lo - 0.5).abs();
705        assert!(diff < 1e-14, "sin(π/6) ≈ 0.5, diff = {diff}");
706    }
707
708    #[test]
709    fn test_cos_pi_over_3() {
710        // cos(π/3) = 0.5 exactly
711        let pi_over_3 = DoubleDouble::PI.mul_f64(1.0 / 3.0);
712        let c = pi_over_3.cos().expect("cos should succeed");
713        let diff = (c.hi + c.lo - 0.5).abs();
714        assert!(diff < 1e-14, "cos(π/3) ≈ 0.5, diff = {diff}");
715    }
716
717    #[test]
718    fn test_dot_dd_precision() {
719        // Ill-conditioned case: naive f64 dot product loses digits.
720        let a = [1e15_f64, 1.0, -1e15];
721        let b = [1.0_f64, 1e15, 1.0];
722        // Exact result: 1e15 + 1e15 - 1e15 = 1e15 but also has 1e15*1 + 1*1e15 - 1e15*1
723        // Simple test: dot([1, 2, 3], [4, 5, 6]) = 32
724        let a2 = [1.0_f64, 2.0, 3.0];
725        let b2 = [4.0_f64, 5.0, 6.0];
726        let result = dot_dd(&a2, &b2);
727        assert!(
728            (result.hi - 32.0).abs() < f64::EPSILON * 4.0,
729            "dot = {}",
730            result.hi
731        );
732    }
733
734    #[test]
735    fn test_sum_dd_catastrophic_cancellation() {
736        // Without compensation this sum loses the 2.0 entirely.
737        let values = [1.0_f64, 1e100, 1.0, -1e100];
738        let result = sum_dd(&values);
739        assert!(
740            (result.to_f64() - 2.0).abs() < 1e-10,
741            "sum_dd of [1, 1e100, 1, -1e100] should be 2.0, got {}",
742            result.to_f64()
743        );
744    }
745
746    #[test]
747    fn test_display_non_empty() {
748        let x = DoubleDouble::new(1.5);
749        let s = format!("{x}");
750        assert!(!s.is_empty(), "Display should produce non-empty string");
751        assert!(s.contains("1.5") || s.contains("1."), "Display: {s}");
752    }
753
754    #[test]
755    fn test_powi_basic() {
756        let two = DoubleDouble::new(2.0);
757        let eight = two.powi(3).expect("2^3 should succeed");
758        assert!(
759            (eight.hi - 8.0).abs() < f64::EPSILON * 4.0,
760            "2^3 = 8, got {}",
761            eight.hi
762        );
763    }
764
765    #[test]
766    fn test_mul_f64() {
767        let x = DoubleDouble::new(3.0);
768        let result = x.mul_f64(4.0);
769        assert!((result.hi - 12.0).abs() < f64::EPSILON * 4.0);
770    }
771
772    #[test]
773    fn test_pi_constant() {
774        let pi = DoubleDouble::PI;
775        let diff = (pi.hi - std::f64::consts::PI).abs();
776        assert!(diff < f64::EPSILON * 2.0, "PI hi part error: {diff}");
777        assert!(
778            pi.lo.abs() > 0.0,
779            "PI lo should be nonzero for extra precision"
780        );
781    }
782
783    #[test]
784    fn test_e_constant() {
785        let e = DoubleDouble::E;
786        let diff = (e.hi - std::f64::consts::E).abs();
787        assert!(diff < f64::EPSILON * 2.0, "E hi part error: {diff}");
788    }
789}