Skip to main content

thermite_special/specialized/generic/bessel/
ik_real.rs

1//! Modified Bessel `$I_\nu$` and `$K_\nu$` at **arbitrary real order**.
2//!
3//! The modified twin of [`bessel_nu`](super::jy_real), and built for the same reason that
4//! one was: Boost's Airy functions reach `$\mathrm{Ai}$` and `$\mathrm{Bi}$` for `$x > 0$`
5//! through `cyl_bessel_k(1/3, p)` and `cyl_bessel_i(\pm 1/3, p)`, not through `$J$`. Only the
6//! `$x < 0$` branch is `$J_{\pm 1/3}$`, which [`bessel_nu`](super::jy_real) already covers.
7//!
8//! Port target: Boost.Math's `temme_ik`, `CF1_ik` and `CF2_ik`, assembled in its
9//! `bessel_ik`. Modelled first in
10//! `notes/special/tools/model_bessel_ik.py`, which is where the three departures below were
11//! measured rather than argued.
12//!
13//! # One body, two arithmetics
14//!
15//! Every function here takes the **order in a real vector `R`** and the **argument in `C`**,
16//! with `C: PrimalProjection<Primal = R>`. On the real line `C = R` and nothing changes. Over
17//! C, `thermite-complex` instantiates the same body with `C = Complex<R>`: the order stays
18//! real (as it does in Amos, whose `zbknu` / `zwrsk` / `zasyi` are exactly the three arms
19//! below), every `z`-dependent quantity becomes complex, and the `nu`-only quantities
20//! (Temme's `$\Gamma(1\pm\nu)$` pieces, the `$(2k+1)^2 - 4\nu^2$` numerators, the recurrence
21//! coefficients) stay real and enter through `C: Mul<R>`, which is two real multiplies
22//! rather than a complex one. Every _decision_ the kernel makes about `z` (region, domain,
23//! overflow corner) goes through [`BesselDetails`], because on `Complex` the plain
24//! comparisons mean something else.
25//!
26//! # Three regions, and one fewer than Boost has
27//!
28//! | `x` | `K` | `I` |
29//! |---|---|---|
30//! | `<= 2` | [`temme_ik`] + upward recurrence | Wronskian, from `K` and [`cf1_i_ratio`] |
31//! | `2 .. max(40, nu^2/3)` | [`cf2_ik`] + upward recurrence | same |
32//! | above that | [`cf2_ik`] + upward recurrence | [`asymptotic_series_g`](super::ik::asymptotic_series_g) |
33//!
34//! **Boost's fourth arm is deleted.** It takes an ascending series for `$I$` whenever
35//! `$x/\nu < 0.25$`. Measured against the continued fraction alone over
36//! orders to 100 and `$x$` to 10 (the whole region where that test can fire), the series is
37//! 0.0 to 2.8 eps and the fraction 0.5 to **5.2 eps**. Both are inside the crate's contract, so
38//! the arm buys nothing a vector packet would not pay for anyway. It also needs a `powf` and a
39//! `tgamma`, and its own prefactor overflows at order 200 where the fraction does not care.
40//!
41//! **`CF2_ik`'s renormalisation is deleted too.** Boost rescales `q`, `prev`, `current` and `C`
42//! whenever `$q < \varepsilon$`, and its comment says why: "particularly an issue for types
43//! which have many digits precision but a narrow exponent range. A typical example being a
44//! double double type." Measured in binary64 over `$u \in [-1/2, 1/2]$` and `$x$` from 2.001 to
45//! `$10^5$`, with and without: **7.72 eps either way**, identical. It is dead code at this
46//! precision, and a per-lane select if kept.
47//!
48//! # What each arm costs
49//!
50//! `$K$` gets cheaper as `$x$` grows and needs **no asymptotic arm at all**: [`cf2_ik`] takes
51//! 9 iterations at `$x = 100$` and **2 at `$x = 10^8$`**, at 1 eps throughout. `$I$` is the
52//! opposite: [`cf1_i_ratio`] grows like `$\sqrt{x}$` (39 iterations at 40, 428 at 5000, and
53//! simply fails to converge by `$x = 10^6$`), which is what the asymptotic handover is for.
54//! Boost's own comment calls that growth `$O(x)$`. Measured here it is `$O(\sqrt{x})$`.
55//!
56//! # Scaling
57//!
58//! Everything is carried in the crate's `SCALED` convention, `$(e^{-x}I_\nu,\; e^{x}K_\nu)$`,
59//! because that is the form the algorithm _natively produces_: [`cf2_ik`] has the `$e^{-x}$` as
60//! an explicit factor, and once `$K$` is scaled the Wronskian returns `$I$` **already scaled,
61//! with no exponential anywhere**. The unscaled form is the one paying for a transcendental,
62//! the reverse of the small-`$x$` arm where Temme's series is naturally unscaled.
63
64use core::ops::{Add, Div, Mul, Sub};
65
66use thermite::{
67    math::{PrimalProjection, TranscendentalMathWithPolicy, algorithms::sum_pair, policy::Policy},
68    prelude::*,
69};
70
71use thermite::const_splat;
72use thermite::element::FloatElement;
73
74use crate::specialized::BesselDetails;
75use crate::specialized::generic::lgamma1p::tgamma1pm1_pair;
76use crate::tables::lgamma1p::LogGamma1p;
77
78/// `$(K_\nu(x), K_{\nu+1}(x))$` **unscaled**, by Temme's series, for `$|x| \le 2$` and
79/// `$\lvert\nu\rvert \le 1/2$`.
80///
81/// Temme, _Journal of Computational Physics_ vol 19, 324 (1975). Boost's `temme_ik`.
82/// The structural twin of
83/// [`temme_y_nu`](super::jy_real::temme_y_nu): the same `gamma1`/`gamma2` limits, the same
84/// `coef` chain, the same paired-Additive shape through
85/// [`sum_pair`](thermite::math::algorithms::sum_pair), combined differently and with the
86/// `coef` multiplier positive rather than negative, since `$I$`/`$K$` do not oscillate.
87///
88/// # Two of the four limits are shipped functions, not guards
89///
90/// `$c = \sin(\pi\nu)/(\pi\nu)$` is exactly [`sinc_pi`](thermite::math::TranscendentalMath::sinc_pi)
91/// and `$d = \sinh\sigma/\sigma$` is exactly [`sinhc`](thermite::math::RealMath::sinhc), so
92/// neither needs the `$0/0$` select Boost writes for it. Only `gamma1` keeps one, and its
93/// limit is `$-\gamma$`.
94///
95/// # Precondition
96///
97/// `$\lvert\nu\rvert \le 1/2$` is not a suggestion. The series is built around
98/// `$\Gamma(1\pm\nu)$` near one. The caller reduces the order and walks `$K$` up, which is
99/// stable because `$K$` is the dominant solution.
100#[inline(always)]
101pub fn temme_ik<P, E, R, C, const NE: usize, const NO: usize>(
102    nu: R,
103    z: C,
104    needed: C::Mask,
105    t: &LogGamma1p<E, NE, NO>,
106) -> (C, C)
107where
108    E: FloatElement,
109    R: FloatVector<Element = E> + TranscendentalMathWithPolicy,
110    C: FloatVector<Mask = R::Mask>
111        + TranscendentalMathWithPolicy
112        + PrimalProjection<Primal = R>
113        + Mul<R, Output = C>
114        + Div<R, Output = C>
115        + Add<R, Output = C>
116        + Sub<R, Output = C>,
117    P: Policy,
118{
119    if needed.none() {
120        return (C::ZERO, C::ZERO);
121    }
122
123    // Everything on the order alone is real.
124    let (gp, gm) = tgamma1pm1_pair::<P, E, R, NE, NO>(nu, t);
125    let c = nu.sinc_pi_p::<P>();
126
127    let at_zero = nu.abs().cmp_lt(<R as FloatConsts>::EPSILON);
128    let gamma1 = at_zero.select(-<R as FloatConsts>::EULER_GAMMA, (R::HALF / nu) * (gp - gm) * c);
129    let gamma2 = (R::TWO + gp + gm) * c * R::HALF;
130
131    // Everything on the argument is `C`. `(z/2)^nu` with a real exponent.
132    //
133    // The real coefficients that multiply a `C` inside an FMA are lifted with
134    // `from_primal` rather than written as `C * R`: on the real line `from_primal` is the
135    // identity and the FMA fuses exactly as it did before this body was shared, so the
136    // real instantiation is bit-identical to the pre-generic kernel.
137    let log_half_z = (z * C::HALF).ln_p::<P>();
138    let b = (z * C::HALF).powf_p::<P>(C::from_primal(nu));
139    let sigma = -(log_half_z * nu);
140    let d = sigma.sinhc_p::<P>();
141
142    let mut p = C::from_primal(gp + R::ONE) / (b + b);
143    let mut q = b * (R::ONE + gm) * R::HALF;
144    let mut f = sigma
145        .cosh_p::<P>()
146        .mul_sube(C::from_primal(gamma1), d * log_half_z * gamma2)
147        / c;
148
149    let f0 = f;
150    let h0 = p;
151    let mut coef = C::ONE;
152
153    let nu2 = nu * nu;
154    // `+z^2/4` where the oscillating twin has `-z^2/4`. That single sign is the difference
155    // between a series whose terms alternate and one whose terms do not.
156    let coef_mult = z * z * C::from_primal(const_splat!(ratio <E>: 1 / 4));
157    let tol = <C as FloatConsts>::EPSILON;
158
159    let mut kf = R::ONE;
160    let step = move || {
161        // One real reciprocal serves all three: `k^2 - nu^2` is `(k - nu)(k + nu)`.
162        let inv = R::ONE / kf.mul_sube(kf, nu2);
163        f = f.mul_adde(C::from_primal(kf), p + q) * inv;
164        p = p * ((kf + nu) * inv);
165        q = q * ((kf - nu) * inv);
166        let h = p - f * kf;
167        coef *= coef_mult / kf;
168        kf += R::ONE;
169
170        (coef * f, coef * h)
171    };
172
173    // Non-convergence is only reachable outside the documented domain, and the partial pair is
174    // still the best available answer there.
175    let (sum, sum1) = match sum_pair::<C, P, _>(tol, needed, (f0, h0), step) {
176        Ok(v) | Err(v) => v,
177    };
178
179    (sum, (sum1 + sum1) / z)
180}
181
182/// `$I_{\nu+1}(x)/I_\nu(x)$` by modified Lentz. Boost's `CF1_ik`.
183///
184/// The same continued fraction as [`cf1_j_ratio`](super::jy_real) with `$a_j = +1$` instead
185/// of `$-1$`, and **no sign chain**: every convergent is positive because `$I_\nu$` does not
186/// oscillate. That is one fewer piece of state than the oscillating twin needs, and is why
187/// the magnitude here arrives without a separate parity to carry.
188///
189/// # Converged lanes freeze, and `needed` is not an optimization
190///
191/// The running value is built by multiplication at the noise floor, so extra steps past
192/// convergence walk a lane off its answer, the strict form of the freeze rule in
193/// [`iterate`](thermite::math::algorithms::iterate). And the trip count grows like
194/// `$\sqrt{x}$`, so a lane bound for the asymptotic arm would otherwise set the packet's cost:
195/// 39 iterations at `$x = 40$`, 428 at 5000, and no convergence at all by `$10^6$`.
196///
197/// The `tiny` sentinel is `sqrt(MIN_POSITIVE)`, which survives being squared by a complex
198/// reciprocal, and `MIN_POSITIVE` itself would not.
199#[inline(always)]
200pub(crate) fn cf1_i_ratio<P, E, R, C>(nu: R, z: C, needed: C::Mask) -> C
201where
202    E: FloatElement,
203    R: FloatVector<Element = E>,
204    C: FloatVector<Mask = R::Mask> + PrimalProjection<Primal = R> + Mul<R, Output = C>,
205    P: Policy,
206{
207    if needed.none() {
208        return C::ZERO;
209    }
210
211    let tol = <C as FloatConsts>::EPSILON.scale(<C::Element as FloatElement>::from_int(2));
212
213    // Boost uses `sqrt(min)` rather than `min` so squaring a substituted value cannot
214    // underflow to zero further down the recurrence.
215    let tiny = C::MIN_POSITIVE.sqrt();
216    let two_over_z = C::TWO / z;
217
218    let mut c = tiny;
219    let mut f = tiny;
220    let mut d = C::ZERO;
221
222    let mut active = needed;
223    let mut kf = R::ONE;
224    let mut i = 0usize;
225
226    while i < P::POLICY.max_iterations {
227        C::_loop_hint();
228
229        let b = two_over_z * (nu + kf);
230
231        let cn = b + C::ONE / c;
232        c = cn.is_zero().select(tiny, cn);
233
234        let dn = b + d;
235        d = C::ONE / dn.is_zero().select(tiny, dn);
236
237        let delta = c * d;
238        f = f.mul_c(active, delta);
239
240        active &= (delta - C::ONE).abs().cmp_gt(tol);
241        if active.none() {
242            break;
243        }
244
245        kf += R::ONE;
246        i += 1;
247    }
248
249    f
250}
251
252/// `$(e^{x}K_\nu(x),\; e^{x}K_{\nu+1}(x))$` for `$|x| > 2$` and `$\lvert\nu\rvert \le 1/2$`.
253///
254/// Thompson and Barnett's `$z_1/z_0 = U(\nu+3/2,\,2\nu+1,\,2x)/U(\nu+1/2,\,2\nu+1,\,2x)$`
255/// (_Computer Physics Communications_ vol 47, 245). Boost's `CF2_ik`.
256///
257/// Unlike the oscillating twin's [`cf2_pq`](super::jy_real), this is **entirely real
258/// arithmetic** on the real line: no complex Lentz, no six accumulators. It carries a
259/// fraction `f` and a series `S` side by side, and the series is the slower of the two to
260/// converge, so `S` sets the stopping test.
261///
262/// # The scaled form is the native one
263///
264/// Boost writes `$K_\nu = \sqrt{\pi/2x}\;e^{-x}/S$`, with the exponential as an explicit
265/// factor rather than something the algorithm computes. Dropping it gives `$e^{x}K_\nu$` for
266/// free, which is the crate's `SCALED` convention, which is why nothing in this file's large-
267/// `$x$` path evaluates an exponential at all.
268///
269/// # Cost falls with `x`
270///
271/// 9 iterations at `$x = 100$`, 6 at 745, 4 at 5000, **2 at `$10^8$`**, at or under 1 eps
272/// throughout. So `$K$` needs no large-`$x$` asymptotic arm, unlike every other member of the
273/// family.
274#[inline(always)]
275fn cf2_ik<P, E, R, C>(nu: R, z: C, needed: C::Mask) -> (C, C)
276where
277    E: FloatElement,
278    R: FloatVector<Element = E>,
279    C: FloatVector<Mask = R::Mask>
280        + PrimalProjection<Primal = R>
281        + Mul<R, Output = C>
282        + Div<R, Output = C>
283        + Add<R, Output = C>,
284    P: Policy,
285{
286    if needed.none() {
287        return (C::ZERO, C::ZERO);
288    }
289
290    let tol = <C as FloatConsts>::EPSILON;
291
292    // The `a_k` chain depends on the order and the step alone, so it stays real.
293    let nu2m = nu.mul_sube(nu, const_splat!(ratio <E>: 1 / 4));
294    let mut a = nu2m;
295    let mut b = (z + C::ONE) * C::TWO;
296
297    let mut d = C::ONE / b;
298    let mut delta = d;
299    let mut f = d;
300
301    let mut prev = C::ZERO;
302    let mut current = C::ONE;
303    let mut cc = -a;
304    let mut qq = C::from_primal(-a);
305    let mut s = qq.mul_adde(delta, C::ONE);
306
307    let mut active = needed;
308    let mut kf = R::TWO;
309    let mut i = 0usize;
310
311    while i < P::POLICY.max_iterations {
312        C::_loop_hint();
313
314        a -= (kf - R::ONE) * R::TWO;
315        b += C::TWO;
316
317        d = C::ONE / d.mul_adde(C::from_primal(a), b);
318        delta *= b.mul_sube(d, C::ONE);
319        f = f.add_c(active, delta);
320
321        // The `q` recurrence and the series that rides on it. Boost renormalises this trio
322        // when `q` gets small. Measured, that is worth exactly nothing in binary64, see the
323        // module docs.
324        let q = (prev - (b - C::TWO) * current) / a;
325        prev = current;
326        current = q;
327        cc *= -a / kf;
328        qq = q.mul_adde(C::from_primal(cc), qq);
329        s = qq.mul_adde(delta, s);
330
331        active &= (qq * delta).abs().cmp_gt(s.abs() * tol);
332        if active.none() {
333            break;
334        }
335
336        kf += R::ONE;
337        i += 1;
338    }
339
340    // sqrt(pi/(2z)) / S. The `e^{-z}` Boost multiplies in here is exactly what SCALED drops.
341    let kv = (C::FRAC_PI_2 / z).sqrt() / s;
342    let kv1 = kv * ((z + (R::HALF + nu)) + f * nu2m) / z;
343
344    (kv, kv1)
345}
346
347/// `$(I_\nu(z), K_\nu(z))$` at **arbitrary real order**, over the positive axis (or, in a
348/// complex arithmetic, the right half-plane), scaled by `$(e^{-z}, e^{z})$` when `SCALED`.
349///
350/// The region select over the arms above. `far_threshold` is where the unscaled form switches
351/// to a halved exponential. It comes from the `BesselI` table so that both asymptotic paths in
352/// the crate use one constant.
353///
354/// # Order reduction, unlike the oscillating twin
355///
356/// `bessel_jy_real` reduces the order only in its small-`x`
357/// region, because Steed and the Hankel expansion take `$\nu$` directly. Here **both** `$K$`
358/// arms want `$\lvert u\rvert \le 1/2$`, so the split `$\nu = n + u$` and the upward walk are
359/// unconditional. That is cheap: `$K$` is the dominant solution, so the walk is its stable
360/// direction and costs exactly `$n$` steps with no trip count and no `$x$` dependence, the
361/// same shape the integer-order `$K$` kernel ships.
362///
363/// # `I` comes out of the Wronskian, which is the whole trick
364///
365/// `$I_\nu K_{\nu+1} + I_{\nu+1}K_\nu = 1/x$` with `$f = I_{\nu+1}/I_\nu$` from
366/// `cf1_i_ratio` gives `$I_\nu = (1/x)/(K_\nu f + K_{\nu+1})$`. Both `$K$` values are already
367/// in hand from the walk, so the whole first kind costs one continued fraction and a divide,
368/// **and no exponential**, because two scaled `$K$`s in the denominator make the quotient
369/// scaled too.
370///
371/// # Negative order
372///
373/// `$K_{-\nu} = K_\nu$` at every order, so `$K$` needs nothing. `$I$` does:
374/// `$I_{-\nu} = I_\nu + \tfrac{2}{\pi}\sin(\nu\pi)K_\nu$`, which in the scaled domain picks up
375/// an `$e^{-2x}$` because the two families are scaled in opposite directions. That factor is a
376/// second exponential and **must not** be recovered from an `expm1` already in hand, see the
377/// measured account on `bessel_ik_half`.
378///
379/// # Off the domain
380///
381/// Lanes outside [`BesselDetails::valid`] are kept out of every convergence mask: a NaN term
382/// never passes a tolerance test, so such a lane would otherwise hold a series open to
383/// `max_iterations` (measured 10.3 ms against 4 us per packet). The origin gets its limits
384/// (`$I_a(0) = 0$`, `$K_a(0) = +\infty$`, the reflection's signed infinity at `$-a$`) and the
385/// rest is NaN.
386#[inline(always)]
387#[allow(clippy::too_many_arguments)]
388pub fn bessel_ik_real<P, E, R, C, const NE: usize, const NO: usize, const SCALED: bool, const NEED_I: bool>(
389    nu: R,
390    z: C,
391    t: &LogGamma1p<E, NE, NO>,
392    far_threshold: E,
393) -> (C, C)
394where
395    E: FloatElement,
396    R: FloatVector<Element = E> + TranscendentalMathWithPolicy,
397    C: FloatVector<Mask = R::Mask>
398        + TranscendentalMathWithPolicy
399        + PrimalProjection<Primal = R>
400        + BesselDetails<C>
401        + Mul<R, Output = C>
402        + Div<R, Output = C>
403        + Add<R, Output = C>
404        + Sub<R, Output = C>,
405    P: Policy,
406{
407    let a = nu.abs();
408
409    // nu = n + u with n whole and |u| <= 1/2, which both K arms require.
410    let n = a.round();
411    let u = a - n;
412
413    let valid = C::valid(z);
414    let zero = z.is_zero();
415    let near = C::near(z) & valid;
416
417    // ---- K at the reduced order ----------------------------------------------------------
418    let (mut kp, mut kc) = cf2_ik::<P, E, R, C>(u, z, valid & !near);
419
420    if const { P::POLICY.avoid_branching } || near.any() {
421        let (t0, t1) = temme_ik::<P, E, R, C, NE, NO>(u, z, near, t);
422        // Temme's series is unscaled and this arm is `|z| <= 2`, so the exponential is bounded
423        // by `e^2` and cannot cost range. It is the only `exp` on the small-`z` path.
424        let e = z.exp_p::<P>();
425        kp = near.select(t0 * e, kp);
426        kc = near.select(t1 * e, kc);
427    }
428
429    // ---- K upward to the wanted order ----------------------------------------------------
430    //
431    // The seed pair starts AT the base order, so after `n` steps the answer is in `prev`,
432    // unlike `bessel_ik_half`, whose pair starts one below it.
433    let two_over_z = C::TWO / z;
434    let mut k = R::ONE;
435    let mut step = R::ONE;
436    let mut i = 0usize;
437
438    while i < P::POLICY.max_iterations {
439        let live = step.cmp_le(n);
440        if live.none() {
441            break;
442        }
443        C::_loop_hint();
444
445        let next = (two_over_z * kc).mul_adde(C::from_primal(u + k), kp);
446        kp = live.select(kc, kp);
447        kc = live.select(next, kc);
448
449        k += R::ONE;
450        step += R::ONE;
451        i += 1;
452    }
453
454    // `K_a(0) = +inf` in either scaling, and the walk above reaches it as `inf * 0`.
455    let k_a = zero.select(C::INFINITY, kp);
456    let k_a1 = zero.select(C::INFINITY, kc);
457
458    // ---- I: the Wronskian below the handover, the asymptotic series above ------------------
459    //
460    // `NEED_I` gates the expensive half: `cf2_ik` costs 2 iterations at `x = 1e8`, while
461    // `cf1_i_ratio` runs 39 at the crossover and 428 by `x = 5000`. A caller wanting only `K`
462    // (`Ai` on the positive axis) skips it explicitly rather than trusting dead-code
463    // elimination of an unused loop, the distinction Boost's `need_i` / `need_k` flags make.
464    let i_a = match const { NEED_I } {
465        false => C::ZERO,
466        true => {
467            let third: R = const_splat!(ratio <E>: 1 / 3);
468            let thresh = (a * a * third).max(const_splat!(int <E>: 40));
469            let use_asym = C::beyond(z, thresh);
470
471            let fv = cf1_i_ratio::<P, E, R, C>(a, z, valid & !use_asym);
472            // `I_a(0) = 0` for every `a > 0` and `1` at `a = 0`, and the Wronskian is
473            // `inf / inf` there. Whole orders never reach here on the real line (they have
474            // their own kernels), but a complex instantiation routes every order this way.
475            let at_origin = a.is_zero().select(C::ONE, C::ZERO);
476            // One division: `z (K f + K_1)` cannot overflow where the quotient is finite
477            // (for `z < 1` the product is below `K`, and for `z > 1` the scaled `K` is bounded).
478            let mut i_a = zero.select(at_origin, C::ONE / (z * k_a.mul_adde(fv, k_a1)));
479
480            if const { P::POLICY.avoid_branching } || use_asym.any() {
481                let far = C::exp_far(z, R::splat(far_threshold));
482                i_a = use_asym.select(super::ik::asymptotic_series_g::<P, E, R, C, true>(z, a, far), i_a);
483            }
484
485            // Negative order. `K` is even and needs nothing. `I` picks up a `K` term, and in
486            // the scaled domain an `e^{-2z}` with it.
487            let reflected = nu.is_negative();
488            if const { P::POLICY.avoid_branching } || reflected.any() {
489                let sp = a.sin_pi_p::<P>();
490                let refl = i_a + (k_a * (R::FRAC_2_PI * sp)) * (-(z + z)).exp_p::<P>();
491                i_a = reflected.select(refl, i_a);
492
493                // At the origin the reflection is `0 + inf * sin(a pi)`: a signed infinity at
494                // non-integer order, and `I_{-n}(0) = I_n(0)` where the sine vanishes. The
495                // formula above reaches it as `inf * 0` in one component.
496                let origin = at_origin + C::from_primal(R::INFINITY.copysign(sp).nz(sp.is_zero()));
497                i_a = (zero & reflected).select(origin, i_a);
498            }
499
500            i_a
501        }
502    };
503
504    let (i_out, k_out) = match const { SCALED } {
505        true => (i_a, k_a),
506        false => {
507            let i_out = match const { NEED_I } {
508                false => i_a,
509                true => {
510                    let far = C::exp_far(z, R::splat(far_threshold));
511                    super::ik::unscale_i_pair_masked::<P, C>(i_a, i_a, z, far).0
512                }
513            };
514
515            (i_out, k_a * (-z).exp_p::<P>())
516        }
517    };
518
519    // Off the domain and not at the origin: undefined, as for the integer `K`.
520    let bad = !valid & !zero;
521    (bad.select(C::NAN, i_out), bad.select(C::NAN, k_out))
522}
Last built: 2026-09-08 21:35:55 UTC