thermite_compensated/special.rs
1use super::{Compensated, CompensatedFloatVector};
2
3use thermite::math::policy::PrecisionPolicy;
4use thermite::math::{RealMathWithPolicy, TranscendentalMathWithPolicy};
5use thermite::prelude::*;
6
7use thermite_special::specialized::{SpecializedRealPrimalMath, SpecializedRealSpecialMath, SpecializedSpecialMath};
8use thermite_special::{RealSpecialMathWithPolicy, SpecialMathWithPolicy};
9
10use crate::specialized::special::SpecializedCompensatedSpecialMath;
11
12// The elliptic kernels (`carlson`, `ellint`) are generic over any float vector whose element
13// carries the Carlson convergence threshold `(3 eps)^(1/8)`: the deviation at which the
14// 7th-order Taylor tail (`~ deviation^8`) drops below rounding. A double-double has its own
15// `eps` (`2^-104` / `2^-46`), so the threshold is recomputed for it rather than lifted from
16// the inner element: with the f64 value the tail would stop at 6.8e-16 and the whole
17// second word would be noise. About three more duplication steps per call buys the rest.
18impl thermite_special::elliptic::EllipticConsts for Compensated<f64> {
19 /// `(3 * 2^-104)^(1/8)`
20 const CARLSON_THRESH: Self = Compensated { value: 0.00014003939092283656, error: 0.0 };
21 /// `2^-14`: the `R_C` series tail `t^8/17` is then `1e-34`. Below this the `ln` arm of
22 /// the closed form loses about `eps/s` with `s ~ sqrt(t) = 0.008`, a few units of 1e-30,
23 /// which is the accuracy floor of `R_J` on this type.
24 const RC_SERIES_THRESH: Self = Compensated { value: 6.103515625e-5, error: 0.0 };
25}
26
27impl thermite_special::elliptic::EllipticConsts for Compensated<f32> {
28 /// `(3 * 2^-46)^(1/8)`
29 const CARLSON_THRESH: Self = Compensated { value: 0.021316588, error: 0.0 };
30 /// `1/128` still: the tail `1.5e-17` is below this type's `2^-46`.
31 const RC_SERIES_THRESH: Self = Compensated { value: 0.0078125, error: 0.0 };
32}
33
34// Compensated is a single-value real, so it belongs in the "primal" tier and gains the
35// value-and-derivative (`_d`) activation forms (via the trait defaults).
36impl<V: CompensatedFloatVector> SpecializedRealPrimalMath<Compensated<V::Element>> for Compensated<V>
37where
38 V: SpecialMathWithPolicy + RealSpecialMathWithPolicy + RealMathWithPolicy,
39 V: SpecializedCompensatedSpecialMath<V::Element>,
40{
41 #[inline(always)]
42 fn langevin_d<P: Policy>(self) -> (Self, Self) {
43 <V as SpecializedCompensatedSpecialMath<V::Element>>::compensated_langevin_d::<P, false>(self)
44 }
45}
46
47impl<V: CompensatedFloatVector> Compensated<V>
48where
49 V: RealMathWithPolicy,
50{
51 #[inline(always)]
52 fn erf_internal_p<P: Policy>(self) -> (Self, Self) {
53 let x = self;
54 let abs_x = x.abs();
55
56 // Series below the split, continued fraction at or above it.
57 //
58 // The series computes *erf* and erfc comes out of it as 1 - erf: the cancellation
59 // in that subtraction is what sets erfc's accuracy, and it grows with erf.
60 // erf(2) = 0.9953 costs ~8 bits, erf(3) = 0.99998 costs ~16. The continued
61 // fraction computes erfc directly with no cancellation, but its Lentz iteration
62 // count climbs as |x| falls: measured 338 double-double steps at 1.5, 202 at 2.0,
63 // 101 at 3.0, and those steps are real (the value is not final until step 316 at
64 // 1.5, so this is not a stopping-test artifact). Lowering the split buys accuracy
65 // with time, and there is no value that gets both.
66 //
67 // So the split is BOTH per type and policy-gated:
68 //
69 // - `Best` and above take `ERF_CF_SPLIT`, which is 1.5 for f64 double-double and
70 // 2 for f32 double-single. The two widths genuinely disagree: at f64 width the
71 // continued fraction holds a flat ~2-7e-31 relative from 1.5 up, against the
72 // series' 6.9e-31 at 1.5 and 7.2e-30 at 1.821. At f32 width the continued
73 // fraction floor is ~1.4-2.4e-12 (400-700 ulp) and the series beats it at every
74 // point from 1.25 to 2.25.
75 // - Below `Best`, both widths use 2. The accuracy is the historical accuracy and
76 // nothing pays the continued fraction's iteration count uninvited.
77 //
78 // This matters beyond erf: `erfinv` refines against erfc, and needs it most
79 // exactly where it was weakest, since large x corresponds to y near 1.
80 // erfinv(0.99) lands on x = 1.821, and at f64/`Best` it improves 13.7x, from
81 // 1.16e-30 to 8.5e-32, while costing ~27,500 ns against ~4,800.
82 let split = if const { P::POLICY.precision.ge(PrecisionPolicy::Best) } {
83 V::ERF_CF_SPLIT
84 } else {
85 V::splat(<V::Element as FloatElement>::ConstInt::<2>::VALUE)
86 };
87
88 let use_series: V::Mask = abs_x.value().cmp_lt(split);
89
90 let use_only_series = use_series.all();
91 let use_only_cf = use_series.none();
92
93 // --- Init Series (erf) ---
94 // erf(x) = 2/sqrt(pi) * (x - x^3/3 + x^5/10 ...)
95 let x2 = -x.square(); // -x^2 for alternating series
96 let mut sum_s = abs_x;
97 let mut term_s = abs_x;
98
99 // --- Init Continued Fraction (erfc) ---
100 // Lentz's method vars
101 //
102 // `tiny` is the stand-in for a denominator that came out non-positive, so it only
103 // has to be negligible against any real term - but it also gets *reciprocated* on
104 // the very first step, and that is what constrains it here.
105 //
106 // `MIN_POSITIVE` cannot be used: 1/2.2e-308 is 4.5e307, and compensated
107 // multiplication splits its operands with Dekker's 2^27+1 factor, which overflows
108 // to infinity for anything past ~1.3e300. The next `f *= c * d` then produced NaN,
109 // which is why erf and erfc returned NaN for every |x| >= 3 - the entire
110 // continued-fraction tail, the only regime that reaches this code.
111 //
112 // sqrt(MIN_POSITIVE)/EPSILON leaves both the sentinel and its reciprocal far
113 // inside the splitter's range while staying utterly negligible as a floor.
114 let tiny = Self::new(V::MIN_POSITIVE.sqrt() / <V as FloatVector>::EPSILON);
115 let mut f = tiny;
116 let mut a = Self::ONE; // a_1 = 1
117 let mut c = tiny;
118 let mut d = Self::ZERO;
119 let b = abs_x; // `b` in Lentz's method is |x|
120
121 let shift = if P::POLICY.unroll_loops { 2 } else { 0 };
122 let mut i = 1;
123 let max_i = (P::POLICY.max_iterations >> shift) + 1;
124
125 // the inner loops are expensive, so avoid unnecessary work
126
127 #[rustfmt::skip]
128 let () = match (use_only_series, use_only_cf) {
129 (true, true) => {
130 // both true -> all values are NaN or Inf
131 },
132 (true, false) => while i < max_i {
133 let next_i = i + (1 << shift);
134 let prev_s = sum_s;
135
136 for k in i..next_i {
137 // --- Series Update ---
138 // term *= -x^2 * (2k-1) / (k * (2k+1))
139 let k_f = k as i64;
140 let k2_p1 = (2 * k + 1) as i64;
141 let k2_m1 = (2 * k - 1) as i64;
142
143 let num = FloatElement::from_int(k2_m1);
144 let den = FloatElement::from_int(k_f * k2_p1);
145
146 term_s *= x2 * Self::from_fraction(V::splat(num), V::splat(den));
147
148 sum_s.accumulate_unnormalized(term_s);
149 }
150
151 if prev_s.cmp_eq(sum_s).all() {
152 // println!("erf converged at i={}", next_i - 1);
153 break;
154 }
155
156 i = next_i;
157 },
158 (false, true) => while i < max_i {
159 let next_i = i + (1 << shift);
160 let prev_f = f;
161
162 for k in i..next_i {
163 // --- CF Update ---
164 // Lentz coefficients: a_k = (k-1)/2
165 if k > 1 {
166 a = Self::splat(FloatElement::from_int((k - 1) as i64)) * Self::HALF;
167 }
168
169 // Lentz steps: D = b + a*D, C = b + a/C
170 d = a.mul_adde(d, b); // D = b + a*D
171
172 c = b + a / c.max(tiny); // if C<=0 -> tiny
173 d = d.max(tiny).approx_reciprocal_p::<P>(); // if D<=0 -> tiny
174
175 f *= c * d;
176 }
177
178 if prev_f.cmp_eq(f).all() {
179 // println!("erfc converged at i={}", next_i - 1);
180 break;
181 }
182
183 i = next_i;
184 },
185 (false, false) => while i < max_i {
186 let next_i = i + (1 << shift);
187 let prev_s = sum_s;
188 let prev_f = f;
189
190 for k in i..next_i {
191 // --- Series Update ---
192 // term *= -x^2 * (2k-1) / (k * (2k+1))
193 let k_f = k as i64;
194 let k2_p1 = (2 * k + 1) as i64;
195 let k2_m1 = (2 * k - 1) as i64;
196
197 let num = FloatElement::from_int(k2_m1);
198 let den = FloatElement::from_int(k_f * k2_p1);
199
200 term_s *= x2 * Self::from_fraction(V::splat(num), V::splat(den));
201
202 let mut new_sum_s = sum_s;
203 new_sum_s.accumulate_unnormalized(term_s);
204
205 sum_s = use_series.select(new_sum_s, sum_s); // but avoid overflowing the sum
206
207 // --- CF Update ---
208 // Lentz coefficients: a_k = (k-1)/2
209 if k > 1 {
210 a = Self::splat(FloatElement::from_int((k - 1) as i64)) * Self::HALF;
211 }
212
213 // Lentz steps: D = b + a*D, C = b + a/C
214 d = a.mul_adde(d, b); // D = b + a*D
215
216 c = b + a / c.max(tiny); // if C<=0 -> tiny
217 d = d.max(tiny).approx_reciprocal_p::<P>(); // if D<=0 -> tiny
218
219 f = use_series.select(f, f * c * d);
220 }
221
222 let series_converged = use_only_cf || prev_s.cmp_eq(sum_s).all();
223 let cf_converged = use_only_series || prev_f.cmp_eq(f).all();
224
225 if series_converged && cf_converged {
226 // println!("erf converged at i={}", next_i - 1);
227 break;
228 }
229
230 i = next_i;
231 },
232 };
233
234 // --- Finalize ---
235
236 // 1. Result from Series, also normalizes the compensated sum
237 let res_erf_s = sum_s * Self::FRAC_2_SQRT_PI;
238
239 // 2. Result from CF (if used)
240 // erfc = e^(-x^2)/sqrt(pi) * f
241 let res_erfc_c = if use_only_series {
242 Self::ZERO // avoid doing exp if not needed
243 } else {
244 f * (-x.square()).exp_p::<P>() * Self::FRAC_1_SQRT_PI
245 };
246
247 // 3. Select based on Method
248 // If series used: erf = res_erf_s, erfc = 1 - res_erf_s
249 // If CF used: erf = 1 - res_erfc_c, erfc = res_erfc_c
250
251 let erf_val = use_series.select(res_erf_s, Self::ONE - res_erfc_c);
252 let erfc_val = use_series.select(Self::ONE - res_erf_s, res_erfc_c);
253
254 // 4. Symmetry for x < 0
255 // erf(-x) = -erf(x)
256 // erfc(-x) = 2 - erfc(x)
257
258 let is_neg = x.value().is_negative();
259
260 let final_erf = erf_val.neg_c(is_neg); // is_neg.select(-erf_val, erf_val);
261 let final_erfc = is_neg.select(Self::TWO - erfc_val, erfc_val);
262
263 (final_erf, final_erfc)
264 }
265}
266
267impl<V: CompensatedFloatVector> SpecializedRealSpecialMath<Compensated<V::Element>> for Compensated<V>
268where
269 V: SpecialMathWithPolicy + RealSpecialMathWithPolicy + RealMathWithPolicy,
270 V: SpecializedCompensatedSpecialMath<V::Element>,
271{
272 #[inline(always)]
273 fn erfinv<P: Policy>(self) -> Self {
274 // High-performance erfinv using Halley's Method seeded by Winitzki's approximation.
275 // This converges in ~3 iterations for 106-bit precision.
276 // However, for very small |y|, we can do better with the Maclaurin series expansion,
277 // despite more iterations, since it avoids expensive calls to log/exp/sqrt functions.
278
279 let y = self;
280 let abs_y = y.abs();
281 let y_value = y.value();
282 let abs_y_value = abs_y.value();
283
284 if abs_y_value.cmp_le(V::MAX_ERFINV_SERIES).all() {
285 // For small |y|, use the Maclaurin series expansion for better performance,
286 // since it doesn't need to call log/exp/sqrt/etc. functions.
287
288 // Maclaurin series for erf_inv(y):
289 // erf_inv(y) = sum_{k=0 to inf} (c_k / (2k+1)) * (sqrt(pi)/2 * y)^(2k+1)
290 // where c_0 = 1, c_k = sum_{m=0 to k-1} (c_m * c_{k-1-m}) / ((m+1)(2m+1))
291
292 let w = abs_y * Self::FRAC_SQRT_PI_2; // Variable w = (sqrt(pi)/2) * |y|
293 let w2 = w.square();
294
295 let mut sum = w; // Initial term (k=0): c_0 = 1, term = w
296 let mut w_pow = w; // Stores w^(2k+1)
297
298 const MAX_COEFFS: usize = 64;
299
300 // Scalar Coefficient history buffer
301 // We need this to compute the convolution for the next c_k.
302 // 64 terms is generally sufficient for convergence where defined,
303 // though it gets slow near |y| ~ 1.
304 let mut coeffs: [Compensated<V::Element>; MAX_COEFFS] = [Element::ZERO; MAX_COEFFS];
305
306 coeffs[0] = Element::ONE; // c_0 = 1
307
308 let max_k = P::POLICY.max_iterations.min(MAX_COEFFS - 1);
309
310 for k in 1..max_k {
311 let prev = sum;
312
313 let mut c_k: Compensated<V::Element> = Element::ZERO;
314
315 for m in 0..k {
316 // Term: (c_m * c_{k-1-m}) / ((m+1)(2m+1))
317 let num = coeffs[m] * coeffs[k - 1 - m];
318
319 let m_i = m as i64;
320 let den_i = (m_i + 1) * (2 * m_i + 1);
321
322 c_k.accumulate_unnormalized(num / <V::Element as FloatElement>::from_int(den_i));
323 }
324
325 coeffs[k] = c_k.normalize();
326
327 // Term = (c_k / (2k+1)) * w^(2k+1)
328 w_pow *= w2; // Next odd power of w
329
330 let k_term_den = (2 * k + 1) as i64;
331
332 sum.accumulate_unnormalized(Self::splat(c_k) * w_pow / V::splat(FloatElement::from_int(k_term_den)));
333
334 // Check for convergence
335 if prev.cmp_eq(sum).all() {
336 // Restore sign: erf_inv(-y) = -erf_inv(y)
337 sum.value = sum.value.mul_sign(y_value);
338 sum.error = sum.error.mul_sign(y_value);
339
340 return sum.normalize();
341 }
342 }
343
344 // The MAX_ERFINV_SERIES cutoff should guarantee convergence,
345 // but if we reach here, we fallback to Halley's method.
346 // This should be very rare, if not impossible.
347 }
348
349 // Detect singularities
350 let is_zero = abs_y_value.cmp_eq(V::ZERO);
351 let is_one = abs_y_value.cmp_eq(V::ONE);
352
353 // 1. Initial guess: the inner vector's own `erfinv`. Measured at y = 0.99 it
354 // carries a relative 1.9e-8, about 27 bits, not the element's full 53, since
355 // the inner kernel is tuned as an approximation rather than as a seed.
356 //
357 // This used to be Winitzki's approximation, good to a relative 3.5e-4, about
358 // 11 bits. Halley is cubic, so 11 bits needs three passes to clear 106 and 27
359 // needs two, and every pass costs a compensated `erf` *and* a compensated
360 // `exp` to form f and f'. Seeding from the cheaper, far better starting point
361 // trades a scalar `erfinv` for one of each.
362 let mut x = Self::new(abs_y_value.erfinv_p::<P>());
363
364 // 2. Halley's Method Iterations (Cubic Convergence)
365 // x_{n+1} = x_n - u / (1 + x_n * u) where u = f(x_n) / f'(x_n)
366 let skip = is_zero | is_one; // cannot be solved as roots
367
368 // Halley is cubic and the seed above already carries ~27 bits, so 27 -> 81 -> 243
369 // clears double-double (106 bits for f64, 48 for f32) in two passes, and three holds
370 // even under a policy whose scalar `erfinv` seed is only the ~11-bit Winitzki
371 // value.
372 //
373 // The cap is a cost guard rather than an accuracy knob. The equality test below
374 // cannot fire when the last bit of the correction oscillates, and that measured as
375 // the full 10,000-iteration policy budget (~12 ms against ~2 us, a 2000x cliff)
376 // on roughly 40% of arguments in [0.05, 1), every one of which had already reached
377 // its final value within three passes.
378 const MAX_HALLEY: usize = 3;
379
380 for _ in 0..P::POLICY.max_iterations.min(MAX_HALLEY) {
381 let prev_x = x;
382 // f = erf(x) - y, routed through erf = 1 - erfc so that near y = 1 the two
383 // quantities being subtracted are both *small* rather than both near 1.
384 //
385 // Measured, this changes nothing: erfinv(0.9999) sits at 3.43e-27 either way,
386 // bit for bit. The cancellation it avoids is not the one that limits this -
387 // f -> 0 at the root by definition, so some cancellation is unavoidable. Kept
388 // because it is the better-conditioned spelling and costs nothing (the kernel
389 // computes erf and erfc together).
390 //
391 // The y -> 1 shortfall was erfc's, not this subtraction's: the residual here
392 // tracks erfc's own relative error times x / (2 * erfc(x) * exp(x^2) / sqrt(pi)),
393 // which at x = 1.821 is about 0.13. erfc measured 7.2e-30 there and this
394 // measured 1.16e-30. Moving `erf_internal_p`'s regime split to 1.5 fixed both.
395 let f = (Self::ONE - abs_y) - x.erfc_p::<P>();
396
397 // f / f'(x) = f * (sqrt(pi)/2) * exp(x^2)
398 let u = f * (Self::FRAC_SQRT_PI_2 * x.square().exp_p::<P>());
399
400 // Halley step: u / (1 + x*u)
401 // Note: f''/f' = -2x, so the Halley term simplifies to this.
402 x.reduce_unnormalized(u / x.mul_adde(u, V::ONE));
403
404 if (skip | x.cmp_eq(prev_x)).all() {
405 break;
406 }
407 }
408
409 x = is_zero.select(Self::ZERO, is_one.select(Self::INFINITY, x));
410
411 // Restore sign: erf_inv(-y) = -erf_inv(y)
412 x.value = x.value.mul_sign(y_value);
413 x.error = x.error.mul_sign(y_value);
414
415 x.normalize()
416 }
417
418 // Exact identity: probit(p) = sqrt(2) * erfinv(2p - 1), with every step in
419 // compensated arithmetic (2p - 1 is an error-free transform here, and erfinv
420 // refines to full double-double precision via Halley's method).
421 #[inline(always)]
422 fn probit<P: Policy>(self) -> Self {
423 Self::erfinv::<P>(self + self - Self::ONE) * Self::SQRT_2
424 }
425
426 #[inline(always)]
427 fn lgamma_r<P: Policy>(self) -> (Self, Self) {
428 <V as SpecializedCompensatedSpecialMath<V::Element>>::compensated_lgamma_r::<P>(self)
429 }
430
431 #[inline(always)]
432 fn langevin<P: Policy>(self) -> Self {
433 <V as SpecializedCompensatedSpecialMath<V::Element>>::compensated_langevin_d::<P, false>(self).0
434 }
435
436 #[inline(always)]
437 fn langevin_1m<P: Policy>(self) -> Self {
438 <V as SpecializedCompensatedSpecialMath<V::Element>>::compensated_langevin_d::<P, true>(self).0
439 }
440
441 #[inline(always)]
442 fn inv_langevin<P: Policy>(self) -> Self {
443 let y = self.abs();
444 // Exact in compensated arithmetic, which is why the complement entry point is
445 // only a different seed here rather than a different algorithm.
446 let t = Self::ONE - y;
447 let y_val = y.value();
448 Self::inv_langevin_refine::<P>(
449 Self::new(y_val.inv_langevin_p::<P>()),
450 self,
451 y,
452 t,
453 y_val.cmp_ge(V::ONE),
454 y_val.cmp_gt(V::ONE) | y_val.is_nan(),
455 )
456 }
457
458 #[inline(always)]
459 fn inv_langevin_1m<P: Policy>(self) -> Self {
460 let y_in = Self::ONE - self;
461 let y = y_in.abs();
462 let t = y_in.value().is_negative().select(Self::ONE - y, self);
463 // The pole and the domain edge read off `t` itself: `1 - t` is exactly 1 in the
464 // leading limb for any tiny `t`, and that is a perfectly good input here.
465 let t_val = self.value();
466 Self::inv_langevin_refine::<P>(
467 Self::new(t_val.inv_langevin_1m_p::<P>()),
468 y_in,
469 y,
470 t,
471 t_val.cmp_eq(V::ZERO) | (y_in.value().is_negative() & y.value().cmp_ge(V::ONE)),
472 t_val.cmp_lt(V::ZERO) | (y_in.value().is_negative() & y.value().cmp_gt(V::ONE)) | t_val.is_nan(),
473 )
474 }
475}
476
477impl<V: CompensatedFloatVector> Compensated<V>
478where
479 V: SpecialMathWithPolicy + RealSpecialMathWithPolicy + RealMathWithPolicy,
480 V: SpecializedCompensatedSpecialMath<V::Element>,
481{
482 /// Newton in compensated arithmetic from a seed accurate to the inner width's u.
483 /// The error squares per step, so double-double needs one and double-single two.
484 /// `y_in` carries the sign, `y = |y_in|`, `t = 1 - y`, and the two masks are the
485 /// pole (`+inf`) and the domain edge (NaN under overflow checking).
486 #[inline(always)]
487 fn inv_langevin_refine<P: Policy>(
488 mut x: Self,
489 y_in: Self,
490 y: Self,
491 t: Self,
492 at_pole: V::Mask,
493 out_of_domain: V::Mask,
494 ) -> Self {
495 let mut i = 0;
496 while i < <V as SpecializedCompensatedSpecialMath<V::Element>>::INV_LANGEVIN_STEPS {
497 x = <V as SpecializedCompensatedSpecialMath<V::Element>>::compensated_inv_langevin_newton::<P>(x, y, t);
498 i += 1;
499 }
500
501 // The pole (a Newton step there is 0/0) and the domain edge.
502 x = at_pole.select(Self::INFINITY, x);
503 if const { P::POLICY.check_overflow } {
504 x = out_of_domain.select(Self::NAN, x);
505 }
506
507 x.copysign(y_in)
508 }
509}
510
511// The gamma family is blanket-implemented over `SpecializedCompensatedSpecialMath`, the
512// same way `SpecialMath` is blanket-implemented over *this* trait. Adding a gamma method
513// therefore touches only the lower rung; see `crate::specialized` for why that rung has
514// to exist at all (the two Compensated widths need different coefficients).
515impl<V: CompensatedFloatVector> SpecializedSpecialMath<Compensated<V::Element>> for Compensated<V>
516where
517 V: SpecialMathWithPolicy + RealSpecialMathWithPolicy + RealMathWithPolicy,
518 V: SpecializedCompensatedSpecialMath<V::Element>,
519{
520 type ExpIntDetails = Self;
521
522 #[inline(always)]
523 fn erf<P: Policy>(self) -> Self {
524 Self::erf_internal_p::<P>(self).0
525 }
526
527 #[inline(always)]
528 fn erfc<P: Policy>(self) -> Self {
529 Self::erf_internal_p::<P>(self).1
530 }
531
532 #[inline(always)]
533 fn tgamma<P: Policy>(self) -> Self {
534 <V as SpecializedCompensatedSpecialMath<V::Element>>::compensated_tgamma::<P>(self)
535 }
536
537 #[inline(always)]
538 fn beta<P: Policy>(a: Self, b: Self) -> Self {
539 <V as SpecializedCompensatedSpecialMath<V::Element>>::compensated_beta::<P>(a, b)
540 }
541
542 fn lambert_w<P: Policy>(self) -> (Self, Self) {
543 // Seed from the standard-precision lambert_w on the value field,
544 // then refine each branch with a single compensated Halley iteration.
545 //
546 // Halley's iteration for w*e^w = x:
547 // ew = exp(w), f = w*ew - x, wp1 = w + 1
548 // d = 2*wp1^2*ew - (w+2)*f
549 // w' = w - 2*wp1*f / d
550
551 let x = self;
552 let (w0_seed, wm1_seed) = x.value.lambert_w_p::<P>();
553
554 let mut w0 = Self::new(w0_seed);
555 let mut wm1 = Self::new(wm1_seed);
556
557 // One compensated Halley step per branch
558 #[inline(always)]
559 fn halley_refine<P: Policy, W>(w: Compensated<W>, x: Compensated<W>) -> Compensated<W>
560 where
561 W: CompensatedFloatVector + RealMathWithPolicy,
562 {
563 let ew = w.exp_p::<P>();
564 let f = w.mul_sube(ew, x);
565 let wp1 = w + W::ONE;
566 let wp2 = wp1 + wp1;
567 let d = (wp1 + W::ONE).nmul_adde(f, wp2 * wp1 * ew);
568 wp2.nmul_adde(f / d, w)
569 }
570
571 w0 = halley_refine::<P, V>(w0, x);
572 wm1 = halley_refine::<P, V>(wm1, x);
573
574 // Edge cases
575 let x_val = x.value();
576 let at_branch = x_val.cmp_eq(FloatConsts::FRAC_NEG_1_E);
577 let at_zero = x_val.is_zero();
578
579 w0 = at_branch.select(Self::NEG_ONE, w0);
580 w0 = at_zero.select(Self::ZERO, w0);
581 wm1 = at_branch.select(Self::NEG_ONE, wm1);
582 wm1 = at_zero.select(Self::new(V::NEG_INFINITY), wm1);
583
584 if const { P::POLICY.check_overflow } {
585 let in_domain = x_val.cmp_ge(FloatConsts::FRAC_NEG_1_E);
586
587 w0 = in_domain.select(w0, Self::NAN);
588 w0 = x_val.cmp_eq(V::INFINITY).select(Self::INFINITY, w0);
589
590 wm1 = in_domain.select(wm1, Self::NAN);
591 wm1 = x_val.cmp_gt(V::ZERO).select(Self::NAN, wm1);
592 }
593
594 (w0, wm1)
595 }
596
597 fn lgamma<P: Policy>(self) -> Self {
598 Self::lgamma_r::<P>(self).0
599 }
600
601 #[inline(always)]
602 fn digamma<P: Policy>(self) -> Self {
603 <V as SpecializedCompensatedSpecialMath<V::Element>>::compensated_digamma::<P>(self)
604 }
605
606 #[inline(always)]
607 fn trigamma<P: Policy>(self) -> Self {
608 <V as SpecializedCompensatedSpecialMath<V::Element>>::compensated_trigamma::<P>(self)
609 }
610
611 /// `n = 0` and `n = 1` reach the tuned double-double digamma/trigamma. **`n >= 2`
612 /// returns NaN**: no double-double algorithm exists for the higher orders yet, and
613 /// silently routing through an f64-precision path would put 53 good bits in a
614 /// 106-bit container, the same reason `Compensated` refuses the shared Lanczos
615 /// tables. NaN over quiet precision loss, like the real kernel's unimplemented
616 /// regions.
617 #[inline(always)]
618 fn polygamma<P: Policy>(self, n: u32) -> Self {
619 match n {
620 0 => SpecializedSpecialMath::digamma::<P>(self),
621 1 => SpecializedSpecialMath::trigamma::<P>(self),
622 // Unimplemented, not undefined: there is no double-double algorithm for the
623 // higher orders here yet. `n` is a scalar, so this is a whole-call `todo!`
624 // rather than a NaN that would propagate silently.
625 _ => todo!("Compensated polygamma(n >= 2) has no double-double algorithm yet"),
626 }
627 }
628
629 // TEMP(bessel_j): disabled until orders beyond J_0 exist - see thermite-special/src/lib.rs.
630 //fn bessel_j<P: Policy, const N: usize>(self) -> Self {
631 // todo!()
632 //}
633}
634
635/// Double-double is still real arithmetic, so the regime and domain rules apply
636/// unchanged - but the Lentz sentinel does not.
637impl<V: CompensatedFloatVector> thermite_special::specialized::ExpIntDetails<Compensated<V::Element>, Compensated<V>>
638 for Compensated<V>
639where
640 Compensated<V>: thermite::vector::FloatVector<Element = Compensated<V::Element>>,
641{
642 /// The default, `MIN_POSITIVE`, is reciprocated on the first Lentz step, and
643 /// 1/2.2e-308 = 4.5e307 is past the ~1.3e300 where compensated multiplication's
644 /// Dekker 2^27+1 splitter overflows to infinity - so every continued-fraction lane
645 /// came back NaN. `expint` takes the fraction for x >= 1, which is exactly where it
646 /// failed.
647 ///
648 /// Same defect and same fix as `Complex`, and as the `erf`/`erfc` tail in this crate:
649 /// a sentinel only has to be negligible as a *floor*, but this one also has to
650 /// survive being inverted.
651 #[inline(always)]
652 fn cf_tiny() -> Compensated<V> {
653 Compensated::new(V::MIN_POSITIVE.sqrt() / <V as FloatVector>::EPSILON)
654 }
655}