treams_core/special/
ferrers.rs

1//! Real-degree Ferrers functions of the first kind with integer order.
2//!
3//! Upstream: `treams.special.lpmv` at a non-integer degree and a real argument, which
4//! calls scipy's `lpmv`.
5//!
6//! [`ferrers_real_degree`] evaluates `P_ν^m(x)` and `dP_ν^m/dx` for a non-integer degree
7//! `0 < ν < 128`, an integer order `|m| <= 130` and `-1 < x <= 1`, with the conventions of
8//! NIST DLMF §14.3 (Ferrers functions "on the cut", Condon–Shortley phase included for
9//! `m > 0`). It implements the DLMF formulas cited below.
10//!
11//! # Method
12//!
13//! Write `μ = |m|`, `n = ⌊ν⌋`, `f = ν - n`, `y = |x|`, `w = 1 - y² = sin²θ` and
14//! `t = s = (1 - y)/2`. The functions are computed for the order `-μ`; positive orders follow
15//! from DLMF 14.9.3, `P_ν^μ = (-1)^μ Γ(ν+μ+1)/Γ(ν-μ+1) P_ν^{-μ}`, with the gamma ratio formed as
16//! `∏_{i=1}^{μ} (ν(ν + 1) - i(i - 1))` from an exactly split `ν(ν + 1)`.
17//!
18//! * **About `x = 1`** (`x >= 0`). DLMF 14.3.1 with Euler's transformation (DLMF 15.8.1) gives
19//!   `P_ν^{-μ}(y) = w^{μ/2}/(2^μ μ!) F(μ - ν, μ + ν + 1; μ + 1; t)`. The series is summed at a
20//!   start degree `ν₀ ≡ ν (mod 1)`, `μ <= ν₀ <= ν`, as large as its alternating leading terms
21//!   allow, and carried to `ν` by the degree recurrence DLMF 14.10.3 written for `P_ν` and
22//!   `D_ν = P_{ν-1} - yP_ν`: `P_{ν+1} = yP_ν - c_ν D_ν`, `D_{ν+1} = wP_ν + y c_ν D_ν`,
23//!   `c_ν = (ν - μ)/(ν + μ + 1)`. This form never subtracts nearly equal values near `y = 1`,
24//!   and increasing the degree is stable: the regular solution dominates where
25//!   `(ν + 1/2) sinθ < μ`, and both solutions oscillate elsewhere. `D_ν₀` comes from the same
26//!   series; the derivative is DLMF 14.10.5, `w P' = (ν - μ) D_ν - μ y P_ν`.
27//! * **About `x = 0`** (small degrees, `(ν + 1/2)|x|` small). `G = (1 - x²)^{-μ/2} P_ν^{-μ}`
28//!   solves `(1 - x²) G'' - 2(μ + 1) x G' + (ν - μ)(ν + μ + 1) G = 0` (from DLMF 14.2.2), so its
29//!   Taylor coefficients satisfy a two-term recurrence started from the values at zero,
30//!   DLMF 14.5.1 and 14.5.2, with `1/Γ` from a Taylor polynomial and DLMF 5.5.1.
31//! * **About `x = -1`** (`x < 0`). The connection formula DLMF 14.9.10,
32//!   `P_ν^{-μ}(-y) = (-1)^μ [cos(νπ) P_ν^{-μ}(y) + sin(νπ) S_ν^{-μ}(y)]` with `S = -(2/π) Q`,
33//!   separates the part regular at `x = -1` from the singular one. `sin(νπ)` and `cos(νπ)` come
34//!   from the exactly reduced degree `ν - round(ν)`, so degrees within an ulp of an integer keep
35//!   their exact offset. Both parts follow from the degenerate (logarithmic) case `c = a + b + μ`
36//!   of the `1 - z` connection formulas of DLMF §15.8 applied to DLMF 14.3.1: in powers of `s`
37//!   with digamma coefficients, plus a finite sum of `μ` terms for `μ >= 1`. The digamma
38//!   reflection DLMF 5.5.4 turns `ψ` at non-positive arguments into `π cot(νπ)`, whose pole is
39//!   cancelled analytically against the vanishing factor `n - ν` of the series coefficients. The
40//!   order-zero expansion gives `Q_ν(y) = ½ Σ_k (ν+1)_k (-ν)_k/(k!)² [2ψ(k+1) - ψ(ν+k+1) -
41//!   ψ(ν-k+1) - ln s] s^k`. These expansions are summed directly when `(ν + 1/2) θ` is small.
42//!   Otherwise the order-zero pair is summed at a lower degree and carried to `ν` by the
43//!   recurrence above; the regular part of order `-μ` comes from the series about `x = 1`, and
44//!   the singular part from `S^1 = -w S^0'/sinθ` and the order recurrence DLMF 14.10.1,
45//!   `S^{k+1} = -2k cotθ S^k - (ν - k + 1)(ν + k) S^{k-1}`, which is stable upwards because
46//!   `Q_ν^μ(y)` dominates `P_ν^μ(y)` as `μ` grows for `y > 0`; its derivative uses
47//!   `w S^μ' = (ν + μ)(ν - μ + 1) sinθ S^{μ-1} + μ y S^μ` (§14.10).
48//! * The digamma function uses DLMF 5.5.2 and the asymptotic expansion DLMF 5.11.2.
49//! * Quantities that can leave the double range carry a separate binary exponent, so only a final
50//!   value or derivative beyond the double range is reported as an error. Sums that share the
51//!   low-order bits of `f` are formed without rounding them as a whole, so their rounding errors
52//!   do not accumulate systematically. At `x = 1` the exact limits are returned.
53use std::f64::consts::PI;
54
55use crate::{Error, MAX_DEGREE, Result, numerics::EULER};
56
57// 128 is MAX_DEGREE and 130 is MAX_REAL_DEGREE_ORDER.
58const INVALID: &str = "noninteger real degree in (0,128], |order|<=130 and -1<x<=1 required";
59const NONFINITE: &str = "non-finite real-degree Legendre result or derivative";
60
61/// The largest `|m|` of [`ferrers_real_degree`], two above its largest degree. Orders
62/// above the degree are valid; the angular functions pass `|m| <= degree`.
63const MAX_REAL_DEGREE_ORDER: i32 = MAX_DEGREE + 2;
64
65/// Relative truncation tolerance for the series (`2^-54`).
66const TOLERANCE: f64 = f64::EPSILON / 4.0;
67/// Safety bound on series terms; the series converge geometrically for `t, s <= 1/2`.
68const MAX_TERMS: u32 = 20_000;
69/// Rescaling threshold `2^512` for recurrences that could leave the double range.
70const BIG: f64 = 1.340_780_792_994_259_7e154;
71const BIG_EXPONENT: i32 = 512;
72/// Largest leading-ratio size `(ν₀ - μ)(ν₀ + μ + 1) t / (μ + 1)` of a regular start series.
73const REGULAR_REACH: f64 = 0.5;
74/// Largest `2 (ν₀ + 1/2) √s ≈ (ν₀ + 1/2) θ` at which the logarithmic series is summed.
75const SINGULAR_REACH: f64 = 3.0;
76const SQRT_PI: f64 = 1.772_453_850_905_516;
77/// The expansion about `x = 0` is used for `(ν + 1/2)|x| <= CENTRAL_REACH` (well conditioned)
78/// and `|x| <=` [`CENTRAL_LIMIT`] (or [`CENTRAL_LIMIT_POSITIVE`] for `x > 0`, where the
79/// series about `x = 1` is cheaper), for degrees and orders that keep `1/Γ` cheap.
80const CENTRAL_REACH: f64 = 2.5;
81const CENTRAL_LIMIT: f64 = 0.5;
82const CENTRAL_LIMIT_POSITIVE: f64 = 0.25;
83const CENTRAL_DEGREE: f64 = 16.0;
84const CENTRAL_ORDER: i32 = 20;
85
86/// Correctly rounded `1/k!` for `k = 0..=130` ([`MAX_REAL_DEGREE_ORDER`]).
87const INV_FACTORIAL: [f64; 131] = [
88    1.0e0,
89    1.0e0,
90    5.0e-1,
91    1.666_666_666_666_666_6e-1,
92    4.166_666_666_666_666_4e-2,
93    8.333_333_333_333_333e-3,
94    1.388_888_888_888_889e-3,
95    1.984_126_984_126_984e-4,
96    2.480_158_730_158_73e-5,
97    2.755_731_922_398_589_3e-6,
98    2.755_731_922_398_589e-7,
99    2.505_210_838_544_172e-8,
100    2.087_675_698_786_81e-9,
101    1.605_904_383_682_161_3e-10,
102    1.147_074_559_772_972_5e-11,
103    7.647_163_731_819_816e-13,
104    4.779_477_332_387_385e-14,
105    2.811_457_254_345_520_6e-15,
106    1.561_920_696_858_622_5e-16,
107    8.220_635_246_624_33e-18,
108    4.110_317_623_312_165e-19,
109    1.957_294_106_339_126_3e-20,
110    8.896_791_392_450_574e-22,
111    3.868_170_170_630_684e-23,
112    1.611_737_571_096_118_4e-24,
113    6.446_950_284_384_474e-26,
114    2.479_596_263_224_797_6e-27,
115    9.183_689_863_795_546e-29,
116    3.279_889_237_069_838e-30,
117    1.130_996_288_644_771_6e-31,
118    3.769_987_628_815_905_4e-33,
119    1.216_125_041_553_518e-34,
120    3.800_390_754_854_743_4e-36,
121    1.151_633_562_077_195e-37,
122    3.387_157_535_521_162e-39,
123    9.677_592_958_631_89e-41,
124    2.688_220_266_286_636_3e-42,
125    7.265_460_179_153_071e-44,
126    1.911_963_205_040_282e-45,
127    4.902_469_756_513_544e-47,
128    1.225_617_439_128_385_8e-48,
129    2.989_310_827_142_404_6e-50,
130    7.117_406_731_291_439e-52,
131    1.655_210_867_742_195_1e-53,
132    3.761_842_881_232_261_6e-55,
133    8.359_650_847_182_804e-57,
134    1.817_315_401_561_479e-58,
135    3.866_628_513_960_594e-60,
136    8.055_476_070_751_236e-62,
137    1.643_974_708_316_579e-63,
138    3.287_949_416_633_158e-65,
139    6.446_959_640_457_172e-67,
140    1.239_799_930_857_148_6e-68,
141    2.339_245_152_560_657_6e-70,
142    4.331_935_467_704_922e-72,
143    7.876_246_304_918_039e-74,
144    1.406_472_554_449_649_8e-75,
145    2.467_495_709_560_789_3e-77,
146    4.254_302_947_518_602e-79,
147    7.210_682_961_895_936_5e-81,
148    1.201_780_493_649_322_6e-82,
149    1.970_131_956_802_168_2e-84,
150    3.177_632_188_390_594_2e-86,
151    5.043_860_616_493_007e-88,
152    7.881_032_213_270_323e-90,
153    1.212_466_494_349_280_4e-91,
154    1.837_070_445_983_758_1e-93,
155    2.741_896_188_035_46e-95,
156    4.032_200_276_522_735_3e-97,
157    5.843_768_516_699_616e-99,
158    8.348_240_738_142_31e-101,
159    1.175_808_554_667_930_8e-102,
160    1.633_067_437_038_793e-104,
161    2.237_078_680_875_058_7e-106,
162    3.023_079_298_479_809e-108,
163    4.030_772_397_973_079e-110,
164    5.303_647_892_069_84e-112,
165    6.887_854_405_285_506e-114,
166    8.830_582_570_878_855e-116,
167    1.117_795_262_136_564e-117,
168    1.397_244_077_670_705e-119,
169    1.724_992_688_482_351_7e-121,
170    2.103_649_620_100_429e-123,
171    2.534_517_614_578_83e-125,
172    3.017_282_874_498_607_3e-127,
173    3.549_744_558_233_655_4e-129,
174    4.127_609_951_434_483e-131,
175    4.744_379_254_522_394_6e-133,
176    5.391_340_061_957_266_6e-135,
177    6.057_685_462_873_333e-137,
178    6.730_761_625_414_815e-139,
179    7.396_441_346_609_687e-141,
180    8.039_610_159_358_355e-143,
181    8.644_742_106_836_94e-145,
182    9.196_534_156_209_511e-147,
183    9.680_562_269_694_223e-149,
184    1.008_391_903_093_148_2e-150,
185    1.039_579_281_539_328e-152,
186    1.060_795_185_244_212_2e-154,
187    1.071_510_288_125_467e-156,
188    1.071_510_288_125_467e-158,
189    1.060_901_275_371_749_4e-160,
190    1.040_099_289_580_146_5e-162,
191    1.009_805_135_514_705_3e-164,
192    9.709_664_764_564_474e-167,
193    9.247_299_775_775_69e-169,
194    8.723_867_712_995_935e-171,
195    8.153_147_395_323_303e-173,
196    7.549_210_551_225_28e-175,
197    6.925_881_239_656_22e-177,
198    6.296_255_672_414_745e-179,
199    5.672_302_407_580_852e-181,
200    5.064_555_721_054_332e-183,
201    4.481_907_717_747_196_5e-185,
202    3.931_497_998_023_856_7e-187,
203    3.418_693_911_325_092_7e-189,
204    2.947_149_923_556_114_4e-191,
205    2.518_931_558_594_969_7e-193,
206    2.134_687_761_521_160_7e-195,
207    1.793_855_261_782_488_2e-197,
208    1.494_879_384_818_74e-199,
209    1.235_437_508_114_661_2e-201,
210    1.012_653_695_175_951_7e-203,
211    8.232_956_871_349_201e-206,
212    6.639_481_347_862_259e-208,
213    5.311_585_078_289_807e-210,
214    4.215_543_712_928_419e-212,
215    3.319_325_758_211_353e-214,
216    2.593_223_248_602_619_6e-216,
217    2.010_250_580_312_108_2e-218,
218    1.546_346_600_240_083_3e-220,
219];
220
221/// Taylor coefficients of `1/Γ(3/2 + h)` about `h = 0` (enough for `|h| <= 1/2`).
222const RGAMMA_TAYLOR: [f64; 21] = [
223    std::f64::consts::FRAC_2_SQRT_PI, // 1/Γ(3/2)
224    -4.117_452_644_528_31e-2,
225    -5.266_544_355_255_445e-1,
226    1.751_020_260_439_345_7e-1,
227    5.096_686_024_770_607_4e-2,
228    -4.215_516_936_853_560_4e-2,
229    6.612_897_826_824_127e-3,
230    2.120_731_442_572_938e-3,
231    -1.110_730_254_594_890_6e-3,
232    1.523_576_207_674_768_8e-4,
233    2.535_520_492_381_416_5e-5,
234    -1.389_680_571_791_375_6e-5,
235    2.156_203_290_514_172_4e-6,
236    5.794_264_054_052_672_6e-8,
237    -8.913_551_118_311_116e-8,
238    1.710_346_941_591_537_4e-8,
239    -9.313_686_445_241_901e-10,
240    -2.680_474_103_349_662_3e-10,
241    7.458_932_233_316_326e-11,
242    -8.012_807_061_414_718e-12,
243    -8.382_343_033_451_855e-14,
244];
245
246/// `mantissa · 2^exponent` with `1/2 <= |mantissa| < 1` (or zero): a double with a wider
247/// exponent range.
248#[derive(Clone, Copy, Debug)]
249struct Scaled {
250    mantissa: f64,
251    exponent: i32,
252}
253
254impl Scaled {
255    fn new(value: f64, exponent: i32) -> Self {
256        let (mantissa, shift) = frexp(value);
257        Self {
258            mantissa,
259            exponent: exponent + shift,
260        }
261    }
262
263    fn mul(self, other: Self) -> Self {
264        Self::new(
265            self.mantissa * other.mantissa,
266            self.exponent + other.exponent,
267        )
268    }
269
270    fn scale(self, factor: f64) -> Self {
271        Self::new(self.mantissa * factor, self.exponent)
272    }
273
274    fn add(self, other: Self) -> Self {
275        if other.mantissa == 0.0 {
276            return self;
277        }
278        if self.mantissa == 0.0 {
279            return other;
280        }
281        let (high, low) = if self.exponent >= other.exponent {
282            (self, other)
283        } else {
284            (other, self)
285        };
286        let aligned = scalbn(low.mantissa, low.exponent - high.exponent);
287        Self::new(high.mantissa + aligned, high.exponent)
288    }
289
290    fn to_f64(self) -> f64 {
291        scalbn(self.mantissa, self.exponent)
292    }
293}
294
295const EXPONENT_MASK: u64 = 0x7ff << 52;
296
297/// `value = mantissa · 2^exponent` with `1/2 <= |mantissa| < 1`; inline fast path for normal
298/// numbers, `libm` otherwise.
299#[inline]
300fn frexp(value: f64) -> (f64, i32) {
301    let bits = value.to_bits();
302    let biased = (bits & EXPONENT_MASK) >> 52;
303    if biased == 0 || biased == 0x7ff {
304        return libm::frexp(value);
305    }
306    let mantissa = f64::from_bits((bits & !EXPONENT_MASK) | (0x3fe << 52));
307    // biased < 2^11, so the conversion is exact.
308    (
309        mantissa,
310        i32::from(u16::try_from(biased).unwrap_or(0)) - 1022,
311    )
312}
313
314/// `value · 2^exponent`, correctly rounded; inline fast path when `2^exponent` is a normal number
315/// and `value` is normalised as in [`Scaled`].
316#[inline]
317fn scalbn(value: f64, exponent: i32) -> f64 {
318    if (-1000..=1000).contains(&exponent) {
319        let power = f64::from_bits(u64::from((exponent + 1023).unsigned_abs()) << 52);
320        let result = value * power;
321        if result == 0.0 || result.abs() >= f64::MIN_POSITIVE {
322            return result;
323        }
324    }
325    libm::scalbn(value, exponent)
326}
327
328fn nonfinite() -> Error {
329    Error::SpecialFunction(NONFINITE.into())
330}
331
332/// `value` rounded towards zero, for `|value| < 2^31` (larger magnitudes saturate). Callers use
333/// it on validated degrees and small shifts; unlike `f64::floor` it needs no library call.
334#[allow(clippy::cast_possible_truncation)] // Truncation is the intent; the range is bounded.
335fn small_integer(value: f64) -> i32 {
336    value as i32
337}
338
339/// `⌊value⌋` for `0 <= value < 2^31`.
340fn floor_small(value: f64) -> f64 {
341    f64::from(small_integer(value))
342}
343
344/// The integer nearest to `0 <= value < 2^31` (halves rounded up).
345fn nearest_integer(value: f64) -> i32 {
346    let whole = small_integer(value);
347    if value - f64::from(whole) >= 0.5 {
348        whole + 1
349    } else {
350        whole
351    }
352}
353
354/// Real-degree Ferrers function `P_v^m(x)` (degree `v` not an integer) and, with
355/// `DERIVATIVE`, its analytic derivative in `x`; without it the second element is `0.0`.
356///
357/// Upstream: `treams.special.lpmv(m, v, x)` at a non-integer degree. Differences:
358/// degrees above 128, orders above 130 and `x` outside `(-1, 1]` give
359/// [`Error::InvalidInput`], and results beyond the double range give
360/// [`Error::SpecialFunction`], as does the infinite derivative at `x = 1` for `|m| = 1`.
361pub(crate) fn ferrers_real_degree<const DERIVATIVE: bool>(
362    v: f64,
363    m: i32,
364    x: f64,
365) -> Result<(f64, f64)> {
366    if !(v > 0.0 && v < f64::from(MAX_DEGREE))
367        || v - floor_small(v) == 0.0
368        || m.unsigned_abs() > MAX_REAL_DEGREE_ORDER.unsigned_abs()
369        || !(x > -1.0 && x <= 1.0)
370    {
371        return Err(Error::InvalidInput(INVALID.into()));
372    }
373    if x >= 1.0 {
374        return endpoint::<DERIVATIVE>(v, m);
375    }
376    let order = m.abs();
377    let y = x.abs();
378    let central_limit = if x > 0.0 {
379        CENTRAL_LIMIT_POSITIVE
380    } else {
381        CENTRAL_LIMIT
382    };
383    let (value, slope) = if v < CENTRAL_DEGREE
384        && order <= CENTRAL_ORDER
385        && y <= central_limit
386        && (v + 0.5) * y <= CENTRAL_REACH
387    {
388        central::<DERIVATIVE>(v, order, x)?
389    } else if x >= 0.0 {
390        regular(v, order, y)?
391    } else {
392        return reflected::<DERIVATIVE>(v, m, y);
393    };
394    if m > 0 {
395        let ratio = gamma_ratio(v, order, None).scale(if order % 2 == 1 { -1.0 } else { 1.0 });
396        let slope = if DERIVATIVE { slope.mul(ratio) } else { slope };
397        return finish::<DERIVATIVE>(value.mul(ratio), slope, y);
398    }
399    finish::<DERIVATIVE>(value, slope, y)
400}
401
402/// Pairs of arguments a few ulps apart, one on each side of a switch between the
403/// evaluation routes of [`ferrers_real_degree`] at degree `v` and order `m`: the edges of
404/// the expansion about `x = 0`, or `x = 0` between the series about `x = 1` and the
405/// connection formula, and the argument where `(ν + 1/2) θ` reaches [`SINGULAR_REACH`],
406/// beyond which the logarithmic series is summed at a lower degree.
407#[cfg(test)]
408pub(crate) fn ferrers_route_switches(v: f64, m: i32) -> Vec<[f64; 2]> {
409    let around = |x: f64| {
410        [
411            x * (1.0 - 4.0 * f64::EPSILON),
412            x * (1.0 + 4.0 * f64::EPSILON),
413        ]
414    };
415    let mut switches = Vec::new();
416    if v < CENTRAL_DEGREE && m.abs() <= CENTRAL_ORDER {
417        let reach = CENTRAL_REACH / (v + 0.5);
418        switches.push(around(CENTRAL_LIMIT_POSITIVE.min(reach)));
419        switches.push(around(-CENTRAL_LIMIT.min(reach)));
420    } else {
421        switches.push([0.0, -f64::MIN_POSITIVE]);
422    }
423    let s = (0.5 * SINGULAR_REACH / (v + 0.5)).powi(2);
424    if s < 0.5 {
425        switches.push(around(2.0f64.mul_add(s, -1.0)));
426    }
427    switches
428}
429
430/// `P_ν^m(-y)` for `0 < y < 1` by the connection formula DLMF 14.9.10.
431fn reflected<const DERIVATIVE: bool>(v: f64, m: i32, y: f64) -> Result<(f64, f64)> {
432    let order = m.abs();
433    let odd = order % 2 == 1;
434    let (sin, cos) = sin_cos_pi(v);
435    // f π cot(πν) with f = ν - ⌊ν⌋, finite as f → 0.
436    let frac = v - floor_small(v);
437    let frac_cot = if frac < 1e-4 {
438        pi_cot_scaled(frac)
439    } else {
440        frac * PI * cos / sin
441    };
442    if order > 0
443        && f64::from(order) <= floor_small(v)
444        && v <= SINGULAR_REACH / (2.0 * (0.5 * (1.0 - y)).sqrt()) - 0.5
445    {
446        return order_series::<DERIVATIVE>(v, m, y, sin, cos, frac_cot);
447    }
448    let slopes = match order {
449        _ if DERIVATIVE => Slopes::Both,
450        0 => Slopes::None,
451        1 => Slopes::Both,
452        _ => Slopes::Singular,
453    };
454    let zero = order_zero(v, y, frac_cot, slopes)?;
455    let ((regular, regular_slope), (singular, singular_slope)) = match order {
456        0 => (
457            (
458                Scaled::new(zero.regular, 0),
459                Scaled::new(zero.regular_slope, 0),
460            ),
461            (
462                Scaled::new(zero.singular, 0),
463                Scaled::new(zero.singular_slope, 0),
464            ),
465        ),
466        // DLMF 14.10 order steps from order zero: w P' = ν(ν+1) sinθ P^{-1} and
467        // w (P^{-1})' = y P^{-1} - sinθ P.
468        1 if v > 1.0 => {
469            let (w, w_low) = one_minus_square(y);
470            let sine = w.sqrt();
471            let sine = sine + 0.5 * w_low / sine;
472            let lowered = zero.regular_slope / (v * (v + 1.0) * sine);
473            (
474                (
475                    Scaled::new(lowered, 0),
476                    Scaled::new(y * lowered - sine * zero.regular, 0),
477                ),
478                raise_singular(v, order, y, &zero),
479            )
480        }
481        _ => (regular(v, order, y)?, raise_singular(v, order, y, &zero)),
482    };
483    let (first, second) = if m > 0 {
484        (
485            gamma_ratio(v, order, None).scale(cos),
486            Scaled::new(if odd { -sin } else { sin }, 0),
487        )
488    } else {
489        (
490            Scaled::new(if odd { -cos } else { cos }, 0),
491            sine_over_gamma_ratio(v, order, sin),
492        )
493    };
494    let value = regular.mul(first).add(singular.mul(second));
495    let slope = if DERIVATIVE {
496        regular_slope
497            .mul(first)
498            .add(singular_slope.mul(second))
499            .scale(-1.0)
500    } else {
501        value
502    };
503    finish::<DERIVATIVE>(value, slope, y)
504}
505
506/// `P_ν^m(-y)` for `0 < y < 1`, `1 <= μ = |m| <= ⌊ν⌋`, from the expansion about `x = -1` in
507/// `s = (1 - y)/2`: the degenerate case `c - a - b = μ` of the `1 - z` connection formulas
508/// (DLMF §15.8) applied to DLMF 14.3.1, after the digamma reflection DLMF 5.5.4,
509/// `P_ν^{-μ}(-y) = (-1)^μ cos(νπ) R + sin(νπ) (L + F/G)` with `G = Γ(ν+μ+1)/Γ(ν-μ+1)`,
510/// `R = (w/4)^{μ/2}/μ! Σ r_k` (that is `P_ν^{-μ}(y)`),
511/// `L = (-1)^μ (w/4)^{μ/2}/(π μ!) Σ r_k [ln s - ψ(k+1) - ψ(k+μ+1) + ψ(ν+μ+k+1) + ψ(ν-μ-k+1)]`,
512/// `F = (-1)^{μ+1} (μ-1)!/π (w/4)^{μ/2} s^{-μ} Σ_{k<μ} (ν+1)_k (-ν)_k s^k/(k! (1-μ)_k)` and
513/// `r_k = (ν+μ+1)_k (μ-ν)_k s^k/((μ+1)_k k!)`. As in [`log_series`], the pole of
514/// `ψ(ν - μ - k + 1)` is cancelled against the factor `n - ν` of `r_k` (`n = ⌊ν⌋`).
515fn order_series<const DERIVATIVE: bool>(
516    v: f64,
517    m: i32,
518    y: f64,
519    sin: f64,
520    cos: f64,
521    frac_cot: f64,
522) -> Result<(f64, f64)> {
523    let order = m.abs();
524    let mu = f64::from(order);
525    let s = 0.5 * (1.0 - y);
526    let (w, w_low) = one_minus_square(y);
527    // Finite sum Σ_{k<μ} f_k and Σ k f_k.
528    let (mut finite, mut finite_slope, mut term) = (1.0, 0.0, 1.0);
529    for k in 1..order {
530        let k = f64::from(k);
531        term *= (v + k) * (k - 1.0 - v) * s / (k * (k - mu));
532        finite += term;
533        finite_slope += k * term;
534    }
535    // Logarithmic part: Σ r_k, Σ k r_k, Σ r_k g_k, Σ (k r_k g_k + r_k).
536    let sums = order_log_sums::<DERIVATIVE>(v, order, s, frac_cot)?;
537    let (regular, regular_slope, log, log_slope) =
538        (sums.regular, sums.regular_slope, sums.log, sums.log_slope);
539    // Assemble with (w/4)^{μ/2}, 1/μ!, (μ-1)!/π, s^{-μ} and the gamma ratio as scaled numbers.
540    let inverse_factorial = usize::try_from(order)
541        .ok()
542        .and_then(|index| INV_FACTORIAL.get(index).zip(INV_FACTORIAL.get(index - 1)))
543        .ok_or_else(nonfinite)?;
544    let base = sine_power(w, w_low, order).mul(Scaled::new(*inverse_factorial.0, -order));
545    let odd = order % 2 == 1;
546    let sign = if odd { -1.0 } else { 1.0 };
547    let regular_part = base;
548    let log_part = base.scale(sign / PI);
549    let (s_mantissa, s_exponent) = frexp(s);
550    // (w/4)^{μ/2} (μ - 1)! = base μ! (μ - 1)!.
551    let finite_part = base
552        .mul(Scaled::new(-sign / (PI * *inverse_factorial.0), 0))
553        .mul(Scaled::new(1.0 / *inverse_factorial.1, 0))
554        .mul(Scaled::new(s_mantissa.powi(-order), -order * s_exponent));
555    let ratio = gamma_ratio(v, order, None);
556    let (regular_part, log_part, finite_part) = if m > 0 {
557        (
558            regular_part.mul(ratio).scale(cos),
559            log_part.mul(ratio).scale(sign * sin),
560            finite_part.scale(sign * sin),
561        )
562    } else {
563        let inverse = Scaled::new(1.0 / ratio.mantissa, -ratio.exponent);
564        (
565            regular_part.scale(sign * cos),
566            log_part.scale(sin),
567            finite_part.mul(inverse).scale(sin),
568        )
569    };
570    let value = regular_part
571        .scale(regular)
572        .add(log_part.scale(log))
573        .add(finite_part.scale(finite));
574    if !DERIVATIVE {
575        return finish::<false>(value, value, y);
576    }
577    // (1 - x²) d/dx = 2 s (1 - s) d/ds.
578    let tilt = mu * (1.0 - 2.0 * s);
579    let slope = regular_part
580        .scale(tilt * regular + 2.0 * (1.0 - s) * regular_slope)
581        .add(log_part.scale(tilt * log + 2.0 * (1.0 - s) * log_slope))
582        .add(finite_part.scale(2.0 * (1.0 - s) * finite_slope - mu * finite));
583    finish::<DERIVATIVE>(value, slope, y)
584}
585
586/// Sums of [`order_series`]: `Σ r_k`, `Σ k r_k`, `Σ r_k g_k` and `Σ (k r_k g_k + r_k)`.
587#[derive(Clone, Copy, Debug, Default)]
588struct OrderSums {
589    regular: f64,
590    regular_slope: f64,
591    log: f64,
592    log_slope: f64,
593    values: Convergence,
594    slopes: Convergence,
595}
596
597impl OrderSums {
598    /// Adds the term `k` (`term = r_k g_k`, `plain = r_k`); returns true once converged, given
599    /// that later weight ratios stay below `ratio()`. Values are frozen once converged.
600    #[inline]
601    fn add<const SLOPE: bool>(
602        &mut self,
603        k: f64,
604        term: f64,
605        plain: f64,
606        ratio: impl Fn() -> f64,
607    ) -> bool {
608        if !self.values.done {
609            self.regular += plain;
610            self.log += term;
611            self.values.update(term.abs() + plain.abs(), &ratio);
612        }
613        if SLOPE {
614            let slope_term = k * term + plain;
615            self.regular_slope += k * plain;
616            self.log_slope += slope_term;
617            self.slopes
618                .update(slope_term.abs() + k * plain.abs(), &ratio);
619            return self.values.done && self.slopes.done;
620        }
621        self.values.done
622    }
623}
624
625/// The logarithmic sums of [`order_series`] with `n - μ = pivot`, in three phases like
626/// [`log_series`]: direct terms for `k <= n - μ`, the cancelled pole at `k = n - μ + 1`, and the
627/// reflected digamma beyond. One division per term serves all reciprocals.
628fn order_log_sums<const SLOPE: bool>(
629    v: f64,
630    order: i32,
631    s: f64,
632    frac_cot: f64,
633) -> Result<OrderSums> {
634    let mu = f64::from(order);
635    let whole = floor_small(v);
636    let frac = v - whole;
637    let pivot = whole - mu;
638    let ratio_after = |k: f64| {
639        let bound = (v + mu + k + 1.0) * ((mu - v + k).abs() + 1.0) / ((mu + k + 1.0) * (k + 1.0));
640        s * bound.max(1.0) * (1.0 + 2.0 / (k + 1.0))
641    };
642    let mut sums = OrderSums::default();
643    // bracket = ln s - ψ(k+1) - ψ(k+μ+1) + ψ(ν+μ+k+1) + ψ(ν-μ-k+1).
644    // For low orders ψ(ν+μ+1) = ψ(ν-μ+1) + Σ_{i=1}^{2μ} 1/(ν-μ+i) and ψ(μ+1) = H_μ - γ
645    // (DLMF 5.5.2) are cheaper than further digamma evaluations.
646    let psi_low = digamma(1.0 + v - mu);
647    let (psi_high, psi_order) = if mu <= 4.0 {
648        (
649            psi_low + shifted_sum(v - mu, 2 * order),
650            shifted_sum(0.0, order) - EULER,
651        )
652    } else {
653        (digamma(v + mu + 1.0), digamma(mu + 1.0))
654    };
655    let mut bracket = s.ln() + EULER - psi_order + psi_high + psi_low;
656    let mut weight = 1.0; // r_k
657    let mut k = 0.0;
658    let mut count = 0;
659    loop {
660        if sums.add::<SLOPE>(k, weight * bracket, weight, || ratio_after(k)) {
661            return Ok(sums);
662        }
663        if k >= pivot {
664            break;
665        }
666        // Step to k + 1, with ψ(ν-μ-k+1) decreasing by 1/(ν-μ-k).
667        let next = k + 1.0;
668        let (upper, shifted, gap) = (v + mu + next, mu + next, v - mu - k);
669        let (upper_next, shifted_gap) = (upper * next, shifted * gap);
670        let reciprocal = 1.0 / (upper_next * shifted_gap);
671        weight *= (mu - v + k) * upper * upper * gap * s * reciprocal;
672        bracket +=
673            (next * shifted_gap - upper * shifted_gap - upper_next * gap - upper_next * shifted)
674                * reciprocal;
675        k = next;
676        count += 1;
677        if count > MAX_TERMS {
678            return Err(nonfinite());
679        }
680    }
681    // k = n - μ + 1: r_k = -f r̂_k; f ψ(f) = f ψ(1 + f) - 1, where ψ(1 + f) is the last ψ(ν-μ-k+1).
682    let next = k + 1.0;
683    let (upper, shifted) = (v + mu + next, mu + next);
684    weight *= upper * s / (shifted * next);
685    bracket += 1.0 / upper - 1.0 / next - 1.0 / shifted;
686    k = next;
687    // Here the bracket holds ψ(1 + f) + the other digammas; split ψ(1 + f) off again for f ψ(f).
688    if sums.add::<SLOPE>(k, weight * (1.0 - frac * bracket), -frac * weight, || {
689        ratio_after(k)
690    }) {
691        return Ok(sums);
692    }
693    // k >= n - μ + 2: ψ(ν-μ-k+1) = ψ(k+μ-ν) - π cot(πν), starting from ψ(2 - f).
694    let next = k + 1.0;
695    let (upper, shifted) = (v + mu + next, mu + next);
696    weight *= (mu - v + k) * upper * s / (shifted * next);
697    bracket += 1.0 / upper - 1.0 / next - 1.0 / shifted + digamma_reflection_step(frac, frac_cot);
698    k = next;
699    loop {
700        if sums.add::<SLOPE>(
701            k,
702            weight * (frac_cot - frac * bracket),
703            -frac * weight,
704            || ratio_after(k),
705        ) {
706            return Ok(sums);
707        }
708        let next = k + 1.0;
709        let (upper, shifted, gap) = (v + mu + next, mu + next, k + mu - v);
710        let (upper_next, shifted_gap) = (upper * next, shifted * gap);
711        let reciprocal = 1.0 / (upper_next * shifted_gap);
712        weight *= (mu - v + k) * upper * upper * gap * s * reciprocal;
713        bracket += (next * shifted_gap - upper * shifted_gap - upper_next * gap
714            + upper_next * shifted)
715            * reciprocal;
716        k = next;
717        count += 1;
718        if count > MAX_TERMS {
719            return Err(nonfinite());
720        }
721    }
722}
723
724/// Converts `(P, (1 - x²) P')` to doubles, rejecting results beyond the double range.
725fn finish<const DERIVATIVE: bool>(value: Scaled, slope: Scaled, y: f64) -> Result<(f64, f64)> {
726    let value = value.to_f64();
727    if !value.is_finite() {
728        return Err(nonfinite());
729    }
730    if !DERIVATIVE {
731        return Ok((value, 0.0));
732    }
733    let (w, _) = one_minus_square(y);
734    let derivative = Scaled::new(slope.mantissa / w, slope.exponent).to_f64();
735    if !derivative.is_finite() {
736        return Err(nonfinite());
737    }
738    Ok((value, derivative))
739}
740
741/// Exact limits at `x = 1`: `P_ν^m(1) = δ_{m0}`; the slope is `ν(ν+1)/2` (`m = 0`), infinite
742/// (`|m| = 1`), `-Γ(ν+3)/(4Γ(ν-1))` (`m = 2`), `-1/4` (`m = -2`) and zero otherwise.
743fn endpoint<const DERIVATIVE: bool>(v: f64, m: i32) -> Result<(f64, f64)> {
744    let value = if m == 0 { 1.0 } else { 0.0 };
745    if !DERIVATIVE {
746        return Ok((value, 0.0));
747    }
748    let derivative = match m {
749        0 => 0.5 * v * (v + 1.0),
750        1 | -1 => return Err(nonfinite()),
751        2 => -0.25 * gamma_ratio(v, 2, None).to_f64(),
752        -2 => -0.25,
753        _ => 0.0,
754    };
755    Ok((value, derivative))
756}
757
758/// `(P_ν^{-μ}(y), (1 - y²) dP_ν^{-μ}/dy)` for `0 <= y < 1`.
759fn regular(v: f64, order: i32, y: f64) -> Result<(Scaled, Scaled)> {
760    let mu = f64::from(order);
761    let whole = floor_small(v);
762    let frac = v - whole;
763    let t = 0.5 * (1.0 - y);
764    let (w, w_low) = one_minus_square(y);
765    let start = if mu >= whole {
766        v
767    } else {
768        // Largest lift j with (j + frac)(2μ + 1 + j + frac) t <= REGULAR_REACH (μ + 1).
769        let base = 2.0 * mu + 1.0;
770        let reach = 0.5 * ((base * base + 4.0 * REGULAR_REACH * (mu + 1.0) / t).sqrt() - base);
771        let lift = floor_small((reach - frac).clamp(0.0, whole - mu));
772        (mu + lift) + frac
773    };
774    // E_k = (a + 1)_{k-1} (b)_k t^k / ((c)_k k!): P = pref (1 + a Σ E_k), D = 2(1 - t) pref Σ k E_k.
775    // b + k = (b_whole + k) + frac is never rounded as a whole: all those sums share the low bits
776    // of frac, so their rounding errors would not average out.
777    let b_whole = mu + 1.0 + (start - frac);
778    let (a, b, c) = (mu - start, b_whole + frac, mu + 1.0);
779    let mut term = (b_whole * t + frac * t) / c;
780    let (mut sum, mut weighted, mut size) = (term, term, term.abs());
781    let mut k = 1.0;
782    let mut count = 0;
783    loop {
784        let rising = a + k;
785        term *= (rising * (b_whole + k) + rising * frac) * t / ((c + k) * (k + 1.0));
786        k += 1.0;
787        sum += term;
788        let current = k * term;
789        weighted += current;
790        size += current.abs();
791        if a + k > 0.0 && current.abs() <= TOLERANCE * size {
792            // Every later ratio of k E_k is positive and below rho.
793            let rho = t * (b + k) * (k + 1.0) / ((c + k) * k);
794            if rho < 1.0 && current.abs() * rho <= TOLERANCE * size * (1.0 - rho) {
795                break;
796            }
797        }
798        count += 1;
799        if count > MAX_TERMS {
800            return Err(nonfinite());
801        }
802    }
803    let mut p = 1.0 + a * sum;
804    let mut d = 2.0 * (1.0 - t) * weighted;
805    let mut exponent = 0;
806    let mut degree = start;
807    for _ in 0..small_integer(v - start) {
808        let coupling = (degree - mu) / (degree + mu + 1.0);
809        let next = y * p - coupling * d;
810        d = w * p + (w_low * p + y * coupling * d);
811        p = next;
812        degree += 1.0;
813        if p.abs().max(d.abs()) > BIG {
814            p = scalbn(p, -BIG_EXPONENT);
815            d = scalbn(d, -BIG_EXPONENT);
816            exponent += BIG_EXPONENT;
817        }
818    }
819    let slope = (v - mu) * d - mu * y * p;
820    if order == 0 {
821        return Ok((Scaled::new(p, exponent), Scaled::new(slope, exponent)));
822    }
823    let inverse_factorial = usize::try_from(order)
824        .ok()
825        .and_then(|index| INV_FACTORIAL.get(index))
826        .ok_or_else(nonfinite)?;
827    let prefactor = sine_power(w, w_low, order).mul(Scaled::new(*inverse_factorial, -order));
828    Ok((
829        prefactor.mul(Scaled::new(p, exponent)),
830        prefactor.mul(Scaled::new(slope, exponent)),
831    ))
832}
833
834/// `(P_ν^{-μ}(x), (1 - x²) dP_ν^{-μ}/dx)` from the expansion about `x = 0`.
835///
836/// `P_ν^{-μ}(x) = (1 - x²)^{μ/2} G(x)`, where `G` solves
837/// `(1 - x²) G'' - 2(μ + 1) x G' + (ν - μ)(ν + μ + 1) G = 0`, so its Taylor coefficients obey
838/// `g_{j+2} = g_j (j + μ - ν)(j + μ + ν + 1)/((j + 1)(j + 2))`; `g_0` and `g_1` are the value
839/// and slope at zero (DLMF 14.5.1, 14.5.2). With `X = x²`, `G = E(X) + x O(X)`.
840fn central<const SLOPE: bool>(v: f64, order: i32, x: f64) -> Result<(Scaled, Scaled)> {
841    let mu = f64::from(order);
842    let scale = scalbn(SQRT_PI, -order);
843    let mut even = scale * rgamma(f64::midpoint(v, mu) + 1.0) * rgamma(f64::midpoint(1.0 - v, mu));
844    let mut odd = -2.0 * scale * rgamma(f64::midpoint(v + mu, 1.0)) * rgamma(f64::midpoint(mu, -v));
845    let square = x * x;
846    let magnitude_x = x.abs();
847    // Sums of a_i X^i, i a_i X^(i-1) (even) and b_i X^i, i b_i X^(i-1) (odd). The value and the
848    // slope converge separately (either may be tiny next to the other); the value is frozen once
849    // converged, so it does not depend on SLOPE.
850    let (mut sum_even, mut sum_odd) = (even, odd);
851    // The slope also needs the odd sum, to its own accuracy.
852    let (mut slope_even, mut slope_odd, mut slope_sum_odd) = (0.0, 0.0, odd);
853    let mut values = Convergence {
854        size: even.abs() + magnitude_x * odd.abs(),
855        ..Convergence::default()
856    };
857    let mut slopes = Convergence {
858        size: odd.abs(),
859        done: !SLOPE,
860        ..Convergence::default()
861    };
862    // Running j + μ - ν, j + μ + ν + 1 and j + 1 for the current even index j = 2(i - 1).
863    let (mut low, mut high, mut first) = (mu - v, mu + v + 1.0, 1.0);
864    let mut index = 1.0;
865    let mut count = 0;
866    while !(values.done && slopes.done) {
867        let reciprocal = 1.0 / (first * (first + 1.0) * (first + 2.0));
868        let ratio_even = low * high * (first + 2.0) * reciprocal;
869        let ratio_odd = (low + 1.0) * (high + 1.0) * first * reciprocal;
870        let partial_even = even * ratio_even;
871        let partial_odd = odd * ratio_odd;
872        even = partial_even * square;
873        odd = partial_odd * square;
874        // Later ratios (times X) stay below rho.
875        let rho = || square * ratio_even.abs().max(ratio_odd.abs()).max(1.0) * (1.0 + 2.0 / index);
876        if !values.done {
877            sum_even += even;
878            sum_odd += odd;
879            values.update(even.abs() + magnitude_x * odd.abs(), rho);
880        }
881        if !slopes.done {
882            slope_even += index * partial_even;
883            slope_odd += index * partial_odd;
884            slope_sum_odd += odd;
885            slopes.update(
886                (index + 1.0) * (2.0 * magnitude_x * partial_even.abs() + partial_odd.abs()),
887                rho,
888            );
889        }
890        low += 2.0;
891        high += 2.0;
892        first += 2.0;
893        index += 1.0;
894        count += 1;
895        if count > MAX_TERMS {
896            return Err(nonfinite());
897        }
898    }
899    let (w, w_low) = one_minus_square(x);
900    let value = sum_even + x * sum_odd;
901    let slope = if SLOPE {
902        w * (2.0 * x * slope_even + slope_sum_odd + 2.0 * square * slope_odd) - mu * x * value
903    } else {
904        0.0
905    };
906    if order == 0 {
907        return Ok((Scaled::new(value, 0), Scaled::new(slope, 0)));
908    }
909    let prefactor = sine_power(w, w_low, order);
910    Ok((
911        prefactor.mul(Scaled::new(value, 0)),
912        prefactor.mul(Scaled::new(slope, 0)),
913    ))
914}
915
916/// `1/Γ(z)` for moderate real `z`: DLMF 5.5.1 shifts into `[1, 2)`, then a Taylor polynomial.
917/// The shifts multiply by `z + k` exactly where `Γ` has poles, so those zeros are exact.
918fn rgamma(z: f64) -> f64 {
919    let truncated = small_integer(z);
920    let floor = if f64::from(truncated) > z {
921        truncated - 1
922    } else {
923        truncated
924    };
925    let (mut z, mut numerator, mut denominator) = (z, 1.0, 1.0);
926    for _ in floor..1 {
927        numerator *= z;
928        z += 1.0;
929    }
930    for _ in 2..=floor {
931        z -= 1.0;
932        denominator *= z;
933    }
934    numerator * rgamma_core(z - 1.5) / denominator
935}
936
937/// `1/Γ(3/2 + h)` for `|h| <= 1/2` by Estrin's scheme (short dependency chains).
938fn rgamma_core(h: f64) -> f64 {
939    let [
940        c0,
941        c1,
942        c2,
943        c3,
944        c4,
945        c5,
946        c6,
947        c7,
948        c8,
949        c9,
950        c10,
951        c11,
952        c12,
953        c13,
954        c14,
955        c15,
956        c16,
957        c17,
958        c18,
959        c19,
960        c20,
961    ] = RGAMMA_TAYLOR;
962    let h2 = h * h;
963    let h4 = h2 * h2;
964    let h8 = h4 * h4;
965    let quad = |a: f64, b: f64, c: f64, d: f64| (a + b * h) + (c + d * h) * h2;
966    let low = (quad(c0, c1, c2, c3) + quad(c4, c5, c6, c7) * h4)
967        + (quad(c8, c9, c10, c11) + quad(c12, c13, c14, c15) * h4) * h8;
968    let high = quad(c16, c17, c18, c19) + c20 * h4;
969    low + high * (h8 * h8)
970}
971
972/// `w^{μ/2}` for `1 - y² = w + w_low` (`0 < w <= 1`).
973fn sine_power(w: f64, w_low: f64, order: i32) -> Scaled {
974    let half = 0.5 * f64::from(order);
975    // Low orders by at most two roundings, with the first-order correction for w_low.
976    let correction = 1.0 + half * (w_low / w);
977    match order {
978        0 => return Scaled::new(1.0, 0),
979        1 => return Scaled::new(w.sqrt() * correction, 0),
980        2 => return Scaled::new(w + w_low, 0),
981        3 => return Scaled::new(w * w.sqrt() * correction, 0),
982        4 => return Scaled::new(w * w * correction, 0),
983        _ => {}
984    }
985    if w > 0.75 {
986        // w - 1 is exact here.
987        return Scaled::new((half * ((w - 1.0) + w_low).ln_1p()).exp(), 0);
988    }
989    let (mut mantissa, mut exponent) = frexp(w);
990    if exponent % 2 != 0 {
991        mantissa *= 2.0;
992        exponent -= 1;
993    }
994    Scaled::new(mantissa.powf(half) * correction, exponent / 2 * order)
995}
996
997/// Order-zero functions at `0 < y < 1`, with `w = 1 - y²`: `P_ν(y)`, `w P_ν'(y)`, and
998/// `S_ν(y) = -(2/π) Q_ν(y)` (Ferrers second kind), `w S_ν'(y)`.
999#[derive(Clone, Copy, Debug)]
1000struct OrderZero {
1001    regular: f64,
1002    regular_slope: f64,
1003    singular: f64,
1004    singular_slope: f64,
1005}
1006
1007/// Which order-zero slopes a caller needs.
1008#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1009enum Slopes {
1010    None,
1011    Singular,
1012    Both,
1013}
1014
1015/// [`OrderZero`] at degree `ν`: the logarithmic expansion at the largest degree `ν₀ ≡ ν`
1016/// (mod 1) with `(ν₀ + 1/2) θ` small, then the degree recurrence (`μ = 0`) for both solutions.
1017/// Only the requested slopes are formed, unless the recurrence needs both.
1018fn order_zero(v: f64, y: f64, frac_cot: f64, slopes: Slopes) -> Result<OrderZero> {
1019    let s = 0.5 * (1.0 - y);
1020    let frac = v - floor_small(v);
1021    let reach = SINGULAR_REACH / (2.0 * s.sqrt()) - 0.5;
1022    let start = if v <= reach {
1023        v
1024    } else {
1025        // Step down by ⌈ν - reach⌉ degrees, but not below 1 + f.
1026        let steps = floor_small(v - reach);
1027        let steps = if steps < v - reach {
1028            steps + 1.0
1029        } else {
1030            steps
1031        };
1032        (v - steps).max((1.0 + frac).min(v))
1033    };
1034    if start >= v {
1035        return match slopes {
1036            Slopes::None => log_series::<false, false>(v, y, frac_cot),
1037            Slopes::Singular => log_series::<true, false>(v, y, frac_cot),
1038            Slopes::Both => log_series::<true, true>(v, y, frac_cot),
1039        };
1040    }
1041    let zero = log_series::<true, true>(start, y, frac_cot)?;
1042    let (w, w_low) = one_minus_square(y);
1043    let (mut p, mut q) = (zero.regular, zero.singular);
1044    let (mut dp, mut dq) = (zero.regular_slope / start, zero.singular_slope / start);
1045    let mut degree = start;
1046    for _ in 0..small_integer(v - start) {
1047        let coupling = degree / (degree + 1.0);
1048        let next_p = y * p - coupling * dp;
1049        let next_q = y * q - coupling * dq;
1050        dp = w * p + (w_low * p + y * coupling * dp);
1051        dq = w * q + (w_low * q + y * coupling * dq);
1052        p = next_p;
1053        q = next_q;
1054        degree += 1.0;
1055    }
1056    Ok(OrderZero {
1057        regular: p,
1058        regular_slope: v * dp,
1059        singular: q,
1060        singular_slope: v * dq,
1061    })
1062}
1063
1064/// `(S_ν^μ(y), (1 - y²) dS_ν^μ/dy)` for `μ >= 1` from order zero: `S^1 = -w S^0'/sinθ`, then the
1065/// order recurrence DLMF 14.10.1 upwards.
1066fn raise_singular(v: f64, order: i32, y: f64, zero: &OrderZero) -> (Scaled, Scaled) {
1067    let (w, w_low) = one_minus_square(y);
1068    let sine = w.sqrt();
1069    let sine = sine + 0.5 * w_low / sine;
1070    let cotangent = y / sine;
1071    let (mut previous, mut current) = (zero.singular, -zero.singular_slope / sine);
1072    let mut exponent = 0;
1073    for k in 1..order {
1074        let k = f64::from(k);
1075        let next = -2.0 * k * cotangent * current - (v - k + 1.0) * (v + k) * previous;
1076        previous = current;
1077        current = next;
1078        if current.abs() > BIG {
1079            previous = scalbn(previous, -BIG_EXPONENT);
1080            current = scalbn(current, -BIG_EXPONENT);
1081            exponent += BIG_EXPONENT;
1082        }
1083    }
1084    let mu = f64::from(order);
1085    let slope = (v + mu) * (v - mu + 1.0) * sine * previous + mu * y * current;
1086    (Scaled::new(current, exponent), Scaled::new(slope, exponent))
1087}
1088
1089/// Convergence test of one series: the term must be negligible twice in a row and the tail,
1090/// bounded geometrically with ratio `rho`, as well. Once reached, it stays reached.
1091#[derive(Clone, Copy, Debug, Default)]
1092struct Convergence {
1093    size: f64,
1094    small_before: bool,
1095    done: bool,
1096}
1097
1098impl Convergence {
1099    #[inline]
1100    fn update(&mut self, magnitude: f64, rho: impl Fn() -> f64) {
1101        if self.done {
1102            return;
1103        }
1104        self.size += magnitude;
1105        let small = magnitude <= TOLERANCE * self.size;
1106        if small && self.small_before {
1107            let rho = rho();
1108            self.done = rho < 1.0 && magnitude * rho <= TOLERANCE * self.size * (1.0 - rho);
1109        }
1110        self.small_before = small;
1111    }
1112}
1113
1114/// Running sums of the logarithmic expansion: `Σ q_k (c_k - ln s)`, `Σ q_k`, and, as requested,
1115/// `Σ (k q_k (c_k - ln s) - q_k)` (`SINGULAR`) and `Σ k q_k` (`REGULAR`). The values and each
1116/// slope converge separately (for tiny `ν` the regular slope is tiny next to everything else);
1117/// the values are frozen once converged, so they do not depend on which slopes are summed.
1118#[derive(Clone, Copy, Debug, Default)]
1119struct Sums<const SINGULAR: bool, const REGULAR: bool> {
1120    singular: f64,
1121    regular: f64,
1122    singular_slope: f64,
1123    regular_slope: f64,
1124    values: Convergence,
1125    singular_slopes: Convergence,
1126    regular_slopes: Convergence,
1127}
1128
1129impl<const SINGULAR: bool, const REGULAR: bool> Sums<SINGULAR, REGULAR> {
1130    /// Adds the term `k` (`term = q_k (c_k - ln s)`, `plain = q_k`); returns true once the
1131    /// remaining tails are negligible, given that later weight ratios stay below `ratio()`.
1132    #[inline]
1133    fn add(&mut self, k: f64, term: f64, plain: f64, ratio: impl Fn() -> f64) -> bool {
1134        // The bracket c_k - ln s grows only logarithmically; allow for it and for the k.
1135        let rho = || ratio() * (1.0 + 2.0 / (k + 1.0));
1136        if !self.values.done {
1137            self.singular += term;
1138            self.regular += plain;
1139            self.values.update(term.abs() + plain.abs(), rho);
1140        }
1141        if SINGULAR {
1142            let slope_term = k * term - plain;
1143            self.singular_slope += slope_term;
1144            self.singular_slopes.update(slope_term.abs(), rho);
1145        }
1146        if REGULAR {
1147            self.regular_slope += k * plain;
1148            self.regular_slopes.update(k * plain.abs(), rho);
1149        }
1150        self.values.done
1151            && (!SINGULAR || self.singular_slopes.done)
1152            && (!REGULAR || self.regular_slopes.done)
1153    }
1154
1155    fn finish(&self, s: f64) -> OrderZero {
1156        OrderZero {
1157            regular: self.regular,
1158            regular_slope: -2.0 * (1.0 - s) * self.regular_slope,
1159            singular: -self.singular / PI,
1160            singular_slope: 2.0 / PI * (1.0 - s) * self.singular_slope,
1161        }
1162    }
1163}
1164
1165/// [`OrderZero`] from the logarithmic expansion about `y = 1` in `s = (1 - y)/2`:
1166/// `P_ν = Σ q_k`, `Q_ν = ½ Σ q_k (c_k - ln s)` with `q_k = (ν+1)_k (-ν)_k s^k/(k!)²` and
1167/// `c_k = 2ψ(k+1) - ψ(ν+k+1) - ψ(ν+1-k)`, with the slopes selected as in [`Sums`].
1168///
1169/// From `k = n + 1` (`n = ⌊ν⌋`) on, `q_k` carries the factor `n - ν = -f`, which is split off
1170/// (`q_k = -f q̂_k`) and cancelled analytically against the pole of `ψ(ν + 1 - k)`:
1171/// `f ψ(f) = f ψ(1 + f) - 1` at `k = n + 1`, and DLMF 5.5.4, `ψ(ν+1-k) = ψ(k-ν) - π cot(πν)`,
1172/// beyond (`frac_cot = f π cot(πν)`). The digamma updates (DLMF 5.5.2) share one division per
1173/// term.
1174fn log_series<const SINGULAR: bool, const REGULAR: bool>(
1175    v: f64,
1176    y: f64,
1177    frac_cot: f64,
1178) -> Result<OrderZero> {
1179    let whole = floor_small(v);
1180    let frac = v - whole;
1181    let s = 0.5 * (1.0 - y);
1182    // Bound on all later weight ratios |q_{k+1}/q_k| (their limit is s).
1183    let ratio_after = |k: f64| {
1184        let bound = (v + k + 1.0) * ((v - k).abs() + 1.0) / ((k + 1.0) * (k + 1.0));
1185        s * bound.max(1.0)
1186    };
1187    let mut sums = Sums::<SINGULAR, REGULAR>::default();
1188    let psi = digamma(v + 1.0);
1189    let mut regular_part = -2.0 * EULER - psi - s.ln(); // 2ψ(k+1) - ψ(ν+k+1) - ln s
1190    let mut psi_down = psi; // ψ(ν + 1 - k)
1191    let mut weight = 1.0; // q_k
1192    let mut k = 0.0;
1193    let mut count = 0;
1194    // Terms k <= n.
1195    loop {
1196        if sums.add(k, weight * (regular_part - psi_down), weight, || {
1197            ratio_after(k)
1198        }) {
1199            return Ok(sums.finish(s));
1200        }
1201        if k >= whole {
1202            break;
1203        }
1204        let next = k + 1.0;
1205        let (up, gap) = (v + next, v - k);
1206        let reciprocal = 1.0 / (next * up * gap);
1207        let inv_next = up * gap * reciprocal;
1208        weight *= -(up * gap) * s * inv_next * inv_next;
1209        regular_part += (2.0 * up - next) * gap * reciprocal;
1210        psi_down -= next * up * reciprocal;
1211        k = next;
1212        count += 1;
1213        if count > MAX_TERMS {
1214            return Err(nonfinite());
1215        }
1216    }
1217    // k = n + 1: q̂_k = q_k/(-f); f ψ(f) = f ψ(1 + f) - 1 with ψ(1 + f) = psi_down.
1218    let next = k + 1.0;
1219    let up = v + next;
1220    let square = next * next;
1221    weight *= up * s / square;
1222    regular_part += (2.0 * up - next) / (next * up);
1223    k = next;
1224    let term = weight * (frac * (psi_down - regular_part) - 1.0);
1225    if sums.add(k, term, -frac * weight, || ratio_after(k)) {
1226        return Ok(sums.finish(s));
1227    }
1228    // k >= n + 2: ψ(ν + 1 - k) = ψ(k - ν) - π cot(πν).
1229    let next = k + 1.0;
1230    let up = v + next;
1231    weight *= up * (k - v) * s / (next * next);
1232    regular_part += (2.0 * up - next) / (next * up);
1233    k = next;
1234    // ψ(k - ν) - (2ψ(k+1) - ψ(ν+k+1) - ln s), with ψ(2 - f) = ψ(1 + f) + the reflection step.
1235    let mut bracket = psi_down + digamma_reflection_step(frac, frac_cot) - regular_part;
1236    loop {
1237        let term = weight * (frac * bracket - frac_cot);
1238        if sums.add(k, term, -frac * weight, || ratio_after(k)) {
1239            return Ok(sums.finish(s));
1240        }
1241        let next = k + 1.0;
1242        let (up, gap) = (v + next, k - v);
1243        let reciprocal = 1.0 / (next * up * gap);
1244        let inv_next = up * gap * reciprocal;
1245        weight *= up * gap * s * inv_next * inv_next;
1246        bracket += (next * up - (2.0 * up - next) * gap) * reciprocal;
1247        k = next;
1248        count += 1;
1249        if count > MAX_TERMS {
1250            return Err(nonfinite());
1251        }
1252    }
1253}
1254
1255/// `ψ(2 - f) - ψ(1 + f)`. For `f <= 1/2` it is `π cot(πf) - 1/f + 1/(1 - f)` (DLMF 5.5.2,
1256/// 5.5.4) from `frac_cot = f π cot(πf)`, with an absolute error of about `ε/f` that the callers'
1257/// factor `f` removes; otherwise two digamma values.
1258fn digamma_reflection_step(frac: f64, frac_cot: f64) -> f64 {
1259    if frac <= 0.5 {
1260        (frac_cot - 1.0) / frac + 1.0 / (1.0 - frac)
1261    } else {
1262        digamma(2.0 - frac) - digamma(1.0 + frac)
1263    }
1264}
1265
1266/// `Σ_{i=1}^{count} 1/(start + i)` for a small integer `count`, as one fraction.
1267fn shifted_sum(start: f64, count: i32) -> f64 {
1268    let (mut numerator, mut denominator) = (0.0, 1.0);
1269    for i in 1..=count {
1270        let factor = start + f64::from(i);
1271        numerator = numerator * factor + denominator;
1272        denominator *= factor;
1273    }
1274    numerator / denominator
1275}
1276
1277/// `πr cot(πr)` for `|r| <= 1/2`, accurate (and finite) as `r → 0`.
1278fn pi_cot_scaled(r: f64) -> f64 {
1279    let z = PI * r;
1280    if z.abs() < 1e-4 {
1281        let z2 = z * z;
1282        1.0 - z2 / 3.0 - z2 * z2 / 45.0
1283    } else {
1284        z / z.tan()
1285    }
1286}
1287
1288/// Digamma for `x >= 1`: DLMF 5.5.2 up to `x >= 10`, then DLMF 5.11.2.
1289fn digamma(mut x: f64) -> f64 {
1290    // Σ 1/(x + i) over the shifts, accumulated as one fraction.
1291    let (mut numerator, mut denominator) = (0.0, 1.0);
1292    let shifts = if x < 10.0 {
1293        small_integer(10.0 - x) + 1
1294    } else {
1295        0
1296    };
1297    for _ in 0..shifts {
1298        numerator = numerator * x + denominator;
1299        denominator *= x;
1300        x += 1.0;
1301    }
1302    let shift = numerator / denominator;
1303    let inv = 1.0 / x;
1304    let inv2 = inv * inv;
1305    let tail = inv2
1306        * (1.0 / 12.0
1307            - inv2
1308                * (1.0 / 120.0
1309                    - inv2
1310                        * (1.0 / 252.0
1311                            - inv2
1312                                * (1.0 / 240.0
1313                                    - inv2
1314                                        * (1.0 / 132.0
1315                                            - inv2 * (691.0 / 32760.0 - inv2 / 12.0))))));
1316    x.ln() - 0.5 * inv - tail - shift
1317}
1318
1319/// `(sin(νπ), cos(νπ))` from the exactly reduced degree `ν - round(ν)`.
1320fn sin_cos_pi(v: f64) -> (f64, f64) {
1321    let nearest = nearest_integer(v);
1322    let (sin, cos) = (PI * (v - f64::from(nearest))).sin_cos();
1323    if nearest % 2 == 0 {
1324        (sin, cos)
1325    } else {
1326        (-sin, -cos)
1327    }
1328}
1329
1330/// Knuth's error-free sum: `a + b = sum + error` exactly.
1331fn two_sum(a: f64, b: f64) -> (f64, f64) {
1332    let sum = a + b;
1333    let b_virtual = sum - a;
1334    let a_virtual = sum - b_virtual;
1335    (sum, (a - a_virtual) + (b - b_virtual))
1336}
1337
1338/// Dekker's exact square: `v² = square + error`.
1339fn exact_square(v: f64) -> (f64, f64) {
1340    let split = 134_217_729.0 * v; // 2^27 + 1
1341    let head = split - (split - v);
1342    let tail = v - head;
1343    let square = v * v;
1344    (
1345        square,
1346        ((head * head - square) + 2.0 * head * tail) + tail * tail,
1347    )
1348}
1349
1350/// `1 - y²` as an unevaluated sum `high + low`, accurate far beyond a double.
1351fn one_minus_square(y: f64) -> (f64, f64) {
1352    let (square, square_error) = exact_square(y);
1353    let (difference, error) = two_sum(1.0, -square);
1354    let low = error - square_error;
1355    let high = difference + low;
1356    (high, low - (high - difference))
1357}
1358
1359/// `ν(ν + 1)` as an unevaluated sum `high + low`.
1360fn degree_product(v: f64) -> (f64, f64) {
1361    let (square, square_error) = exact_square(v);
1362    let (high, error) = two_sum(square, v);
1363    (high, error + square_error)
1364}
1365
1366/// `Γ(ν + μ + 1)/Γ(ν - μ + 1) = ∏_{i=1}^{μ} (ν + i)(ν + 1 - i)`; with `skip = Some(i)` the
1367/// factor `ν + 1 - i` of that pair is left out.
1368fn gamma_ratio(v: f64, order: i32, skip: Option<i32>) -> Scaled {
1369    let (high, low) = degree_product(v);
1370    let mut product = 1.0;
1371    let mut exponent = 0;
1372    for i in 1..=order {
1373        let index = f64::from(i);
1374        if skip == Some(i) {
1375            product *= v + index;
1376        } else {
1377            // Multiply by the unrounded factor: a rounded one would carry the same error (from
1378            // the fractional bits of ν(ν + 1)) for every i of a binade, and these would add up.
1379            let (difference, error) = two_sum(high, -(index * (index - 1.0)));
1380            product = product * difference + product * (error + low);
1381        }
1382        if i % 32 == 0 {
1383            let (mantissa, shift) = frexp(product);
1384            product = mantissa;
1385            exponent += shift;
1386        }
1387    }
1388    Scaled::new(product, exponent)
1389}
1390
1391/// `sin(νπ) Γ(ν - μ + 1)/Γ(ν + μ + 1)`; outside the caller domain (`μ > round(ν)`) the
1392/// vanishing factor `ν - round(ν)` of the gamma ratio is cancelled against `sin(νπ)`.
1393fn sine_over_gamma_ratio(v: f64, order: i32, sin: f64) -> Scaled {
1394    let nearest = nearest_integer(v);
1395    if nearest + 1 > order {
1396        let ratio = gamma_ratio(v, order, None);
1397        return Scaled::new(sin / ratio.mantissa, -ratio.exponent);
1398    }
1399    let ratio = gamma_ratio(v, order, Some(nearest + 1));
1400    let z = PI * (v - f64::from(nearest));
1401    let sinc = if z.abs() < 1e-4 {
1402        PI * (1.0 - z * z / 6.0)
1403    } else {
1404        PI * z.sin() / z
1405    };
1406    let sign = if nearest % 2 == 0 { 1.0 } else { -1.0 };
1407    Scaled::new(sign * sinc / ratio.mantissa, -ratio.exponent)
1408}
1409
1410#[cfg(test)]
1411mod tests {
1412    #![allow(clippy::float_cmp)] // Tests compare exact endpoint values.
1413    use super::*;
1414
1415    /// `(ν, m, x, P, dP/dx)` from 30-digit references.
1416    const REFERENCES: [(f64, i32, f64, f64, f64); 13] = [
1417        (
1418            2.3,
1419            1,
1420            0.5,
1421            -1.158_877_916_558_258_6,
1422            -3.105_224_571_364_395,
1423        ),
1424        (
1425            2.3,
1426            -2,
1427            0.1,
1428            8.210_878_443_624_224e-2,
1429            5.272_898_021_258_5e-2,
1430        ),
1431        (
1432            0.2,
1433            0,
1434            -0.999_999,
1435            -1.797_687_281_513_591_6,
1436            1.870_981_884_756_791e5,
1437        ),
1438        (
1439            10.000_000_000_000_002,
1440            6,
1441            -0.9,
1442            7.664_247_046_048_816e4,
1443            1.738_989_837_866_624_4e6,
1444        ),
1445        (
1446            127.2,
1447            64,
1448            0.999_999,
1449            2.133_285_121_993_467_3e-24,
1450            -6.826_489_043_723_42e-17,
1451        ),
1452        (
1453            65.2,
1454            -64,
1455            -0.9999,
1456            1.336_260_716_230_590_1e6,
1457            -4.275_790_332_015_145e11,
1458        ),
1459        // A large order left of centre, and a degree 1e-13 above an integer next to -1.
1460        (
1461            127.9,
1462            121,
1463            -0.34,
1464            2.108_817_946_242_377_7e242,
1465            1.073_952_115_051_059e243,
1466        ),
1467        (
1468            127.000_000_000_000_1,
1469            -63,
1470            -0.999,
1471            -4.423_236_554_341_923e-87,
1472            1.388_205_387_672_740_3e-82,
1473        ),
1474        (
1475            120.6,
1476            0,
1477            1.0 - f64::EPSILON / 2.0,
1478            9.999_999_999_991_86e-1,
1479            7.332_479_999_997_015_5e3,
1480        ),
1481        (
1482            0.5,
1483            0,
1484            0.0,
1485            5.393_526_011_883_794e-1,
1486            5.901_702_995_080_481e-1,
1487        ),
1488        (
1489            127.2,
1490            127,
1491            0.0,
1492            -4.160_241_594_472_013e250,
1493            -1.949_011_429_617_312_4e251,
1494        ),
1495        (
1496            127.2,
1497            -127,
1498            -0.5,
1499            -3.491_950_613_042_738_7e-246,
1500            2.869_573_962_247_748_2e-244,
1501        ),
1502        (2.3, 0, 1.0, 1.0, 3.794_999_999_999_999_5),
1503    ];
1504
1505    #[test]
1506    fn matches_high_precision_references() {
1507        for (v, m, x, value, derivative) in REFERENCES {
1508            let (p, q) = ferrers_real_degree::<true>(v, m, x).unwrap();
1509            let (p_false, zero) = ferrers_real_degree::<false>(v, m, x).unwrap();
1510            assert!(
1511                (p - value).abs() <= 1e-13 * value.abs(),
1512                "{v} {m} {x}: {p} vs {value}"
1513            );
1514            assert!(
1515                (q - derivative).abs() <= 1e-13 * derivative.abs(),
1516                "{v} {m} {x}: {q} vs {derivative}"
1517            );
1518            assert_eq!(p_false.to_bits(), p.to_bits());
1519            assert_eq!(zero.to_bits(), 0.0_f64.to_bits());
1520        }
1521    }
1522
1523    #[test]
1524    fn endpoint_limits_are_exact() {
1525        assert_eq!(
1526            ferrers_real_degree::<true>(2.3, 0, 1.0).unwrap(),
1527            (1.0, 0.5 * 2.3 * 3.3)
1528        );
1529        assert_eq!(
1530            ferrers_real_degree::<true>(2.3, -2, 1.0).unwrap(),
1531            (0.0, -0.25)
1532        );
1533        assert_eq!(
1534            ferrers_real_degree::<true>(2.3, 7, 1.0).unwrap(),
1535            (0.0, 0.0)
1536        );
1537        let (value, derivative) = ferrers_real_degree::<true>(2.3, 2, 1.0).unwrap();
1538        assert_eq!(value, 0.0);
1539        assert!((derivative + 10.607_025).abs() < 1e-13 * 10.607_025);
1540        assert_eq!(
1541            ferrers_real_degree::<false>(2.3, 1, 1.0).unwrap(),
1542            (0.0, 0.0)
1543        );
1544        for m in [1, -1] {
1545            let error = ferrers_real_degree::<true>(2.3, m, 1.0).unwrap_err();
1546            assert!(
1547                matches!(&error, Error::SpecialFunction(message) if message.contains("non-finite"))
1548            );
1549        }
1550    }
1551
1552    #[test]
1553    fn rejects_inputs_outside_the_domain() {
1554        let nan = f64::NAN;
1555        let cases = [
1556            (0.0, 0, 0.5),
1557            (-0.0, 0, 0.5),
1558            (2.0, 0, 0.5),
1559            (128.0, 0, 0.5),
1560            (128.5, 0, 0.5),
1561            (-0.5, 0, 0.5),
1562            (nan, 0, 0.5),
1563            (f64::INFINITY, 0, 0.5),
1564            (2.5, 131, 0.5),
1565            (2.5, i32::MIN, 0.5),
1566            (2.5, 0, -1.0),
1567            (2.5, 0, 1.000_000_1),
1568            (2.5, 0, nan),
1569        ];
1570        for (v, m, x) in cases {
1571            assert!(
1572                matches!(
1573                    ferrers_real_degree::<false>(v, m, x),
1574                    Err(Error::InvalidInput(_))
1575                ),
1576                "{v} {m} {x}"
1577            );
1578            assert!(
1579                matches!(
1580                    ferrers_real_degree::<true>(v, m, x),
1581                    Err(Error::InvalidInput(_))
1582                ),
1583                "{v} {m} {x}"
1584            );
1585        }
1586    }
1587
1588    #[test]
1589    fn tiny_degrees_and_overflow() {
1590        let (value, derivative) = ferrers_real_degree::<true>(5e-324, 0, -0.8).unwrap();
1591        assert_eq!(value, 1.0);
1592        assert!(derivative.abs() < f64::MIN_POSITIVE);
1593        // P_127.2^126(-0.999999) ≈ 3.2e605 overflows; the negative order ≈ 2.29e105 does not.
1594        for result in [
1595            ferrers_real_degree::<false>(127.2, 126, -0.999_999),
1596            ferrers_real_degree::<true>(127.2, 126, -0.999_999),
1597        ] {
1598            assert!(
1599                matches!(result, Err(Error::SpecialFunction(message)) if message.contains("non-finite"))
1600            );
1601        }
1602        let (value, _) = ferrers_real_degree::<true>(127.2, -126, -0.999_999).unwrap();
1603        assert!((value / 2.286_871_2e105 - 1.0).abs() < 1e-6);
1604    }
1605
1606    /// Degree recurrence DLMF 14.10.3 and derivative DLMF 14.10.5 across all evaluation routes,
1607    /// including degrees next to integers and arguments next to -1.
1608    #[test]
1609    fn recurrences_hold_across_routes() {
1610        let degrees: [f64; 8] = [
1611            1.3,
1612            2.000_000_001,
1613            2.999_999_999,
1614            7.5,
1615            20.25,
1616            41.000_000_000_01,
1617            63.7,
1618            125.9,
1619        ];
1620        let arguments = [
1621            -0.999_999_9,
1622            -0.99,
1623            -0.7,
1624            -0.3,
1625            -0.05,
1626            0.0,
1627            0.05,
1628            0.3,
1629            0.7,
1630            0.99,
1631            0.999_999_9,
1632        ];
1633        let mut checked = 0;
1634        for v in degrees {
1635            let top = small_integer(v.floor()) - 1;
1636            for m in [0, 1, -1, 2, -top / 2, top / 2, -top, top] {
1637                for x in arguments {
1638                    let results = [
1639                        ferrers_real_degree::<true>(v, m, x),
1640                        ferrers_real_degree::<false>(v - 1.0, m, x),
1641                        ferrers_real_degree::<false>(v + 1.0, m, x),
1642                    ];
1643                    // Results beyond the double range (large orders next to x = -1) are errors.
1644                    let [Ok((p, derivative)), Ok((lower, _)), Ok((upper, _))] = results else {
1645                        assert!(results.iter().all(|result| matches!(
1646                            result,
1647                            Ok(_) | Err(Error::SpecialFunction(_))
1648                        )));
1649                        continue;
1650                    };
1651                    let order = f64::from(m);
1652                    // Values near the underflow threshold only carry absolute accuracy.
1653                    let scale = (v - order + 1.0).abs() * upper.abs()
1654                        + (2.0 * v + 1.0) * p.abs()
1655                        + (v + order).abs() * lower.abs()
1656                        + 1e-288;
1657                    let recurrence =
1658                        (v - order + 1.0) * upper - (2.0 * v + 1.0) * x * p + (v + order) * lower;
1659                    assert!(
1660                        recurrence.abs() <= 1e-12 * scale,
1661                        "recurrence {v} {m} {x}: {recurrence} / {scale}"
1662                    );
1663                    checked += 1;
1664                    let expected = (v + order) * lower - v * x * p;
1665                    let deviation = (1.0 - x) * (1.0 + x) * derivative - expected;
1666                    let slope_scale = (v + order).abs() * lower.abs()
1667                        + v * p.abs()
1668                        + (1.0 - x) * (1.0 + x) * derivative.abs()
1669                        + 1e-288;
1670                    assert!(
1671                        deviation.abs() <= 1e-12 * slope_scale,
1672                        "derivative {v} {m} {x}: {deviation} / {slope_scale}"
1673                    );
1674                }
1675            }
1676        }
1677        assert!(
1678            checked > 600,
1679            "only {checked} combinations were representable"
1680        );
1681    }
1682}