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}