thermite_complex/math/special/mod.rs
1#![allow(clippy::needless_arbitrary_self_type)]
2
3//! Special functions for [`Complex`] (`special` feature).
4//!
5//! Implements `thermite_special`'s [`SpecializedSpecialMath`], giving complex
6//! vectors the [`SpecialMath`](thermite_special::SpecialMath) API.
7//!
8//! `thermite-special` splits its families along the line this crate needs:
9//! `Special` is documented as valid for real and complex vectors alike, while
10//! `RealSpecial` (`erfinv`, `probit`, `gelu`, `swish`, `algebraic_sigmoid`,
11//! `lgamma_r`, ...) and `RealPrimal` (the `_d` forms) are real-only. `Complex`
12//! implements the first and not the others, as it implements
13//! [`CoreMath`](thermite::math::CoreMath) but not
14//! [`RealMath`](thermite::math::RealMath).
15//!
16//! [`erf`](SpecializedSpecialMath::erf) and [`erfc`](SpecializedSpecialMath::erfc)
17//! are implemented over the whole plane. The holomorphic defaults (`hermite`,
18//! `hermitev`, `chebyshev`, `jacobi`, `legendre`, `gaussian`) are complex
19//! polynomial recurrences and are inherited as they are.
20//! [`logistic_sigmoid`](SpecializedSpecialMath::logistic_sigmoid) and
21//! [`softplus`](SpecializedSpecialMath::softplus) must be overridden: their
22//! defaults are stabilized for the real axis with `|x|` and `max(x, 0)`.
23//!
24//! # Element-specific functions
25//!
26//! Anything carrying a coefficient table goes through
27//! [`SpecializedComplexSpecialMath`], which is implemented per element type the way
28//! `thermite-special`'s own `ps.rs`/`pd.rs` are. [`trigamma`](SpecializedSpecialMath::trigamma)
29//! and [`lambert_w`](SpecializedSpecialMath::lambert_w) are implemented there;
30//! [`beta`](SpecializedSpecialMath::beta) is a default written on `tgamma`. So is the
31//! Faddeeva function, whose own module is [`faddeeva`].
32//!
33//! The whole Gamma family is implemented, on the tables `thermite-special` exports
34//! from [`thermite_special::tables`]. Only part of each table survives the crossing:
35//! the Lanczos sums and the digamma `p_large` are analytic approximations that hold
36//! off the real axis, while the digamma `[1, 2]` rational and the trigamma regions
37//! are minimax fits to real intervals and are unusable here - so `digamma` and
38//! `trigamma` lean on a recurrence where the real versions reach for a rational.
39//!
40//! [`expint`](SpecializedSpecialMath::expint) is inherited whole and simply runs in
41//! complex arithmetic; what changes is [`ExpIntDetails`], all three methods of it.
42//! [`use_series`](ExpIntDetails::use_series) picks the regime by `norm_sqr` rather than
43//! the lexicographic `cmp_lt`, and additionally claims the whole left half-plane, where
44//! the Stieltjes continued fraction degrades toward the cut but the series stops
45//! alternating and converges cleanly. [`invalid`](ExpIntDetails::invalid) drops the real
46//! version's `x < 0` hole, since the principal branch covers the cut plane; the cut
47//! itself needs no handling, as all of the multivaluedness is the `-ln z` term and the
48//! principal `ln` already carries it. [`cf_tiny`](ExpIntDetails::cf_tiny) backs the Lentz
49//! sentinel off `MIN_POSITIVE`, which a complex reciprocal squares into zero.
50//!
51//! `E_1` holds machine precision over the cut plane. Higher orders come off the order
52//! recurrence and lose roughly `|z|^(N-1)/(N-1)!`, matching what the real path considers
53//! reliable - but the asymptotic series that path swaps in past its threshold has no
54//! complex counterpart yet, so very large `|z|` at high `N` is not covered.
55//!
56//! # Not implemented
57//!
58//! `bessel_j` is disabled crate-wide until orders beyond `J_0` exist upstream.
59//!
60//! `Complex<Compensated<..>>` gets the element-agnostic functions but `todo!()`s the
61//! table-driven ones; `Complex<Dual<..>>` has all of them, and differentiates through
62//! them, since the shared bodies are generic over [`RealValue`](crate::RealValue).
63
64use thermite::math::policy::{DefaultPolicy, Policy};
65use thermite::math::{CoreMathWithPolicy as _, FloatConsts, TranscendentalMathWithPolicy as _};
66use thermite::prelude::*;
67use thermite_special::specialized::{ExpIntDetails, SpecializedSpecialMath};
68
69use crate::Complex;
70use crate::math::ComplexMathWithPolicy as _;
71use crate::math::specialized::ComplexVector;
72use crate::vector::RealFloatVector;
73use thermite_special::tables::Lanczos;
74
75pub mod faddeeva;
76
77/// Terms of the Taylor series, which needs roughly `2|z|^2` of them.
78///
79/// The loop exits once every lane has converged, after ~20 for typical arguments,
80/// so this cap only costs the large-`|z|` lanes that need it.
81const SERIES_TERMS: usize = 160;
82
83/// `$|z|^2$` past which the series cannot converge within [`SERIES_TERMS`].
84///
85/// The series needs roughly `$2|z|^2$` terms, so 160 of them reach `$|z| \approx 8.9$`.
86const SERIES_RADIUS_SQ: i64 = 64;
87
88/// `$-Re(z^2)$` past which the series alternates badly enough to be abandoned.
89///
90/// Pulling `$e^{-z^2}$` out front removes the alternation **only where `$Re(z^2) > 0$`**.
91/// Near the imaginary axis `$z^2$` is negative real, the ratio `$2z^2/(2n+1)$` is
92/// negative again, and the terms peak around `$e^{|z|^2}$` on their way to a sum of
93/// order `$|z|$` - about `$0.43|z|^2$` digits lost. At `$z = 0.01 + 6i$` that is 15
94/// digits, and the old implementation returned one correct digit there.
95///
96/// 8 keeps the loss under ~3.5 digits. Inside this band the series is the better regime
97/// at every policy and is *structurally* exact in ways `w` is not: for purely imaginary
98/// `z` every term is purely imaginary, so `erf(iy)` has a real part of exactly zero.
99const SERIES_ALTERNATION_LIMIT: i64 = 8;
100
101/// `$Re(z^2) = x^2 - y^2$` past which `erfc` must be computed directly rather than as
102/// `1 - erf`.
103///
104/// `Re(z^2)`, not `|z|`, governs `$e^{-z^2}$`, hence how small `erfc` is, hence how
105/// badly `1 - erf` cancels. By `x = 6` there is nothing left: `erf(6)` has rounded to
106/// exactly 1.0 while `erfc(6)` is 2e-17.
107const DIRECT_ERFC_LIMIT: i64 = 6;
108
109/// `(erf(z), erfc(z))` for `Re z >= 0`. The reflections in the impl below need no more.
110///
111/// Both regimes are evaluated on every lane and blended, the lanes of a vector not
112/// agreeing on which applies.
113///
114/// # Accuracy
115///
116/// ~1 ulp over the whole half-plane. `erfc` comes from [`faddeeva`] via
117/// `$\operatorname{erfc}(z) = e^{-z^2}w(iz)$` for `$|z| \ge 1$` and from the Taylor
118/// series inside that, and the two regimes have no gap between them.
119///
120/// This replaced a continued fraction keyed on `Re(z^2) >= 6`, which left a wedge of
121/// large `|Im z|` where neither regime was valid: the series truncated before converging
122/// and the continued fraction was never selected. Measured against a 50-digit oracle,
123/// `erfc(0.1 + 10i)` was wrong by 36 orders of magnitude and is now good to 1e-13.
124#[inline(always)]
125fn erf_erfc_positive<P: Policy, E, V>(z: Complex<V>) -> (Complex<V>, Complex<V>)
126where
127 V: RealFloatVector<Element = E>,
128 Complex<V>: SpecializedComplexSpecialMath<Complex<E>> + GenericVector<Mask = V::Mask>,
129{
130 let one = Complex::<V>::ONE;
131
132 let z2 = z.square();
133 let exp_nz2 = (-z2).exp_p::<P>(); // e^{-z^2}, common to both regimes
134
135 // --- regime selection ---
136 //
137 // Three independent reasons to take `erfc` from `w` rather than from `1 - erf`:
138 //
139 // 1. `Re(z^2) >= 6`, where `erfc` is so much smaller than `erf` that the
140 // subtraction has no digits left. This was the old continued fraction's job.
141 // 2. `Re(z^2) <= -8`, where the series alternates and cancels.
142 // 3. `|z| > 8`, where the series truncates before it converges.
143 //
144 // Cases 2 and 3 together are the wedge, and neither was covered before: the
145 // continued fraction was selected on `Re(z^2) >= 6`, so everything with large
146 // `|Im z|` fell through to a series that could not deliver it.
147 //
148 // Inside the remaining band the series wins at every policy and is kept - `w` at
149 // `Average` is 4e-10 where the series is at the last ulp. Widening this trades
150 // accuracy in the bulk for nothing.
151 let cancels = z2
152 .re
153 .cmp_ge(thermite::const_splat!(int <V::Element>: DIRECT_ERFC_LIMIT));
154 let alternates = z2
155 .re
156 .cmp_le(thermite::const_splat!(int <V::Element>: -SERIES_ALTERNATION_LIMIT));
157 let beyond_series = z
158 .norm_sqr()
159 .cmp_gt(thermite::const_splat!(int <V::Element>: SERIES_RADIUS_SQ));
160
161 let use_w = cancels | alternates | beyond_series;
162
163 let mut series_erf = Complex::<V>::EMPTY;
164 let mut w_erfc = Complex::<V>::EMPTY;
165
166 // --- Taylor series (Abramowitz & Stegun 7.1.6) ---
167 //
168 // erf(z) = (2/sqrt(pi)) e^{-z^2} * sum_{n>=0} t_n,
169 // t_0 = z, t_n = t_{n-1} * 2z^2 / (2n + 1)
170 //
171 // The e^{-z^2} factor absorbs the alternation where `Re(z^2) > 0`; case 2 above is
172 // where it does not, and those lanes are on `w` instead.
173 if const { P::POLICY.avoid_branching } || !use_w.all() {
174 let two_z2 = z2 + z2;
175
176 let mut term = z;
177 let mut sum = z;
178
179 let eps_sqr: V = <V as FloatVector>::EPSILON * <V as FloatVector>::EPSILON;
180
181 let mut n = 1usize;
182 while n < SERIES_TERMS {
183 let denom = V::splat(<V::Element as FloatElement>::from_int(2 * n as thermite::LargeInt + 1));
184
185 // `(term * two_z2) / denom` costs the same roundings, but puts the reciprocal
186 // on the loop-carried chain: `term` then waits a division *and* a multiply
187 // per iteration. Scaling the loop-invariant `two_z2` instead leaves one
188 // complex multiply between successive terms, and the reciprocal issues
189 // alongside it.
190 let ratio = two_z2 / denom;
191
192 term *= ratio;
193 sum += term;
194
195 // |term| <= eps * |sum| in every lane that will actually use this. The
196 // `| use_w` matters: a single large-|z| lane never converges, and without it
197 // one such lane drags the whole vector to SERIES_TERMS for a result that is
198 // then discarded.
199 let converged = term.norm_sqr().cmp_le(sum.norm_sqr() * eps_sqr);
200
201 if (converged | use_w).all() {
202 break;
203 }
204
205 n += 1;
206 }
207
208 series_erf = sum * exp_nz2 * <V as thermite::math::FloatConsts>::FRAC_2_SQRT_PI;
209 }
210
211 // --- Faddeeva, for the erfc side ---
212 //
213 // erfc(z) = e^{-z^2} w(iz)
214 //
215 // `Re z >= 0` here, so `Im(iz) >= 0` and `w` never pays for its lower half-plane
216 // reflection. `e^{-z^2}` is already in hand for the series, so this costs one
217 // `w` - a single reciprocal and an N-term Horner - where the continued fraction it
218 // replaced took 32 complex divisions.
219 if const { P::POLICY.avoid_branching } || use_w.any() {
220 w_erfc = exp_nz2 * SpecializedComplexSpecialMath::faddeeva_w::<P>(Complex::new(-z.im, z.re));
221 }
222
223 let erf = use_w.select(one - w_erfc, series_erf);
224 let erfc = use_w.select(w_erfc, one - series_erf);
225
226 (erf, erfc)
227}
228
229// `E` is named rather than written `V::Element` because the self-referential form
230// sends the trait solver into an overflow; the supertrait pins it regardless.
231impl<E, V: RealFloatVector<Element = E>> SpecializedSpecialMath<Complex<E>> for Complex<V>
232where
233 Complex<V>: SpecializedComplexSpecialMath<Complex<E>> + GenericVector<Mask = V::Mask>,
234{
235 type ExpIntDetails = Self;
236
237 /// The error function over the whole complex plane.
238 ///
239 /// `erf` is entire and odd. The negative-real half-plane comes from
240 /// `erf(-z) = -erf(z)`, a conditional negation, not a branch.
241 #[inline(always)]
242 fn erf<P: Policy>(self) -> Self {
243 let neg = self.re.is_negative();
244 let z = Complex::new(self.re.neg_c(neg), self.im.neg_c(neg));
245
246 let (erf, _) = erf_erfc_positive::<P, E, V>(z);
247
248 Complex::new(erf.re.neg_c(neg), erf.im.neg_c(neg))
249 }
250
251 /// The complementary error function over the whole complex plane.
252 ///
253 /// The `1 - erf(z)` default cancels for large `Re z`, where `erfc` is the function
254 /// one wants in the first place; the continued fraction computes it directly there.
255 /// The negative half-plane uses `erfc(z) = 2 - erfc(-z)`.
256 #[inline(always)]
257 fn erfc<P: Policy>(self) -> Self {
258 let neg = self.re.is_negative();
259 let z = Complex::new(self.re.neg_c(neg), self.im.neg_c(neg));
260
261 let (_, erfc) = erf_erfc_positive::<P, E, V>(z);
262
263 neg.select(Self::TWO - erfc, erfc)
264 }
265
266 /// `$\sigma(z) = \frac{1}{1 + e^{-z}}$`
267 ///
268 /// The default stabilizes for the real axis by negating on `is_positive()` and
269 /// selecting, neither of which is holomorphic. The plain definition is, and the
270 /// default itself falls back to it at lower precision policies.
271 #[inline(always)]
272 fn logistic_sigmoid<P: Policy>(self) -> Self {
273 (Self::ONE + (-self).exp_p::<P>()).reciprocal_p::<P>()
274 }
275
276 /// `$\frac{1}{k}\ln(1 + e^{kz})$`
277 ///
278 /// The default's `max(x, 0) + ln1p(e^{-|kx|})` is the real-axis overflow-stable
279 /// rearrangement, and neither `|x|` nor `max` is holomorphic. This uses the
280 /// analytic definition, and so overflows for large `Re(kz)` where the real form
281 /// would not.
282 #[inline(always)]
283 fn softplus<P: Policy>(self, k: Self, rcp_k: Self) -> Self {
284 (Self::ONE + (self * k).exp_p::<P>()).ln_p::<P>() * rcp_k
285 }
286
287 // The Gamma family and Lambert W carry element-specific coefficient tables, so
288 // they route through `SpecializedComplexSpecialMath` below rather than living here.
289
290 #[inline(always)]
291 fn tgamma<P: Policy>(self) -> Self {
292 self.complex_tgamma::<P>()
293 }
294
295 #[inline(always)]
296 fn lgamma<P: Policy>(self) -> Self {
297 self.complex_lgamma::<P>()
298 }
299
300 #[inline(always)]
301 fn digamma<P: Policy>(self) -> Self {
302 self.complex_digamma::<P>()
303 }
304
305 #[inline(always)]
306 fn trigamma<P: Policy>(self) -> Self {
307 self.complex_trigamma::<P>()
308 }
309
310 #[inline(always)]
311 fn beta<P: Policy>(a: Self, b: Self) -> Self {
312 a.complex_beta::<P>(b)
313 }
314
315 /// Both `$W_0$` and `$W_{-1}$` are genuine branches of the complex Lambert W, so
316 /// the real signature carries over unchanged - it simply cannot reach `$W_k$` for
317 /// `$|k| \ge 2$`.
318 #[inline(always)]
319 fn lambert_w<P: Policy>(self) -> (Self, Self) {
320 self.complex_lambert_w::<P>()
321 }
322
323 // TEMP(bessel_j): disabled until orders beyond J_0 exist - see thermite-special/src/lib.rs.
324 //#[inline(always)]
325 //fn bessel_j<P: Policy, const N: usize>(self) -> Self {
326 // todo!()
327 //}
328
329}
330
331/// All three of the shared `expint` kernel's decisions change over C. Everything else
332/// about that kernel - the series, the Lentz continued fraction, the order recurrence -
333/// is inherited unchanged and simply runs in complex arithmetic.
334impl<E, V: RealFloatVector<Element = E>> ExpIntDetails<Complex<E>, Complex<V>> for Complex<V>
335where
336 Complex<V>: FloatVector<Element = Complex<E>, Mask = V::Mask>,
337{
338 #[inline(always)]
339 fn use_series(z: Complex<V>) -> V::Mask {
340 // Two regions, for two different reasons.
341 //
342 // The unit disc is the real rule, but by modulus rather than by `cmp_lt` (which
343 // on `Complex` is the lexicographic sort order, and would hand a point like
344 // 0.5 + 100i to the series, where it diverges). Compared as `norm_sqr < 1` to
345 // skip the root - squaring is monotone and the threshold is its own square.
346 //
347 // The whole left half-plane is added because the Stieltjes continued fraction
348 // degrades as arg z approaches the cut - measurably by |Arg z| ~ 177 deg, and
349 // completely on the cut itself. The series has no such trouble there: its terms
350 // are (-z)^k / (k k!), so for Re z < 0 they stop alternating and the sum simply
351 // accumulates, which is the cancellation-free direction. (The reverse of the
352 // real line, where positive x is exactly what makes the series cancel and the
353 // fraction is preferred.)
354 z.norm_sqr().cmp_lt(V::ONE) | z.re.is_negative()
355 }
356
357 /// Nothing but NaN is out of domain.
358 ///
359 /// Real `E_N` is a half-line function and the default NaNs out `x < 0`. `E_N(z)` is
360 /// holomorphic on the whole cut plane `|Arg z| < pi`, so the negative reals are
361 /// in-domain here, approached from above. The cut needs no handling of its own: all
362 /// of the multivaluedness sits in the `-ln z` term of the series, and the principal
363 /// `ln` this crate provides already carries exactly that branch.
364 #[inline(always)]
365 fn invalid(z: Complex<V>) -> V::Mask {
366 z.is_nan()
367 }
368
369 /// The default sentinel, `MIN_POSITIVE`, cannot be used here: a complex reciprocal
370 /// is `conj(z) / |z|^2`, and `MIN_POSITIVE^2` underflows to zero, so the very first
371 /// Lentz step divides by zero and every continued-fraction lane comes back NaN.
372 ///
373 /// `sqrt(MIN_POSITIVE) / EPSILON` is the principled choice: the square root is the
374 /// hard floor for surviving the squaring, and dividing by EPSILON backs off it far
375 /// enough that the reciprocal's square stays inside the exponent range too. Holds
376 /// with room to spare for f32 and f64 alike.
377 #[inline(always)]
378 fn cf_tiny() -> Complex<V> {
379 Complex::real(V::MIN_POSITIVE.sqrt() / <V as FloatVector>::EPSILON)
380 }
381}
382
383// ---------------------------------------------------------------------------
384// Per-element hook
385// ---------------------------------------------------------------------------
386
387/// The complex special functions whose algorithms carry element-specific coefficient
388/// tables.
389///
390/// [`SpecializedSpecialMath`] for `Complex<V>` is one blanket impl that forwards here,
391/// so the f32 and f64 cases can diverge exactly the way `thermite-special`'s own
392/// `ps.rs`/`pd.rs` do. The shared bodies are free functions in this module that take
393/// their coefficients as slices; an impl supplies the table and little else.
394///
395/// `Self` is the *complex* vector, so these are ordinary `self` methods and the
396/// per-element dispatch is in the impl header (`for Complex<V> where V::Element = f64`).
397/// That shape is what lets [`decl_complex_math!`](crate::math::specialized) generate
398/// [`ComplexSpecialMath`] from it, exactly as it generates
399/// [`ComplexMath`](crate::math::ComplexMath) from `SpecializedComplexMath`.
400///
401/// A type only reaches `SpecialMath` over C by implementing this, which is why the
402/// element-agnostic members (`erf`, `logistic_sigmoid`, ...) are not on it: they stay
403/// in the blanket impl and cost an implementor nothing. The Gamma members keep their
404/// `complex_` prefix because `SpecialMath` already exposes `tgamma`/`lgamma`/... on the
405/// same types, and two traits offering one name makes every call ambiguous.
406pub trait SpecializedComplexSpecialMath<E>: ComplexVector<Element = E> {
407 fn complex_tgamma<P: Policy>(self) -> Self;
408 fn complex_lgamma<P: Policy>(self) -> Self;
409 fn complex_digamma<P: Policy>(self) -> Self;
410 fn complex_trigamma<P: Policy>(self) -> Self;
411 fn complex_lambert_w<P: Policy>(self) -> (Self, Self);
412
413 /// The Faddeeva function `$w(z) = e^{-z^2}\operatorname{erfc}(-iz)$`.
414 ///
415 /// Carries the Weideman coefficient table, hence its place here. See
416 /// [`faddeeva`] for the algorithm and the accuracy ladder.
417 fn faddeeva_w<P: Policy>(self) -> Self;
418
419 /// `$\operatorname{erfcx}(z) = e^{z^2}\operatorname{erfc}(z) = w(iz)$`.
420 ///
421 /// The scaled complementary error function: `erfc` without the exponential
422 /// underflow, so it stays meaningful where `erfc` itself has flushed to zero.
423 #[inline(always)]
424 fn erfcx<P: Policy>(self) -> Self {
425 // erfcx(z) = w(iz), and i*(x + iy) = -y + ix
426 Self::from_parts(-self.im(), self.re()).faddeeva_w::<P>()
427 }
428
429 /// The Voigt function `$K(x, y) = \operatorname{Re} w(x + iy)$`, the convolution of
430 /// a Gaussian and a Lorentzian in normalized coordinates, as a real value.
431 ///
432 /// The one consumer that wants the real part *alone*, and so the one that depends on
433 /// the near-real-axis correction. Use `Best` or above; that is where it is enabled.
434 ///
435 /// Normalization is left to the caller: physical line shapes want an additional
436 /// `$1/(\sigma\sqrt{2\pi})$` and a scaling of `x` and `y` by the Doppler width.
437 #[inline(always)]
438 fn voigt<P: Policy>(self) -> Self::Real {
439 self.faddeeva_w::<P>().re()
440 }
441
442 /// `$B(a, b) = \frac{\Gamma(a)\Gamma(b)}{\Gamma(a+b)}$`
443 ///
444 /// Deliberately *not* `exp(lgamma(a) + lgamma(b) - lgamma(a+b))`: over C the
445 /// principal log-gamma branches do not add, so the exponentiated form is correct
446 /// only up to a factor of `$e^{2\pi i k}$`. The quotient of gammas has no branch
447 /// to get wrong, at the cost of overflowing where the log form would not.
448 #[inline(always)]
449 fn complex_beta<P: Policy>(self, b: Self) -> Self {
450 self.complex_tgamma::<P>() * b.complex_tgamma::<P>() / (self + b).complex_tgamma::<P>()
451 }
452}
453
454// ---------------------------------------------------------------------------
455// Shared bodies
456// ---------------------------------------------------------------------------
457
458/// Lift a real coefficient table to complex constants, so the tuned
459/// [`poly`](thermite::math::CoreMathWithPolicy::poly_p) /
460/// [`poly_rev`](thermite::math::CoreMathWithPolicy::poly_rev_p) can be used directly
461/// rather than hand-rolling a Horner loop - those pick up Estrin-style ILP under
462/// policies that allow unrolling, which a sequential Horner cannot.
463///
464/// The table is `const` and everything here is `#[inline(always)]`, so the lift folds
465/// away; what remains at runtime is the zero imaginary part in each coefficient add,
466/// which IEEE forbids folding (`-0.0 + 0.0` is `+0.0`).
467#[inline(always)]
468fn complex_consts<V: RealFloatVector, const N: usize>(c: &[V::Element; N]) -> [Complex<V::Element>; N] {
469 let zero = <V::Element as crate::RealValue>::VAL_ZERO;
470 let mut out = [Complex::new(c[0], zero); N];
471
472 let mut i = 1;
473 while i < N {
474 out[i] = Complex::new(c[i], zero);
475 i += 1;
476 }
477
478 out
479}
480
481/// Shared complex `tgamma`, by the Lanczos approximation.
482///
483/// Lanczos is an analytic approximation, not a minimax fit, so it carries over from
484/// the real implementation unchanged apart from the arithmetic. The left half-plane
485/// comes from the reflection formula `Gamma(z)Gamma(1-z) = pi/sin(pi z)`.
486///
487/// Unlike the real version this does not split the `pow` in two for large arguments,
488/// so it overflows around `Re z ~ 171` (f64) rather than reaching the very top of the
489/// range. It also has no integer fast path: over C that test would only fire on a
490/// measure-zero set.
491#[inline(always)]
492fn tgamma_impl<P: Policy, V: RealFloatVector, const N: usize>(z: Complex<V>, l: &Lanczos<V::Element, N>) -> Complex<V> {
493 let reflect = z.re.cmp_lt(V::HALF);
494 let w = reflect.select(Complex::ONE - z, z);
495
496 let gh = V::splat(l.g) - V::HALF;
497 let zgh = Complex::new(w.re + gh, w.im);
498
499 let lanczos = w.poly_rev_p::<P, N>(&complex_consts::<V, N>(&l.p_rev))
500 / w.poly_rev_p::<P, N>(&complex_consts::<V, N>(&l.q_rev));
501
502 // zgh^(w - 1/2) * e^(-zgh) * lanczos_sum(w), with the two exponentials folded into
503 // one: exp((w - 1/2) ln(zgh) - zgh).
504 //
505 // The real version cannot do this - it calls `powf` and then divides by `exp(zgh)`,
506 // so the `pow` overflows on its own well before the product does, and it has to
507 // split the exponent in half and square the result to compensate. Folding removes
508 // the intermediate entirely: nothing overflows until the answer does. It is also
509 // one transcendental cheaper, `powf` being `exp(e ln x)` underneath.
510 //
511 // `Re zgh >= g > 0` on this branch, so the `ln` never approaches its cut.
512 let e = Complex::new(w.re - V::HALF, w.im);
513 let res = (e * zgh.ln_p::<P>() - zgh).exp_p::<P>() * lanczos;
514
515 // Gamma(z) = pi / (sin(pi z) Gamma(1 - z))
516 let refl = Complex::real(<V as FloatConsts>::PI) / (z.sin_pi_p::<P>() * res);
517
518 reflect.select(refl, res)
519}
520
521/// Shared complex `lgamma`, from the `exp(g)`-scaled Lanczos sum.
522///
523/// # Branch
524///
525/// For `Re z >= 1/2` this is the *continuous* log-gamma, not merely a principal
526/// value: both logarithms it takes are of arguments confined to the right half-plane,
527/// so neither crosses the cut, and the large imaginary parts come from the
528/// `(z - 1/2) ln(zgh)` product rather than from a wrapped logarithm.
529///
530/// The reflected half-plane is another matter - `ln(sin(pi z))` is principal there, so
531/// the result can differ from the continuous branch by a multiple of `2 pi i`.
532#[inline(always)]
533fn lgamma_impl<P: Policy, V: RealFloatVector, const N: usize>(z: Complex<V>, l: &Lanczos<V::Element, N>) -> Complex<V> {
534 let reflect = z.re.cmp_lt(V::HALF);
535 let w = reflect.select(Complex::ONE - z, z);
536
537 let b = Complex::new(w.re - V::HALF, w.im);
538 let a = Complex::new(b.re + V::splat(l.g), b.im).ln_p::<P>() - Complex::ONE;
539
540 let s =
541 w.poly_p::<P, N>(&complex_consts::<V, N>(&l.p_expg_scaled)) / w.poly_p::<P, N>(&complex_consts::<V, N>(&l.q));
542
543 let res = a * b + s.ln_p::<P>();
544
545 // ln Gamma(z) = ln(pi) - ln(sin(pi z)) - ln Gamma(1 - z)
546 let refl = Complex::real(<V as FloatConsts>::LN_PI) - z.sin_pi_p::<P>().ln_p::<P>() - res;
547
548 reflect.select(refl, res)
549}
550
551/// Shared complex `digamma`.
552///
553/// Only `p_large` of the real [`Digamma`] table is usable here: the `[1, 2]` rational
554/// beside it is a minimax fit to a real interval and says nothing off the axis, while
555/// `p_large` is a genuine asymptotic series. So the recurrence does the work the
556/// rational does in the real version, walking `Re z` up to `shift` before expanding.
557#[inline(always)]
558fn digamma_impl<P: Policy, V: RealFloatVector, const NL: usize>(
559 z: Complex<V>,
560 p_large: &[V::Element; NL],
561 shift: V::Element,
562) -> Complex<V> {
563 let reflect = z.re.cmp_lt(V::HALF);
564
565 let mut w = reflect.select(Complex::ONE - z, z);
566 let mut refl = Complex::<V>::ZERO;
567
568 if const { P::POLICY.avoid_branching } || reflect.any() {
569 // psi(z) = psi(1 - z) - pi cot(pi z)
570 let (s, c) = z.sincos_pi_p::<P>();
571 refl = -(c / s * Complex::real(<V as FloatConsts>::PI));
572 }
573
574 // psi(w) = psi(w + 1) - 1/w, walked until the series below applies.
575 let shift = V::splat(shift);
576 let mut acc = Complex::<V>::ZERO;
577 let mut active = w.re.cmp_lt(shift);
578
579 while active.any() {
580 acc = active.select(acc - w.finv_p::<P>(), acc);
581 w = active.select(w + Complex::ONE, w);
582 active = w.re.cmp_lt(shift);
583 }
584
585 // psi(w) ~ ln(w-1) + 1/(2(w-1)) - u P(u), u = 1/(w-1)^2
586 let xm1 = w - Complex::ONE;
587 let u = (xm1 * xm1).finv_p::<P>();
588
589 let psi = xm1.ln_p::<P>() + (xm1 + xm1).finv_p::<P>() - u * u.poly_p::<P, NL>(&complex_consts::<V, NL>(p_large));
590
591 let total = acc + psi;
592
593 reflect.select(refl + total, total)
594}
595
596/// Shared complex trigamma.
597///
598/// Three stages, none of them the real implementation's: that one leans on minimax
599/// rationals fitted to intervals of the real line, which say nothing off the axis.
600/// This is the classical route instead - reflect, recurse, expand.
601///
602/// * `bernoulli` are `$B_2, B_4, \ldots$` in order, the asymptotic series coefficients.
603/// * `shift` is the `Re z` the recurrence walks up to before that series is used.
604/// Both are the per-element tuning knobs: a shorter table wants a larger shift.
605#[inline(always)]
606fn trigamma_impl<P: Policy, V: RealFloatVector, const NB: usize>(
607 z: Complex<V>,
608 bernoulli: &[V::Element; NB],
609 shift: V::Element,
610) -> Complex<V> {
611 // Reflect the left half-plane: psi_1(z) + psi_1(1 - z) = pi^2 / sin^2(pi z).
612 let reflect = z.re.cmp_lt(V::HALF);
613
614 let mut w = reflect.select(Complex::ONE - z, z);
615 let mut refl = Complex::<V>::ZERO;
616
617 if const { P::POLICY.avoid_branching } || reflect.any() {
618 let s = z.sin_pi_p::<P>();
619 refl = Complex::real(<V as FloatConsts>::PI_SQUARED) / (s * s);
620 }
621
622 // psi_1(w) = 1/w^2 + psi_1(w + 1), walked until Re w is large enough for the
623 // series below. Bounded: the reflection already put Re w >= 1/2, so this runs at
624 // most `shift` times.
625 let shift = V::splat(shift);
626 let mut acc = Complex::<V>::ZERO;
627 let mut active = w.re.cmp_lt(shift);
628
629 while active.any() {
630 let t = (w * w).finv_p::<P>();
631 acc = active.select(acc + t, acc);
632 w = active.select(w + Complex::ONE, w);
633 active = w.re.cmp_lt(shift);
634 }
635
636 // psi_1(w) ~ 1/w + 1/(2w^2) + sum_k B_2k / w^(2k+1)
637 let u = w.finv_p::<P>();
638 let u2 = u * u;
639
640 // Horner in u^2, leading term first. The coefficients are real, so each step is
641 // two FMAs on the real part (the coefficient rides the second) and two on the
642 // imaginary - never a complex multiply followed by a separate add.
643 let mut tail = Complex::real(V::splat(bernoulli[NB - 1]));
644 let mut i = NB - 1;
645 while i > 0 {
646 i -= 1;
647
648 let c = V::splat(bernoulli[i]);
649
650 tail = Complex::new(
651 tail.im.nmul_adde(u2.im, tail.re.mul_adde(u2.re, c)),
652 tail.re.mul_adde(u2.im, tail.im * u2.re),
653 );
654 }
655
656 let psi = acc + u + u2 * V::HALF + (u2 * u) * tail;
657
658 reflect.select(refl - psi, psi)
659}
660
661/// Shared complex Lambert W, returning `$(W_0, W_{-1})$`.
662///
663/// Both are ordinary branches of `$W$` over C - unlike the real case, where `$W_{-1}$`
664/// exists only on `$[-1/e, 0)$`. A Halley iteration from a per-region initial guess;
665/// Halley is cubic, so a handful of steps suffices once the guess is in the right
666/// basin, and picking that basin is the whole difficulty.
667///
668/// * `c` is `[11/72, r^2]`: the third Puiseux coefficient, and the squared radius
669/// around `$-1/e$` inside which that series is used.
670/// * `iters` is the Halley count.
671///
672/// Accurate away from the branch cuts. Near the cut on `$(-\infty, -1/e)$` the two
673/// branches exchange values and a guess can land in the wrong basin, so results there
674/// follow whichever branch the iteration converged to, not the principal labelling.
675#[inline(always)]
676fn lambert_w_impl<P: Policy, V: RealFloatVector>(
677 z: Complex<V>,
678 c: &[V::Element; 2],
679 iters: usize,
680) -> (Complex<V>, Complex<V>) {
681 let e = <V as FloatConsts>::E;
682
683 // --- Branch-point series in p = sqrt(2(ez + 1)) ---
684 // The two branches meet at z = -1/e, where p = 0 and both equal -1:
685 // W_0 = -1 + p - p^2/3 + 11 p^3/72
686 // W_{-1} = -1 - p - p^2/3 - 11 p^3/72
687 // The even term keeps its sign, the odd ones flip.
688 let ez1 = z.mul_adde(e, Complex::ONE);
689 let p = (ez1 + ez1).sqrt();
690 let p2 = p.square();
691 let p3 = p2 * p;
692
693 // Both coefficients are real, so these are the componentwise (single-rounding) FMA.
694 let odd = p3.mul_adde(V::splat(c[0]), p);
695 let even = p2.nmul_adde(<V as FloatConsts>::FRAC_1_3, Complex::NEG_ONE);
696
697 let w0_branch = even + odd;
698 let wm1_branch = even - odd;
699
700 // --- Logarithmic guess, W_k(z) ~ L1 - L2 + L2/L1, L1 = ln z + 2*pi*i*k ---
701 let lnz = z.ln_p::<P>();
702 let l1_0 = lnz;
703 let l1_m1 = Complex::new(lnz.re, lnz.im - <V as FloatConsts>::TAU);
704
705 let l2_0 = l1_0.ln_p::<P>();
706 let l2_m1 = l1_m1.ln_p::<P>();
707
708 let w0_log = l1_0 - l2_0 + l2_0 / l1_0;
709 let wm1_log = l1_m1 - l2_m1 + l2_m1 / l1_m1;
710
711 // --- Middle region for W_0 ---
712 // ez/(2 + ez) is exact at z = -1/e and at z = 0 and decent between, covering the
713 // gap where L1 = ln z passes through zero and the logarithmic guess degenerates.
714 let ez = z * e;
715 let w0_mid = ez / (ez + Complex::real(V::TWO));
716
717 // --- Region selection ---
718 let d = Complex::new(z.re + <V as FloatConsts>::FRAC_NEG_1_E.abs(), z.im);
719 let near_branch = d.norm_sqr().cmp_lt(V::splat(c[1]));
720 let far = z.norm_sqr().cmp_gt(e * e);
721
722 let mut w0 = near_branch.select(w0_branch, far.select(w0_log, w0_mid));
723 let mut wm1 = near_branch.select(wm1_branch, wm1_log);
724
725 // --- Halley ---
726 let mut n = 0;
727 while n < iters {
728 n += 1;
729 w0 = halley::<P, V>(w0, z);
730 wm1 = halley::<P, V>(wm1, z);
731 }
732
733 // --- Edge cases ---
734 if const { P::POLICY.check_overflow } {
735 let zero = z.re.is_zero() & z.im.is_zero();
736 let nonzero = !zero;
737
738 // W_0(0) = 0; W_{-1}(0) = -inf, the real convention for the limit.
739 w0 = nonzero.select(w0, Complex::ZERO);
740 wm1 = nonzero.select(wm1, Complex::new(V::NEG_INFINITY, V::ZERO));
741 }
742
743 (w0, wm1)
744}
745
746/// One Halley step for `w e^w = z`, written through `e^{-w}` so that neither tail
747/// overflows:
748///
749/// ```text
750/// g = w - z e^{-w}
751/// d = (w^2 + 2w + 2) + (w + 2) z e^{-w}
752/// w' = w - 2(w + 1) g / d
753/// ```
754#[inline(always)]
755fn halley<P: Policy, V: RealFloatVector>(w: Complex<V>, z: Complex<V>) -> Complex<V> {
756 let enw = (-w).exp_p::<P>();
757 let wp1 = w + Complex::ONE;
758 let zenw = z * enw;
759
760 // w^2 + 2w + 2 = (w + 1)^2 + 1, and each product below folds its addend into the
761 // complex FMA rather than rounding the product first.
762 let q = wp1.mul_adde(wp1, Complex::ONE);
763
764 let g = w - zenw;
765 let d = (wp1 + Complex::ONE).mul_adde(zenw, q);
766
767 (wp1 + wp1).nmul_adde(g / d, w)
768}
769
770// ---------------------------------------------------------------------------
771// Per-element impls
772// ---------------------------------------------------------------------------
773
774/// `$B_2, B_4, \ldots, B_{14}$`. Exact rationals, so f32 and f64 differ only in the
775/// rounding; what differs materially is how many are worth keeping.
776macro_rules! bernoulli_b2n {
777 ($t:ty) => {
778 [
779 1.0 / 6.0,
780 -1.0 / 30.0,
781 1.0 / 42.0,
782 -1.0 / 30.0,
783 5.0 / 66.0,
784 -691.0 / 2730.0,
785 7.0 / 6.0,
786 ]
787 };
788}
789
790impl<V: RealFloatVector<Element = f32>> SpecializedComplexSpecialMath<Complex<f32>> for Complex<V> {
791 #[inline(always)]
792 fn complex_tgamma<P: Policy>(self) -> Self {
793 tgamma_impl::<P, V, _>(self, &thermite_special::tables::LANCZOS_F32)
794 }
795
796 #[inline(always)]
797 fn complex_lgamma<P: Policy>(self) -> Self {
798 lgamma_impl::<P, V, _>(self, &thermite_special::tables::LANCZOS_F32)
799 }
800
801 #[inline(always)]
802 fn complex_digamma<P: Policy>(self) -> Self {
803 digamma_impl::<P, V, _>(self, &thermite_special::tables::DIGAMMA_F32.p_large, 10.0)
804 }
805
806 #[inline(always)]
807 fn complex_trigamma<P: Policy>(self) -> Self {
808 // 24 bits of mantissa are exhausted long before the table is, so stop early
809 // and let the (cheaper) recurrence make up the difference.
810 const B: [f32; 4] = [1.0 / 6.0, -1.0 / 30.0, 1.0 / 42.0, -1.0 / 30.0];
811 trigamma_impl::<P, V, 4>(self, &B, 8.0)
812 }
813
814 #[inline(always)]
815 fn complex_lambert_w<P: Policy>(self) -> (Self, Self) {
816 const C: [f32; 2] = [11.0 / 72.0, 0.09];
817 lambert_w_impl::<P, V>(self, &C, 3)
818 }
819
820 #[inline(always)]
821 fn faddeeva_w<P: Policy>(self) -> Self {
822 self::faddeeva::faddeeva_w::<P, f32, V>(self)
823 }
824}
825
826impl<V: RealFloatVector<Element = f64>> SpecializedComplexSpecialMath<Complex<f64>> for Complex<V> {
827 #[inline(always)]
828 fn complex_tgamma<P: Policy>(self) -> Self {
829 tgamma_impl::<P, V, _>(self, &thermite_special::tables::LANCZOS_F64)
830 }
831
832 #[inline(always)]
833 fn complex_lgamma<P: Policy>(self) -> Self {
834 lgamma_impl::<P, V, _>(self, &thermite_special::tables::LANCZOS_F64)
835 }
836
837 #[inline(always)]
838 fn complex_digamma<P: Policy>(self) -> Self {
839 digamma_impl::<P, V, _>(self, &thermite_special::tables::DIGAMMA_F64.p_large, 10.0)
840 }
841
842 #[inline(always)]
843 fn complex_trigamma<P: Policy>(self) -> Self {
844 const B: [f64; 7] = bernoulli_b2n!(f64);
845 trigamma_impl::<P, V, 7>(self, &B, 16.0)
846 }
847
848 #[inline(always)]
849 fn complex_lambert_w<P: Policy>(self) -> (Self, Self) {
850 const C: [f64; 2] = [11.0 / 72.0, 0.09];
851 lambert_w_impl::<P, V>(self, &C, 4)
852 }
853
854 #[inline(always)]
855 fn faddeeva_w<P: Policy>(self) -> Self {
856 self::faddeeva::faddeeva_w::<P, f64, V>(self)
857 }
858}
859
860/// Rebuild a real coefficient array as `Dual` constants - same values, zero
861/// derivative parts, which is exactly what a constant of the expansion is.
862///
863/// Always sourced from the f64 tables. `E` is generic in the `Dual` impl below, so
864/// the f32/f64 table choice cannot be made there, and the array lengths differ; using
865/// the longer table costs an f32 inner a few extra terms rather than correctness.
866#[cfg(feature = "dual")]
867#[inline(always)]
868fn dual_consts<E, const N: usize, const M: usize>(src: &[f64; N]) -> [thermite_dual::Dual<E, M>; N]
869where
870 E: thermite::element::FloatElementWithBits + thermite_dual::DualValue,
871{
872 let mut out = [thermite_dual::Dual::<E, M>::constant(E::from_f64(src[0])); N];
873
874 let mut i = 1;
875 while i < N {
876 out[i] = thermite_dual::Dual::constant(E::from_f64(src[i]));
877 i += 1;
878 }
879
880 out
881}
882
883/// The Lanczos parameters as `Dual` constants; see [`dual_consts`].
884#[cfg(feature = "dual")]
885#[inline(always)]
886fn dual_lanczos<E, const M: usize>() -> Lanczos<thermite_dual::Dual<E, M>, 13>
887where
888 E: thermite::element::FloatElementWithBits + thermite_dual::DualValue,
889{
890 let l = &thermite_special::tables::LANCZOS_F64;
891
892 Lanczos {
893 g: thermite_dual::Dual::constant(E::from_f64(l.g)),
894 p_rev: dual_consts(&l.p_rev),
895 q_rev: dual_consts(&l.q_rev),
896 p_expg_scaled: dual_consts(&l.p_expg_scaled),
897 q: dual_consts(&l.q),
898 }
899}
900
901// The composite storage types reach `SpecialMath` over C through this trait like any
902// other. They take no position on the coefficient tables - which is the status quo,
903// since the Gamma family was unimplemented for them before this trait existed too -
904// but implementing it is what keeps `erf`, `erfc`, `logistic_sigmoid` and the
905// polynomial families working on `Complex<Dual<..>>` / `Complex<Compensated<..>>`.
906//
907// The element type is written out structurally rather than as an associated type so
908// that it is visibly disjoint from the f32/f64 impls above; a bare `E` would leave
909// coherence unable to prove the two cannot overlap.
910#[cfg(feature = "dual")]
911impl<E, V: FloatVector<Element = E>, const N: usize> SpecializedComplexSpecialMath<Complex<thermite_dual::Dual<E, N>>>
912 for Complex<thermite_dual::Dual<V, N>>
913where
914 E: thermite::element::FloatElementWithBits + thermite_dual::DualValue,
915 thermite_dual::Dual<V, N>: RealFloatVector<Element = thermite_dual::Dual<E, N>>,
916{
917 #[inline(always)]
918 fn complex_tgamma<P: Policy>(self) -> Self {
919 tgamma_impl::<P, thermite_dual::Dual<V, N>, 13>(self, &dual_lanczos::<E, N>())
920 }
921
922 #[inline(always)]
923 fn complex_lgamma<P: Policy>(self) -> Self {
924 lgamma_impl::<P, thermite_dual::Dual<V, N>, 13>(self, &dual_lanczos::<E, N>())
925 }
926
927 #[inline(always)]
928 fn complex_digamma<P: Policy>(self) -> Self {
929 digamma_impl::<P, thermite_dual::Dual<V, N>, 8>(
930 self,
931 &dual_consts(&thermite_special::tables::DIGAMMA_F64.p_large),
932 thermite_dual::Dual::constant(E::from_f64(10.0)),
933 )
934 }
935
936 /// The shared body is already generic over `RealValue`, so it differentiates
937 /// itself; all `Dual` has to supply is the same table with zero derivative parts,
938 /// which is what `Dual::constant` means.
939 ///
940 /// This is `psi_2` by forward-mode AD, without a tetragamma ever being written.
941 #[inline(always)]
942 fn complex_trigamma<P: Policy>(self) -> Self {
943 // The f64-grade table regardless of the inner element: `V::Element` is not
944 // known here, and over-converging an f32 costs a few terms rather than
945 // correctness.
946 let b = [
947 thermite_dual::Dual::<E, N>::constant(E::from_f64(1.0 / 6.0)),
948 thermite_dual::Dual::<E, N>::constant(E::from_f64(-1.0 / 30.0)),
949 thermite_dual::Dual::<E, N>::constant(E::from_f64(1.0 / 42.0)),
950 thermite_dual::Dual::<E, N>::constant(E::from_f64(-1.0 / 30.0)),
951 thermite_dual::Dual::<E, N>::constant(E::from_f64(5.0 / 66.0)),
952 thermite_dual::Dual::<E, N>::constant(E::from_f64(-691.0 / 2730.0)),
953 thermite_dual::Dual::<E, N>::constant(E::from_f64(7.0 / 6.0)),
954 ];
955
956 trigamma_impl::<P, thermite_dual::Dual<V, N>, 7>(
957 self,
958 &b,
959 thermite_dual::Dual::<E, N>::constant(E::from_f64(16.0)),
960 )
961 }
962
963 #[inline(always)]
964 fn complex_lambert_w<P: Policy>(self) -> (Self, Self) {
965 let c = [
966 thermite_dual::Dual::<E, N>::constant(E::from_f64(11.0 / 72.0)),
967 thermite_dual::Dual::<E, N>::constant(E::from_f64(0.09)),
968 ];
969
970 lambert_w_impl::<P, thermite_dual::Dual<V, N>>(self, &c, 4)
971 }
972
973 #[inline(always)]
974 fn faddeeva_w<P: Policy>(self) -> Self {
975 use self::faddeeva::{Weideman, WeidemanTables, faddeeva_w_with, weideman_n};
976
977 // Sourced from the f64 tables for the same reason as [`dual_consts`]: `E` is
978 // generic here, so the f32/f64 choice cannot be made. The tier still follows the
979 // policy, but is capped by f64's ladder rather than the inner element's.
980 macro_rules! tier {
981 ($n:literal) => {
982 faddeeva_w_with::<P, thermite_dual::Dual<E, N>, thermite_dual::Dual<V, N>, $n>(
983 self,
984 thermite_dual::Dual::constant(E::from_f64(<f64 as Weideman<$n>>::L)),
985 &dual_consts::<E, $n, N>(&<f64 as Weideman<$n>>::A),
986 thermite_dual::Dual::constant(E::from_f64(<f64 as WeidemanTables>::HUGE)),
987 thermite_dual::Dual::constant(E::from_f64(<f64 as WeidemanTables>::REAL_AXIS_Y)),
988 thermite_dual::Dual::constant(E::from_f64(<f64 as WeidemanTables>::REAL_AXIS_X)),
989 )
990 };
991 }
992
993 macro_rules! is {
994 ($n:literal) => {
995 const { weideman_n(P::POLICY.precision, <f64 as WeidemanTables>::MAX_N) <= $n }
996 };
997 }
998
999 if is!(8) {
1000 tier!(8)
1001 } else if is!(16) {
1002 tier!(16)
1003 } else if is!(24) {
1004 tier!(24)
1005 } else if is!(32) {
1006 tier!(32)
1007 } else {
1008 tier!(40)
1009 }
1010 }
1011}
1012
1013/*
1014#[cfg(feature = "compensated")]
1015impl<V: FloatVector>
1016 SpecializedComplexSpecialMath<Complex<thermite_compensated::Compensated<V::Element>>>
1017 for Complex<thermite_compensated::Compensated<V>>
1018where
1019 thermite_compensated::Compensated<V>: RealFloatVector<Element = thermite_compensated::Compensated<V::Element>>,
1020{
1021 #[inline(always)]
1022 fn complex_tgamma<P: Policy>(self) -> Self {
1023 todo!("complex tgamma over Compensated: needs the real complex tgamma first")
1024 }
1025
1026 #[inline(always)]
1027 fn complex_lgamma<P: Policy>(self) -> Self {
1028 todo!("complex lgamma over Compensated: needs the real complex lgamma first")
1029 }
1030
1031 #[inline(always)]
1032 fn complex_digamma<P: Policy>(self) -> Self {
1033 todo!("complex digamma over Compensated: needs the real complex digamma first")
1034 }
1035
1036 #[inline(always)]
1037 fn complex_trigamma<P: Policy>(self) -> Self {
1038 todo!("complex trigamma over Compensated: not yet ported - needs the table as Compensated constants")
1039 }
1040
1041 #[inline(always)]
1042 fn complex_lambert_w<P: Policy>(self) -> (Self, Self) {
1043 todo!("complex lambert_w over Compensated: not yet ported - needs the coefficients as Compensated constants")
1044 }
1045
1046 #[inline(always)]
1047 fn faddeeva_w<P: Policy>(self) -> Self {
1048 todo!("Faddeeva over Compensated: needs Weideman tables as Compensated constants")
1049 }
1050}
1051*/
1052
1053decl_complex_math! {
1054 /// Complex special functions that carry an element-specific coefficient table.
1055 ///
1056 /// Generated from [`SpecializedComplexSpecialMath`] by the same macro that builds
1057 /// [`ComplexMath`](crate::math::ComplexMath), so `z.faddeeva_w_p::<Precision>()` behaves as
1058 /// `z.norm_p::<Precision>()` does and generic code can bound on `V: ComplexSpecialMath`.
1059 ///
1060 /// Only the members with no home in [`SpecialMath`](thermite_special::SpecialMath) are
1061 /// re-exposed here. The Gamma family is already `z.tgamma()` there; declaring it a
1062 /// second time would make every such call ambiguous whenever both traits are in scope.
1063 trait ComplexSpecial<FloatElement>: ComplexVector {
1064 /// `$w(z) = e^{-z^2}\operatorname{erfc}(-iz)$`, the Faddeeva function (also the
1065 /// complex error function, or the plasma dispersion function up to a factor).
1066 ///
1067 /// See [`faddeeva`] for the algorithm, the accuracy ladder, and the one
1068 /// caveat that matters (`$\operatorname{Re} w$` near the real axis).
1069 fn faddeeva_w[][](self: Self) -> Self;
1070
1071 /// `$\operatorname{erfcx}(z) = e^{z^2}\operatorname{erfc}(z) = w(iz)$`, the
1072 /// scaled complementary error function.
1073 fn erfcx[][](self: Self) -> Self;
1074
1075 /// The Voigt function `$K(x, y) = \operatorname{Re} w(x + iy)$`, as a real value.
1076 fn voigt[][](self: Self) -> Self::Real;
1077 }
1078}