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}