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