cdflib 0.4.1

Pure-Rust port of CDFLIB: cumulative distribution functions (CDF) associated with common probability distributions
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
#![cfg(not(miri))]

//! Targeted tests that drive control flow into special-function
//! branches the reference-table fixtures don't sample. Each test names
//! the branch by its triggering condition rather than by source
//! location, so the descriptions don't rot with source edits.
//! Numerical correctness against the F90 is the reference-table
//! fixtures' job.

mod common;

use cdflib::special::internal::{
    apser, beta_grat, beta_rcomp, beta_rcomp1, beta_up, fpser, gam1, gamma_rat1, rcomp, rexp,
};
use cdflib::special::{
    beta_inc, gamma, gamma_inc, gamma_inc_inv, gamma_log, psi, try_gamma_inc, try_gamma_inc_inv,
    GammaIncError, GammaIncInvError,
};
use cdflib::{ContinuousCdf, DiscreteCdf, FisherSnedecorNoncentral, NegativeBinomial, Poisson};

// ---------- gamma.rs ----------

#[test]
fn rexp_large_positive_and_negative() {
    // Drives rexp's x.exp() fallback for |x| > 0.15, both the
    // positive and negative branches.
    let pos = rexp(0.5); // exp(0.5) - 1
    assert!((pos - (0.5_f64.exp() - 1.0)).abs() < 1e-13);

    let neg = rexp(-0.5); // exp(-0.5) - 1
    assert!((neg - ((-0.5_f64).exp() - 1.0)).abs() < 1e-13);
}

#[test]
fn gam1_above_one_and_negative() {
    // gam1(1.2): the a > 1 branch (d > 0 and t > 0).
    // 1/Γ(2.2) - 1 ≈ 1/1.10180 - 1 ≈ -0.0924.
    let g_big = gam1(1.2);
    let expected_big = 1.0 / gamma(2.2) - 1.0;
    assert!((g_big - expected_big).abs() < 1e-12, "gam1(1.2) = {g_big}");

    // gam1(-0.3): the a < 0 branch (t < 0 AND d ≤ 0).
    // 1/Γ(0.7) - 1.
    let g_neg = gam1(-0.3);
    let expected_neg = 1.0 / gamma(0.7) - 1.0;
    assert!(
        (g_neg - expected_neg).abs() < 1e-12,
        "gam1(-0.3) = {g_neg}, want {expected_neg}"
    );
}

#[test]
fn gamma_negative_argument_and_overflow_paths() {
    use cdflib::special::{try_gamma, GammaDomainError};

    // |a| ≥ 15 reflection branch with t > 0.9. Γ(-15.95) routes through
    // t = 0.95 → t = 1 - 0.95 = 0.05.
    let g_307 = gamma(-15.95);
    assert!(g_307.is_finite() && g_307 != 0.0, "g_307 = {g_307}");

    // Reflection at a negative integer with |a| ≥ 15: sin(πt) = 0 → pole.
    assert_eq!(try_gamma(-20.0), Err(GammaDomainError::Pole(-20.0)));
    assert_eq!(try_gamma(-25.0), Err(GammaDomainError::Pole(-25.0)));

    // Same pole via the |a| < 15 branch.
    assert_eq!(try_gamma(-2.0), Err(GammaDomainError::Pole(-2.0)));
    assert_eq!(try_gamma(-5.0), Err(GammaDomainError::Pole(-5.0)));

    // Reflection-branch overflow: g exceeds POS_EXPARG for a far enough
    // negative non-integer.
    assert_eq!(try_gamma(-200.7), Err(GammaDomainError::Overflow(-200.7)));

    // Large positive overflow: a ≥ 15 and g > 0.99999·POS_EXPARG.
    assert_eq!(try_gamma(200.0), Err(GammaDomainError::Overflow(200.0)));
}

#[test]
fn psi_reflection_and_overflow_branches() {
    use cdflib::special::{try_psi, PsiError};

    // psi(0) is a pole.
    assert_eq!(try_psi(0.0), Err(PsiError::Pole(0.0)));

    // psi's small-|x| reflection path: aug = -1/x. Use a tiny positive x
    // below xsmall = 1e-9. CDFLIB: psi(x) ≈ -1/x - γ + O(x).
    let small = 1e-12;
    let p_small = psi(small);
    assert!(
        (p_small + 1.0 / small).abs() < 1.0,
        "psi(1e-12) = {p_small}"
    );

    // Sign-negation branch in psi's cotan reduction. psi(-0.6) routes
    // through it.
    let p462 = psi(-0.6);
    assert!(p462.is_finite());

    // Singularity at a negative integer: z = 0 inside the m+m == n
    // branch. psi(-2.0) hits this exactly (Pole variant.
    assert_eq!(try_psi(-2.0), Err(PsiError::Pole(-2.0)));
    assert_eq!(try_psi(-3.0), Err(PsiError::Pole(-3.0)));

    // w ≥ xmax1 branch in psi's reflection: huge negative argument →
    // Overflow variant.
    assert_eq!(try_psi(-1e20), Err(PsiError::Overflow(-1e20)));

    // x ≥ xmax1 in psi's asymptotic block: huge positive argument
    // bypasses the rational and returns aug + ln(x).
    let big = 1e20_f64;
    let p_big = psi(big);
    assert!((p_big - big.ln()).abs() < 1.0, "psi(1e20) = {p_big}");
}

#[test]
#[should_panic(expected = "psi(0): ψ has a pole at 0")]
fn psi_panics_on_pole() {
    let _ = psi(0.0);
}

#[test]
fn rcomp_branches() {
    // Exercise both rcomp's a < 20 and a ≥ 20 paths. Values are
    // positive; just sanity-check finite.
    //
    // a < 1 branch: a · exp(t) · (1 + gam1(a))
    let r1 = rcomp(0.5, 1.0);
    assert!(r1.is_finite() && r1 > 0.0);

    // 1 ≤ a < 20: exp(t) / Γ(a)
    let r2 = rcomp(5.0, 3.0);
    assert!(r2.is_finite() && r2 > 0.0);

    // a ≥ 20: asymptotic. Use moderate x near a so u ≠ 0.
    let r3 = rcomp(50.0, 50.0);
    assert!(r3.is_finite() && r3 > 0.0);

    // a ≥ 20 with x so tiny that x/a underflows to zero.
    // x = 0 hits the a·x == 0 short-circuit upstream, so we need
    // x > 0 but x/a == 0. f64::MIN_POSITIVE / 1e15 underflows.
    let r4 = rcomp(1e20, f64::MIN_POSITIVE);
    // Result must be exactly 0 by the early-return.
    assert_eq!(r4, 0.0);
}

#[test]
fn gamma_inc_taylor_qans_negative_branch() {
    // qans < 0 inside taylor_p_over_xa. Happens for a < 1 and
    // x < 1.1 along the use_main_form path when the truncation gives
    // a slightly negative Q; with a tiny x and a near 1 the truncated
    // series can dip below zero.
    let (p, q) = gamma_inc(0.99, 1.0e-8);
    assert!(p.is_finite() && q.is_finite());
    assert!((p + q - 1.0).abs() < 1e-10);
}

#[test]
fn gamma_inc_temme_indeterminate_sentinel() {
    // Tricomi–Temme L=1 sentinel. Triggers when s ≈ 0 and a · ε² > 3.28e-3,
    // i.e. when the asymptotic expansion can no longer resolve P vs Q. Need a >
    // 3.28e-3 / EPS² ≈ 6.6e28.
    //
    // For a = x = 1e30, the dispatcher routes to the a ≥ big branch
    // with l = x/a = 1 → s = 0 → enters temme_for_l_eq_1, which hits
    // the sentinel.
    assert!(matches!(
        try_gamma_inc(1e30, 1e30),
        Err(GammaIncError::Indeterminate { .. })
    ));
}

// Several defensive sentinels inside gamma_inc's a ≥ big and
// Tricomi–Temme-general branches fire only in narrow regimes (s in a band of
// width ~ε·√a, or the truncated Taylor series dipping below zero by a few
// ULPs). These match defensive code in cdflib.f90 and aren't reached by any
// fixture row; the Tricomi–Temme L=1 sentinel is the only one exercised above.

// ---------- beta.rs ----------

#[test]
fn fpser_full_body() {
    // fpser body past the early t < NEG_EXPARG exit. With a = 2.0 and
    // x = 0.1, t = 2·ln(0.1) ≈ -4.6 > NEG_EXPARG, so we fall through to
    // the series.
    let eps = f64::EPSILON.max(1e-15);
    let r = fpser(2.0, 0.1, 0.1, eps);
    // I_{0.1}(2, 0.1) is small but positive; sanity-check.
    assert!(r.is_finite() && r > 0.0, "fpser(2, 0.1, 0.1) = {r}");

    // Also exercise the a ≤ 1e-3 · eps skip, with a so small the
    // exp(t) prefactor is left at 1.0.
    let r2 = fpser(1e-20, 1.0, 0.5, eps);
    assert!(r2.is_finite());
}

#[test]
fn apser_large_b_eps_else_branch() {
    // apser's else branch: b · eps > 0.02. The eps argument here is
    // treated as a tolerance, not the machine epsilon, so we can pass a
    // "large" eps directly to force the else.
    let r = apser(1e-15, 5.0, 0.05, 0.1);
    assert!(r.is_finite());
}

#[test]
fn beta_rcomp_degenerate_and_small_branches() {
    // x == 0 or y == 0 short-circuit.
    assert_eq!(beta_rcomp(2.0, 3.0, 0.0, 1.0), 0.0);
    assert_eq!(beta_rcomp(2.0, 3.0, 1.0, 0.0), 0.0);

    // x > 0.375 AND y ≤ 0.375 → alnrel(-y), y.ln() path.
    let r1 = beta_rcomp(2.0, 5.0, 0.8, 0.2);
    assert!(r1.is_finite() && r1 > 0.0);

    // a0 ≥ 1 path (both a, b ≥ 1 but min(a, b) < 8).
    let r2 = beta_rcomp(2.0, 3.0, 0.4, 0.6);
    assert!(r2.is_finite() && r2 > 0.0);

    // b0 > 1 path inside small a0.
    let r3 = beta_rcomp(0.5, 3.5, 0.3, 0.7);
    assert!(r3.is_finite() && r3 > 0.0);

    // b0 ≤ 1 path (small a0 AND small b0).
    let r4 = beta_rcomp(0.4, 0.8, 0.3, 0.7);
    assert!(r4.is_finite() && r4 > 0.0);
}

#[test]
fn gamma_rat1_branches() {
    // a · x == 0: trivial short-circuit (caller would normally avoid
    // this, but the guard exists).
    let eps = f64::EPSILON.max(1e-15);
    // a == 0 and x == 1: x > a → (1.0, 0.0).
    let (p, q) = gamma_rat1(0.0, 1.0, 1.0, eps);
    assert_eq!((p, q), (1.0, 0.0));
    // a == 1 and x == 0: x ≤ a → (0.0, 1.0).
    let (p, q) = gamma_rat1(1.0, 0.0, 1.0, eps);
    assert_eq!((p, q), (0.0, 1.0));

    // a == 1/2 AND x < 0.25 → erf branch.
    let (p, q) = gamma_rat1(0.5, 0.1, 0.0, eps);
    assert!(p.is_finite() && q.is_finite() && (p + q - 1.0).abs() < 1e-12);

    // a == 1/2 AND x ≥ 0.25 → erfc branch.
    let (p, q) = gamma_rat1(0.5, 0.5, 0.0, eps);
    assert!((p + q - 1.0).abs() < 1e-12);

    // x < 1.1, use_main_form path: small a, small x to satisfy
    // z > -0.13394.
    let (p, q) = gamma_rat1(0.05, 0.3, 0.0, eps);
    assert!((p + q - 1.0).abs() < 1e-10);

    // Continued-fraction branch (x ≥ 1.1).
    let (p, q) = gamma_rat1(0.7, 2.0, 1.0, eps);
    assert!((p + q - 1.0).abs() < 1e-10);
}

#[test]
fn beta_rcomp1_branches() {
    // a0 ≥ 8 path (a, b ≥ 8). Use mu = 0 so the result matches
    // beta_rcomp · exp(0); keeps the assertion simple.
    let r1 = beta_rcomp1(0, 10.0, 12.0, 0.45, 0.55);
    let r1_ref = beta_rcomp(10.0, 12.0, 0.45, 0.55);
    assert!(r1.is_finite() && (r1 - r1_ref).abs() / r1_ref.abs() < 1e-12);

    // a0 ≥ 8 with the b ≤ a sub-branch.
    let r2 = beta_rcomp1(0, 15.0, 10.0, 0.6, 0.4);
    let r2_ref = beta_rcomp(15.0, 10.0, 0.6, 0.4);
    assert!(r2.is_finite() && (r2 - r2_ref).abs() / r2_ref.abs() < 1e-12);

    // |e| > 0.6 branch in u. Needs |lambda/a| > 0.6 with a0 ≥ 8.
    // lambda = a - (a+b)·x = 10 - 22·0.1 = 7.8, |7.8/10| = 0.78.
    let r_467 = beta_rcomp1(0, 10.0, 12.0, 0.1, 0.9);
    assert!(r_467.is_finite() && r_467 > 0.0);

    // |e| > 0.6 branch in v. Symmetric: a > b sub-branch makes
    // lambda = (a+b)y - b; |lambda/b| large for skewed y.
    let r_474 = beta_rcomp1(0, 15.0, 8.0, 0.95, 0.05);
    assert!(r_474.is_finite() && r_474 > 0.0);

    // a0 < 8 paths.
    let r3 = beta_rcomp1(0, 0.5, 12.0, 0.3, 0.7);
    let r3_ref = beta_rcomp(0.5, 12.0, 0.3, 0.7);
    assert!(r3.is_finite() && (r3 - r3_ref).abs() / r3_ref.abs() < 1e-12);

    // b0 ≤ 1 path with apb > 1. a = 0.5, b = 0.7 → both ≤ 1,
    // apb = 1.2 > 1, so the (1 + gam1(u))/apb branch fires.
    let r_528 = beta_rcomp1(0, 0.5, 0.7, 0.4, 0.6);
    assert!(r_528.is_finite() && r_528 > 0.0);

    // b0 ≤ 1 path with esum(mu, z) underflowing to 0. mu sufficiently
    // negative drives exp(mu + z) to 0. mu = -800 puts the exponent
    // below -708 (NEG_EXPARG).
    let r_underflow = beta_rcomp1(-800, 0.5, 0.5, 0.4, 0.6);
    assert_eq!(r_underflow, 0.0);
}

#[test]
fn beta_up_b_gt_1_branches() {
    // beta_up's b > 1 path with n large enough to drive the
    // "decreasing terms" inner loop.
    let eps = f64::EPSILON.max(1e-15);
    let r = beta_up(2.0, 3.0, 0.4, 0.6, 10, eps);
    assert!(r.is_finite() && r > 0.0);

    // Also a path where y ≤ 1e-4 (k = nm1 branch).
    let r2 = beta_up(2.0, 3.0, 0.99999, 1e-5, 5, eps);
    assert!(r2.is_finite());
}

#[test]
fn beta_grat_overflow_sentinel() {
    use cdflib::special::internal::BetaGratError;
    // b · z == 0.0 early-return. With b ≈ 0 and z finite, b·z is
    // exactly 0 and beta_grat returns BzZero.
    let eps = f64::EPSILON.max(1e-15);
    assert_eq!(
        beta_grat(20.0, 0.0, 0.5, 0.5, 0.0, eps),
        Err(BetaGratError::BzZero)
    );

    // Happy-path smoke test: deep-tail call doesn't panic.
    let w2 = beta_grat(100.0, 0.5, 0.5, 0.5, 0.0, eps).unwrap();
    assert!(w2.is_finite());
}

#[test]
fn beta_inc_fpser_apser_dispatch() {
    // fpser branch in beta_inc's small_branch dispatch. Needs
    // b0 < eps · min(1, a0), i.e. b strictly less than ~1e-15 with a
    // moderate.
    let (w, w1) = beta_inc(5.0, 1e-17, 0.5, 0.5);
    assert!((w + w1 - 1.0).abs() < 1e-10);

    // apser branch. Needs a0 < eps · min(1, b0) AND b0 · x0 ≤ 1.
    let (w, w1) = beta_inc(1e-17, 5.0, 0.05, 0.95);
    assert!((w + w1 - 1.0).abs() < 1e-10);
}

// ---------- distribution single-line gaps ----------

#[test]
fn poisson_inverse_cdf_high_quantile() {
    // hi *= 2 expansion in Poisson::inverse_cdf. The mean + 10σ + 10
    // heuristic comfortably covers any p representable as f64 < 1
    // (the inverse Normal at nextDown(1.0) is only ≈ 8σ), so this
    // expansion is structurally defensive, exercised only when the
    // initial range undershoots. The test just confirms the
    // surrounding inverse_cdf path returns a consistent result.
    let d = Poisson::new(4.0);
    let p = 1.0 - 1e-12;
    let s = d.inverse_cdf(p).unwrap();
    assert!(d.cdf(s) >= p);
    assert!(s > 0 && s < 100);
}

#[test]
fn negative_binomial_inverse_cdf_high_quantile() {
    // Same defensive expansion pattern as Poisson::inverse_cdf, applied
    // to NegativeBinomial.
    let d = NegativeBinomial::new(5, 0.05);
    let p = 1.0 - 1e-10;
    let s = d.inverse_cdf(p).unwrap();
    assert!(d.cdf(s) >= p);
}

#[test]
fn fisher_snedecor_noncentral_pdf_basic() {
    // The degenerate aup - 1 + b == 0 branch inside cumfnc's
    // forward-summation loop. Achieving that exact equality from the
    // public API is impossible without intimate knowledge of the
    // dispatcher's internal counters; the branch is structurally
    // guarded against a 0·log(0) form. Exercise the surrounding code
    // with a representative input.
    let d = FisherSnedecorNoncentral::new(4.0, 8.0, 2.5);
    let x = 1.0;
    let c = d.cdf(x);
    assert!(c.is_finite() && (0.0..=1.0).contains(&c));
}

// ---------- gamma_log smoke check ----------

#[test]
fn gamma_log_does_not_regress() {
    // Canary against accidental constant changes in the asymptotic branch.
    let v = gamma_log(50.0);
    // ln Γ(50) ≈ 144.5658...
    assert!((v - 144.56574394634488).abs() < 1e-10);
}

// ---------- gamma_inc_inv extreme-value paths ----------

// The reference-table fixtures for gamma_inc_inv sample a and p on a
// moderate grid; the tests below drive branches that need genuinely extreme
// inputs (subnormals, a ≥ 10²², caller-supplied bad initial approximations).
// They are coverage-driven only: numerical correctness is tested elsewhere.

// Several defensive paths in gamma_inc_inv are structurally unreachable
// in IEEE 754 f64 yet are retained for strict F90 fidelity:
//
//   * qg == 0 (qg = q · gamma(a+1) underflow). For a ∈ (0, 1),
//     gamma(a+1) attains its minimum ≈ 0.8856 near a ≈ 0.4616, so
//     q · g ≥ 0.4428 · 2⁻¹⁰⁷⁴ for any positive f64 q. That rounds up to
//     2⁻¹⁰⁷⁴, never to 0.
//   * b == 0 after the qg check (b = qg/a with a < 1 only magnifies qg).
//   * the xn == 0 early-return on the b ≥ 0.45 small-b path. b ≥ 0.45
//     together with NOT-go_to_40 (qg ≤ 0.6 a) forces q ∈ [0.45 a/g,
//     0.6 a/g]; on the entire band the three formulas in
//     initial_approx_small_b stay bounded well above 0.
//   * r == 0 in schroder_p/schroder_q. rcomp and the internal r
//     in gamma_inc share the same dominant exp(a·ln x − x) factor and
//     underflow at the same threshold; when that fires, gamma_inc
//     returns (1, 0) or (0, 1) at S40, so the pn == 0 || qn == 0 guard
//     trips first.
//   * x ≤ 0 in the 2nd-order Schröder branch. Entry requires |t| ≤ 0.1
//     AND |w·t| ≤ 0.1, which bounds |h| = |t·(1 + w·t)| ≤ 0.11; hence
//     x = xn·(1 − h) stays in [0.89 xn, 1.11 xn] > 0.
//   * iter >= 20 (NotConverged). Schröder's method has super-quadratic
//     local convergence; once the iterate is close enough to enter the
//     2nd-order branch its error squares each step. Stalling at |d| > eps
//     for 20 deterministic iterations is not observed on any IEEE 754 f64
//     input.

#[test]
fn gamma_inc_inv_small_a_label_30_early_return() {
    // The label-30 c1..c5 fallback path with BMIN[iop] ≥ b returns the
    // c1..c5 approximation directly. f64::EPSILON ≈ 2.22e-16 is *not* >
    // 1e-10, so iop = 0 and BMIN[0] = 1e-28. With a = 0.5 and q = 1e-29,
    // b = q · gamma(1.5) / 0.5 ≈ 1.8e-29 < 1e-28.
    let q = 1.0e-29;
    let p = 1.0 - q;
    let (r, _) = gamma_inc_inv(0.5, -1.0, p, q);
    assert!(r.is_finite() && r > 0.0);
}

#[test]
fn gamma_inc_inv_amin_early_return() {
    // a ≥ AMIN[iop] = 500 (iop = 0) with d = |1 - xn0/a| ≤ DMIN[iop] = 1e-6.
    // With p = 0.5 the rational s ≈ 0, so xn0 ≈ a + (s²−1)/3 ≈ a − 1/3.
    // d ≈ 1/(3a). a = 1e7 gives d ≈ 3.3e-8 < 1e-6 → early-return.
    let (r, _) = gamma_inc_inv(1.0e7, -1.0, 0.5, 0.5);
    assert!(r.is_finite() && r > 0.0);
    assert!((r - 1.0e7).abs() < 1.0);
}

#[test]
fn gamma_inc_inv_initial_approx_small_b_bq_branch() {
    // Drives the b·q ≤ 1e-8 branch of initial_approx_small_b. The
    // small-b path (label 40) takes qg > 0.6 a, and on that path
    // b·q = q·qg/a. With a = q = 1e-9: qg ≈ 1e-9, 0.6a = 6e-10 < qg,
    // b = 1, b·q = 1e-9 ≤ 1e-8.
    let r = try_gamma_inc_inv(1.0e-9, -1.0, 1.0 - 1.0e-9, 1.0e-9);
    // The routine may legitimately return Ok or a soft-failure error;
    // the only goal here is to drive the code path.
    match r {
        Ok((x, _iters)) => assert!(x.is_finite() && x > 0.0),
        Err(GammaIncInvError::NoSolution)
        | Err(GammaIncInvError::IterationFailed)
        | Err(GammaIncInvError::UncertainAccuracy { .. }) => {}
        Err(e) => panic!("unexpected error {e:?}"),
    }
}

// ---- Schröder iteration give-up paths (use caller-supplied x0 to inject
// pathological state). All of these correspond to F90 ierr ∈ {-6,-7,-8}
// outcomes that the original code reports and the port preserves.

#[test]
fn gamma_inc_inv_schroder_p_subnormal_p() {
    // schroder_p's "p ≤ 1e10 · MIN_POSITIVE" early-return. Using x0 > 0
    // routes through schroder_p for p ≤ 0.5; we just need p subnormal.
    let p = 1.0e-300;
    let q = 1.0; // 1 - 1e-300 == 1.0 in f64
    let r = try_gamma_inc_inv(2.0, 1.0, p, q);
    assert!(matches!(r, Err(GammaIncInvError::UncertainAccuracy { .. })));
}

#[test]
fn gamma_inc_inv_schroder_q_subnormal_q() {
    // Symmetric: schroder_q's "q ≤ 1e10 · MIN_POSITIVE" early-return.
    // x0 > 0 with p > 0.5 routes through schroder_q.
    let q = 1.0e-300;
    let p = 1.0;
    let r = try_gamma_inc_inv(2.0, 1.0, p, q);
    assert!(matches!(r, Err(GammaIncInvError::UncertainAccuracy { .. })));
}

#[test]
fn gamma_inc_inv_schroder_p_amax_certify_fail() {
    // schroder_p's amax < a block with |1 - xn/a| ≤ 2·EPSILON.
    // amax = 0.4e-10 / EPSILON² ≈ 8.1e21. Pick a = 1e25 and x0 = a.
    // Routes through schroder_p since p < 0.5.
    let a = 1.0e25;
    let r = try_gamma_inc_inv(a, a, 0.5, 0.5);
    assert!(matches!(r, Err(GammaIncInvError::UncertainAccuracy { .. })));
}

#[test]
fn gamma_inc_inv_schroder_q_amax_certify_fail() {
    // Same as above on the q branch (p > 0.5 → schroder_q).
    let a = 1.0e25;
    let r = try_gamma_inc_inv(a, a, 0.7, 0.3);
    assert!(matches!(r, Err(GammaIncInvError::UncertainAccuracy { .. })));
}

#[test]
fn gamma_inc_inv_schroder_p_saturates_to_zero() {
    // gamma_inc(a, x) returns (0, 1) for x deep below the mode when a is
    // moderate. Routes through schroder_p (p ≤ 0.5). pn = 0 trips the
    // soft-failure guard.
    let r = try_gamma_inc_inv(10.0, 1.0e-100, 0.1, 0.9);
    assert!(matches!(r, Err(GammaIncInvError::UncertainAccuracy { .. })));
}

#[test]
fn gamma_inc_inv_schroder_q_saturates_to_zero() {
    // Symmetric on the q branch: x deep above the mode → qn = 0.
    let r = try_gamma_inc_inv(10.0, 1.0e10, 0.9, 0.1);
    assert!(matches!(r, Err(GammaIncInvError::UncertainAccuracy { .. })));
}

#[test]
fn gamma_inc_inv_schroder_p_first_order_negative() {
    // First-order Schröder step with t = (pn − p)/r ≥ 1 ⇒ x = xn·(1−t) ≤ 0.
    // Choose x0 well above the true x: gamma_inc(2, 10) ≈ (0.9995, 5e-4),
    // r ≈ 4.5e-4. For p = 0.01: t ≈ 0.99/4.5e-4 ≈ 2200 ≫ 1.
    let r = try_gamma_inc_inv(2.0, 10.0, 0.01, 0.99);
    assert!(matches!(r, Err(GammaIncInvError::IterationFailed)));
}

#[test]
fn gamma_inc_inv_schroder_q_first_order_negative() {
    // Symmetric on the q branch: feed x0 well below the true x with p > 0.5.
    // gamma_inc(2, 0.01) ≈ (5e-5, 1−5e-5), r ≈ 1e-2. For q = 0.01:
    // t = (q − qn)/r = (0.01 − 0.99995)/0.01 ≈ −99. |t| ≫ 0.1 →
    // first-order: x = xn·(1−t) = 0.01·100 = 1.0 (positive). Need a
    // sign-flipped case: x0 huge with q close to 1.
    //
    // Easier route: use p > 0.5 close to 1 so x_true is large, with x0
    // tiny. Then qn ≈ 1 ≫ q, t large positive → x = xn·(1−t) < 0.
    let r = try_gamma_inc_inv(2.0, 0.01, 0.99, 0.01);
    assert!(matches!(r, Err(GammaIncInvError::IterationFailed)));
}