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}