Skip to main content

gam_math/
probability.rs

1use libm::{erf, erfc};
2use statrs::function::{
3    beta::{beta_reg, inv_beta_reg, ln_beta},
4    gamma::gamma_ur,
5};
6
7const INV_SQRT_PI: f64 = 0.564_189_583_547_756_3;
8const SQRT_2_OVER_PI: f64 = 0.797_884_560_802_865_4;
9
10/// Quantile (inverse CDF) of a Beta distribution with shape parameters `a > 0`
11/// and `b > 0` at probability `p`: the value `x in [0, 1]` with
12/// `I_x(a, b) = p`, where `I` is the regularized incomplete beta.
13///
14/// `p <= 0` maps to the support floor and `p >= 1` to the support ceiling. A
15/// non-finite or non-positive shape yields `NaN`.
16pub fn beta_quantile(p: f64, a: f64, b: f64) -> f64 {
17    if !(a.is_finite() && a > 0.0 && b.is_finite() && b > 0.0) {
18        return f64::NAN;
19    }
20    if !p.is_finite() || p <= 0.0 {
21        return 0.0;
22    }
23    if p >= 1.0 {
24        return 1.0;
25    }
26    match lower_tail_beta_quantile(p, a, b) {
27        Some(x) => x,
28        None => inv_beta_reg(a, b, p),
29    }
30}
31
32/// `Beta⁻¹(p; a, b)` on the branch where the answer is small enough for the
33/// ascending series to be exact, or `None` when it is not.
34///
35/// `inv_beta_reg` converges on an ABSOLUTE tolerance in `x`, so it cannot
36/// resolve a quantile below about `1e-16`: it stalls and returns a number in
37/// the `1e-17..1e-19` band unrelated to the answer. That band is not exotic —
38/// it is the ordinary lower tail of a beta-regression predictive interval
39/// whenever the mean is small. For `Beta(0.04, 3.96)` at `p = 0.025`, the
40/// shapes a mean of `0.01` with a fifth of the Bernoulli variance produces, it
41/// returned `6.7e-18` where the truth is `1.5e-41` (#2528).
42///
43/// The lower tail has a convergent ascending series,
44///
45/// ```text
46/// I_x(a,b) = x^a / B(a,b) · S(x),   S(x) = Σ_{k≥0} c_k x^k,
47/// c_k = (1−b)_k / (k!·(a+k)),       c_0 = 1/a
48/// ```
49///
50/// whose leading term inverts in closed form to
51/// `x₀ = exp([ln p + ln a + ln B(a,b)] / a)`. Refining it in `y = ln x` rather
52/// than in `x` is what removes the floor: the answer's own variable becomes the
53/// iteration variable, so an absolute step tolerance in `y` is a RELATIVE
54/// tolerance in `x` and there is nothing to stall against. The iteration is
55/// also better conditioned than the one it replaces —
56/// `G(y) = ln I_{e^y}(a,b) − ln p` has `G′(y) = a + x·S′(x)/S(x) → a`, a
57/// constant, where the `x`-space derivative `∂I/∂x` spans hundreds of orders
58/// over the same range.
59///
60/// Underflow then becomes something the function can state rather than paper
61/// over: a true quantile below `f64::MIN_POSITIVE` reaches `exp(y) = 0`, which
62/// is the correctly rounded answer, instead of a spurious positive floor a
63/// caller cannot distinguish from a resolved bound.
64///
65/// The branch condition is `x·max(1, b) ≤ ½`, which is derived rather than
66/// tuned. The term ratio is `|x·(k+1−b)/(k+1)|·(a+k)/(a+k+1)`, and
67/// `|k+1−b| ≤ (k+1)·max(1, b)` for every `k ≥ 0`, so the condition bounds every
68/// ratio by `½` and the series reaches `f64` resolution in at most
69/// [`BETA_SERIES_MAX_TERMS`] terms. It is the same boundary, for the same
70/// reason, that `crates/gam-terms/src/basis/polylog.rs` uses for its own
71/// ascending series.
72fn lower_tail_beta_quantile(p: f64, a: f64, b: f64) -> Option<f64> {
73    let ln_b = ln_beta(a, b);
74    if !ln_b.is_finite() {
75        return None;
76    }
77    // Leading-order inverse: `I_x ≈ x^a / (a·B(a,b))` as `x → 0`.
78    let mut y = (p.ln() + a.ln() + ln_b) / a;
79    if !y.is_finite() {
80        return None;
81    }
82    // Reject before iterating if the seed is outside the series branch. The
83    // seed underestimates `x` for `b < 1` and overestimates it for `b > 1`, by
84    // a factor that is itself `1 + O(x)`, so a seed comfortably inside the
85    // branch keeps every iterate inside it.
86    let ratio_bound = (0.5_f64).ln() - b.max(1.0).ln();
87    if !(y <= ratio_bound) {
88        return None;
89    }
90    let ln_p = p.ln();
91    for _ in 0..BETA_NEWTON_MAX_STEPS {
92        let x = y.exp();
93        if x * b.max(1.0) > 0.5 {
94            return None;
95        }
96        let (sum, derivative_sum) = beta_ascending_series(x, a, b)?;
97        if !(sum.is_finite() && sum > 0.0 && derivative_sum.is_finite()) {
98            return None;
99        }
100        // `G(y) = a·y − ln B(a,b) + ln S(e^y) − ln p`.
101        let g = a * y - ln_b + sum.ln() - ln_p;
102        let g_prime = a + x * derivative_sum / sum;
103        if !(g.is_finite() && g_prime.is_finite() && g_prime > 0.0) {
104            return None;
105        }
106        let step = g / g_prime;
107        if !step.is_finite() {
108            return None;
109        }
110        y -= step;
111        // Absolute in `y` is relative in `x`, which is the whole point.
112        if step.abs() <= f64::EPSILON * y.abs().max(1.0) {
113            break;
114        }
115    }
116    let x = y.exp();
117    if x.is_finite() && (0.0..=1.0).contains(&x) {
118        Some(x)
119    } else {
120        None
121    }
122}
123
124/// `(S(x), S′(x))` for `S(x) = Σ_{k≥0} (1−b)_k · x^k / (k!·(a+k))`.
125///
126/// Accumulated by the ratio `t_{k+1} = t_k·(k+1−b)/(k+1)` on the Pochhammer
127/// factor, so no factorial or gamma is formed. `None` if the guard term count
128/// is exhausted, which the caller's branch condition makes unreachable.
129fn beta_ascending_series(x: f64, a: f64, b: f64) -> Option<(f64, f64)> {
130    let mut pochhammer_over_factorial = 1.0_f64;
131    let mut power = 1.0_f64;
132    let mut sum = 1.0 / a;
133    let mut derivative_sum = 0.0_f64;
134    for k in 1..=BETA_SERIES_MAX_TERMS {
135        let kf = k as f64;
136        pochhammer_over_factorial *= (kf - b) / kf;
137        let coefficient = pochhammer_over_factorial / (a + kf);
138        // `power` holds `x^{k-1}` here, which is what `S′` wants.
139        derivative_sum += kf * coefficient * power;
140        power *= x;
141        let term = coefficient * power;
142        sum += term;
143        if term.abs() <= f64::EPSILON * sum.abs() {
144            return Some((sum, derivative_sum));
145        }
146    }
147    None
148}
149
150/// `I_x(a,b)` from `ln(x)`, retaining a representable result when `x` itself
151/// underflows.
152///
153/// The ordinary `beta_reg(a,b,x)` interface necessarily loses every result
154/// whose beta argument is below the smallest subnormal, even when the
155/// regularized integral is much larger because `a < 1`. On the derived
156/// ascending-series branch, `x` appears only in the well-scaled correction
157/// `S(x)` while its leading power stays in log space:
158///
159/// `ln I_x(a,b) = a·ln(x) − ln B(a,b) + ln S(x)`.
160///
161/// The same term-ratio proof used by [`lower_tail_beta_quantile`] supplies the
162/// branch boundary. Outside that boundary, the ordinary regularized-beta
163/// implementation receives a representable argument and remains the canonical
164/// general evaluator.
165fn regularized_beta_lower_from_log_x(log_x: f64, a: f64, b: f64) -> f64 {
166    if !(a.is_finite() && a > 0.0 && b.is_finite() && b > 0.0)
167        || log_x.is_nan()
168        || log_x > 0.0
169    {
170        return f64::NAN;
171    }
172    if log_x == 0.0 {
173        return 1.0;
174    }
175    if log_x == f64::NEG_INFINITY {
176        return 0.0;
177    }
178
179    let series_limit = (0.5_f64).ln() - b.max(1.0).ln();
180    if log_x <= series_limit {
181        let x = log_x.exp();
182        let Some((sum, _)) = beta_ascending_series(x, a, b) else {
183            return f64::NAN;
184        };
185        let log_beta = ln_beta(a, b);
186        if !(sum.is_finite() && sum > 0.0 && log_beta.is_finite()) {
187            return f64::NAN;
188        }
189        return (a * log_x - log_beta + sum.ln()).exp();
190    }
191
192    beta_reg(a, b, log_x.exp())
193}
194
195/// `ln(1 / (1 + exp(log_ratio)))` without overflowing or rounding a
196/// representable small unit fraction to zero.
197#[inline]
198fn log_reciprocal_one_plus_exp(log_ratio: f64) -> f64 {
199    if log_ratio <= 0.0 {
200        -log_ratio.exp().ln_1p()
201    } else {
202        -log_ratio - (-log_ratio).exp().ln_1p()
203    }
204}
205
206/// Guard term count for [`beta_ascending_series`]. The caller's `x·max(1,b) ≤ ½`
207/// branch bounds every term ratio by `½`, so the series reaches one ulp of an
208/// `O(1/a)` partial sum in at most `53` terms; this is the non-convergence
209/// guard, not the expected count.
210const BETA_SERIES_MAX_TERMS: usize = 128;
211
212/// Guard step count for the log-space Newton. From a seed whose relative error
213/// is `O(x)` the iteration is quadratic, so it converges in two or three steps
214/// over the whole branch; this is the non-convergence guard.
215const BETA_NEWTON_MAX_STEPS: usize = 32;
216
217/// The part of `x·x` that `f64` cannot hold: `x² = x*x + square_residual(x)`,
218/// exactly, for every `x` whose square neither overflows nor goes subnormal.
219///
220/// This exists because of what `exp` does to a squared argument. Rounding
221/// `x*x` perturbs it by at most `ulp(x²)/2` — a RELATIVE perturbation of
222/// `ε/2`, which is unremarkable on its own. But `exp` converts a relative
223/// perturbation `δ` of its ARGUMENT into a relative perturbation `x²·δ` of
224/// its RESULT, so `exp(x*x)` carries `x²·ε/2` relative error: `3.7e-14` at
225/// `x = 26`, and `7.7e-14` at the `x ≈ 37` where `φ(x)` finally underflows.
226/// That is two orders worse than the `exp` evaluation's own rounding, and it
227/// is the error `erfcx` and `normal_pdf` were both actually delivering.
228///
229/// The residual is the whole of that discarded term and is itself exactly
230/// representable (Dekker's two-product theorem, in its one-FMA form), so
231/// `exp(x²) = exp(x*x)·exp(residual)` and `exp(residual) = 1 + residual` to
232/// `O(residual²)` — below `1e-27` over the entire domain either caller uses.
233/// One multiply by `1 + residual` therefore buys back every digit, and the
234/// callers below apply it fused so the correction itself costs one more
235/// rounding and nothing else.
236///
237/// `mul_add` is a single instruction wherever FMA is in the baseline ISA
238/// (aarch64, and x86-64 built with `+fma`); on a baseline x86-64 build it is
239/// a `glibc` call, measured at ~2.5 ns. Against `erfcx`'s 38 ns that is 9%;
240/// against `normal_pdf`'s 6.2 ns it is 40% of a function that is nowhere the
241/// bottleneck of a row loop that also assembles a design row and a Hessian
242/// block. Both callers guard the pathological arguments BEFORE calling this,
243/// so it never has to defend `±∞` (whose residual would be `NaN`).
244#[inline]
245fn square_residual(x: f64, rounded_square: f64) -> f64 {
246    x.mul_add(x, -rounded_square)
247}
248
249/// Standard normal PDF phi(x).
250///
251/// The squared argument is carried exactly (see `square_residual`); without
252/// that, `exp(-½·fl(x*x))` degrades like `x²·ε/2` and reaches `5.7e-14`
253/// relative before `φ` underflows, against the `3.3e-16` it holds with.
254#[inline]
255pub fn normal_pdf(x: f64) -> f64 {
256    const INV_SQRT_2PI: f64 = 0.398_942_280_401_432_7;
257    let rounded_square = x * x;
258    let head = INV_SQRT_2PI * (-0.5 * rounded_square).exp();
259    if head == 0.0 || head.is_nan() {
260        // The pdf underflowed or `x` was `±∞` (head `0`), or `x` was `NaN`.
261        // Neither admits a relative correction, and `±∞` would feed the
262        // residual an `∞ − ∞`; return the limit the plain form gives.
263        return head;
264    }
265    let residual = square_residual(x, rounded_square);
266    head.mul_add(-0.5 * residual, head)
267}
268
269/// Standard normal CDF Phi(x) evaluated via the exact special-function identity
270///
271///   Phi(x) = 0.5 * erfc(-x / sqrt(2)).
272///
273/// This is the exact Gaussian CDF semantics used throughout the codebase. The
274/// numerical `erfc` implementation may use internal approximations, but the
275/// returned function is the standard normal CDF itself rather than a separate
276/// polynomial surrogate surface.
277#[inline]
278pub fn normal_cdf(x: f64) -> f64 {
279    0.5 * erfc(-x / std::f64::consts::SQRT_2)
280}
281
282/// Two-sided standard-normal probability `P(|Z| ≥ |z|)`.
283///
284/// The exact symmetric identity is `erfc(|z|/√2)`. Evaluating that identity
285/// directly avoids both the cancellation in `2·(1 − Φ(|z|))` and an
286/// unnecessary rounding from multiplying a one-sided tail by two.
287#[inline]
288pub fn normal_two_sided_probability(z: f64) -> f64 {
289    erfc(z.abs() / std::f64::consts::SQRT_2)
290}
291
292/// Two-sided Student-t probability `P(|T_ν| ≥ |t|)`.
293///
294/// For finite `ν > 0`,
295///
296/// `P(|T_ν| ≥ |t|) = I_x(ν/2, 1/2)`, `x = ν / (ν + t²)`.
297///
298/// Neither `t²` nor `x` is formed directly. Their ratio is carried as
299/// `ln(t²/ν)`, and the regularized beta receives `ln(x)`. This matters beyond
300/// avoiding overflow: for `ν = 1` and `t = f64::MAX`, `x` underflows to zero
301/// although the Cauchy tail is still a representable subnormal. The log-beta
302/// series preserves that probability. Invalid degrees of freedom produce
303/// `NaN`; infinite statistics map to the exact limiting probability zero.
304pub fn student_t_two_sided_probability(t: f64, degrees_of_freedom: f64) -> f64 {
305    let half_df = 0.5 * degrees_of_freedom;
306    if t.is_nan()
307        || !(degrees_of_freedom.is_finite()
308            && degrees_of_freedom > 0.0
309            && half_df > 0.0)
310    {
311        return f64::NAN;
312    }
313    if t.is_infinite() {
314        return 0.0;
315    }
316
317    let log_t_squared_over_df = 2.0 * t.abs().ln() - degrees_of_freedom.ln();
318    let log_x = log_reciprocal_one_plus_exp(log_t_squared_over_df);
319    regularized_beta_lower_from_log_x(log_x, half_df, 0.5)
320}
321
322/// Chi-squared survival probability `P(X_ν > statistic)`.
323///
324/// Uses the regularized upper incomplete gamma directly instead of
325/// reconstructing a small tail as `1 − P(ν/2, statistic/2)`.
326pub fn chi_square_sf(statistic: f64, degrees_of_freedom: f64) -> f64 {
327    let half_df = 0.5 * degrees_of_freedom;
328    if statistic.is_nan()
329        || statistic < 0.0
330        || !(degrees_of_freedom.is_finite()
331            && degrees_of_freedom > 0.0
332            && half_df > 0.0)
333    {
334        return f64::NAN;
335    }
336    if statistic == 0.0 {
337        return 1.0;
338    }
339    if statistic == f64::INFINITY {
340        return 0.0;
341    }
342    gamma_ur(half_df, 0.5 * statistic)
343}
344
345/// One `λ_j · χ²_{h_j}` term of a linear combination of independent
346/// chi-squares, with the weight's SIGN and the term's degrees of freedom both
347/// carried explicitly.
348///
349/// Two things separate this from the `&[f64]` weight list
350/// `weighted_chi_square_sf` takes, and each of them is a distribution the
351/// one-degree-of-freedom non-negative form cannot express:
352///
353/// * **A negative weight makes a RATIO a tail.** `P(A/B > t)` for independent
354///   non-negative `A`, `B` is `P(A − tB > 0)`, so every F-shaped reference —
355///   any statistic whose scale was estimated from the same data — is a
356///   *signed* combination evaluated at zero. The classical `F_{a,b}` is the
357///   two-term case `λ = (1, −t·a/b)`, `h = (a, b)`.
358/// * **A multiplicity is not `h` copies of a weight.** It is, mathematically,
359///   but the Imhof integrand costs one `atan` and one `ln` per TERM, and a
360///   residual sum of squares carries `n − p` unit weights. Folding them into
361///   one term with `h = n − p` is what makes an `n`-sized reference cost the
362///   same as a `p`-sized one.
363#[derive(Clone, Copy, Debug, PartialEq)]
364pub struct WeightedChiSquareTerm {
365    /// `λ_j`, of either sign. A zero weight contributes nothing and is dropped.
366    pub weight: f64,
367    /// `h_j > 0`. Real rather than integral: a two-moment summary of a spectrum
368    /// is a chi-square with a fractional shape, and this type is what carries it.
369    pub degrees_of_freedom: f64,
370}
371
372/// `signed_weighted_chi_square_sf` at a caller-chosen absolute accuracy,
373/// returning the bound actually achieved alongside the value.
374///
375/// # Method
376///
377/// Imhof's (1961) inversion in its general central form, of which the
378/// non-negative unit-`h` case documented on `weighted_chi_square_sf` is the
379/// specialization:
380///
381/// ```text
382/// P(Q > x) = 1/2 + (1/π) ∫_0^∞ sin θ(u) / (u ρ(u)) du,
383/// θ(u) = ½ Σ_j h_j arctan(λ_j u) − ½ x u,
384/// ρ(u) = Π_j (1 + λ_j² u²)^{h_j/4}.
385/// ```
386///
387/// Nothing in the derivation asks `λ_j > 0` — `arctan` is odd and `λ²` is even,
388/// so a negative weight simply turns its part of the phase the other way.
389///
390/// # Two truncation bounds, because one of them stops working at `x = 0`
391///
392/// The oscillatory bound `16/(x·U·ρ(U))` documented on
393/// `weighted_chi_square_sf` divides by `x`, and the ratio references this
394/// signed form exists for are evaluated at exactly `x = 0`, where the phase
395/// stops turning at all: `θ(u) → (π/4)·Σ_j h_j·sgn(λ_j)`, a constant. There is
396/// no oscillation left to cancel, so the alternating-series argument yields
397/// nothing.
398///
399/// What replaces it is the AMPLITUDE, which the same `x = 0` makes strong
400/// rather than weak. For `u ≥ U` and `t = u/U ≥ 1`,
401/// `(1 + λ²u²)/(1 + λ²U²) ≥ (1 + t²)/2 ≥ t` on every term ACTIVE at `U`
402/// (`|λ_j|·U ≥ 1`) and `≥ 1` on the rest, so `ρ(u) ≥ ρ(U)·t^{H/4}` with
403/// `H = Σ_{active} h_j` and
404///
405/// ```text
406/// |tail(U)| ≤ ∫_U^∞ du/(u ρ(u)) ≤ 4 / (H · ρ(U)).
407/// ```
408///
409/// This is a bound on the answer, not a guess about it, and it is the CHEAP
410/// one exactly where the oscillatory bound is unavailable: a ratio reference
411/// carries the residual `χ²_{n−p}`, so `H` is of order `n` and `ρ` grows like
412/// `U^{n/2}` — a handful of panels. Both bounds are evaluated and the smaller
413/// is taken, which also strictly improves the non-negative case at small `x`,
414/// where `16/(x·U·ρ)` is what used to make the sweep long.
415///
416/// # Phase monotonicity, generalized
417///
418/// The oscillatory bound is valid only past the point where `|θ′| ≥ x/4`. With
419/// mixed signs `φ′(u) = ½ Σ_j h_j λ_j/(1 + λ_j²u²)` is no longer monotone in
420/// `u`, so the test is applied to `½ Σ_j h_j |λ_j|/(1 + λ_j²u²)` — an upper
421/// bound on `|φ′|` that IS decreasing, hence a condition at `U` that holds for
422/// every `u ≥ U`. On non-negative weights the two expressions coincide.
423///
424/// # Exact special cases
425///
426/// * no nonzero weight — `Q ≡ 0`;
427/// * all weights positive and `x ≤ 0`, or all negative and `x ≥ 0` — the
428///   inequality is decided by the support;
429/// * all weights bit-identical — `Q = λ·χ²_{Σh}` exactly, on either sign.
430///
431/// Returns `NaN` if any weight is non-finite, if any degrees-of-freedom is not
432/// finite and positive, or if `statistic` is `NaN`.
433pub fn signed_weighted_chi_square_sf_to_tolerance(
434    terms: &[WeightedChiSquareTerm],
435    statistic: f64,
436    absolute_tolerance: f64,
437) -> (f64, f64) {
438    let tolerance = if absolute_tolerance.is_finite() && absolute_tolerance > 0.0 {
439        absolute_tolerance
440    } else {
441        WEIGHTED_CHI_SQUARE_TOLERANCE
442    };
443    if statistic.is_nan() {
444        return (f64::NAN, f64::NAN);
445    }
446    let mut active = Vec::with_capacity(terms.len());
447    for term in terms {
448        if !term.weight.is_finite()
449            || !(term.degrees_of_freedom.is_finite() && term.degrees_of_freedom > 0.0)
450        {
451            return (f64::NAN, f64::NAN);
452        }
453        if term.weight != 0.0 {
454            active.push(*term);
455        }
456    }
457    if active.is_empty() {
458        // `Q` is identically zero: it exceeds a negative threshold with
459        // certainty and a non-negative one never.
460        return (if statistic < 0.0 { 1.0 } else { 0.0 }, 0.0);
461    }
462    let all_positive = active.iter().all(|term| term.weight > 0.0);
463    let all_negative = active.iter().all(|term| term.weight < 0.0);
464    if all_positive && statistic <= 0.0 {
465        // `Q > 0` almost surely once one weight is positive.
466        return (1.0, 0.0);
467    }
468    if all_negative && statistic >= 0.0 {
469        // `Q < 0` almost surely once every weight is negative.
470        return (0.0, 0.0);
471    }
472    let first = active[0].weight;
473    if active.iter().all(|term| term.weight == first) {
474        let total_df: f64 = active.iter().map(|term| term.degrees_of_freedom).sum();
475        // `P(λ·χ² > x)` is the χ² upper tail at `x/λ` for `λ > 0` and the LOWER
476        // tail there for `λ < 0`, because dividing by a negative number turns
477        // the inequality around.
478        let scaled = statistic / first;
479        let tail = if first > 0.0 {
480            chi_square_sf(scaled, total_df)
481        } else {
482            1.0 - chi_square_sf(scaled, total_df)
483        };
484        return (tail, 0.0);
485    }
486    imhof_survival(&active, statistic, tolerance)
487}
488
489/// Default absolute accuracy `weighted_chi_square_sf` certifies on its Imhof
490/// truncation. It is four orders below the smallest probability any consumer
491/// of a survival function resolves in practice and eleven below one, so the
492/// truncation is never the term that limits a reported tail.
493pub const WEIGHTED_CHI_SQUARE_TOLERANCE: f64 = 1e-11;
494
495/// Gauss-Legendre nodes and weights on `[-1, 1]`, 16 points. A 16-node rule is
496/// exact through degree 31, which is far beyond the smooth amplitude
497/// `1/(u ρ(u))` over one phase period; the panel width, not the node count, is
498/// what resolves the oscillation.
499const GAUSS_LEGENDRE_16: [(f64, f64); 8] = [
500    (0.095_012_509_837_637_44, 0.189_450_610_455_068_64),
501    (0.281_603_550_779_258_9, 0.182_603_415_044_923_64),
502    (0.458_016_777_657_227_37, 0.169_156_519_395_002_65),
503    (0.617_876_244_402_643_8, 0.149_595_988_816_576_7),
504    (0.755_404_408_355_003, 0.124_628_971_255_534_07),
505    (0.865_631_202_387_831_8, 0.095_158_511_682_492_6),
506    (0.944_575_023_073_232_6, 0.062_253_523_938_647_456),
507    (0.989_400_934_991_649_9, 0.027_152_459_411_754_176),
508];
509
510/// Imhof's integrand `sin θ(u) / (u ρ(u))` with the `u → 0` limit folded in.
511#[inline]
512fn imhof_integrand(terms: &[WeightedChiSquareTerm], statistic: f64, u: f64) -> f64 {
513    if u == 0.0 {
514        let mean: f64 = terms
515            .iter()
516            .map(|term| term.weight * term.degrees_of_freedom)
517            .sum();
518        return 0.5 * (mean - statistic);
519    }
520    let mut phase = -0.5 * statistic * u;
521    let mut log_rho = 0.0;
522    for term in terms {
523        let wu = term.weight * u;
524        phase += 0.5 * term.degrees_of_freedom * wu.atan();
525        log_rho += 0.25 * term.degrees_of_freedom * wu.mul_add(wu, 1.0).ln();
526    }
527    phase.sin() / (u * log_rho.exp())
528}
529
530/// `ln ρ(u)`, the Imhof amplitude exponent.
531#[inline]
532fn imhof_log_rho(terms: &[WeightedChiSquareTerm], u: f64) -> f64 {
533    terms
534        .iter()
535        .map(|term| {
536            let wu = term.weight * u;
537            0.25 * term.degrees_of_freedom * wu.mul_add(wu, 1.0).ln()
538        })
539        .sum()
540}
541
542/// `½ Σ_j h_j|w_j|/(1 + w_j²u²)`, a DECREASING upper bound on the magnitude of
543/// the non-linear part of the phase's own derivative.
544///
545/// The oscillatory truncation bound is valid only past the point where the
546/// phase is monotone with `|θ'| ≥ x/4`, which needs `|φ'(u)| ≤ x/4` for every
547/// `u` past the truncation point rather than at it. With mixed-sign weights
548/// `φ'` is not monotone, so the test is applied to this bound instead; on
549/// non-negative weights the two are the same expression.
550#[inline]
551fn imhof_phase_slack(terms: &[WeightedChiSquareTerm], u: f64) -> f64 {
552    terms
553        .iter()
554        .map(|term| {
555            let wu = term.weight * u;
556            0.5 * term.degrees_of_freedom * term.weight.abs() / wu.mul_add(wu, 1.0)
557        })
558        .sum()
559}
560
561/// `4/(H·ρ(U))`, the AMPLITUDE truncation bound, with
562/// `H = Σ_{|w_j|U ≥ 1} h_j` the degrees of freedom already active at `U`.
563///
564/// Valid unconditionally — it bounds `∫_U^∞ du/(u ρ(u))` and never looks at the
565/// phase — and it is the only bound available at `statistic = 0`, where the
566/// oscillatory one divides by zero. `None` when nothing is active yet, since
567/// `ρ` is then still flat and there is no decay to integrate against.
568/// Panel width that resolves the integrand's AMPLITUDE, as opposed to its
569/// phase.
570///
571/// The phase rule below sizes a panel so it sweeps at most one oscillation.
572/// That is necessary and it is not sufficient: `1/(u ρ(u))` has structure of
573/// its own, on the scale `1/|λ|` where `(1 + λ²u²)^{h/4}` turns over, and a
574/// panel far wider than that scale is a 16-node rule aliasing a factor it never
575/// sampled. The two rules coincide only when the phase happens to turn at the
576/// same rate the amplitude does — which is exactly what fails when the phase
577/// rate is small: a ratio reference is evaluated at `statistic = 0`, and a
578/// two-term `F`-shaped combination can have `Σ h_j|λ_j|` of order one while
579/// `max_j|λ_j|` is also of order one, so `4π/Σ h|λ| ≈ 12` against an amplitude
580/// scale of `1`. Measured on `F_{1,5}` at `f = 0.05`: the phase-only panel
581/// returned `0.8319119` against the exact `0.8319122`, an error of `3.4e-7`
582/// certified at `1e-11`.
583///
584/// The scale is not a guess. As a function of complex `u` the integrand's
585/// nearest singularities are the branch points of `(1 + λ_j²u²)^{h_j/4}` at
586/// `u = ±i/|λ_j|`; the closest is `d = 1/max_j|λ_j|`, and the `−xu/2` phase and
587/// the `1/u` are entire and removable respectively. Gauss–Legendre with `N`
588/// nodes on a panel of half-width `a` converges like `ϱ^{-2N}` in the Bernstein
589/// parameter of the largest ellipse the integrand is analytic in, and an
590/// ellipse with semi-minor axis `d` has `ϱ` solving `(ϱ − 1/ϱ)/2 = d/a`. So
591/// asking `ϱ^{-2N} ≤ tolerance` fixes the half-width:
592///
593/// ```text
594/// ϱ = tolerance^{-1/2N},   a = d / [(ϱ − 1/ϱ)/2].
595/// ```
596///
597/// This is a RATE, not a certificate: the Bernstein bound also carries the
598/// integrand's maximum modulus on that ellipse, which the ellipse touching the
599/// branch point does not bound. The node count is what carries the margin, and
600/// the margin is MEASURED rather than asserted —
601/// `the_quadrature_resolves_the_amplitude_not_only_the_phase` compares against
602/// a reference at a far finer panel and reads the achieved error off it.
603///
604/// A looser request buys a wider panel here, which is the right direction: the
605/// consumer that derives its tolerance from the resolution of the statistic it
606/// is scoring pays for what it asked for.
607#[inline]
608fn imhof_amplitude_panel(max_abs_weight: f64, tolerance: f64) -> f64 {
609    let node_count = 2.0 * GAUSS_LEGENDRE_16.len() as f64;
610    let bernstein = tolerance.recip().powf(0.5 / node_count);
611    let semi_minor_ratio = 0.5 * (bernstein - bernstein.recip());
612    if !(semi_minor_ratio > 0.0 && max_abs_weight > 0.0) {
613        return f64::INFINITY;
614    }
615    2.0 / (max_abs_weight * semi_minor_ratio)
616}
617
618#[inline]
619fn imhof_amplitude_bound(terms: &[WeightedChiSquareTerm], u: f64) -> Option<f64> {
620    let active_df: f64 = terms
621        .iter()
622        .filter(|term| term.weight.abs() * u >= 1.0)
623        .map(|term| term.degrees_of_freedom)
624        .sum();
625    (active_df > 0.0).then(|| 4.0 / (active_df * imhof_log_rho(terms, u).exp()))
626}
627
628/// Cost backstop on the Imhof panel sweep.
629///
630/// The truncation point `U` needed for a given bound scales as
631/// `(16/(x·tol·C))^{2/(2+m)}` in the number `m` of weights that are *active*
632/// (`w_j U ≳ 1`) there, and the panel count as `U·x/4π`. With three or more
633/// comparable weights that count stays in the thousands for any statistic a
634/// likelihood-ratio consumer produces, so this backstop is unreachable — it
635/// exists for the one degenerate corner where it is not: two weights spread
636/// over several orders of magnitude, with a large statistic, where the sweep
637/// would otherwise run for tens of millions of panels to buy digits far below
638/// the modelling error of any statistic being referenced against it. The
639/// achieved bound is returned rather than discarded, so a caller that lands in
640/// that corner can see it instead of inferring it.
641///
642/// The panel width is the smaller of the phase rule and
643/// `imhof_amplitude_panel`, so the count above is a LOWER bound on what the
644/// sweep costs. It moves the corner slightly closer without changing which
645/// corner it is: the amplitude panel is `2/(|λ|_max·s(tol))`, independent of
646/// the statistic, so it binds where the phase rate is small — and a small phase
647/// rate is a small truncation point, which is the cheap end.
648pub const IMHOF_MAX_PANELS: usize = 1 << 21;
649
650fn imhof_survival(
651    terms: &[WeightedChiSquareTerm],
652    statistic: f64,
653    tolerance: f64,
654) -> (f64, f64) {
655    // A panel has to resolve the WHOLE phase, not just the `−xu/2` half. The
656    // total phase rate is bounded by `|θ'(u)| = |φ'(u) − x/2| ≤ (Σ h_j|w_j| +
657    // |x|)/2` — `|φ'|` is largest at the origin, where it is `½ Σ h_j|w_j|` —
658    // so a panel of `4π/(|x| + Σ h_j|w_j|)` sweeps at most one full oscillation
659    // anywhere on the half-line. Sizing on `4π/x` alone is correct only in the
660    // tail: at a small statistic that panel is enormous while the arctan part
661    // of the phase still turns over on the scale `1/w_j`, and the 16-node rule
662    // then aliases it (measured: a monotonicity violation of ~1e-5 at
663    // `x ≈ 4e-4`). At `x = 0` — the ratio references — the arctan part is the
664    // ONLY phase there is, and sizing on it is what keeps the rule honest.
665    let rate: f64 = terms
666        .iter()
667        .map(|term| term.degrees_of_freedom * term.weight.abs())
668        .sum();
669    let phase_panel = 4.0 * std::f64::consts::PI / (statistic.abs() + rate);
670    // ...and it has to resolve the AMPLITUDE as well; see
671    // `imhof_amplitude_panel` for why the phase rule alone is not enough and
672    // where the second scale comes from.
673    let max_abs_weight = terms
674        .iter()
675        .map(|term| term.weight.abs())
676        .fold(0.0_f64, f64::max);
677    let panel = phase_panel.min(imhof_amplitude_panel(max_abs_weight, tolerance));
678    let mut integral = 0.0_f64;
679    let mut lower = 0.0_f64;
680    let mut bound = f64::INFINITY;
681    for _ in 0..IMHOF_MAX_PANELS {
682        let upper = lower + panel;
683        let half = 0.5 * (upper - lower);
684        let mid = 0.5 * (upper + lower);
685        let mut panel_value = 0.0;
686        for &(node, weight) in &GAUSS_LEGENDRE_16 {
687            let offset = half * node;
688            panel_value += weight
689                * (imhof_integrand(terms, statistic, mid + offset)
690                    + imhof_integrand(terms, statistic, mid - offset));
691        }
692        integral += half * panel_value;
693        lower = upper;
694        // The amplitude bound holds unconditionally; the oscillatory one only
695        // once the phase is monotone, and only for a positive statistic.
696        // Whichever is available and smaller is the certified accuracy.
697        bound = imhof_amplitude_bound(terms, lower).unwrap_or(f64::INFINITY);
698        if statistic > 0.0 && imhof_phase_slack(terms, lower) <= 0.25 * statistic {
699            let oscillatory =
700                16.0 / (statistic * lower * imhof_log_rho(terms, lower).exp());
701            bound = bound.min(oscillatory);
702        }
703        if bound <= tolerance {
704            break;
705        }
706    }
707    (
708        (0.5 + integral / std::f64::consts::PI).clamp(0.0, 1.0),
709        bound,
710    )
711}
712
713/// Fisher-Snedecor survival probability `P(F_{d1,d2} > statistic)`.
714///
715/// The complementary regularized-beta identity is evaluated directly:
716///
717/// `I_x(d2/2, d1/2)`, `x = d2 / (d2 + d1·statistic)`.
718///
719/// The beta argument is derived in log space, so neither `d1·statistic` nor
720/// the denominator can overflow before a representable tail is recovered.
721pub fn fisher_snedecor_sf(
722    statistic: f64,
723    numerator_degrees_of_freedom: f64,
724    denominator_degrees_of_freedom: f64,
725) -> f64 {
726    let beta_a = 0.5 * denominator_degrees_of_freedom;
727    let beta_b = 0.5 * numerator_degrees_of_freedom;
728    if statistic.is_nan()
729        || statistic < 0.0
730        || !(numerator_degrees_of_freedom.is_finite()
731            && numerator_degrees_of_freedom > 0.0
732            && denominator_degrees_of_freedom.is_finite()
733            && denominator_degrees_of_freedom > 0.0
734            && beta_a > 0.0
735            && beta_b > 0.0)
736    {
737        return f64::NAN;
738    }
739    if statistic == 0.0 {
740        return 1.0;
741    }
742    if statistic == f64::INFINITY {
743        return 0.0;
744    }
745
746    let log_ratio = numerator_degrees_of_freedom.ln() + statistic.ln()
747        - denominator_degrees_of_freedom.ln();
748    let log_x = log_reciprocal_one_plus_exp(log_ratio);
749    regularized_beta_lower_from_log_x(log_x, beta_a, beta_b)
750}
751
752/// Scaled complementary error function `erfcx(x) = exp(x²) · erfc(x)`,
753/// specialized to the closed domain `x ∈ [0, +∞]`.
754///
755/// `+∞` maps to the exact limiting value `0`; `NaN` and negative inputs map to
756/// `NaN` because they violate this restricted kernel's domain. For
757/// `0 ≤ x < 26` the direct `exp(x²)·erfc(x)` form is finite. Beyond that point
758/// a six-correction asymptotic expansion avoids overflow while retaining the
759/// representable subnormal tail. At the switch, the first omitted term is
760/// below `2e-17` relative to the leading term.
761///
762/// The direct branch carries `x²` exactly (see `square_residual`). Without
763/// that correction the branch degraded like `x²·ε/2` — `1.4e-14` at `x = 10`,
764/// `5.7e-14` by the top of its range — while the asymptotic branch that takes
765/// over at `26` was already delivering `3e-16`. The seam was therefore a
766/// 190-fold step DOWN in error at the point where the code switches to what
767/// reads like the fallback, and the whole `[0, 26)` interval, where every
768/// probit / Mills / log-CDF consumer actually lives, was the inaccurate side.
769/// Both branches now hold `< 5e-16`, so the crossover is invisible.
770#[inline]
771pub fn erfcx_nonnegative(x: f64) -> f64 {
772    if x.is_nan() || x < 0.0 {
773        return f64::NAN;
774    }
775    if x == f64::INFINITY {
776        return 0.0;
777    }
778    if x < 26.0 {
779        // `x` is finite and in `[0, 26)`, so the square is exact-splittable and
780        // `head` is finite and strictly positive (`erfc(26⁻) ≈ 1e-295`).
781        let rounded_square = x * x;
782        let head = rounded_square.exp() * erfc(x);
783        head.mul_add(square_residual(x, rounded_square), head)
784    } else {
785        let inv = 1.0 / x;
786        let inv2 = inv * inv;
787        // erfcx(x) ~ 1/(sqrt(pi)x) * sum_n (-1)^n (2n-1)!!/(2x^2)^n.
788        // Horner form keeps the correction well scaled when `inv2` is tiny.
789        let poly = 1.0
790            + inv2
791                * (-0.5
792                    + inv2
793                        * (0.75
794                            + inv2
795                                * (-1.875
796                                    + inv2 * (6.5625 + inv2 * (-29.53125 + inv2 * 162.421875)))));
797        inv * poly * INV_SQRT_PI
798    }
799}
800
801/// Computes `log(1 - exp(-a))` for `a >= 0` without cancellation.
802#[inline]
803pub fn log1mexp_positive(a: f64) -> f64 {
804    assert!(a >= 0.0, "log1mexp_positive requires a >= 0: a={a}");
805    if a == f64::INFINITY {
806        // `e^{-∞}` is an exact zero, so the result is an exact (positive) zero
807        // rather than the `-0.0` that `ln_1p(-0.0)` would return.
808        return 0.0;
809    }
810    if a > core::f64::consts::LN_2 {
811        (-(-a).exp()).ln_1p()
812    } else if a > 0.0 {
813        (-(-a).exp_m1()).ln()
814    } else {
815        f64::NEG_INFINITY
816    }
817}
818
819// A finite binary64 is an integer multiple of 2^-1074. Its largest possible
820// significand occupies bits 2045..=2097 on that lattice. Thirty-three limbs
821// leave 14 carry bits, enough to sum at most 2^14-1 finite inputs exactly.
822const EXACT_BINARY64_SUM_WORDS: usize = 33;
823const EXACT_BINARY64_SUM_MAX_TERMS: usize = (1 << 14) - 1;
824const _: () = assert!(EXACT_BINARY64_SUM_WORDS * 64 == 2112);
825
826/// Why [`exact_binary64_sum_sign`] could not classify its finite exact sum.
827#[derive(Clone, Copy, Debug, Eq, PartialEq)]
828pub enum ExactBinary64SumSignError {
829    /// One input was not a finite binary64.
830    NonFiniteTerm { index: usize },
831    /// The fixed exact accumulator's structural term bound was exceeded.
832    TermCapacityExceeded { maximum: usize },
833}
834
835impl std::fmt::Display for ExactBinary64SumSignError {
836    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
837        match self {
838            Self::NonFiniteTerm { index } => {
839                write!(formatter, "exact binary64 sum term {index} is not finite")
840            }
841            Self::TermCapacityExceeded { maximum } => write!(
842                formatter,
843                "exact binary64 sum exceeds its structural {maximum}-term capacity"
844            ),
845        }
846    }
847}
848
849impl std::error::Error for ExactBinary64SumSignError {}
850
851/// Exact sign of a finite binary64 sum, independent of order and cancellation.
852///
853/// Every input is decoded as an integer significand on the common `2^-1074`
854/// lattice. Positive and negative magnitudes accumulate into separate fixed
855/// 2,112-bit unsigned integers; comparing those integers returns the sign of
856/// the exact real sum, with no floating-point reduction and no tolerance.
857///
858/// At most 16,383 terms are admitted, the largest count whose worst-case carry
859/// is structurally contained by the fixed accumulator.
860pub fn exact_binary64_sum_sign(
861    values: impl IntoIterator<Item = f64>,
862) -> Result<std::cmp::Ordering, ExactBinary64SumSignError> {
863    fn add_magnitude(
864        accumulator: &mut [u64; EXACT_BINARY64_SUM_WORDS],
865        value: f64,
866    ) -> Result<(), ExactBinary64SumSignError> {
867        let magnitude_bits = value.to_bits() & !(1_u64 << 63);
868        let exponent_bits = ((magnitude_bits >> 52) & 0x7ff) as usize;
869        let fraction = magnitude_bits & ((1_u64 << 52) - 1);
870        let (significand, shift) = if exponent_bits == 0 {
871            (fraction, 0usize)
872        } else {
873            ((1_u64 << 52) | fraction, exponent_bits - 1)
874        };
875        if significand == 0 {
876            return Ok(());
877        }
878
879        let mut word = shift / 64;
880        let offset = shift % 64;
881        let (low_sum, low_carry) =
882            accumulator[word].overflowing_add(significand << offset);
883        accumulator[word] = low_sum;
884        word += 1;
885
886        let high = if offset == 0 {
887            0
888        } else {
889            significand >> (64 - offset)
890        };
891        let (high_sum, high_carry) = accumulator[word].overflowing_add(high);
892        let (high_sum, carry_carry) = high_sum.overflowing_add(u64::from(low_carry));
893        accumulator[word] = high_sum;
894        let mut carry = high_carry || carry_carry;
895        word += 1;
896        while carry {
897            if word == EXACT_BINARY64_SUM_WORDS {
898                return Err(ExactBinary64SumSignError::TermCapacityExceeded {
899                    maximum: EXACT_BINARY64_SUM_MAX_TERMS,
900                });
901            }
902            let (sum, next_carry) = accumulator[word].overflowing_add(1);
903            accumulator[word] = sum;
904            carry = next_carry;
905            word += 1;
906        }
907        Ok(())
908    }
909
910    let mut positive = [0_u64; EXACT_BINARY64_SUM_WORDS];
911    let mut negative = [0_u64; EXACT_BINARY64_SUM_WORDS];
912    for (index, value) in values.into_iter().enumerate() {
913        if index == EXACT_BINARY64_SUM_MAX_TERMS {
914            return Err(ExactBinary64SumSignError::TermCapacityExceeded {
915                maximum: EXACT_BINARY64_SUM_MAX_TERMS,
916            });
917        }
918        if !value.is_finite() {
919            return Err(ExactBinary64SumSignError::NonFiniteTerm { index });
920        }
921        let target = if value.is_sign_negative() {
922            &mut negative
923        } else {
924            &mut positive
925        };
926        add_magnitude(target, value)?;
927    }
928    for index in (0..EXACT_BINARY64_SUM_WORDS).rev() {
929        match positive[index].cmp(&negative[index]) {
930            std::cmp::Ordering::Less => return Ok(std::cmp::Ordering::Less),
931            std::cmp::Ordering::Greater => return Ok(std::cmp::Ordering::Greater),
932            std::cmp::Ordering::Equal => {}
933        }
934    }
935    Ok(std::cmp::Ordering::Equal)
936}
937
938/// Numerically stable signed log-sum-exp.  Given pairs
939/// `(log|aⱼ|, sign(aⱼ))` (with `signs[j] ∈ {−1, 0, +1}`), returns
940/// `(log|S|, sign(S))` for `S = Σⱼ signs[j]·exp(log_mags[j])`.  Positive
941/// and negative magnitudes are first reduced together, after one common
942/// log-space rescaling, with a twofold compensated sum. This avoids rounding
943/// each same-sign subtotal through `ln` and `exp` before subtracting them — an
944/// avoidable loss that is amplified in cancellation-conditioned derivative
945/// cumulants. If the compensated residual lies inside its forward-error bound,
946/// the function instead uses the two-subtotal log-domain difference
947/// `log(|p − n|) = max(log p, log n) +
948/// log1mexp(|log p − log n|)`. That branch retains differences between two input
949/// logs even when their exponentials round to the same `f64`. When all signs are
950/// zero or all magnitudes are `−∞`, returns `(NEG_INFINITY, 0.0)`.
951///
952/// A `+∞` log-magnitude denotes an infinite-magnitude term (`exp(+∞) = +∞`)
953/// and dominates the sum: if it appears only with positive sign the result
954/// is `(+∞, +1)`; only with negative sign, `(+∞, −1)` (a log-magnitude of
955/// `+∞` with sign `−1` encodes the value `−∞`); with both signs the sum is
956/// the indeterminate `+∞ − ∞`, returned as `(NaN, 0.0)`.  A `−∞`
957/// log-magnitude is `exp(−∞) = 0` and is correctly dropped.
958pub fn signed_log_sum_exp(log_mags: &[f64], signs: &[f64]) -> (f64, f64) {
959    // Infinite-magnitude terms dominate any finite contribution, so resolve
960    // them before the finite log-sum-exp reduction below. `−∞` log-magnitudes
961    // are `exp(−∞) = 0` and need no special handling.
962    let mut has_pos_inf = false;
963    let mut has_neg_inf = false;
964    for (idx, &lm) in log_mags.iter().enumerate() {
965        if lm == f64::INFINITY {
966            if signs[idx] > 0.0 {
967                has_pos_inf = true;
968            } else if signs[idx] < 0.0 {
969                has_neg_inf = true;
970            }
971        }
972    }
973    match (has_pos_inf, has_neg_inf) {
974        // P = +∞, N = +∞ ⇒ indeterminate +∞ − ∞.
975        (true, true) => return (f64::NAN, 0.0),
976        // P = +∞, N < ∞ ⇒ S = +∞.
977        (true, false) => return (f64::INFINITY, 1.0),
978        // N = +∞, P < ∞ ⇒ S = −∞, encoded as log-magnitude +∞ with sign −1.
979        (false, true) => return (f64::INFINITY, -1.0),
980        (false, false) => {}
981    }
982
983    let mut pos_max = f64::NEG_INFINITY;
984    let mut neg_max = f64::NEG_INFINITY;
985    for (idx, &lm) in log_mags.iter().enumerate() {
986        if signs[idx] > 0.0 {
987            pos_max = pos_max.max(lm);
988        } else if signs[idx] < 0.0 {
989            neg_max = neg_max.max(lm);
990        }
991    }
992
993    if pos_max == f64::NEG_INFINITY && neg_max == f64::NEG_INFINITY {
994        // Both partial sums are empty: no terms at all, all signs zero, or every
995        // magnitude `−∞` (each `exp(−∞) = 0`). The signed sum is exactly `0`.
996        return (f64::NEG_INFINITY, 0.0);
997    }
998
999    // First reduce the signed terms directly after one common scaling. `head`
1000    // plus `tail` is a twofold sum: TwoSum recovers every addition's exact
1001    // residual, so cancellation does not discard the low part of either
1002    // same-sign subtotal before the final subtraction.
1003    let common_max = pos_max.max(neg_max);
1004    let mut signed_head = 0.0_f64;
1005    let mut signed_tail = 0.0_f64;
1006    let mut absolute_scaled_sum = 0.0_f64;
1007    let mut finite_term_count = 0usize;
1008    for (idx, &lm) in log_mags.iter().enumerate() {
1009        if !lm.is_finite() || !(signs[idx] > 0.0 || signs[idx] < 0.0) {
1010            continue;
1011        }
1012        let magnitude = (lm - common_max).exp();
1013        let term = if signs[idx] > 0.0 {
1014            magnitude
1015        } else {
1016            -magnitude
1017        };
1018        let combined = signed_head + term;
1019        let shifted = combined - signed_head;
1020        let residual = (signed_head - (combined - shifted)) + (term - shifted);
1021        signed_head = combined;
1022        signed_tail += residual;
1023        absolute_scaled_sum += magnitude;
1024        finite_term_count += 1;
1025    }
1026    let signed_scaled_sum = signed_head + signed_tail;
1027
1028    // Each scaled exponential and each accumulated residual contributes at most
1029    // one working-precision rounding. This conservative Wilkinson-style bound
1030    // decides from the operation count, rather than from a fitted threshold,
1031    // whether the linear-domain residual has a trustworthy sign and magnitude.
1032    // Below the bound, retain the input-log separation in the log-domain branch.
1033    let direct_error_bound =
1034        (finite_term_count as f64 + 2.0) * f64::EPSILON * absolute_scaled_sum;
1035    if signed_scaled_sum.abs() > direct_error_bound {
1036        return (
1037            common_max + signed_scaled_sum.abs().ln(),
1038            signed_scaled_sum.signum(),
1039        );
1040    }
1041
1042    // When exponentiation itself cannot resolve the signed residual, reduce
1043    // positive and negative groups separately in log space. Their internal sums
1044    // are still twofold-compensated before taking the logarithm.
1045    let mut pos_sum = 0.0_f64;
1046    let mut pos_tail = 0.0_f64;
1047    let mut neg_sum = 0.0_f64;
1048    let mut neg_tail = 0.0_f64;
1049    for (idx, &lm) in log_mags.iter().enumerate() {
1050        if !lm.is_finite() {
1051            continue;
1052        }
1053        if signs[idx] > 0.0 {
1054            let term = (lm - pos_max).exp();
1055            let combined = pos_sum + term;
1056            let shifted = combined - pos_sum;
1057            pos_tail += (pos_sum - (combined - shifted)) + (term - shifted);
1058            pos_sum = combined;
1059        } else if signs[idx] < 0.0 {
1060            let term = (lm - neg_max).exp();
1061            let combined = neg_sum + term;
1062            let shifted = combined - neg_sum;
1063            neg_tail += (neg_sum - (combined - shifted)) + (term - shifted);
1064            neg_sum = combined;
1065        }
1066    }
1067    pos_sum += pos_tail;
1068    neg_sum += neg_tail;
1069
1070    let log_pos = if pos_sum > 0.0 {
1071        pos_max + pos_sum.ln()
1072    } else {
1073        f64::NEG_INFINITY
1074    };
1075    let log_neg = if neg_sum > 0.0 {
1076        neg_max + neg_sum.ln()
1077    } else {
1078        f64::NEG_INFINITY
1079    };
1080
1081    if log_neg == f64::NEG_INFINITY {
1082        return (log_pos, 1.0);
1083    }
1084    if log_pos == f64::NEG_INFINITY {
1085        return (log_neg, -1.0);
1086    }
1087    if log_pos > log_neg {
1088        let gap = log_pos - log_neg;
1089        (log_pos + log1mexp_positive(gap), 1.0)
1090    } else if log_neg > log_pos {
1091        let gap = log_neg - log_pos;
1092        (log_neg + log1mexp_positive(gap), -1.0)
1093    } else {
1094        (f64::NEG_INFINITY, 0.0)
1095    }
1096}
1097
1098/// Numerically stable `ln Φ(x)` for the standard normal CDF. For `x ≥ 0`,
1099/// evaluates `ln(1 - 0.5 erfc(x/sqrt(2)))` with `ln_1p`, retaining the small
1100/// negative result after `Φ(x)` itself rounds to one. For `x < 0`, rewrites
1101/// `ln Φ(x) = −u² + ln(½·erfcx(u))`, `u = −x/√2`,
1102/// which preserves digits throughout the representable left tail without a
1103/// probability floor. Returns the corresponding IEEE limit at infinities and
1104/// propagates `NaN`.
1105#[inline]
1106pub fn normal_logcdf(x: f64) -> f64 {
1107    if x == f64::INFINITY {
1108        return 0.0;
1109    }
1110    if x == f64::NEG_INFINITY {
1111        return f64::NEG_INFINITY;
1112    }
1113    if x.is_nan() {
1114        return f64::NAN;
1115    }
1116    if x < 0.0 {
1117        let (u, scaled_tail) = negative_normal_tail_components(x);
1118        negative_normal_logcdf_from_scaled_tail(u, scaled_tail)
1119    } else {
1120        let upper_tail = 0.5 * erfc(x / std::f64::consts::SQRT_2);
1121        (-upper_tail).ln_1p()
1122    }
1123}
1124
1125/// Numerically stable `ln(1 − Φ(x)) = ln Φ(−x)` for the standard normal
1126/// survival function.  Delegates to `normal_logcdf(-x)` so the deep-right
1127/// tail benefits from the same `erfcx`-based representation.
1128#[inline]
1129pub fn normal_logsf(x: f64) -> f64 {
1130    normal_logcdf(-x)
1131}
1132
1133/// Joint evaluation of `ln Φ(x)` and the Mills-ratio analogue
1134/// `φ(x) / Φ(x)`, signed for the symmetric branch.  Used by the latent
1135/// probit families where the inverse-link gradient needs the ratio and
1136/// the likelihood needs the log-CDF on the same `x`; computing both in
1137/// one call shares the `erfcx` evaluation that dominates the cost in the
1138/// deep tail.
1139#[inline]
1140pub fn signed_probit_logcdf_and_mills_ratio(x: f64) -> (f64, f64) {
1141    if x == f64::INFINITY {
1142        return (0.0, 0.0);
1143    }
1144    if x == f64::NEG_INFINITY {
1145        return (f64::NEG_INFINITY, f64::INFINITY);
1146    }
1147    if x.is_nan() {
1148        return (f64::NAN, f64::NAN);
1149    }
1150    if x < 0.0 {
1151        let (u, scaled_tail) = negative_normal_tail_components(x);
1152        (
1153            negative_normal_logcdf_from_scaled_tail(u, scaled_tail),
1154            SQRT_2_OVER_PI / scaled_tail,
1155        )
1156    } else {
1157        let upper_tail = 0.5 * erfc(x / std::f64::consts::SQRT_2);
1158        let cdf = 1.0 - upper_tail;
1159        let lambda = normal_pdf(x) / cdf;
1160        ((-upper_tail).ln_1p(), lambda)
1161    }
1162}
1163
1164#[inline]
1165fn negative_normal_tail_components(x: f64) -> (f64, f64) {
1166    assert!(x.is_finite() && x < 0.0);
1167    let u = -x / std::f64::consts::SQRT_2;
1168    (u, erfcx_nonnegative(u))
1169}
1170
1171#[inline]
1172fn negative_normal_logcdf_from_scaled_tail(u: f64, scaled_tail: f64) -> f64 {
1173    -u * u + scaled_tail.ln() - std::f64::consts::LN_2
1174}
1175
1176/// Stable value and first four derivatives of `ln Φ(x)`.
1177///
1178/// The moderate regime uses the exact Mills-ratio recurrence, with the brackets
1179/// collected in `q = λ + x` once `x < 0` so that they do not cancel as `λ`
1180/// closes on `−x`. In the deep left tail, differentiating the Laplace continued
1181/// fraction
1182///
1183/// `φ(t)/Φ(-t) = t + 1/(t + 2/(t + 3/(...)))`, `t = -x`,
1184///
1185/// carries the small correction to `t` independently, so `f'' -> -1` and the
1186/// higher derivatives approach zero without subtracting nearly equal `f64`s.
1187/// In the right tail, signed log-magnitude sums preserve polynomially weighted
1188/// derivatives even when `φ(x)/Φ(x)` itself has rounded to zero.
1189#[inline]
1190pub fn normal_logcdf_derivatives(x: f64) -> [f64; 5] {
1191    if x.is_nan() {
1192        return [f64::NAN; 5];
1193    }
1194    if x == f64::INFINITY {
1195        return [0.0; 5];
1196    }
1197    if x == f64::NEG_INFINITY {
1198        return [f64::NEG_INFINITY, f64::INFINITY, -1.0, 0.0, 0.0];
1199    }
1200
1201    const RIGHT_LOG_MAGNITUDE_SWITCH: f64 = 8.0;
1202    if x <= LEFT_CONTINUED_FRACTION_SWITCH {
1203        return normal_logcdf_derivatives_left_tail(x);
1204    }
1205    if x >= RIGHT_LOG_MAGNITUDE_SWITCH {
1206        return normal_logcdf_derivatives_right_tail(x);
1207    }
1208
1209    let (log_cdf, lambda) = signed_probit_logcdf_and_mills_ratio(x);
1210    let x2 = x * x;
1211    if x < 0.0 {
1212        // Left of the origin the brackets below are collected in the SAME Mills
1213        // correction `q = λ + x` the continued-fraction branch carries, because
1214        // written in `λ` they cancel catastrophically long before the branch
1215        // ends. `λ(x) → −x` as `x → −∞`, so every term of, say,
1216        // `(x³−3x) + (7x²−4)λ + 12xλ² + 6λ³` grows like `|x|³` while their sum
1217        // decays: at `x = −4` they are `−52`, `456`, `−857`, `453` and add to
1218        // `−0.0023`, a cancellation of 380000 that costs eleven digits. In `q`
1219        // the same bracket is `−6q³ + 6xq² + (4−x²)q − x`, whose terms are
1220        // `−0.069`, `−1.22`, `−2.71`, `4` — a cancellation of 1847, three
1221        // orders milder. The reformulation is exact (`λ = q − x` substituted and
1222        // re-collected), costs the same flops, and buys 16–34x across the whole
1223        // branch: worst over `x ∈ [−4, 0]` falls from `4.5e−11` to `2.8e−12`.
1224        //
1225        // `q` itself is safe to form here: `λ/2 ≤ |x| ≤ 2λ` holds over most of
1226        // the range, so `λ + x` is EXACT by Sterbenz, and where it is not (`x`
1227        // near 0) `q` is the same size as `λ` and nothing cancels. That is the
1228        // whole reason the rewrite works — it moves the cancellation out of the
1229        // brackets and into a subtraction that has none.
1230        //
1231        // Past the origin `q → x` is no longer small, the `λ` form has nothing
1232        // to cancel (`λ → 0` and `x² − 1` dominates), and it is the more
1233        // accurate of the two — hence the sign test rather than a blanket swap.
1234        let q = lambda + x;
1235        let q2 = q * q;
1236        return [
1237            log_cdf,
1238            lambda,
1239            -lambda * q,
1240            lambda * (2.0 * q2 - x * q - 1.0),
1241            lambda * (-6.0 * q2 * q + 6.0 * x * q2 + (4.0 - x2) * q - x),
1242        ];
1243    }
1244    let lambda2 = lambda * lambda;
1245    let lambda3 = lambda2 * lambda;
1246    [
1247        log_cdf,
1248        lambda,
1249        -lambda * (x + lambda),
1250        lambda * (x2 - 1.0 + 3.0 * x * lambda + 2.0 * lambda2),
1251        -lambda
1252            * ((x * x2 - 3.0 * x) + (7.0 * x2 - 4.0) * lambda + 12.0 * x * lambda2 + 6.0 * lambda3),
1253    ]
1254}
1255
1256#[derive(Clone, Copy)]
1257struct MillsCorrectionDerivatives {
1258    value: f64,
1259    first: f64,
1260    second: f64,
1261    third: f64,
1262}
1263
1264/// `x` at or below which the left-tail Mills ratio is taken from the Laplace
1265/// continued fraction rather than from `erfcx`. Equivalently `t = −x ≥ 4`.
1266const LEFT_CONTINUED_FRACTION_SWITCH: f64 = -4.0;
1267
1268/// The Laplace continued-fraction **correction** to the left-tail Mills ratio,
1269///
1270/// `q(t) = λ(−t) − t = 1/(t + 2/(t + 3/(...)))`,   `λ(x) = φ(x)/Φ(x)`,
1271///
1272/// together with its first three derivatives in `t`. Requires `t ≥ 4`.
1273///
1274/// `q` is the whole content of the left tail that is NOT the leading `t`: it
1275/// decays like `1/t − 2/t³ + 10/t⁵ − ...`, and every operation building it is
1276/// a division or an addition of positive quantities, so it carries full
1277/// relative precision no matter how small it gets. That is the property its
1278/// two consumers need, and it is why the correction is returned separately
1279/// instead of pre-added to `t`:
1280///
1281/// * [`normal_logcdf_derivatives_left_tail`] needs `f'' = −(1 + q')` and the
1282///   higher derivatives, which tend to `−1` and `0` and would be destroyed by
1283///   differencing nearly equal `f64`s.
1284/// * [`cone_boundary_log_factor_and_derivatives`] needs `∂corr/∂a = b − q(t)`,
1285///   which is the same statement one substitution away (#2306 §4).
1286///
1287/// Recovering `q` from a separately computed `λ` — `q = λ − t` — is exactly the
1288/// cancellation this exists to avoid, and it is not a small effect: at `t = 1e8`
1289/// it costs every significant digit, and past `t ≈ 2e8` it returns the wrong
1290/// SIGN. The reference itself has to be carried at ~120 decimal digits before it
1291/// reproduces what this recursion gives in binary64.
1292#[inline]
1293fn mills_correction_continued_fraction(t: f64) -> MillsCorrectionDerivatives {
1294    assert!(t.is_finite() && t >= 4.0);
1295    let mut q = MillsCorrectionDerivatives {
1296        value: 0.0,
1297        first: 0.0,
1298        second: 0.0,
1299        third: 0.0,
1300    };
1301    // The truncation error is damped by a product of the continued-fraction
1302    // sensitivities `n/(t + q)^2`, so the depth must be sized at `t = 4` — the
1303    // LEAST converged point of the domain, and the one the log-CDF branch sits
1304    // exactly on. Each successive derivative converges roughly 15x slower than
1305    // the last, because differentiating the recursion multiplies each level's
1306    // contribution by another factor of that same sensitivity. Measured against
1307    // a 60-digit reference at `t = 4`:
1308    //
1309    // ```text
1310    //            q         q'        q''       q'''
1311    //   32   1.9e-15    7.0e-14    1.4e-12    2.1e-11
1312    //   64   2.3e-23    1.4e-21    4.4e-20    1.0e-18
1313    // ```
1314    //
1315    // 32 levels is enough for the VALUE and nothing else: it leaves `q'''` — the
1316    // fourth log-CDF derivative — wrong in its eleventh digit. The depths that
1317    // first reach `1e-17` at `t = 4` are 41, 47, 53 and 60 for the four
1318    // channels, so 64 covers the worst of them with ~200x of margin, and the
1319    // requirement falls off fast enough (33 levels at `t = 6`, 24 at `t = 8`,
1320    // 12 at `t = 20`) that one constant sized for the edge is safe everywhere
1321    // above it. The extra levels are pure convergence — every step divides
1322    // positive quantities — so they cannot destabilise a large `t`.
1323    for n in (1..=64).rev() {
1324        let denominator = t + q.value;
1325        let inv_denominator = denominator.recip();
1326        let value = f64::from(n) / denominator;
1327        let denominator_first = 1.0 + q.first;
1328        let a = denominator_first * inv_denominator;
1329        let b = q.second * inv_denominator;
1330        let c = q.third * inv_denominator;
1331        q = MillsCorrectionDerivatives {
1332            value,
1333            first: -value * denominator_first / denominator,
1334            second: value * (2.0 * a * a - b),
1335            third: value * (-6.0 * a * a * a + 6.0 * a * b - c),
1336        };
1337    }
1338    q
1339}
1340
1341#[inline]
1342fn normal_logcdf_derivatives_left_tail(x: f64) -> [f64; 5] {
1343    assert!(x.is_finite() && x <= LEFT_CONTINUED_FRACTION_SWITCH);
1344    let t = -x;
1345    let q = mills_correction_continued_fraction(t);
1346    [
1347        normal_logcdf(x),
1348        t + q.value,
1349        -(1.0 + q.first),
1350        q.second,
1351        -q.third,
1352    ]
1353}
1354
1355#[inline]
1356fn normal_logcdf_derivatives_right_tail(x: f64) -> [f64; 5] {
1357    assert!(x.is_finite() && x >= 8.0);
1358    const LOG_SQRT_2PI: f64 = 0.918_938_533_204_672_7;
1359    let log_cdf = normal_logcdf(x);
1360    let u = x / std::f64::consts::SQRT_2;
1361    let log_lambda = -u * u - LOG_SQRT_2PI - log_cdf;
1362    let log_x = x.ln();
1363    let inv_x2 = x.recip() * x.recip();
1364
1365    let first = log_lambda.exp();
1366    let second = signed_exp_sum(&[log_x + log_lambda, 2.0 * log_lambda], &[-1.0, -1.0]);
1367    let third = signed_exp_sum(
1368        &[
1369            2.0 * log_x + (-inv_x2).ln_1p() + log_lambda,
1370            3.0_f64.ln() + log_x + 2.0 * log_lambda,
1371            2.0_f64.ln() + 3.0 * log_lambda,
1372        ],
1373        &[1.0, 1.0, 1.0],
1374    );
1375    let fourth = signed_exp_sum(
1376        &[
1377            3.0 * log_x + (-3.0 * inv_x2).ln_1p() + log_lambda,
1378            7.0_f64.ln() + 2.0 * log_x + (-(4.0 / 7.0) * inv_x2).ln_1p() + 2.0 * log_lambda,
1379            12.0_f64.ln() + log_x + 3.0 * log_lambda,
1380            6.0_f64.ln() + 4.0 * log_lambda,
1381        ],
1382        &[-1.0, -1.0, -1.0, -1.0],
1383    );
1384    [log_cdf, first, second, third, fourth]
1385}
1386
1387#[inline]
1388fn signed_exp_sum(log_magnitudes: &[f64], signs: &[f64]) -> f64 {
1389    let (log_magnitude, sign) = signed_log_sum_exp(log_magnitudes, signs);
1390    if sign == 0.0 {
1391        0.0
1392    } else {
1393        sign * log_magnitude.exp()
1394    }
1395}
1396
1397#[inline]
1398fn acklam_lower_tail_quantile_from_log_probability(log_p: f64) -> f64 {
1399    const C: [f64; 6] = [
1400        -7.784_894_002_430_293e-3,
1401        -3.223_964_580_411_365e-1,
1402        -2.400_758_277_161_838,
1403        -2.549_732_539_343_734,
1404        4.374_664_141_464_968,
1405        2.938_163_982_698_783,
1406    ];
1407    const D: [f64; 4] = [
1408        7.784_695_709_041_462e-3,
1409        3.224_671_290_700_398e-1,
1410        2.445_134_137_142_996,
1411        3.754_408_661_907_416,
1412    ];
1413    let q = (-2.0 * log_p).sqrt();
1414    (((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
1415        / ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
1416}
1417
1418/// Standard normal quantile Φ⁻¹(p) using Acklam's rational approximation.
1419#[inline]
1420pub fn standard_normal_quantile(p: f64) -> Result<f64, String> {
1421    if !(p.is_finite() && p > 0.0 && p < 1.0) {
1422        return Err(format!("normal quantile requires p in (0,1), got {p}"));
1423    }
1424
1425    const A: [f64; 6] = [
1426        -3.969_683_028_665_376e1,
1427        2.209_460_984_245_205e2,
1428        -2.759_285_104_469_687e2,
1429        1.383_577_518_672_69e2,
1430        -3.066_479_806_614_716e1,
1431        2.506_628_277_459_239,
1432    ];
1433    const B: [f64; 5] = [
1434        -5.447_609_879_822_406e1,
1435        1.615_858_368_580_409e2,
1436        -1.556_989_798_598_866e2,
1437        6.680_131_188_771_972e1,
1438        -1.328_068_155_288_572e1,
1439    ];
1440    const P_LOW: f64 = 0.02425;
1441    const P_HIGH: f64 = 1.0 - P_LOW;
1442
1443    let mut x = if p < P_LOW {
1444        acklam_lower_tail_quantile_from_log_probability(p.ln())
1445    } else if p <= P_HIGH {
1446        let q = p - 0.5;
1447        let r = q * q;
1448        (((((A[0] * r + A[1]) * r + A[2]) * r + A[3]) * r + A[4]) * r + A[5]) * q
1449            / (((((B[0] * r + B[1]) * r + B[2]) * r + B[3]) * r + B[4]) * r + 1.0)
1450    } else {
1451        -acklam_lower_tail_quantile_from_log_probability((1.0 - p).ln())
1452    };
1453    for _ in 0..2 {
1454        let density = normal_pdf(x);
1455        if !(density.is_finite() && density > 0.0) {
1456            break;
1457        }
1458        // Residual F(x) − p, formed without catastrophic cancellation in
1459        // either tail. For an upper-tail iterate `x > 0`, `normal_cdf(x)`
1460        // saturates to ~1, so the direct `normal_cdf(x) − p` annihilates the
1461        // tiny residual the polish must act on; instead use the upper-tail
1462        // complement `F(x) − p = (1 − p) − 0.5·erfc(x/√2)`, where both terms
1463        // are the small upper-tail quantities (`1 − p` is exact by Sterbenz
1464        // for `p ∈ [½,1)`). For `x ≤ 0`, `normal_cdf(x) = 0.5·erfc(|x|/√2)` is
1465        // itself the faithfully carried small lower-tail value, so the direct
1466        // form is already cancellation-free.
1467        let residual = if (0.25..=0.75).contains(&p) {
1468            // Central band. Both tail forms below subtract two quantities of
1469            // size ~½, so their difference carries an absolute error of one ulp
1470            // of ½ (1.1e-16) NO MATTER how small the true residual is. Since
1471            // `Δx ≈ residual_error / φ(x)`, the returned quantile then carries a
1472            // FIXED absolute error ~1.2e-16 and a relative error ~1.2e-16/|x|
1473            // that diverges as `p → ½`: measured 4.1e-14 at `p = 0.50125` and
1474            // 1.2e-03 at `p = ½ + 2.75e-14`, against ~2e-16 everywhere else in
1475            // this module. The polish cannot repair the seed there — the
1476            // residual it is handed is quantized to multiples of one ulp of ½
1477            // and is usually exactly 0, so the answer that ships is the raw
1478            // Acklam seed at its own 1.15e-9.
1479            //
1480            // Subtracting the ½ ANALYTICALLY removes it: `F(x) − p` is
1481            // `(F(x) − ½) − (p − ½)` = `½·erf(x/√2) − δ`, and both terms are now
1482            // of size |δ| with full RELATIVE accuracy — `erf` near 0 is `z·R(z²)`,
1483            // no cancellation — so the residual error is `ε·|δ|` and the relative
1484            // error in `x` is `ε` uniformly, including in the limit `x → 0`.
1485            //
1486            // The band is the exactness domain of `δ`, not a tuning choice:
1487            // Sterbenz's lemma makes `p − ½` exact for `p ∈ [¼, 1]`, and the
1488            // reflection `p ↦ 1 − p` maps that onto `[0, ¾]`, so `[¼, ¾]` is
1489            // where δ is exact on both sides. It is also where the centered form
1490            // is the better one: outside it `|x| > 0.6745` and the tail forms
1491            // carry relative accuracy in their own small quantity, which is what
1492            // the deep tails need. At the shared boundary the two agree to
1493            // within a factor of two, so nothing steps across the seam.
1494            0.5 * erf(x / std::f64::consts::SQRT_2) - (p - 0.5)
1495        } else if x > 0.0 {
1496            (1.0 - p) - 0.5 * erfc(x / std::f64::consts::SQRT_2)
1497        } else {
1498            normal_cdf(x) - p
1499        };
1500        let correction = residual / density;
1501        let denominator = 1.0 + 0.5 * x * correction;
1502        if !(correction.is_finite() && denominator.is_finite() && denominator != 0.0) {
1503            break;
1504        }
1505        let step = correction / denominator;
1506        if !step.is_finite() {
1507            break;
1508        }
1509        x -= step;
1510        if step.abs() <= 2.0 * f64::EPSILON * x.abs().max(1.0) {
1511            break;
1512        }
1513    }
1514    Ok(x)
1515}
1516
1517/// Standard normal quantile from `log_p = ln Φ(x)`.
1518///
1519/// Unlike [`standard_normal_quantile`], this remains defined when `Φ(x)` is
1520/// smaller than the least positive `f64`, and when `Φ(x)` is so close to one
1521/// that exponentiating `log_p` rounds to exactly one. Acklam's lower-tail
1522/// approximation supplies the initial point; Newton polishing solves
1523/// `ln Φ(x) = log_p` with the stable log-CDF and Mills ratio, so neither tail
1524/// forms a probability-space subtraction.
1525#[inline]
1526pub fn standard_normal_quantile_from_log_cdf(log_p: f64) -> Result<f64, String> {
1527    if !(log_p.is_finite() && log_p < 0.0) {
1528        return Err(format!(
1529            "normal log-quantile requires finite log_p < 0, got {log_p}"
1530        ));
1531    }
1532
1533    if log_p > -std::f64::consts::LN_2 {
1534        // Reflect through the upper tail without forming `1 - exp(log_p)`.
1535        let log_q = (-log_p.exp_m1()).ln();
1536        return standard_normal_quantile_from_log_cdf(log_q).map(|x| -x);
1537    }
1538
1539    let p = log_p.exp();
1540    let mut x = if p > 0.0 {
1541        standard_normal_quantile(p)?
1542    } else {
1543        acklam_lower_tail_quantile_from_log_probability(log_p)
1544    };
1545    for _ in 0..4 {
1546        let (current_log_p, mills_ratio) = signed_probit_logcdf_and_mills_ratio(x);
1547        if !(current_log_p.is_finite() && mills_ratio.is_finite() && mills_ratio > 0.0) {
1548            break;
1549        }
1550        let step = (current_log_p - log_p) / mills_ratio;
1551        if !step.is_finite() {
1552            break;
1553        }
1554        x -= step;
1555        if step.abs() <= 2.0 * f64::EPSILON * x.abs().max(1.0) {
1556            break;
1557        }
1558    }
1559    Ok(x)
1560}
1561
1562#[cfg(test)]
1563mod tests {
1564    use super::*;
1565
1566    const TOL: f64 = 1e-12;
1567
1568    fn rel_err(got: f64, expected: f64) -> f64 {
1569        (got - expected).abs() / expected.abs().max(1e-300)
1570    }
1571
1572    #[test]
1573    fn student_t_primitives_keep_the_tail_that_one_minus_the_cdf_destroys() {
1574        // References are correctly rounded doubles from a 60-dps regularized
1575        // incomplete beta. The `nu = 10000, t = 10` row is here because a
1576        // plausible-looking hand-extrapolated literal for it (1.60e-23) is 20%
1577        // from the truth: this table has to come from the reference, not from
1578        // pattern-matching the rows above it.
1579        const ROWS: [(f64, f64, f64); 10] = [
1580            (5.0, 20.0, 2.887758186612086e-6),
1581            (5.0, 40.0, 9.205981085886477e-8),
1582            (30.0, 10.0, 2.2876257041148065e-11),
1583            (30.0, 20.0, 3.3745418328856434e-19),
1584            (30.0, 40.0, 6.863022597203202e-28),
1585            (500.0, 8.0, 4.3648313969400955e-15),
1586            (500.0, 10.0, 6.930246799119958e-22),
1587            (500.0, 20.0, 4.056001518093838e-66),
1588            (500.0, 40.0, 3.14532145912912e-158),
1589            (10000.0, 10.0, 9.816403714331914e-24),
1590        ];
1591        // Bar: 1e-11, a measured envelope rather than a derivation. Everything
1592        // below the incomplete beta is derivable -- the identity is exact and
1593        // forms no difference -- but `beta_reg` is statrs's continued fraction
1594        // and its error is a property of that implementation, so the honest
1595        // thing is to measure it and say so. Worst over this table by shape
1596        // parameter `a = nu/2`:
1597        //
1598        //     a = 2.5     2e-15
1599        //     a = 15      1.6e-13
1600        //     a = 250     2.3e-13
1601        //     a = 5000    2.0e-12
1602        //
1603        // It grows slowly with nu, which is what a continued fraction needing
1604        // more terms looks like, and it does *not* grow with tail depth -- the
1605        // nu = 500 rows sit at 2e-13 whether the answer is 1e-15 or 1e-158.
1606        // That is the distinction that matters: a fixed relative cost, not a
1607        // cancellation. Bar is 5x the worst measured.
1608        let bar = 1.0e-11;
1609        for (nu, t, want) in ROWS {
1610            let got = student_t_sf(t, nu);
1611            let rel = ((got - want) / want).abs();
1612            assert!(
1613                rel <= bar,
1614                "student_t_sf({t}, {nu}) = {got:e}, want {want:e}, relative {rel:e} > {bar:e}"
1615            );
1616            let got_two_sided = student_t_two_sided_probability(t, nu);
1617            let two_sided_rel = ((got_two_sided - 2.0 * want) / (2.0 * want)).abs();
1618            assert!(
1619                two_sided_rel <= bar,
1620                "student_t_two_sided_probability({t}, {nu}) = {got_two_sided:e}, \
1621                 want {:e}, relative {two_sided_rel:e} > {bar:e}",
1622                2.0 * want
1623            );
1624            // The reflection. `1 - want` is O(1), so its own absolute error of
1625            // one ulp is a relative error of one ulp -- which is exactly why
1626            // reflecting is safe here and reconstructing the small tail is not.
1627            let lower = student_t_sf(-t, nu);
1628            assert!(
1629                (lower - (1.0 - want)).abs() <= 2.0 * f64::EPSILON,
1630                "student_t_sf({}, {nu}) = {lower}, want {}",
1631                -t,
1632                1.0 - want
1633            );
1634        }
1635        // Symmetry at the median, and the degenerate arguments.
1636        for nu in [1.0_f64, 5.0, 1e4] {
1637            assert!(
1638                (student_t_sf(0.0, nu) - 0.5).abs() <= f64::EPSILON,
1639                "median at nu = {nu}"
1640            );
1641        }
1642        assert!(student_t_sf(1.0, 0.0).is_nan(), "nu = 0 is not a t");
1643        assert!(
1644            student_t_sf(1.0, f64::INFINITY).is_nan(),
1645            "nu = inf is not a t"
1646        );
1647        assert_eq!(student_t_sf(f64::INFINITY, 5.0), 0.0, "tail beyond +inf");
1648        assert_eq!(
1649            student_t_sf(f64::NEG_INFINITY, 5.0),
1650            1.0,
1651            "tail beyond -inf"
1652        );
1653    }
1654
1655    #[test]
1656    fn normal_sf_keeps_the_upper_tail_that_one_minus_the_cdf_destroys() {
1657        // `Φ(x)` rounds to exactly 1.0 once its upper tail drops below half an
1658        // ulp of one, so `1 - normal_cdf(x)` returns exactly zero from x ~ 8.3 up
1659        // and is already 7% high at x = 8. `normal_sf` computes the tail rather
1660        // than reconstructing it. References are correctly rounded doubles from a
1661        // 60-dps `erfc(x/√2)/2`.
1662        //
1663        // Bar: `x * x * eps`, which is derived rather than chosen. Forming the
1664        // argument `u = x / √2` rounds it, a relative eps, i.e. an absolute
1665        // `u * eps`. The relative condition number of `erfc` at `u` is
1666        // `u * |erfc'(u)| / erfc(u)`, and since `erfc(u) ~ exp(-u^2) / (u√π)` for
1667        // large `u` that tends to `2u^2 = x^2`. So the returned tail inherits
1668        // `x^2 * eps` from the argument alone, before `erfc`'s own couple of ulp
1669        // -- 36 ulp at x = 6, 1370 ulp at x = 37. That is intrinsic to taking a
1670        // z score as the input: the tail is exponentially steep in `x`, so the
1671        // last bit of `x` is worth `x^2` bits of the tail. It is also
1672        // irrelevant next to what it replaces, which is a relative error of 1.
1673        const ROWS: [(f64, f64); 13] = [
1674            (0.5, 0.3085375387259869),
1675            (2.0, 0.02275013194817921),
1676            (4.0, 3.1671241833119924e-5),
1677            (5.0, 2.866515718791933e-7),
1678            (6.0, 9.86587645037698e-10),
1679            (7.0, 1.279812543885835e-12),
1680            (8.0, 6.220960574271784e-16),
1681            (8.3, 5.205569744890254e-17),
1682            (9.0, 1.1285884059538405e-19),
1683            (12.0, 1.776482112077679e-33),
1684            (20.0, 2.7536241186062337e-89),
1685            (30.0, 4.906713927148187e-198),
1686            (37.0, 5.725571222524577e-300),
1687        ];
1688        for (x, want) in ROWS {
1689            let bar = (x * x + 2.0) * f64::EPSILON;
1690            let got = normal_sf(x);
1691            let rel = ((got - want) / want).abs();
1692            assert!(
1693                rel <= bar,
1694                "normal_sf({x}) = {got:e}, want {want:e}, relative {rel:e} > {bar:e}"
1695            );
1696            let got_two_sided = normal_two_sided_probability(x);
1697            let two_sided_rel = ((got_two_sided - 2.0 * want) / (2.0 * want)).abs();
1698            assert!(
1699                two_sided_rel <= bar,
1700                "normal_two_sided_probability({x}) = {got_two_sided:e}, \
1701                 want {:e}, relative {two_sided_rel:e} > {bar:e}",
1702                2.0 * want
1703            );
1704            // The value this replaces. Above the saturation point it is not a
1705            // less accurate answer, it is no answer.
1706            if x >= 8.3 {
1707                assert_eq!(
1708                    1.0 - normal_cdf(x),
1709                    0.0,
1710                    "1 - normal_cdf({x}) is expected to have saturated"
1711                );
1712            }
1713        }
1714        // Complementarity holds wherever the sum is representable, and the
1715        // symmetry that makes a two-sided p-value a single call.
1716        for x in [-3.0_f64, -0.25, 0.0, 0.25, 3.0] {
1717            let sum = normal_sf(x) + normal_cdf(x);
1718            assert!((sum - 1.0).abs() <= 2.0 * f64::EPSILON, "sf + cdf = {sum}");
1719            assert_eq!(normal_sf(-x), normal_cdf(x), "sf(-x) != cdf(x) at {x}");
1720        }
1721    }
1722
1723    /// The final representable normal two-sided tail is subnormal. This is a
1724    /// separate absolute/ULP assertion because a conventional relative-error
1725    /// helper with a normal-number floor would make the edge vacuous.
1726    #[test]
1727    fn normal_two_sided_tail_retains_subnormal_edge() {
1728        const EXPECTED_AT_38: f64 = 5.770_856_702_007_929e-316;
1729        let got = normal_two_sided_probability(38.0);
1730        let ulps = got.to_bits().abs_diff(EXPECTED_AT_38.to_bits());
1731        assert!(
1732            got.is_subnormal() && ulps <= 128,
1733            "two-sided normal tail at z=38: got {got:.17e}, \
1734             expected {EXPECTED_AT_38:.17e}, ulps {ulps}"
1735        );
1736        assert_eq!(normal_two_sided_probability(40.0), 0.0);
1737        assert_eq!(normal_two_sided_probability(f64::INFINITY), 0.0);
1738        assert!(normal_two_sided_probability(f64::NAN).is_nan());
1739    }
1740
1741    /// `t²` and then `ν/(ν+t²)` both underflow at this edge, but the Cauchy
1742    /// tail itself is still representable. The analytic Cauchy survival law is
1743    /// an independent oracle for the log-beta implementation.
1744    #[test]
1745    fn student_t_two_sided_tail_retains_subnormal_cauchy_edge() {
1746        const EXPECTED: f64 = 3.541_315_033_259_774_5e-309;
1747        let got = student_t_two_sided_probability(f64::MAX, 1.0);
1748        let analytic = 2.0 * (1.0 / f64::MAX).atan() / std::f64::consts::PI;
1749        let pinned_ulps = got.to_bits().abs_diff(EXPECTED.to_bits());
1750        let analytic_ulps = got.to_bits().abs_diff(analytic.to_bits());
1751        assert!(
1752            got.is_subnormal() && pinned_ulps <= 512 && analytic_ulps <= 512,
1753            "Cauchy tail at f64::MAX: got {got:.17e}, pinned {EXPECTED:.17e}, \
1754             analytic {analytic:.17e}, pinned ulps {pinned_ulps}, \
1755             analytic ulps {analytic_ulps}"
1756        );
1757    }
1758
1759    #[test]
1760    fn distribution_survival_primitives_define_boundaries_and_identities() {
1761        assert_eq!(normal_sf(f64::INFINITY), 0.0);
1762        assert_eq!(normal_sf(f64::NEG_INFINITY), 1.0);
1763        assert!(normal_sf(f64::NAN).is_nan());
1764
1765        assert_eq!(student_t_two_sided_probability(0.0, 7.0), 1.0);
1766        assert_eq!(student_t_sf(0.0, 7.0), 0.5);
1767        assert!(student_t_sf(f64::NAN, 7.0).is_nan());
1768
1769        assert_eq!(chi_square_sf(0.0, 3.0), 1.0);
1770        assert_eq!(chi_square_sf(f64::INFINITY, 3.0), 0.0);
1771        assert!(chi_square_sf(-1.0, 3.0).is_nan());
1772        assert!(chi_square_sf(1.0, 0.0).is_nan());
1773
1774        assert_eq!(fisher_snedecor_sf(0.0, 3.0, 20.0), 1.0);
1775        assert_eq!(
1776            fisher_snedecor_sf(f64::INFINITY, 3.0, 20.0),
1777            0.0
1778        );
1779        assert!(fisher_snedecor_sf(-1.0, 3.0, 20.0).is_nan());
1780        assert!(fisher_snedecor_sf(1.0, 0.0, 20.0).is_nan());
1781        assert!(fisher_snedecor_sf(1.0, 3.0, 0.0).is_nan());
1782
1783        // χ²₁ is the square of a standard normal; F₁,₁ is the square of a
1784        // Cauchy. These identities independently anchor both direct survival
1785        // implementations in a small-tail regime.
1786        let statistic = 160.0_f64;
1787        let chi_expected = normal_two_sided_probability(statistic.sqrt());
1788        let chi_got = chi_square_sf(statistic, 1.0);
1789        assert!(rel_err(chi_got, chi_expected) <= 2.0e-13);
1790
1791        let f_expected = student_t_two_sided_probability(statistic.sqrt(), 1.0);
1792        let f_got = fisher_snedecor_sf(statistic, 1.0, 1.0);
1793        assert!(rel_err(f_got, f_expected) <= 2.0e-13);
1794    }
1795
1796    #[test]
1797    /// The lower tail of a beta quantile, where `inv_beta_reg`'s absolute
1798    /// convergence tolerance in `x` used to stall (#2528).
1799    ///
1800    /// Shapes are the ones `gam_inference::probability` derives from a mean and
1801    /// a variance (`precision = mu(1-mu)/total_var - 1`), so every row is the
1802    /// lower endpoint of a 95% predictive interval a caller can actually ask
1803    /// for. References are an 80-digit bisection in `ln x` on
1804    /// `I_x(a,b) = p`; the `Beta(0.1, 0.1)` row is additionally checkable in
1805    /// closed form, since `I_x -> x^a/(a B(a,b))` gives
1806    /// `x = (p a B(a,b))^(1/a)` there.
1807    ///
1808    /// What shipped before, against the same references: `6.7e-18` for the
1809    /// first row (true `1.5e-41`, relative error 4.6e+23), `5.8e-18` for the
1810    /// second (true `6.3e-161`), and `9.6e-19` for the underflow row, whose
1811    /// true quantile is `7.7e-688` and whose only correct `f64` answer is `0`.
1812    /// The failure was not a loss of digits but a floor: every one of those
1813    /// returns is the solver's own resolution limit rather than a quantile.
1814    fn beta_quantile_resolves_the_lower_tail_below_the_solver_floor() {
1815        const CASES: [(f64, f64, f64, f64); 8] = [
1816            (0.04, 3.96, 0.025, 1.4749755854885786e-41),
1817            (0.01, 0.99, 0.025, 6.326229749489128e-161),
1818            (
1819                0.046666666666666666,
1820                2.2866666666666666,
1821                0.025,
1822                1.488779171021457e-35,
1823            ),
1824            (0.05, 0.95, 0.025, 9.875267916846768e-33),
1825            (0.1, 0.9, 0.025, 1.12479965068234e-16),
1826            (0.3, 0.7, 0.025, 7.6005358168401896e-6),
1827            (0.5, 0.5, 0.025, 1.5413331334360133e-3),
1828            (0.1, 0.1, 1.0e-4, 8.869280655550463e-38),
1829        ];
1830        let mut worst = 0.0_f64;
1831        for (a, b, p, want) in CASES {
1832            let got = beta_quantile(p, a, b);
1833            let relative = ((got - want) / want).abs();
1834            assert!(
1835                relative <= 16.0 * f64::EPSILON,
1836                "beta_quantile({p}, {a}, {b}) = {got:e}, want {want:e}, relative {relative:e}"
1837            );
1838            worst = worst.max(relative);
1839        }
1840        println!("worst relative error over the lower-tail table: {worst:e}");
1841
1842        // The true quantile here is 7.7e-688. It is not representable, so the
1843        // correctly rounded answer is zero, and a caller reading a positive
1844        // lower bound could not tell that it had underflowed.
1845        let underflowed = beta_quantile(0.025, 0.0023333333333333335, 2.3310000000000004);
1846        assert!(
1847            underflowed == 0.0,
1848            "a quantile below MIN_POSITIVE must round to zero, got {underflowed:e}"
1849        );
1850
1851        // The upper tail of the same shape is not on the series branch and is
1852        // still `inv_beta_reg`'s answer, at `inv_beta_reg`'s own accuracy. It is
1853        // asserted here so that widening the branch cannot silently move it.
1854        const UPPER: f64 = 0.12274676682071068;
1855        let upper = beta_quantile(0.975, 0.04, 3.96);
1856        assert!(
1857            ((upper - UPPER) / UPPER).abs() <= 1.0e-11,
1858            "upper tail moved: {upper:e}, want {UPPER:e}"
1859        );
1860    }
1861
1862    #[test]
1863    fn beta_quantile_matches_known_reference_values() {
1864        let cases: [(f64, f64, f64, f64); 8] = [
1865            (0.025, 2.0, 2.0, 0.094_299_3),
1866            (0.975, 2.0, 2.0, 0.905_700_7),
1867            (0.5, 2.0, 2.0, 0.5),
1868            (0.025, 0.8, 4.0, 0.002_339_1),
1869            (0.975, 0.8, 4.0, 0.564_717_3),
1870            (0.025, 5.0, 1.5, 0.408_549_1),
1871            (0.5, 20.0, 80.0, 0.197_994_8),
1872            (0.975, 20.0, 80.0, 0.283_367_6),
1873        ];
1874        for (p, a, b, expected) in cases {
1875            let got = beta_quantile(p, a, b);
1876            let abs = (got - expected).abs();
1877            assert!(
1878                abs < 1e-5,
1879                "beta_quantile(p={p}, a={a}, b={b}) = {got}, expected ≈ {expected} (abs err {abs})"
1880            );
1881        }
1882    }
1883
1884    #[test]
1885    fn beta_quantile_boundaries_and_degeneracy() {
1886        assert_eq!(beta_quantile(0.0, 2.0, 3.0), 0.0);
1887        assert_eq!(beta_quantile(-0.5, 2.0, 3.0), 0.0);
1888        assert_eq!(beta_quantile(1.0, 2.0, 3.0), 1.0);
1889        assert_eq!(beta_quantile(1.5, 2.0, 3.0), 1.0);
1890        assert!(beta_quantile(0.5, -1.0, 3.0).is_nan());
1891        assert!(beta_quantile(0.5, 2.0, 0.0).is_nan());
1892        assert!(beta_quantile(0.5, f64::NAN, 3.0).is_nan());
1893        let mut prev = 0.0;
1894        for i in 1..100 {
1895            let p = i as f64 / 100.0;
1896            let q = beta_quantile(p, 3.0, 5.0);
1897            assert!(q > prev, "beta quantile not increasing at p={p}");
1898            prev = q;
1899        }
1900    }
1901
1902    // ── normal_pdf ────────────────────────────────────────────────────────────
1903
1904    #[test]
1905    fn normal_pdf_at_zero() {
1906        let expected = 1.0 / (2.0 * std::f64::consts::PI).sqrt();
1907        assert!((normal_pdf(0.0) - expected).abs() < TOL);
1908    }
1909
1910    #[test]
1911    fn normal_pdf_symmetry() {
1912        for &x in &[0.5, 1.0, 2.0, 3.0, 5.0] {
1913            assert_eq!(normal_pdf(x), normal_pdf(-x), "symmetry failed at x={x}");
1914        }
1915    }
1916
1917    /// `x*x` is exact-splittable and the split is what `exp` needs.
1918    ///
1919    /// Two independent statements, because the correction is only worth what
1920    /// its residual is worth. First, `x*x + residual` is `x²` EXACTLY: checked
1921    /// against a Veltkamp/Dekker split, which reaches the same residual through
1922    /// pure multiplies and adds and shares no code path with the `mul_add`
1923    /// route. Second, the residual is not decorative — for these arguments it
1924    /// is a relative perturbation of `x²` big enough that `exp` amplifies it
1925    /// past a single ulp of the result.
1926    #[test]
1927    fn square_residual_completes_the_rounded_square_exactly() {
1928        // 2^27 + 1: Veltkamp's splitting factor, exact for any `x` whose
1929        // scaled form does not overflow.
1930        const SPLIT: f64 = 134_217_729.0;
1931        let mut saw_amplified = false;
1932        for &x in &[
1933            0.1, 0.7, 1.3, 2.9, 6.1, 10.5, 14.3, 19.7, 23.9, 25.9999, 34.7,
1934        ] {
1935            let rounded = x * x;
1936            let residual = square_residual(x, rounded);
1937
1938            let c = x * SPLIT;
1939            let head = c - (c - x);
1940            let tail = x - head;
1941            let dekker = ((head * head - rounded) + 2.0 * head * tail) + tail * tail;
1942            assert_eq!(
1943                residual, dekker,
1944                "x={x}: mul_add residual {residual:e} != Dekker residual {dekker:e}"
1945            );
1946
1947            // `exp` multiplies a relative argument perturbation by the argument.
1948            let amplified = (residual / rounded).abs() * rounded;
1949            if amplified > f64::EPSILON {
1950                saw_amplified = true;
1951            }
1952        }
1953        assert!(
1954            saw_amplified,
1955            "no test argument had a residual `exp` could amplify past one ulp; \
1956             the correction under test would be untested"
1957        );
1958    }
1959
1960    /// `φ(x)` against an EXTERNAL high-precision reference (mpmath, dps=60).
1961    ///
1962    /// Every argument here has an INEXACT square, which is the whole point.
1963    /// `exp(−½·fl(x*x))` misplaces the argument by `x²·ε/2` RELATIVE, and `exp`
1964    /// hands that straight back as relative error in the result: `1.4e-14` at
1965    /// `x ≈ 17`, `5.7e-14` by `x ≈ 35`, where `φ` is still a normal `f64`. Only
1966    /// the top of the range makes that visible, so the table has to reach it —
1967    /// a `φ` table that stops at `x = 5` cannot tell the two forms apart.
1968    ///
1969    /// `1.5e-15` (≈7 ulp) is the portability allowance: `f64::exp` is the
1970    /// platform libm and the only part of this that is not fixed by the crate
1971    /// graph, and it is worth ~1 ulp on the implementations in use. That still
1972    /// leaves 38x of margin against the defect at the top of the table.
1973    #[test]
1974    fn normal_pdf_matches_high_precision_reference() {
1975        const TOLERANCE: f64 = 1.5e-15;
1976        let refs: &[(f64, f64)] = &[
1977            (0.5, 0.35206532676429947),
1978            (1.0, 0.24197072451914334),
1979            (2.5, 0.017528300493568537),
1980            (4.0, 0.00013383022576488534),
1981            (7.3, 1.0693837871541648e-12),
1982            (11.9, 7.090702668428078e-32),
1983            (17.4, 7.201308152719057e-67),
1984            (23.6, 4.555989824112156e-122),
1985            (29.1, 5.229437243665329e-185),
1986            (34.7, 1.368008224488383e-262),
1987        ];
1988        for &(x, reference) in refs {
1989            // The small arguments anchor the ordinary range; the large ones are
1990            // where the defect lives, and every one of THOSE has to have a
1991            // square `f64` cannot hold or it exercises nothing.
1992            assert!(
1993                x <= 5.0 || square_residual(x, x * x) != 0.0,
1994                "x={x} squares exactly, so it cannot exercise the correction"
1995            );
1996            let rel = rel_err(normal_pdf(x), reference);
1997            assert!(
1998                rel < TOLERANCE,
1999                "normal_pdf({x}) = {:.17e}, reference {reference:.17e}, rel {rel:.3e}",
2000                normal_pdf(x)
2001            );
2002        }
2003    }
2004
2005    /// `φ` off the ordinary domain, where the square has no usable residual:
2006    /// `±∞` squares to `∞` and would hand the correction an `∞ − ∞`.
2007    #[test]
2008    fn normal_pdf_nonfinite_and_underflowed_arguments() {
2009        assert_eq!(normal_pdf(f64::INFINITY), 0.0);
2010        assert_eq!(normal_pdf(f64::NEG_INFINITY), 0.0);
2011        assert!(normal_pdf(f64::NAN).is_nan());
2012        // Past ~38.6 the pdf underflows; it must reach zero, not NaN.
2013        assert_eq!(normal_pdf(40.0), 0.0);
2014        assert_eq!(normal_pdf(-40.0), 0.0);
2015        assert_eq!(normal_pdf(f64::MAX), 0.0);
2016        // Just inside the underflow edge the result is subnormal but positive.
2017        let edge = normal_pdf(38.0);
2018        assert!(edge > 0.0 && edge.is_subnormal(), "phi(38) = {edge:e}");
2019    }
2020
2021    #[test]
2022    fn normal_pdf_positive() {
2023        for &x in &[-5.0, -1.0, 0.0, 1.0, 5.0] {
2024            assert!(normal_pdf(x) > 0.0, "pdf should be positive at x={x}");
2025        }
2026    }
2027
2028    // ── normal_cdf ────────────────────────────────────────────────────────────
2029
2030    #[test]
2031    fn normal_cdf_at_zero_is_half() {
2032        assert!((normal_cdf(0.0) - 0.5).abs() < TOL);
2033    }
2034
2035    #[test]
2036    fn normal_cdf_symmetry() {
2037        for &x in &[0.5, 1.0, 2.0, 3.0] {
2038            let sum = normal_cdf(x) + normal_cdf(-x);
2039            assert!(
2040                (sum - 1.0).abs() < TOL,
2041                "cdf symmetry failed at x={x}: sum={sum}"
2042            );
2043        }
2044    }
2045
2046    #[test]
2047    fn normal_cdf_bounds() {
2048        assert!(normal_cdf(10.0) > 0.9999);
2049        assert!(normal_cdf(-10.0) < 1e-22);
2050        assert!(normal_cdf(0.0) > 0.0);
2051        assert!(normal_cdf(0.0) < 1.0);
2052    }
2053
2054    #[test]
2055    fn normal_cdf_at_1_96_near_0975() {
2056        // Phi(1.96) ≈ 0.975 — canonical two-sided 5% critical value.
2057        let p = normal_cdf(1.959_963_985);
2058        assert!((p - 0.975).abs() < 1e-8, "p={p}");
2059    }
2060
2061    // ── erfcx_nonnegative ─────────────────────────────────────────────────────
2062
2063    #[test]
2064    fn erfcx_zero_is_one_and_negative_domain_is_rejected() {
2065        assert_eq!(erfcx_nonnegative(0.0), 1.0);
2066        assert!(erfcx_nonnegative(-f64::MIN_POSITIVE).is_nan());
2067        assert!(erfcx_nonnegative(-1.0).is_nan());
2068        assert!(erfcx_nonnegative(f64::NEG_INFINITY).is_nan());
2069    }
2070
2071    #[test]
2072    fn erfcx_positive_inf_returns_zero() {
2073        assert_eq!(erfcx_nonnegative(f64::INFINITY), 0.0);
2074    }
2075
2076    #[test]
2077    fn erfcx_nan_propagates() {
2078        assert!(erfcx_nonnegative(f64::NAN).is_nan());
2079    }
2080
2081    #[test]
2082    fn erfcx_small_positive_matches_direct() {
2083        use libm::erfc;
2084        for &x in &[0.1_f64, 0.5, 1.0, 5.0, 10.0, 25.0] {
2085            let got = erfcx_nonnegative(x);
2086            let expected = (x * x).exp() * erfc(x);
2087            let err = rel_err(got, expected);
2088            assert!(
2089                err < 1e-10,
2090                "x={x}: got={got} expected={expected} rel={err}"
2091            );
2092        }
2093    }
2094
2095    #[test]
2096    fn erfcx_large_x_positive_and_finite() {
2097        // For x >= 26 the asymptotic branch must remain positive and finite.
2098        let got = erfcx_nonnegative(50.0);
2099        assert!(got.is_finite() && got > 0.0, "erfcx(50)={got}");
2100        // Leading asymptotic term: 1/(x*sqrt(pi)).
2101        let asymptotic = 1.0 / (50.0 * std::f64::consts::PI.sqrt());
2102        assert!(
2103            rel_err(got, asymptotic) < 1e-3,
2104            "got={got} asymptotic={asymptotic}"
2105        );
2106    }
2107
2108    /// The two branches must describe one function across `x = 26`.
2109    ///
2110    /// Note WHY the plain `exp(x*x)·erfc(x)` below is a legitimate oracle at
2111    /// this particular argument and nowhere else: `26² = 676` is exactly
2112    /// representable, so the rounded square carries no residual and the direct
2113    /// form is momentarily as good as the corrected one. That is also exactly
2114    /// why this check was blind to the `x²·ε/2` defect it looks like it should
2115    /// have caught — at `25.9` the same comparison would have failed by
2116    /// `5.7e-14`, but the seam was only ever probed at the one point in the
2117    /// neighbourhood where the defect vanishes. The bit-adjacent step below
2118    /// cannot substitute for it either: `d(ln erfcx)/dx ≈ −2x` at the switch,
2119    /// so one ulp of `x` moves the true value by `1.8e-13`, three times the
2120    /// defect. It takes a reference at a DISTANCE from the seam — the table in
2121    /// `erfcx_matches_high_precision_reference` — to see the defect at all.
2122    #[test]
2123    fn erfcx_asymptotic_switch_matches_finite_direct_identity() {
2124        let switch = 26.0_f64;
2125        assert_eq!(
2126            square_residual(switch, switch * switch),
2127            0.0,
2128            "676 must be exact for the direct form below to be an oracle"
2129        );
2130        let direct = (switch * switch).exp() * erfc(switch);
2131        let asymptotic = erfcx_nonnegative(switch);
2132        assert!(
2133            rel_err(asymptotic, direct) < 1.0e-15,
2134            "switch mismatch: asymptotic={asymptotic:.17e}, direct={direct:.17e}"
2135        );
2136
2137        // Continuity across the branch cut, up to how fast the function itself
2138        // moves over one ulp of `x` (`|d ln erfcx/dx| ≈ 2x` ⇒ ~1.9e-13 here).
2139        let immediately_below = f64::from_bits(switch.to_bits() - 1);
2140        let below = erfcx_nonnegative(immediately_below);
2141        let step = 2.0 * switch * (switch - immediately_below);
2142        assert!(
2143            rel_err(asymptotic, below) < 2.0 * step,
2144            "discontinuous switch: below={below:.17e}, at={asymptotic:.17e}, \
2145             one-ulp travel {step:.3e}"
2146        );
2147    }
2148
2149    #[test]
2150    fn erfcx_preserves_representable_subnormal_tail() {
2151        let tail = erfcx_nonnegative(f64::MAX);
2152        assert!(tail > 0.0 && tail.is_subnormal(), "erfcx(MAX)={tail:e}");
2153    }
2154
2155    /// Absolute-accuracy pin against an EXTERNAL high-precision reference
2156    /// (mpmath, dps=60) spanning the direct branch `[0.1, 26)`. This is the
2157    /// root-cause guard: the previous `exp(x²)·erfc(x)` direct form was built on
2158    /// `statrs::erfc`, whose ~1e-10 relative accuracy silently poisoned every
2159    /// downstream probit / Mills / log-CDF derivative.
2160    ///
2161    /// The table had a SECOND job it was not doing. Of its twelve arguments,
2162    /// eleven — `0.5`, `2`, `3.5`, `6`, `9`, `13`, `18`, `22`, `25.5`, and the
2163    /// two whose squares are far too small to matter — square EXACTLY in `f64`,
2164    /// so `fl(x*x) = x²` and the `x²·ε/2` error the rounded square feeds `exp`
2165    /// was identically zero at every one of them. The twelfth, `25.9999`, does
2166    /// not square exactly; it was the one point in the table where the defect
2167    /// was live, and its literal had been recorded WITH the defect in it —
2168    /// `0.021683668126370212` against a true `0.021683668126369115`, off by
2169    /// `5.1e-14`. Three independent high-precision routes (`exp(x²)·erfc(x)`,
2170    /// the 12-term asymptotic series, and a 400-level Laplace continued
2171    /// fraction) and `scipy.special.erfcx` all agree on the corrected value.
2172    /// A `1e-13` tolerance then accepted a reference that was itself wrong by
2173    /// half the tolerance, which is how a 190x accuracy defect sat under a
2174    /// test named for high precision.
2175    ///
2176    /// So the table now RUNS ON arguments with inexact squares (`10.5`,
2177    /// `14.3`, `19.7`, `23.9` alongside the original grid) and the tolerance is
2178    /// `1.5e-15` — 38x below the defect at the top of the range, and still ~7
2179    /// ulp of headroom for the platform `f64::exp` (the only part of this path
2180    /// not pinned by the crate graph; `erfc` comes from the `libm` crate and is
2181    /// identical everywhere).
2182    #[test]
2183    fn erfcx_matches_high_precision_reference() {
2184        const TOLERANCE: f64 = 1.5e-15;
2185        // (x, mpmath exp(x²)·erfc(x) at dps=60, rounded to f64).
2186        let refs: &[(f64, f64)] = &[
2187            (0.1, 0.8964569799691267),
2188            (0.5, 0.6156903441929259),
2189            (1.0, 0.427583576155807),
2190            (2.0, 0.25539567631050575),
2191            (3.5, 0.1552936556088943),
2192            (6.0, 0.09277656780053835),
2193            (9.0, 0.06230772403777468),
2194            (10.5, 0.05349189974656412),
2195            (13.0, 0.043271921864609694),
2196            (14.3, 0.0393580473372741),
2197            (18.0, 0.03129571781590521),
2198            (19.7, 0.028602309402825203),
2199            (22.0, 0.025618570005879453),
2200            (23.9, 0.023585649371803793),
2201            (25.5, 0.022108108052519827),
2202            (25.9999, 0.021683668126369115),
2203        ];
2204        for &(x, reference) in refs {
2205            let got = erfcx_nonnegative(x);
2206            let rel = rel_err(got, reference);
2207            assert!(
2208                rel < TOLERANCE,
2209                "erfcx({x}) = {got:.17e}, reference {reference:.17e}, rel {rel:.3e}"
2210            );
2211        }
2212        // The point of the added arguments: at least four of them must have a
2213        // square `f64` cannot hold, or the table is back to testing nothing.
2214        let inexact = refs
2215            .iter()
2216            .filter(|&&(x, _)| square_residual(x, x * x) != 0.0)
2217            .count();
2218        assert!(
2219            inexact >= 4,
2220            "only {inexact} of {} reference arguments have an inexact square",
2221            refs.len()
2222        );
2223    }
2224
2225    // ── log1mexp_positive ─────────────────────────────────────────────────────
2226
2227    #[test]
2228    fn log1mexp_at_zero_is_neg_inf() {
2229        assert_eq!(log1mexp_positive(0.0), f64::NEG_INFINITY);
2230    }
2231
2232    #[test]
2233    fn log1mexp_recovers_log_one_minus_exp() {
2234        // Verify exp(log1mexp(a)) + exp(-a) ≈ 1 for several a > 0. This
2235        // roundtrip avoids computing `(1 - exp(-a)).ln()` directly, which
2236        // suffers catastrophic cancellation for large a (e.g. a=20 where
2237        // `1.0 - exp(-20)` loses 9 decimal digits from the subtraction).
2238        for &a in &[0.001_f64, 0.5, std::f64::consts::LN_2, 1.0, 5.0, 20.0] {
2239            let lm = log1mexp_positive(a);
2240            let roundtrip = lm.exp() + (-a).exp();
2241            assert!(
2242                (roundtrip - 1.0).abs() < 1e-14,
2243                "a={a}: exp(log1mexp(a)) + exp(-a) = {roundtrip}, expected 1.0"
2244            );
2245        }
2246    }
2247
2248    #[test]
2249    fn log1mexp_at_ln2_is_neg_ln2() {
2250        let ln2 = std::f64::consts::LN_2;
2251        let got = log1mexp_positive(ln2);
2252        assert!((got - (-ln2)).abs() < TOL, "got={got}");
2253    }
2254
2255    // ── signed_log_sum_exp ────────────────────────────────────────────────────
2256
2257    #[test]
2258    fn slse_all_positive_single() {
2259        let (lm, sg) = signed_log_sum_exp(&[2.0], &[1.0]);
2260        assert!((lm - 2.0).abs() < TOL);
2261        assert!((sg - 1.0).abs() < TOL);
2262    }
2263
2264    #[test]
2265    fn slse_difference_recovers_log2() {
2266        // 3 - 1 = 2 → log|2| = ln(2), sign = +1.
2267        let log3 = 3.0_f64.ln();
2268        let log1 = 0.0_f64; // ln(1)
2269        let (lm, sg) = signed_log_sum_exp(&[log3, log1], &[1.0, -1.0]);
2270        assert!((lm - 2.0_f64.ln()).abs() < TOL, "lm={lm}");
2271        assert!((sg - 1.0).abs() < TOL, "sg={sg}");
2272    }
2273
2274    #[test]
2275    fn slse_cancellation_gives_neg_inf() {
2276        // a - a = 0 → log|0| = -∞.
2277        let ln2 = 2.0_f64.ln();
2278        let (lm, sg) = signed_log_sum_exp(&[ln2, ln2], &[1.0, -1.0]);
2279        assert_eq!(lm, f64::NEG_INFINITY);
2280        assert_eq!(sg, 0.0);
2281    }
2282
2283    #[test]
2284    fn slse_compensated_signed_reduction_preserves_conditioned_residual() {
2285        // High-precision truth for these exact f64 log inputs is
2286        // -7.141194316117315021451...e-13. Reducing the positive and negative
2287        // groups through separate logarithms first returned
2288        // -7.141196119493781e-13: two otherwise harmless log roundings were
2289        // amplified by the nearly cancelling subtraction.
2290        let log_magnitudes = [
2291            -8.752777116220523,
2292            -8.741767521635955,
2293            -8.77021076826994,
2294            -8.75153786858979,
2295            -8.754172660745834,
2296            -8.768217028174623,
2297            -8.756625396724502,
2298            -8.737312647396818,
2299        ];
2300        let signs = [1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0];
2301        let (log_magnitude, sign) = signed_log_sum_exp(&log_magnitudes, &signs);
2302        let got = sign * log_magnitude.exp();
2303        let truth = -7.141194316117315e-13;
2304        let legacy = -7.141196119493781e-13;
2305        assert_eq!(sign, -1.0);
2306        assert!(
2307            (got - truth).abs() < (legacy - truth).abs(),
2308            "compensated signed reduction did not improve the conditioned residual: \
2309             got={got:.17e}, truth={truth:.17e}, legacy={legacy:.17e}"
2310        );
2311    }
2312
2313    #[test]
2314    fn slse_log_domain_branch_retains_sub_ulp_two_term_gap() {
2315        // exp(-gap) rounds to 1.0 at this gap, so a purely linear-domain signed
2316        // reduction sees 1 - 1. The forward-error gate must route to the
2317        // log-domain difference, where the distinct input logs retain the gap.
2318        let gap = f64::EPSILON * 0.25;
2319        let (log_magnitude, sign) = signed_log_sum_exp(&[0.0, -gap], &[1.0, -1.0]);
2320        assert_eq!(sign, 1.0);
2321        assert_eq!(log_magnitude, log1mexp_positive(gap));
2322    }
2323
2324    #[test]
2325    fn exact_binary64_sum_sign_resolves_midpoint_and_both_adjacent_sides() {
2326        let half_upper_ulp_at_one = 2.0_f64.powi(-53);
2327        let least_subnormal = f64::from_bits(1);
2328        assert_eq!(
2329            exact_binary64_sum_sign([
2330                1.0,
2331                half_upper_ulp_at_one,
2332                -1.0,
2333                -half_upper_ulp_at_one,
2334            ]),
2335            Ok(std::cmp::Ordering::Equal),
2336            "an exact rounding midpoint must compare equal"
2337        );
2338        assert_eq!(
2339            exact_binary64_sum_sign([
2340                1.0,
2341                half_upper_ulp_at_one,
2342                least_subnormal,
2343                -1.0,
2344                -half_upper_ulp_at_one,
2345            ]),
2346            Ok(std::cmp::Ordering::Greater),
2347            "one binary lattice quantum above the midpoint must compare positive"
2348        );
2349        assert_eq!(
2350            exact_binary64_sum_sign([
2351                1.0,
2352                half_upper_ulp_at_one,
2353                -least_subnormal,
2354                -1.0,
2355                -half_upper_ulp_at_one,
2356            ]),
2357            Ok(std::cmp::Ordering::Less),
2358            "one binary lattice quantum below the midpoint must compare negative"
2359        );
2360    }
2361
2362    #[test]
2363    fn exact_binary64_sum_sign_enforces_its_finite_structural_contract() {
2364        assert_eq!(
2365            exact_binary64_sum_sign([f64::MAX, -f64::MAX, f64::from_bits(1)]),
2366            Ok(std::cmp::Ordering::Greater),
2367        );
2368        assert_eq!(
2369            exact_binary64_sum_sign([0.0, f64::NAN]),
2370            Err(ExactBinary64SumSignError::NonFiniteTerm { index: 1 }),
2371        );
2372        assert_eq!(
2373            exact_binary64_sum_sign(
2374                std::iter::repeat_n(1.0, EXACT_BINARY64_SUM_MAX_TERMS + 1)
2375            ),
2376            Err(ExactBinary64SumSignError::TermCapacityExceeded {
2377                maximum: EXACT_BINARY64_SUM_MAX_TERMS,
2378            }),
2379        );
2380    }
2381
2382    #[test]
2383    fn slse_empty_returns_neg_inf_with_zero_sign() {
2384        // With no terms the sum is exactly 0, so the docstring contract is
2385        // `(−∞, 0.0)`. (This test previously encoded the buggy `+1.0` positive-sum
2386        // convention, which contradicted both the docstring and the cancellation
2387        // test below; rewritten to the correct zero sign.)
2388        let (lm, sg) = signed_log_sum_exp(&[], &[]);
2389        assert_eq!(lm, f64::NEG_INFINITY);
2390        assert_eq!(sg, 0.0);
2391    }
2392
2393    #[test]
2394    fn slse_all_zero_signs_return_zero_sign() {
2395        // A single term whose sign is 0 contributes nothing; S = 0 ⇒ (−∞, 0.0).
2396        let (lm, sg) = signed_log_sum_exp(&[0.0], &[0.0]);
2397        assert_eq!(lm, f64::NEG_INFINITY);
2398        assert_eq!(sg, 0.0);
2399    }
2400
2401    #[test]
2402    fn slse_all_neg_inf_magnitudes_return_zero_sign() {
2403        // Every magnitude is exp(−∞) = 0 regardless of sign, so the sum is 0 and
2404        // the reported sign must be 0.0, not +1.0.
2405        let (lm, sg) = signed_log_sum_exp(&[f64::NEG_INFINITY, f64::NEG_INFINITY], &[1.0, -1.0]);
2406        assert_eq!(lm, f64::NEG_INFINITY);
2407        assert_eq!(sg, 0.0);
2408    }
2409
2410    #[test]
2411    fn slse_pos_inf_dominates() {
2412        let (lm, sg) = signed_log_sum_exp(&[f64::INFINITY, 1.0], &[1.0, -1.0]);
2413        assert_eq!(lm, f64::INFINITY);
2414        assert_eq!(sg, 1.0);
2415    }
2416
2417    #[test]
2418    fn slse_neg_inf_dominates() {
2419        let (lm, sg) = signed_log_sum_exp(&[f64::INFINITY, 1.0], &[-1.0, 1.0]);
2420        assert_eq!(lm, f64::INFINITY);
2421        assert_eq!(sg, -1.0);
2422    }
2423
2424    #[test]
2425    fn slse_both_inf_signs_gives_nan() {
2426        let (lm, sg) = signed_log_sum_exp(&[f64::INFINITY, f64::INFINITY], &[1.0, -1.0]);
2427        assert!(lm.is_nan());
2428        assert_eq!(sg, 0.0);
2429    }
2430
2431    // ── normal_logcdf ─────────────────────────────────────────────────────────
2432
2433    #[test]
2434    fn logcdf_at_zero_is_log_half() {
2435        let got = normal_logcdf(0.0);
2436        let expected = 0.5_f64.ln();
2437        assert!((got - expected).abs() < TOL, "got={got}");
2438    }
2439
2440    #[test]
2441    fn logcdf_pos_inf_is_zero() {
2442        assert_eq!(normal_logcdf(f64::INFINITY), 0.0);
2443    }
2444
2445    #[test]
2446    fn logcdf_neg_inf_is_neg_inf() {
2447        assert_eq!(normal_logcdf(f64::NEG_INFINITY), f64::NEG_INFINITY);
2448    }
2449
2450    #[test]
2451    fn logcdf_nan_is_nan() {
2452        assert!(normal_logcdf(f64::NAN).is_nan());
2453    }
2454
2455    #[test]
2456    fn logcdf_matches_log_cdf_for_moderate_x() {
2457        for &x in &[-2.0_f64, -1.0, 0.0, 1.0, 2.0, 3.0] {
2458            let got = normal_logcdf(x);
2459            let expected = normal_cdf(x).ln();
2460            assert!(
2461                (got - expected).abs() < 1e-10,
2462                "x={x}: got={got} expected={expected}"
2463            );
2464        }
2465    }
2466
2467    #[test]
2468    fn logcdf_deep_left_tail_stays_finite() {
2469        // For very negative x, normal_cdf(x) underflows to 0, but logcdf should
2470        // remain finite and large-negative.
2471        let got = normal_logcdf(-20.0);
2472        assert!(got.is_finite() && got < -100.0, "logcdf(-20)={got}");
2473    }
2474
2475    #[test]
2476    fn logcdf_positive_tail_does_not_round_through_unit_cdf() {
2477        let x = 10.0_f64;
2478        let got = normal_logcdf(x);
2479        let expected = (-0.5 * erfc(x / std::f64::consts::SQRT_2)).ln_1p();
2480        assert!(
2481            got < 0.0,
2482            "logcdf(10) must retain its negative tail: {got:e}"
2483        );
2484        assert_eq!(got.to_bits(), expected.to_bits());
2485    }
2486
2487    #[test]
2488    fn log_cdf_quantile_round_trips_both_unrepresentable_tails() {
2489        for x in [-1.0e6, -40.0, -10.0, -2.0, 0.0, 2.0, 10.0] {
2490            let log_p = normal_logcdf(x);
2491            let recovered = standard_normal_quantile_from_log_cdf(log_p)
2492                .expect("finite strict log-CDF has a quantile");
2493            assert!(
2494                (recovered - x).abs() <= 2.0e-12 * x.abs().max(1.0),
2495                "log-quantile round trip at x={x}: log_p={log_p}, recovered={recovered}"
2496            );
2497        }
2498    }
2499
2500    // ── normal_logsf ─────────────────────────────────────────────────────────
2501
2502    #[test]
2503    fn logsf_at_zero_is_log_half() {
2504        let got = normal_logsf(0.0);
2505        let expected = 0.5_f64.ln();
2506        assert!((got - expected).abs() < TOL, "got={got}");
2507    }
2508
2509    #[test]
2510    fn logsf_mirrors_logcdf() {
2511        // logsf(x) = logcdf(-x) by definition.
2512        for &x in &[-3.0_f64, -1.0, 0.0, 1.0, 3.0] {
2513            assert_eq!(normal_logsf(x), normal_logcdf(-x));
2514        }
2515    }
2516
2517    // ── signed_probit_logcdf_and_mills_ratio ──────────────────────────────────
2518
2519    #[test]
2520    fn probit_at_pos_inf() {
2521        let (lc, mr) = signed_probit_logcdf_and_mills_ratio(f64::INFINITY);
2522        assert_eq!(lc, 0.0);
2523        assert_eq!(mr, 0.0);
2524    }
2525
2526    #[test]
2527    fn probit_at_neg_inf() {
2528        let (lc, mr) = signed_probit_logcdf_and_mills_ratio(f64::NEG_INFINITY);
2529        assert_eq!(lc, f64::NEG_INFINITY);
2530        assert_eq!(mr, f64::INFINITY);
2531    }
2532
2533    #[test]
2534    fn probit_nan_propagates() {
2535        let (lc, mr) = signed_probit_logcdf_and_mills_ratio(f64::NAN);
2536        assert!(lc.is_nan() && mr.is_nan());
2537    }
2538
2539    #[test]
2540    fn probit_at_zero_logcdf_and_mills() {
2541        let (lc, mr) = signed_probit_logcdf_and_mills_ratio(0.0);
2542        assert!((lc - 0.5_f64.ln()).abs() < TOL, "lc={lc}");
2543        // phi(0)/Phi(0) = 0.3989.../0.5 ≈ 0.7979.
2544        assert!((mr - 0.797_884_560_802_865).abs() < 1e-10, "mr={mr}");
2545    }
2546
2547    #[test]
2548    fn probit_positive_branch_matches_logcdf() {
2549        for &x in &[0.5_f64, 1.0, 2.0, 3.0] {
2550            let (lc, mr) = signed_probit_logcdf_and_mills_ratio(x);
2551            let lc_ref = normal_logcdf(x);
2552            let mr_ref = normal_pdf(x) / normal_cdf(x);
2553            assert!(
2554                (lc - lc_ref).abs() < 1e-10,
2555                "x={x}: lc={lc} lc_ref={lc_ref}"
2556            );
2557            assert!(
2558                (mr - mr_ref).abs() < 1e-10,
2559                "x={x}: mr={mr} mr_ref={mr_ref}"
2560            );
2561        }
2562    }
2563
2564    #[test]
2565    fn probit_negative_branch_matches_logcdf() {
2566        for &x in &[-0.5_f64, -1.0, -2.0, -5.0] {
2567            let (lc, mr) = signed_probit_logcdf_and_mills_ratio(x);
2568            let lc_ref = normal_logcdf(x);
2569            assert!(
2570                (lc - lc_ref).abs() < 1e-10,
2571                "x={x}: lc={lc} lc_ref={lc_ref}"
2572            );
2573            assert!(mr.is_finite() && mr > 0.0, "x={x}: mr={mr}");
2574        }
2575    }
2576
2577    #[test]
2578    fn probit_mills_ratio_has_no_deep_tail_floor() {
2579        let x = -1.0e305_f64;
2580        let (log_cdf, mills_ratio) = signed_probit_logcdf_and_mills_ratio(x);
2581        assert_eq!(log_cdf, f64::NEG_INFINITY);
2582        assert!(mills_ratio.is_finite());
2583        assert!(
2584            ((mills_ratio / -x) - 1.0).abs() < 5.0e-15,
2585            "mills({x:e})={mills_ratio:e}"
2586        );
2587    }
2588
2589    #[test]
2590    fn normal_logcdf_derivative_stack_has_honest_infinite_limits() {
2591        assert_eq!(normal_logcdf_derivatives(f64::INFINITY), [0.0; 5]);
2592        assert_eq!(
2593            normal_logcdf_derivatives(f64::NEG_INFINITY),
2594            [f64::NEG_INFINITY, f64::INFINITY, -1.0, 0.0, 0.0]
2595        );
2596        assert!(
2597            normal_logcdf_derivatives(f64::NAN)
2598                .into_iter()
2599                .all(f64::is_nan)
2600        );
2601
2602        for x in [-1.0e200_f64, 1.0e200_f64] {
2603            let derivatives = normal_logcdf_derivatives(x);
2604            assert!(
2605                derivatives.into_iter().all(|value| !value.is_nan()),
2606                "NaN derivative at x={x:e}: {derivatives:?}"
2607            );
2608        }
2609    }
2610
2611    #[test]
2612    fn normal_logcdf_left_tail_derivatives_do_not_cancel() {
2613        let x = -1.0e100_f64;
2614        let derivatives = normal_logcdf_derivatives(x);
2615        assert_eq!(derivatives[2], -1.0);
2616        assert!(derivatives[3] > 0.0 && derivatives[3].is_finite());
2617        assert!(
2618            (derivatives[3] / 2.0e-300 - 1.0).abs() < 2.0e-14,
2619            "third derivative={:e}",
2620            derivatives[3]
2621        );
2622        assert_eq!(derivatives[4], 0.0);
2623    }
2624
2625    #[test]
2626    fn normal_logcdf_right_tail_preserves_weighted_subnormal_derivatives() {
2627        let derivatives = normal_logcdf_derivatives(38.6);
2628        assert_eq!(derivatives[1], 0.0);
2629        assert!(derivatives[2] < 0.0 && derivatives[2].is_subnormal());
2630        assert!(derivatives[3] > 0.0 && derivatives[3].is_subnormal());
2631        assert!(derivatives[4] < 0.0 && derivatives[4].is_subnormal());
2632    }
2633
2634    #[test]
2635    fn normal_logcdf_tail_stack_is_finite_difference_consistent() {
2636        let h = 1.0e-4_f64;
2637        for x in [-8.0_f64, -4.0, 8.0, 20.0] {
2638            let center = normal_logcdf_derivatives(x);
2639            let left = normal_logcdf_derivatives(x - h);
2640            let right = normal_logcdf_derivatives(x + h);
2641            for order in 1..=3 {
2642                let finite_difference = (right[order] - left[order]) / (2.0 * h);
2643                let expected = center[order + 1];
2644                let relative = (finite_difference - expected).abs() / expected.abs().max(1.0e-300);
2645                assert!(
2646                    relative < 2.0e-5,
2647                    "x={x}, order={order}: fd={finite_difference:e}, expected={expected:e}, rel={relative:e}"
2648                );
2649            }
2650        }
2651    }
2652
2653    /// Absolute-accuracy pin of the full `ln Φ(x)` derivative tower against an
2654    /// EXTERNAL high-precision reference (mpmath, dps=60), covering all three
2655    /// branches (continued-fraction left tail at x=−4, the moderate Mills
2656    /// recurrence for x∈(−4, 8), and both signs). Before the `erfc` root-cause
2657    /// fix the moderate branch's `λ = φ/Φ` inherited `statrs::erfc`'s ~1e-10
2658    /// error, so `f''` was wrong by ~1e-9 near the −4 seam; this pins every
2659    /// entry to `2e-11` relative, catching that regression head-on rather than
2660    /// through a seam-straddling finite difference.
2661    #[test]
2662    fn normal_logcdf_derivative_tower_matches_high_precision_reference() {
2663        // (x, [value, f', f'', f''', f''''] from mpmath at dps=60).
2664        let refs: &[(f64, [f64; 5])] = &[
2665            (
2666                -4.0,
2667                [
2668                    -10.360101486527291,
2669                    4.2256071444894711,
2670                    -0.95332716160257737,
2671                    0.017856339307658426,
2672                    0.0095065764315958691,
2673                ],
2674            ),
2675            // Two points well inside the continued-fraction branch, where the
2676            // truncation the depth controls is the ONLY error source: at -4 the
2677            // branch is at its least converged, and these confirm it stays put.
2678            (
2679                -10.0,
2680                [
2681                    -53.231285150512471,
2682                    10.098093233962512,
2683                    -0.99055462217434374,
2684                    0.0017864003921165069,
2685                    0.00049785382237944016,
2686                ],
2687            ),
2688            (
2689                -6.0,
2690                [
2691                    -20.736768949974706,
2692                    6.1584826045445989,
2693                    -0.97601236321083323,
2694                    0.0069535374991643118,
2695                    0.0028992056785575027,
2696                ],
2697            ),
2698            (
2699                -2.0,
2700                [
2701                    -3.7831843336820319,
2702                    2.3732155328228409,
2703                    -0.88572089958591874,
2704                    0.059355861291565813,
2705                    0.039421993865946813,
2706                ],
2707            ),
2708            (
2709                -1.0,
2710                [
2711                    -1.8410216450092635,
2712                    1.5251352761609812,
2713                    -0.80090233442965121,
2714                    0.11693119540604883,
2715                    0.07917498368074563,
2716                ],
2717            ),
2718            (
2719                -0.3,
2720                [
2721                    -0.96210281816885066,
2722                    0.99816596885848332,
2723                    -0.69688551072964971,
2724                    0.18398317992442132,
2725                    0.11037564722092704,
2726                ],
2727            ),
2728            (
2729                0.5,
2730                [
2731                    -0.36894641528865639,
2732                    0.50916043383703349,
2733                    -0.5138245643036329,
2734                    0.27099012446870783,
2735                    0.088167801929197554,
2736                ],
2737            ),
2738            (
2739                2.0,
2740                [
2741                    -0.023012909328963488,
2742                    0.055247862678989959,
2743                    -0.11354805168857645,
2744                    0.18439481503247759,
2745                    -0.18785468561160969,
2746                ],
2747            ),
2748        ];
2749        // The moderate-branch statrs regression produced ~1e-9 errors in f''.
2750        // The bound used to sit at 1e-10 to respect what was called the
2751        // continued-fraction branch's "inherent" ~2e-11 in f''''; that was not
2752        // inherent but a depth, and at 64 levels the branch reproduces this
2753        // 60-digit reference EXACTLY at x = -4, -6 and -10. What remains is the
2754        // moderate branch, where the brackets are already collected in `q` and
2755        // the floor is `λ`'s own relative error amplified by `λ/q` (18.7 at the
2756        // switch): 1.8e-13 at x = -2, the worst point here. 1e-11 keeps 55x of
2757        // headroom over that while still failing the 32-level truncation head-on.
2758        for &(x, reference) in refs {
2759            let got = normal_logcdf_derivatives(x);
2760            for (order, (&g, &r)) in got.iter().zip(reference.iter()).enumerate() {
2761                let rel = (g - r).abs() / r.abs().max(1.0e-3);
2762                assert!(
2763                    rel < 1.0e-11,
2764                    "normal_logcdf_derivatives({x})[{order}] = {g:.17e}, reference {r:.17e}, \
2765                     rel {rel:.3e} >= 1e-11"
2766                );
2767            }
2768        }
2769    }
2770
2771    // ── standard_normal_quantile ──────────────────────────────────────────────
2772
2773    #[test]
2774    fn quantile_rejects_out_of_range() {
2775        assert!(standard_normal_quantile(0.0).is_err());
2776        assert!(standard_normal_quantile(1.0).is_err());
2777        assert!(standard_normal_quantile(-0.1).is_err());
2778        assert!(standard_normal_quantile(1.1).is_err());
2779        assert!(standard_normal_quantile(f64::NAN).is_err());
2780    }
2781
2782    #[test]
2783    fn quantile_at_half_is_near_zero() {
2784        let q = standard_normal_quantile(0.5).unwrap();
2785        assert!(q.abs() < 1e-10, "quantile(0.5)={q}");
2786    }
2787
2788    #[test]
2789    fn quantile_at_0975_is_near_196() {
2790        let q = standard_normal_quantile(0.975).unwrap();
2791        assert!((q - 1.959_963_984_540_054).abs() < 1e-14, "q={q}");
2792    }
2793
2794    /// `standard_normal_quantile` and its log-CDF sibling, against a 120-digit
2795    /// root of `Φ(x) = p` (respectively `ln Φ(x) = log_p`).
2796    ///
2797    /// The seed is Acklam's rational approximation, whose accuracy is `1.15e-9`
2798    /// relative; the two Halley steps after it are what make the result
2799    /// ulp-accurate. Deleting the polish loop entirely leaves EVERY other
2800    /// quantile test in this module green except `quantile_roundtrip_cdf`, and
2801    /// that one only by a factor of 1.9 — so the polish had no real gate. This
2802    /// table is that gate: it fails by six orders if the seed ships unpolished.
2803    ///
2804    /// The grid straddles Acklam's own `P_LOW = 0.02425` branch on both sides,
2805    /// runs out to `p = 1e-300` where the seed is far from the root, and covers
2806    /// the reflected upper tail where the residual must be formed from
2807    /// `(1 − p) − ½erfc(x/√2)` rather than `Φ(x) − p`.
2808    /// The CENTRAL band, where the residual `F(x) − p` must never be formed
2809    /// against `½`.
2810    ///
2811    /// The sibling table above straddles Acklam's `P_LOW` branch and runs into
2812    /// both tails, but its tightest central point is `p = 0.5000000001`. That
2813    /// is not where the old residual failed. Forming `F(x) − p` as
2814    /// `(1 − p) − ½erfc(x/√2)` (or `F(x) − p` directly) subtracts two numbers
2815    /// of size ~½, so the residual carries a FIXED absolute error of one ulp of
2816    /// ½ however small the true residual is; `Δx ≈ residual_error / φ(x)` then
2817    /// pins the quantile's ABSOLUTE error at ~1.2e-16 and lets its RELATIVE
2818    /// error grow like `1.2e-16 / |x|` without bound as `p → ½`.
2819    ///
2820    /// Measured against a 50-digit `erfinv` reference at the exact `f64`
2821    /// abscissae below, before the centered residual and after:
2822    ///
2823    /// | `p`             | before   | after   |
2824    /// |-----------------|----------|---------|
2825    /// | `½ + 2⁻⁴⁵`      | 1.13e-09 | 2.3e-16 |
2826    /// | `0.5012506…`    | 7.31e-15 | 2.3e-16 |
2827    /// | `0.4987493…`    | 7.33e-15 | 2.3e-16 |
2828    ///
2829    /// The `1.13e-09` is not a coincidence: it is `|A[5] − √(2π)| / √(2π)`,
2830    /// Acklam's own advertised accuracy. As `p → ½` the seed reduces to
2831    /// `A[5]·(p − ½)` and the polish is handed a residual quantized to
2832    /// multiples of one ulp of ½ — usually exactly `0` — so the raw seed is
2833    /// what shipped.
2834    ///
2835    /// The bar is `4·f64::EPSILON` relative: half an ulp for the correctly
2836    /// rounded reference literal, the rest for the evaluator. Worst measured
2837    /// margin over this table is 1.0 ulp.
2838    #[test]
2839    fn normal_quantile_is_ulp_accurate_through_the_median() {
2840        // `[p, Φ⁻¹(p)]`, the second entry correctly rounded from a 50-digit
2841        // `sqrt(2)·erfinv(2p − 1)` evaluated at the EXACT binary `p`.
2842        const CENTRAL_REFERENCE: [[f64; 2]; 19] = [
2843            [0.5000000000000284, 7.124266047159724e-14],
2844            [0.4999999999999716, -7.124266047159724e-14],
2845            [0.5000000009313226, 2.3344794983332983e-09],
2846            [0.4999999990686774, -2.3344794983332983e-09],
2847            [0.5000009536743164, 2.390507006295574e-06],
2848            [0.500000001, 2.5066282037387115e-09],
2849            [0.4999999999, -2.506628482030354e-10],
2850            [0.5001, 0.00025066283008800747],
2851            [0.4999, -0.00025066283008800747],
2852            [0.51, 0.025068908258711057],
2853            [0.49, -0.025068908258711057],
2854            [0.55, 0.12566134685507416],
2855            [0.45, -0.12566134685507402],
2856            [0.6, 0.2533471031357997],
2857            [0.4, -0.2533471031357997],
2858            [0.7, 0.5244005127080407],
2859            [0.3, -0.5244005127080408],
2860            [0.75, 0.6744897501960817],
2861            [0.25, -0.6744897501960817],
2862        ];
2863        let bar = 4.0 * f64::EPSILON;
2864        let mut worst = 0.0_f64;
2865        let mut worst_at = f64::NAN;
2866        for [p, expected] in CENTRAL_REFERENCE {
2867            let got = standard_normal_quantile(p).expect("central p is in (0,1)");
2868            let relative = ((got - expected) / expected).abs();
2869            if relative > worst {
2870                worst = relative;
2871                worst_at = p;
2872            }
2873            assert!(
2874                relative <= bar,
2875                "Phi^-1({p}) = {got}, expected {expected}, relative {relative:e} > {bar:e}"
2876            );
2877        }
2878        println!("central quantile worst relative {worst:e} at p = {worst_at}");
2879    }
2880
2881    #[test]
2882    fn normal_quantiles_match_independent_high_precision_reference() {
2883        const QUANTILE_REFERENCE: [[f64; 2]; 22] = [
2884            [1e-300, -37.0470962993612],
2885            [1e-100, -21.273453560965326],
2886            [1e-20, -9.262340089798407],
2887            [1e-08, -5.612001244174789],
2888            [0.001, -3.0902323061678136],
2889            [0.02424, -1.9731366119445441],
2890            [0.02425, -1.972961051311885],
2891            [0.02426, -1.9727855514678605],
2892            [0.05, -1.6448536269514726],
2893            [0.1, -1.2815515655446004],
2894            [0.25, -0.6744897501960817],
2895            [0.4, -0.2533471031357997],
2896            [0.5, 0.0],
2897            [0.6, 0.2533471031357997],
2898            [0.75, 0.6744897501960817],
2899            [0.9, 1.2815515655446006],
2900            [0.95, 1.6448536269514722],
2901            [0.975, 1.9599639845400538],
2902            [0.99, 2.3263478740408408],
2903            [0.999, 3.090232306167813],
2904            [0.99999999, 5.612001243305505],
2905            [0.9999999999999999, 8.209536151601387],
2906        ];
2907        for [p, want] in QUANTILE_REFERENCE {
2908            let got = standard_normal_quantile(p).expect("p in (0,1) has a quantile");
2909            let error = (got - want).abs();
2910            // `Φ⁻¹(½) = 0` exactly, so it is the one absolute comparison.
2911            let budget = if want == 0.0 {
2912                1e-16
2913            } else {
2914                4e-15 * want.abs()
2915            };
2916            assert!(
2917                error <= budget,
2918                "Φ⁻¹({p}): got {got:.17e}, want {want:.17e} (error {error:.3e} > {budget:.3e})"
2919            );
2920        }
2921
2922        const LOG_CDF_QUANTILE_REFERENCE: [[f64; 2]; 9] = [
2923            [-0.7, -0.008559478582480282],
2924            [-2.0, -1.1015196284987503],
2925            [-10.0, -3.913946240531893],
2926            [-50.0, -9.674825283612357],
2927            [-200.0, -19.803669380301212],
2928            [-1000.0, -44.6157477319694],
2929            [-10000.0, -141.37983987312717],
2930            [-100000.0, -447.1978936785251],
2931            [-1000000.0, -1414.2077829910174],
2932        ];
2933        for [log_p, want] in LOG_CDF_QUANTILE_REFERENCE {
2934            let got =
2935                standard_normal_quantile_from_log_cdf(log_p).expect("finite log_p < 0 has a root");
2936            let error = (got - want).abs();
2937            // Rounding `log_p` itself to `f64` already moves the root by
2938            // `ulp(log_p)·dx/d(log_p)`, and `dx/d(log_p) = Φ/φ = 1/λ` — about
2939            // `1.25` near `p = ½` and `≈ 1/|x|` in the deep tail. That input
2940            // conditioning, not the solver, is what limits `log_p = −0.7`,
2941            // where the root sits at `−0.00856` and one ulp of `0.7` is already
2942            // `1.4e-16` of it.
2943            let conditioning = 8.0 * f64::EPSILON * log_p.abs() / want.abs().max(0.8);
2944            let budget = 4e-15 * want.abs() + conditioning;
2945            assert!(
2946                error <= budget,
2947                "Φ⁻¹(exp({log_p})): got {got:.17e}, want {want:.17e} \
2948                 (error {error:.3e} > {budget:.3e})"
2949            );
2950        }
2951    }
2952
2953    #[test]
2954    fn quantile_antisymmetry() {
2955        let q_lo = standard_normal_quantile(0.1).unwrap();
2956        let q_hi = standard_normal_quantile(0.9).unwrap();
2957        assert!((q_lo + q_hi).abs() < 1e-10, "q_lo={q_lo} q_hi={q_hi}");
2958    }
2959
2960    #[test]
2961    fn quantile_roundtrip_cdf() {
2962        for &p in &[
2963            0.001, 0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99, 0.999,
2964        ] {
2965            let q = standard_normal_quantile(p).unwrap();
2966            let p_back = normal_cdf(q);
2967            // RELATIVE, and sized by what the round trip can cost: a few ulp of
2968            // `q` propagated through `φ(q)`, plus a couple of ulp from `erfc`
2969            // itself. The former absolute `1e-10` bar was two orders looser than
2970            // an unpolished Acklam seed at its worst point.
2971            assert!(
2972                (p_back - p).abs() <= 1e-14 * p,
2973                "roundtrip failed at p={p}: q={q} p_back={p_back}"
2974            );
2975        }
2976    }
2977}
2978
2979/// The SIGNED, multiplicity-carrying form — the generalization the estimated-
2980/// scale references need (gam#2672).
2981/// Standard normal survival probability `P(Z > x)`.
2982///
2983/// This is evaluated as `½·erfc(x/√2)`, not as `1 − Φ(x)`. The latter loses
2984/// relative accuracy as soon as `Φ(x)` approaches one and becomes identically
2985/// zero for every representable `x` above roughly `8.3`, while the direct
2986/// complementary form retains the full representable tail.
2987#[inline]
2988pub fn normal_sf(x: f64) -> f64 {
2989    0.5 * erfc(x / std::f64::consts::SQRT_2)
2990}
2991
2992/// Student-t survival probability `P(T_ν > t)`.
2993///
2994/// The small tail is always obtained from
2995/// [`student_t_two_sided_probability`]. For negative `t`, subtracting its
2996/// half-tail from one constructs the large probability, where subtraction is
2997/// well conditioned.
2998pub fn student_t_sf(t: f64, degrees_of_freedom: f64) -> f64 {
2999    let two_sided = student_t_two_sided_probability(t, degrees_of_freedom);
3000    if t < 0.0 {
3001        1.0 - 0.5 * two_sided
3002    } else {
3003        0.5 * two_sided
3004    }
3005}
3006
3007#[cfg(test)]
3008mod signed_weighted_chi_square_tests {
3009    use super::*;
3010
3011    fn term(weight: f64, degrees_of_freedom: f64) -> WeightedChiSquareTerm {
3012        WeightedChiSquareTerm {
3013            weight,
3014            degrees_of_freedom,
3015        }
3016    }
3017
3018    /// THE identity the signed form exists for, against a closed form computed
3019    /// a completely different way (the regularized incomplete beta):
3020    ///
3021    /// ```text
3022    /// P(F_{a,b} > f) = P( (χ²_a/a) / (χ²_b/b) > f ) = P( χ²_a − (f·a/b)·χ²_b > 0 ).
3023    /// ```
3024    ///
3025    /// A ratio's tail IS a signed combination evaluated at zero. Fractional `a`
3026    /// is included because a two-moment summary of a smooth's null spectrum is a
3027    /// chi-square with a non-integral shape, which is exactly what this form is
3028    /// asked for.
3029    #[test]
3030    fn the_f_tail_is_the_two_term_signed_combination_at_zero() {
3031        let mut worst = 0.0_f64;
3032        for &(a, b) in &[
3033            (1.0_f64, 5.0_f64),
3034            (2.0, 17.0),
3035            (3.0, 26.0),
3036            (0.7, 24.0),
3037            (5.4, 191.0),
3038            (11.0, 4.0),
3039        ] {
3040            for &f in &[0.05_f64, 0.5, 1.0, 2.5, 9.0, 40.0] {
3041                let terms = [term(1.0, a), term(-f * a / b, b)];
3042                let (got, bound) = signed_weighted_chi_square_sf_to_tolerance(
3043                    &terms,
3044                    0.0,
3045                    WEIGHTED_CHI_SQUARE_TOLERANCE,
3046                );
3047                let want = fisher_snedecor_sf(f, a, b);
3048                let error = (got - want).abs();
3049                worst = worst.max(error);
3050                assert!(
3051                    error <= 1e-9 + bound,
3052                    "F({a},{b}) at {f}: imhof {got} vs beta {want} \
3053                     (error {error:.3e}, certified bound {bound:.3e})"
3054                );
3055            }
3056        }
3057        println!("worst |imhof − F| over the grid: {worst:.3e}");
3058    }
3059
3060    /// The certified bound at `statistic = 0` — where the oscillatory bound does
3061    /// not exist and the amplitude bound is the whole contract. Checked against
3062    /// a reference computed at a far stricter request, so the assertion is that
3063    /// the RETURNED bound actually bounds the error.
3064    #[test]
3065    fn the_amplitude_bound_certifies_the_zero_statistic_answer() {
3066        let cases: [&[WeightedChiSquareTerm]; 3] = [
3067            &[term(1.0, 1.0), term(-0.05, 26.0)],
3068            &[term(0.9, 1.0), term(0.2, 3.0), term(-0.01, 191.0)],
3069            &[term(1.0, 5.4), term(-2.5, 1.0), term(-0.004, 44.0)],
3070        ];
3071        for terms in cases {
3072            let (reference, reference_bound) =
3073                signed_weighted_chi_square_sf_to_tolerance(terms, 0.0, 1e-14);
3074            for tolerance in [1e-4_f64, 1e-7, 1e-10] {
3075                let (got, bound) =
3076                    signed_weighted_chi_square_sf_to_tolerance(terms, 0.0, tolerance);
3077                assert!(
3078                    bound <= tolerance,
3079                    "asked {tolerance:.0e}, certified {bound:.3e} on {terms:?}"
3080                );
3081                assert!(
3082                    (got - reference).abs() <= bound + reference_bound,
3083                    "{got} vs {reference} exceeds the certified {bound:.3e} + \
3084                     {reference_bound:.3e} on {terms:?}"
3085                );
3086            }
3087        }
3088    }
3089
3090    /// The panel rule has to resolve the integrand's AMPLITUDE, not only its
3091    /// phase, and this is the arm that measures whether it does.
3092    ///
3093    /// The reference is the same quadrature at a panel forced far below either
3094    /// rule (by asking for an accuracy the sizing then honours), so the
3095    /// comparison isolates the discretization from the truncation. The shapes
3096    /// are the ones where the two scales come apart: a small phase rate
3097    /// (`statistic = 0`, weights that nearly cancel) against an amplitude that
3098    /// turns over at `u ≈ 1`.
3099    ///
3100    /// Pre-fix, `F_{1,5}` at `f = 0.05` missed by `3.4e-7` while certifying
3101    /// `1e-11`.
3102    #[test]
3103    fn the_quadrature_resolves_the_amplitude_not_only_the_phase() {
3104        let cases: [&[WeightedChiSquareTerm]; 5] = [
3105            &[term(1.0, 1.0), term(-0.01, 5.0)],
3106            &[term(1.0, 1.0), term(-0.2, 2.0)],
3107            &[term(1.0, 3.0), term(-1.0, 3.0)],
3108            &[term(0.9, 1.0), term(0.2, 4.0), term(-0.05, 26.0)],
3109            &[term(1.0, 0.7), term(-0.006, 24.0)],
3110        ];
3111        let mut worst = 0.0_f64;
3112        for terms in cases {
3113            for &statistic in &[0.0_f64, 0.3, -0.2] {
3114                let (reference, reference_bound) =
3115                    signed_weighted_chi_square_sf_to_tolerance(terms, statistic, 1e-15);
3116                let (got, bound) = signed_weighted_chi_square_sf_to_tolerance(
3117                    terms,
3118                    statistic,
3119                    WEIGHTED_CHI_SQUARE_TOLERANCE,
3120                );
3121                let error = (got - reference).abs();
3122                worst = worst.max(error);
3123                assert!(
3124                    error <= bound + reference_bound,
3125                    "{terms:?} at {statistic}: {got} vs {reference} differs by {error:.3e}, \
3126                     above the certified {bound:.3e} + {reference_bound:.3e}"
3127                );
3128            }
3129        }
3130        println!("worst discretization error against the fine-panel reference: {worst:.3e}");
3131    }
3132
3133}