Skip to main content

thermite_compensated/
math.rs

1use crate::{Compensated, CompensatedFloatVector, ScalarValue};
2
3use thermite::prelude::*;
4
5use thermite::element::FloatElementWithBits;
6use thermite::vector::AsFloatVectorWithBitsKernel;
7
8use thermite::math::policy::{
9    PrecisionPolicy,
10    policies::{CheckOverflow, PreserveDenormals},
11};
12use thermite::math::specialized::{
13    SpecializedCoreMath, SpecializedPrimalMath, SpecializedRealMath, SpecializedSpatialMath,
14    SpecializedTranscendentalMath,
15};
16use thermite::math::{RealMathWithPolicy, TranscendentalMathWithPolicy};
17
18// Deliberately its own primal: the error half of a double-double constant
19// carries real precision, not augmentation, so tables must keep it. The
20// `PrimalProjection` fixpoint (`Primal = Self`, identity conversions) comes
21// from the blanket impl in `thermite::math`, via `SpecializedPrimalMath` below.
22impl<V: CompensatedFloatVector> SpecializedCoreMath<Compensated<V::Element>> for Compensated<V>
23where
24    V: RealMathWithPolicy,
25{
26    #[inline(always)]
27    fn inverse_sqrt<P: Policy>(self) -> Self {
28        Self::rsqrt(self)
29    }
30}
31
32impl<V: CompensatedFloatVector> SpecializedPrimalMath<Compensated<V::Element>> for Compensated<V> where
33    V: RealMathWithPolicy
34{
35}
36
37impl<V: CompensatedFloatVector> SpecializedTranscendentalMath<Compensated<V::Element>> for Compensated<V>
38where
39    V: RealMathWithPolicy,
40{
41    /// `$(\sin \pi x, \cos \pi x)$`, reducing **before** multiplying by pi.
42    ///
43    /// The inherited default is `sin_cos(self * PI)`, which throws away most of what
44    /// this type exists for. Forming `x * PI` rounds the product, so the argument handed
45    /// to `sin_cos` already carries an absolute error of about `|x| * 2^-106`; at
46    /// `x = -1000.5` (an ordinary argument for the gamma reflection) that is three or
47    /// four digits gone before any trigonometry happens.
48    ///
49    /// Reducing first avoids it entirely. `sin(pi(n + r)) = (-1)^n sin(pi r)` for integer
50    /// `n`, and `x - round(x)` is *exact*, so the only rounded product is `r * PI` with
51    /// `|r| <= 1/2`. Same for cosine, with the same sign flip.
52    #[inline(always)]
53    fn sincos_pi<P: Policy>(self) -> (Self, Self) {
54        // n = round(x), r = x - n exactly, |r| <= 1/2.
55        let n = self.value().round();
56        let r = self - Self::new(n);
57
58        let (s, c) = <Self as SpecializedTranscendentalMath<Compensated<V::Element>>>::sin_cos::<P>(r * Self::PI);
59
60        // (-1)^n: odd n flips both. Halving is exact, so `n/2` having a fractional part
61        // is the oddness test. Past 2^mantissa every representable value is even, which
62        // this reports correctly rather than by accident.
63        let half = n * V::HALF;
64        let odd = half.cmp_ne(half.floor());
65
66        (s.neg_c(odd), c.neg_c(odd))
67    }
68
69    #[inline(always)]
70    fn sin_cos<P: Policy>(self) -> (Self, Self) {
71        // 1. Argument Reduction
72        // Reduce x to r in [-pi/4, pi/4]
73        // k = round(x / (pi/2))
74
75        // 1a. Calculate k = round(x / (pi/2)) = round(x * (2/pi))
76        let k = (self * Self::FRAC_2_PI).value().round();
77        let k_comp = Self::new(k);
78
79        // 1b. Compute r = x - k * (pi/2)
80        // We must use the Compensated constant FRAC_PI_2 for high precision subtraction.
81        // x - (k * PI/2)
82        let r = k_comp.mul_add(-Self::FRAC_PI_2, self);
83
84        let r2 = -r.square(); // -r^2, used for iterative multiplication
85
86        // 2. Series Expansion
87        // sin(r) = r - r^3/3! + r^5/5! ...
88        // cos(r) = 1 - r^2/2! + r^4/4! ...
89
90        // Initialize sums and terms
91        // Term indices:
92        // k=1: sin term needs /2*3, cos term needs /1*2
93
94        let mut sin = r;
95        let mut term_s = r;
96
97        let mut cos = Self::ONE;
98        let mut term_c = Self::ONE;
99
100        let shift = if P::POLICY.unroll_loops { 1 } else { 0 }; // Conservative unroll
101        let mut i = 1;
102
103        // Approx 20 iterations sufficient for full compensated precision
104        let max_i = (P::POLICY.max_iterations >> shift) + 1;
105
106        while i < max_i {
107            let next_i = i + (1 << shift);
108            let prev_s = sin;
109            let prev_c = cos;
110
111            for k in i..next_i {
112                let k2 = (2 * k) as i64;
113
114                let div_c = (k2 - 1) * k2; // ((2k-1)*2k)
115                let div_s = k2 * (k2 + 1); // (2k*(2k+1))
116
117                // These should almost always succeed, since within the iteration limits
118                // k will be at most around 20, so div_c and div_s will be at most
119                // around 1600 or so before converging. This is well within the range of
120                // even f32 integer representation.
121                let (Some(div_c), Some(div_s)) = (FloatElement::try_from_int(div_c), FloatElement::try_from_int(div_s))
122                else {
123                    #[cold]
124                    fn this_branch_is_unlikely() {}
125                    this_branch_is_unlikely();
126                    break;
127                };
128
129                // Update Cosine Term: prev_term * (-r^2) / div_c
130                term_c *= r2 / V::splat(div_c);
131                cos.accumulate_unnormalized(term_c);
132
133                // Update Sine Term: prev_term * (-r^2) / div_s
134                term_s *= r2 / V::splat(div_s);
135                sin.accumulate_unnormalized(term_s);
136            }
137
138            if (prev_s.cmp_eq(sin) & prev_c.cmp_eq(cos)).all() {
139                // println!("trig converged at i={}", next_i - 1);
140                break;
141            }
142
143            i = next_i;
144        }
145
146        // If we have bit manipulation capabilities, we can analyze k directly
147        // for efficient quadrant handling.
148        if let Some((bit0, bit1)) = thermite::with_bits!([k]: [V; 1]
149            as fn(values: [W; _]) -> (V::Mask, V::Mask) where V: CompensatedFloatVector
150        {
151            // Use integer mask logic on k. We can check the low 2 bits of k.
152            let k_int: W::SignedBits = values[0].cast();
153
154            // Construct bitmasks
155            let bit0 = (k_int & NumericVector::ONE).cmp_ne(NumericVector::ZERO); // true if k % 2 != 0 (quadrants 1, 3)
156            let bit1 = (k_int & NumericVector::TWO).cmp_ne(NumericVector::ZERO); // true if k % 4 >= 2 (quadrants 2, 3)
157
158            (bit0.cast(), bit1.cast())
159        }) {
160            // Reconstruction using bitwise quadrant logic
161            // Determine the final sin/cos based on the quadrant k.
162            // The quadrant mapping for sin(x) / cos(x) where x = k * pi/2 + r:
163            // k % 4 == 0:  sin ->  s,   cos ->  c
164            // k % 4 == 1:  sin ->  c,   cos -> -s
165            // k % 4 == 2:  sin -> -s,   cos -> -c
166            // k % 4 == 3:  sin -> -c,   cos ->  s
167
168            // Swap sin/cos if k is odd (quadrants 1, 3), and normalize the results
169            let mut final_sin = bit0.select(cos, sin).normalize();
170            let mut final_cos = bit0.select(sin, cos).normalize(); // Note: sign is handled next
171
172            // Sign logic:
173            // Sin sign: positive in 0, 1. Negative in 2, 3. -> invert if bit1 is true.
174            // Cos sign: positive in 0, 3. Negative in 1, 2. -> invert if (bit0 ^ bit1) is true.
175            let neg_sin = bit1;
176            let neg_cos = bit0 ^ bit1;
177
178            final_sin.value = final_sin.value.neg_c(neg_sin);
179            final_sin.error = final_sin.error.neg_c(neg_sin);
180
181            final_cos.value = final_cos.value.neg_c(neg_cos);
182            final_cos.error = final_cos.error.neg_c(neg_cos);
183
184            return (final_sin, final_cos); // skip the fallback implementation
185        }
186
187        // Reconstruction (Pure Float)
188        //
189        // We calculate coefficients S_k and C_k based on k mod 4 using only float math.
190        // k_rem = k - 4 * round(k / 4). Range is {-2, -1, 0, 1, 2}.
191        //
192        // Mapping:
193        // k_rem  |  C_k (cos k*pi/2)  |  S_k (sin k*pi/2)
194        // ------------------------------------------------
195        //   0    |    1               |    0
196        //   1    |    0               |    1
197        //   2    |   -1               |    0
198        //  -1    |    0               |   -1
199        //  -2    |   -1               |    0
200        //
201        // Formulas:
202        // C_k = 1 - |k_rem|
203        // S_k = k_rem * (2 - |k_rem|)
204
205        let k_div4 = (k * V::splat(FloatElement::from_ratio(1, 4))).round();
206        let k_rem = k_div4.nmul_adde(V::splat(<V::Element as FloatElement>::ConstInt::<4>::VALUE), k);
207        let k_rem_abs = k_rem.abs();
208
209        let c_k = V::ONE - k_rem_abs;
210        let s_k = k_rem * (V::TWO - k_rem_abs);
211
212        // Apply rotation:
213        // sin(out) = sin(r)*C_k + cos(r)*S_k
214        // cos(out) = cos(r)*C_k - sin(r)*S_k
215
216        // Efficient mixing without full compensated addition (since terms are disjoint/zero)
217        let final_sin = Compensated {
218            value: cos.value.mul_adde(s_k, sin.value * c_k),
219            error: cos.error.mul_adde(s_k, sin.error * c_k),
220        };
221
222        let final_cos = Compensated {
223            value: sin.value.nmul_adde(s_k, cos.value * c_k),
224            error: sin.error.nmul_adde(s_k, cos.error * c_k),
225        };
226
227        (final_sin, final_cos)
228    }
229
230    #[inline(always)]
231    fn sinc<P: Policy>(self) -> Self {
232        // Use non-compensated 4th root epsilon for tiny check, since
233        // the Taylor series is actually very good for very small x.
234        let is_tiny = self.value().abs().cmp_lt(FloatConsts::FOURTH_ROOT_EPSILON);
235
236        let x2 = self.square();
237
238        // if branching, use Taylor series for tiny x without calling sine.
239        if !P::POLICY.avoid_branching && is_tiny.all() {
240            let res = x2 / V::splat(<V::Element as FloatElement>::ConstInt::<120>::VALUE);
241            return x2.mul_add(res - Self::FRAC_1_6, Self::ONE);
242        }
243
244        // For very small x, sinc(x) ~ 1 - x^2/6 + x^4/120
245        let num = is_tiny.select(x2, self.sin_p::<P>());
246        let den = is_tiny.select(
247            Self::splat_value(<V::Element as FloatElement>::ConstInt::<120>::VALUE),
248            self,
249        );
250
251        // combined division, since division is expensive
252        let mut y = num / den;
253
254        y = is_tiny.select(x2.mul_add(y - Self::FRAC_1_6, Self::ONE), y);
255
256        if P::POLICY.check_overflow {
257            y = self.value().is_infinite().select(Self::ZERO, y);
258        }
259
260        y
261    }
262
263    /// `atanh(x)/x`, the cardinal form of `atanh`: same structure as `sinc` below, with the
264    /// even series `1 + x^2/3 + x^4/5`. Domain `[-1, 1]`, where both ends are `+inf`.
265    #[inline(always)]
266    fn atanhc<P: Policy>(self) -> Self {
267        let is_tiny = self.value().abs().cmp_lt(FloatConsts::FOURTH_ROOT_EPSILON);
268
269        let x2 = self.square();
270
271        if !P::POLICY.avoid_branching && is_tiny.all() {
272            let res = x2 / V::splat(<V::Element as FloatElement>::ConstInt::<5>::VALUE);
273            return x2.mul_add(res + Self::FRAC_1_3, Self::ONE);
274        }
275
276        let num = is_tiny.select(x2, self.atanh_p::<P>());
277        let den = is_tiny.select(
278            Self::splat_value(<V::Element as FloatElement>::ConstInt::<5>::VALUE),
279            self,
280        );
281
282        let mut y = num / den;
283
284        y = is_tiny.select(x2.mul_add(y + Self::FRAC_1_3, Self::ONE), y);
285
286        y
287    }
288
289    /// `sinh(x)/x`, the hyperbolic twin of `sinc` above and structurally identical to it:
290    /// the series `1 + x^2/6 + x^4/120` adds where `sinc` subtracts, and the limit at
291    /// infinity is `+inf` rather than zero.
292    #[inline(always)]
293    fn sinhc<P: Policy>(self) -> Self {
294        let is_tiny = self.value().abs().cmp_lt(FloatConsts::FOURTH_ROOT_EPSILON);
295
296        let x2 = self.square();
297
298        if !P::POLICY.avoid_branching && is_tiny.all() {
299            let res = x2 / V::splat(<V::Element as FloatElement>::ConstInt::<120>::VALUE);
300            return x2.mul_add(res + Self::FRAC_1_6, Self::ONE);
301        }
302
303        let num = is_tiny.select(x2, self.sinh_p::<P>());
304        let den = is_tiny.select(
305            Self::splat_value(<V::Element as FloatElement>::ConstInt::<120>::VALUE),
306            self,
307        );
308
309        let mut y = num / den;
310
311        y = is_tiny.select(x2.mul_add(y + Self::FRAC_1_6, Self::ONE), y);
312
313        if P::POLICY.check_overflow {
314            // sinh(inf)/inf is NaN; the limit is +inf from both sides.
315            y = self.value().is_infinite().select(Self::INFINITY, y);
316        }
317
318        y
319    }
320
321    #[inline(always)]
322    fn sinh_cosh<P: Policy>(self) -> (Self, Self) {
323        let abs_x = self.abs();
324        let ex = abs_x.exp_p::<P>();
325
326        let hex_inv = Self::HALF / ex;
327        let exh = Self::HALF * ex;
328
329        // sinh = (e^x - e^-x) / 2, sinh is an odd function, so sinh(x) == -sinh(-x)
330        // cosh = (e^x + e^-x) / 2, cosh is an even function, so cosh(x) == cosh(|x|)
331
332        ((exh - hex_inv).mul_sign(self), exh + hex_inv)
333    }
334
335    #[inline(always)]
336    fn sinh<P: Policy>(self) -> Self {
337        // (e^x - e^-x) / 2
338        let abs_x = self.abs();
339        let ex = abs_x.exp_p::<P>();
340
341        ex.mul_sube(Self::HALF, Self::HALF / ex).mul_sign(self)
342    }
343
344    #[inline(always)]
345    fn cosh<P: Policy>(self) -> Self {
346        // (e^x + e^-x) / 2
347        // cosh is an even function, so cosh(x) == cosh(|x|)
348        let abs_x = self.abs();
349        let ex = abs_x.exp_p::<P>();
350
351        ex.mul_adde(Self::HALF, Self::HALF / ex)
352    }
353
354    #[inline(always)]
355    fn tanh<P: Policy>(self) -> Self {
356        // (e^2x - 1) / (e^2x + 1)
357        let e2x_m1 = (self + self).exp_m1_p::<P>();
358        e2x_m1 / (e2x_m1 + Self::TWO)
359    }
360
361    #[inline(always)]
362    fn asin<P: Policy>(self) -> Self {
363        // asin(x) = atan(x / sqrt(1 - x^2))
364
365        let omx2 = if const { P::POLICY.precision.lt(PrecisionPolicy::Average) } {
366            -self.square() + V::ONE // less accurate but faster
367        } else {
368            // (1-x)*(1+x) is generally more accurate than 1-x^2 near 1
369            (Self::ONE - self) * (self + V::ONE)
370        };
371
372        (self / omx2.sqrt()).atan_p::<P>()
373    }
374
375    #[inline(always)]
376    fn acos<P: Policy>(self) -> Self {
377        // acos(x) = pi/2 - asin(x)
378        Self::FRAC_PI_2 - self.asin_p::<P>()
379    }
380
381    #[inline(always)]
382    fn atan<P: Policy>(self) -> Self {
383        let x = self;
384        let abs_x = x.abs();
385
386        // Constants
387        // tan(pi/8) = sqrt(2) - 1
388        let tan_pi_8 = Self::SQRT_2 - Self::ONE;
389
390        // 1. Argument Reduction
391        // Goal: reduce x to [0, tan(pi/8)] approx [0, 0.414]
392
393        // Check if x > 1
394        let gt_1 = abs_x.value().cmp_gt(V::ONE);
395
396        // if x > 1: x = 1/x
397        // We will compute pi/2 - atan(1/x) later
398        let mut curr = gt_1.select(abs_x.approx_reciprocal_p::<P>(), abs_x);
399
400        // Check if x > tan(pi/8)
401        let gt_tan_pi8 = curr.value().cmp_gt(tan_pi_8.value());
402
403        // if x > tan(pi/8): x = (x-1)/(x+1)
404        // We will add pi/4 later
405        let shifted = (curr - Self::ONE) / (curr + Self::ONE);
406
407        curr = gt_tan_pi8.select(shifted, curr);
408
409        // 2. Series Evaluation
410        // z - z^3/3 + z^5/5 ...
411
412        let z = curr;
413        let z2 = -z.square(); // negative for alternating series subtraction
414
415        let mut sum = z;
416        let mut term = z;
417
418        let shift = if P::POLICY.unroll_loops { 2 } else { 0 };
419
420        let mut i = 1;
421        let max_i = (P::POLICY.max_iterations >> shift) + 1;
422
423        // atan is very slow to converge
424        while i < max_i {
425            let next_i = i + (1 << shift);
426            let prev = sum;
427
428            for k in i..next_i {
429                let div = (2 * k) + 1; // 3, 5, 7...
430
431                term *= z2;
432
433                // NOTE: Doesn't need explicit normalization later, due to sum being used
434                sum.accumulate_unnormalized(term / V::splat(FloatElement::from_int(div as i64)));
435            }
436
437            if prev.cmp_eq(sum).all() {
438                // println!("atan converged at i={}", next_i - 1);
439                break;
440            }
441
442            i = next_i;
443        }
444
445        // If we did the tan(pi/8) shift, add pi/4
446        // sum = sum + pi/4
447        sum = gt_tan_pi8.select(sum + Self::FRAC_PI_4, sum);
448
449        // If we did the >1 inversion, subtract from pi/2
450        // sum = pi/2 - sum
451        sum = gt_1.select(Self::FRAC_PI_2 - sum, sum);
452
453        // Restore Sign
454        let xv = x.value();
455        sum.value = sum.value.mul_sign(xv);
456        sum.error = sum.error.mul_sign(xv);
457
458        sum
459    }
460
461    #[inline(always)]
462    fn asinh<P: Policy>(self) -> Self {
463        // asinh(x) = ln(x + sqrt(x^2 + 1)), rearranged so the argument never approaches 1.
464        //
465        // Written directly, small x sends `x + sqrt(x^2 + 1)` to 1 + x + O(x^2). A
466        // double-double holds that to 106 bits *relative to 1*, so the part that carries
467        // the answer keeps only 106 - log2(1/x) of them. At x = 1e-14 the result was good
468        // to ~65 bits, not 106.
469        //
470        // With s = sqrt(1 + x^2), the offset from 1 is available in closed form:
471        //   x + s - 1 = x + (s^2 - 1)/(s + 1) = x + x^2/(1 + s)
472        // so feeding that to ln_1p keeps the small quantity small the whole way.
473
474        let x_abs = self.abs();
475        let s = (x_abs.square() + V::ONE).sqrt();
476        let offset = x_abs.square() / (Self::ONE + s) + x_abs;
477
478        // negate result if input was negative
479        offset.ln_1p_p::<P>().neg_c(self.value().is_negative())
480    }
481
482    #[inline(always)]
483    fn acosh<P: Policy>(self) -> Self {
484        // ln(x + sqrt(x^2 - 1))
485        // defined for x >= 1
486        (self + (self.square() - V::ONE).sqrt()).ln_p::<P>()
487    }
488
489    #[inline(always)]
490    fn atanh<P: Policy>(self) -> Self {
491        // atanh(x) = 0.5 * ln((1+x)/(1-x)), through ln_1p for the same reason as `asinh`:
492        // the ratio tends to 1 as x -> 0, and a double-double near 1 knows the part that
493        // matters to only 106 - log2(1/x) bits.
494        //
495        //   (1 + x)/(1 - x) = 1 + 2x/(1 - x)
496        //
497        // so the offset from 1 is exact and small, and ln_1p costs the same as ln.
498        let two_x = self + self;
499
500        (two_x / (Self::ONE - self)).ln_1p_p::<P>() * Self::HALF
501    }
502
503    #[inline(always)]
504    fn exp<P: Policy>(self) -> Self {
505        Self::exp_internal::<P, EXP_MODE_EXP>(self)
506    }
507
508    #[inline(always)]
509    fn exph<P: Policy>(self) -> Self {
510        Self::exp_internal::<P, EXP_MODE_EXPH>(self)
511    }
512
513    #[inline(always)]
514    fn exp2<P: Policy>(self) -> Self {
515        Self::exp_internal::<P, EXP_MODE_POW2>(self)
516    }
517
518    #[inline(always)]
519    fn exp10<P: Policy>(self) -> Self {
520        Self::exp_internal::<P, EXP_MODE_POW10>(self)
521    }
522
523    #[inline(always)]
524    fn exp_m1<P: Policy>(self) -> Self {
525        Self::exp_internal::<P, EXP_MODE_EXPM1>(self)
526    }
527
528    #[inline(always)]
529    fn exp2_m1<P: Policy>(self) -> Self {
530        Self::exp_internal::<P, EXP_MODE_POW2M1>(self)
531    }
532
533    #[inline(always)]
534    fn exp10_m1<P: Policy>(self) -> Self {
535        Self::exp_internal::<P, EXP_MODE_POW10M1>(self)
536    }
537
538    #[inline(always)]
539    fn powf<P: Policy>(self, e: Self) -> Self {
540        // pow(x, y) = exp(y * ln(x))
541        (e * self.ln_p::<P>()).exp_p::<P>()
542    }
543
544    #[inline(always)]
545    fn cbrt<P: Policy>(self) -> Self {
546        let s = self.value().cbrt_p::<P>();
547
548        if const { P::POLICY.precision.le(PrecisionPolicy::Worst) } {
549            return Self::new(s);
550        }
551
552        // Compute s^3 using scalar 'two_prod'
553        // This avoids full Compensated * Compensated overhead.
554        // s^2 = p2 + e2
555        let (p2, e2) = <V as ScalarValue>::square(s);
556        // s^3 = p3 + e3_base (approx)
557        let (p3, e3_base) = V::two_prod(p2, s);
558
559        // Complete the error term for s^3: e3 = s*e2 + e3_base
560        let e3 = s.mul_adde(e2, e3_base);
561
562        // Compute High-Precision Residual: r = x - s^3
563        //
564        // `value - p3` annihilates (`p3` is `s^3`), so the residual IS the result. Strict,
565        // same reason as `sqrt`.
566        let (diff_hi, _) = V::two_diff(self.value, p3);
567        let (diff_lo, _) = V::two_diff(self.error, e3);
568
569        // Collapse to scalar (valid because diff is tiny, approx 10^-16)
570        let (r, _) = V::two_sum(diff_hi, diff_lo);
571
572        // Halley Correction Term: s * (r / (2*s^3 + x))
573
574        // Denom: 2*s^3 + x
575        // We use p3 for s^3 (high part is sufficient for the denominator slope),
576        // and avoid a register by just adding p3 to itself.
577        let den = p3 + p3 + self.value;
578
579        // Correction = s * (r / den)
580        // We assume 'den' is safe (if self > 0).
581        // If self == 0, den == 0, results in NaN (similar to your Newton code).
582        let correction = s * (r / den);
583
584        Self::renormalized(s, correction)
585    }
586
587    #[inline(always)]
588    fn ln<P: Policy>(self) -> Self {
589        // Initial Guess using base instruction
590        let y_approx = Self::new(self.value().ln_p::<P>());
591
592        if const { P::POLICY.precision.le(PrecisionPolicy::Worst) } {
593            return y_approx;
594        }
595
596        // Compute exp(y) with our high-precision implementation
597        let e_y = y_approx.exp_p::<P>();
598
599        // Halley's Iteration (One pass is sufficient for Double-Double)
600        // Correction = 2 * (self - e_y) / (self + e_y)
601
602        let diff = self - e_y;
603        let sum = self + e_y;
604
605        let correction = diff / sum;
606
607        let result = y_approx + (correction + correction);
608
609        // Handle Special Cases (Zero, Negative) if strictly required
610        if const { P::POLICY.check_overflow } {
611            // ln(0) -> -inf
612            self.is_zero().select(Self::NEG_INFINITY, result)
613        } else {
614            result
615        }
616    }
617
618    #[inline(always)]
619    fn ln_1p<P: Policy>(self) -> Self {
620        let y_approx = Self::new(self.value().ln_1p_p::<P>());
621
622        if const { P::POLICY.precision.le(PrecisionPolicy::Worst) } {
623            return y_approx;
624        }
625
626        // We are solving: expm1(y) - x = 0
627        //
628        // Newton: y - f/f'
629        // f(y) = e^y - 1 - x
630        // f'(y) = e^y = (e^y - 1) + 1
631        //
632        // Halley: y - 2 * f * f' / (2(f')^2 - f * f'')
633        // Since f'(y) = f''(y) = e^y, this simplifies nicely:
634        //
635        // Correction = 2 * (expm1(y) - x) / (expm1(y) + x + 2)
636
637        let z = y_approx.exp_m1_p::<P>();
638
639        let n = z - self;
640        let d = z + self + Self::TWO;
641
642        let correction = n / d;
643
644        // Note: We subtract the correction because of the sign of the numerator (z - x).
645        // Standard form is y - (f/...), here num is f, so we subtract.
646        y_approx - (correction + correction)
647    }
648
649    #[inline(always)]
650    fn log2<P: Policy>(self) -> Self {
651        self.ln_p::<P>() * Self::LOG2_E
652    }
653
654    #[inline(always)]
655    fn log10<P: Policy>(self) -> Self {
656        self.ln_p::<P>() * Self::LOG10_E
657    }
658
659    #[inline(always)]
660    fn log_n_n<P: Policy, const N: usize>(self) -> Self {
661        match N {
662            0 => Self::ZERO,     // log(x)/log(0) = log(x)/-infinity = 0
663            1 => Self::INFINITY, // log(x)/log(1) = log(x)/0 = complex infinity, only return real part
664            2 => self.log2_p::<P>(),
665            10 => self.log10_p::<P>(),
666            n if n <= 32 => {
667                // Use precomputed 1/ln(n) table for small integer bases
668                self.ln_p::<P>() * <V as crate::consts::CompensatedLogTable<V>>::LOG_TABLE[n - 3]
669            }
670            _ => self.ln_p::<P>() / V::splat(FloatElement::from_int(N as i64)).ln_p::<P>(),
671        }
672    }
673
674    #[inline(always)]
675    fn log_n<P: Policy>(self, n: u32) -> Self {
676        match n {
677            0 => Self::ZERO,
678            1 => Self::INFINITY,
679            2 => self.log2_p::<P>(),
680            10 => self.log10_p::<P>(),
681            n if n <= 32 => self.ln_p::<P>() * <V as crate::consts::CompensatedLogTable<V>>::LOG_TABLE[n as usize - 3],
682            _ => self.ln_p::<P>() / V::splat(FloatElement::from_int(n as i64)).ln_p::<P>(),
683        }
684    }
685
686    /// The `_ext` form exists so a caller who already has `ln(x)` can hand it to the
687    /// low-precision approximation instead of paying for it twice. The compensated
688    /// path never takes that approximation (it evaluates `ln(1 - e^-x)` exactly), so
689    /// there is nothing to reuse and the hint is dropped, the same way the f64 kernel
690    /// (`math/specialized/pd.rs`) and `Complex` do.
691    #[inline(always)]
692    fn ln1m_expnx_ext<P: Policy>(self, _lnx: Self) -> Self {
693        self.ln1m_expnx_p::<P>()
694    }
695}
696
697#[rustfmt::skip]
698impl<V: CompensatedFloatVector> SpecializedSpatialMath<Compensated<V::Element>> for Compensated<V> where V: RealMathWithPolicy {
699    #[inline(always)] fn l2_norm_squared<P: Policy>(self) -> Self { self.square() }
700    #[inline(always)] fn l2_norm<P: Policy>(self) -> Self { self.abs() }
701    #[inline(always)] fn l1_norm<P: Policy>(self) -> Self { self.abs() }
702}
703
704impl<V: CompensatedFloatVector> SpecializedRealMath<Compensated<V::Element>> for Compensated<V>
705where
706    V: RealMathWithPolicy,
707{
708    #[inline(always)]
709    fn atan2<P: Policy>(self, x: Self) -> Self {
710        // y = self
711        let y = self;
712        let x_value = x.value();
713
714        // Handle x = 0
715        let x_is_zero = x_value.is_zero();
716
717        // If x=0, y>0 -> pi/2, y<0 -> -pi/2
718        // We can cheat: atan2(y, 0) is roughly atan(Inf * sign(y))
719        // But doing it explicitly is cleaner.
720
721        let pi_2 = Self::FRAC_PI_2;
722        let y_is_neg = y.value().cmp_lt(V::ZERO);
723        let on_axis_res = pi_2.neg_c(y_is_neg);
724
725        // Standard case
726        let z = y / x;
727        let mut res = z.atan_p::<P>();
728
729        // Adjust quadrant based on x and y
730        // if x < 0:
731        //   if y >= 0: res += pi
732        //   if y < 0:  res -= pi
733
734        let x_is_neg = x_value.cmp_lt(V::ZERO);
735        let offset = Self::PI.neg_c(y_is_neg);
736
737        res = x_is_neg.select(res + offset, res);
738
739        x_is_zero.select(on_axis_res, res)
740    }
741}
742
743#[derive(Clone, Copy, PartialEq, Eq)]
744#[repr(u8)]
745enum ExpMode {
746    Exp = 0, // exp(x)
747    Expm1,   // exp(x) - 1
748    Exph,    // exp(x) / 2
749    Pow2,    // 2^x
750    Pow2m1,  // 2^x - 1
751    Pow10,   // 10^x
752    Pow10m1, // 10^x - 1
753}
754
755const EXP_MODE_EXP: u8 = ExpMode::Exp as u8;
756const EXP_MODE_EXPM1: u8 = ExpMode::Expm1 as u8;
757const EXP_MODE_EXPH: u8 = ExpMode::Exph as u8;
758const EXP_MODE_POW2: u8 = ExpMode::Pow2 as u8;
759const EXP_MODE_POW2M1: u8 = ExpMode::Pow2m1 as u8;
760const EXP_MODE_POW10: u8 = ExpMode::Pow10 as u8;
761const EXP_MODE_POW10M1: u8 = ExpMode::Pow10m1 as u8;
762
763// impl<V: FloatVectorWithBits> Compensated<V> {
764//     #[inline(always)]
765//     pub(crate) fn ldexp_p<P: Policy>(self, exp: V::SignedBits) -> Self {
766//         let value = self.value.ldexp_p::<P>(exp);
767//         let error = self.error.ldexp_p::<P>(exp);
768//         Self { value, error }
769//     }
770
771//     #[inline(always)]
772//     pub(crate) fn frexp_p<P: Policy>(self) -> (Self, V::SignedBits) {
773//         let (value, exp) = self.value.frexp_p::<P>();
774//         let result = Self {
775//             value,
776//             error: self.error.ldexp_p::<P>(-exp), // scale error accordingly
777//         };
778//         (result, exp)
779//     }
780// }
781
782const MAX_PRECISION_HI_ONLY: PrecisionPolicy = PrecisionPolicy::Average;
783
784impl<V: CompensatedFloatVector> Compensated<V> {
785    #[inline(always)]
786    fn exp_core<P: Policy, const MODE: u8>(x: Self) -> Self {
787        let mut sum = Self::ZERO;
788        let mut term = Self::ONE;
789
790        let mut i = 1;
791        let shift = if P::POLICY.unroll_loops { 2 } else { 0 };
792        let max_i = (P::POLICY.max_iterations >> shift) + i;
793
794        // Taylor series expansion for expm1:
795        while i < max_i {
796            let next_i = i + (1 << shift);
797
798            let prev_sum = sum;
799
800            for k in i..next_i {
801                let n = V::splat(FloatElement::from_int(k as i64));
802
803                if const { P::POLICY.precision.gt(MAX_PRECISION_HI_ONLY) } {
804                    term *= x / n;
805                } else {
806                    // using only r_hi for term calculation
807                    term *= Self::from_fraction(x.value, n);
808                }
809
810                // accumulate_unnormalized is safe here because 'term' decreases in magnitude rapidly
811                sum.accumulate_unnormalized(term);
812            }
813
814            if prev_sum.cmp_eq(sum).all() {
815                // println!("exp converged at i={}", next_i - 1);
816                break;
817            }
818
819            i = next_i;
820        }
821
822        // Note that the with_bits version will
823        // need to do this with expm1, just not here.
824        if const { EXP_MODE_EXPM1 != MODE && EXP_MODE_POW2M1 != MODE && EXP_MODE_POW10M1 != MODE } {
825            sum += V::ONE;
826        }
827
828        if const { P::POLICY.precision.le(MAX_PRECISION_HI_ONLY) } {
829            // Linear correction for low part
830            // sum += exp(r_hi) * r_lo
831            if const { EXP_MODE_EXPM1 == MODE || EXP_MODE_POW2M1 == MODE || EXP_MODE_POW10M1 == MODE } {
832                sum = (sum + V::ONE).mul_adde(x.error, sum);
833            } else {
834                sum = sum.mul_adde(x.error, sum);
835            }
836        }
837
838        sum
839    }
840
841    #[inline(always)]
842    fn exp_internal<P: Policy, const MODE: u8>(x: Self) -> Self {
843        struct ExpKernelWithBits<V: CompensatedFloatVector, P: Policy, const MODE: u8>(
844            core::marker::PhantomData<(V, P)>,
845        );
846
847        // V _might_ have the ability to do bit-manipulation, but we don't know that. This is
848        // a way to access a type `W` that has the same bits as `V`, but implements
849        // the necessary traits for bit-level operations. If that's the case, we can use better
850        // range-reduction techniques and opt for ldexp instead of repeated squaring.
851        impl<V: CompensatedFloatVector, P: Policy, const MODE: u8> AsFloatVectorWithBitsKernel<V, 2>
852            for ExpKernelWithBits<V, P, MODE>
853        {
854            type Output = Compensated<V>;
855
856            #[inline(always)]
857            fn with_bits<
858                W: FloatVectorWithBits<
859                        Element = V::Element,
860                        Lanes = V::Lanes,
861                        Mask = V::Mask,
862                        Signed = V::Signed,
863                        Unsigned = V::Unsigned,
864                        ExtendedPrecision = <V as FloatVector>::ExtendedPrecision,
865                    > + CastVector<V>,
866            >(
867                self,
868                c: [W; 2],
869            ) -> Self::Output {
870                let mut x = Compensated::<V> {
871                    value: c[0].cast_into(),
872                    error: c[1].cast_into(),
873                };
874
875                let mut overflows: V::Mask = GenericMask::FALSY;
876                let mut underflows: V::Mask = GenericMask::FALSY;
877
878                if const { P::POLICY.check_overflow } {
879                    let max_exp = <W::Element as FloatElementWithBits>::from_signed(
880                        <W::Element as FloatElementWithBits>::EXP_BIAS,
881                    ) + Element::ONE;
882
883                    let overflow_boundary = match MODE {
884                        EXP_MODE_POW2 | EXP_MODE_POW2M1 => max_exp,
885                        EXP_MODE_POW10 | EXP_MODE_POW10M1 => max_exp * FloatConsts::LOG10_2,
886                        EXP_MODE_EXP | EXP_MODE_EXPM1 | EXP_MODE_EXPH => max_exp * FloatConsts::LN_2,
887                        _ => Element::ZERO, // unreachable
888                    };
889
890                    overflows = x.value.cmp_gt(V::splat(overflow_boundary));
891                    underflows = x.value.cmp_lt(V::splat(-overflow_boundary)); // underflow to 0
892
893                    // zero the lanes to avoid the loop producing NaNs/Infs/subnormals
894                    x = x.nz(overflows | underflows);
895                }
896
897                let k;
898                let mut r;
899
900                // range reduction
901                if const { EXP_MODE_POW2 == MODE || EXP_MODE_POW2M1 == MODE } {
902                    // Base 2: 2^x, k = round(x), r = (x - k) * ln(2)
903                    k = x.value().round();
904                    r = (x - k) * Compensated::LN_2;
905                } else {
906                    if const { EXP_MODE_POW10 == MODE || EXP_MODE_POW10M1 == MODE } {
907                        // Base 10: 10^x = e^(x * ln10), k = round(x * log2(10)), r = x * ln(10) - k * ln(2)
908                        k = (x.value() * <V as FloatConsts>::LOG2_10).round();
909
910                        r = x * Compensated::LN_10;
911                    } else {
912                        // Base e: exp(x), expm1(x), exph(x), k = round(x * log2(e)), r = x - k * ln(2)
913                        k = (x.value() * <V as FloatConsts>::LOG2_E).round();
914
915                        r = x;
916                    }
917
918                    // Standard 3-part Payne-Hanek style reduction
919                    r -= k * V::LN_2_EXTENDED[0];
920                    r -= k * V::LN_2_EXTENDED[1];
921                    r -= k * V::LN_2_EXTENDED[2];
922                }
923
924                // we only need k as an integer for ldexp
925                let mut k = k.cast::<W>().cast::<W::SignedBits>();
926
927                // exp_core returns either exp or expm1 of the reduced argument
928                let mut y = Compensated::<V>::exp_core::<P, MODE>(r);
929
930                let y0 = y; // save for exm1 adjustment
931
932                if const { EXP_MODE_EXPH == MODE } {
933                    // to divide res by 2, we can just subtract 1 from the exponent
934                    k -= NumericVector::ONE;
935                } else if const { EXP_MODE_EXPM1 == MODE || EXP_MODE_POW2M1 == MODE || EXP_MODE_POW10M1 == MODE } {
936                    // expm1/exp2m1/exp10m1 needs an adjustment of +1 before scaling
937                    y += V::ONE;
938                }
939
940                // 2^k * exp(r), go through W WithBits type for ldexp
941                // don't bother with overflow checks in ldexp, we've already done that
942                y.value = W::cast_from(y.value).ldexp_p::<CheckOverflow<P, false>>(k).cast_into();
943                // The low word gets `Preserve`. The overflow pre-check above bounds the
944                // *value*, which is what justifies scaling it with the exponent clamp
945                // turned off, but it says nothing about the low word, which sits ~53
946                // binades below and leaves the representable range first. An unclamped
947                // `ldexp` writes a negative biased exponent straight into the exponent
948                // field: exp(-700) came back with a value of 9.86e-305 and a low word of
949                // -2.74e+295, which then poisoned everything refining through `exp`
950                // (`ln(1e-300)` was off by exactly 2.0, `ln(1e300)` was NaN, because
951                // Halley's `(x - e_y)/(x + e_y)` collapses to -1 on a garbage `e_y`).
952                //
953                // `PreserveDenormals` takes `ldexp`'s two-multiply path, which lets IEEE
954                // gradual underflow produce the subnormal instead of wrapping, so the
955                // correction survives rather than merely not being poison.
956                y.error = W::cast_from(y.error)
957                    .ldexp_p::<PreserveDenormals<CheckOverflow<P, false>>>(k)
958                    .cast_into();
959
960                // Backstop: a correction can never be as large as the value it corrects.
961                // Cannot fire on a well-formed result, and costs one compare.
962                y.error = y.error.nz(y.error.abs().cmp_ge(y.value.abs()));
963
964                if const { EXP_MODE_EXPM1 == MODE || EXP_MODE_POW2M1 == MODE || EXP_MODE_POW10M1 == MODE } {
965                    // small input values get the raw unscaled result
966                    y = k.is_zero().select(y0, y - V::ONE);
967                }
968
969                if const { P::POLICY.check_overflow } {
970                    // zero lane on underflow, set to infinity on overflow
971                    y = overflows.select(Compensated::INFINITY, y.nz(underflows));
972                }
973
974                y
975            }
976        }
977
978        // This will always be zero-cost, but will only succeed if V
979        // supports the necessary bit-level operations, returning None otherwise.
980        if let Some(res) = V::with_bits(
981            [x.value, x.error],
982            ExpKernelWithBits::<V, P, MODE>(core::marker::PhantomData),
983        ) {
984            return res; // skip the fallback implementation
985        }
986
987        // approximate scaling factor based on element size
988        // f32 = 8, f64 = 12
989        let n = size_of::<V::Element>() + 4;
990
991        // crude range-reduction, assumes x is not larger than 2^N,
992        // which is reasonable for exp inputs.
993        let scale = V::splat(FloatElement::from_int(1 << n));
994        let mut r = x / scale;
995
996        let overflows = r.value.cmp_gt(V::ONE);
997        let underflows = r.value.cmp_lt(V::NEG_ONE);
998
999        r = r.nz(overflows | underflows); // zero lane on overflow/underflow
1000
1001        if const { EXP_MODE_POW2 == MODE || EXP_MODE_POW2M1 == MODE } {
1002            r *= Self::LN_2;
1003        } else if const { EXP_MODE_POW10 == MODE || EXP_MODE_POW10M1 == MODE } {
1004            r *= Self::LN_10;
1005        }
1006
1007        let mut y = Self::exp_core::<P, MODE>(r);
1008
1009        for _ in 0..n {
1010            let y_sq = y.square();
1011
1012            y = if const { EXP_MODE_EXPM1 == MODE || EXP_MODE_POW2M1 == MODE || EXP_MODE_POW10M1 == MODE } {
1013                // correction for expm1 squaring
1014                y_sq + Compensated {
1015                    value: y.value + y.value, // 2x should be lossless
1016                    error: y.error + y.error, // using addition for performance
1017                }
1018            } else {
1019                y_sq
1020            };
1021        }
1022
1023        if const { EXP_MODE_EXPH == MODE } {
1024            y *= Self::HALF;
1025        }
1026
1027        // zero lane on underflow, set to infinity on overflow
1028        y = overflows.select(Self::INFINITY, y.nz(underflows));
1029
1030        y
1031    }
1032}
Last built: 2026-09-08 21:35:55 UTC