Skip to main content

thermite_special/
lib.rs

1#![doc = include_str!("../README.md")]
2#![no_std]
3#![cfg_attr(docsrs, feature(doc_cfg))]
4#![allow(clippy::needless_arbitrary_self_type, clippy::needless_range_loop)]
5
6use thermite::{
7    element::{Element, ElementExt, FloatElementWithBits},
8    math::{
9        TranscendentalMathWithPolicy,
10        policy::{DefaultPolicy, Policy},
11        scalar::Unwrap,
12    },
13    vector::{FloatVector, FloatVectorWithBits},
14};
15
16pub mod specialized;
17
18/// Raw approximation coefficients behind the Gamma family.
19///
20/// Public because the sibling crates build their own kernels on the same
21/// constants (`thermite-complex` needs them for the complex Gamma family), and
22/// `#[doc(hidden)]` because that is the only audience it is meant for. Contents,
23/// layout and names track whatever the current approximation needs and change
24/// without notice - depend on the functions, not on these.
25#[doc(hidden)]
26pub mod tables;
27
28use crate::specialized::{CarlsonKind, EllipticKind, WrapTo};
29
30/// Elliptic integral request structs and the traits they implement:
31///
32/// - Carlson symmetric integrals (for [`SpecialMath::carlson`]): [`CarlsonRf`](elliptic::CarlsonRf),
33///   [`CarlsonRc`](elliptic::CarlsonRc), [`CarlsonRd`](elliptic::CarlsonRd), [`CarlsonRj`](elliptic::CarlsonRj),
34///   [`CarlsonRg`](elliptic::CarlsonRg), implementing [`CarlsonKind`].
35/// - Legendre integrals (for [`SpecialMath::ellint`]): [`EllintK`](elliptic::EllintK)/[`EllintF`](elliptic::EllintF),
36///   [`EllintE`](elliptic::EllintE)/[`EllintEInc`](elliptic::EllintEInc),
37///   [`EllintD`](elliptic::EllintD)/[`EllintDInc`](elliptic::EllintDInc),
38///   [`EllintPi`](elliptic::EllintPi)/[`EllintPiInc`](elliptic::EllintPiInc), implementing
39///   [`EllipticKind`]. Completeness is encoded by the struct - a complete integral has no `phi` field.
40pub mod elliptic {
41    pub use crate::specialized::EllipticConsts;
42
43    pub use crate::specialized::{CarlsonKind, CarlsonRc, CarlsonRd, CarlsonRf, CarlsonRg, CarlsonRj};
44
45    pub use crate::specialized::{
46        EllintD, EllintDInc, EllintE, EllintEInc, EllintF, EllintK, EllintPi, EllintPiInc, EllipticKind,
47    };
48}
49
50macro_rules! decl_math {
51    ($(
52        $(#[$trait_meta:meta])*
53        trait $trait:ident $(: $($bound:ident)&+)? { $(
54            $(#[$meta:meta])*
55            fn $name:ident [ $($generics:tt)* ][$($generic_names:ident),*]( $($arg_name:ident :$arg_ty:ty),* $(,)?) -> $ret:ty
56                $(where [ $($where_clause:tt)* ])?;
57        )*
58        // Optional block of "kind-dispatched" methods: a single request-struct argument carrying
59        // the operation's data (e.g. `CarlsonRf { x, y, z }`). The struct's `eval` (a CarlsonKind /
60        // EllipticKind impl) does the work; this generates the full trait family (policy + default +
61        // dispatched vector impl + scalar) around it. Because the struct's element backend
62        // (EllipticEval) covers both `Vector<R>` and scalar floats, the same bound works at the
63        // scalar layer - no Unwrap wrapping needed here.
64        $(@kinds {$(
65            $(#[$kmeta:meta])*
66            fn $kname:ident : $ktrait:path;
67        )*})?
68        }
69    )*) => {paste::paste! {$(
70        #[doc = "" $trait " Math functions for floating-point vectors with customizable policies.\n\n"]
71        #[doc = "Each method has a `_p`-suffixed variant in this trait that accepts a leading `P: Policy` generic.\n\n"]
72        #[doc = "All floating-point vector types that implement [`Specialized" $trait "Math`](specialized::SpecializedSpecialMath) will\n"]
73        #[doc = "automatically implement this trait, and [`" $trait "Math`] as well."]
74        $(#[$trait_meta])*
75        #[thermite::dispatch(Self)]
76        pub trait [<$trait MathWithPolicy>]: $($($bound +)+)? {$(
77            $(#[$meta])* fn [<$name _p>]<P: Policy, $($generics)*>($($arg_name: $arg_ty),*) -> $ret
78                $(where $($where_clause)*)?;
79        )*
80        $($(
81            $(#[$kmeta])* fn [<$kname _p>]<P: Policy, K: $ktrait<Output = Self>>(kind: K) -> Self;
82        )*)?
83        }
84
85        #[doc = "" $trait " Math functions for floating-point vectors using the default policy.\n\n"]
86        #[doc = "Implementors of [`" $trait "MathWithPolicy`] automatically implement this trait.\n\n"]
87        #[doc = "Each method here has a `_p`-suffixed counterpart in [`" $trait "MathWithPolicy`] that\n"]
88        #[doc = "accepts a leading `P: Policy` generic for fine-grained precision/performance control."]
89        $(#[$trait_meta])*
90        #[thermite::dispatch(Self)]
91        pub trait [<$trait Math>]: [<$trait MathWithPolicy>] {$(
92            $(#[$meta])* #[inline(always)] fn $name<$($generics)*>($($arg_name: $arg_ty),*) -> $ret
93                $(where $($where_clause)*)?
94            { [<$trait MathWithPolicy>]::[<$name _p>]::<DefaultPolicy, $($generic_names),*>($($arg_name),*) }
95        )*
96        $($(
97            $(#[$kmeta])* #[inline(always)] fn $kname<K: $ktrait<Output = Self>>(kind: K) -> Self
98            { [<$trait MathWithPolicy>]::[<$kname _p>]::<DefaultPolicy, K>(kind) }
99        )*)?
100        }
101
102        impl<M> [<$trait Math>] for M where M: [<$trait MathWithPolicy>] {}
103
104        #[thermite::dispatch(Self)]
105        impl<E, V: FloatVector<Element = E> $(+ $($bound +)+)?> [<$trait MathWithPolicy>] for V
106        where
107            V: specialized::[<Specialized $trait Math>]<E>,
108        {$(
109            #[cfg(not(feature = "disable_dispatch"))]
110            $(#[$meta])* #[inline(always)] fn [<$name _p>]<P: Policy, $($generics)*>($($arg_name: $arg_ty),*) -> $ret
111                $(where $($where_clause)*)?
112            { V::$name::<P, $($generic_names),*>($($arg_name),*) }
113
114            #[cfg(feature = "disable_dispatch")]
115            $(#[$meta])* #[skip_dispatch] #[inline(always)] fn [<$name _p>]<P: Policy, $($generics)*>($($arg_name: $arg_ty),*) -> $ret
116                $(where $($where_clause)*)?
117            { V::$name::<P, $($generic_names),*>($($arg_name),*) }
118        )*
119        $($(
120            // Kind methods delegate to the request struct's own `eval`; `#[dispatch]` wraps this in
121            // the per-ISA trampolines, so `eval`'s inner Carlson/AGM work runs under target_feature.
122            #[cfg(not(feature = "disable_dispatch"))]
123            $(#[$kmeta])* #[inline(always)] fn [<$kname _p>]<P: Policy, K: $ktrait<Output = Self>>(kind: K) -> Self
124            { kind.eval::<P>() }
125
126            #[cfg(feature = "disable_dispatch")]
127            $(#[$kmeta])* #[skip_dispatch] #[inline(always)] fn [<$kname _p>]<P: Policy, K: $ktrait<Output = Self>>(kind: K) -> Self
128            { kind.eval::<P>() }
129        )*)?
130        })*
131
132        #[doc = "Aggregate of all scalar special-math traits with customizable policies."]
133        #[doc = ""]
134        #[doc = "This trait collects every method from the following trait families into a single"]
135        #[doc = "trait implemented directly on `f32` and `f64`:"]
136        #[doc = ""]
137        $(#[doc = "- [`" [<$trait MathWithPolicy>] "`]"])*
138        #[doc = ""]
139        #[doc = "All methods are prefixed with `scalar_` to avoid conflicts with inherent methods"]
140        #[doc = "on `f32`/`f64`. The policy-aware versions additionally carry a `_p` suffix."]
141        #[doc = ""]
142        #[doc = "# Limitations"]
143        #[doc = ""]
144        #[doc = "This trait is **only** implemented for bare scalar types. Code that is generic over"]
145        #[doc = "a `FloatVector` bound will not accept a bare `f32` or `f64` - the scalar must be"]
146        #[doc = "wrapped in [`Vector`](thermite::Vector) first (e.g., `Vector::<f32>(x)`) to satisfy"]
147        #[doc = "that bound. `ScalarSpecialMath` exists purely as a convenience for call-sites that"]
148        #[doc = "already hold a concrete scalar and do not need to be generic."]
149        #[doc = ""]
150        #[doc = "For convenience, a default-policy version is provided by [`ScalarSpecialMath`], which"]
151        #[doc = "drops the `_p` suffix and uses [`DefaultPolicy`](thermite::math::policy::DefaultPolicy) for all operations."]
152        #[thermite::dispatch(Self)]
153        #[diagnostic::on_unimplemented(
154            message = "`{Self}` is not a bare floating-point scalar",
155            note = "`ScalarSpecialMathWithPolicy` is implemented only for the bare scalar types `f32` and `f64`. For SIMD vectors, bound on `FloatVector` plus the special-math traits (`SpecialMath`, `RealSpecialMath`, ...) instead."
156        )]
157        pub trait ScalarSpecialMathWithPolicy: ElementExt<Element = Self> + FloatElementWithBits {$($(
158             $(#[$meta])* fn [<scalar_ $name _p>]<P: Policy, $($generics)*>($($arg_name: $arg_ty),*) -> $ret
159                $(where $($where_clause)*)?;
160        )*
161        $($(
162            $(#[$kmeta])* fn [<scalar_ $kname _p>]<P: Policy, K: WrapTo>(kind: K) -> Self
163            where K::Wrapped: $ktrait, <K::Wrapped as $ktrait>::Output: Unwrap<Unwrapped = Self>;
164        )*)?
165        )*}
166
167        #[doc = "Aggregate of all scalar special-math traits using the default policy."]
168        #[doc = ""]
169        #[doc = "This trait collects every method from the following trait families into a single"]
170        #[doc = "trait implemented directly on `f32` and `f64`, using the default policy for all operations:"]
171        #[doc = ""]
172        $(#[doc = "- [`" [<$trait Math>] "`]"])*
173        #[doc = ""]
174        #[doc = "All methods are prefixed with `scalar_` to avoid conflicts with inherent methods"]
175        #[doc = "on `f32`/`f64`. See [`ScalarSpecialMathWithPolicy`] for the policy-aware variant,"]
176        #[doc = "which additionally carries a `_p` suffix on each method."]
177        #[doc = ""]
178        #[doc = "# Limitations"]
179        #[doc = ""]
180        #[doc = "This trait is **only** implemented for bare scalar types. Code that is generic over"]
181        #[doc = "a `FloatVector` bound will not accept a bare `f32` or `f64` - the scalar must be"]
182        #[doc = "wrapped in [`Vector`](thermite::Vector) first (e.g., `Vector::<f32>(x)`) to satisfy"]
183        #[doc = "that bound. `ScalarSpecialMath` exists purely as a convenience for call-sites that"]
184        #[doc = "already hold a concrete scalar and do not need to be generic."]
185        #[doc = ""]
186        #[doc = "All types that implement [`ScalarSpecialMathWithPolicy`] automatically implement this trait."]
187        #[thermite::dispatch(Self)]
188        #[diagnostic::on_unimplemented(
189            message = "`{Self}` is not a bare floating-point scalar",
190            note = "`ScalarSpecialMath` is implemented only for the bare scalar types `f32` and `f64`. For SIMD vectors, bound on `FloatVector` plus the special-math traits (`SpecialMath`, `RealSpecialMath`, ...) instead."
191        )]
192        pub trait ScalarSpecialMath: ScalarSpecialMathWithPolicy {$($(
193            $(#[$meta])* #[inline(always)] fn [<scalar_ $name>]<$($generics)*>($($arg_name: $arg_ty),*) -> $ret
194                $(where $($where_clause)*)?
195            { ScalarSpecialMathWithPolicy::[<scalar_ $name _p>]::<DefaultPolicy, $($generic_names),*>($($arg_name),*) }
196        )*
197        $($(
198            $(#[$kmeta])* #[inline(always)] fn [<scalar_ $kname>]<K: WrapTo>(kind: K) -> Self
199            where K::Wrapped: $ktrait, <K::Wrapped as $ktrait>::Output: Unwrap<Unwrapped = Self>
200            { ScalarSpecialMathWithPolicy::[<scalar_ $kname _p>]::<DefaultPolicy, K>(kind) }
201        )*)?
202        )*}
203
204        impl<M> ScalarSpecialMath for M where M: ScalarSpecialMathWithPolicy {}
205
206        #[thermite::dispatch(Self)]
207        impl<E: ElementExt<Element = Self> + FloatElementWithBits> ScalarSpecialMathWithPolicy for E
208        where
209            thermite::Vector<E>: Unwrap<Unwrapped = E> +
210                FloatVectorWithBits<Element = E,
211                    Signed: Unwrap<Unwrapped = <E as Element>::Signed>,
212                    Unsigned: Unwrap<Unwrapped = <E as Element>::Unsigned>,
213                    SignedBits: Unwrap<Unwrapped = <E as FloatElementWithBits>::SignedBits>,
214                    Bits: Unwrap<Unwrapped = <E as FloatElementWithBits>::Bits>
215                >
216                $(+ specialized::[<Specialized $trait Math>]<E>)*,
217            E: thermite::register::FloatRegister<Storage = E>,
218        {$($(
219            $(#[$meta])* #[skip_dispatch] #[inline(always)] fn [<scalar_ $name _p>]<P: Policy, $($generics)*>($($arg_name: $arg_ty),*) -> $ret
220                $(where $($where_clause)*)?
221            {
222                let ($(decl_math!(@SELF $arg_name this),)*) = Unwrap::wrap(($($arg_name,)*));
223
224                let res = <thermite::Vector<E> as specialized::[<Specialized $trait Math>]<E>>::$name::<P, $($generic_names),*>($(decl_math!(@SELF $arg_name this)),*);
225
226                Unwrap::unwrap(res)
227            }
228        )*
229        $($(
230            // Kind methods: wrap the scalar request into its width-1 vector form (WrapTo), run the
231            // vector-only `eval`, then unwrap the scalar result. The backend stays vector-only.
232            $(#[$kmeta])* #[skip_dispatch] #[inline(always)] fn [<scalar_ $kname _p>]<P: Policy, K: WrapTo>(kind: K) -> Self
233            where K::Wrapped: $ktrait, <K::Wrapped as $ktrait>::Output: Unwrap<Unwrapped = Self>
234            { Unwrap::unwrap(<K::Wrapped as Unwrap>::wrap(kind).eval::<P>()) }
235        )*)?
236        )*}
237    }};
238
239    // rename `self` to `this`. Requires an existing ident to bind to.
240    (@SELF self $rename:ident) => { $rename };
241    (@SELF $other:ident $rename:ident) => { $other };
242}
243
244decl_math! {
245    /// Special math functions that are valid for both real and complex floating-point vectors.
246    #[diagnostic::on_unimplemented(
247        message = "`{Self}` does not provide special math (`erf`, `gamma`, activations, ...)",
248        note = "The special-math traits are auto-implemented for every float vector (any `FloatVector` whose element is `f32`/`f64`) and for composite float types. A bare `f32`/`f64` does not qualify - wrap it in `Vector::<f32>::splat(x)`, or use `ScalarSpecialMath`'s `scalar_`-prefixed methods.",
249        note = "If `{Self}` already is a `FloatVector` and only the method call fails to resolve, bring the trait into scope: `use thermite_special::SpecialMath;` (or the relevant `RealSpecialMath` / `RealPrimalMath`)."
250    )]
251    trait Special: TranscendentalMathWithPolicy {
252        /// Computes the error function.
253        ///
254        /// For f32 vectors, this is still decently accurate even with the `Medium` and `Worst` precision policies,
255        /// thanks to good approximations that don't rely on the precision of `exp`. Subsequently, performance
256        /// of the lower precision policies is excellent. Furthermore, if using on a GPU with native `exp` support,
257        /// all precision policies will have good performance and accuracy.
258        fn erf[][](self: Self) -> Self;
259
260        /// Computes the complementary error function.
261        fn erfc[][](self: Self) -> Self;
262
263        /// Computes the Logistic sigmoid function, defined as `$\sigma(x) = \frac{1}{1 + e^{-x}}$`.
264        ///
265        /// It's worth mentioning that the derivative of the logistic sigmoid can be computed very cheaply
266        /// from the output of the logistic sigmoid itself, in the form of:
267        ///
268        /// ```rust,ignore
269        /// let s = x.logistic_sigmoid();
270        /// let derivative = s * (1.0 - s); // or s.nmul_adde(s, s), which may be slightly faster
271        /// ```
272        ///
273        /// Notably, for `f32` and `f64` this implementation still has good precision for the `Worst`
274        /// precision policy, and for the `Best` precision policies handles very large positive and negative
275        /// inputs without overflow or underflow issues.
276        fn logistic_sigmoid[][](self: Self) -> Self;
277
278        /// Computes the softplus function, defined as `$\frac{1}{k}\ln(1 + e^{kx})$`.
279        ///
280        /// This is a smooth approximation to the ReLU function
281        /// that is more numerically stable for large inputs.
282        ///
283        /// The parameter `k` controls the steepness of the curve, with larger values approaching ReLU more closely.
284        /// Pass `k = 1` and `rcp_k = 1` for the standard softplus with no steepness scaling.
285        ///
286        /// `rcp_k` must equal `1/k`. It is passed explicitly so callers that invoke softplus repeatedly
287        /// with the same `k` can pre-compute the reciprocal once rather than recomputing it per call.
288        ///
289        /// To also obtain the derivative with respect to `x`, use
290        /// [`softplus_d`](crate::RealPrimalMath::softplus_d).
291        fn softplus[][](self: Self, k: Self, rcp_k: Self) -> Self;
292
293        /// Computes the Gamma function (`$\Gamma(z)$`) for any real input, for each value in a vector.
294        ///
295        /// This implementation uses a few different behaviors to ensure the greatest precision where possible.
296        ///
297        /// * For non-integer positive inputs, it uses the Lanczos approximation.
298        /// * For small non-integer negative inputs, it uses the recursive identity `$\Gamma(z) = \Gamma(z+1)/z$` until `z` is positive.
299        /// * For large non-integer negative inputs, it uses the reflection formula `$-\pi / (\Gamma(z)\sin(\pi z)\,z)$`.
300        /// * For positive integers, it simply computes the factorial in a tight loop to ensure precision. Lookup tables could not be used with SIMD.
301        /// * At zero, the result will be positive or negative infinity based on the input sign (signed zero is a thing).
302        ///
303        /// **NOTE**: The Gamma function is not defined for negative integers.
304        fn tgamma[][](self: Self) -> Self;
305
306        /// Computes the natural log of the Gamma function (`$\ln|\Gamma(x)|$`) for any real input, for each value in a vector.
307        fn lgamma[][](self: Self) -> Self;
308
309        /// Computes the digamma function `$\psi(x) = \frac{\mathrm{d}}{\mathrm{d}x}\ln\Gamma(x) = \frac{\Gamma'(x)}{\Gamma(x)}$`
310        /// for any real input, for each value in a vector.
311        ///
312        /// The argument is handled in three regimes:
313        ///
314        /// * For `x >= 10`, an asymptotic expansion in `$1/x^2$` is used.
315        /// * For smaller `x`, the recurrence `$\psi(x) = \psi(x+1) - 1/x$` shifts the argument into
316        ///   `[1, 2]`, where a rational minimax approximation `$\psi(x) = (x - x_0)(Y + R(x-1))$` is used
317        ///   (`$x_0$` is the positive root of `$\psi$`).
318        /// * For `x <= -1`, the reflection formula `$\psi(1-x) = \psi(x) + \pi\cot(\pi x)$` is applied.
319        ///
320        /// **NOTE**: The digamma function is not defined at zero or the negative integers; those inputs
321        /// yield NaN when overflow checking is enabled.
322        fn digamma[][](self: Self) -> Self;
323
324        /// Computes the Beta function `$\mathrm{B}(x, y)$`
325        fn beta[][](self: Self, y: Self) -> Self;
326
327        /// Computes the m-th derivative of the n-th degree Jacobi polynomial
328        ///
329        /// A the special case where α and β are both zero, the Jacobi polynomial reduces to a
330        /// Legendre polynomial.
331        ///
332        /// **NOTE**: Given constant α, β or `n`, LLVM will happily optimize those away and unroll loops.
333        fn jacobi[][](self: Self, alpha: Self, beta: Self, n: u32, m: u32) -> Self;
334
335        /// Computes the N-th degree physicists' [Hermite polynomial](https://en.wikipedia.org/wiki/Hermite_polynomials)
336        /// `H_n(x)` where `x` is `self` and `N` is the polynomial degree.
337        ///
338        /// This uses the recurrence relation to compute the polynomial iteratively.
339        fn hermite[const N: usize][N](self: Self) -> Self;
340
341        /// Computes the n-th degree physicists' [Hermite polynomial](https://en.wikipedia.org/wiki/Hermite_polynomials)
342        /// `H_n(x)` where `x` is `self` and `n` is a vector of unsigned integers representing the polynomial degree.
343        ///
344        /// The polynomial is calculated independently per-lane with the given degree in `n`.
345        ///
346        /// This uses the recurrence relation to compute the polynomial iteratively.
347        fn hermitev[][](self: Self, n: Self::Unsigned) -> Self;
348
349        /// Evaluates a finite series of [Chebyshev polynomials](https://en.wikipedia.org/wiki/Chebyshev_polynomials)
350        /// of the `K`-th kind at `x = self`:
351        ///
352        /// ```math
353        /// \sum_{k=0}^{N-1} \mathrm{coeffs}[k] \cdot P_k(x)
354        /// ```
355        ///
356        /// where `P_k` is `T_k`, `U_k`, `V_k`, or `W_k` depending on `K`. All four kinds share the
357        /// recurrence `$P_{k+1}(x) = 2x \cdot P_k(x) - P_{k-1}(x)$` with `P_0(x) = 1`; they differ only in
358        /// `P_1(x)`:
359        ///
360        /// | `K` | Kind   | `P_1(x)`   | Notes |
361        /// |-----|--------|------------|-------|
362        /// | `1` | First  (`T_k`) | `x`        | Most common; minimax/approximation basis on `[-1, 1]`. |
363        /// | `2` | Second (`U_k`) | `2x`       | Related to `$\sin((k+1)\theta)/\sin(\theta)$` under `$x = \cos\theta$`. |
364        /// | `3` | Third  (`V_k`) | `2x - 1`   | "Airfoil" polynomials; `$\cos((k+\tfrac12)\theta)/\cos(\theta/2)$`. |
365        /// | `4` | Fourth (`W_k`) | `2x + 1`   | `$\sin((k+\tfrac12)\theta)/\sin(\theta/2)$`. |
366        ///
367        /// Any other value of `K` is a compile-time error.
368        ///
369        /// Evaluation is done via Clenshaw's backward recurrence with FMA, which is
370        /// more numerically stable than a forward sum when the partial sums of
371        /// `$\sum c_k P_k$` are much smaller than `$\max_k |c_k P_k|$` (e.g. fitted minimax series
372        /// with alternating-sign coefficients). `N` is the *length* of the coefficient
373        /// slice, so the highest polynomial term is `P_{N-1}`; `N = 0` is rejected,
374        /// `N = 1` evaluates to `coeffs[0]`.
375        ///
376        /// `coeffs[0]` multiplies `P_0 = 1`, `coeffs[1]` multiplies `P_1(x)` (which depends on `K`),
377        /// and so on. Because LLVM sees both `K` and `N` as constants, the recurrence loop and the
378        /// `P_1` selection are fully unrolled and specialized at monomorphization time.
379        #[skip_dispatch] fn chebyshev[const K: usize, const N: usize][K, N](self: Self, coeffs: &[Self::Element; N]) -> Self;
380
381        /// Computes the Gaussian function with amplitude `a` and standard deviation `c`, defined as `$a\, e^{-\frac{1}{2}(x/c)^2}$`.
382        ///
383        /// The position `b` is assumed to be zero. For a non-zero position, use `self - b` as the input.
384        fn gaussian[][](self: Self, a: Self, c: Self) -> Self;
385
386        /// Computes the m-th associated n-th degree Legendre polynomial,
387        /// where m=0 signifies the regular n-th degree Legendre polynomial.
388        ///
389        /// If `m` is odd, the input is only valid between -1 and 1
390        ///
391        /// **NOTE**: Given constant `n` and/or `m`, LLVM will happily unroll and optimize inner loops.
392        ///
393        /// Internally, this is computed with [`jacobi`](SpecialMath::jacobi) when m > 0.
394        fn legendre[][](self: Self, n: u32, m: u32) -> Self;
395
396        /// Computes both branches of the Lambert W function simultaneously: (`$W_0(x)$`, `$W_{-1}(x)$`).
397        ///
398        /// The `$W_0$` result is valid for `x >= -1/e`; the `$W_{-1}$` result is valid for `-1/e <= x < 0`.
399        /// Outside these domains, the respective result is NaN (when overflow checking is enabled).
400        fn lambert_w[][](self: Self) -> (Self, Self);
401
402        // TEMP(bessel_j): disabled until orders beyond J_0 exist. Only f32 `J_0` was
403        // ever implemented, so every composite type (Dual, Complex, Compensated) could
404        // do nothing but `todo!()`. Re-enable this line and the ones marked
405        // TEMP(bessel_j) elsewhere together.
406        //fn bessel_j[const N: usize][N](self: Self) -> Self;
407
408        /// Computes the generalized exponential integral `E_n(x)` for integer order `n`.
409        fn expint[const N: usize][N](self: Self) -> Self;
410
411        @kinds {
412            /// Carlson symmetric elliptic integral, selected by a [`CarlsonKind`] request struct
413            /// with named fields - the arity (and which argument is the parameter / repeated one)
414            /// is fixed per kind, so the wrong shape is a compile error.
415            ///
416            /// ```rust,ignore
417            /// let rf = V::carlson(CarlsonRf { x, y, z });
418            /// let rj = V::carlson_p::<Precision, _>(CarlsonRj { x, y, z, p });
419            /// ```
420            fn carlson: CarlsonKind;
421
422            /// Legendre elliptic integral, selected by an [`EllipticKind`] request struct. Each
423            /// form ([`EllintK`](elliptic::EllintK)/[`EllintF`](elliptic::EllintF)/[`EllintE`](elliptic::EllintE)/
424            /// [`EllintEInc`](elliptic::EllintEInc)/[`EllintD`](elliptic::EllintD)/[`EllintDInc`](elliptic::EllintDInc)/
425            /// [`EllintPi`](elliptic::EllintPi)/[`EllintPiInc`](elliptic::EllintPiInc)) carries exactly
426            /// its own arguments; completeness is encoded by whether the struct has a `phi` field.
427            ///
428            /// ```rust,ignore
429            /// let k_int = V::ellint(EllintK { k });                       // K(k)
430            /// let e_inc = V::ellint_p::<Precision, _>(EllintEInc { phi, k }); // E(phi, k)
431            /// ```
432            fn ellint: EllipticKind;
433        }
434    }
435
436    /// Special math functions that are only defined for real-valued floating-point vectors.
437    ///
438    /// These functions either rely on ordering/sign information that has no complex analogue
439    /// (e.g. `erfinv`, `probit`, `lgamma_r`), or use the real absolute value in a way that
440    /// makes them non-holomorphic (e.g. `algebraic_sigmoid`).
441    #[diagnostic::on_unimplemented(
442        message = "`{Self}` does not provide real-valued special math (`erfinv`, `probit`, `lgamma_r`, ...)",
443        note = "`RealSpecialMath` is only meaningful for real-valued float vectors - complex number types deliberately do not implement it. A bare `f32`/`f64` does not qualify - wrap it in `Vector::<f32>::splat(x)`, or use `ScalarSpecialMath`."
444    )]
445    trait RealSpecial: SpecialMathWithPolicy {
446        /// Computes the inverse error function.
447        fn erfinv[][](self: Self) -> Self;
448
449        /// Computes the Probit function, the inverse of the cumulative distribution function
450        /// of the standard normal distribution.
451        fn probit[][](self: Self) -> Self;
452
453        /// GELU activation function, defined as `$\tfrac{1}{2} x \left(1 + \operatorname{erf}\!\left(\frac{\alpha x}{\sqrt{2}}\right)\right)$`,
454        /// where `alpha` helps control the shape of the curve. The standard GELU function
455        /// is recovered when `alpha` is 1.
456        ///
457        /// For f32 vectors, this remains decently accurate even with the `Medium` and `Worst` precision policies,
458        /// thanks to good `erf` implementations at the various precision levels. See `erf` for more details.
459        ///
460        /// To also obtain the derivative with respect to `x` (which shares most of the computation), use
461        /// [`gelu_d`](crate::RealPrimalMath::gelu_d).
462        fn gelu[][](self: Self, alpha: Self) -> Self;
463
464        /// Swish activation function, defined as `$x\,\sigma(\beta x) = \frac{x}{1 + e^{-\beta x}}$`,
465        /// where `beta` controls the sharpness of the gate. The standard Swish/SiLU function
466        /// is recovered when `beta` is 1. As `beta -> 0`, the output approaches `x/2` (half-identity);
467        /// as `beta -> inf`, Swish approaches ReLU.
468        ///
469        /// To also obtain the derivative with respect to `x`, use
470        /// [`swish_d`](crate::RealPrimalMath::swish_d).
471        fn swish[][](self: Self, beta: Self) -> Self;
472
473        /// Computes the algebraic sigmoid function, defined as `$\frac{x}{(1 + |x|^N)^{1/N}}$`, where
474        /// `N` is a positive integer parameter that controls the steepness of the curve.
475        ///
476        /// This also has the unique behavior where for `N=0`, the function is just the identity function,
477        /// and for `N=1` it is the [softsign function](https://en.wikipedia.org/wiki/Activation_function#Softsign).
478        ///
479        /// **Note**: This function uses `$|x|^N$` (the real absolute value), making it non-holomorphic
480        /// and therefore only meaningful for real-valued inputs.
481        ///
482        /// To also obtain the derivative with respect to `x`, use
483        /// [`algebraic_sigmoid_d`](crate::RealPrimalMath::algebraic_sigmoid_d).
484        fn algebraic_sigmoid[const N: usize][N](self: Self) -> Self;
485
486        /// Algebraic analogue of the [Swish](https://en.wikipedia.org/wiki/Swish_function) activation,
487        /// defined as `$x\left(\frac{1}{2} + \frac{x}{2\sqrt{1 + x^2}}\right)$`. Equivalent to gating `x` by
488        /// `(1 + algebraic_sigmoid::<2>(x)) / 2`, the `[0, 1]`-rescaled `N=2` algebraic sigmoid.
489        ///
490        /// Like standard Swish/SiLU, this is smooth and non-monotonic - it dips slightly below zero
491        /// for moderately negative `x` before rising - and shares the same asymptotes (`f(x) -> x` as
492        /// `x -> ∞`, `f(x) -> 0` as `x -> -∞`). Unlike Swish, it requires no `exp` or `log`, making
493        /// it substantially cheaper on hardware without fast transcendentals.
494        ///
495        /// To also obtain the derivative with respect to `x` (which shares most of the underlying
496        /// computation, notably `$1/\sqrt{1 + x^2}$`), use
497        /// [`algebraic_swish_d`](crate::RealPrimalMath::algebraic_swish_d).
498        ///
499        /// # Historical note
500        ///
501        /// Algebraic gating functions of this form are effectively unknown in modern deep learning,
502        /// which standardized on `exp`-based activations (sigmoid, Swish/SiLU, GELU) once GPUs made
503        /// `exp` essentially free - a single-cycle special-function-unit op on most modern hardware.
504        /// On CPUs the calculus is different: a vectorized `exp` still costs ~20+ cycles even with
505        /// good polynomial approximations, while `sqrt`/`rsqrt` are cheap hardware ops (often
506        /// approximated in 4-7 cycles). For CPU-side inference, training on CPU, or embedded targets
507        /// without a transcendental SFU, this remains a competitive Swish-shaped activation at a
508        /// fraction of the cost.
509        fn algebraic_swish[][](self: Self) -> Self;
510
511        /// Computes the natural log of the Gamma function (`$\ln|\Gamma(x)|$`) for any real input, for each value in a vector,
512        /// and returns the sign of the Gamma function from before the absolute value was taken.
513        fn lgamma_r[][](self: Self) -> (Self, Self);
514
515        /// Computes the definite integral of the Gaussian function from `x0` to `x1`, with amplitude `a` and standard deviation `c`.
516        /// This is more efficient than evaluating the indefinite integral at both limits and subtracting.
517        ///
518        /// The position `b` is assumed to be zero, so offset the limits accordingly for a non-zero position.
519        fn gaussian_integral[][](x0: Self, x1: Self, a: Self, c: Self) -> Self;
520    }
521
522    /// "Primal" special functions: the value-and-derivative (`_d`) forms of the activation
523    /// functions, returning `(value, derivative)` together.
524    ///
525    /// These exist for *single-value* real numbers (`f32`, `f64`, `Compensated`, ...) where the
526    /// analytic derivative is a useful, cheaply-shared byproduct of the value. They are **not**
527    /// implemented for derivative-carrying numbers such as `Dual`: an automatic-differentiation
528    /// type already produces the derivative from the plain value form (e.g. [`gelu`](RealSpecialMath::gelu)),
529    /// so the bundled `_d` derivative would be redundant work at the wrong level of abstraction.
530    ///
531    /// Each `*_d` method mirrors the like-named value-only function in [`SpecialMath`] /
532    /// [`RealSpecialMath`], returning that same value as the first tuple element.
533    #[diagnostic::on_unimplemented(
534        message = "`{Self}` does not provide value-and-derivative special math (`softplus_d`, `gelu_d`, ...)",
535        note = "`RealPrimalMath` builds on `RealSpecialMath` and is only meaningful for real-valued float vectors. A bare `f32`/`f64` does not qualify - wrap it in `Vector::<f32>::splat(x)`, or use `ScalarSpecialMath`."
536    )]
537    trait RealPrimal: RealSpecialMathWithPolicy {
538        /// [`softplus`](SpecialMath::softplus) together with its derivative w.r.t. `x`
539        /// (the logistic sigmoid `$\sigma(kx)$`).
540        fn softplus_d[][](self: Self, k: Self, rcp_k: Self) -> (Self, Self);
541
542        /// [`gelu`](RealSpecialMath::gelu) together with its derivative w.r.t. `x`.
543        fn gelu_d[][](self: Self, alpha: Self) -> (Self, Self);
544
545        /// [`swish`](RealSpecialMath::swish) together with its derivative w.r.t. `x`.
546        fn swish_d[][](self: Self, beta: Self) -> (Self, Self);
547
548        /// [`algebraic_sigmoid`](RealSpecialMath::algebraic_sigmoid) together with its derivative w.r.t. `x`.
549        fn algebraic_sigmoid_d[const N: usize][N](self: Self) -> (Self, Self);
550
551        /// [`algebraic_swish`](RealSpecialMath::algebraic_swish) together with its derivative w.r.t. `x`.
552        fn algebraic_swish_d[][](self: Self) -> (Self, Self);
553    }
554}