Skip to main content

tenferro_df64_proof/
lib.rs

1//! External-scalar proof crate: a scalar type that tenferro does not define.
2//!
3//! This crate exists to show that the ordinary CPU numerical path in
4//! `tenferro-cpu` does not require the preset scalar types. It defines its own
5//! real scalar as a two-`f64` expansion, stores it in
6//! a host [`tenferro_tensor::TypedTensor`], and executes through
7//! [`tenferro_cpu::scalar_binary_into`] and [`tenferro_cpu::scalar_fold`] using
8//! the same traversal the preset scalars use.
9//!
10//! The type is deliberately small. It backs four arithmetic operations with an
11//! exact two-sum so that low-order information survives accumulation, which an
12//! `f64` round trip destroys. `xprec::Df64` is the production-shaped equivalent
13//! and can replace this type without changing the tenferro side.
14
15#![deny(missing_docs)]
16
17pub mod ad;
18
19pub mod conversion;
20mod dense;
21pub mod extension;
22
23tenferro_tensor::define_scalar_set! {
24    /// Tag for the external extended-precision set.
25    ///
26    /// # Examples
27    ///
28    /// ```rust
29    /// use tenferro_df64_proof::ExtendedTag;
30    ///
31    /// assert_ne!(ExtendedTag::F64, ExtendedTag::Df64);
32    /// ```
33    pub enum ExtendedTag {
34        /// Standard double precision.
35        F64 => f64 : Float 0 64,
36        /// The external two-component scalar, ranked above double precision.
37        Df64 => Df64 : Float 1 64,
38    }
39    /// Value enum for the external extended-precision set.
40    ///
41    /// # Examples
42    ///
43    /// ```rust
44    /// use tenferro_df64_proof::{Df64, ExtendedSet, ExtendedTag};
45    /// use tenferro_tensor::{DynRank, Host, ScalarSet, TypedTensor};
46    ///
47    /// let value = ExtendedSet::Df64(
48    ///     TypedTensor::<_, DynRank, Host>::from_host_vec_col_major(vec![1], vec![Df64::from_f64(2.0)])?,
49    /// );
50    /// assert_eq!(value.tag(), ExtendedTag::Df64);
51    /// # Ok::<(), tenferro_tensor::Error>(())
52    /// ```
53    pub enum ExtendedSet;
54}
55
56/// A real scalar carrying a high and a low `f64` component.
57///
58/// `hi` holds the rounded value and `lo` the exact residual, so
59/// `hi + lo` is the represented real number.
60///
61/// # Examples
62///
63/// ```rust
64/// use tenferro_df64_proof::Df64;
65///
66/// let carried = Df64 { hi: 1.0, lo: 2f64.powi(-80) };
67/// assert_eq!(carried.narrow_to_f64(), 1.0);
68/// assert_ne!(carried, Df64::from_f64(1.0));
69/// ```
70#[derive(Clone, Copy, Debug, Default, PartialEq)]
71pub struct Df64 {
72    /// Rounded component.
73    pub hi: f64,
74    /// Exact residual component.
75    pub lo: f64,
76}
77
78/// Exact sum of two `f64` values as a high and low component (Knuth's two-sum).
79#[inline]
80fn two_sum(a: f64, b: f64) -> (f64, f64) {
81    let sum = a + b;
82    let b_virtual = sum - a;
83    let error = (a - (sum - b_virtual)) + (b - b_virtual);
84    (sum, error)
85}
86
87impl Df64 {
88    /// Build a scalar with no low component.
89    ///
90    /// # Examples
91    ///
92    /// ```rust
93    /// use tenferro_df64_proof::Df64;
94    ///
95    /// assert_eq!(Df64::from_f64(1.5), Df64 { hi: 1.5, lo: 0.0 });
96    /// ```
97    #[inline]
98    #[must_use]
99    pub fn from_f64(value: f64) -> Self {
100        Self { hi: value, lo: 0.0 }
101    }
102
103    /// Additive identity.
104    ///
105    /// # Examples
106    ///
107    /// ```rust
108    /// use tenferro_df64_proof::Df64;
109    ///
110    /// assert_eq!(Df64::zero(), Df64 { hi: 0.0, lo: 0.0 });
111    /// ```
112    #[inline]
113    #[must_use]
114    pub fn zero() -> Self {
115        Self::from_f64(0.0)
116    }
117
118    /// Explicitly narrow to `f64`, discarding the low component.
119    ///
120    /// This is a numerical conversion, not a reinterpretation: it allocates
121    /// nothing and reads only the rounded component.
122    ///
123    /// # Examples
124    ///
125    /// ```rust
126    /// use tenferro_df64_proof::Df64;
127    ///
128    /// // The low component is deliberately dropped, and is not recovered.
129    /// assert_eq!(Df64 { hi: 1.0, lo: 2f64.powi(-80) }.narrow_to_f64(), 1.0);
130    /// ```
131    #[inline]
132    #[must_use]
133    pub fn narrow_to_f64(self) -> f64 {
134        self.hi
135    }
136
137    /// Quotient, refined to the full two-component precision.
138    ///
139    /// The first component of the result is the ordinary `f64` quotient and the
140    /// remaining error is recovered by two Newton corrections evaluated in the
141    /// two-component arithmetic, so the quotient carries both components.
142    ///
143    /// # Examples
144    ///
145    /// ```rust
146    /// use tenferro_df64_proof::Df64;
147    ///
148    /// // One divided by three is not a binary fraction, and the low component holds
149    /// // the part an `f64` quotient cannot.
150    /// let third = Df64::from_f64(1.0).ratio(Df64::from_f64(3.0));
151    /// assert_eq!(third.hi, 1.0 / 3.0);
152    /// assert_ne!(third.lo, 0.0);
153    /// assert!((third * Df64::from_f64(3.0) - Df64::from_f64(1.0)).abs_hi() < 1e-31);
154    /// ```
155    #[inline]
156    #[must_use]
157    pub fn ratio(self, divisor: Self) -> Self {
158        if divisor.hi == 0.0 || !divisor.hi.is_finite() || !self.hi.is_finite() {
159            // IEEE semantics for a non-finite operand, an infinite divisor, or a zero
160            // divisor: the first component is the answer and no correction applies.
161            return Self::from_f64(self.hi / divisor.hi);
162        }
163        let mut quotient = Self::from_f64(self.hi / divisor.hi);
164        for _ in 0..2 {
165            // The residual is computed in the two-component arithmetic, so the
166            // correction carries the part the previous quotient could not represent.
167            let residual = self - divisor * quotient;
168            if residual.hi == 0.0 {
169                break;
170            }
171            quotient = quotient + Self::from_f64(residual.hi / divisor.hi);
172        }
173        quotient
174    }
175
176    /// Square root, refined to the full two-component precision.
177    ///
178    /// A negative operand follows IEEE: the result is NaN with a zero low
179    /// component.
180    ///
181    /// # Examples
182    ///
183    /// ```rust
184    /// use tenferro_df64_proof::Df64;
185    ///
186    /// let root = Df64::from_f64(2.0).sqrt();
187    /// assert_eq!(root.hi, 2.0_f64.sqrt());
188    /// // Squaring recovers two to a precision an `f64` root cannot reach.
189    /// assert!((root * root - Df64::from_f64(2.0)).abs_hi() < 1e-31);
190    /// assert!(Df64::from_f64(-1.0).sqrt().hi.is_nan());
191    /// ```
192    #[inline]
193    #[must_use]
194    pub fn sqrt(self) -> Self {
195        if self.hi < 0.0 || !self.hi.is_finite() {
196            return Self::from_f64(self.hi.sqrt());
197        }
198        if self.hi == 0.0 {
199            return self;
200        }
201        let mut root = Self::from_f64(self.hi.sqrt());
202        for _ in 0..2 {
203            // Newton's step for a square root, evaluated in the two-component
204            // arithmetic so the refinement is not limited by `f64`.
205            let next = (root + self.ratio(root)) * Self::from_f64(0.5);
206            if next == root {
207                break;
208            }
209            root = next;
210        }
211        root
212    }
213
214    /// Absolute value of the rounded component.
215    ///
216    /// This is the scale a convergence test uses; it does not widen the represented
217    /// value.
218    ///
219    /// # Examples
220    ///
221    /// ```rust
222    /// use tenferro_df64_proof::Df64;
223    ///
224    /// assert_eq!(Df64 { hi: -2.0, lo: 1.0 }.abs_hi(), 2.0);
225    /// ```
226    #[inline]
227    #[must_use]
228    pub fn abs_hi(self) -> f64 {
229        self.hi.abs()
230    }
231}
232
233/// Exact product of two `f64` values as a high and low component (Dekker's
234/// two-product), used by the expansion product.
235#[inline]
236fn split(a: f64) -> (f64, f64) {
237    let factor = 134_217_729.0_f64;
238    let c = factor * a;
239    let hi = c - (c - a);
240    (hi, a - hi)
241}
242
243#[inline]
244fn two_product(a: f64, b: f64) -> (f64, f64) {
245    let product = a * b;
246    let (a_hi, a_lo) = split(a);
247    let (b_hi, b_lo) = split(b);
248    let error = ((a_hi * b_hi - product) + a_hi * b_lo + a_lo * b_hi) + a_lo * b_lo;
249    (product, error)
250}
251
252impl tenferro_tensor_core::Scalar for Df64 {
253    const DOMAIN: tenferro_tensor_core::ScalarDomain = tenferro_tensor_core::ScalarDomain::Field;
254}
255
256impl tenferro_tensor_core::ScalarArithmetic for Df64 {
257    fn scalar_zero() -> Self {
258        Self::zero()
259    }
260
261    fn scalar_one() -> Self {
262        Self::from_f64(1.0)
263    }
264
265    fn scalar_add(self, rhs: Self) -> Self {
266        std::ops::Add::add(self, rhs)
267    }
268
269    fn scalar_sub(self, rhs: Self) -> Self {
270        std::ops::Sub::sub(self, rhs)
271    }
272
273    /// First-order expansion product: the exact leading product plus the
274    /// first-order correction terms.
275    fn scalar_mul(self, rhs: Self) -> Self {
276        let (leading, trailing) = two_product(self.hi, rhs.hi);
277        let correction = trailing + self.hi * rhs.lo + self.lo * rhs.hi;
278        let (hi, lo) = two_sum(leading, correction);
279        Self { hi, lo }
280    }
281}
282
283/// The external contribution's addition operation.
284///
285/// The operation is a type in the crate that owns the scalar, so tenferro's
286/// kernels are instantiated once per element type and operation rather than once
287/// per call site.
288///
289/// # Examples
290///
291/// ```rust
292/// use tenferro_df64_proof::{Df64, Df64Add};
293/// use tenferro_cpu::BinaryScalarOp;
294///
295/// let sum = <Df64Add as BinaryScalarOp<Df64>>::apply(
296///     Df64::from_f64(1.0),
297///     Df64::from_f64(2.0),
298/// );
299/// assert_eq!(sum, Df64::from_f64(3.0));
300/// ```
301pub struct Df64Add;
302
303impl tenferro_cpu::BinaryScalarOp<Df64> for Df64Add {
304    fn apply(lhs: Df64, rhs: Df64) -> Df64 {
305        std::ops::Add::add(lhs, rhs)
306    }
307}
308
309impl std::ops::Mul for Df64 {
310    type Output = Self;
311
312    /// Two-component product: the exact leading product plus the first-order
313    /// corrections, which is what the contract's arithmetic promises.
314    #[inline]
315    fn mul(self, other: Self) -> Self {
316        tenferro_tensor_core::ScalarArithmetic::scalar_mul(self, other)
317    }
318}
319
320impl std::ops::Div for Df64 {
321    type Output = Self;
322
323    /// Two-component quotient, refined to both components.
324    #[inline]
325    fn div(self, other: Self) -> Self {
326        Df64::ratio(self, other)
327    }
328}
329
330impl std::ops::Add for Df64 {
331    type Output = Self;
332
333    /// Exact addition of two expansions.
334    #[inline]
335    fn add(self, other: Self) -> Self {
336        let (s1, s2) = two_sum(self.hi, other.hi);
337        let (t1, t2) = two_sum(self.lo, other.lo);
338        let (s2, s3) = two_sum(s2, t1);
339        let lo = s3 + t2;
340        let (hi, lo) = two_sum(s1, s2 + lo);
341        Self { hi, lo }
342    }
343}
344
345impl std::ops::Neg for Df64 {
346    type Output = Self;
347
348    #[inline]
349    fn neg(self) -> Self {
350        Self {
351            hi: -self.hi,
352            lo: -self.lo,
353        }
354    }
355}
356
357impl std::ops::Sub for Df64 {
358    type Output = Self;
359
360    /// Exact subtraction.
361    #[inline]
362    fn sub(self, other: Self) -> Self {
363        std::ops::Add::add(self, std::ops::Neg::neg(other))
364    }
365}