Skip to main content

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}