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}